Ultimate

CLI reference

The binary is x. One command registry — a command that is not in it does not exist, and there is no second place to register one.

x help                 # the catalogue
x help <command>       # usage for one command; a name no command has is X_CLI_UNKNOWN_COMMAND
x <command> --help     # the same thing, except on db and mcp — see Subcommands below
x version              # CLI version
ConventionDetail
--jsonevery command accepts it and prints a single machine-readable object on stdout. Human output goes to stdout too, but never mixed with JSON
Exit codes0 success · 1 the command failed (a typed X_* error is printed) · 2 usage error (X_CLI_BAD_FLAG, X_CLI_UNKNOWN_COMMAND)
Errorsalways code + cause + fix. See Error codes
App detectionmost commands walk up for app.config.ts and fail with X_NOT_IN_APP if there is none. The exceptions: new, test, doctor, errors, docs, affected, ci, pr, help, version
Flagslong form only, --flag value or --flag=value. Booleans negate as --no-<flag>
Global flags--json / -j, --help / -h, --cwd <dir>, --verbose — accepted by every command. --verbose carries each step's captured output on a green run too, in the terminal and under --json alike (steps[].output); without it a step's output appears only when it fails. As of 2026-10
Subcommandswhen a command has them, its default is declared, never positional: x actions is x actions list because that command sets defaultSubcommand: 'list'. A command with no defensible default declares none and refuses the bare form with X_CLI_BAD_FLAG — exactly db and mcp, pinned by parse.test.ts. --help is read before the subcommand, so x db --help and x mcp --help print usage like x help db. The parser answered subcommands[0] until 1.2.0, which is why x db used to run the migration generator
Passthrougha bare -- sends everything after it to the underlying tool untouched
Telemetrynone written. The gate proves one thing: every host a string literal in a non-test packages/cli/src file spells is reserved or a reasoned row in scripts/cli-outbound.ts, else X_CLI_OUTBOUND_UNDECLARED. A host built at run time, and every other package, is outside it (FAQ)
--json shape{ ok, command, summary, steps?, findings?, data? }. Findings are { code, cause, fix, docs?, at?, meta? }

Command index

As of 2026-10. shipped = implemented in packages/cli; planned = specified, not yet built — calling it exits with X_NOT_IMPLEMENTED and a fix: line pointing at the closest shipped command.

CommandDoesStatus
x new <name>scaffold a monorepo that already runsshipped
x devall roles in one process: embedded services, sub-second reload, /_x mountedshipped
x stop [--timeout 30s]stop this checkout's x dev (the pid its .x/dev.lock names, nothing else) and return once the next x dev can bootshipped
x g <kind> <name>scaffold a primitive with its testshipped
x db <sub>gen, migrate, reset, seed, studio, branch, backfill, diagnosticsshipped
x verify [--only <step>[,<step>…] [--shard i/n [--timings f]] | --changed [--base <ref>] [--dirty]] [--workers N] [--isolate] · x verify merge <part.json…>the gate — 21 steps, in this order: typecheck, lint, boundaries, filesize, package-shape, errors, unit, contract, live, job, exercise, e2e, eval, drift, contract-diff, budgets, seo, i18n, policy, manifest, roadmap. --only <step>[,<step>…] runs the named steps, in one process and in gate order, for an iteration loop and announces NOT A GATE RUN in the summary and in --json (data.notAGateRun, data.only — always a list), writing no floor file; an unknown name is refused with every valid name and the nearest match. The gate is this command with no flag. There is no --skipshipped
x env [check|example|init]validate the process env against envSchema, regenerate .env.example from it, or write .env.production (init --production)shipped
x secrets <sub>the committed encrypted secrets file: show, init, edit, set, rotateshipped
x auth seal-mfaone-shot: seal every second-factor secret still stored in the clearshipped
x vapid [show|create]the Web Push key pair: which one this app signs with, or mint one and seal both halvesshipped
x buildcontainer image, single binary, prerendered static site, or a prebuilt bundle (--target docker|binary|static|prebuilt)shipped
x deployrun the container deploy plan: migrate first, then the serving rolesshipped
x manifestregenerate x.manifest.json and openapi.jsonshipped
x routesevery served route: pages, then actions, queries and runtime.ts routesshipped
x mcp serve · x mcp toolsserve the framework MCP tools over stdio or HTTP · list themshipped
x mcp lock <name> [--check]fetch the remote MCP server mcpTools({ name }) names and write its allowed tools to tools.lock.json · --check writes nothing and fails on drift (X_MCP_REMOTE_TOOL_CHANGED)shipped
x doctorenvironment, versions, drift, the newest migration's snapshot sidecar, ports, PWA prerequisites — each with a fixshipped
x help / x versioncatalogue and versionshipped
x actions / x queries / x entitiesintrospect the declaration registriesshipped
x jobslist, show, retry, cancel, remove and promote jobs; pause and resume a queue · drain is plannedshipped
x taskscron tasks, their timezone and their next runshipped
x policywhich clause decided a permission, and whyshipped
x exercise [<name>] [--seed n]run every action, query and job once on generated rows, with no test written; the gate's exercise stepshipped
x drill [<name>] [--list] [--budget-ms n]the app's web and worker roles as real processes on real Postgres, Redis and NATS, each dependency faulted through a proxy or each role signalled, red on any framework contract that does not hold. Not a gate step: it needs the servicesshipped
x i18nadd, sync, check catalogsshipped
x mail preview <id>render a mail's declared preview in every locale, nothing sentshipped
x test <type>run one of the six test types, or the whole suiteshipped
x clean [--dry-run]remove what test runs leave behind: migrated test templates, .x/cache, stray .x folders, stale probe databasesshipped
x affectedthe workspaces a diff can have broken, transitivelyshipped
x graph [--from <a> --to <b>] · x impact <file|primitive|entity>the app as one typed graph, each edge declared, import or observed · what an edit reachesshipped
x client vendor <name> · x client customer --out <dir>one OpenAPI compiler, two clients: a third party's document into apps/web/clients/<name>/ (outbound() + t schemas), or the app's own published document into a client with no framework importshipped
x shot <route>photograph one route from a real browser, plus a verdict.json a picture cannot carryshipped
x pr review|resolve|replyinline review threads — the ones gh pr view --comments does not show — plus every review verdict, every PR comment, and which review bots did not reviewshipped
x cithe workflow runs for this branch, and the findings inside the failed step's log; x ci tested: whether a push reuses a green PR runshipped
x errors explain <CODE>code → cause, fix, docsshipped
x docs "<question>"framework docs and this wiki, offline, from the install — the first lookupshipped
x fix boundary <file>plan the minimal cut for an import that crossed a surface boundary (never rewrites)shipped
x upgradeone major per run: exact pins, the Bun floor, bun install, the gate, the section to readshipped
x cachetag graph, bust, clear, statsplanned
x branchcopy-on-write branch environmentsplanned
x statusconnected-client build-ID distribution, role healthplanned
x logs tailstructured logs + OTel spansplanned
x tokencreate and grant MCP scopesplanned
x aieval, cache stats, reindexplanned
x money add-currencyextend the currency tableplanned
x config showthe resolved app.config.tsplanned

x new

x new <name> [--dir path] [--shape api|blog|dashboard] [--no-example] [--no-git] [--dry-run] [--force] [--json]
FlagTypeDefaultMeaning
--dirstringcwdparent directory to create the app in
--example / --no-examplebooleantrueinclude the example feature slice (default: on; --no-example for an empty app/)
--shapestringnonestart from an app shape — api, blog or dashboard, each an overlay over this scaffold (Shapes). An unknown name is X_NEW_SHAPE_UNKNOWN; with --no-example, X_NEW_SHAPE_CONFLICT (a shape is its own example). Both refuse before a file is written. dashboard then runs x g sign-in --password in the new app (listed under data.generate by --dry-run); a refusal there is X_NEW_SHAPE_GENERATE_FAILED
--git / --no-gitbooleantruegit init, then one commit named x new. On by default because three surfaces assume a repository — x affected, x ci and x pr. A git that is absent or fails never fails x new: the tree is the verdict, and data.git carries { initialized, committed, problem }
--dry-runbooleanfalseprint the file list, write nothing
--forcebooleanfalsewrite into a directory that already exists

The name must be one app.config.ts accepts. x new asks core's own defineConfig about the slug before anything is planned (appNameIssue, packages/cli/src/app-name.ts) — it is written verbatim as name:. Refused with X_CLI_BAD_FLAG and a corrected name in the fix:, the rest of your flags kept:

You typeWhy it is refusedfix:
x new 9livesstarts with a digit — core's pattern is ^[a-z][a-z0-9-]{1,63}$x new app-9lives
x new aone characterx new a-app
a slug over 64 characterspast the pattern's boundthe slug cut at 64, trailing - dropped
$ x new myapp --dry-run --json
{"ok":true,"command":"new","summary":"…","data":{"dir":"/home/me/myapp","files":["README.md","AGENTS.md",…],"dryRun":true}}

This page states no file count, deliberately — one template added moves it, and a number hand-copied onto four pages goes stale on three of them. scripts/generator-counts.ts is the single source: it re-derives both shapes from the CLI's own generators and, as a step of x verify's manifest check, turns any page quoting a stale one red on the same commit. Two moves worth knowing about rather than counting: the scaffold gained apps/web/app/auth/dev-actor.ts and its test, because hooks.authenticate is the only place an actor can come from and a generated app configured none — so every route declaring a policy: refused every request and the boot warned X_CONFIG_INVALID on every start; and it gained .github/workflows/ci.yml, which runs bun run setup && bun run verify on every push and pull request. Derive the number:

x new myapp --dry-run --json | jq '.data.files | length'

--dry-run --json lists every file, which is also how you check what a build target expects to find.

Deployment artifacts are part of the scaffold — an app is deployable the moment it is generated:

PathWhy
apps/web/server.tsthe production entry: runRole({ root, env: Bun.env }). Also --target binary's entry
apps/web/prerender.ts--target static's entry
docker/Dockerfile--target docker's entry. Defaults ROLE=web, PORT=3000, health-checks /readyz
docker/Dockerfile.dockerignorethis exact name — not a root .dockerignore. Drops *.test.ts, *.test.tsx and the root docs/; an app serving a file from docs/ re-includes it with !docs/<path>
docker/docker-compose.prod.ymlone service per role: migrate, web, sync, worker, scheduler
docker/docker-compose.dev.ymlparity checks only; x dev needs none of it
docker/helm/the chart, 12 files — Chart.yaml, values.yaml and 10 templates (_helpers.tpl, _volumes.tpl, deployments, service, migrate-job, ingress, hpa, networkpolicy, servicemonitor, alert-rules). Written As of 2026-08-19, which is what makes x deploy --method helm work in a scaffold; the last two render only where the cluster serves the Prometheus operator's kinds (auto)
docker/README.mdhow the two compose files differ
.github/workflows/ci.ymlthe generated app's own CI: bun run setup then bun run verify, on push and pull request — the same two commands its README.md opens with
bin/setup.tsTypeScript Bun runs, never shell — package.json's setup script (bun bin/setup.ts), so bun run setup is the same command in PowerShell, cmd and bash; bun run dev is x dev and bun run verify is x verify, and neither needs a file. There is no bin/check.ts As of 30.0: x verify's budgets step builds when the stats are missing or stale, so a wrapper that built first would be a second name for the gate

The framework repo's own docker/helm carries one template the scaffold does not — pdb.yaml, cluster policy rather than app topology. It ships in no npm tarball, so copying it is a git clone of this repo, not an install. An app chart from before the monitoring pair gets it from x g chart-alerts.

bunx create-ultimate myapp is the same generator without a global install — and it says so in its own refusals, As of 2026-08-23: a fix: line is a command the reader is meant to RUN, and x new myapp named a binary they have not installed yet. Errors: X_GENERATE_CONFLICT (the directory exists — fix is the same command with --force), X_CLI_BAD_FLAG (no name given, or a path where a name goes), X_BUN_VERSION.

x dev

