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| Convention | Detail |
|---|---|
--json | every command accepts it and prints a single machine-readable object on stdout. Human output goes to stdout too, but never mixed with JSON |
| Exit codes | 0 success · 1 the command failed (a typed X_* error is printed) · 2 usage error (X_CLI_BAD_FLAG, X_CLI_UNKNOWN_COMMAND) |
| Errors | always code + cause + fix. See Error codes |
| App detection | most 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 |
| Flags | long 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 |
| Subcommands | when 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 |
| Passthrough | a bare -- sends everything after it to the underlying tool untouched |
| Telemetry | none 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.
| Command | Does | Status |
|---|---|---|
x new <name> | scaffold a monorepo that already runs | shipped |
x dev | all roles in one process: embedded services, sub-second reload, /_x mounted | shipped |
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 boot | shipped |
x g <kind> <name> | scaffold a primitive with its test | shipped |
x db <sub> | gen, migrate, reset, seed, studio, branch, backfill, diagnostics | shipped |
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 --skip | shipped |
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, rotate | shipped |
x auth seal-mfa | one-shot: seal every second-factor secret still stored in the clear | shipped |
x vapid [show|create] | the Web Push key pair: which one this app signs with, or mint one and seal both halves | shipped |
x build | container image, single binary, prerendered static site, or a prebuilt bundle (--target docker|binary|static|prebuilt) | shipped |
x deploy | run the container deploy plan: migrate first, then the serving roles | shipped |
x manifest | regenerate x.manifest.json and openapi.json | shipped |
x routes | every served route: pages, then actions, queries and runtime.ts routes | shipped |
x mcp serve · x mcp tools | serve the framework MCP tools over stdio or HTTP · list them | shipped |
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 doctor | environment, versions, drift, the newest migration's snapshot sidecar, ports, PWA prerequisites — each with a fix | shipped |
x help / x version | catalogue and version | shipped |
x actions / x queries / x entities | introspect the declaration registries | shipped |
x jobs | list, show, retry, cancel, remove and promote jobs; pause and resume a queue · drain is planned | shipped |
x tasks | cron tasks, their timezone and their next run | shipped |
x policy | which clause decided a permission, and why | shipped |
x exercise [<name>] [--seed n] | run every action, query and job once on generated rows, with no test written; the gate's exercise step | shipped |
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 services | shipped |
x i18n | add, sync, check catalogs | shipped |
x mail preview <id> | render a mail's declared preview in every locale, nothing sent | shipped |
x test <type> | run one of the six test types, or the whole suite | shipped |
x clean [--dry-run] | remove what test runs leave behind: migrated test templates, .x/cache, stray .x folders, stale probe databases | shipped |
x affected | the workspaces a diff can have broken, transitively | shipped |
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 reaches | shipped |
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 import | shipped |
x shot <route> | photograph one route from a real browser, plus a verdict.json a picture cannot carry | shipped |
x pr review|resolve|reply | inline review threads — the ones gh pr view --comments does not show — plus every review verdict, every PR comment, and which review bots did not review | shipped |
x ci | the workflow runs for this branch, and the findings inside the failed step's log; x ci tested: whether a push reuses a green PR run | shipped |
x errors explain <CODE> | code → cause, fix, docs | shipped |
x docs "<question>" | framework docs and this wiki, offline, from the install — the first lookup | shipped |
x fix boundary <file> | plan the minimal cut for an import that crossed a surface boundary (never rewrites) | shipped |
x upgrade | one major per run: exact pins, the Bun floor, bun install, the gate, the section to read | shipped |
x cache | tag graph, bust, clear, stats | planned |
x branch | copy-on-write branch environments | planned |
x status | connected-client build-ID distribution, role health | planned |
x logs tail | structured logs + OTel spans | planned |
x token | create and grant MCP scopes | planned |
x ai | eval, cache stats, reindex | planned |
x money add-currency | extend the currency table | planned |
x config show | the resolved app.config.ts | planned |
x new
x new <name> [--dir path] [--shape api|blog|dashboard] [--no-example] [--no-git] [--dry-run] [--force] [--json]| Flag | Type | Default | Meaning |
|---|---|---|---|
--dir | string | cwd | parent directory to create the app in |
--example / --no-example | boolean | true | include the example feature slice (default: on; --no-example for an empty app/) |
--shape | string | none | start 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-git | boolean | true | git 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-run | boolean | false | print the file list, write nothing |
--force | boolean | false | write 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 type | Why it is refused | fix: |
|---|---|---|
x new 9lives | starts with a digit — core's pattern is ^[a-z][a-z0-9-]{1,63}$ | x new app-9lives |
x new a | one character | x new a-app |
| a slug over 64 characters | past the pattern's bound | the 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:
| Path | Why |
|---|---|
apps/web/server.ts | the 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.dockerignore | this 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.yml | one service per role: migrate, web, sync, worker, scheduler |
docker/docker-compose.dev.yml | parity 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.md | how the two compose files differ |
.github/workflows/ci.yml | the 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.ts | TypeScript 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]| Flag | Type | Default | Meaning |
|---|---|---|---|
--port | string | 3000 | HTTP port. The sync role listens on --port + 1 |
--role | string | web,sync,worker,scheduler | comma-separated roles to run in this process |
--once | boolean | false | boot, 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:
| Registered | Served as |
|---|---|
action / mutator | POST /api/<resource>/<verb>, policy enforced by the pipeline |
route | its URL, in its declared render mode, with that mode's cache headers |
job | claimed off the real Postgres queue by the worker role |
task | dispatched 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:
| Panel | Kills the question |
|---|---|
routes | which URL renders how, with which budget |
timeline | where did this request spend its milliseconds, and did it loop |
live | why did this subscriber not get the row |
jobs | which step failed, and what is queued |
db | what is in the table, and does the schema match the migrations (read-only SQL) |
mail | what did that email look like, in that locale |
cache | which tags would this invalidation bust |
policy | which clause decided, for which actor |
manifest | is the committed x.manifest.json current |
services | which database/events/storage this process is talking to, and its reload count |
boundaries | which 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.
| Env | Unset means | Set means |
|---|---|---|
DATABASE_URL | PGlite in this process | that Postgres |
NATS_URL | in-process fanout | that 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_BUCKET | the KV bucket x_presence | that bucket — one per app on a shared cluster |
S3_ENDPOINT | .x/storage on disk | that 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.
| Case | What happens | --json data |
|---|---|---|
a live x dev holds .x/dev.lock | SIGTERM 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 } |
| Windows | kill is TerminateProcess, so no drain runs; x stop clears the lock itself | { …, drained: false } |
| no lock, or a lock naming a dead pid | nothing is signalled, exit 0. A stale lock is left for the next x dev's preflight to clear | { stopped: false } |
the pid outlives --timeout | X_DEV_STOP_TIMEOUT, naming the pid; the lock stays | — |
| Flag | Type | Default | Does |
|---|---|---|---|
--timeout | duration (30s, 2m, PT1M) | core's default drain deadline (25 s) + the 5 s release floor + 5 s | how 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:
| Shape | Writes | Also edits |
|---|---|---|
--password | sign-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 setup | apps/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 setup | as 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.
| Flag | Type | Default | Meaning |
|---|---|---|---|
--feature | string | derived from the name | feature 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 |
--surface | string | app | site or app |
--app | string | web | route, 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 |
--permission | string | <name>:read | admin: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} |
--live | boolean | false | query only: make it subscribable. On any other generator it is X_CLI_BAD_FLAG naming the x g query … --live to run instead |
--admin | boolean | false | resource 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 |
--locales | string | every locale with a catalog in packages/i18n/catalogs/, else en | comma-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) |
--password | boolean | false | sign-in only: an address and a password over login() |
--oidc | string | — | sign-in only: an OpenID Connect provider id (corp-sso; lowercase, digits, hyphens, not a built-in), discovered from <PROVIDER>_ISSUER |
--print | boolean | false | chart-alerts only: print the alert rules as plain Prometheus rule groups, write nothing. On any other generator it is X_CLI_BAD_FLAG |
--force | boolean | false | overwrite existing files |
--dry-run | boolean | false | the 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:
| Edited | What lands | When it cannot |
|---|---|---|
packages/db/src/client.ts | the 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.ts | nothing — it is checked: it must export from ./client | X_DB_HANDLE_UNREGISTERED naming the export line |
packages/db/package.json, apps/web/package.json | the 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 untouched | left to package-shape (X_WORKSPACE_DEP_UNDECLARED), whose fix: is the same line |
packages/i18n/catalogs/<locale>.json | the 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.ts | a 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 again | X_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.ts | every 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 from | left 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 none | X_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 migratebun 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.
| Generator | Files into a bare slice | Its own | Slice modules it plants |
|---|---|---|---|
x g action · x g mutator | 5 | 3 — the declaration, a unit test and a .contract.test.ts | policy.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_REQUIRED | 2 | what the slice lacks of repo.ts + repo.test.ts, policy.ts + policy.test.ts; never entity.ts |
x g job | 2 | 2 | none — 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_REQUIRED | 2 | what the slice lacks of repo.ts + repo.test.ts; never entity.ts |
x g task | 4 | 4 — the task and the job it enqueues | none |
x g entity · x g policy | 5 · 2 | all of them — x g entity counts the catalog it merges its admin labels into | it 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:
| Generator | Test filename it writes | Step that runs it |
|---|---|---|
x g action · x g mutator | <name>.test.ts and <name>.contract.test.ts | unit, contract |
x g query --live | <name>.live.test.ts | live |
x g job · x g task · x g backfill | <name>.job.test.ts | job |
x g query (not live) · x g entity · x g policy · x g route | <name>.test.ts | unit |
--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]| Subcommand | Does | Notes |
|---|---|---|
gen ["<name>"] | diff the app's entities against what the migrations declare, write the next migration, then rewrite the schema dump | the 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 |
migrate | apply pending migrations, verify the result, then rewrite the schema dump | literally 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 |
reset | delete the embedded data directory, then migrate | embedded 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 named | replayable: 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 list | every managed branch of this database: name, location, created-at, size | managed 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 --list | print the x_backfills ledger — one row per backfill() pass, newest first | what has already been swept |
backfill --pending | every backfill the app declared, minus the ones that completed | the 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 --all | gate one sweep (or every pending one) and enqueue it | dry 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 |
diagnostics | what the database is doing now: blockers (each waiting pid and the blockedBy pid holding it, at data.blockers.rows[]), activity, slowQueries, unusedIndexes, invalidIndexes, notValidConstraints, deadTuples | facts, 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 rows | dry 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 this | Run |
|---|---|
| every seed this environment takes | x db seed --json |
| one named seed | x db seed dev --json |
| the reference data only, anywhere | x db seed --tier reference --json |
dev fixtures on a box whose ULTIMATE_ENV says production | x 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 keys | x 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:
| Fact | Why |
|---|---|
an external create runs through @ultimat3/db, not psql | createBranch() 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 date | ultimate: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 verb | reapBranches() 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 |
| Flag | Type | Default | Meaning |
|---|---|---|---|
--name | string | — | 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-destructive | boolean | false | let gen emit a DROP whose down cannot restore the rows. X_MIGRATION_IRREVERSIBLE's own fix: line names it |
--list | boolean | false | backfill: print the ledger |
--pending | boolean | false | backfill: declared minus completed, judged in this ULTIMATE_ENV. Non-zero exit when anything is unswept |
--all | boolean | false | backfill: every pending sweep, isolated per name |
--write | boolean | false | backfill: enqueue the pass. Without it the command is a dry run and writes nothing |
--force | boolean | false | backfill: sweep a name the ledger records as completed, into a new ledger row |
--status | string | — | backfill: running, completed or failed. Anything else is X_CLI_BAD_FLAG naming the three |
--limit | string | 100 | backfill: 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:
| File | Holds |
|---|---|
packages/db/migrations/<id>.sql | the up, then a lone -- down line, then the reverse |
packages/db/migrations/<id>.snapshot.json | the schema this migration leaves behind — what the next x db gen diffs against |
packages/db/migrations/<id>.hash | the 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.
| rule | files | steps |
|---|---|---|
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 |
generated | x.manifest.json | lint, contract-diff, manifest |
source | apps/, packages/, guards/, scripts/, types/, bin/ | every step but drift |
drift-input | packages/db/**, apps/*/clients/**, openapi.json, any module importing @ultimat3/entity and everything it imports | every step |
root-config · deleted · unknown · outside | a root file, any app.config.ts, a vanished module, another directory, a path outside the app | every 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 key | What it sets |
|---|---|
steps | steps 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 · agentsMdMaxBytes | a 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).
| Step | Checks |
|---|---|
typecheck | tsc across every workspace |
lint | Biome: no any, no default exports, no bare Error, no raw colours, no hardcoded user-facing strings |
boundaries | surface 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 |
filesize | a source file over 500 lines |
package-shape | a 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 |
errors | every X_* code has a runnable fix and a docs page |
unit | pure logic — services, money, policy predicates, matchers |
contract | action/query schemas, policy denials, emitted OpenAPI and MCP shapes |
live | live-query snapshot, incremental patches, reconnect delta, policy-filtered rows |
job | step replay, idempotency dedupe, retry/backoff, concurrency, outbox atomicity |
e2e | a real browser (raw CDP) against the app, spawned on a throwaway database when Chrome is present, including offline and SW update |
eval | prompt scores vs. their recorded baselines, and a prompt with no eval at all |
drift | schema vs migrations |
contract-diff | the code vs the committed x.manifest.json: a breaking change without a major bump |
budgets | per-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 |
manifest | the 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 |
roadmap | framework 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]| Flag | Type | Default | Meaning |
|---|---|---|---|
--target | string | docker | docker (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 |
--tag | string | ultimate-app:dev | image tag, docker target only |
--out | string | .x/app (.x/static for static) | output path, binary and static targets only; relative to the cwd |
--platform | string | the host | binary 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-preflight | boolean | off | skip 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-capture | boolean | off | static 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.
--target | Entry file | Command it execs | Output |
|---|---|---|---|
docker | docker/Dockerfile | docker build -f <root>/docker/Dockerfile -t <tag> <root> | one OCI image, ROLE selects behaviour |
binary | apps/web/server.ts | bun build --compile --minify [--target <platform>] --define ULTIMATE_FRAMEWORK_VERSION="<version>" <root>/apps/web/server.ts --outfile <out>[.exe] | a single executable |
static | apps/web/prerender.ts | bun 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.
| Field | Row | Meaning |
|---|---|---|
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:
reason | Cause | Is there an edit? |
|---|---|---|
surface-forbids-static | the surface cannot declare render: 'static' at all (app/) | no — move the route to site/, or serve it |
mode-revalidates | render: 'isr' regenerates on a tag or ttl, which a published file cannot | yes — render: 'static' |
mode-per-request | render: 'ssr' / 'stream' on a surface that does allow static | yes — render: 'static' |
no-prerender-paths | render: 'static' with dynamic params and no prerender(), so nothing enumerated | yes — 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 binarycompilesapps/web/server.tsas a launcher for the app tree it is started in:--compile-autoload-package-jsonand--compile-autoload-tsconfig, so the app's packages andpathsaliases resolve at run time, and--external '@ultimat3/*', so the framework is ONE instance — loaded from the app'snode_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, withnode_modulesinstalled 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-containedximage (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]| Flag | Type | Default | Meaning |
|---|---|---|---|
--image | string | ultimate-app:dev | image reference to deploy |
--method | string | compose | compose or helm |
--release | string | app.config.ts's name | helm release name; helm only |
--namespace | string | the kube context's | helm namespace; helm only |
--timeout | string | 15m | how long helm waits for the migrate hook and the rollout; helm only |
--dry-run | boolean | false | print 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]| Flag | Type | Default | Meaning |
|---|---|---|---|
--url | string | required | a bare http(s) origin; a path, query or credentials are refused |
--build | string | x.manifest.json's buildId | the id that must answer: BUILD_ID, or whatever CI stamped |
--window | string | 60s | how long consecutive good samples must span. 0: answered once |
--interval | string | 5s | the gap between samples |
--timeout | string | 10m | the whole wait, window and queues together |
--idle-ok | string | none | queues exempt while their depth (due + running) is 0: a scale-to-zero pool |
| Stage | Rule |
|---|---|
| sample | GET <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) |
| window | wait 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 |
| coverage | samples through the load balancer, reported as samples. Never a claim about each pod |
| queues | after 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_URL | queues.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.
| Env | Default | Meaning |
|---|---|---|
ROLE | web | one of web, sync, worker, scheduler, migrate, replicator. There is no all |
PORT | 3000 | empty or whitespace falls back to the default; anything else must be an integer in 0–65535 (0 is legal — an ephemeral port) |
HOST | 0.0.0.0 | the 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 |
| Failure | Code | Fix as printed |
|---|---|---|
ROLE names something else | X_ROLE_UNKNOWN | docker run -e ROLE=web <image> |
PORT is not a TCP port | X_PORT_INVALID | docker 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:
| File | Committed | Holds |
|---|---|---|
secrets.enc.json | yes | the sealed envelope: v, alg, the master key's id, the IV, the ciphertext |
.secrets.key | no — x secrets init writes the .gitignore rule before it writes the key | 64 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.
| Subcommand | Does |
|---|---|
show (default) | names, lengths, and whether envSchema declares each one; retired master keys by id (retiredKeyIds). Never a value, in either renderer |
init | a fresh master key, the .gitignore rule, and an empty sealed file. Refuses to overwrite either file |
edit | decrypt 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 |
rotate | reseal 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:
| Step | Command | State |
|---|---|---|
| 1 | x secrets rotate | new 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 |
| 2 | the app's re-seal backfill() | every value whose sealedKeyId() is a retired id is opened and sealed again |
| 3 | x 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.
| Opens | the app's database, as every other command does (DATABASE_URL, or the embedded one) — the framework's tables are applied first |
| Needs | the master key: ULTIMATE_SECRETS_KEY or .secrets.key. With neither it is X_SEAL_KEY_MISSING and no row is written |
| Idempotent | a sealed value is counted and never rewritten, so an interrupted run is finished by running it again |
| Safe under traffic | each 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 |
| Reaches | PostgresAuthAdapter'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.
| Subcommand | Does |
|---|---|
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 |
create | mints 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]| Flag | Type | Default | Meaning |
|---|---|---|---|
--check | boolean | false | fail if the committed files are stale instead of rewriting them |
--openapi / --no-openapi | boolean | true | also 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.
| Row | Surface | Last column (declared by) | --json keys |
|---|---|---|---|
| a page | site / app | its file | method (GET), path, surface, file, render, hydrate, offline, budget |
a mounted page — every screen defineAdmin() serves | site / app | the call and the permissions that gate it: defineAdmin() · admin:read + widgets:read | the page keys plus mount: { by, permissions }; a file route has no mount key |
| an action or query projection | api | the primitive, its name and its policy: action publishPost · posts:publish | method, path, surface, primitive, name, auth, policy |
a plain route — a runtime.ts one (an OAuth endpoint) or a declared webhookReceiver()'s (webhook.<name>, public) | api | runtime.ts routes <name>, then its policy | the 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]| Flag | Type | Default | Meaning |
|---|---|---|---|
--transport | string | stdio | stdio for an editor client, http for a socket |
--port | string | 9229 | HTTP 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.
| Tool | Does | Scope |
|---|---|---|
routes.list | the x routes --json table: pages (render mode, hydrate, offline, budget), then every action, query and runtime.ts route | dev:read |
schema.describe | entities with columns, types and invariants | dev:read |
policies.list | every policy: permission, subject, where it is enforced | dev:read |
actions.describe | actions and queries: schemas, policy, cache tags, MCP exposure | dev:read |
jobs.inspect | job definitions, retry policy and steps | dev:read |
queue.depth | pending, running and failed counts per queue | dev:read |
manifest.read | the generated x.manifest.json as text | dev:read |
errors.explain | X_* → cause, fix, docs | dev:read |
db.query | ONE read-only SQL statement; limit defaults to 100 rows, maximum 1000 | db:read |
db.migrate | apply pending migrations to a branch database | db:migrate |
tests.run | run the suite, structured results — an error outside any test (1 error beside 0 fail) or a non-zero exit is a failure, never failed: 0 | dev:test |
verify.run | run x verify, structured per-step result | dev:test |
logs.tail | last N log lines, optionally for one role | dev: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.shot | photograph one route at a named viewport, light or dark; PNG path plus verdict. Launches a browser | dev:test |
ui.island | photograph an island in every state its *.island.states.ts declares, as x shot --island does | dev:test |
ui.inspect | DOM, computed-style and accessibility facts for a bounded set of selectors, in one navigation | dev:test |
ui.interact | drive one route through a bounded step list, then photograph it; refuses a password field and any step that leaves the origin | dev:test |
ui.diff | pixel-compare two PNGs the other ui.* tools wrote under .x/shot/; launches no browser | dev: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:
| When | Codes | Effect |
|---|---|---|
boot — configuring an MCP surface with defineAppMcp | X_MCP_TOOL_UNDECLARED, X_MCP_TOOL_UNSAFE, X_MCP_TOOL_DUPLICATE | the call throws; no server starts |
| runtime — one request | X_MCP_TOOL_UNKNOWN, X_INPUT_INVALID (an isError result), X_MCP_SCOPE_DENIED, X_MCP_QUERY_REJECTED, X_MCP_NOT_BRANCH_DB, X_MCP_PROTOCOL | that 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).
| Listing | One line each | --json |
|---|---|---|
| missing | x 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 deliberate | data.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.
| Finding | Condition | Fix |
|---|---|---|
X_SEAL_KEY_MISSING | sealed columns, and no usable master key in this environment | x secrets init, or export ULTIMATE_SECRETS_KEY |
X_SEAL_RESEAL_PENDING | sealed columns, and a retired key still in ULTIMATE_SECRETS_RETIRED_KEYS — a rotation whose re-seal is not finished | a 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.
| Finding | Condition | Fix |
|---|---|---|
X_MFA_SECRET_UNSEALED | x_users rows hold a second-factor secret that is not sealed; each is refused at the second factor until it is | x auth seal-mfa --json |
X_FRAMEWORK_TABLE_ORPHANED | x_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 using | psql "$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]| Subcommand | Does |
|---|---|
list | queue 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 |
drain | planned, 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" --jsonRuns 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]| Subcommand | Does |
|---|---|
list | every 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 field | Is |
|---|---|
nextMs · next | the next occurrence: epoch ms, and ISO-8601 in the task's zone |
lastMs · last | the occurrence the scheduler last dispatched — what it was scheduled for, same two renderings. null (- in the table) for a task that has never fired |
lastFiredAtMs | when 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?
| Step | What happens |
|---|---|
| database | a 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 |
| rows | a fresh pair of tenants per primitive, every table seeded off its declaration (x db seed --generate's graph) |
| actor | the first of x policy explain's actors the primitive's policy allows, in its own tenant |
| input | the override, then an id field whose stem names a seeded entity (postId → posts), then the minimal draw of its schema |
| room | network 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.
| Drill | Fault | Must hold |
|---|---|---|
pg-blackhole | live sockets unanswered, server healthy | requests-coded, requests-bounded, recovers, jobs-once |
pg-slow | every byte delayed half the deadline | requests-coded, requests-bounded, recovers |
pg-restart | reset, then refuse, then pass | requests-coded, readyz-flips, recovers, jobs-once |
redis-down | the cache tier blackholed | requests-coded, requests-prompt, requests-ok, recovers |
nats-down | the bus refused | requests-coded, recovers, sync-resumes |
worker-sigkill | SIGKILL the worker, then a fresh one | jobs-terminal, jobs-once, requests-coded |
worker-sigterm | SIGTERM the worker, then a fresh one | role-exits, jobs-terminal, jobs-once |
web-sigterm | SIGTERM the web role under load | requests-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]| Subcommand | Does |
|---|---|
list | every 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 requestThe 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]| Subcommand | Does |
|---|---|
check | scan 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.
| Flag | Type | Default | Meaning |
|---|---|---|---|
| (positional) | mail id | — | the defineMail({ id }) to render |
--locale | string | every routed locale | one locale |
--tz | IANA zone | UTC | every date in the body; never the machine's zone (X_TIMEZONE_INVALID) |
--out | string | — | 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>]| Flag | Type | Default | Meaning |
|---|---|---|---|
| (positional) | test type | every type | one of the six; a test's type is its filename suffix |
--filter | string | — | 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-empty | boolean | off | a 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 |
--sample | string | — | run at most N files of the selection, deterministically. A fast signal for the eval loop — never a gate |
--workers | string | one 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 |
--affected | boolean | off | narrow 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 |
--base | string | origin/HEAD's branch | the 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 |
--dirty | boolean | off | union the working tree in — uncommitted and untracked. Needs --affected |
--seed | string | — | 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.
| Step | What | Refused with |
|---|---|---|
| 1 | clean tree, git is the only state between runs | X_UPGRADE_DIRTY_TREE |
| 2 | read every workspace's @ultimat3/* pins, overrides and resolutions included | X_UPGRADE_PIN_UNREADABLE, X_UPGRADE_MIXED_PINS |
| 3 | bun pm view @ultimat3/cli versions: the next major's newest; the last major's newest when no later major exists | X_UPGRADE_REGISTRY_UNREACHABLE, X_UPGRADE_HOP_TOO_WIDE, X_UPGRADE_VERSION_UNPUBLISHED, X_BUN_VERSION |
| 4 | write exact pins, raise the Bun floor | — |
| 5 | bun install | X_UPGRADE_INSTALL_FAILED, fix: carries the undo |
| 6 | the installed CLI: x manifest, x doctor --json, x exercise --json, x verify --json | X_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 field | Meaning |
|---|---|
from, to | the lowest version pinned; this hop's target |
next | what a bare x upgrade takes after this hop; null when the walk is over |
done | verify's verdict, and the exit code follows it. true when already there, false on a dry run |
dryRun | nothing written, no gate run |
app | whether the root holds app.config.ts |
proof | what 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 |
agentsMd | link: 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? } |
then | the 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 site | Edit |
|---|---|
engines.bun, packageManager, .bun-version | raised 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 YAML | same, quotes kept. An expression (${{ env.BUN_VERSION }}) or an x-range is listed in manual |
oven/bun: tag | keeps 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]| Flag | Type | Default | Meaning |
|---|---|---|---|
--base | string | origin/HEAD's branch | the 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 |
--dirty | boolean | off | union the working tree in: uncommitted changes plus untracked files |
--paths | boolean | off | stdout 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:
| Change | Affected |
|---|---|
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.json | that workspace only |
a .md | nothing. 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:
| Provenance | Source | Edges |
|---|---|---|
declared | x.manifest.json's facts, from the registries | requires (permission), invalidates / reads-tag / revalidates (cache tag), exposes (MCP tool), publishes (OpenAPI operation), references (entity), enqueues (task → job), reads (channel → entity) |
import | the module graph: every app file's imports, type-only included; the test preload is an import of every test | imports, declared-in (primitive → its file, matched by identity against the registries) |
observed | .x/exercise.json, when newer than x.manifest.json | writes / 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.
| Subcommand | Reads | Writes | The client |
|---|---|---|---|
vendor <name> | --from (a file, or an https URL read through outbound({ dial: 'public' })); omitted, the committed source.json | apps/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) |
customer | the 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 --package | createClient({ 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) |
| Flag | Meaning |
|---|---|
--from | vendor 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 |
--only | keep 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 |
--out | customer only, required |
--mount | customer only: a declared bearer mount's prefix |
--package | customer only: a package.json named so; publishing is yours |
| Failure | Code |
|---|---|
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.1 | X_CLIENT_SPEC_INVALID |
| a vendor answer that fails its declared shape | X_OUTBOUND_BODY_INVALID (core's), the field in meta.issues |
| a documented vendor non-2xx | X_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.
| Flag | Default | Meaning |
|---|---|---|
<route> | required | a 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 |
--port | 0 | the 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 |
--full | on (--no-full for the fold) | a fold-sized viewport crops nearly every route |
--settle | 2000 | matches the hydration runtime's own requestIdleCallback timeout |
--timeout | 30000 | one navigation |
--browser | PUPPETEER_EXECUTABLE_PATH, then CHROME_PATH | refused before anything boots if the path does not exist |
--cdp-url | SCRAPE_CDP_URL | attach to a browser somebody else is running instead of launching one here. ws:/wss:/http:/https:; anything else is refused before the attach |
--allow-hosts | the app's host only | extra hosts the page may request |
--cookie | none | name=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-status | absent — any 2xx | the 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 |
--locale | the app's defaultLocale, always pinned | one 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 |
--matrix | off | every 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 |
--viewport | absent — 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 |
--theme | absent — the box's own preference and the app's own theme.defaultMode | light 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 --jsonThree 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.
| Flag | Default | Meaning |
|---|---|---|
--island <name> | required | the 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 state | one state. An id the manifest does not declare is refused by name, with the known ids |
--settle | 2000 | the 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-islands | off | every 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 carries | Why |
|---|---|
| the counts, and the commands that re-run everything / one island / one state | an artifact that cannot be regenerated is one nobody trusts twice |
| what the capture could not see | the verdict's own blind list, handed in rather than reworded — two wordings would drift |
per island: the app-relative path of the .island.tsx | so a reader can open the component the pictures are of |
per state: the id, the title and the note | the 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 verdict | ok, 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 box | recorded, 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:
| Hazard | What x pr review does |
|---|---|
A review decision goes stale. It survives later pushes, so CHANGES_REQUESTED can predate the commits that addressed it | compares 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: null | selects 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.
| Subject | Read from | Written as |
|---|---|---|
a dead job (x_jobs id) | its row and its steps — capture off or on | repro/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.
| Reads | Topic |
|---|---|
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 codes | wiki.<Page>, wiki.<Page>#<anchor>, the anchor GitHub renders |
an X_* code | redirected 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.
| Command | Purpose | fix: today |
|---|---|---|
x cache [graph|bust <tag>|clear|stats] | what a write evicts; targeted eviction | x dev — then the cache panel at /_x |
x branch [<name>|delete <name>] | copy-on-write database + preview URL + scoped MCP socket | x db branch list --json — the database half: list, create <name>, delete <name> |
x status | role health and the build-ID distribution of connected clients | x doctor --json |
x logs tail | structured logs and spans, filterable | x dev — then the timeline panel at /_x |
x token [create --scopes <s>|grant <scope>] | MCP tokens and scopes | x mcp serve --help |
x ai [eval <name>|cache|reindex] | eval scores, cache hit rate and tokens saved, vector reindex | x test eval --json |
x money add-currency <ISO> --exponent <n> | extend the currency table | x 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 show | the resolved configuration, defaults included | x 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 docs | Use instead |
|---|---|
x db apply | x db migrate |
x gen <kind> | x g <kind> (or x generate) |
x deploy compose / x deploy static | x deploy --method compose / x build --target static |
x mcp (bare) | x mcp serve |
x routes list | x routes |
Related: Getting started · Configuration · Testing · Deployment · Troubleshooting.