The ready line names the live feed, As of 2026-09-05: live=in-process under the embedded database (repository writes in this process reach subscribers — the bridge PGlite's missing walsender needed), live=replication with a real DATABASE_URL (the WAL decoder, --role replicator here or elsewhere), live=none without the sync role. --json carries it as liveFeed. The app's MCP endpoint, when apps/<app>/mcp.ts exports one, prints as mcp POST /mcp in the summary and mcp in --json; an app exporting several prints one line per endpoint, and --json lists them all in mcpPaths, endpoint #0 first.

x dev [--port 3000] [--role web,worker] [--once] [--json]
FlagTypeDefaultMeaning
--portstring3000HTTP port. The sync role listens on --port + 1
--rolestringweb,sync,worker,schedulercomma-separated roles to run in this process
--oncebooleanfalseboot, report, exit — for smoke tests and CI

Boots the app: embedded Postgres (PGlite under .x/pgdata), the in-process event bus, a local directory for S3, then every module under apps/*/{site,app,api,shared} and packages/*/src — importing them IS the registration. What those modules registered is then served:

RegisteredServed as
action / mutatorPOST /api/<resource>/<verb>, policy enforced by the pipeline
routeits URL, in its declared render mode, with that mode's cache headers
jobclaimed off the real Postgres queue by the worker role
taskdispatched by the scheduler role
—/_x, the dev dashboard from @ultimat3/admin

A module that will not import becomes a finding on the result rather than a dead process, so the dev loop stays reachable while something is broken.

What it watches is decided before a descriptor is taken, and the ignore set is your own .gitignore, As of 2026-09-06. x dev walks the app root and opens one non-recursive watcher per directory it admits, skipping an ignored one at descent rather than filtering its events afterwards — a recursive watch on the root took an inotify descriptor per directory in the tree, including .git/, node_modules/ and the .x/ this same process writes to continuously (measured: 110 descriptors on the reference app, 39 of them noise; 1901 on a monorepo root, 1490 of them .git/ and node_modules/, against a per-user budget of 8192 on many distributions). The rules are git's own — an unanchored coverage/ matches at any depth, a leading / or an inner slash pins the pattern where it is written, ! re-includes, a trailing / matches a directory and never a file — and ignore files in ancestor directories up to the repository root count, so an app inside a monorepo inherits it. Editing .gitignore re-reads it and re-walks the tree in place; no restart, and nothing ever runs git check-ignore. Five directory names are ignored whatever your file says, because it cannot be relied on to name them: .git, .x, node_modules, .personal, .claude. dist and coverage are not among them — root-anchor them in your .gitignore (x new now scaffolds /dist/ and /coverage/) or an apps/web/site/coverage/page.tsx route is a directory git will not commit and the dev loop will not reload. A save that touches five files is one reload, and a save arriving while a reload is still building is one more reload after it, never a second one racing it.

What a reload changes, As of 2026-09-27. Every file the process loaded is hashed on each reload; a changed one — a component, a helper, a .module.scss, a Sass partial a sheet @uses, a JSON catalog — leaves Bun's module registry together with every module that imports it, up to the route modules, and the scan imports that chain again. A route module that comes back as a new instance has its entry replaced, so the next request serves what is on disk; its islands are re-bundled on the same tick, so the two are one generation; the isr store is emptied, so a page's first render does not answer for its whole ttl. A save that will not import is a finding on /_x and the last page that did import stays up. The replaced instances are collected — nothing is kept per save (app-reload-graph.ts).

A module that defines a primitive — it exports an entity, an action, a query, a mutator, a job or a task, or it is apps/web/api/index.ts — is pinned: never re-evaluated, and the invalidation stops at it: its exports are held by every importer and by a registry that refuses a second definition of one name. A save that reaches one restarts x dev, As of 22.12 — the pinned module itself, or anything it imports: a slice's service.ts under its query, an admin page's view under defineAdmin. x dev is a supervisor and a child: the child prints restarting: <saved file> under <pinned module> — … on stderr, retires its worker first — it stops claiming and waits for every running job to finish, aborting none (As of 25.0.0; before, the drain cut off a job running past drain.deadlineMs with X_DRAINING and the next child ran it again), printing restart waits for <n> running job(s) to finish — … while it waits, Ctrl-C cutting the wait short at the drain deadline — then drains, releases the port, the lock and the embedded Postgres, and exits 75; the supervisor boots a fresh child on the same port (--port 0 is pinned to one free port before the first boot). Every other save stays the in-process reload above, and Ctrl-C stops both. --once and an embedded startDev() are unsupervised: there the save is X_DEV_RESTART_REQUIRED on /_x (or startDev({ onRestart }) is called). With --json, each child prints its own ready document. Until 22.12 such a save logged reloaded and served the old code until a manual restart. The route's guard (policy) is read by the HTTP pipeline at boot, so adding one is a restart too, and so is an edited site asset under an unchanged stylesheet that names it with Sass asset(). Until 2026-09-27 only the edited route module itself was re-imported (?x-reload=<hash>), and every component it imported stayed the cached one: x dev logged reloaded and rendered the old component until a restart.

Without --once the process stays up until it is signalled. Ctrl-C runs the same three-phase drain a production SIGTERM runs — stop accepting, finish in-flight, close — and only then releases the embedded Postgres, the worker and the file watcher, so .x/pgdata is never left locked by a process that has gone. x mcp serve --transport http behaves identically.

/_x/<panel> is one tab per panel; ?json=1 (or accept: application/json) returns exactly what the tab draws. Eleven panels — the nine @ultimat3/admin ships plus the two only the CLI can answer:

PanelKills the question
routeswhich URL renders how, with which budget
timelinewhere did this request spend its milliseconds, and did it loop
livewhy did this subscriber not get the row
jobswhich step failed, and what is queued
dbwhat is in the table, and does the schema match the migrations (read-only SQL)
mailwhat did that email look like, in that locale
cachewhich tags would this invalidation bust
policywhich clause decided, for which actor
manifestis the committed x.manifest.json current
serviceswhich database/events/storage this process is talking to, and its reload count
boundarieswhich import crosses a surface or a layer

/_x answers a loopback Host only — localhost, *.localhost, 127.0.0.1, [::1] — and refuses any other with 421 X_DEV_HOST_REFUSED (a DNS-rebound name is how a page reads a loopback server). The db panel runs a statement on a same-origin POST only, inside a BEGIN READ ONLY transaction that always rolls back: curl -sS -H 'accept: application/json' --data-urlencode 'sql=select 1' http://localhost:3000/_x/db. A GET /_x/db?sql=… is X_METHOD_NOT_ALLOWED; a cross-site POST is X_CSRF_BLOCKED.

Every one of the eleven answers in a x dev process. Three of them read facts only this process holds, so nothing else can serve them: timeline is core's own spans, recorded by the exporter x dev installs at boot (tracing is always on and free until one is configured); cache is the report invalidateTags() already built, kept by @ultimat3/cache; policy is @ultimat3/policy's own policyMatrix() run over every capability an action or query gates, against one actor per role the app declared with defineRoles plus the anonymous caller — never a second reading of the actor. The matrix is evaluated with no row, and each cell's trace says so: a row-level rule decides again on the real request.

timeline carries two SQL facts and they are not the same fact. repeatedSql is a measurement over the trace: every statement text that appeared twice. nPlusOne is the verdict — one entry per statement shape this request repeated past five, each carrying X_N_PLUS_ONE_QUERY or X_N_PLUS_ONE_WRITE with the runnable fix:, counted with attribution applied and expectedQueryLoop honoured. Only x dev installs the ledger behind it; a host that did not answers nPlusOne: null, which is "nobody counted" and not "this request was clean". The same verdicts reach three more places, never re-counted: x dev's own findings (text and --json), the browser error overlay for the request that looped, and one logger.warn per request per code.

live lists the registered live queries and notes that no subscriber list is attached — @ultimat3/realtime does not retain a subscriber's matcher trace, which is the rest of that panel's question. A panel whose source a host has not wired answers ok: false with the exact wiring line rather than an empty tab, and a panel that can degrade says which half is missing rather than rendering an empty one as an answer.

EnvUnset meansSet means
DATABASE_URLPGlite in this processthat Postgres
NATS_URLin-process fanoutthat NATS server — set, it is the bus for every role; a bus of the app's own is transport in export const runtime = { … } in apps/<app>/runtime.ts instead
NATS_KV_BUCKETthe KV bucket x_presencethat bucket — one per app on a shared cluster
S3_ENDPOINT.x/storage on diskthat S3

realtime.enabled: false in app.config.ts drops the sync and replicator roles from the set x dev starts, and a container's ROLE=sync answers the same way — no node, so no live feed.

migrate is a real role but not a dev role: it runs once, as x db migrate; naming it under --role is X_CLI_BAD_FLAG. replicator does run under x dev --role replicator, but stays out of the default set — x dev with no --role still runs web,sync,worker,scheduler, because the replicator takes a slot on a shared database. With DATABASE_URL unset the embedded PGlite still serves no logical replication, so the role is refused, but X_CLI_BAD_FLAG now names the fix: set DATABASE_URL to a Postgres with wal_level=logical. With DATABASE_URL set, the role starts for real: advisory lock → postgresChangeFeed() → changeFeedReplicator → publish to the transport. Errors: X_CLI_BAD_FLAG, X_PORT_IN_USE, X_ENV_MISSING, X_DB_DRIFT.

x stop

x stop [--timeout 30s] [--json]

Ends this checkout's x dev and returns only once the next x dev there can boot. As of 30.0.

CaseWhat happens--json data
a live x dev holds .x/dev.lockSIGTERM to the pid the lock names, then wait for the pid to exit and the lock to clear{ stopped: true, pid, port, waitedMs, drained: true }
Windowskill is TerminateProcess, so no drain runs; x stop clears the lock itself{ …, drained: false }
no lock, or a lock naming a dead pidnothing is signalled, exit 0. A stale lock is left for the next x dev's preflight to clear{ stopped: false }
the pid outlives --timeoutX_DEV_STOP_TIMEOUT, naming the pid; the lock stays—
FlagTypeDefaultDoes
--timeoutduration (30s, 2m, PT1M)core's default drain deadline (25 s) + the 5 s release floor + 5 show long to wait for the drain. An app that configured a longer drain passes it here: x stop loads no app

The lock is the only authority. No --all, no port lookup, no process-name match: on a box shared by several agents, pkill ends someone else's server and x stop never does. Dev services an app runs beside x dev (its own compose file) are the app's to stop. X_DEV_ALREADY_RUNNING's fix: is this command.

Errors: X_DEV_STOP_TIMEOUT, X_CLI_BAD_FLAG (a --timeout that is not a positive duration).

x g

x g resource|action|mutator|backfill|job|route|policy|entity|query|task <name> [--feature f]
x g island <name> [--at <dir>]              # a client component on an otherwise static page
x g admin:page <name> --permission <perm> [--at <dir>]   # a custom admin screen, guarded by construction
x g guard <name>                            # a convention this app enforces, as a build error
x g sign-in --password | --oidc <provider>  # the app's one sign-in: pages, actions, boot guard
x g chart-alerts [--print]                  # the chart's ServiceMonitor + alert rules, backfilled
x g app <name>                              # one more app workspace: apps/<name>/
x g package <name>                          # one more shared package: packages/<name>/

x g app <name> / x g package <name> (As of 2026-11) add a workspace to the monorepo: apps/<name>/ (package.json + tsconfig.json) or packages/<name>/ (manifest, README, src/index.ts and its test), named @<scope>/<name> with the scope read off the root package.json (@acme/root → acme). Nothing outside the new directory is edited: the root workspaces globs and tsconfig alias already reach it. A name the other kind already holds is X_WORKSPACE_NAME_TAKEN (both would install as @<scope>/<name>); one the same kind holds is X_GENERATE_CONFLICT. The run prints bun install then bunx x verify --changed --dirty (data.next). A workspace is no slice: it reads --force and --dry-run only, and any other x g flag (--feature, --surface, --live, --admin, --locales, --at, --app, --permission) is X_CLI_BAD_FLAG, fix the same run without it.

x g sign-in (As of 2026-11) is what a deployed scaffold needs to issue a session: before it, a production web role answered every policy-protected route 401 (packages/cli/e2e/sign-in.e2e.test.ts proves the after). Exactly one of the two flags, and no <name> — an app has one sign-in; anything else is X_CLI_BAD_FLAG, and so is any other x g flag it would ignore (--feature, --surface, --at, --permission, --live, --admin), and either flag on another generator. Its keys land in every catalog the app ships, or the ones --locales names, as every other generator's do. The auth slice lands under apps/web/app/auth/ beside x new's auth.ts, plus apps/web/runtime.ts and the edits in the last column. A dry run lists the deletions in data.removed, apart from data.files; a real run reports them under the same key:

ShapeWritesAlso edits
--passwordsign-in/page.tsx (a native form, 0 kB) posting to createSession over login(); sign-in/second-factor/page.tsx posting to confirmSession over completeMfa() (the sealed challenge travels in an HttpOnly cookie); destroySession over logout() + signOutHeaders(); sign-in-guard.ts; apps/web/runtime.ts — 22 files on a dry run in the default scaffold, 24 after setupapps/web/api/index.ts (the three actions), app.config.ts (auth: { signInPath: '/auth/sign-in' }), the catalogs
--oidc <provider>oidc.ts — discoverOAuthProvider on the FIRST sign-in, never at import — and sign-in-routes.ts, the oauthLogin legs on runtime.routes; the sign-in page (one link to /auth/sso/<provider>); destroySession; the guard — 20 files on a dry run (plus the two in data.removed), 22 after setupas above, plus <PROVIDER>_ISSUER, _CLIENT_ID, _CLIENT_SECRET in envSchema; and the authenticator swap: authenticator.ts loses the development fallback and dev-actor.ts is deleted, only while all four files are byte-for-byte what x new wrote — an edited one is an X_GENERATE_CONFLICT finding naming the line to delete by hand

The boot guard is generated, app-owned: apps/web/runtime.ts calls assertSignInConfigured(process.env) before the boot binds a port, and outside development/test a web or sync role with a sign-in key unset refuses to start — X_AUTH_SIGN_IN_UNCONFIGURED, fix export <KEY>="<value>" in the role's deployed environment, then restart it (x env example lists the keys). The keys: ULTIMATE_SECRETS_KEY for --password (it seals the second-factor challenge); the provider's three plus SESSION_SECRET and APP_URL for --oidc. A second x g sign-in is X_GENERATE_CONFLICT and writes nothing. --password keeps the development viewer: it ships no sign-up page, so the first user is register(appAuth(), { email, password, orgId, roles }). An app that already has an apps/web/runtime.ts is that conflict too — merge the two runtime objects by hand.

x g chart-alerts (As of 2026-11) writes the two monitoring templates x new's chart carries into an app chart that predates them — docker/helm/templates/servicemonitor.yaml and alert-rules.yaml, named after Chart.yaml's name — and appends the serviceMonitor: / alerts: blocks values.yaml lacks. Only what is missing: a template already there is kept (--force rewrites both), and a values key already there — enabled: false included — is never touched; both templates read a missing block as auto, rendering only where the cluster serves the Prometheus operator's kinds. A second run writes nothing. No <name>, and any other x g flag but --force/--dry-run is X_CLI_BAD_FLAG. --print writes nothing and needs no chart: the same rules as plain Prometheus rule groups, defaults in place, for a loader outside Kubernetes (--json: data.rules, the YAML text). No docker/helm/Chart.yaml is X_CHART_MISSING, fix x g chart-alerts --print --json. x doctor lists the missing templates. The rules: Observability.

x g guard <name> writes guards/<name>.ts and its test. A name the framework ships a guard under — raw-length, raw-breakpoint, raw-z-index, raw-shadow, raw-motion, undefined-style-class, undeclared-custom-property, repo-raw-sql and every other name in SHIPPED_GUARD_NAMES (packages/cli/src/templates/scaffold-guards.ts) — writes THAT guard, which is how an app adopts one it does not hold; any other name writes the blank template. x doctor lists the shipped ones missing, and the held ones that differ from the current template (Interface rules).

Alias: x generate.

<name> is one directory segment for every generator but route, whose name is its URL: x g route casos/[id]/notificar writes apps/web/app/casos/[id]/notificar/page.tsx. Each segment is a word (a-z, 0-9, -, _) or [param] / [...rest]; .., . and empty segments are X_CLI_BAD_FLAG.

FlagTypeDefaultMeaning
--featurestringderived from the namefeature slice to write into. Not for resource: its name IS the slice (app/<name>/, page at app/<names>/), and a --feature naming anything else is X_CLI_BAD_FLAG
--surfacestringappsite or app
--appstringwebroute, island, query only: the app workspace under apps/ the files land in (x g route reports --app admin → apps/admin/app/reports/page.tsx). An app that is not there is X_CLI_BAD_FLAG, fix x g app <app>; any other generator registers in files only apps/web holds (API index, roles, exercise file, db handle) and refuses it, fix the same run without the flag
--permissionstring<name>:readadmin:page only: the permission the page's own work needs, on top of the frame's admin:read. A <resource>:<verb>, or namespaced with more segments (admin:blog:write, As of 27.6.2 — one colon exactly was required, which refused every admin:<area>:<verb> a staff console declares); an empty segment, a space or a quote is X_CLI_BAD_FLAG before a file is written — the value reaches the emitted source three times and Permission is ${string}:${string}
--livebooleanfalsequery only: make it subscribable. On any other generator it is X_CLI_BAD_FLAG naming the x g query … --live to run instead
--adminbooleanfalseresource only: also emit the per-entity admin override and list it under resources: in apps/admin/app/admin/admin.ts. The screen itself needs no flag — an entity in the handle is an admin screen
--localesstringevery locale with a catalog in packages/i18n/catalogs/, else encomma-separated locales; lands each generator's catalog entry in every one. Never starts a catalog in an app that has any: a non-default locale with no packages/i18n/catalogs/<locale>.json is X_CLI_BAD_FLAG, fix: x i18n add <locale>, before a file is planned — a catalog a generator started would hold only its own keys. A non-default locale's new keys land marked ⟦…⟧, as x i18n add seeds them, so x i18n check reports them until translated. In an app with no catalogs yet, a first --locales en,es writes en bare and es marked. As of 2026-10-06 (localiseCatalogs, packages/cli/src/generate-catalog-locales.ts)
--passwordbooleanfalsesign-in only: an address and a password over login()
--oidcstring—sign-in only: an OpenID Connect provider id (corp-sso; lowercase, digits, hyphens, not a built-in), discovered from <PROVIDER>_ISSUER
--printbooleanfalsechart-alerts only: print the alert rules as plain Prometheus rule groups, write nothing. On any other generator it is X_CLI_BAD_FLAG
--forcebooleanfalseoverwrite existing files
--dry-runbooleanfalsethe write plan, nothing written: every file the run would touch — the ones it creates (a slice module already on disk is skipped, not listed) and, As of 27.6.2, the ones it EDITS outside the slice (role map, typed handle, API index, manifests, catalogs, the i18n index), in the order the run lists them. --json: data.files is that list, data.edits the subset that already exists, data.refreshes the committed contracts (x.manifest.json, openapi.json) a real run re-projects. ok: false with every finding the real run would report — an existing file, an API-index binding already held, a grant or a handle entry it cannot place

resource emits the whole slice — entity, repo, policy, errors, service, actions, live, jobs, ui, the form island, the plural route and a test beside each declaration — 38 files on a dry run in an app scaffolded with its example slice, 40 on the real run after bun run setup (x.manifest.json and openapi.json re-projected; 41 dry with --admin) — and x g resource in a --no-example app, which also gains two package.json edits, writes 40 and 42 files (43 and 45 with --admin; --live is refused — a resource already ships a live query, and the flag belongs to x g query: X_CLI_BAD_FLAG, fix x g query <name>-feed --feature <name> --live), and no migration: x db gen is the only writer of packages/db/migrations, so a new slice is x g resource <name>, then x db gen "create <table>", then x db migrate, then x build --target static && x verify --only budgets — the four commands the run prints, in that order (data.next under --json). The plural route is reachable as written, As of 2026-10-01: its load reads the list through the slice's query as the request's actor (no org in the URL), AsyncRegion renders ready, empty and refused, the form island is mounted, and the route declares policy: { permission: '<name>:read' }. Its budget.js is MEASURED on x new's app (58,522 B, Bun 1.4.0) and carries the measured: / why: comment bun run budget-raises reads; an app that charges more to every document — navigation: { client: [...] } adds its router — is over it on the printed build, and the raise is that edit. The page sits in apps/web/shared/shell.tsx when the app has one. backfill emits a backfill() declaration with its source() and handle() to fill in — see Migrations and backfills. x g query and x g backfill read a table and never create one, As of 2026-11-02: both name a slice holding an entity.ts, by --feature <slice> or by a name that is one, and neither finding it is X_FEATURE_REQUIRED, zero files written, its fix the closest slice's --feature line or x g resource <name>. x g resource and x g entity are what create a table. Every generator produces code that passes x verify unmodified, and writes it through the app's own Biome when the app has one: node_modules/.bin/biome over the TypeScript files that run created, best effort, never a failed x g. A slice whose entity.ts declares another table under its name gets no foundation from x g action/x g mutator, and x g query there is X_QUERY_SLICE_ENTITY. A written contract test reads its exercise from apps/web/exercise.ts (target.contract({ exercise })), and the same run inserts its entry there — once, the file created from x new's when absent, As of 2026-11-08; a file that is not one export const exercise = defineExercise({ … }) is X_EXERCISE_FILE_UNEDITABLE, the entry to paste in its fix:. Errors: X_GENERATE_CONFLICT, X_DB_HANDLE_UNREGISTERED, X_FEATURE_UNKNOWN, X_FEATURE_REQUIRED, X_QUERY_SLICE_ENTITY, X_EXERCISE_FILE_UNEDITABLE.

A name the templates would mangle is X_CLI_BAD_FLAG before anything is planned, each with a fix: that runs: a reserved word or a leading digit; a letter outside ASCII (Über → x g entity uber — it was dropped to ber); an already-plural resource/entity name (posts → x g resource post — it became entity('postses')); and a name whose type spelling an emitted file also uses as a global or local type (promise, omit, partial, row → x g resource promise-resource).

entity and resource write a repo over the typed handle, and register the entity in it, As of 2026-10. The generated repo.ts holds no sql literal, no row decoding and no tenant predicate of any kind — byId(id), list(limit) and insert(row) read db.<table>, and the handle scopes each to the actor's org (Entities and migrations) — and a table is on the handle only when its entity is in the set packages/db/src/client.ts passes to database(). The generator makes that edit, and four more:

EditedWhat landsWhen it cannot
packages/db/src/client.tsthe entity's import, and <table>: <entity>, in const entities = { … }, in key order. Created whole only where the db package would export it: its index.ts names ./client, or it has no index. Never created in a db package of the app's own (As of 27.6.2: an index.ts that exports no handle — a schema and a seed over @ultimat3/db's client)X_DB_HANDLE_UNREGISTERED, exit 1, the two lines in the fix: — the files are written, the handle is not. In an app with its own db package the finding sits on index.ts and says the generated repo expects database()'s handle
packages/db/src/index.tsnothing — it is checked: it must export from ./clientX_DB_HANDLE_UNREGISTERED naming the export line
packages/db/package.json, apps/web/package.jsonthe workspace dependency each new import needs: @<app>/db in the app, and @<app>/web in the db package only when the handle this run leaves imports an entity from it. Planned with the handle edit and written together — a handle that refuses the entry leaves both manifests untouchedleft to package-shape (X_WORKSPACE_DEP_UNDECLARED), whose fix: is the same line
packages/i18n/catalogs/<locale>.jsonthe labels the admin reads for the new screen — admin.<table>.title and one admin.<table>.field.<column> per column — merged in the file's OWN key order (a sorted level stays sorted, a hand-ordered one gains its keys at the end), in every locale the run writes to—
apps/web/shared/roles.tsa written policy.ts's grants (<feature>:read to member, <feature>:write to admin) and the admin's three per-table permissions, declared and granted. A permission is declared only where its grant can be placed, and one any definePermissions([…]) call already declares is not declared againX_PERMISSION_UNGRANTED, exit 1, naming each role and the permissions its grants owes — an app whose role map lives elsewhere, has no such role, or holds a list that cannot be edited safely is told, and the file is left byte-identical
apps/web/api/index.tsevery action, query, job and task module the run wrote, imported and listed under actions / queries / jobs / tasks — the lists Api, the typed client's shape, is read fromleft alone when the file is not defineApi({ actions: [...] }); X_JOB_UNREGISTERED on the manifest step names a job nothing lists. A module whose binding (its file name, camelCased) the index already holds — another feature's reindex-post, or api / defineApi / health — is X_GENERATE_CONFLICT before anything is written, fix: the same run with the feature as a prefix
apps/admin/app/admin/admin.ts (--admin only)the override's import, and <table>: <name>AdminResource, under resources: — the block is created, last in the call, when there is noneX_ADMIN_RESOURCE_UNWIRED, exit 1, the two lines in the fix:

The summary line ends with what the new table owes, as commands that run as printed, and --json carries them as data.next:

✓ wrote 7 file(s) for entity widget — next: bunx x db gen "create widgets" && bunx x db migrate

bun install leads the list when a manifest gained a dependency. --dry-run runs the same edits against the plan held in memory, so it lists every file above that the run would change (As of 27.6.2; it listed the generator's own files only).

An edit to a file the app owns is syntax-aware, and refused rather than guessed, As of 27.6.2. Every list a generator grows — a role's grants, definePermissions([…]), a defineApi list, the handle's entity set — is located on the source with comments and literal contents masked (packages/cli/src/source-list-edit.ts), so an apostrophe, a bracket or a comma inside a comment is never code. A list holding a comment gains rows and no existing byte moves; a comment-free one is re-wrapped the way Biome prints it. The result is read back before it is written — the same entries plus the new ones, the text around it untouched — and an edit that does not read back that way is the finding in the table above, never a write. (x g entity read each apostrophe in a role map's comments as a string delimiter and rewrote the comments between two of them into permissions.)

A feature's second entity is a sibling file, As of 27.6.2. x g entity <name> --feature <feature> in a slice whose entity.ts declares another table writes entity-<name>.ts, entity-<name>.test.ts, repo-<name>.ts and repo-<name>.test.ts beside it — one file per table, nothing the author wrote edited — and registers it exactly as a first entity: handle, role map, admin labels, data.next naming its own table. It was X_GENERATE_CONFLICT on the first entity's four files, with a --force that would have written the second table over the first. The same entity again is still that conflict, on its own files. x db gen reads the entity registry, so a sibling is in the next migration because it is declared. The other generators (action, query, job, …) keep reading the slice's entity.ts and repo.ts: a primitive over a sibling's table imports ./repo-<name> by hand.

Catalog writes are the labels the admin reads: x g entity, x g resource and x g admin:page merge their keys (admin.<table>.title, admin.<table>.field.<column>; admin.<name>.title for a page) into every locale the app has a catalog for — the default locale bare, each other locale marked ⟦…⟧ exactly as x i18n add seeds it, so x i18n check reports them as owed rather than rendering a raw key. Existing keys always win. --dry-run lists each catalog it would change; --locales <one> narrows the run to the catalogs named.

x g admin:page DECLARES the permission it requires, As of 2026-09. It emitted permissions: ['ops:read'] and nothing anywhere declared ops:read, so assertPermission threw X_PERMISSION_UNKNOWN on the first request that reached the page — a screen the generator built and no actor could open, under a green gate: the policy step reads roleDefinitions() and routeEntries(), and an admin page is neither a role nor a route. The page file now carries the definePermissions([...]) call and the PermissionRegistry augmentation beside its AdminCustomPage, and its emitted test fails if either is dropped. Registration is a side effect of import and pages: already imports that module, which is why the declaration is in the page and not in a policy.ts beside it. Since 27.2.0 a policyAuthz() map that omits the permission decides it by the role map, so the role grant is the one declaration; a map entry only refines it. The page lands in apps/admin/app/admin/pages/ without --at — beside the declaration, inside the app scan (DEFAULT_ADMIN_PAGE_DIR); apps/admin/src/pages, the old default, was never imported.

A narrower generator plants the slice modules its own source imports, As of 2026-08 — they used to be x g resource's alone, so x g job into a hand-written slice emitted import * as repo from '../repo' against a file nobody had written. Which modules differ per generator, on purpose: a job has no request behind it and evaluates no policy, so x g job plants no policy.ts.

GeneratorFiles into a bare sliceIts ownSlice modules it plants
x g action · x g mutator53 — the declaration, a unit test and a .contract.test.tspolicy.ts + policy.test.ts; errors.ts only when its body reads a row: a slice with an entity whose repo exports byId
x g query --feature <slice> (± --live)refused: no entity.ts is X_FEATURE_REQUIRED2what the slice lacks of repo.ts + repo.test.ts, policy.ts + policy.test.ts; never entity.ts
x g job22none — a slice with no entity gets the neutral body, never an invented table
x g backfill --feature <slice>refused: no entity.ts is X_FEATURE_REQUIRED2what the slice lacks of repo.ts + repo.test.ts; never entity.ts
x g task44 — the task and the job it enqueuesnone
x g entity · x g policy5 · 2all of them — x g entity counts the catalog it merges its admin labels intoit is the slice module

repo.ts is never planted alone: it imports ./entity for its row type, so the pair goes in together or the unresolved import has only moved — and repo.test.ts rides with it, running the repo against the in-memory driver. An entity planted this way is registered in the handle exactly as x g entity's is. Counts re-derive with --dry-run --json (data.files), As of 2026-10.

A module the slice already has is skipped — never a conflict, never overwritten, --force included. A foundation module belongs to the slice, not to the generator that needed it, and --force is about the primitive you named: clobbering policy.ts to regenerate one action would delete every rule in it. Regenerating a slice module is its own generator — x g entity, x g policy. So a second x g action into a finished slice writes exactly its own 3 files, and one into a half-built slice writes only what is missing.

x g action and x g mutator complete nothing a slice's author wrote. A module already on disk gets no companion from them — no entity.test.ts, repo.test.ts or policy.test.ts, no admin catalog labels — because those assert the shape x g resource writes, not the one that is there. A repo.ts with no byId gets the lookup-free body. And a hand-written policy.ts that does not export what the generated file imports (can<Feature>Write; for an action also <feature>Tag) is refused before anything is written: X_SLICE_POLICY_INCOMPLETE, naming the file and the missing exports. A destructured export counts, one level of relative export * from is followed, and a barrel that cannot be followed (a package) is never refused. Into such a slice the generated unit test grants what the policy grants — the one permission of a literal can('<perm>') — and when that cannot be read it makes no handler assertion and says so in a comment; the contract test makes no foreign-org assertion against a policy it did not write. As of 2026-10.

Every generated entity is tenant-scoped, and there is no --no-tenant flag, As of 2026-09-05 — decided rather than omitted. orgId is not one generator's: x g action, x g query, x g policy and x g job all decide on it too, so a flag on x g entity alone would leave x g resource emitting an action over a column its entity no longer has. A single-tenant app makes the edit per slice, and the emitted entity.ts names it in the comment above tenant: 'orgId': delete that line and the orgId: uuid() column, drop 'orgId' from indexes, let entity.test.ts expect $tenantColumn null and orgScoped false, and drop orgId from repo.test.ts's draft along with its last two tests, and let policy.ts's write rule stop reading input.orgId. repo.ts needs no edit: it names no org. From then on x g action and x g mutator read the slice as x g job does, As of 2026-11-02: no orgId in the input, either test or a policy they plant. A policy.ts still deciding on input.orgId keeps it in the input, because the action compiles against the rule it imports and that file is the author's.

The filename carries the test's type, As of 2026-08-19 — x verify selects a suite by it, so a generated test that landed in a plain *.test.ts ran under unit and x test contract answered X_TEST_NO_FILES:

GeneratorTest filename it writesStep that runs it
x g action · x g mutator<name>.test.ts and <name>.contract.test.tsunit, contract
x g query --live<name>.live.test.tslive
x g job · x g task · x g backfill<name>.job.test.tsjob
x g query (not live) · x g entity · x g policy · x g route<name>.test.tsunit

--force writes the new name and does not touch a file under the old one, because the old name is not in its output list. Delete the orphan by hand.

The synopsis above is GENERATORS in packages/cli/src/cmd-generate.ts, which x g --help prints verbatim — run it if this list and the CLI ever disagree.

x db

x db gen "add publish_at" | migrate | reset | studio
     | seed [<name>] [--tier reference|dev] [--dry-run] [--json]
     | branch list | branch create <name> | branch delete <name>
     | backfill --list [--name n] [--status s] [--limit n] [--json]
     | backfill --pending [--json]
     | backfill <name>|--all [--write] [--force] [--json]
     | diagnostics [--limit n] [--json]
SubcommandDoesNotes
gen ["<name>"]diff the app's entities against what the migrations declare, write the next migration, then rewrite the schema dumpthe message becomes the id's slug; left out it is change. Opens no database of yours — the diff reads the previous migration's snapshot, and the dump is replayed on a scratch database it boots and discards: the embedded one, restored from a snapshot under .x/cache after its first boot, or a real Postgres when a migration creates an extension PGlite cannot link (the engine rule). Exit 1 with a migration written is possible: the dump failed after the migration was generated, the summary and the finding both say so, and the next move is a bare x db gen, never the same message again. A bare x db gen with nothing to generate rewrites packages/db/schema/ alone, which makes it X_SCHEMA_DUMP_DRIFT's whole fix. When the app does not load, nothing is compared and the summary says so — nothing was compared — the app did not load (N finding(s)); fix them, then run x db gen again — never "no migration needed". --json: data.schemaDump = { directory, status, files, written, removed }, status one of written, unchanged, skipped (no migration yet), failed. --allow-destructive lets it emit a drop whose down cannot restore the rows; --rolling-unsafe lets it drop a column never retired, for an app that stops before it starts → A column drop takes two releases
migrateapply pending migrations, verify the result, then rewrite the schema dumpliterally ROLE=migrate's own runMigrations — which ends by diffing the live schema against the ledger it just wrote, on the connection it already holds. A difference is X_DB_DRIFT and a non-zero exit. Then, x db migrate only: the dump is regenerated as gen does, and the database is reopened to report any trigger, function, view, type or sequence no migration creates (unexpected-object, also X_DB_DRIFT). --json carries the same data.schemaDump
resetdelete the embedded data directory, then migrateembedded database only — against an external Postgres it exits X_NOT_IMPLEMENTED and tells you to drop and recreate it yourself
seed [<name>]apply the app's seeds — every one this environment takes, or the one namedreplayable: a second run writes nothing and raises nothing, on Postgres as well as in memory. One transaction per seed, so one bad fixture graph cannot roll back the seeds that already landed. --dry-run reports what each would write and writes nothing
studio—planned: exits X_NOT_IMPLEMENTED pointing at the /_x db panel. It used to shell out to bunx drizzle-kit studio; one subcommand is not worth a second schema engine
branch listevery managed branch of this database: name, location, created-at, sizemanaged means this framework made it — external, a database carrying createBranch()'s comment on database marker; embedded, a pgdata-<name> directory. A branch cloned by the pre-1.2.x psql shell-out carries no marker, so it is absent here and delete refuses it → Known gaps. size reads unknown on the embedded database — measuring it is a full directory walk. The delete path only touches what this lists
branch create <name>CREATE DATABASE … TEMPLATE copy-on-write clone (PGlite: a copied data directory)the isolation an agent should use before migrating. An existing name is X_BRANCH_EXISTS, never an overwrite
branch delete <name>delete that branch database (PGlite: its pgdata-<name> directory)a name list does not show is refused with X_DB_BRANCH_FAILED naming what it would have touched — that is the whole guard, and it is why there is no --force
backfill --listprint the x_backfills ledger — one row per backfill() pass, newest firstwhat has already been swept
backfill --pendingevery backfill the app declared, minus the ones that completedthe alarm: a sweep merged and never enqueued had no ledger row and was invisible everywhere. Non-zero exit when anything is unswept, so a cron reads the code and not the table
backfill <name> / backfill --allgate one sweep (or every pending one) and enqueue itdry run by default — --write is never implied. --write enqueues; the workers perform the pass, because the queue is a job's execution surface. --all isolates per name and continues past a failure, exiting non-zero naming each
diagnosticswhat the database is doing now: blockers (each waiting pid and the blockedBy pid holding it, at data.blockers.rows[]), activity, slowQueries, unusedIndexes, invalidIndexes, notValidConstraints, deadTuplesfacts, never a verdict: exit 0 whatever they say. The one failure is a connection that never opens, X_DB_UNAVAILABLE. Every section is one catalog read in its own BEGIN READ ONLY with a 2 s statement_timeout, capped at --limit rows (default 20, at most 1000). A section that fails is { available: false, code, reason, fix } in place and the other six still answer; slowQueries is unavailable until an operator installs pg_stat_statements (never on the embedded database). No user table is read; session SQL is printed with every literal as ?; a credential in a reason is masked. Whoever runs it sees the redacted SQL of every session on the app's database role (every role, when it holds pg_read_all_stats): statement shape and identifiers, never values. unusedIndexes skips unique and primary indexes and carries statsAgeSeconds and replica. Opens the database without applying the framework tables; an embedded one x dev holds is refused (X_DEV_ALREADY_RUNNING). --json: data.<section>. Arbitrary read-only SQL is db.query over x mcp serve
recount <entity>.<column>compare a countedIn counter with the true count of its child rowsdry run by default: lists the drifted rows (at most 50, the count whole) and exits non-zero with X_LIFECYCLE_TRIGGER_MISSING when any drifted. --write sets them in one cross-tenant transaction and exits 0. An unknown <entity>.<column> is X_CLI_BAD_FLAG. --json: data = { target, write, drifted, rows }

A bare x db backfill is X_CLI_BAD_FLAG naming a shape that works: the four answer four different questions, and defaulting to one of them is the ambiguity axiom 1 refuses.

Seeds have TIERS, and production takes one of them, As of 2026-08. A seed declares tier: 'reference' — data the app is wrong without, which ships to production through this same command — or tier: 'dev', the default, which is fixture data. A bare x db seed runs every tier the resolved environment takes: reference and dev everywhere, reference alone under ULTIMATE_ENV=production.

To do thisRun
every seed this environment takesx db seed --json
one named seedx db seed dev --json
the reference data only, anywherex db seed --tier reference --json
dev fixtures on a box whose ULTIMATE_ENV says productionx db seed dev --tier dev --json
the same, with no argv to change (a release-phase container)ULTIMATE_SEED_TIER=dev x db seed --json
rows for every entity with no seed written, parents first, same-tenant keysx db seed --generate --rows 3 --seed 1 --json

Naming the tier is both the selection and the consent, and that is why it is one word rather than two flags: a cluster that sets ULTIMATE_ENV=production on every box — staging included — would otherwise have to lie about the environment to load its own fixtures. A dev seed reached under production with neither the flag nor the variable is X_CLI_BAD_FLAG and a non-zero exit; the cause names both spellings and the fix: is the runnable one.

--generate draws rows off the entity declarations alone — the seed graph x exercise runs on: --rows per table (per tenant when tenant-scoped, default 3), --seed the integer it is drawn from, so the same seed writes the same rows. Generated rows are dev fixtures with no natural key to replay against, so production refuses them whatever tier is named (X_SEED_ENVIRONMENT); --dry-run counts the plan and writes nothing. The pass reports as one row named (generated).

--json carries a row per seed — { file, name, tier, status, ms, inserted, updated, skipped }, slowest first — plus the totals, because a seed run that got slow is diagnosed by which file took the time. skipped is the replay: a row already stored, with no statement sent for it.

branch requires a verb, and the verb set is closed. The first argument was the branch name, so x db branch with the word ls cloned a database called ls instead of listing anything — and every verb is itself a legal branch name, which is why verb-first is the only shape where a name cannot be read as a subcommand. As of 2026-08 a bare name is refused: a word outside list, create and delete is X_CLI_UNKNOWN_COMMAND and its fix: hands the caller's own word back inside the command that still creates it (x db branch create <word>). The two retired verbs are refused too since 26.0.0, one verb each with no alias, but their fix: names the replacement instead — ls gets x db branch list --json, drop <name> gets x db branch delete <name> — because create drop would clone a database for a caller who asked to delete one.

Two more facts about the set, both As of 2026-08:

FactWhy
an external create runs through @ultimat3/db, not psqlcreateBranch() writes the marker comment list finds branches by. The psql shell-out wrote the database and no marker, so every branch the CLI made was invisible to the only lister the framework has
the marker records the base, not just the dateultimate:branch:<base>:<iso> As of 2026-08-19, and BranchInfo carries base alongside createdAt. listBranches() walks pg_database for the whole server, so two Ultimate apps sharing one Postgres plus one nightly reapBranches was the other app's branches dropped — a DROP DATABASE nobody asked for and nothing recovers from. The reaper skips any branch whose base is not this database, and skips a pre-3.x marker that records no base rather than dropping it: a branch of nothing is not a branch of this database. Self-healing with no migration — the next createBranch writes the base down. The payload is split on the ISO tail, not on the first :, because a database name may contain one
there is no reap verbreapBranches() is a task — a nightly sweep with an app-chosen max age. A CLI verb would be a second path to one job, and no default max age is defensible
FlagTypeDefaultMeaning
--namestring—migration or branch name, or the backfill to filter the ledger by. Under backfill --list it is the only spelling of that filter: a positional there is a pass target in every other shape and is refused with X_CLI_BAD_FLAG rather than silently dropped
--allow-destructivebooleanfalselet gen emit a DROP whose down cannot restore the rows. X_MIGRATION_IRREVERSIBLE's own fix: line names it
--listbooleanfalsebackfill: print the ledger
--pendingbooleanfalsebackfill: declared minus completed, judged in this ULTIMATE_ENV. Non-zero exit when anything is unswept
--allbooleanfalsebackfill: every pending sweep, isolated per name
--writebooleanfalsebackfill: enqueue the pass. Without it the command is a dry run and writes nothing
--forcebooleanfalsebackfill: sweep a name the ledger records as completed, into a new ledger row
--statusstring—backfill: running, completed or failed. Anything else is X_CLI_BAD_FLAG naming the three
--limitstring100backfill: max ledger rows

The backfill ledger is what has already been SWEPT. One row per pass — name, status, rows processed, last cursor, the app version that started it, started/completed — keyed by run rather than by name, so enqueue({ force: true }) writes a new row instead of editing the one it reruns. A completed row is what makes the next pass a no-op — not the next enqueue: the one-live-run index covers ready/delayed/running/suspended, and a completed job is in none of them, so a second enqueue() creates a real job row whose pass then reports skipped: true.

$ x db backfill --list
  name           status     rows   cursor      started-at                duration-ms  run-id
  reindex-posts  completed  84000  -           2026-08-14T09:00:00.000Z  240000       9c1e…
  recount-likes  running    12500  post_12500  2026-08-14T09:31:12.004Z  -            7f3a…
✓ 2 backfill pass(es)

started-at is the ledger's own ISO string, printed verbatim — this repo formats no date without an explicit IANA timeZone, and not formatting is the one rendering with no zone to get wrong. An empty ledger exits 0: "nothing has swept this database yet" is an answer. --json carries the same rows plus the definition checksum. The live half of the same ledger is on x jobs list, and /_x's jobs panel carries the whole of it.

One migration engine, everywhere. gen calls @ultimat3/db's generateMigration(); migrate and reset call migrate() — the same x_migrations ledger, the same per-migration checksum, the same session-pinned advisory lock a ROLE=migrate container runs. A laptop, CI, staging and production therefore share one answer to "what has been applied". Before 1.2.0 these shelled out to bunx drizzle-kit, a second engine with a second journal that no package.json declared and bunx fetched unpinned at run time. Nothing in the request path goes through an ORM either: reads and writes run on @ultimat3/entity's hand-written postgresDriver().

One migration is one file. x db gen writes three, all committed:

FileHolds
packages/db/migrations/<id>.sqlthe up, then a lone -- down line, then the reverse
packages/db/migrations/<id>.snapshot.jsonthe schema this migration leaves behind — what the next x db gen diffs against
packages/db/migrations/<id>.hashthe hash x verify's drift step checks: the loaded entity registry plus every non-test file under packages/db/src — the registry half sees what describe() means, the source half sees a seed or helper edit

Plus the schema dump — packages/db/schema/, the whole schema as SQL, one file per table, rewritten on every x db gen and x db migrate and held by the drift step (X_SCHEMA_DUMP_DRIFT). Layout, limits and what it is not: Migrations and backfills → The schema dump.

And x db gen is that directory's only writer, As of 2026-08. x new scaffolds no migration: a hand-written first file carried no .snapshot.json — the one artifact only the generator produces — so the app's first x db migrate and first x db gen refused each other. A pristine scaffold's first two commands are x db gen "initial" then x db migrate (bun run setup runs both), and until the first has run, x verify's drift step is red naming exactly that — for an app that declares an entity, since zero declared against zero recorded is agreement and --no-example is therefore green. A foreign key is emitted as alter table … add constraint after every table statement, never as a references clause inside create table, so the order entities happen to register in cannot make a migration unappliable.

A rendered fix: names bunx x where node_modules/.bin/x exists (As of 2026-11-02, both renderers, after && too; a # tail and quoted text stay as written). x need not be on PATH. Identical findings collapse to one, with meta.count and meta.reportedAt.

x env init --production (As of 30.0.0) writes .env.production, the file x deploy --method compose reads: ULTIMATE_ENV=production, IMAGE=ultimate-app:dev (the tag x build --target docker makes), POSTGRES_PASSWORD and a DATABASE_URL naming the compose db service with it, APP_URL and SYNC_URL on localhost, and every secret the app declares or the framework refuses to boot without, 32 random bytes as hex each. Secrets are never printed: --json lists the key names (data.written). .gitignore gains .env.production first, the file is mode 600, and a second run refuses (X_ENV_FILE_EXISTS): the password is the one the database volume was initialised with. A required key nothing can derive is written empty, named in data.unfilled, and exits 1.

x env check over a table naming no environment reads it as production, as the boot does, and its summary says checked as production: no ULTIMATE_ENV named. x doctor asks about this machine and stays quiet on that shell. On this machine: ULTIMATE_ENV=development x env check.

The schema dump against the newest snapshot (As of 2026-11-02, drift, no database) is X_DB_DRIFT: a column in packages/db/schema/04_tables/<t>.sql no snapshot records means an applied migration was edited and regenerated. A table hand-written SQL names is exempt.

X_DB_DRIFT has two detectors, and each answers what the other cannot. x verify's drift step hashes the loaded entity registry plus the bytes of every non-test file under packages/db/src against the .hash sidecars — no database, so it runs in a CI with nothing listening, and it catches "you edited an entity and never generated". The registry half sees an entity declared under apps/; the source half sees a seed or helper edit. Bytes, not meaning: a formatter pass over packages/db/src after x db gen moves the hash too, and x db gen answers "no migration needed" and re-records it. x db migrate diffs the live catalog against the x_migrations ledger — a database, so it runs only where one is open, and it catches "someone changed the schema by hand". A table in the x_ namespace is framework bookkeeping (the ledger, the queue, the outbox, the auth tables) and is never counted as drift. The drift step's dump rail is the one part of it that boots a database — a scratch embedded one, its own — so CI still needs nothing listening.

A separate <id>.down.sql is not a migration and is never applied — that was a hand-written pre-1.2.0 layout, and reading it as one would drop every table the pair exists to reverse.

Errors, seed: X_DECLARATION_UNKNOWN (no seed of that name — the cause lists the ones there are), X_CLI_BAD_FLAG (an unknown --tier, one name declared twice, or a dev seed reached under production with no consent), and whatever the seed itself threw, reported per seed with the file it came from.

Errors, schema: X_DB_DRIFT, X_SCHEMA_DUMP_DRIFT, X_DB_GEN_FAILED, X_DB_MIGRATE_FAILED, X_DB_BRANCH_FAILED, X_MIGRATION_CONFLICT, X_MIGRATION_IRREVERSIBLE, X_MIGRATION_VIEW_DEPENDS, X_MIGRATE_CONCURRENT, X_NOT_IMPLEMENTED. X_DB_STUDIO_FAILED is reserved and no longer thrown — x db studio is planned.

Errors, backfill: X_BACKFILL_PENDING (--pending, and the only one that is not a refusal — the sweep simply has not run), X_BACKFILL_UNKNOWN, X_BACKFILL_ENVIRONMENT, X_BACKFILL_MIGRATION_PENDING, X_BACKFILL_APPLIED, X_BACKFILL_RUNNING. All six are @ultimat3/jobs', carry a one-line runnable fix:, and are what a non-zero exit from these shapes means — full wording in Error codes → Backfills. X_BACKFILL_STALLED is the seventh and never reaches this command: it is raised by the pass, on a worker, so it surfaces through x jobs show <id>.

x verify

x verify [--only <step>[,<step>…] [--shard i/n [--timings file]] | --changed [--base <ref>] [--dirty]] [--workers N] [--isolate] [--json]
x verify merge <part.json…> [--json]

--changed, As of 31.0 — the steps whose inputs the diff touches, NOT A GATE RUN like --only and never beside it (X_CLI_BAD_FLAG). The diff is x affected's: <base>...HEAD, --base default the remote-tracking default branch (origin/HEAD, else origin/main, else local main; As of 31.1), --dirty adds uncommitted and untracked work. Each file gets one rule, each rule selects steps; data.changed carries { base, dirty, files: [{ path, rule }], steps: [{ name, files }] } and the human render one step ← files line per step. Fail-open: a path no rule speaks for runs the gate. Each suite but eval then runs only the test files the diff's import closure reaches — x test --affected's selection, data.changed.tests — a suite none of them belongs to is dropped, and the unit coverage floor is not judged on the subset; a file the import graph cannot speak for (a root config, a catalog) runs every test.

rulefilessteps
doc*.md, docs/, wiki/package-shape, errors, unit, manifest, roadmap
test*.test.ts(x), e2e/typecheck, lint, filesize, package-shape, errors + the suite that owns the file
generatedx.manifest.jsonlint, contract-diff, manifest
sourceapps/, packages/, guards/, scripts/, types/, bin/every step but drift
drift-inputpackages/db/**, apps/*/clients/**, openapi.json, any module importing @ultimat3/entity and everything it importsevery step
root-config · deleted · unknown · outsidea root file, any app.config.ts, a vanished module, another directory, a path outside the appevery step

As of 22.7 — the width is a memory budget (max(2.75 GiB, 25% of RAM) at 1.25 GiB a worker — no fixed 4 GiB cap since 2026-11-07, at most one per core, printed on the step line; ULTIMATE_TEST_MEMORY_BUDGET / ULTIMATE_TEST_MAX_WORKERS override, --workers wins), shared machine-wide across concurrent runs, and test files are NOT isolated unless --isolate or x.verify.json "isolate": true says so → Testing. --shard i/n (with --only unit|contract|live|job|e2e) runs one CI job's deterministic slice — live and e2e stay one process inside it, a slice per runner, each runner with its own services (As of 2026-11-07) — and x verify merge folds every job's --json part back into the gate verdict (X_VERIFY_SHARD_INVALID, X_VERIFY_MERGE_INCOMPLETE, X_VERIFY_MERGE_INPUT) → CI: the gate across parallel jobs. The paragraphs below on --workers and batching describe 22.6 and earlier where they differ.

x.verify.json keyWhat it sets
stepssteps this repo may never report as skipped (X_VERIFY_SUITE_VANISHED)
coverage{ "lines", "funcs", "why"?, "exclude"?: [{ "glob", "why" }] } — the floor the unit step holds an app's whole source tree to (X_COVERAGE_BELOW_FLOOR, X_COVERAGE_FLOOR_UNSTATED, X_COVERAGE_FLOOR_STALE) → Testing. x doctor prints every exclude
stepTimeoutMs{ "<step>": milliseconds } — a step's own deadline. Default 8 minutes for the six suites, 5 for every other step; past it the step is X_VERIFY_STEP_TIMEOUT and its processes are killed
isolate · typecheckBin · agentsMdMaxBytesa fresh global per test file · the tsc-compatible binary · the AGENTS.md byte budget

Either render prints one line per finished step on stderr — {"step":"lint","ok":true,"ms":21987} — as each finishes, and every minute a step is still running a still running: unit (12m 3s) line; under --json stdout stays the single document. A cancelled job's log ends on the last step that finished, and a harness that kills a process after N silent minutes never kills a healthy gate.

The single gate. Green means shippable; CI runs exactly this. One step list, in cost order, shared with the framework repo's own bun run verify — and the gate is this command with no flag, because "green" has to mean the same thing for everyone. --only <step> runs one step for an iteration loop, and --only typecheck,lint,boundaries runs several in ONE process — one CLI boot and one app load instead of one per step — always in the gate's declared order, whatever order they were typed in. Neither is a gate run: it announces NOT A GATE RUN in the summary and in --json (data.notAGateRun, and data.only as the list of steps — a list of one for a single name), exits with the steps' own status, and writes no floor file — even a list naming every step. An unknown or empty item is refused (X_CLI_BAD_FLAG) naming every valid step, with a fix that corrects the typo and keeps the rest of the list. There is no --skip. A step with nothing to check in this project reports as skipped (-), never as passed — and the summary counts skips apart from passes and names them, so a gate that is green because a suite does not exist has to say so on the one line every reader sees:

✓ 20 of 21 steps passed in …ms — 1 skipped: roadmap

--json carries the same fact twice: steps[].skipped per step, and data.skipped as the list of names beside data.failed. all {n} steps passed is printed only when nothing was skipped.

x.verify.json ratchets those skips. Hand-written, committed at the repo root, read by the gate and written by nothing:

{ "steps": ["typecheck", "lint", "boundaries", "unit", "contract", "live", "e2e", "manifest"] }

Every name in it is a step this repo has already run, so a step in the list with nothing left to check is a deleted suite — reported as failed, not skipped, with X_VERIFY_SUITE_VANISHED and the two edits that resolve it. A repo with no such file is not ratcheted, and a name the gate does not run is refused by the manifest step (X_CONFIG_INVALID) rather than silently covering nothing. Removing a line is allowed; it just has to be a diff somebody reviews.

--workers widens the test steps only. unit, contract, job and eval run as bun test --parallel=N, each worker with its own database; live and e2e are serial by declaration and say so in the output. x test live and x test e2e obey the same declaration, As of 2026-08-27 — they did not, so x test live --workers 8 ran eight processes over the very files x verify ran one over. --workers is accepted there and clamps to 1. That clamp read the POSITIONAL, so the bare x test --workers 8 still widened over the same files until 2026-09; the selection is now partitioned by each file's own type. The default width is a memory budget, As of 2026-11-07: max(2.75 GiB, 25% of RAM) (BUDGET_RULE) at 1.25 GiB a worker (WORKER_BYTES, GATE_BUDGET_FLOOR and GATE_BUDGET_SHARE in packages/cli/src/test-workers.ts), at most one per core — the paragraph under x verify above. No fixed ceiling of 8: it held a 12-core box with 30 GB free to 8. --workers accepts 1 to 64 on both x verify and x test; an explicit width is the caller's call and is not held to memory.

A large selection runs in batches, As of 22.6.2. A --parallel worker's heap grows with every file it runs and is returned only when its bun test exits, so one process over the whole corpus peaked higher with every test an app added — and the default of 22.6.1 (18 workers on a 12-core box with 35 GB free, 1 GiB budgeted a worker) took a 45 GB machine with no swap down. A pass now runs as ceil(files / (workers x 24)) bun test --parallel=N processes in sequence, the files dealt round-robin over the sorted list, so the peak depends on the width and not on the corpus. One adopter's x test unit, 768 files, 12 cores: 22.6.1's default (18 workers, one process) peaked at 17.1-18.2 GB of process tree in 132-167s; 22.6.2's default (13-14 workers, 3 batches) at 11.3-12.2 GB in 152-163s, on a box shared at load 30-40. At the same width the split costs no wall time within noise (12 workers: 150-158s unbatched, 147s in three batches). The split is a pure function of the file list and the width, so the fix: a failure prints (x test unit --workers N) reruns the same batches; a -- --watch run is never split; a forwarded -- --bail starts no batch after a red one. x test --json carries batches when a pass was split, and each batch's output is kept under a line naming it.

Bun owns the pool, As of 2026-08-27. The CLI used to pack the files into N bins itself (largest-first greedy over file SIZE) and spawn one bun test per bin; --parallel=N hands each free worker the next file instead. Expect no speed-up — the two forms measure the same. Four interleaved runs each on this repo's 1296-file unit corpus, 8 workers: 58.2/60.0/65.0/66.5s hand-packed against 54.5/57.8/61.7/64.5s under --parallel=8. Both are work-bound — the corpus is 436.7s of file time, so eight workers cannot beat 54.6s however the files are dealt, and the slowest single file is 20.5s. The packer is gone because it was 233 lines buying nothing, not because it was slow (#342).

StepChecks
typechecktsc across every workspace
lintBiome: no any, no default exports, no bare Error, no raw colours, no hardcoded user-facing strings
boundariessurface and layer imports, resolved transitively; package tiers in a monorepo; an app's guards/*.ts. A guard may spawn a process (check is any async function): it runs under this step's deadline, so a slow one sets stepTimeoutMs.boundaries in x.verify.json
filesizea source file over 500 lines
package-shapea workspace package missing README.md, CLAUDE.md, tsconfig.json, src/index.ts; a workspace importing a sibling workspace it does not declare (X_WORKSPACE_DEP_UNDECLARED); a workspace's shipped source importing a registry package — @ultimat3/* included, type-only imports too — that neither its own package.json nor the root's declares (X_WORKSPACE_EXTERNAL_DEP_UNDECLARED, fix bun add <pkg> --cwd <workspace>). Builtins (node:*, bun, bun:*), workspaces and tsconfig paths aliases are exempt. As of 31.1
errorsevery X_* code has a runnable fix and a docs page
unitpure logic — services, money, policy predicates, matchers
contractaction/query schemas, policy denials, emitted OpenAPI and MCP shapes
livelive-query snapshot, incremental patches, reconnect delta, policy-filtered rows
jobstep replay, idempotency dedupe, retry/backoff, concurrency, outbox atomicity
e2ea real browser (raw CDP) against the app, spawned on a throwaway database when Chrome is present, including offline and SW update
evalprompt scores vs. their recorded baselines, and a prompt with no eval at all
driftschema vs migrations
contract-diffthe code vs the committed x.manifest.json: a breaking change without a major bump
budgetsper-route JS bytes, the global style layer every document carries (X_STYLES_GLOBAL_MISSING), and every route that reads live rows with no island to receive them (X_LIVE_ROUTE_NO_ISLAND). Builds first when .x/build-stats.json is missing, of an earlier measurement rule, or older than the newest app source: x build --target static --no-preflight --no-capture --out .x/verify-static, under this step's deadline (stepTimeoutMs.budgets), never over a deployable .x/static. A failed build is the step's verdict, with the build's own findings. As of 30.0
manifestthe files an agent reads: x.manifest.json and openapi.json freshness — the one check x manifest --check runs (X_MANIFEST_MISSING / X_MANIFEST_DRIFT / X_MANIFEST_STALE) — .env.example, a hand-written AGENTS.md that exists and is under 12kB, and x.verify.json naming only steps the gate runs
roadmapframework repo only — every docs/idea/14-roadmap.md milestone carries a status marker, and a milestone marked shipped still has the artifacts its own row names

A test's type is its filename suffix — *.contract.test.ts, *.live.test.ts, *.job.test.ts, *.e2e.test.ts (or any test under e2e/), *.eval.test.ts. Everything else is a unit test, so no test can fall between two steps.

eval is the one step that applies with no suite of its own: a prompt no defineEval names is X_EVAL_MISSING, and an eval whose baseline was never recorded is X_EVAL_BASELINE_MISSING, because a skipped step — or one gating against nothing — would read as a green gate over untested code. It gates on the drop from each eval's committed baseline, never on an absolute score — ULTIMATE_EVAL_RECORD=1 x test eval re-records those baselines so accepting a new number is a reviewable diff.

That flag and this step are mutually exclusive. x verify with ULTIMATE_EVAL_RECORD set is X_EVAL_RECORDING and the suite does not run: recording makes every eval write the numbers it just measured and pass, so a gate that inherited the flag reports green over scores nothing compared — and rewrites the committed baselines on its way through.

$ x verify --json
{"ok":false,"command":"verify",
 "summary":"1 of 21 steps failed — 3 skipped: e2e, contract-diff, roadmap","steps":[
  {"name":"budgets","ok":false,"durationMs":812,"skipped":false,"findings":[
    {"code":"X_BUDGET_EXCEEDED","cause":"/pricing ships 61.2kb of JS (minified, uncompressed) over a 40kb budget via site/pricing/page.tsx -> islands/post-form-04f75f18.js; charged: /islands/post-form-04f75f18.js 50123 B, inline scripts 855 B",
     "fix":"edit apps/web/site/pricing/page.tsx — set budget: { js: '62kb' } with // measured: 62682 B (x build --target static) — why: <the function it buys> directly above it; or x routes --json for the chain and move the heavy import behind hydrate: 'interaction'",
     "docs":"https://github.com/developerz-ai/ultimate/wiki/Error-Codes",
     "at":"apps/web/site/pricing/page.tsx",
     "meta":{"jsBytes":62682,"charged":[{"url":"/islands/post-form-04f75f18.js","bytes":50123}],"inlineJsBytes":855}}]}],
 "data":{"failed":["budgets"],"skipped":["e2e","contract-diff","roadmap"],"durationMs":11153}}

A finding is code, cause, fix, and optionally docs, at and meta. at is the path an agent opens — a file, or the stuck test file for a timed-out step — and is absent when there is no such path. meta is structured facts behind cause, never the only home of one: an unmeasured budget carries its route in meta.route (X_BUDGET_UNMEASURED — at names the file that declares it), while X_BUDGET_EXCEEDED carries what the bytes were charged as (meta.jsBytes, meta.charged, meta.inlineJsBytes) and spells the route in cause. X_VERIFY_STEP_TIMEOUT carries meta.step, meta.deadlineMs, meta.killed: [{pid, command}] and meta.inFlight: [{command, files, workers, stuck}]; at is the stuck test file when one could be named, and the failed step's output is the tail of what the killed run had printed.

Errors: X_VERIFY_FAILED (with the failing step names), plus each step's own code.

x build

x build --target docker|binary|static|prebuilt [--tag name] [--out path] [--platform bun-target] [--no-preflight] [--no-capture] [--json]
FlagTypeDefaultMeaning
--targetstringdockerdocker (one image, all roles), binary (bun build --compile), static (prerendered site/), prebuilt (the island store and compiled stylesheets, written to node_modules/.cache/ultimate — the app image's own RUN line; no gate, no subprocess, refuses --tag and --out). The list is BUILD_TARGETS in packages/cli/src/cmd-build.ts
--tagstringultimate-app:devimage tag, docker target only
--outstring.x/app (.x/static for static)output path, binary and static targets only; relative to the cwd
--platformstringthe hostbinary target only: bun-linux-x64, bun-linux-arm64, bun-windows-x64 or bun-darwin-arm64, passed to Bun as --target (the list is BINARY_PLATFORMS in packages/cli/src/cmd-build.ts). A Windows executable — --platform bun-windows-x64, or no --platform on a Windows host — is written as <out>.exe, and data.artifact names that file; data.platform echoes the flag. Any other value is X_CLI_BAD_FLAG before the gate runs, fix: x build --target binary --platform bun-linux-x64. For local runs and CI cross-compiles: a Windows host deploys the Linux image under Docker Desktop (Deployment). As of 2026-10-05
--no-preflightbooleanoffskip the six static gate steps the build runs first (typecheck, lint, boundaries, filesize, package-shape, errors) — only when x verify has run them, as its budgets step does when it builds. Not on prebuilt. As of 22.7
--no-capturebooleanoffstatic only: stamp every og.capture card's og:image tags and photograph none, so no browser is needed (X_SEO_CARD_BROWSER_MISSING otherwise). x verify's own build and scripts/reference-app-gate.ts pass it, and only they do — a deployable build takes the pictures. Reaches the app's prerender.ts as ULTIMATE_BUILD_CAPTURE=off. As of 2026-11

A flag the chosen target never reads — --tag on binary/static, --out on docker, --platform on anything but binary, any of them on prebuilt — is X_CLI_BAD_FLAG before the gate runs, with fix: x build --target <target>.

--out for static is emptied before every build, so it must be provably the build's: absent, empty, the default .x/static, or a directory holding the .x-export marker every static build writes. The app root, an ancestor of it, or any other non-empty directory (apps, .x, .x/pgdata, a home directory) is X_BUILD_OUT_UNSAFE and nothing is removed. A module that will not import fails the static build with X_BUILD_FAILED (fix: x verify --only manifest --json) before the export is touched — its page would otherwise vanish from the artifact under a green build.

Every target has one entry file

BUILD_ENTRY is the whole mapping. The entry is checked before the verify gate runs and before the builder is spawned, because bun build's own "module not found" names no owner.

--targetEntry fileCommand it execsOutput
dockerdocker/Dockerfiledocker build -f <root>/docker/Dockerfile -t <tag> <root>one OCI image, ROLE selects behaviour
binaryapps/web/server.tsbun build --compile --minify [--target <platform>] --define ULTIMATE_FRAMEWORK_VERSION="<version>" <root>/apps/web/server.ts --outfile <out>[.exe]a single executable
staticapps/web/prerender.tsbun run <root>/apps/web/prerender.ts --out <out>prerendered site/

All three are written by x new. A missing one is X_BUILD_ENTRY_MISSING, whose fix names the file and points at a fresh scaffold — the usual cause is an app scaffolded before 1.1.0 wrote server.ts and prerender.ts, or a deleted docker/Dockerfile.

Runs the static verify steps first (typecheck, lint, boundaries, filesize, package-shape, errors); if any fail, exits non-zero without building. The content-hash build ID every target shares is x.manifest.json's, written by x manifest, not computed here. Errors: X_BUILD_ENTRY_MISSING, X_BUILD_FAILED, X_BUILD_OUT_UNSAFE, X_CLI_BAD_FLAG; an unknown --target is X_CLI_UNKNOWN_COMMAND with build --target docker as the suggestion.

--target static names every route it emitted, and every one it did not

.x/static/ is a partial site: site/ routes declaring render: 'static' become files, and everything else is served by the app. Both renderers say so — data.emitted and data.skipped under --json, the same rows as a table in the terminal. A screenshot tool pointed at the directory used to photograph the landing page and file "the island did not mount" against a route that had never been in the artifact.

FieldRowMeaning
emitted{ route, path, file }one file, and the declared route that produced it — /blog/:slug writes one row per prerendered path
skipped{ route, surface, render, reason, why }one declared route that wrote nothing, and the cause

Every declared route is in exactly one of the two. A route in neither is the defect this exists to close.

reason is a closed set, and the four are not one cause wearing four names — the surface is asked first, because app/ allows only stream | ssr and no render: edit can put an app/ route in the artifact:

reasonCauseIs there an edit?
surface-forbids-staticthe surface cannot declare render: 'static' at all (app/)no — move the route to site/, or serve it
mode-revalidatesrender: 'isr' regenerates on a tag or ttl, which a published file cannotyes — render: 'static'
mode-per-requestrender: 'ssr' / 'stream' on a surface that does allow staticyes — render: 'static'
no-prerender-pathsrender: 'static' with dynamic params and no prerender(), so nothing enumeratedyes — add prerender()

Both lists come from prerenderSite, which writes .x/static-report.json beside .x/build-stats.json; x build deletes any previous copy before it spawns the builder, so a stale inventory can never be reported as this run's. An app whose apps/web/prerender.ts does not call prerenderSite writes neither file, and x verify's budgets step, which ran this build itself, reds it with X_BUDGET_UNMEASURED.

Budgets are unchanged and still measured in real bytes on disk — measureDocumentJs weighs every <script src> and every data-x-entry island chunk in the emitted document against the files in out, which is what catches one engine imported twice. A route that is skipped here is still weighed: it is rendered in memory and thrown away, so a budget: on an ssr or isr route has a number.

--target binary compiles apps/web/server.ts as a launcher for the app tree it is started in: --compile-autoload-package-json and --compile-autoload-tsconfig, so the app's packages and paths aliases resolve at run time, and --external '@ultimat3/*', so the framework is ONE instance — loaded from the app's node_modules, the same copy the app's own modules register into. It bundles neither the app nor the framework: run it from the app root, with node_modules installed beside it; it needs no Bun. It still passes --define ULTIMATE_FRAMEWORK_VERSION="<version>", which only bundled framework code reads — none, in this graph; the framework's self-contained x image (docker/Dockerfile) is where the define is load-bearing. What is still unmeasured is the target end to end on a bare VM (Known gaps). As of 2026-10-06

x deploy

x deploy --image repo/app:tag [--method compose|helm] [--release name] [--namespace ns] [--timeout 15m] [--dry-run] [--json]
FlagTypeDefaultMeaning
--imagestringultimate-app:devimage reference to deploy
--methodstringcomposecompose or helm
--releasestringapp.config.ts's namehelm release name; helm only
--namespacestringthe kube context'shelm namespace; helm only
--timeoutstring15mhow long helm waits for the migrate hook and the rollout; helm only
--dry-runbooleanfalseprint the plan, run nothing

compose is six ordered steps against docker/docker-compose.prod.yml — run --rm migrate to completion, then up -d for web, sync, worker, scheduler, then run --rm backfill last. migrate gates and backfill triggers: a slow UPDATE inside a release gate holds the deploy open against a database still serving the previous release, so data sweeps are enqueued after the new pods serve and the workers already draining the queue perform them. The backfill service runs x db backfill --all --write --json and exits. Steps run sequentially and stop at the first non-zero exit; the fix is that step's command, so you can rerun it directly for full output. helm is one helm upgrade --install <release> docker/helm [--namespace <ns>] --wait --timeout <timeout> --output json --set-string image.repository=… --set-string image.tag=… against the chart x new writes. It waits, As of 2026-09-23: without --wait the command exited 0 the moment the API server accepted the objects, and helm's own 5m default failed an upgrade whose migrate hook was still applying a long migration. --json reports helm's own verdict as data.rollout — { release, namespace, revision, status }, status being helm's word (deployed, failed) or unknown when helm printed no record. The release was the literal app for every app; it is now app.config.ts's name, and --release app addresses a release an earlier version created. --release, --namespace and --timeout on --method compose are refused (X_CLI_BAD_FLAG), never ignored. There is no X_NOT_IMPLEMENTED branch: the command implements helm completely, so an app that deleted its chart gets helm's own error and an app with no helm on PATH gets X_CLI_UNEXPECTED naming the binary to install. An image pinned by digest is refused before the upgrade starts — the chart renders repository:tag with no digest branch.

An unknown --method is refused, never defaulted. x deploy --method helmm is X_CLI_UNKNOWN_COMMAND listing compose, helm; it used to run the six-step Compose plan and report method: "compose" back to an operator who asked for a Helm upgrade.

There is no --critical flag, As of 2026-08-23. It was deleted; the parser answers X_CLI_BAD_FLAG and lists the flags deploy does declare. It parsed, it was echoed into the plan JSON as critical: <bool>, and no file in packages/ read that field — so it changed nothing about what the command did, on either method (packages/cli/src/cmd-deploy.ts says so where it used to be declared). It is not coming back: the framework force-reloads nothing and no longer pretends to → docs/architecture/13-topology-runtime.md. The flag list this build ships is x help --json, never this page.

Errors: X_DEPLOY_FAILED (a step exited non-zero — its fix: is that step's exact command), X_CLI_BAD_FLAG (a digest-pinned --image under --method helm, a helm-only flag under --method compose, a --timeout helm would reject, a --release/--namespace that is not a DNS-1123 label, or a flag deploy does not declare), X_CONFIG_INVALID (--method helm with no --release and an app.config.ts whose name is not a helm release name), X_CLI_UNKNOWN_COMMAND, X_CLI_UNEXPECTED.

--method compose reads .env.production before its first step (As of 30.0.0): no file, or a ${VAR:?} of docker/docker-compose.prod.yml it leaves empty (and the shell does not set), is X_DEPLOY_ENV_MISSING naming the variables, and nothing runs; x env init --production writes the file. A failed step's X_DEPLOY_FAILED cause carries the container's own line — the last rendered X_* refusal, read from the one-shot's output or the serving role's docker compose logs.

x deploy verify

Does production serve this build, does it hold, and does a worker of this build claim every queue. A GitOps roll never runs x deploy; this runs in either case. HTTP and Postgres only. As of 30.0.0

x deploy verify --url <origin> [--build <id>] [--window 60s] [--interval 5s] [--timeout 10m] [--idle-ok <queue,…>] [--json]
FlagTypeDefaultMeaning
--urlstringrequireda bare http(s) origin; a path, query or credentials are refused
--buildstringx.manifest.json's buildIdthe id that must answer: BUILD_ID, or whatever CI stamped
--windowstring60show long consecutive good samples must span. 0: answered once
--intervalstring5sthe gap between samples
--timeoutstring10mthe whole wait, window and queues together
--idle-okstringnonequeues exempt while their depth (due + running) is 0: a scale-to-zero pool
StageRule
sampleGET <origin>/readyz on a new connection each, cache-control: no-cache, redirects refused. Good: 2xx, x-ultimate-build equals the build, and no Age above 0 (a CDN copy is not the fleet)
windowwait for the first good sample, then --window of consecutive good ones. A bad sample restarts it, and since the first good one fewer than one sample in ceil(window / interval) may be bad: a fleet stuck with old pods stays red
coveragesamples through the load balancer, reported as samples. Never a claim about each pod
queuesafter the window, when DATABASE_URL is set. Expected: jobs.queues plus every registered job's queue. Served: a row not draining, of this build. Re-read until served or --timeout; a failed read keeps the last grade, and none landing is X_DB_UNAVAILABLE
no DATABASE_URLqueues.graded: false and the summary says so. Never the embedded database, never counted as a pass

--json data: { build, samples: { total, good, byBuild, byStatus, share, cached }, windowMs, queues: { graded, served, unserved: [{ queue, builds, depth }], idle } }. byStatus counts a sample nothing answered as unreachable. builds names what the workers claiming that queue run (unknown for a row from before 30.0.0), so a stalled roll reads as one.

Errors: X_DEPLOY_BUILD_MISMATCH (no sample carried the build), X_DEPLOY_BUILD_UNSTAMPED (2xx with no header, and no build ever named), X_DEPLOY_UNSTABLE (the build answered, the window never closed), X_DEPLOY_QUEUE_UNSERVED (one finding per queue), X_CLI_BAD_FLAG.

X_MIGRATE_CONCURRENT is thrown, As of 2026-08. ROLE=migrate takes the migration lock by a bounded pg_try_advisory_lock poll on one pinned session — one try per 500ms against a 60s budget (MIGRATION_LOCK_WAIT_MS) — and holds it for the whole run. Two overlapping deploys therefore serialise while the first is merely slow: the second waits, then applies. A lock still held when the budget runs out is X_MIGRATE_CONCURRENT and a non-zero exit, because blocking pg_advisory_lock has no timeout and a wedged predecessor sat inside one statement with nothing in the logs — that is what 1.2.0 does → Known gaps.

Serving in production — ROLE, PORT and HOST

There is no x serve command. A container runs the scaffolded apps/web/server.ts, which calls runRole({ root, env: Bun.env }) — the production boot path, with no dev watcher, no /_x, and dev: false. It binds 0.0.0.0 unless HOST says otherwise, because a process bound to loopback inside a container is unreachable from the port mapping, the load balancer and every health probe at once.

EnvDefaultMeaning
ROLEwebone of web, sync, worker, scheduler, migrate, replicator. There is no all
PORT3000empty or whitespace falls back to the default; anything else must be an integer in 0–65535 (0 is legal — an ephemeral port)
HOST0.0.0.0the interface the web and sync roles bind, and the metrics endpoint with them — one decision. Empty or whitespace falls back to the default. HOST=127.0.0.1 inside a container is unreachable through docker run -p — the proxy connects to the container's bridge address, never its loopback — and reachable only where the container shares the host's network namespace (--network host) or through a sidecar and ssh -L inside it. That is the exposure an app which admits one implicit actor without a login wants, and until 2026-09-07 such an app could not run in a container at all. runRole({ …, hostname: '127.0.0.1' }) overrides the variable, the way port overrides PORT, for an app that must never trust the deployment to set it
FailureCodeFix as printed
ROLE names something elseX_ROLE_UNKNOWNdocker run -e ROLE=web <image>
PORT is not a TCP portX_PORT_INVALIDdocker run -e PORT=3000 <image>

ROLE=migrate is the release phase. It starts only the db/queue pair, applies the app's migrations through the ledger — carrying APP_VERSION when set — logs { applied, available, appVersion }, and exits. It never holds a port and never serves. Every other role loads the app's modules (importing them is registration), resolves the build id from BUILD_ID or the manifest, assembles its routes, and holds until SIGTERM → Deployment.

The scaffolded docker/Dockerfile defaults NODE_ENV=production ROLE=web PORT=3000, exposes 3000, and health-checks /readyz.

x secrets

x secrets [show|init|edit|set <NAME>|rotate [--drop <keyId>]] [--json]

Two files, one committed and one never:

FileCommittedHolds
secrets.enc.jsonyesthe sealed envelope: v, alg, the master key's id, the IV, the ciphertext
.secrets.keyno — x secrets init writes the .gitignore rule before it writes the key64 hex characters, mode 0600; on Windows, an ACL granting the current account alone

ULTIMATE_SECRETS_KEY is read first and the key file second, so a container is handed its key by the platform and ships no key file at all.

On Windows the mode keeps no one out — every file reports 0666 and inherits the profile directory's ACL. So every key write (init, rotate, the staged .secrets.key.next) runs icacls <temp> /inheritance:r /grant:r "<DOMAIN>\<user>:F" on the temp file before the rename makes it live: one explicit entry, nothing inherited. Read it back with icacls .secrets.key. An icacls that fails writes no key at all: X_SECRETS_KEY_ACL_FAILED, fix where.exe icacls, or keep no key file and set ULTIMATE_SECRETS_KEY. A rename over a key another process holds open (an editor, an indexer, a virus scan) is retried 5 times over ~150 ms on EPERM/EBUSY before it fails. As of 2026-10-05.

A secret is an environment variable. The decrypted payload is a flat map of ENV_NAME to value, and installSecrets() writes each one into the process environment where nothing has already set it — the real environment always wins, so one image runs in Compose and on K8s with the same committed file. Everything downstream is what already existed:

// app.config.ts
await installSecrets();
export const envSchema = {
  SESSION_SECRET: { type: 'string', secret: true, description: 'Cookie signing key' },
} satisfies EnvSchema;
export const env = defineEnv(envSchema);

One declaration, one row in .env.example, one mask (maskedEnvValues), one redaction entry, one reader (env.SESSION_SECRET). There is no secrets.get() — a second accessor would be values with no declaration, no type and no mask.

SubcommandDoes
show (default)names, lengths, and whether envSchema declares each one; retired master keys by id (retiredKeyIds). Never a value, in either renderer
inita fresh master key, the .gitignore rule, and an empty sealed file. Refuses to overwrite either file
editdecrypt into a temp buffer outside the repo, open $VISUAL/$EDITOR, reseal on save. The buffer is deleted in a finally and by a SIGINT/SIGTERM handler; an unchanged buffer writes nothing
set <NAME>one value, read from stdin — never argv, which lands in shell history and in ps
rotatereseal the same values under a new master key. The committed file is written first and the key last: only the committed one can be restored from git. The key it replaces joins the ring — see below
rotate --drop <keyId>remove one retired key from the ring, by id. Generates no new key and leaves .secrets.key alone

The key ring. seal() writes under the current key; open() picks the key a value names. So a rotation must keep the old key readable until every sealed value is re-sealed:

StepCommandState
1x secrets rotatenew key live; the old one is in secrets.enc.json as ULTIMATE_SECRETS_RETIRED_KEYS, sealed under the new key. A deploy is still handed one key
2the app's re-seal backfill()every value whose sealedKeyId() is a retired id is opened and sealed again
3x secrets rotate --drop <keyId>the retired key is gone; a value still naming it is X_SEAL_KEY_UNKNOWN

ULTIMATE_SECRETS_RETIRED_KEYS is comma-separated, 64 hex characters each, and reaches the process through installSecrets() like any secret — the real environment wins. It is kept by default and never behind a flag: rotate overwrites the only copy of the old key on disk, so dropping it then would leave every sealed value unreadable. show reports it by key id, not as an app secret.

$ x secrets init --json
{"ok":true,"command":"secrets","summary":"sealed secrets.enc.json — master key 4f2a…",
 "data":{"path":"secrets.enc.json","keyPath":".secrets.key","keyId":"4f2a…","gitignore":"added"}}

$ printf %s "$STRIPE_KEY" | x secrets set STRIPE_KEY --json
$ x secrets show
name            value       length     envSchema
SESSION_SECRET  [redacted]  44 chars   yes
STRIPE_KEY      [redacted]  32 chars   no

--json carries exactly what the terminal does, which is why no subcommand has a --reveal flag: the one way to see a value is x secrets edit, and the one way to read one is the app reading env.<NAME>.

Errors: X_SECRETS_KEY_MISSING, X_SECRETS_KEY_INVALID, X_SECRETS_KEY_MISMATCH, X_SECRETS_FILE_MISSING, X_SECRETS_FILE_INVALID, X_SECRETS_TAMPERED, X_SECRETS_PLAINTEXT_INVALID, X_SECRETS_EDITOR_MISSING, X_SECRETS_EDIT_FAILED, X_GENERATE_CONFLICT (init over an existing file), X_CLI_BAD_FLAG (--drop naming an id the ring does not hold).

x auth

x auth seal-mfa [--json]

One subcommand, and no default: a bare x auth is refused (X_CLI_BAD_FLAG names the subcommand), as x db, x mcp and x pr are — the one subcommand rewrites credential rows, and the read-only answer (how many are unsealed) is x doctor's. seal-mfa seals every x_users.mfa_secret still stored in the clear, in place, under the app's master key (As of 2026-10). It is the one-shot an upgrade onto sealed second-factor secrets owes — completeMfa refuses a plaintext value with X_MFA_SECRET_UNSEALED, whose fix is this command, and x doctor reports how many rows are left.

Opensthe app's database, as every other command does (DATABASE_URL, or the embedded one) — the framework's tables are applied first
Needsthe master key: ULTIMATE_SECRETS_KEY or .secrets.key. With neither it is X_SEAL_KEY_MISSING and no row is written
Idempotenta sealed value is counted and never rewritten, so an interrupted run is finished by running it again
Safe under trafficeach write is a compare-and-set on the value read: a user who re-enrolled mid-run keeps the new secret, and the row is counted skipped
ReachesPostgresAuthAdapter's x_users. An app on its own AuthAdapter calls sealMfaSecrets({ adapter }) from @ultimat3/auth
$ x auth seal-mfa --json
{"ok":true,"command":"auth","summary":"sealed 2 second-factor secret(s); 0 already sealed, 0 changed mid-run and left alone",
  "data":{"sealed":2,"alreadySealed":0,"skipped":0}}

Errors: X_SEAL_KEY_MISSING, X_NOT_IN_APP, X_DB_UNAVAILABLE, X_AUTH_WRITE_FAILED.

x vapid

x vapid [show|create] [--json]

The Web Push key pair pwa.push signs with (RFC 8292), As of 26.1.0. Both halves are environment variables — ULTIMATE_VAPID_PUBLIC_KEY and ULTIMATE_VAPID_PRIVATE_KEY — and never config, so they cannot come from different pairs. PWA and offline.

SubcommandDoes
show (default)names the pair the boot would sign with — environment, sealed (secrets.enc.json) or development (the published pair a local process falls back to, which a deployed boot refuses) — and its public key. Never the private key, in either renderer
createmints a P-256 pair on WebCrypto and seals BOTH halves into secrets.enc.json in one write. Refuses (X_GENERATE_CONFLICT) when either is already sealed: a new pair unsubscribes every browser that subscribed with the old one, so rotating is x secrets edit (delete both lines) and then create — a decision, never a re-run
$ x vapid create --json
{"ok":true,"command":"vapid","summary":"sealed a new Web Push key pair into secrets.enc.json — ULTIMATE_VAPID_PUBLIC_KEY and ULTIMATE_VAPID_PRIVATE_KEY",
 "data":{"path":"secrets.enc.json","publicKey":"BBwN…","sealed":["ULTIMATE_VAPID_PUBLIC_KEY","ULTIMATE_VAPID_PRIVATE_KEY"]}}

A key pair minted elsewhere (web-push generate-vapid-keys) is the same format: set the two variables directly. x env example lists both in the Framework section once pwa.push is wired (pwa.vapid set), and x env check reports a deployed table that lacks them (X_PWA_VAPID_KEY_MISSING).

show reads development only when there is nothing sealed to read — no secrets.enc.json, or no master key for it. A sealed file it cannot open is that refusal, with its own fix (X_SECRETS_TAMPERED, X_SECRETS_KEY_MISMATCH, X_SECRETS_FILE_INVALID): the pair is there, and create would refuse it.

Errors: X_SECRETS_KEY_MISSING (create with no x secrets init), X_GENERATE_CONFLICT, X_NOT_IN_APP, and the X_SECRETS_* refusals above.

x manifest

x manifest [--check] [--no-openapi] [--json]
FlagTypeDefaultMeaning
--checkbooleanfalsefail if the committed files are stale instead of rewriting them
--openapi / --no-openapibooleantruealso write openapi.json

Regenerates the generated facts: routes, entities, actions, mutators, queries, jobs, tasks, policies, cache tags, MCP tools, budgets, build ID. Never hand-edit the output. Errors: X_MANIFEST_STALE, X_MANIFEST_DRIFT, X_MANIFEST_BREAKING.

x routes

x routes [--surface site|app|api|shared] [--json]

No positional: x routes list (the retired spelling) or x routes app is X_CLI_UNKNOWN_COMMAND with fix x routes --json — the filter is --surface.

Every URL the app serves through a primitive, in one table (As of 2026-10-07, owner decision 2): the pages from describePages(), then — sorted by path — the action and query routes from apiRoutes() (the builder x dev and the container mount) and the plain routes of apps/<app>/runtime.ts. The MCP dev tool routes.list answers the same rows.

RowSurfaceLast column (declared by)--json keys
a pagesite / appits filemethod (GET), path, surface, file, render, hydrate, offline, budget
a mounted page — every screen defineAdmin() servessite / appthe call and the permissions that gate it: defineAdmin() · admin:read + widgets:readthe page keys plus mount: { by, permissions }; a file route has no mount key
an action or query projectionapithe primitive, its name and its policy: action publishPost · posts:publishmethod, path, surface, primitive, name, auth, policy
a plain route — a runtime.ts one (an OAuth endpoint) or a declared webhookReceiver()'s (webhook.<name>, public)apiruntime.ts routes <name>, then its policythe same keys, primitive: null
$ x routes --surface api --json
{"ok":true,"command":"routes","summary":"2 routes","findings":[],"data":{"routes":[
  {"method":"GET","path":"/_x/query/org-feed","surface":"api","primitive":"query","name":"orgFeed","auth":"public","policy":"allow"},
  {"method":"POST","path":"/api/posts/publish","surface":"api","primitive":"action","name":"publishPost","auth":"required","policy":"posts:publish"}]}}

Not listed: a bearer mount's copies of the API (defineApi({ http: { mounts } })), a page's post: binding, and the framework's own asset, island and /_x routes.

Errors: X_ROUTE_CONFLICT, X_ROUTE_META_MISSING.

x mcp

x mcp tools [--json]
x mcp serve [--transport stdio|http] [--port 9229] [--json]
FlagTypeDefaultMeaning
--transportstringstdiostdio for an editor client, http for a socket
--portstring9229HTTP port when --transport http. 9229 is also the inspector's default; a taken port is X_PORT_IN_USE, naming the next one

Serves @ultimat3/mcp's dev server — 19 tools, one catalog, the same on both transports. Every tool declares a scope; the local developer's caller carries all five, and an HTTP caller carries whatever its bearer token was issued.

ToolDoesScope
routes.listthe x routes --json table: pages (render mode, hydrate, offline, budget), then every action, query and runtime.ts routedev:read
schema.describeentities with columns, types and invariantsdev:read
policies.listevery policy: permission, subject, where it is enforceddev:read
actions.describeactions and queries: schemas, policy, cache tags, MCP exposuredev:read
jobs.inspectjob definitions, retry policy and stepsdev:read
queue.depthpending, running and failed counts per queuedev:read
manifest.readthe generated x.manifest.json as textdev:read
errors.explainX_* → cause, fix, docsdev:read
db.queryONE read-only SQL statement; limit defaults to 100 rows, maximum 1000db:read
db.migrateapply pending migrations to a branch databasedb:migrate
tests.runrun the suite, structured results — an error outside any test (1 error beside 0 fail) or a non-zero exit is a failure, never failed: 0dev:test
verify.runrun x verify, structured per-step resultdev:test
logs.taillast N log lines, optionally for one roledev:logs
graph.query{ node }: what a change to a file or primitive reaches (x impact); { from, to }: the edges linking two nodes (x graph --from --to)dev:read
ui.shotphotograph one route at a named viewport, light or dark; PNG path plus verdict. Launches a browserdev:test
ui.islandphotograph an island in every state its *.island.states.ts declares, as x shot --island doesdev:test
ui.inspectDOM, computed-style and accessibility facts for a bounded set of selectors, in one navigationdev:test
ui.interactdrive one route through a bounded step list, then photograph it; refuses a password field and any step that leaves the origindev:test
ui.diffpixel-compare two PNGs the other ui.* tools wrote under .x/shot/; launches no browserdev:read

db.query and db.migrate refuse structurally — multiple statements, a mutating keyword (a data-modifying CTE included), a locking clause, EXPLAIN ANALYZE, a non-branch target — before the host runs anything.

Never exposed in ROLE=web. Errors split by when they fire:

WhenCodesEffect
boot — configuring an MCP surface with defineAppMcpX_MCP_TOOL_UNDECLARED, X_MCP_TOOL_UNSAFE, X_MCP_TOOL_DUPLICATEthe call throws; no server starts
runtime — one requestX_MCP_TOOL_UNKNOWN, X_INPUT_INVALID (an isError result), X_MCP_SCOPE_DENIED, X_MCP_QUERY_REJECTED, X_MCP_NOT_BRANCH_DB, X_MCP_PROTOCOLthat call is refused; the server keeps serving

A tool this caller may not see is absent from tools/list and answers ToolNotFound, never Forbidden. x token grant <scope> takes effect on the next connection — scopes are fixed for the life of one. Full model: MCP and AI.

x doctor

x doctor [--port 3000] [--json]

Checks Bun version, env completeness, source drift, the newest migration's .snapshot.json, port availability, PWA prerequisites and embedded-Postgres readiness — each failing check carries its own fix command.

It also lists the shipped guards the app's guards/ does not hold, and the ones it holds that differ from the current template — a copy from an older release, or the app's own edit. As of 2026-10-07 (owner decision 8: shipped guards stay copies the app owns).

ListingOne line each--json
missingx g guard <name>data.guards.missing[]: { name, add }
differs from the template (line endings ignored)x g guard <name> --force — rewrites the guard and its test; review the diff, and restore your edit if it was deliberatedata.guards.differs[]: { name, refresh }

A listing, never a finding: deleting or editing a guard is how an app changes a rule, so the verdict does not move.

The chart's monitoring templates, the same way (As of 2026-11): an app with a docker/helm/Chart.yaml and no templates/servicemonitor.yaml or templates/alert-rules.yaml gets one line per missing file, then x g chart-alerts, which writes only those (--json: data.chartAlerts, { missing[], add }). A chart that dropped them on purpose keeps a green verdict; an app with no chart is listed nothing.

The embedded half is the bare-VM case: the external-database probe answers nothing the moment DATABASE_URL is unset, and that silence is the configuration a fresh box has. So x doctor also asks whether @electric-sql/pglite resolves from the app root — a resolve, never an import, because loading it boots the WASM build and takes the single-writer lock the next command needs. Red only where both hold: DATABASE_URL unset and the peer unresolvable, reported as X_DB_UNAVAILABLE with @ultimat3/db's own sentence and its own runnable fix (Bare VM). The snapshot half is As of 2026-08 and separate from drift on purpose: they are two questions with two remedies, and x db gen's own X_MIGRATION_SNAPSHOT_MISSING was a condition this diagnostic could not see at all, so x doctor --json — the fix: of the X_CLI_UNEXPECTED an author reaches it through — ran clean over a broken app.

It probes BOTH ports x dev binds — PORT for the web role and PORT + 1 for the sync node — and the suggestion moves both: x doctor --port 3000 with 3001 taken answers x dev --port 3002, never 3001, which is the port it has just reported as taken (As of 2026-09; the same repair X_PORT_IN_USE's own fix: took in x dev). A --port at the top of the range has no room for the sync node at all, so it reports the boot's own X_PORT_INVALID rather than probing a port below it that x dev would never bind. The port is x dev's own: --port, then PORT, then 3000 — under PORT=4000 it probes 4000 and 4001, never 3000.

APP_URL, when set, must be an http:// or https:// origin (X_CONFIG_INVALID, the sync boot's own refusal), and a loopback one must name that same port — APP_URL=http://localhost:3000 beside PORT=4000 is X_APP_URL_PORT_MISMATCH, whose fix is the export line with the port x dev serves on. Unset is no finding; a public origin is the deploy's and is not judged here.

Sealed columns against the key ring. An app that declares .sealed() columns is checked for two conditions; an app with none is never judged.

FindingConditionFix
X_SEAL_KEY_MISSINGsealed columns, and no usable master key in this environmentx secrets init, or export ULTIMATE_SECRETS_KEY
X_SEAL_RESEAL_PENDINGsealed columns, and a retired key still in ULTIMATE_SECRETS_RETIRED_KEYS — a rotation whose re-seal is not finisheda backfill() rewriting each row, then x secrets rotate --drop <keyId>

The ring is read as a booted process would see it — the real environment first, then the committed secrets.enc.json.

What an upgrade left in the auth tables (As of 2026-10). Asked of an external DATABASE_URL only — the embedded database is never opened by a diagnostic.

FindingConditionFix
X_MFA_SECRET_UNSEALEDx_users rows hold a second-factor secret that is not sealed; each is refused at the second factor until it isx auth seal-mfa --json
X_FRAMEWORK_TABLE_ORPHANEDx_auth_failures is still present: no release reads it, and the boot does not drop a table a replica on the previous release may still be usingpsql "$DATABASE_URL" -c 'drop table if exists x_auth_failures', once every replica runs this release

The output is a CommandResult like every other command's: findings, not a checks array.

$ x doctor --json
{"ok":false,"command":"doctor","summary":"1 finding(s)","findings":[{"code":"X_DB_DRIFT",
  "cause":"packages/db has a schema but no migration recorded it",
  "fix":"x db gen \"initial\"","docs":"https://github.com/developerz-ai/ultimate/wiki/Error-Codes",
  "at":"packages/db/migrations"}],"data":{"count":1,"codes":["X_DB_DRIFT"]}}

x actions · x queries · x entities

x actions  [list|show <name>] [--json]
x queries  [list|show <name>] [--json]
x entities [list|show <name>] [--json]

The declaration registries, projected. list is the default subcommand; show <name> is one declaration (it was describe until 26.0.0). Same rows the manifest and the MCP actions.describe tool are built from — the CLI keeps no second table.

$ x actions list
  name          verb     resource  path                  capability    mcp
  createPost    create   posts     /api/posts/create     post:create   yes
  publishPost   publish  posts     /api/posts/publish    post:publish  yes

$ x entities show posts --json
{"ok":true,"command":"entities","summary":"entity posts","data":{"name":"posts","table":"posts",
  "primaryKey":["id"],"columns":[…],"invariants":[…],"softDelete":true,"orgScoped":true}}

A name nothing registered is X_DECLARATION_UNKNOWN, whose fix names the nearest real one. A module that would not import is reported as a finding — the listing describes what loaded, and never pretends the rest is absent.

x jobs

x jobs [list|show <id>|retry <id>|cancel <id>|delete <id>|promote <id>|resolve-step <id> <step>
       |pause <queue>|resume <queue>|drain] [--queue q] [--state s] [--name n] [--limit n]
       [--after cursor] [--from-step name] [--reason text] [--ran|--not-ran] [--output json] [--json]
SubcommandDoes
listqueue depth (with a failed bucket), ONE page of matching rows, the dead-letter list — a dead job is never filtered out of view — the backfill() passes in flight, the paused queues and the live workers. --limit is 1 to 200 (default 100). --json answers the framework's one page shape — data.rows, data.nextCursor, data.hasMore — and nextCursor is a string exactly when another row exists: the command reads one row past the page (at 200, one probe after it), so a full last page is hasMore: false with no cursor, never one that fetches an empty page. Pass nextCursor as --after while hasMore is true. Until 25.0.0 it was data.next, guessed from the row count
show <id>state, attempt, every step's result, the payload (input, with every key the app declared secret redacted), the failure's stack, the last progress the body reported, the remaining retry delays, the concurrency key the run counts under (concurrencyKey — concurrency.key(input), null unless the job declares a keyed cap), and the x_backfills row for this run when the job is a backfill (backfill: null for every other job)
retry <id>re-queue a job that has FINISHED; a job still ready, delayed or running is refused with X_JOB_NOT_REQUEUEABLE. --from-step <name> drops that step and every step that started after it, so they re-execute while everything before it replays from storage
cancel <id>stop a job that has not finished — the way to end a runaway backfill() sweep. --reason <text> is recorded on the job. Re-reads the row after cancelling, so exit 0 means it is genuinely stopped: a job that already finished, an id no queue holds, and a driver with no introspect.cancel all raise X_JOB_NOT_CANCELLABLE rather than reporting success
delete <id>delete one job and its step records. An unknown id is X_JOB_UNKNOWN; a running job is refused with X_JOB_NOT_REMOVABLE, whose fix is x jobs cancel <id> --json — deleting the row under a body does not stop the body
promote <id>make a job waiting on its run time — delayed at enqueue, or backing off before a retry — due now. Anything else (due, running, finished, suspended in a step.sleep) is X_JOB_NOT_PROMOTABLE, naming the state
resolve-step <id> <step>answer a once step whose outcome is unknown (X_STEP_OUTCOME_UNKNOWN): --ran --output '<json>' records it completed — a replay returns that JSON, default null — and --not-ran records it cleared, so the next run calls it again. Exactly one of the two. Refused on a running job, or a step that did not start-and-not-complete (X_STEP_NOT_RESOLVABLE). Answers the job's trace; then x jobs retry <id>
pause <queue>no worker claims from the queue; enqueues still land. A row every worker's next claim reads, so it holds fleet-wide within one poll interval. Idempotent. Answers the paused list as the queue now holds it
resume <queue>undo it
drainplanned, As of 25.0.0: exits X_NOT_IMPLEMENTED before the queue boots and before a job is leased, pointing at x jobs list --json. Postgres is the one durable job driver, so there is nowhere to drain to. Its two former --to values, redis and nats, were X_NOT_IMPLEMENTED stubs on every method — a drain leased the whole pending batch for five minutes, failed every enqueue and nacked it back — and 25.0.0 deleted both stubs and the unreachable drain body. 25.0.0 also deleted the --to and --dry-run flags: the parser refuses them as unknown

show, retry, cancel, delete and promote each take one id positional, resolve-step an id and a step name; pause and resume take one queue positional. There is no bulk verb here — bulk is requeueMany / removeMany on JobIntrospection, which a dashboard calls. --queue, --state, --name, --limit and --after narrow list only. A bare x jobs is list. One verb each, no alias: ls and rm are X_CLI_UNKNOWN_COMMAND since 26.0.0.

x jobs cancel 019ff1c5-0000-7000-8000-000000000001 --reason "wrong tenant" --json

Runs against the app's own driver — the ambient one when a process already installed it, otherwise the same embedded Postgres queue x dev boots. show, retry and cancel load the app first, As of 2026-10: a trace is the row read through the job's own declaration, so unloaded every run showed concurrencyKey: null and no retry delays. An app that half-loads still answers, with those two fields degraded. An <id> that is not a uuid is X_JOB_UNKNOWN before any statement is sent — x jobs show nosuch used to reach Postgres and come back X_DB_STATEMENT_FAILED [22P02].

list reports only the sweeps still running, because it is a live view of the queue; the whole ledger, finished passes included, is x db backfill --list. A driver that ships no backfill ledger answers with no passes rather than failing the command — the queue is still the question here.

Errors: X_JOB_UNKNOWN, X_JOB_NOT_CANCELLABLE, X_JOB_NOT_REMOVABLE, X_JOB_NOT_PROMOTABLE, X_CLI_BAD_FLAG (a --limit over 200 names the walk), and X_NOT_IMPLEMENTED from a driver with no introspection.

A run refused by its concurrency key reads state: failed with X_JOB_KEY_BUSY in lastError; the fix it carries is x jobs list --name <job> --state running --json — the runs holding the key.

x tasks

x tasks [list|show <name>] [--count n] [--json]
SubcommandDoes
listevery registered task: cron, timezone, catch-up policy, the jobs it enqueues, the occurrence it last fired and its next one
show <name>the same plus the cron in words and the next --count occurrences (default 5, max 50)
--json fieldIs
nextMs · nextthe next occurrence: epoch ms, and ISO-8601 in the task's zone
lastMs · lastthe occurrence the scheduler last dispatched — what it was scheduled for, same two renderings. null (- in the table) for a task that has never fired
lastFiredAtMswhen that dispatch happened, on the queue's clock. lastFiredAtMs - lastMs is the scheduler's lag

The last fire is read from the app's queue (JobIntrospection.taskFires()), so x tasks opens it the way x jobs list does: the running one inside x dev, the app's own otherwise — about 0.6 s on an embedded database already initialised, and nothing in an app that declares no task. It is the last occurrence that enqueued, never the scheduler's watermark — arming and skipping move that. As of 2026-10.

Every instant is rendered in the task's own tz, never a machine-local default: a 0 3 * * * in America/New_York reads 2026-03-06T03:00:00-05:00 before the spring-forward and 2026-03-09T03:00:00-04:00 after it. Same wall clock, different instant — the ambiguity the required tz exists to remove.

Errors: X_DECLARATION_UNKNOWN (with the nearest name as its fix), X_CLI_BAD_FLAG.

x exercise

x exercise [<name>] [--seed n] [--json]

Every action, query and job the app registers, run once against generated rows, with no test written. The question no written test answers: what does each primitive do when it actually runs?

StepWhat happens
databasea scratch one — embedded, or a server named by TEST_DATABASE_URL when a migration needs an extension the embedded one cannot link — with the framework tables and every migration applied. Never the developer's own: the pass commits
rowsa fresh pair of tenants per primitive, every table seeded off its declaration (x db seed --generate's graph)
actorthe first of x policy explain's actors the primitive's policy allows, in its own tenant
inputthe override, then an id field whose stem names a seeded entity (postId → posts), then the minimal draw of its schema
roomnetwork sealed, mail captured, job queue in memory, clock frozen. A call out is refused, so the primitive is unreached, never sent

Each primitive ends reached, refused (no actor its policy allows — by design), off (an override with a reason) or unreached. --json and .x/exercise.json carry the same document: { seed, counts, records[] }, each record { kind, name, outcome, code?, reason?, statements, effects, ms, override? }. A <name> runs that one primitive; an unknown name is X_DECLARATION_UNKNOWN.

unreached is red: X_CONTRACT_EXERCISE_UNREACHED, and its fix: is the entry to paste into apps/web/exercise.ts — valid TS naming the actor the pass ran as (testActor(…).actor, imported from @ultimat3/policy) and the seeded row each id field was bound to (As of 2026-11). The exercise option .contract() takes, one shape:

import type { Actor } from '@ultimat3/core';
import { defineExercise } from '@ultimat3/testing';

declare const author: Actor; // a signed-in actor the policy allows

export const exercise = defineExercise({
  publishPost: {
    actor: author,
    input: (drawn) => ({ ...(drawn as Record<string, unknown>), postId: '<a real id>' }),
  },
  sendInvoice: { off: 'charges a card through the payment provider' },
});

A blank off reason is X_CONTRACT_EXERCISE_UNDECLARED; so is an exercise.ts with no single defineExercise() export. The gate's exercise step runs this command as a child and is red on exactly its findings, plus the static half of the tenant proof: an entity with no tenant column that references one with a tenant and declares no shared reason.

Errors: X_CONTRACT_EXERCISE_UNREACHED, X_CONTRACT_EXERCISE_UNDECLARED, X_DECLARATION_UNKNOWN, X_CLI_BAD_FLAG.

x drill

x drill [<name>] [--list] [--budget-ms n] [--json]

The app run through its dependencies failing, As of 2026-11-06. Each drill boots a fleet on a scratch database made on TEST_DATABASE_URL and dropped after: ROLE=migrate, then web and worker as separate processes (apps/web/server.ts), each dialling Postgres, and Redis or NATS where the app declares them, through a Bun TCP fault proxy; nats-down adds the sync role and a sync client that dials it. Load runs before, through and after the fault over the site's parameterless pages and every action an anonymous POST reaches, each request a fresh input drawn as x exercise draws one (apps/web/exercise.ts honoured; data.skippedWrites names every action left out and why) — the writes are how a job reaches the queue for the worker drills. /readyz is sampled throughout.

DrillFaultMust hold
pg-blackholelive sockets unanswered, server healthyrequests-coded, requests-bounded, recovers, jobs-once
pg-slowevery byte delayed half the deadlinerequests-coded, requests-bounded, recovers
pg-restartreset, then refuse, then passrequests-coded, readyz-flips, recovers, jobs-once
redis-downthe cache tier blackholedrequests-coded, requests-prompt, requests-ok, recovers
nats-downthe bus refusedrequests-coded, recovers, sync-resumes
worker-sigkillSIGKILL the worker, then a fresh onejobs-terminal, jobs-once, requests-coded
worker-sigtermSIGTERM the worker, then a fresh onerole-exits, jobs-terminal, jobs-once
web-sigtermSIGTERM the web role under loadrequests-ok, role-exits

Thresholds come from the app: the request deadline (@ultimat3/http's default, 30 s, because configureHttp is runtime code the CLI does not run), drain.deadlineMs plus drain.readinessGraceMs, and health.readiness — under 'process', readyz-flips expects 200 throughout. --budget-ms replaces the deadline the verdict judges against, not the app's, and is printed in data.budgets.override. An answer may reach the load up to 500 ms past the deadline (DRILL_ANSWER_SLACK_MS: the 504 a deadline fires arrives a beat after it). A 5xx with no X_* code, or X_INTERNAL, is always red. A property with no evidence is red, never passed: a job property is red when no job ran during the drill, sync-resumes when no sync client ran.

A service the app uses must be up for every drill; an unset one is refused, never skipped. A named drill that faults a service the app does not declare is refused; a whole run lists it under data.notRun. --list prints the table and runs nothing. --json: { budgets, targets, skippedWrites, drills[], notRun }, each drill { name, verdict, evidence, budgets, baseline }.

Errors: X_DRILL_FAILED (one per property that did not hold), X_DRILL_SERVICE_MISSING, X_CLI_UNKNOWN_COMMAND, X_CLI_BAD_FLAG.

x policy

x policy [list|explain <subject>] [--json]
SubcommandDoes
listevery permission, the roles that grant it, and the actions and queries that enforce it — plus the permissions nothing enforces, which are grants that do nothing
explain <subject>the allow/deny matrix, one row per actor per declaration — every declared role plus anonymous, evaluated once for each action or query that enforces the subject — naming the clause that decided and its reason

<subject> resolves in order against a permission (post:publish), an action name (publishPost), a query name (postFeed), then an action's HTTP path (/api/posts/publish) — so the fix: line printed by an X_FORBIDDEN is runnable whichever of the four the throwing surface had to hand.

$ x policy explain publishPost
  action publishPost — policy post:publish
  actor      verdict  deciding      reason
  anonymous  deny     post:publish  no actor for post:publish
  author     deny     post:publish  post:publish predicate returned false
  reader     deny     post:publish  actor lacks post:publish
    evaluated with no request input and no row — a rule reading either decides again on the real request

The verdict comes from @ultimat3/policy's own policyMatrix() over the app's real Policy objects — the same evaluation the request path runs, never a second one. It runs outside a request, which is what the last line says: a rule reading input or a row decides again on the real call, so a predicate returned false here is a no-input verdict rather than a standing denial. A policy that cannot be evaluated at all outside a request — a predicate dereferencing input.post.id has nothing to dereference — reports decidable: false and prints that note in place of the table, never a table of invented denials.

Errors: X_DECLARATION_UNKNOWN, X_CLI_BAD_FLAG.

x i18n

x i18n [check|add <locale>|sync <locale>] [--json]
SubcommandDoes
checkscan every t('…') in the app's source, audit it against every catalog on disk, and fail on a missing key
add <locale>write packages/i18n/catalogs/<locale>.json seeded with the default locale's keys and values
sync <locale>add the keys that locale is missing; a key it already has is never overwritten, translated or not

Catalogs are one flat file per locale under packages/i18n/catalogs/. check reports three things separately: missing (a gap, and an exit code), unused (defined and never called), and dynamic — a t(plans.${plan}.name) the extractor cannot resolve. A dynamic call contributes its static head as a runtime-key prefix, so a key only ever reached that way is never listed unused; an expression with no static head contributes nothing, because a guessed prefix would suppress real gaps.

Errors: X_CATALOG_MISSING_KEYS (per locale, as a finding), X_I18N_FAMILY_INCOMPLETE (a _select_/_ordinal_ family with no _other), X_CATALOG_INVALID, X_GENERATE_CONFLICT (add over an existing catalog), X_CLI_BAD_FLAG, X_SCAFFOLD_PATH_ESCAPE.

x mail

x mail preview <id> [--locale <tag>] [--tz <zone>] [--out <dir>] [--json]

As of 2026-11-06. Renders the mail's defineMail({ preview }) data through renderMessage's own path, with nothing sent. No --locale: every locale the app routes.

FlagTypeDefaultMeaning
(positional)mail id—the defineMail({ id }) to render
--localestringevery routed localeone locale
--tzIANA zoneUTCevery date in the body; never the machine's zone (X_TIMEZONE_INVALID)
--outstring—write <id>.<locale>.html and <id>.<locale>.txt there

--json: data.tz is the zone rendered in; data.previews[] is { locale, subject, missing, files }; missing names every catalog key the locale cannot resolve, and any is a non-zero exit. x verify's i18n step renders every declared preview in every locale the same way, so a preview is a test too.

Errors: X_MAIL_PREVIEW_MISSING, X_MAIL_PREVIEW_INVALID, X_MAIL_TEMPLATE_UNKNOWN, X_CLI_BAD_FLAG.

x test

x test [unit|contract|live|job|e2e|eval] [--filter path[,path…]] [--allow-empty] [--sample N]
       [--affected [--base <ref>] [--dirty]] [--workers N] [--seed N] [--json]
       [-- <bun test flags>]
FlagTypeDefaultMeaning
(positional)test typeevery typeone of the six; a test's type is its filename suffix
--filterstring—only files whose path contains this substring — or ANY of several, comma-separated (--filter apps/web/app/billing/,apps/web/app/case/), in one run. An empty item (a,,b) is refused: '' matches every path
--allow-emptybooleanoffa selection that matches no test file exits 0, spawns nothing, and says so (no test file matches … — 0 test file(s) ran, data.files: 0, data.empty: true) instead of X_TEST_NO_FILES. For a runner that computes the selection; a hand-typed typo stays red without it
--samplestring—run at most N files of the selection, deterministically. A fast signal for the eval loop — never a gate
--workersstringone per 1.25 GiB of a max(2.75 GiB, 25% of RAM) budget, at most one per core (the gate's width)worker count, 1 to 64; each worker gets its own template-cloned database
--affectedbooleanoffnarrow the selection to the tests the diff can break: in an app root, by import closure (the files x affected reports as data.tests); elsewhere, the workspaces the diff touches and everything that depends on them. --base/--dirty without it are refused, not ignored
--basestringorigin/HEAD's branchthe ref to diff against, merge-base (<base>...HEAD). Unset: origin/HEAD's target, else origin/main, else local main — x affected's default. Needs --affected
--dirtybooleanoffunion the working tree in — uncommitted and untracked. Needs --affected
--seedstring—replay one simulateJobs seed, the N an X_JOB_SIM_INVARIANT names: handed to every bun test child as ULTIMATE_JOB_SIM_SEED, which runs that seed alone. A uint32, else X_CLI_BAD_FLAG. The shard reproduce line does not carry it yet
-- <args>tail—handed to bun test verbatim, before the file list — x test unit -- --coverage --bail. The reproduce line carries it back. x test is the only command that reads a -- tail: every other one refuses it (X_CLI_BAD_FLAG) rather than dropping it, which is what all of them did until 2026-09

The type rule is x verify's, not a second one — so x test contract runs exactly what the gate's contract step runs. A selection that matches no files is X_TEST_NO_FILES (green with --allow-empty); an unknown type is X_CLI_BAD_FLAG naming the six and suggesting the nearest.

In an app root --affected narrows by import closure, As of 2026-11 — every slice shares the apps/web workspace, so the workspace answer ran the whole app. The rule is x affected's data.tests, never a second one: a test runs when its imports reach a changed file, transitively; a changed file the import graph cannot speak for (app.config.ts, package.json, a catalog, anything outside the app) runs every test; a discovered test outside the scanned globs always runs. data.affected.tests carries { app, all, reason, forcing, files: [{ path, why }] } — reason is closure: N changed file(s) or all: <forcing files>, why is closure, all or unmapped. Outside an app root, data.affected has no tests and the workspace scope is unchanged.

--affected narrows the FEEDBACK, never the gate. The GATE stays un-narrowable — x verify with no flag is the gate, and x verify --only <step> announces NOT A GATE RUN in both renderers precisely so a narrowed run can never be read as one — because a gate that can be scoped is a gate that can be scoped wrong. Nothing affected is green with zero spawns, not a failure: editing a .md should not fail a build. A failure's fix: carries --affected --base <ref> back with it, because --affected decides which files exist to run at all and a rerun without it selects the whole corpus.

The inner loop in this repository is bun run test:affected, As of 2026-11-07 — x test --affected --base origin/main --dirty from the root: the workspaces the branch and the working tree touch, plus every workspace that depends on one; a changed file no workspace owns (package.json, bun.lock, tsconfig*.json, biome.json, scripts/**, the test preload) runs everything, and .md alone runs nothing. bun run test is x test over the whole corpus — the same passes and width as the gate's test steps. Neither is the gate: CI and x verify merge run full x verify.

A selection that mixes serial files with the rest is more than one run, As of 2026-09. --parallel=N is the width of the WHOLE bun test, so the bare x test --workers 8 — which selects every type — ran live and e2e eight at a time beside the unit corpus, over the very files x verify runs one at a time. The files are partitioned by type: one pass at the requested width for everything that may share a pool, then one pass at --parallel=1 per serial type, each reproducible on its own (x test live --workers 1). --json carries a passes array when there was more than one.

x clean

x clean [--dry-run] [--json]

Every test leftover the framework knows of, cleared (As of 27.3.0): the migrated test-database templates under .x/test-db, the whole .x/cache, any .x folder below the app root, and — with TEST_DATABASE_URL set — every probe database whose run is over. --dry-run names them and removes nothing; --json carries data.paths and data.databases. Never touched: .x/pgdata (the dev database), .x/storage, the build output. The test runner sweeps the same set before every run, keeping the newest two of each cache, so x clean is for reclaiming the rest by hand. Testing has the table.

x upgrade

x upgrade [--to <version>] [--dry-run] [--json]

One major per run, so each hop's gate attributes its red to that hop. As of 30.0.0.

An app with no x upgrade yet (below 30.0.0) runs the newest CLI: bunx @ultimat3/cli@latest upgrade --json. The only command that does not hand over to an older app CLI (why); the gate still runs from the CLI the hop installs. Needs Bun at the newest release's floor: below it, X_BUN_VERSION, fix: bun upgrade.

StepWhatRefused with
1clean tree, git is the only state between runsX_UPGRADE_DIRTY_TREE
2read every workspace's @ultimat3/* pins, overrides and resolutions includedX_UPGRADE_PIN_UNREADABLE, X_UPGRADE_MIXED_PINS
3bun pm view @ultimat3/cli versions: the next major's newest; the last major's newest when no later major existsX_UPGRADE_REGISTRY_UNREACHABLE, X_UPGRADE_HOP_TOO_WIDE, X_UPGRADE_VERSION_UNPUBLISHED, X_BUN_VERSION
4write exact pins, raise the Bun floor—
5bun installX_UPGRADE_INSTALL_FAILED, fix: carries the undo
6the installed CLI: x manifest, x doctor --json, x exercise --json, x verify --jsonX_UPGRADE_GATE_UNREADABLE, as a finding on a red hop

A dry run refuses exactly what the real run would, then stops before step 4. It never commits; git diff is the hop. Under --json fd 1 is one document, fd 2 a {"upgrade":"<stage>"} line per stage and a heartbeat a minute.

--to: an exact release in the next major. Over a mixed set a run moves the LOWEST pins one major: ^19 beside ^29 is told --to 20.x.y, that hop rewrites only the pins at or below it (a pin above is never moved backwards) and names its walkthrough, next names the following hop, and the last one levels the set. A straggler jump straight to the highest major is X_UPGRADE_HOP_TOO_WIDE: it skipped every walkthrough between.

data fieldMeaning
from, tothe lowest version pinned; this hop's target
nextwhat a bare x upgrade takes after this hop; null when the walk is over
doneverify's verdict, and the exit code follows it. true when already there, false on a dry run
dryRunnothing written, no gate run
appwhether the root holds app.config.ts
proofwhat done rests on: verify, scripts or none
section{ title, url, local } in Upgrading; local is the same section as an x docs topic, printed whole offline; null when no major is crossed
pins[]{ file, name, from, to }
bun{ from, to, edited[], manual[] }. manual[]: { path, line, text, why }, a site only you can move, listed on every run
agentsMdlink: AGENTS.md was byte-identical to CLAUDE.md and the hop makes it a relative symlink to it · linked: already one · differs: two different files, left alone; merge what only AGENTS.md says into CLAUDE.md, then ln -sf CLAUDE.md AGENTS.md · none: one of the two is missing · null: no hop planned
guards{ reported, missing[], differs[] } from the hop's own x doctor. Offered, never overwritten: x g guard <name> --force is yours. reported: false is a CLI too old to list them, which is not the same as none
manifest{ ok, findings[] }. A failure does not stop the gate
exercise{ reported, ok, findings[] } from the hop's own x exercise --json, listed (and printed) before the gate's findings; each fix pastes as written. reported: false: the hop's CLI answered no exercise document — unknown, never a finding
verify{ ok, summary, failed[], findings[] }, each finding { step?, code, cause, fix, at? }
thenthe next thing to do, in a sentence naming the commands

null for guards, manifest, exercise and verify means no gate ran: a dry run, already there, or no app.

Pins and no app.config.ts. Needs no app: the root is the app at or above the cwd, else the nearest manifest holding @ultimat3/* pins, or the workspace root claiming it. No such manifest is X_UPGRADE_PIN_UNREADABLE; an app below that root is X_NOT_IN_APP, with the cd. With no app, x manifest, x doctor and x verify cannot run, so the hop runs the typecheck and test scripts the root declares: data.scripts[] is { name, ok, code, output }, proof is scripts, and the summary says no x verify ran. Neither script declared: proof: none, done: false, red. It checked nothing, so it never claims otherwise.

Bun siteEdit
engines.bun, packageManager, .bun-versionraised to the release's engines.bun, never lowered. A spelling a text edit would falsify (1.3.x, >=1.3.0 <1.4.0, bun@1.3.6+sha512…, v1.3.0) is listed in manual
bun-version: in YAMLsame, quotes kept. An expression (${{ env.BUN_VERSION }}) or an x-range is listed in manual
oven/bun: tagkeeps its suffix and precision: 1.3-alpine → 1.4-alpine. A $VARIABLE is listed in manual, as is a digest whose tag is below the floor or does not state the patch (1.4-alpine@sha256:… at a 1.4.4 floor). 1.4.4-alpine@sha256:… is not

A hop below 30.0.0 reports X_BUDGET_UNMEASURED until x build --target static --json has run, as that release's x verify does.

x affected

x affected [--base <ref>] [--dirty] [--paths] [--json]
FlagTypeDefaultMeaning
--basestringorigin/HEAD's branchthe ref to diff against. The diff is <base>...HEAD — three dots, so it is measured from the merge-base and an unrelated commit on main does not widen the answer. Unset, the base is the remote-tracking default branch, as already fetched (never fetched for you): origin/HEAD's target, else origin/main, else local main when there is no remote-tracking ref (origin/HEAD or origin/main) (As of 31.1). data.base names the ref used
--dirtybooleanoffunion the working tree in: uncommitted changes plus untracked files
--pathsbooleanoffstdout is the directories alone, one per line — bun test $(x affected --paths); notes and the ✓ summary go to stderr (As of 31.1). Under --json stdout stays the one document, and data is byte-identical either way

Transitive, or it is worse than nothing. A → B → C means a change in C must reach A; a single pass over an unordered list reaches only B, and a scoped run that misses a transitive dependent is a green checkmark on a broken repo. The graph it walks is the one every workspace's package.json declares — which is why package-shape refuses an undeclared cross-workspace import (X_WORKSPACE_DEP_UNDECLARED): an edge missing from a manifest is an edge missing from this answer, silently.

Three rules the set obeys:

ChangeAffected
any non-doc file no workspace owns — tsconfig.json, tsconfig.base.json, biome.json, bun.lock, root package.json, app.config.ts, scripts/**, guards/**every workspace, and the root itself: x test --affected runs every test file, those under no workspace included. Decided by ownership, never by a list of names
a workspace's own package.jsonthat workspace only
a .mdnothing. A doc has no compilation unit to re-check

Why the default base is origin/main, not main. Local main moves only on a pull, so in a checkout nobody pulls — or a worktree — it lags origin, and main...HEAD counts every file origin merged since as this branch's work. origin/main moves on any git fetch. x verify --changed and x test --affected read the same default.

--base is the default and --dirty is opt-in, which is the reverse of the obvious design. This repo runs several agents concurrently in one checkout (no worktrees), so a working-tree diff returns every agent's uncommitted work at once and the "affected" set quietly expands to nearly the whole monorepo — narrowing nothing while reading as though it did. A ref diff is stable under that. --dirty is the right answer for one developer iterating alone.

Typecheck is deliberately not scoped. bun run typecheck is tsc -b, a project build over a shared .tsbuildinfo, so a "scoped" typecheck is either the whole project anyway or a weaker check. The time is in the tests, which is what x test --affected narrows.

x graph · x impact

x graph [--from <node> --to <node>] [--json]
x impact <file|primitive|entity> [--json]

The app as one graph, rebuilt per call from the loaded app (As of 2026-11). Nodes: route, action, mutator, query, job, task, entity, permission, channel, tag, mcpTool, operation, file, test, each id <kind>:<name>. An edge is from depends on to, with its provenance:

ProvenanceSourceEdges
declaredx.manifest.json's facts, from the registriesrequires (permission), invalidates / reads-tag / revalidates (cache tag), exposes (MCP tool), publishes (OpenAPI operation), references (entity), enqueues (task → job), reads (channel → entity)
importthe module graph: every app file's imports, type-only included; the test preload is an import of every testimports, declared-in (primitive → its file, matched by identity against the registries)
observed.x/exercise.json, when newer than x.manifest.jsonwrites / reads (entity), enqueues (job)

x impact answers { routes, actions, queries, jobs, mcpTools, publishedOperations, tests, budgets, opaque, provenance }, each entry with the provenances of the path that reached it. A non-empty publishedOperations means contract-diff will have an opinion. A file with a computed import() is opaque: it and its dependents are in every answer. x graph --from a --to b prints the shortest chain of edges, or none known and the provenances searched. A name, id or path that is no node is X_GRAPH_NODE_UNKNOWN with the nearest three. Queries carry no MCP fact in the manifest, so a query's mcpTools is reached only through an action that imports it.

x affected in an app root adds data.tests: the test files whose import closure meets the diff (all: true with forcing when a changed file is read rather than imported — app.config.ts, package.json, a catalog, anything outside the app).

x client

x client vendor <name> [--from <file|url>] [--only <tag,…>] [--json]
x client customer --out <dir> [--mount <prefix>] [--only <tag,…>] [--package <name>] [--json]

One compiler ($ref, objects, arrays, enums, const, oneOf/anyOf, allOf, nullable), two emitters. As of 2026-11.

SubcommandReadsWritesThe client
vendor <name>--from (a file, or an https URL read through outbound({ dial: 'public' })); omitted, the committed source.jsonapps/web/clients/<name>/{source.json,schemas.ts,client.ts}create<Name>Client({ baseUrl, dial, deadlineMs, headers?, deps? }): one outbound() call per operation, input parsed with t before a byte leaves, the answer through answer.json(schema)
customerthe committed openapi.json, or the file a bearer mount declares (--mount /v1 needs openapi: on that mount)<dir>/{index.ts,types.ts}, plus package.json with --packagecreateClient({ baseUrl, headers?, fetch? }): plain fetch, no import beyond ./types; a non-2xx is ProblemError, refusalOf(error, 'op') narrows it to the codes the operation declares; a Retry-After on 429/503 is waited out once (at most 60 s)
FlagMeaning
--fromvendor only. A path from the current directory or an https URL. Written to source.json as two-space JSON, so the next regeneration is offline
--onlykeep operations carrying one of these tags; a construct the compiler refuses outside them is never read. A regeneration with no --only keeps the tags the stamp records. Matching nothing is X_CLI_BAD_FLAG
--outcustomer only, required
--mountcustomer only: a declared bearer mount's prefix
--packagecustomer only: a package.json named so; publishing is yours
FailureCode
a construct it does not read (not, if/then/else, patternProperties, a non-JSON body, a cookie parameter, a typed additionalProperties beside properties)X_CLIENT_SPEC_UNSUPPORTED, the JSON pointer in the cause
not OpenAPI 3.0 or 3.1X_CLIENT_SPEC_INVALID
a vendor answer that fails its declared shapeX_OUTBOUND_BODY_INVALID (core's), the field in meta.issues
a documented vendor non-2xxX_OUTBOUND_REFUSED, meta.status

The stamp is the build error. Every generated .ts opens with // @x-client {command, source, sourceHash, bodyHash}. The gate's drift step reads it back under apps/*/clients/ and the source globs: the source document's parsed content moved is X_CLIENT_STALE (fix: the stamp's own command), the body's bytes moved is X_CLIENT_EDITED. Reformatting source.json changes nothing; the .ts files carry biome-ignore-all format. Names come from operationId, so a contract-diff red is a customer's client breaking. rpc stays the client inside the app's own repo.

x shot

x shot <route> [--port 0] [--out <dir>] [--no-full] [--settle 2000]
               [--timeout 30000] [--browser <path>] [--cdp-url <ws://…>]
               [--allow-hosts a.com,b.com] [--theme light|dark] [--locale <l>]
               [--cookie name=value[,name=value]] [--expect-status 404]
               [--viewport 390x844,1440x900] [--json]
x shot --matrix [<route>] [--locale <l>] [--theme light|dark] [--viewport …] [--cookie …] [--json]
x shot --island <name> [--state <id>] [--json]
x shot --all-islands [--json]

The framework's stated primary developer is an agent, and an agent cannot look at anything. It can read a file and run a command that prints. x shot turns a rendered route into both.

FlagDefaultMeaning
<route>requireda path on the app. An absolute URL is X_CLI_BAD_FLAG — pointing a headless browser inside your network at someone else's site is not a screenshot tool's job
--port0the kernel picks a free one. Never 3000, which another project usually holds
--out<root>/.x/shot/<slug>/.x/ is already gitignored in every scaffold. A relative --out resolves against the cwd, as x build --out does
--fullon (--no-full for the fold)a fold-sized viewport crops nearly every route
--settle2000matches the hydration runtime's own requestIdleCallback timeout
--timeout30000one navigation
--browserPUPPETEER_EXECUTABLE_PATH, then CHROME_PATHrefused before anything boots if the path does not exist
--cdp-urlSCRAPE_CDP_URLattach to a browser somebody else is running instead of launching one here. ws:/wss:/http:/https:; anything else is refused before the attach
--allow-hoststhe app's host onlyextra hosts the page may request
--cookienonename=value[,name=value], As of 2026-09-29: set in the browser for the app's own origin before the first navigation, so a page is photographed with a cookie-held choice already made — x shot --matrix --cookie consent=granted shoots every cell with the consent banner answered. Applies to --matrix cells and --island states too. A name that is not an RFC 6265 token, or a value holding a space, ;, ,, a quote or a backslash, is X_CLI_BAD_FLAG before anything boots
--expect-statusabsent — any 2xxthe document status this shot is ok with, As of 2026-09-26: x shot /nope --expect-status 404 photographs the not-found page on purpose. Absent, a non-2xx document fails the verdict and the summary names the status. With --matrix it applies to every cell
--localethe app's defaultLocale, always pinnedone of the app's locales, As of 2026-09-25. Sent as Accept-Language on every request the page makes, and for a non-default locale the path is prefixed (x shot /precios --locale en opens /en/precios). Absent, Accept-Language is still pinned to the default locale, so a picture never depends on the language of the machine's Chrome. An undeclared locale is X_CLI_BAD_FLAG before anything boots; refused beside --island
--matrixoffevery static site/ route × every locale × light/dark × 390x844 and 1440x900 — or the --viewport list instead (laid out at exactly that width: scrollbars are hidden, As of 2026-09-29 — a classic scrollbar made every capture 15 px narrow), into .x/shot/matrix/<route>/<locale>/<theme>-<width>x<height>/ (<theme>-<width>/ before 2026-10: two sizes sharing a width wrote one directory), plus .x/shot/matrix/index.html, a contact sheet of every picture with failures marked. One dev server for the whole run. A <route> narrows it to that route, --locale and --theme to one value of their axis; ok only when every cell's verdict is. Refused beside --island/--all-islands
--viewportabsent — the driver's default size<w>x<h>[,<w>x<h>…] in CSS pixels, As of 2026-10 (#444): the route photographed once per size over one dev server, each into <out>/<w>x<h>/ with its own shot.png and verdict.json; ok only when every size's verdict is, and the summary is the first failing size's. x shot /pricing --viewport 390x844,820x1180,1440x900 replaces a hand-written browser script. With --matrix the list replaces the default pair. One flag holding a list, never repeated — the parser keeps a repeated flag's last value. A side outside 1..16384, anything but digits around an x (0x10, 1e3, 390.5), an empty entry or the same size twice is X_CLI_BAD_FLAG before anything boots; refused beside --island/--all-islands, whose states declare their own viewport
--themeabsent — the box's own preference and the app's own theme.defaultModelight or dark, As of 2026-09-19. Both prefers-color-scheme is emulated and the scheme is stored as the visitor's choice under THEME_STORAGE_KEY before navigation — the boot script since 20.2.0 answers defaultMode before the OS, so emulation alone photographs a dark-default app dark whatever was asked (#489). The ui.shot/ui.inspect/ui.interact tools' theme is this flag. Refused beside --island, which photographs both themes

verdict.json is the half that gates, and the more important of the two files: a picture cannot tell you the island threw or logged. It carries the console lines, the island counts, the canvas size, the network tallies, gpu — { api, available, adapter, fallback } from the page's navigator.gpu.requestAdapter(), recorded and never gating, null when the page answered no probe (As of 31.1) — and blind, which names what this capture could not observe. A tool that silently omits what it cannot see is worse than one that says so.

ok is decided by buildVerdict in packages/cli/src/shot-verdict.ts — read it there rather than trusting a count here: nothing logged an error, nothing threw, no island failed to mount, the document photographed is the route asked for, and the document answered 2xx (or exactly --expect-status; status and expectedStatus ride verdict.json, null when no response was seen — unseen never gates). A failed subresource — a missing favicon — is counted under network.failed and never gates. The island condition is new in 8.0.0: before it, every island's mount() could reject and the run still reported ok: true, "clean". The last matters most: a route behind auth: 'required' photographs the sign-in page and reports every island missing, which reads as a bug in the app when it is a bug in the capture.

It drives x dev, never the static build. x build --target static prerenders site/ only, so an app/ route would photograph the landing page. If an x dev is already running on the checkout it is reused rather than booted over — embedded Postgres is single-writer, so a second boot is X_DEV_ALREADY_RUNNING and no picture is ever taken. The verdict says which happened.

x shot launches Chrome itself, over raw CDP — no browser library in the app. It takes --browser, else CHROME_PATH, else the first of the probed paths (/usr/bin/google-chrome and its siblings), or attaches with --cdp-url. With none of them it answers X_SHOT_BROWSER_MISSING, whose fix is export CHROME_PATH=/usr/bin/google-chrome.

--cdp-url is how a run gets a browser this box could not have started, As of 2026-08-24. Every stealth provider — Browser Use Cloud, Scrapfly, Browserless, Remote Browser and the CAPTCHA-solving services — sells the same shape: create a session over their API, get a wss:// CDP endpoint back, connect to a real un-fingerprintable Chromium behind it. @ultimat3/scraping's remoteBrowser({ cdpUrl }) has called that its primary production path since it shipped, and until now no CLI command could reach it: x shot only ever called localBrowser(), so a CI runner or a container with no Chrome could take no picture at all.

# A provider session — the API call is theirs, the endpoint is what x shot needs
CDP=$(curl -s -X POST https://api.example.com/v3/browsers -H "Authorization: Bearer $KEY" | jq -r .cdpUrl)
x shot /pricing --cdp-url "$CDP" --json

# A sidecar in the same compose network
x shot /pricing --cdp-url http://chrome:9222 --json

Three rules decide which browser a run gets, and the third is the one worth knowing: both flags together is refused rather than ranked (one names a Chrome to start, the other says the browser is somebody else's); an exported SCRAPE_CDP_URL is a shell-wide default, so --browser beside it wins and launches locally; and --browser is not read at all on an attach, so a correct remote run is never refused for a binary it will never execute. The variable is SCRAPE_CDP_URL and not a name of the CLI's own because @ultimat3/scraping's own refusal already tells its reader remoteBrowser({ cdpUrl: env.SCRAPE_CDP_URL }).

Closing an attached browser ends the remote session too — remoteBrowser() calls close() and not disconnect(), deliberately: a disconnect leaves a browser somebody is billing for running until its provider times it out, and nobody attributes that bill to the run that caused it.

Never a step of x verify. A gate that needs a browser goes red for reasons unrelated to the change, and CI does not install one.

x shot --island

x shot --island <name> [--state <id>] [--settle 2000] [--json]
x shot --all-islands [--settle 2000] [--json]

One component, in the states it declares, photographed one address at a time. A route shot answers "what does this page look like"; this answers "what does this component look like in the state a reviewer cannot click their way to" — an empty result set, a save the server refused, a quota banner.

The states come from a sibling <name>.island.states.ts beside the island, and that file is pure data: no JSX, no solid-js, one erased import type. It has to be, because the complete expected picture list is computed from it before a browser exists, which is what lets the run diff what it owed against what landed and refuse X_SHOT_ISLAND_MISSING rather than reporting a clean run with no pictures in it. An import of the component itself is X_TEST_ISLAND_STATES_NOT_PURE.

FlagDefaultMeaning
--island <name>requiredthe manifest's own name. A route positional beside it is refused — they are two different subjects, not two spellings of one
--state <id>every declared stateone state. An id the manifest does not declare is refused by name, with the known ids
--settle2000the readiness window. Ready is quiet, not idle: fonts, then N consecutive frames with an unchanged network-activity counter — never "nothing in flight", which never comes for a fixture declared pending
--all-islandsoffevery island in the app, in every state each declares. Its own spelling and never --island with no value: the parser refuses a bare --island ("expects a value"), so "every island" had no form a reader could type that could not be read as a mistyped name. Naming it beside --island, beside --state or beside a route positional is refused by name

Everything else — --out, --timeout, --browser, --cdp-url, --allow-hosts — is the route shot's, unchanged. Pictures land at .x/shot/island/<name>/<state>-<theme>.png, one per state per theme, beside a verdict.json of the same shape and an index.md one level up.

x shot --all-islands is the one command that answers "what does this app look like". It expands every *.island.states.ts in the app through the same plan the one-island form uses, boots one dev server, and keeps one browser per declared viewport across every island — forty states at one size is still one launch. A state that cannot be photographed does not stop the run, and not the next island either: every picture the app can produce is taken, every verdict and the index are written, and only then does the missing-picture gate turn the refusals into a non-zero exit. An app that declares no states at all is refused by name rather than answered with an empty gallery — "produced nothing and exited 0" is the one outcome a reader cannot tell from success.

.x/shot/island/index.md

The file an agent opens instead of guessing at forty PNGs. Markdown, written by both forms — a single-island run gets one too, because the file that says what a picture is cannot be a property of how many islands were asked for.

It carriesWhy
the counts, and the commands that re-run everything / one island / one statean artifact that cannot be regenerated is one nobody trusts twice
what the capture could not seethe verdict's own blind list, handed in rather than reworded — two wordings would drift
per island: the app-relative path of the .island.tsxso a reader can open the component the pictures are of
per state: the id, the title and the notethe note is the "you cannot reach this by clicking, because …" line, and it is the whole reason a reviewer knows what they are looking at
per state: one path per theme, and the state's own verdictok, or every reason it is not — a missing picture, a mount that never happened, a request no stub answers, a console error
per state: console warnings and whether the content overflows its boxrecorded, and gating nothing. A signal that fails a run is a signal an author switches off, and a box its content spills out of is the commonest ugly-UI symptom a pixel-tight PNG cannot show. stateShotOk reads neither

One session per picture, and it costs a browser launch each: page.console() and page.pageErrors() are bounded rings over the whole session, so a shared one files state A's console errors under state B — and per-state attribution is the half of the artifact that gates.

An unstubbed HTTP request fails the run. The page's seal replaces fetch and XMLHttpRequest before the island's chunk is imported, answers the state's routes, and publishes everything else; the capture refuses with X_SHOT_ISLAND_UNSTUBBED_REQUEST naming each method and path. A component whose fetch quietly hangs paints its own loading branch, and the picture then shows a fixture gap dressed up as a real component state.

A socket needs no stub, As of 2026-09-17. WebSocket and EventSource are replaced with an INERT stand-in rather than a refusal: no real dial, no network, no message and no error ever delivered; close()/send() are no-ops. A snapshot of an island does not need a live socket — every state's fixture already rides props/routes — so a component whose mount() unconditionally dials @ultimat3/realtime's LiveClient.connect() used to fail every state of every such island with X_SHOT_ISLAND_UNSTUBBED_REQUEST, and the error's own fix: told the author to declare a stub whose match the harness's "<METHOD> <pathname>" grammar can never accept — WS ws://… matches no isStubMatch, so the printed remedy could never be run. readyState DOES leave CONNECTING once, on a zero-delay timer that fires onopen/an 'open' listener and nothing else — a component whose mount() awaits the socket's own open event before rendering anything (not true of this app's LiveClient, whose connect() registers callbacks and returns, but not a fact this harness may assume of every app) would otherwise hang the mount forever, trading one impossible --settle deadline for the unsatisfiable stub fix this defect was about. Each socket a component constructs is recorded on its own sockets list instead — in the readiness probe, on IslandStateShot.sockets, and in --json — never on unstubbed, and stateShotOk reads neither: a component legitimately holding an open, silent channel is not the same fact as a request nobody stubbed, and this list gates nothing. Not a candidate for the blind-spot list below, either — that list is for a fact a picture cannot show, and both sockets fields ride --json plainly. A real unstubbed fetch/XHR is unaffected and still fails the run exactly as above.

Open popups are in the frame (As of 2026-09-29). The crop is the union of the target's box and every visible [role=listbox], [role=menu], [role=dialog], [role=alertdialog], [role=tooltip], dialog[open] and :popover-open in the harness — portalled or not — so a combobox state declared open photographs its listbox instead of a closed input. verdict.json's box stays the target's own box.

The picture is the crop target plus a small margin. The rectangle is the state's declared selector, or the island's host element, translated out of viewport coordinates into the page's — As of 2026-08-26; measured before that, 720×560 for a component whose own box the same verdict reported as 688×104. The margin came later, because a pixel-tight crop shaves off everything a component paints outside its border box — a box-shadow, an outline, a focus ring, a hairline border on a subpixel — and a reviewer then reads a component with no elevation as flat, which is a change the component never made. It is clamped to the document at both ends, so the frame never starts in negative space and never runs past the page, and it may only ever make the frame bigger than the component's own box.

Both themes are photographed by emulating prefers-color-scheme, not by setting an attribute — As of 2026-08-26. An attribute on the document is the outcome of a theme decision and the component owns it: one that resolves 'system' itself deletes or overwrites it on mount, so the harness is silently overruled and both pictures converge. Measured on the reference app, <state>-light.png and <state>-dark.png came back byte-identical, same md5, from two addresses that really did serve different documents. The attribute is still set, because it is right for a component that reads a theme it does not own.

x pr · x ci

x pr review [--pr <n>] [--repo owner/name] [--all] [--full] [--json]
x pr resolve <thread-id> [--json]
x pr reply <thread-id> --body "…" [--json]
x ci [runs] [--branch <name>] [--run <id>] [--repo owner/name] [--tail <n>] [--full] [--workflow <name>] [--event <push|pull_request|workflow_dispatch>] [--json]
x ci tested [--max-age 24h] [--repo owner/name] [--workflow ci.yml] [--json]

gh pr view --comments does not show inline review threads. It shows issue comments; the actionable findings are anchored to lines and reachable only through GraphQL, and the thread id needed to resolve one exists nowhere else. That is the whole reason x pr exists — otherwise every team on Ultimate rediscovers the same query.

Two hazards it encodes, both of which have bitten in this repo:

HazardWhat x pr review does
A review decision goes stale. It survives later pushes, so CHANGES_REQUESTED can predate the commits that addressed itcompares the deciding review's commit oid against headRefOid and says stale outright. Not timestamps — two reviews seconds apart can straddle a push, and the timestamp then calls one of them current
An outdated thread has line: nullselects originalLine too, so a thread whose diff moved still has a locator instead of arriving anchorless

It reads latestOpinionatedReviews, not reviews(last: n): the most recent reviews are often all COMMENTED at the current head, so dating the decision from "the newest review" reports a stale CHANGES_REQUESTED as current — the hazard, inverted.

--json carries the whole conversation, additively (as of 2026-11, 31.1.0): beside threads, reviews (every review: state — APPROVED, CHANGES_REQUESTED, COMMENTED, … — author, isBot, commit, stale, body), comments (the PR's issue comments, updatedAt included), reviewsTruncated/commentsTruncated (newest 50 each), and bots — one row per review bot: { login, reviews, comments, lastReviewAt, notice, blocked }. notice is the newest "did not review" notice the bot posted as a PR comment — recognised by CodeRabbit's `` tag or a heading that begins with the notice's title, never by a phrase in prose; a submitted review is never one (kind: 'rate-limited' — CodeRabbit's rate or review limit; kind: 'skipped' — review skipped, too many files), and blocked: true means no review of its came after it: no new threads is a review that never happened, not a clean one. Bodies are clamped like thread bodies; --full lifts it. Same one GraphQL round trip. The human render adds one count line and, per bot with a notice, one line: <bot> did not review: … when blocked, else <bot> posted a … notice at …, then reviewed at ….

Resolving closes a conversation, not a finding. Whether the code is fixed is a fact no GitHub mutation observes, and x pr resolve says so rather than implying otherwise.

x ci exists because "gh run view prints a tree of ticks and one cross, and the error is inside a per-job log that is mostly setup noise." For a verify job the log tail is the findings block — this repo's gate already prints X_* codes and executable fix: lines — so x ci reads them back out into findings[] in the standard shape. cause and fix are the text the gate wrote, with every transport wrapper undone — ##[group], ##[error]/##[warning] prefixes, an echoed ::error …:: command (its %0A decoded), and a <comment id=…> fence a nested x ci printed; the log's foreign origin is source: 'ci-log' and the job meta.job — printed on the finding's head as [from=ci-log <job>] — never markup inside fix. ok: false iff a run failed; ok: true means no failed run — an in-flight or queued run still completes with ok: true and is reported through the running > 0 line and a per-run headSha, not as a green verdict. x pr review always exits 0, because unresolved threads are the work to do, not a failure of the command that listed them. The two flags narrow the gh run list server-side — --workflow takes the workflow name or filename (ci.yml), --event takes push|pull_request|workflow_dispatch — so x ci --workflow ci.yml --event push --branch main answers "is main's push CI green" instead of folding every main-branch workflow into one verdict.

x ci tested answers whether a push may reuse a green pull-request run — { tested, condition, reason, pr?, run? }, for GITHUB_SHA or HEAD. tested: true needs all of: the subject ends (#N); PR N merged as this very commit; its head is in this repository (a fork's run is never reused); its head tree is this tree; the newest same-repository run of --workflow (default ci.yml) on that head concluded success, under --max-age old (default 24h: the runner image and the registry move). Any doubt — a condition unmet, gh absent, an API that did not answer — is tested: false and a full run. Exit 0 on every answer, so a workflow reads data.tested; only an unreadable --max-age refuses (X_CLI_BAD_FLAG). The workflow shape that consumes it: CI-Parallel-Gate. As of 2026-11.

Neither is a step of x verify. A gate that needs network and a GitHub token goes red for reasons unrelated to the change.

x errors

x errors [explain <CODE> [--recent]|list] [--json]

--recent (in an app) adds what THIS app has seen of the code: its groups in x_error_groups (count, first and last seen) and the bundles x_failures holds, each with its x repro line.

The Error codes table, programmatically. Runs outside an app: triaging a code must not need an app root.

$ x errors explain X_CURSOR_INVALID --json
{"ok":true,"command":"errors","summary":"X_CURSOR_INVALID — pagination cursor is malformed…",
 "data":{"code":"X_CURSOR_INVALID","cause":"pagination cursor is malformed, tampered with or from
 another query","fix":"drop the cursor and request the first page again (after: null)",
 "docs":"https://github.com/developerz-ai/ultimate/wiki/Error-Codes",
 "site":{"at":"@ultimat3/core/src/cursor.ts","line":26}}}

site is the CLI locating a throw site in the tree — it is not a field the thrown error carries. A code raised at more than one site gets one of them, named in the fix line; run the command rather than quoting the line. docs is ERROR_DOCS_URL, the same URL for every code.

list enumerates every registered code, and names under data.unavailable any package this process could not import — a list silently missing codes is worse than one that says which. An unregistered code is X_ERROR_CODE_UNKNOWN with the nearest real code as its fix; the command never invents an explanation.

x repro

x repro <jobId|requestId|traceId|fingerprint> [--write] [--verify] [--json]
x repro --list [--code X_…] [--json]
x repro --from <file> [--write] [--verify] [--json]

A production failure becomes a failing test. As of 2026-11.

SubjectRead fromWritten as
a dead job (x_jobs id)its row and its steps — capture off or onrepro/repro-<id>.job.test.ts: reproJob(runJobs, …) replays the completed steps from their stored output and runs once more
a captured 5xx (request id, trace id, bundle id, or a fingerprint's newest)x_failures, written when failureCapture.enabled (x dev: always)repro/repro-<id>.contract.test.ts: reproduce(bundle, { driver }) seeds the rows the call read, freezes the clock at the failure, invokes as the actor

The written test is RED until the fault is fixed. --verify runs it: data.reproduced is true when it failed with the captured code; otherwise the answer is X_REPRO_NOT_REPRODUCED, naming the masked fields and the captured build — a fault in the database or a different build replays green. Masked input (.sensitive(), secret-named keys) and masked columns are stood in for from the schema. --list reads the bundles with each group's count and last sighting. An id nothing holds is X_REPRO_UNKNOWN; a request id with capture off is X_REPRO_CAPTURE_OFF.

x docs

x docs "<question|topic|symbol>" [--limit <n>] [--json]

The first lookup, before stating or working around a framework limit. Offline, no app needed. As of 30.0.0.

ReadsTopic
every installed @ultimat3/* package's README.md, CLAUDE.md and source headers — the app's direct dependencies and the CLI's own<pkg>.<module>, <pkg>.README#<anchor>
this wiki, shipped in the @ultimat3/cli tarball (wiki/) — every page but Error codeswiki.<Page>, wiki.<Page>#<anchor>, the anchor GitHub renders
an X_* coderedirected to x errors explain <CODE>, the one way to a code

A question is ranked; a Page#anchor topic (x docs Upgrading#28x--2900-entry-by-entry) prints that whole section, subsections included, and nothing past it. x upgrade --json's section.local is that topic. A search hit carries a capped excerpt.

The ranking — which hits, in which order — is not a stable surface: a minor may reorder it. The --json shape is. An install with no wiki/ still answers from the packages, with an X_CLI_UNEXPECTED finding.

x fix

x fix boundary <file> [--json]

The minimal cut for an import that crossed a surface boundary — the command every X_SURFACE_BOUNDARY finding names in its fix: line. It prints a plan and writes nothing; there is no --write.

For each violation involving the file it reports the offending edge, the full chain that makes it one, and the edit to make. For the shared/ fattening case it generates the split: when exactly one surface reaches the module and the module lands on the same surface as the file it imports, the plan carries the git mv plus every import specifier that move invalidates — the move alone is not a repair, it just relocates the break. When two surfaces reach it, or when relocating would leave the forbidden edge exactly where it was, the module has to be cut by hand and the plan says so rather than guessing.

<file> is app-root-relative, or any suffix that matches exactly one source file — the short form a fix: line emits. Errors: X_FIX_TARGET_UNKNOWN (with the nearest real path as its fix), X_CLI_BAD_FLAG on an ambiguous suffix.

Planned commands

Specified in the design docs, not yet implemented. Every one is in the command registry: calling it exits X_NOT_IMPLEMENTED with a fix: naming the closest shipped command, because "not built yet" and "not a command" are different facts and only one of them is true.

The table is PLANNED_COMMANDS in packages/cli/src/cmd-planned.ts; cmd-planned.test.ts asserts every row is reachable through the parser and that no fix points at another planned command. PLANNED_SUBCOMMANDS in the same file is the one-level-down version — a subcommand of a shipped command that this build does not implement: x db studio and x jobs drain. Each stays in its command's subcommand list, so the parser reaches it and x help <command> lists it.

CommandPurposefix: today
x cache [graph|bust <tag>|clear|stats]what a write evicts; targeted evictionx dev — then the cache panel at /_x
x branch [<name>|delete <name>]copy-on-write database + preview URL + scoped MCP socketx db branch list --json — the database half: list, create <name>, delete <name>
x statusrole health and the build-ID distribution of connected clientsx doctor --json
x logs tailstructured logs and spans, filterablex dev — then the timeline panel at /_x
x token [create --scopes <s>|grant <scope>]MCP tokens and scopesx mcp serve --help
x ai [eval <name>|cache|reindex]eval scores, cache hit rate and tokens saved, vector reindexx test eval --json
x money add-currency <ISO> --exponent <n>extend the currency tablex errors explain X_CURRENCY_UNKNOWN --json — extend it in code: registerCurrency({ code, exponent, name }) from @ultimat3/money, once at boot. Shipped As of 2026-08, and it is what this command promised
x config showthe resolved configuration, defaults includedx manifest --json

Flags reach the same answer, As of 2026-10: x money add-currency USD --exponent 2 and x logs tail --follow --json exit X_NOT_IMPLEMENTED with the row's fix:, as the bare form does.

Names that moved

Older name in the design docsUse instead
x db applyx db migrate
x gen <kind>x g <kind> (or x generate)
x deploy compose / x deploy staticx deploy --method compose / x build --target static
x mcp (bare)x mcp serve
x routes listx routes

Related: Getting started · Configuration · Testing · Deployment · Troubleshooting.

Edit this page on GitHubThis page as markdown