# @bitmagic/cli

Build Bitmagic games from your own agent tool (Claude Code, Codex, OpenCode, ...) instead of the
web Creator UI. `bitmagic init` scaffolds a project with the engine vendored in as source, your
agent edits `src/work/`, and `bitmagic verify` / `bitmagic publish` take it to bitmagic.ai — no
web project or per-session pod required.

## Who can use it

The CLI requires an **active Bitmagic Pro subscription**. There is no other way in — an Auth0 role
used to work and no longer does.

You do not have to go and find a signup page: `bitmagic login` checks as soon as you have
authenticated and, if you are not subscribed, opens Stripe Checkout in the browser you are already
in and waits for it to complete.

```
$ bitmagic login
Opened https://…/activate?user_code=KLMN-PQRS
Code:   KLMN-PQRS
Waiting for approval…
Logged in to dev as you@example.com.

The Bitmagic CLI needs a Bitmagic Pro subscription.
Pro gives you a weekly and a 5-hour spark allowance that refill on their own,
and pays for generating assets, cover art and worlds.

Opening checkout in your browser:
  https://checkout.stripe.com/c/pay/cs_…

Waiting for your subscription… (Ctrl-C is safe — run `bitmagic subscribe` to resume)

Bitmagic Pro is active. You are ready — try `bitmagic init`.
```

Ctrl-C at any point is safe; your login is already stored. `bitmagic subscribe` picks up where you
left off, and `bitmagic whoami` says whether Pro is active and when it renews.

The spark allowance refills on a rolling basis — a weekly budget and a 5-hour one — and is what pays
for `bitmagic generate`, `cover` and `forge`. When an allowance runs out, generation stops there
unless you have allowed your purchased spark balance to take over: run `bitmagic allowance
--allow-sparks` once and it does from then on. `bitmagic allowance` shows both windows and when they
reset; `bitmagic usage` shows where the sparks went.

## Install

```bash
npm install -g @bitmagic/cli
bitmagic --version
```

That is the only time you need npm. From then on, `bitmagic self-update` updates the CLI — see
[Staying current](#staying-current) for why the npm line is the wrong tool for the second install.

### The dev line

```bash
npm install -g @bitmagic/cli@dev
```

installs the unreleased build the Bitmagic team and external testers run — a prerelease such as
`0.1.46-dev.2`, cut from the same `main` the release comes from. It exists because new
`/api/cli/v1` endpoints reach the **dev** api-server (`https://beta.portal.bitmagic.cloud`) before
production, so a CLI that uses them has to be installable before it is released. Pair it with
`bitmagic login --env dev`.

`bitmagic self-update --tag latest` puts you back on the release at any time, and `--tag dev` takes
you the other way. One global install is a single `bitmagic` on your PATH, so `bitmagic --version`
is how you tell which line you are on: a `-dev.N` suffix means the dev line. To have both at once,
see below.

### Running both lines side by side

Install each line into its own root, and let the directory you are standing in decide which one
`bitmagic` resolves to. Nothing here is Bitmagic-specific — it is two ordinary
[direnv](https://direnv.net) files over two ordinary pnpm installs.

```bash
pnpm add -g --global-dir ~/.bitmagic-cli/prod --global-bin-dir ~/.bitmagic-cli/prod/bin @bitmagic/cli
pnpm add -g --global-dir ~/.bitmagic-cli/dev  --global-bin-dir ~/.bitmagic-cli/dev/bin  @bitmagic/cli@dev
```

pnpm refuses a global bin directory that is not already on your `PATH`, so add both to your shell
profile once (they are only ever *prepended* per-directory below, so the order here does not
matter). Then sort your projects into two folders, and give each one **two files that do two
different jobs**:

```bash
# prod-games/.envrc          — which BUILD runs
PATH_add "$HOME/.bitmagic-cli/prod/bin"

# prod-games/.bitmagic-env   — which SERVER it talks to
env=prod
```

```bash
# dev-games/.envrc
PATH_add "$HOME/.bitmagic-cli/dev/bin"

# dev-games/.bitmagic-env
env=dev
```

The split is not arbitrary. `PATH` is a shell concept, so choosing the binary needs direnv and
works only where direnv runs. The **environment** is resolved by the CLI itself, reading
`.bitmagic-env` off the disk — so it is right in your terminal, in a script, in CI, and in a coding
agent's non-interactive shell, which sources no profile and fires no direnv hook. That last case is
why the environment moved out of `.envrc`: an agent asked to create a game in `prod-games/` used to
get whatever environment you last logged in to, and mint the game on the wrong server.

`.bitmagic-env` also takes an optional `apiUrl`, which is the only way to point `local` at a
checkout on shifted ports — `local` otherwise assumes the defaults:

```bash
# local-games/.bitmagic-env
env=local
apiUrl=http://localhost:3102
```

`bitmagic self-update` works inside this setup unchanged: it reinstalls into the root the
`bitmagic` you just ran came from, with the same `--global-dir` and `--global-bin-dir`, so updating
from `dev-games/` cannot disturb the prod root. (Running `pnpm add -g` by hand would, unless you
repeat both flags.) It finds a `pnpm` to run on disk rather than on `PATH` — neither root has one
beside it, and both flags pin where the install lands, so any `pnpm` is the right one.

`direnv allow` each, and both halves are right in each tree:

```
$ cd prod-games        && bitmagic --version   →  0.1.50
                          bitmagic whoami      →  env: prod (from .bitmagic-env in this folder or a parent)
$ cd dev-games/my-game && bitmagic --version   →  0.1.51-dev.0
                          bitmagic whoami      →  env: dev (from .bitmagic-env in this folder or a parent)
```

`--version` alone is not the check. Right after a release the `dev` tag can briefly name the same
build as `latest`, and two roots holding one version look exactly like direnv not firing —
`command -v bitmagic` and `whoami`'s source line are what actually distinguish them.

Both files are inherited by every project beneath the folder, so you do not need a copy per game.
They disagree about one thing, deliberately: `.bitmagic-env` **loses** to a project's own
`bitmagic.json` pin, so a `prod` project sitting under `dev-games/` still talks to prod and only
its *binary* is wrong — which the CLI then tells you about. Sort by environment and the question
never comes up.

The command keeps its name in both trees, which matters: `bitmagic init` writes bare `bitmagic dev`
and `bitmagic reload` into the project's `AGENTS.md`, so a `bitmagic-dev` alias would leave your
coding agent running the wrong build.

Four things worth knowing:

- **Your logins already coexist.** Credentials live in one `~/.bitmagic/credentials.json` keyed per
  environment, so a `bitmagic login --env dev` and a prod login sit side by side — they can even be
  different accounts. `bitmagic login` needs no `--env` in either folder, and a login that took its
  environment from the folder does **not** become your machine-wide default: like a project pin,
  a folder marker says "everything under here is prod", not "point this whole machine at prod".
- **The CLI catches a failed switch.** Run a dev build inside a project pinned to `prod` and it says
  so before the command, naming the right line. It suggests `npm i -g` because that is the fix for a
  single-install machine — with this setup, read it instead as "direnv did not fire, or you are in
  the wrong folder".
- **The update nudge goes quiet for one run after you switch.** Both installs share one
  `~/.bitmagic/update-check.json`, which records a single line's answer, so the first run after
  crossing over refetches in the background and says nothing. It is back the run after.
- **Each root is its own ~28 MB.** Installs are still fast — pnpm serves both from your shared store
  rather than re-downloading — but the two roots do not share files with each other.

## Quick start

```bash
bitmagic login             # device-code auth in your browser
bitmagic init my-game      # scaffold a new project into ./my-game
cd my-game
bitmagic dev &              # FIRST, and in the background — it never exits
                            # then open http://localhost:3011/ and leave it open
bitmagic cover              # generate cover art from GAME-DESIGN.md
bitmagic check              # typecheck against the vendored engine
bitmagic verify             # boot the game in a real browser and report what broke
bitmagic publish            # verify-gate, build, and ship — private by default
```

**Start `bitmagic dev` first, not last.** It is a long-running server, so an agent working through
a list tends to leave it until the end — and then nobody saw the game get built. Started first, the
browser reloads itself on every change and the assets panel shows each generation as it runs.

## Commands

| Command | What it does |
|---|---|
| `bitmagic login [--env <env>]` | Device-code auth against Auth0; stores credentials per environment. |
| `bitmagic subscribe [--no-wait]` | Start the Bitmagic Pro subscription the CLI requires; opens checkout and waits for it. |
| `bitmagic usage [--days N]` | Where this account's sparks went, and what Pro allowance is left. |
| `bitmagic allowance [--allow-sparks \| --block-sparks]` | The two Pro allowance windows, and whether your purchased sparks may fund work once they run out. `--allow-sparks` needs a real terminal — it authorises spending your money, so an agent cannot run it for you. |
| `bitmagic logout [--env <env>]` | Remove stored credentials for an environment. |
| `bitmagic reset-account [--env <env>] [--yes] [--keep-cli]` | **Deletes your account** on `dev` or `local` so the first-run experience can be tested again — games, sparks, profile, terms acceptance and Bitmagic Pro (cancelled in Stripe first). Then clears that environment's stored credentials **and uninstalls this CLI**, so the next run starts where a new creator starts: `npm install -g @bitmagic/cli`. `--keep-cli` skips that last step. Refuses `prod`, where the endpoint is not deployed at all. Asks you to type the environment name; `--yes` skips that and is required in a shell with no terminal. Your project directory is left alone — its `gameId` no longer exists, so start the fresh run in a new one. |
| `bitmagic whoami [--env <env>]` | Show the identity the CLI is authenticated as — the account's email address alongside its user id — and which environment answered, including where that environment was chosen from. |
| `bitmagic init [dir] [--template <id>] [--name <name>] [--idea "<pitch>"] [--art-style <voxel\|low-poly>] [--env <env>] [--force]` | Scaffold a new project: mints a game, downloads the vendored engine, writes `AGENTS.md`, `GAME-DESIGN.md` and templates. Starts from `empty-3d` unless told otherwise. `--idea` seeds the design doc's pitch **and** picks the template whose camera and controls fit it — "a doom-like corridor shooter" scaffolds `first-person`, "jump between floating islands" scaffolds `sidescroller`. `--template` overrides both; a suggestion that fails for any reason falls back to `empty-3d` rather than failing the scaffold. Dependencies are installed with **pnpm** when your machine has it and npm otherwise — the lockfile that produces is what every later command reads, so it never tells you to run the wrong one. |
| `bitmagic import <export.zip> [dir] [--name <name>] [--new-game] [--no-rehost] [--env <env>] [--force]` | Convert a game exported from the **web Creator** into a CLI project. Takes the `.zip` the Creator's "Export game" button downloads, writes its `source/` into `src/work/`, vendors the current engine around it, and produces the same project `init` does. **Keeps the game's id**, so `publish` updates the game you already have and every asset URL keeps resolving; `--new-game` forks instead, minting a fresh game and rewriting the id through `game.json` and `world.json`. Assets belonging to a **different** environment than the project are copied onto this one and the references rewritten, so a dev game imported to prod serves its own assets (`--no-rehost` leaves them). Defaults the directory to `./<gameId>`. See **Importing a Creator game** below. |
| `bitmagic check` | Typecheck (`tsc --noEmit`) against the vendored engine. Fast; does not prove the game runs. |
| `bitmagic dev [--port <port>] [--editor-port <port>] [--no-open] [--mobile] [--mobile-http]` | Build, then serve everything and keep it running: `tsc --watch`, `vite`, and a **Game / Editor** view that reloads itself when the project changes, and shows where the game is published (with a QR code) once it has been. Every project on the machine gets its own fixed pair of ports and keeps them — 3010/3011 for the first, 3020/3021 for the next, and so on (see **One project, one set of ports** below). Opens the view in your browser, unless the tab from the previous run is still there, in which case that tab reloads itself instead; `--no-open` (or `BITMAGIC_NO_OPEN=1`) skips opening altogether. `--mobile` also serves the game to phones on your network, over https, and prints a QR code for that. The one command to leave open. See **The dev view** below. |
| `bitmagic reload [--port <port>]` | Tell a running `bitmagic dev` to reload the browser now. For agents: run it when you finish a round of edits. Exits 0 and does nothing when no dev server is running. Reaches only this project's own server — never another project's, whatever port that happens to be on. |
| `bitmagic verify [--timeout <ms>] [--renderer webgpu\|webgl] [--platform desktop\|mobile\|both] [--fast] [--watch]` | Build, boot the game in a headless browser, start the game the way a player would, and report what broke. That means clicking through whatever the pre-play screen asks for — a level chooser or any other `hud.startScreen.selections` step is answered with its first option, the same default the engine takes when a flow skips it — and then pressing the Play button. A game without the button passes as long as gameplay starts by itself (auto-starting genres, custom start UIs); a run where gameplay never starts fails. The settle is adaptive: the run ends as soon as the live engine proves stable gameplay (state, rendered frames, no fresh errors, player standing), with `--timeout` as the cap. The verdict includes live engine state — actual GPU backend, fps, player position, physics body count, gameplay events (see **Gameplay events** below) — recorded in `result.json`. `--renderer` pins the pipeline (default `webgpu`, what players run; a silent WebGPU→WebGL2 fallback **fails** the run, and `--renderer webgl` is the deliberate escape hatch). A run failing on a lost WebGL context retries once on software rendering automatically. `--fast` skips the screenshot pipeline (then there is no thumbnail candidate from this run). `--watch` keeps everything warm — incremental `tsc --watch`, one vite, one browser — and re-verifies on file change in seconds; `--fast` is the watch default (`--no-fast` restores the screenshot). `--platform` picks what the run emulates — see **Verifying the mobile path** below. Only one verify drives a browser at a time on a machine: a second one, from any project, waits its turn and says whom it is waiting for (see **One verify at a time** below). Every run also reports the engine's mobile-parity check: a desktop action with no touch button is a warning, and a **failure** when `game.json` declares `primaryPlatform: "mobile"`. Writes the artifacts the publish gate reads (see below). |
| `bitmagic build [--single-file]` | Bundle the game into a single `.bitmagic/build/index.html`. The engine, your game and its data are inlined; three.js, Rapier and the other third-party packages load from a CDN at the versions your import map pins — the same shape a game published from bitmagic.ai has. `--single-file` inlines those too, for a game that has to run with no network (offline, `file://`, a packaged shell); the file is several megabytes bigger. Rarely needs to be run by hand — `publish` builds automatically when the project changed. |
| `bitmagic publish [--visibility public\|private] [--name <name>] [--description <desc>] [--force] [--single-file] [--version-url] [--no-qr]` | Verify-gate, build if needed, and upload straight to GCS. `--single-file` publishes the offline bundle described under `bitmagic build`; switching the flag between runs always rebuilds, since it changes nothing on disk for the reuse check to notice. **Keeps the game's current visibility**; a new game is private until published with `--visibility public`. Prints the link worth sharing: `bitmagic.ai/play/<ID>/` — the game's page, with its name, cover and comments — for a public game, and `bitmagic.ai/games/<ID>/` for a private one. Both always serve the newest publish, so a link handed out today keeps working after tomorrow's publish; `--version-url` also prints the URL of this one build (`.../indexN.html`), for pinning a bug report to it. Shows the shared URL as a QR code so you can scan it onto a phone — drawn in the terminal, or written to `.bitmagic/publish-qr.png` when it cannot be. `--no-qr` skips both. |
| `bitmagic self-update [--tag latest\|dev]` | Update the **CLI itself** to the newest build of its line, installing into the place this CLI actually lives rather than wherever the `npm` on your `PATH` would put it — which under nvm is routinely somewhere else. Reads the project's line from `bitmagic.json` when there is one, so it also fixes a wrong-line install; `--tag` overrides. Verifies the version on disk moved, and refuses (naming the path) for an `npx` run, a source checkout, or an install shape it cannot recognise. Not to be confused with `upgrade`, which is about the project's engine. |
| `bitmagic upgrade [--engine <version>] [--force]` | Refresh vendored platform files and the CLI's own skills to the currently published engine. Refuses a dirty git tree unless `--force`. Also records the project's `environment` in `bitmagic.json` if it has none yet — the engine it just vendored came from that api-server — and says so when it does. |
| `bitmagic generate <skybox\|background\|block-type\|sound\|music\|image\|video\|character\|animation\|model\|prop\|vehicle> ...` | Generate one asset directly into `src/work/world.json`. Costs sparks. `model` mints a **new** 3D object from a prompt, `prop` upgrades an existing placeholder **in place** (both produce voxels directly; `--mesh` takes the older mesh path; a low-poly project keeps the mesh as a `glb` asset — see **Art style**), `vehicle` adds a new drivable one, `animation` takes a parameter spec rather than a prompt, `block-type` registers a custom voxel block, `video` makes a short clip for a cutscene (played with `engine.playVideo("<id>")`) — see **Cutscene clips** below. `skybox` and `background` write the **same** field: pick one. **`character` now lives in its own group** — `bitmagic character generate|search|add` — and `bitmagic generate character` is a deprecated alias of the first; see **Characters**. |
| `bitmagic character <generate\|search\|add> ...` | Forge a character, look through the ready-made ones, or add one. `generate` costs sparks and may substitute a close-enough stored body (`--fresh` always forges). `search` lists the shelf's closest matches with their scores — free, changes nothing. `add` puts a stored body in the game for free and never forges. See **Characters** below. |
| `bitmagic assets add <file> [--name <n>] [--asset-id <id>] [--keep-glb\|--voxelize] [--voxel-size <m>] [--max-voxel-size <m>] [--height <m>] [--hollow\|--solid] [--force]` | Upload **your own** file — `.glb`, `.vxl`, `.png/.jpg/.webp`, `.opus/.ogg/.m4a/.mp3`, `.json` — into the game's assets and write its entry into `src/work/world.json`. A `.glb` is voxelized in a headless browser (Chrome + a free port in 3000–3199, like `prop`) unless `--keep-glb` — or unless the project is low-poly (see **Art style**). Free — no sparks. See **Adding your own assets** below. |
| `bitmagic assets list` | The assets in `world.json`: id, name, type, size, placed instances, and total references. Local, no network. |
| `bitmagic assets materials <assetId> [--set <file.json>] [--auto] [--clear]` | Show what an asset's voxels look like, or say what they are **made of** — so a sword's steel blade shines and its leather grip does not. With no flag it prints the asset's colour groups; `--set` applies your verdicts; `--auto` lets a model decide (what `generate prop` and `generate model` already do on their own); `--clear` removes them. Free — no sparks, nothing generated. See **Voxel materials** below. |
| `bitmagic assets remove <assetId> [--force]` | Remove an asset from `world.json` (the uploaded file stays in storage). Refuses while anything in `world.json` still references it; `--force` removes the asset and its placed instances together. |
| `bitmagic cover [--prompt "..."] [--force] [--no-start-screen]` | Generate the game's cover art from `GAME-DESIGN.md`: sets the start screen, saves `.bitmagic/cover.webp`, becomes the publish thumbnail. Costs sparks. |
| `bitmagic reference <make\|accept\|show\|clear> ...` | Make a concept image of the game's world, iterate until it is right, and `accept` one — the accepted image becomes the default style reference for `generate skybox\|background\|block-type\|image`, `cover` and `forge`. Only `make` costs sparks (priced like an image). See **Reference image** below. |
| `bitmagic judge [--genre <name>] [--platform desktop\|mobile] [--min-score <1-10>]` | Grade the screenshot `bitmagic verify` captured against a quality rubric built from `GAME-DESIGN.md` (plus an optional genre hint) with a vision model, and print a scorecard: four dimension scores and at most three actionable findings, also written to `.bitmagic/judge.json`. Informs rather than gates — exits 0 whatever the score, unless `--min-score` sets a bar (exit 4 below it). `--platform` picks which frame to grade; with no flag it grades the mobile one when the game is mobile-primary and `screenshot.mobile.png` exists, and the mobile rubric folds touch-HUD legibility, thumb reach and phone framing into the existing `readability` and `polish` dimensions rather than adding a fifth score. Warns when the project changed since the screenshot. Costs sparks (a few per run — one call to a model that reasons about the frame before it scores). |
| `bitmagic trailer record [--seconds N] [--resolution WxH] [--flythrough <path.json>]` | Record gameplay for a trailer: opens Chrome on the running `bitmagic dev`, waits for Play, starts the engine's F9 recorder for you, stops after N seconds or your F9, and prints the recording dir (`.bitmagic/trailer/recordings/<name>/`). `--flythrough` records an authored camera path with nobody playing. |
| `bitmagic trailer make [--music-prompt "..." \| --music <file> \| --no-music] [--title <t>] [--seconds 30] [--ui] [--ui-keyframe-rate 30] [--publish] [--reset-shots]` | The one-shot: analyze the newest recording, lay a music bed (generated from a prompt — sparks — or your file), write a first cut to `trailer/shots.json` or cut the one you edited, render `<game>-trailer.mp4` (new `-rN` each run), and with `--publish` put it on the CDN and the game page. |
| `bitmagic trailer analyze \| beats \| cut \| render \| merge` | The steps of `make` as separate commands: score a recording into highlight windows with contact sheets; detect a track's beat grid; turn `trailer/shots.json` into a frame-exact `edl.json`; render an EDL (`--ui` redraws the HUD, `--ui-keyframe-rate` caps how often it may change, `--logo`, `--music …`); join recordings from separate sittings. |
| `bitmagic trailer publish <file.mp4> [--no-attach]` | Upload a trailer (`.mp4`/`.webm`, ≤ 200 MiB, header-checked so a renamed image is refused before upload) to the CDN under `game-media/<gameId>/` and put it at the front of the game page gallery. Re-running replaces the previous trailer (remembered in `.bitmagic/trailer/published.json`); the page is capped at 12 media items. Works before the first publish — the page goes live on the first `bitmagic publish --visibility public`. `--no-attach` uploads only. |
| `bitmagic forge --prompt "..." [--city\|--dungeon\|--platformer\|--freeform] [--name <name>] [--resume <jobId>]` | Design and bake a whole playable level into `world.json`. Costs sparks, and takes many minutes — the bake alone can take twenty. `--resume <jobId>` picks a failed run back up from the last step that finished, rather than redoing the whole thing. |
| `bitmagic forge --edit "..." [--level <id or name>]` | Change a detail of a level this project forged — a door style, the light fixtures, a prop — without re-forging it. The stored design is patched and only what the change touches is re-made; the level is replaced in place. See [Editing a forged level](#editing-a-forged-level). |
| `bitmagic tools <list\|path\|install> [bundle]` | The script bundles the CLI ships for skills that drive Blender, Python or Node (`blender-level`, `unity-import`, `metahuman`): what they are, where this install keeps them, and `install` to copy one into the project's `tools/`. Local, no network, no sparks. |
| `bitmagic levels <list\|add\|rename\|set-start\|remove> ...` | Manage the levels `forge` creates: which one boots, their names, registering a bake you already have, deleting one. Local, no network, no sparks. See **Managing levels** below. |

Every command exits non-zero on failure with a single-line message (no stack trace) and, where a
distinction is useful to an automated caller, a specific exit code — see **Exit codes** below.

## Generating an animation

Unlike every other generator, `animation` does not take a prompt — it takes an **authored
parameter spec** in one of three families, and the server builds the clip from it with
Bitmagic's own animation forger. No text-to-motion service is involved, which is why it is
deterministic and returns in about a second. It is also the cheapest generator — one spark
a clip — so re-running it while you tune the dials is the intended way to use it.

The gait family fits flags, so the common case needs no file:

```bash
bitmagic generate animation \
  --family gait --name LimpStride --motion-id mLimpStride01 \
  --duration 1.1 --based-on walk --design-speed 1.2 \
  --dial stance=0.66 --dial limpSide=right --dial limpDepth=0.7
```

`melee` (weapon strikes) and `oneShot` (waves, salutes, crouches, celebrations) are lists of
nested key poses, so they come from a file — or stdin:

```bash
bitmagic generate animation --spec clip.json
cat clip.json | bitmagic generate animation --spec -
```

Two things the spec will refuse, both before anything is sent:

- A **clip name containing idle/walk/run/jump**. The engine routes clips to locomotion states
  by substring, so `LimpWalk` would seize walking for every character in the game. Use
  `LimpStride`.
- A **dial outside its range**. The ranges are the ones the built-in clips were tuned within;
  past them the IK starts clamping and the feet skate.

`--motion-id` is the clip's own engine id (`m` + 4 or more letters/digits) and is what
`playCustomAnimation("mLimpStride01")` resolves. It is not the minted `asset_…` id — pass that
as `--asset-id` if you have one.

### From a video (`--video`)

The one exception to "no text-to-motion service": a clip can be made from a **video of a person
performing the move**, through Uthana's video-to-motion. The creator supplies the motion by doing
it, not by describing it, so it sits beside the authored forger rather than replacing it.

```bash
bitmagic generate animation --video ~/wave.mov --name "Wave"
bitmagic generate animation --video ~/jog.webm --name "Jog" --slot run --loop
```

- `.mp4`, `.mov`, `.avi` or `.webm`; **2–60 seconds**; under 200 MB. One person, whole body in
  frame, plain background, good light. Anything else is re-encoded server-side, so a browser's
  WebM recording is as good as a phone's mp4.
- `--slot idle|walk|run` pins the clip to that locomotion state for the player **and every NPC**
  (persisted as `locomotionState` on the asset — no template code to edit). Omit it for a one-shot
  clip and play it with `playCustomAnimation("<motionId>")`.
- It costs sparks (15, charged when Uthana accepts the video) and takes minutes: the command
  uploads the file, submits a job, and polls until the clip lands in `src/work/world.json`.
- `--auto-trim` cuts the "walking to the camera" frames off both ends before Uthana sees the
  clip: pose analysis finds where the person stepped back into frame after pressing record and
  where they came forward to press stop. Use it for clips recorded that way; when the analysis
  is not confident, nothing is trimmed. The result reports what was removed, in the log line and
  in `--json` (`trimmedStartSeconds`/`trimmedEndSeconds`, both absent when nothing was cut).
- `--trim-start <s> --trim-end <s>` (always together) keep exactly that window of the clip
  instead — the kept range must be at least 2 seconds. An explicit window replaces `--auto-trim`
  and cannot be combined with it. This is what the dev view's Record-motion panel sends after
  the creator confirms the trim on its handles.
- `--spec`/`--family` cannot be combined with `--video`.

`bitmagic dev` has the same flow behind its **Record motion** button — record with the webcam or
pick a file, name it, choose a slot. While you review the clip, the sidecar uploads it and runs
the trim analysis (free — no Uthana call), and the proposed cut appears on a trim bar with
draggable handles under the preview. Adjust it or press **Use full clip**, then **Make the
animation** submits the window you confirmed through this command's lane. Analysis failures never
block anything: the handles just stay at full range.

## Environments

`--env` accepts `dev`, `prod`, or `local`. Each command resolves its target in this order, most
specific first:

1. the `--env` flag, on the commands that take one;
2. the `BITMAGIC_ENV` environment variable — the override for the commands that do not
   (`dev`, `upgrade`, `forge`, `generate`, `publish`), and the one thing that outranks a project;
3. the `environment` field in the project's `bitmagic.json`;
4. the `env` in the nearest `.bitmagic-env`, walking up — the **folder** the project lives under;
5. the stored default, written by `bitmagic login`;
6. **`prod`** — the fixed default when nothing above has an opinion, whatever build is running.

Step 4 is for sorting projects into folders by environment — see **Running both lines side by
side** below. Its rank is the point of it, in both directions. It sits *below* the project's own
pin, so a `prod` project dropped into a `dev` folder stays `prod` — the folder never re-aims a
checkout that already declares itself, which is exactly what `BITMAGIC_ENV` does do. And it sits
*above* the stored default, so `bitmagic init` — the one moment there is no pin yet, because the
project does not exist — takes the folder's answer instead of whatever you last logged in to.

Unlike steps 1 and 2 it needs no shell involvement, which is the reason it exists: a `direnv`
`.envrc` exporting `BITMAGIC_ENV` is invisible to a coding agent, whose commands run through a
non-interactive shell that sources no profile and fires no hook.

Step 3 is what makes a checkout self-describing: `bitmagic init` records which api-server minted
the project's `gameId` and served the engine pinned beside it, so every later command in that
directory targets the same one no matter what you last logged in to. `bitmagic upgrade` backfills
the field on projects created before it existed. Because the file is committed, cloning a project
gets you its environment too — which also means a project created against `local` points a
teammate at *their* localhost.

`bitmagic whoami` prints the resolved environment and where it came from, and `bitmagic dev` prints
it at startup. A pinned project ignores the stored default, so `bitmagic login` inside one logs in
to the project's environment *without* changing your machine-wide default. `BITMAGIC_API_URL`
overrides the URL alone, keeping the environment name and its Auth0 tenant.

Step 6 used to ask the CLI about itself — a `-dev.N` build defaulted to `dev`, a release build to
`prod` — so that each published line reached the api-server it was developed against without a
flag. `.bitmagic-env` now carries that intent explicitly, and explicit beats inferred: the lane rule
made one command mean different things on two machines, over a version string nobody was reading.

So the last word is simply `prod`, the environment a creator who has said nothing wants. The two
lines still pair with different api-servers — `@bitmagic/cli@dev` exists to exercise endpoints that
reach **dev** first, and running it against an environment that has not caught up fails on
endpoints that are not there yet — but that pairing is now something you state, once, in the folder
you keep those projects in. **Give every non-prod tree a `.bitmagic-env`**; anything without one is
talking to production.

**Inside a project, the pin decides the line**, and nobody should be asked which one they want:
`dev` and `local` projects take `@bitmagic/cli@dev`, `prod` projects take the plain
`@bitmagic/cli`. Run a command in a project on the other line and the CLI opens with a notice
naming the install to switch — and prints only that one, in place of the usual "a newer CLI is
available" nudge, so there is never a choice of two install commands. The AGENTS.md the CLI
scaffolds carries the same rule with the project's own command already filled in, which is what
keeps an agent from putting the question to you.

## Importing a Creator game

A game built in the web Creator becomes a CLI project in one command:

```
bitmagic import ~/Downloads/ZW22NSTB96OQ.zip
```

The zip is the one the Creator's **Export game** button downloads. What comes out is an ordinary
project — the layout below, `bitmagic dev`, `check`, `generate`, `forge` and `publish` all work on it
with nothing import-specific anywhere. Three things about the conversion are worth knowing.

**The game id is kept.** That is the point of the command: the exported id is the row that owns the
game's assets, its published page, its plays and its likes. Keeping it means `bitmagic publish`
updates the game you already have rather than standing up a second copy beside it. `--new-game` is
there for the other intention — a fork, a variant, something to ship separately — and rewrites the
id through `game.json` and `world.json`. Assets already in `world.json` still load from the original
game's storage after a fork; they are public URLs and keep working, and only newly generated assets
land under the new id.

**The id only exists on the api-server that minted it.** A game exported from bitmagic.ai is a prod
game; one exported from the dev Creator is a dev game. Importing one into a project pinned to the
other produces something that builds, runs, and then fails every server command with "not found or
is not owned by the caller". `import` reads the export's own asset URLs, works out which server the
game belongs to, and refuses the mismatch by name rather than letting you find out later. `--env`
picks the environment; `--new-game` sidesteps the question entirely, since a fork adopts no id.

**Crossing environments moves the assets too.** A game's assets are absolute URLs on the bucket that
minted them, and a project on the other environment inheriting those URLs is not survivable: the
origin bucket's CORS policy does not allow the new environment's origin, so the game loads its own
assets in development (localhost is allowed by both) and then fails for every player once published.
So a cross-environment import downloads each foreign asset, re-uploads it under this project's game,
and rewrites every reference — the slow step, several minutes for a large game, and it says so as it
goes. Shared defaults on hosts we do not control (the ImageKit skybox, a URL of your own) are left
alone. An asset that cannot be copied keeps its original URL and is **named** in the output rather
than dropped; it will still fail for players, so fix or replace it before publishing. `--no-rehost`
skips the whole pass, which is only ever right for a throwaway look at a game you will not ship.

**The engine moves forward.** The Creator freezes a game against the engine it was last edited with;
the project vendors whatever is published today. `import` names both versions when they differ. Run
`bitmagic check` first — anything the engine renamed in between shows up there as a compile error,
and there is no migration lane in the pro lane to do it for you.

`GAME-DESIGN.md` and `mechanics-plan.md` move from the export's `source/` to the project root, which
is where `cover`, `judge` and `reference` read them. The Creator's own edit history, session logs and
LLM-usage records are in the zip too and are deliberately left there: nothing in a project reads
them.

## Project layout and artifacts

```
my-game/
  src/work/          # your game — Game.ts, game.json, world.json
  engine/             # vendored engine, read-only, replaced wholesale by `bitmagic upgrade`
    LICENSE.md            # the engine's licence — see Licensing below
    THIRD-PARTY-NOTICES.md # three.js, Rapier and the rest
  AGENTS.md            # guidance for the agent editing this project — read by Cursor, Codex and the rest
  CLAUDE.md            # just `@AGENTS.md`, because Claude Code does not read AGENTS.md itself
  GAME-DESIGN.md        # what the game is — yours to write; bitmagic cover reads it
  .agents/skills/       # the shipped skills — Cursor, Codex, Copilot, Cline, OpenCode read this
  .claude/
    settings.json          # yours; seeded with a Stop hook that runs `bitmagic reload`
    skills/                 # the same shipped skills, where Claude Code looks; your own are untouched
  .kiro/                 # only with `--agent kiro`: skills/ plus a steering file pointing at AGENTS.md
  trailer/
    shots.json            # the trailer's cut — which moments, how long, the end card; bitmagic trailer make re-cuts from it
  .bitmagic/
    cover.webp           # bitmagic cover's artwork — the publish thumbnail when present
    cover.json            # its URL and the design hash it was generated from
    reference/             # bitmagic reference's candidate images and their records
    reference.json          # the accepted reference image — absent until one is accepted
    AGENTS.md.latest       # the current AGENTS.md template, when upgrade kept your edited one
    dev.json                # the running `bitmagic dev`'s ports, so `bitmagic reload` finds it
    mobile-qr.png            # the phone URL as a QR code, written when --mobile cannot draw one
    jobs/                   # one file per generation in flight — what the assets panel reads
    trailer/
      recordings/<name>/    # F9 recordings: <name>_%06d.png at 60 fps + timeline.json + capture.mp4 (the footage as one file)
      <name>/               # that recording's analysis.json, sheets/, beats.json, music.mp3, edl.json and renders
      published.json        # the trailer's CDN URL after bitmagic trailer publish
    previews/                # rendered asset thumbnails, cached so they are drawn once
    videos/<assetId>.png     # 8 frames from a generated clip, tiled — what `generate video` leaves to look at
    build/
      index.html            # bitmagic build's output bundle
      manifest.json          # engineVersion, fingerprint, bytes, sha256, builtAt
    publish/
      source.zip            # the source archive each publish uploads (see Publishing)
    verify/
      result.json              # bitmagic verify's verdict — ok, failures, warnings, fingerprint, at, platform, renderer, settle, snapshot, mobileParity (and platforms[] after a --platform both run)
      screenshot.png            # the publish thumbnail when there is no cover art (not written by --fast runs)
      screenshot.mobile.png      # the same for a --platform mobile run
      console.log                # captured browser console output
      console.mobile.log          # the mobile run's console, kept separate so a --platform both run keeps both
      console.retry.log            # the software-GL retry's console, when a lost context forced one
```

### Gameplay events

Verify boots the game with `?eventlog=1`, which asks the engine to run a passive gameplay-event
session (`window.__bmDebug` — see the engine's `docs/debug-namespace.md`). The run's `result.json`
then carries an `events` block: counts by type (`player-death`, `pickup`, `match-end`, …), the
counts within the first 10 seconds of gameplay, when the match ended, and the last 50 raw events.
Two signals surface as **warnings** (never failures — verify provides no input, so only
spontaneous events are judged): the player dying 3+ times in the first 10 seconds of gameplay,
and the match ending within 5 seconds of starting. A vendored engine too old to know
`?eventlog=1` yields `events: null` and no output — run `bitmagic upgrade` to get the session.

### Verifying the mobile path

Most of what the engine does differently on a phone is decided by one branch — the touch
controls, a pixel-ratio clamp, a lower level-detail tier, a lighter light warmup, postFX and SSR
off, a rescaled HUD, fullscreen on start. `--platform mobile` runs that branch for real:
Playwright emulates an iPhone 14 (a phone viewport, touch input, device pixel ratio 3), the game
boots with the engine's own `?platform=mobile` override, and Play is **tapped** rather than
clicked. That is more than the web Creator's Mobile tab can do — it letterboxes an iframe and
moves a mouse pointer around, which never reaches the branch at all.

With no flag, the platform comes from the project: `mobile` when `src/work/game.json` declares
`primaryPlatform: "mobile"`, `desktop` otherwise. `--platform both` runs one after the other and
passes only if both do. A game whose `world.json` sets `worldProfileData.mobileOrientation:
"landscape"` is verified on its side.

The mobile run writes `screenshot.mobile.png`, which `bitmagic judge` grades by default for a
mobile-primary game and `bitmagic publish` uses as the thumbnail when there is no cover art.

**The parity gate.** The engine already checks, at every game start, whether every desktop key
has a touch button to match — and logs the gaps. `bitmagic verify` now reads that, on desktop
runs too, and names both the key and the system that registered it:

```
warning: These actions have no touch button, so phone players cannot reach them: KeyF(CombatSystem).
```

It is a **failure** rather than a warning when `game.json` says `primaryPlatform: "mobile"`: on a
game built for phones, an action a phone cannot reach is not a portability nicety. The fix is to
bind both sides in one call — `playerController.registerCustomAction({ action, desktop: { keys:
['KeyF'] }, mobile: { label: 'ATTACK', behavior: 'tap' } })` — never a raw `registerKeyHandler`.
The gaps are also in `result.json` as `mobileParity`.

## Cover art

`bitmagic cover` generates the game's cover from `GAME-DESIGN.md` — the same artwork the web
Creator's Planning Mode produces, from the same style direction. It sets `hud.startScreen.imageUrl`
so the image appears in the game itself, saves `.bitmagic/cover.webp`, and hands that to
`bitmagic publish` as the portal thumbnail.

Run it **early** — right after the design doc says what the game is, before implementing it. The
first build takes a while, and the cover is what there is to look at meanwhile. In iTerm2, WezTerm
and Konsole it is drawn straight into the terminal; everywhere else the path and URL are printed
and `.bitmagic/cover.webp` is there to open (`BITMAGIC_NO_INLINE_IMAGE=1` turns the drawing off).

Re-running on an unchanged design generates nothing and costs nothing — it reports the existing
cover. Edit the design and run again, or pass `--force`, to replace it. Because it writes
`world.json`, it invalidates a passing verify: run it before `bitmagic verify`, not after.

## Cutscene clips

`bitmagic generate video --prompt "the castle gates swing open, dust drifting"` makes a short 720p
clip, writes it into `world.json` as a `type: "video"` asset, and prints the `engine.playVideo("<id>")`
call that plays it as a fullscreen overlay. `--duration` sets the length (billed per second);
`--image` animates a still you already have as the first frame, which is much slower and takes the
clip's shape from the image rather than the 16:9 default.

Because a clip is paid for before anyone can tell whether it is any good, the command leaves you
something to look at: **8 frames spread across the clip, tiled into `.bitmagic/videos/<assetId>.png`**.
Open it to check the clip actually shows what was asked for, without downloading the mp4 and writing
an ffmpeg filter by hand. It needs `ffmpeg` on `PATH`; without it the command still succeeds and
says why there are no frames. `--no-contact-sheet` skips the download.

The same pass says **whether the clip carries an audio track** — today's generated clips are silent,
so a cutscene that needs sound needs a music bed under it, and that is invisible in the frames.

To watch it move rather than as stills, `bitmagic dev`'s assets panel plays it.

One rule when you wire it up: call `engine.playVideo()` **from the Play click or later**, never
before. Without a prior user gesture the browser blocks autoplay, and the engine resolves the call
immediately — so the cutscene silently does not run. `engine/agent-docs/video-cutscenes.md` covers
that and what to freeze while a cutscene plays.

## Spoken lines

`bitmagic generate speech --text "Halt, traveler. You shall not pass."` speaks a line, writes it
into `world.json` as an audio asset, and prints the `engine.playSound("<id>")` call that plays it.
Unlike a music bed it is registered as a **one-shot, not a loop** — dialogue plays when something
triggers it.

`--voice` picks who says it: `narrator` (the default), `male`, `female`, or a raw Eleven Labs
`voice_id` if you have cloned one of your own. `--model` switches model, and `--stability`,
`--similarity-boost`, `--style` and `--speed` are passed through to Eleven Labs when you want them;
leave them alone and Eleven Labs' own defaults apply.

It is **billed per second of audio produced**, not per character of text. A sentence costs about a
spark. The estimate charged up front comes from the length of the text; what you actually pay comes
from the finished audio.

Like `generate music`, it can write a file instead of a game asset:

```bash
bitmagic generate speech --text "Halt, traveler." --out line.mp3 --no-world
```

`--out` saves the MP3, `--no-world` leaves `world.json` untouched. That pair is the input to the
MetaHuman lip-sync tool (`tools/lipsync/make_line.py --audio line.mp3`), where a spoken line is
cut into mouth cues rather than played as-is. Use `--out` on its own to keep both.

## Reference image

When a game has a strong setting — a tropical island, a neon city — one accepted concept image can
keep everything generated for it looking like the same place. `bitmagic reference make` draws a
wide establishing shot of the world from `GAME-DESIGN.md` (or `--prompt`), saves it under
`.bitmagic/reference/`, and prints its path and URL; in iTerm2, WezTerm and Konsole it is drawn
straight into the terminal, like the cover. Look at it, then either iterate —

```bash
bitmagic reference make --from <id> --prompt "same island, but at dusk"
```

— which steers the next candidate by the previous image, or accept it:

```bash
bitmagic reference accept <id>
```

Accepting re-hosts the image to your game's own storage (so it outlives the generator's URL) and
records it in `.bitmagic/reference.json`. From then on `generate skybox`, `generate background`,
`generate block-type`, `generate image`, `bitmagic cover` and `bitmagic forge` use it as their
style reference by default — the forge's designer is shown the actual picture, then the forge
renders its design, has a vision critic compare it with the picture and revises the design toward
it, up to `--match-rounds` times (default 2; each round is a full design call, so expect a few more
minutes; `--match-rounds 0` designs once). Each round prints its score and the URLs of the renders
the critic compared, and the level's notes record the final match — and each says
"Using the project's reference image" when it does. `--no-reference` skips it for one call,
`--reference-image <file>` or `--reference-image-url <url>` substitutes another image for one
call, `bitmagic reference show` shows what is accepted, and `bitmagic reference clear` stops
using it. If `bitmagic dev` is open, the accepted image is also on screen there — click the game's
name in the top bar (see **The top bar** below).

**Your own picture works everywhere a generated one does.** "Create a world like this" with a
photo in hand is `bitmagic forge --prompt "..." --reference-image ./that-photo.jpg` — the file
(PNG, JPEG or WebP) is uploaded to your game's storage and used for that call only, ahead of any
accepted reference. The same flag is on the four image generators, `cover`, and
`reference make` (to draw candidates in your picture's style). And when the picture should be the
game's look outright, accept it directly: `bitmagic reference accept ./that-photo.jpg`.

Candidates cost sparks like any image generation; `accept`, `show` and `clear` are free.
`.bitmagic/` is not committed, so paste the accepted URL into `GAME-DESIGN.md` — on a fresh clone,
`bitmagic reference accept <url>` re-accepts the same image from that line.

## Making a trailer

`bitmagic trailer` takes gameplay to a published trailer, and your agent is the editor. With
`bitmagic dev` running:

```bash
bitmagic trailer record --seconds 60          # Chrome opens on the dev view; press Play and play well
bitmagic trailer make --music-prompt "driving synthwave, no vocals" --title "Joyride" --publish
```

`record` starts the engine's F9 recorder for you and stops it after the time is up (or when you
press F9); frames and a `timeline.json` of events, sounds and HUD state land in
`.bitmagic/trailer/recordings/<name>/`. `make` scores the timeline into highlight windows (with a
contact sheet per window under `.bitmagic/trailer/<name>/sheets/`), generates a music bed from the
prompt (`bitmagic generate music`, billed per second — `--music <file>` uses your own track,
`--no-music` skips it), writes a first cut to **`trailer/shots.json`**, snaps every cut to the beat,
renders `<game>-trailer.mp4` with the game's own sounds reconstructed under the music, and with
`--publish` uploads it and puts it first in your game page's gallery, printing the URL.

Tuning is editing `trailer/shots.json` — which moments (`in` is a source frame at 60 fps), how
long (`beats` with music, `seconds` without), in what order, with what end card — and running
`bitmagic trailer make` again: every render is a new `-rN` file, so nothing is lost. The shipped
`making-a-trailer` skill teaches your agent the craft (ordering, pacing, variety, what not to do)
and how to turn "faster", "more crashes" or "title at the end" into that edit. `--ui` redraws the
recorded HUD over the footage through the running dev view. No one to play? Author a camera path
in `trailer/flythrough.json` and `bitmagic trailer record --flythrough trailer/flythrough.json`
records a fly-through with nobody at the keyboard.

Needs `ffmpeg` and Google Chrome on this machine; each command says so if one is missing.

## The dev view

`bitmagic dev` is meant to be started once and left running, beside your agent:

```bash
bitmagic dev
#   Opened http://localhost:3011/   ← game + editor
#
#   Game only        http://localhost:3010/
#   Editor actions   .bitmagic/edit/events.jsonl
```

(A CLI installed from `@bitmagic/cli@dev` serves the same two pages on 3110 and 3111 instead, so the
two lines can run at once — see **Two installs, two lanes** below.)

It opens that page for you. Pass `--no-open`, or set `BITMAGIC_NO_OPEN=1`, if you would rather it
did not — on a machine with no browser it simply prints the URL instead.

It opens it **once**. Stop `bitmagic dev` and start it again — an agent starting a new session, a
restart to add `--mobile`, a background task that got killed — and the tab you already have finds
the new server by itself and reloads its own page; the banner then says `Reusing` instead of
`Opened`, and no second tab appears. Close the tab and the next start opens one again. (The
project keeps the same ports across restarts, which is what makes the old tab's address still the
right one — see **One project, one set of ports** below.)

The reason it opens rather than only printing: agents are told to start `bitmagic dev` as a
background task, which means everything it prints — including the URL — lands in a log the creator
never reads. A dev server nobody opened is worth exactly as much as one nobody started.

While the game boots, the mark hops over the stage — the first time only. Later reloads hold it
still, and only if the reload is slow enough to be worth explaining. If a boot fails it stays put,
says what went wrong and offers to try again, instead of leaving you a black rectangle.

### Where your game is published

Once the project has been published, **Published…** in the top bar's **⋯** menu opens the game's
address, the visibility it went out with, which publish this is and how long ago — and the same QR
code the terminal draws, so the phone in your hand can reach the build without you typing anything.

A dot appears on the **⋯** button within a second or two of a publish finishing, with no reload:
the page polls, and the publish it should show almost always happens in another terminal — often
your agent's, which you never look at. Opening the menu clears the dot, so it means *new* rather
than *exists*. Until the first publish the row is there but dead, because a control that opens an
empty dialog is worse than one that is plainly not ready.

It works even when the game will not boot, which is deliberate: that is the moment the link matters
most, since the last good build is still live at that address.

Put the page beside your agent's window. It is one page with
two tabs over the same running game, so switching costs nothing and does not restart anything:

- **Game** — your game, playing. Click into it to capture the mouse, exactly as a player would.
- **Editor** — the same world, paused, with a free-flying camera, the scene hierarchy and the
  transform gizmo. This is the editor the web Creator uses, which already ships inside `engine/`.
  Click an object to select it, then move, rotate, scale, snap it to the ground, edit its numbers in
  the inspector, delete it, or right-click to add one from the project's assets.

**Screenshot** in the top bar — or the **S** key — copies the current frame to your clipboard, so
you can paste it straight into your agent's chat — the fastest way to show it something that looks
wrong. It captures whichever tab you are on, so you can grab gameplay or the editor view. If your
browser refuses the clipboard write the image is saved to your downloads instead, and the status bar
says so.

**Reload** is the same, on **R**. Both keys are ignored while you are typing and while a dialog is
open, and they only reach the page when the chrome has focus: click into the game and the game gets
your keys, which is what you want — press F9 there and the recorder starts.

**Record gameplay…** in the **⋯** menu holds the recording settings; the recording itself is a key.
Press **F9** in the game to start, play, press F9 again to stop — or let `bitmagic trailer record`
do the pressing. In the default frame-by-frame mode the engine renders every frame at the chosen
resolution — 60 fps output regardless of how fast your machine renders — and streams them to this
project's `.bitmagic/trailer/recordings/gameplay_recording_*/` along with a `timeline.json` of game
events, sounds and HUD state, ready for `bitmagic trailer make`. While recording, the browser
encodes the frames into a high-bitrate video (Chrome's WebCodecs — the platform's hardware encoder
where there is one, software otherwise) and the game runs as fast as that encoder allows; nothing
is dropped, the recording just counts frames rather than seconds. Once you stop, the sidecar turns
the capture into `capture.mp4` and the PNG frames, which takes a moment per minute of footage —
`bitmagic trailer record` waits for it. Video mode instead captures in real time and downloads a
`.webm`. If the panel says your project's engine is too old for the disk streaming, run
`bitmagic upgrade` and reload.

**Record motion…** in the same menu makes an animation out of you. It records two to sixty seconds
from your webcam — or takes a video file you already have — sends it to be turned into a motion
clip, and drops the result into the project's assets, ready to play on a character. There is a
3-2-1 countdown before the webcam rolls, and the walk-to-the-camera frames at the start are trimmed
off for you — the note when it finishes says how much came off each end, and opening the clip shows
you exactly where.

### The top bar

The bar starts by saying **whose** view this is, because with two games open — and often two
CLIs, one from each line — two dev views look identical otherwise. After the wordmark comes a
badge: **DEV BUILD**, in the warning tint, for a CLI installed from `@bitmagic/cli@dev`, or
**RELEASE** for the release line (hover for the exact version). Then the game by its name — the
`gameName` in `src/work/game.json`, or the folder's name until it has one. The browser tab's title
leads with the game's name too, and says `(dev build)` on the dev line, so the tab strip tells them
apart before you click.

Click the name for the rest of the answer: the game's id, the environment it talks to (`dev` or
`prod`), its genre and physics mode, the art style every generation inherits, the platform it is
built for, its engine version — and the two pictures that decide how the whole project looks, the
**accepted reference image** and the **cover art**, side by side. Both were previously visible only
in a terminal (`bitmagic reference show`, `bitmagic cover`); the dialog draws them from the local
copies under `.bitmagic/`, so they are there whether or not the recorded URL still resolves. Click
either thumbnail to see it full size — a couple of hundred pixels of an image generated at 1024 is
not enough to judge the look it exists to fix; click anywhere, or press Escape, and the dialog is
still underneath. A project missing one is told which command makes it. The reference's caption warns you when it was
never re-hosted, because that URL — the one every later generation inherits — will expire.

What you reach for all session sits in the bar: the **Game / Editor** tabs, **Auto-reload**, the
status light, **Assets** (which shows whether the panel is open), **Screenshot** and **Reload**.
Everything that opens a dialog and is wanted once a week — Published, and the two recorders — lives
behind **⋯**, so the bar stops growing a button every time the CLI grows a feature.

3010 still serves the game on its own, for a second window or a bookmark.

### On a real phone

`bitmagic dev --mobile` serves the game to your own network as well, and prints a QR code to scan:

```bash
bitmagic dev --mobile
#   On your phone    https://192.168.1.23:3012/
#   QR code          .bitmagic/mobile-qr.png — open it to scan.
```

This is the thing no browser can fake. Emulation gets you a small viewport and synthetic touches;
a phone gets you iOS Safari's fullscreen rules, the real orientation behaviour, real multi-touch,
and a real thermal budget on a real GPU. The emulated run (**Verifying the mobile path** above)
still comes first — it is faster and it catches the obvious — but it is not a device.

Three things are worth knowing about how it works.

**The game is proxied, so its assets load.** Your levels and models live on the asset CDN, which
answers only the origins it allowlists — `http://localhost:3000`–`3199`, and no address a phone can
present. A phone asking the bucket directly gets nothing, and a level whose terrain never arrived
looks like a broken forge rather than a blocked request: you fall through the floor forever. So the
phone never asks the bucket. It asks your machine, which fetches on its behalf, where the rule does
not apply. Only `*.bitmagic.ai` and `*.bitmagic.cloud` are relayed this way; a third-party CDN in
your game has to allow the origin itself, as most do.

**It is served over https, and the certificate is self-signed.** Your phone will warn once per
address — tap *Show Details*, then *Visit Website*, and it remembers. The warning buys something
real: `navigator.gpu` exists only in a secure context, so over plain `http` the device would quietly
run the classic WebGL renderer and you would be testing a pipeline your players are not on. Pass
`--mobile-http` to skip the certificate anyway, and accept WebGL on the device. If `openssl` is not
installed, that is what happens by itself, and the banner says so.

**Reloads reach the phone.** It listens on the same channel the dev view does, so an agent edit —
or `bitmagic reload` — reloads what you are holding, without picking it up.

The editor is not exposed: the phone gets the game and the game's assets, and nothing that can write
to your project. The game, though, is genuinely visible to everything on your network for as long as
the command runs. On a café's Wi-Fi, that is worth a thought.

**If those ports are busy it moves and tells you** — it does not refuse to start. That matters
because agents run `bitmagic dev` as a background task, so a refusal would die into a log nobody
reads. `.bitmagic/dev.json` records what it actually bound, so `bitmagic reload` still finds it, and
the browser is opened for you either way. A port you name yourself with `--port` / `--editor-port`
is never moved: if it is taken you are told, because naming one means you wanted that one.

Both ports stay inside 3000–3199 whatever happens: the asset CDN allowlists only those origins, and
outside that window the world loads with no terrain.

### Two installs, two lanes

If you run both published lines side by side (**Running both lines side by side** above), the two
`bitmagic`s never reach for the same port. The CDN's 3000–3199 window is split down the middle, and
an install takes its numbers from its own release line:

| | Game | Game / Editor | `--mobile` | Scanned when those are busy |
|---|---|---|---|---|
| **Release line** (`@bitmagic/cli`) | 3010 | 3011 | 3012 | 3000–3099 |
| **Dev line** (`@bitmagic/cli@dev`) | 3110 | 3111 | 3112 | 3100–3199 |

`verify`, `forge` and `generate` take their throwaway port from the same half, so a verify run on
one line cannot take a port the other line's `bitmagic dev` was about to want. Nothing needs
configuring — the version the install was published under is the whole of the rule.

The hundreds digit is the only difference, so a number still reads as "game", "shell" or "phone" at
a glance, and which install printed it is obvious from the URL alone.

A port you name yourself with `--port` or `--editor-port` is honoured from either half — naming one
means you know what is on the machine. Only the defaults, and only the scan behind them, stay in
their lane. If a whole lane is somehow full, the command says so and stops rather than crossing
over: the other half staying free is the promise that makes this worth having.

`bitmagic judge` is unaffected. It binds nothing and starts no browser — it grades the screenshot
`bitmagic verify` already captured, over one call to the API server.

Only one `bitmagic dev` runs per project. Starting a second is refused and points at the first —
two would fight over one `world.json`, and `bitmagic reload` could only reach whichever wrote its
handle last.

### One project, one set of ports

Within a lane, every project on the machine gets its own fixed ports the first time `bitmagic dev`
runs in it, and keeps them from then on: the same three numbers, one tens-digit along per project.

| | Game | Game / Editor | `--mobile` |
|---|---|---|---|
| First project (release line) | 3010 | 3011 | 3012 |
| Second project | 3020 | 3021 | 3022 |
| Third project | 3030 | 3031 | 3032 |
| … up to the ninth | 3090 | 3091 | 3092 |

On the dev line the same table reads 3110/3111/3112, 3120/3121/3122, and so on. So the hundreds
digit still says which install, the tens digit says which project, and the last digit still reads
game, shell or phone.

Which project holds which slot is recorded in `~/.bitmagic/dev-ports.json`, keyed by the project's
folder. Before this existed every project started at the lane's defaults and drifted to a random
port when those were busy, so which game answered on 3011 depended on start order, bookmarks went
stale, and a `bitmagic reload` from one project could reach another's tab. Now a project's address
is a fact about the project. If its own ports are held by something else when it starts, it drifts
for that run only, says so, and tries its own numbers again next time. A tenth project on one lane
falls back to the old scan, with a line saying why.

`verify`, `forge` and `generate` keep their throwaway ports away from every promised slot, whether
or not that project's `dev` is up at the moment — so a verify never squats on the ports another
project is about to want. A port you name with `--port` and `--editor-port` together bypasses the
registry entirely, as before.

### One verify at a time

`bitmagic verify` reads the game's frame rate off a real browser, and two verifies at once on one
laptop starve each other into a verdict about the machine rather than about the game. So only one
verify drives a browser at a time, machine-wide, across every project and both CLI lines. A second
one — from any folder, started by any agent — waits its turn and says so:

```
Another `bitmagic verify` (pid 48213) in /home/me/games/racer is running — waiting for it, so this run's fps is measured on a quiet machine…
Continuing.
```

The wait covers the browser run, not the build in front of it, so queued verifies still compile in
parallel. `--watch` takes its turn per iteration, so two watch loops in two projects alternate.
`bitmagic publish`'s smoke check queues the same way. A verify that was killed mid-run is noticed
within a second and does not hold anyone up. `BITMAGIC_NO_VERIFY_LOCK=1` opts out, for a CI matrix
that parallelises on purpose and does not grade fps; `forge` and `generate` bakes run for minutes
and are not part of the queue.

**Every change in the Editor tab is saved to `src/work/world.json` as you make it.** There is no
Save button and no unsaved state: your agent reads the same file, and `git diff` is the record of
what you moved. Saves are surgical — one entry per object touched, nothing else in the file is
rewritten.

### The assets panel

A strip down the right-hand side, on both tabs, listing every asset in the project — name, type,
size, and a thumbnail. Toggle it with **Assets** in the top bar; it remembers.

Above the assets sit the generations **currently running**, with their live progress line and how
long they have been going. That includes generations started somewhere else: a `bitmagic generate`
or `bitmagic forge` your agent runs in its own window appears here within a second, because each
command records itself in `.bitmagic/jobs/` and the dev server watches that directory. Watching a
twenty-minute forge tick through its steps is the point.

A generation that finishes disappears from the list and its asset appears below, briefly
highlighted. One that fails disappears the same way, and says why on the terminal of whoever ran it
— the error is a tool diagnostic ("Unknown preset …", "Not enough sparks …") that only the agent
choosing the arguments can act on, so it goes there rather than onto a red card beside the game.
One whose process was killed drops off the list rather than spinning forever.

Thumbnails are drawn by the engine on demand — images are their own preview, meshes and voxel
assets are rendered once and cached in `.bitmagic/previews/` — so they cost nothing on later
reloads. The same cache feeds the editor's right-click **Add object** palette, which used to show
"No Preview" on every tile.

**Click a row to look at the asset properly.** A mesh or a voxel asset opens in a viewer you can
drag to turn and scroll to zoom, at full detail rather than the coarse version a 46px tile is drawn
from; an image opens as the image; a sound gets a player; an animation plays on the engine's block
character — the same clip preview the Creator has — with play/pause and a speed slider. A clip
generated from a recording also shows its source video, with the trim marked on it: the frame
dims behind a `CUT 1.2s FROM THE START` badge whenever the playhead is inside a removed stretch,
over a seek bar split into labelled `cut | kept | cut` segments.

**A character asset gets a row of built-in animations** — Idle, Walk, Run, Jump, Punch, Kick, Flip,
Dance — so you can see the body move rather than stand there. Each plays the engine's own clip on
your rigged character, with the same play/pause and speed slider; **Model** goes back to the still
mesh. The clips are looked up in your project's vendored engine, so they are the ones your game
actually plays. A very old engine has no `characterUrl` option and falls back to playing the clip
on the block character.

Beside the viewer is everything the row has no
room for — extent in world units, voxel count, triangle count, and the asset id, with a button to
copy it so you can paste it into a `bitmagic generate prop --asset …`. Escape closes it. The game
pauses while it is open and resumes when you close it.

The viewer is served by `bitmagic dev` itself, so it arrives with the CLI: no `bitmagic upgrade`,
and no dependency on how old the engine vendored into your project is. It does borrow one thing
from that engine — the voxel loader, so what you see is what your project's own code builds.

### Auto-reload

The browser reloads itself when the project changes, so you watch your agent's work appear instead
of reaching for F5. Three things drive it:

- **The file watcher.** `src/` and `dist/` are watched, and a reload fires once the writes go quiet.
- **The build gate.** A reload waits for `tsc --watch` to finish and land clean. If the build fails
  the reload is dropped and the page says so — reloading would show you the previous build's code
  running against your new source, which is the most confusing outcome available.
- **`bitmagic reload`.** The watcher has to guess when an agent is finished, because an agent pauses
  to think. This command replaces the guess. `bitmagic init` wires it to Claude Code's `Stop` hook in
  `.claude/settings.json`, so Claude fires it at the end of every turn; any other agent can run it
  directly. `bitmagic upgrade` adds the hook to an existing project without touching anything else in
  that file.

Turn it off with the **Auto-reload** checkbox in the top bar — the setting sticks, and you get the
old "changed on disk, click to reload" banner instead. It also falls back to the banner on its own
while you are mid-edit in the Editor tab, rather than reloading a drag away.

### What your agent sees

Every editor action is appended to `.bitmagic/edit/events.jsonl`, one JSON object per line, and
printed to the command's own output — so the agent that launched `bitmagic dev` can tell what you
did without inferring it from a diff:

```
{"at":"…","event":"object.moved","objectId":"inst_…","position":{"from":{…},"to":{…}}}
{"at":"…","event":"terrain.saved","voxelUrl":"https://…/game-terrain-….vxl"}
{"at":"…","event":"hq.requested","assetId":"asset_…","prompt":"a mossy stone arch","command":"bitmagic generate prop …"}
```

Most lines are worked out by comparing `world.json` before and after a save. The sculpting ones are
not: a terrain save changes one URL that says nothing about what was carved, and a save that
**failed** changes no file at all — so the editor reports those itself rather than leaving your
agent to infer a thing that left no trace.

### Regenerating a placeholder

Forged levels are built from placeholder assets — box-model stand-ins. Select one and the object
inspector offers **Generate high-quality version**: describe what it should be, press Generate, and
the editor runs the job. It takes a couple of minutes, you can keep editing while it runs, and when
it lands every already-placed instance of that asset upgrades at once — the new mesh is written
under the same asset id rather than appearing as a second asset beside the old one.

It costs sparks, and it needs you to be logged in (the mesh comes from an authenticated server
route). Logged out, the editor saves your description and shows the command instead, which does
exactly the same thing:

```bash
bitmagic generate prop --asset <assetId> --prompt "<what it should be>"
```

Either way it lands in the journal — `hq.started` then `hq.completed` — so your agent finds out an
asset changed and why, instead of tripping over it. If it fails after the source exists, the error
carries the URL: retry with `--master-url <url>` (or `--glb-url <url>` on the mesh path) and you
are not charged again.

Both ports must fall between 3000 and 3199. That is the window the asset CDN allows as an origin;
outside it the game loads with no terrain and no error.

### Sculpting the ground

Click the ground and you are in the engine's terrain tools; **Save & Exit** in their toolbar keeps
the work. The sculpted volume is uploaded and `src/work/world.json` gets the one line that points at
it, the same surgical write every other edit here makes.

That upload is the only part of `bitmagic dev` that needs an account — everything else runs offline.
Logged out, the save fails and **says so in the toolbar, leaving your edits where they are**: run
`bitmagic login`, then press Save & Exit again. Nothing is thrown away without you saying so, and
`terrain.saveFailed` lands in the journal either way, so your agent knows what happened.

Per-object voxel sculpting works the same way — select a voxel object, edit it, Save & Exit.

## Adding your own assets

`bitmagic assets add` is the Creator's Assets-tab upload for a project your own agent edits: a
Blender model, a sound library, a texture, a `.vxl` exported from the Creator or a config JSON goes
into storage and into `src/work/world.json` in one command. Nothing here costs sparks.

```bash
bitmagic assets add ./models/lantern.glb                 # voxelized in a headless browser → a `vxl` asset
bitmagic assets add ./models/lantern.glb --keep-glb      # kept as a polygon mesh (`glb`), 2 m tall
bitmagic assets add ./models/lantern.glb --voxel-size 0.05 --height 1.2 --hollow
bitmagic assets add ./sfx/door-slam.mp3 --name doorSlam  # audio, uploaded as-is
bitmagic assets add ./textures/rune.png                  # image; the thumbnail is the image
bitmagic assets add ./data/waves.json                    # config data for `loadJsonAsset`
bitmagic assets list
bitmagic assets remove asset_1755600000000_k3j9x1
```

What each file becomes:

| File | Asset `type` | Notes |
|---|---|---|
| `.glb` | `vxl` | Uploaded first (kept as `sourceGlbUrl`), then the engine voxelizes it: `--voxel-size` (finest, default 0.1 m), `--max-voxel-size` (flat areas, default 0.5 m), `--height` (scale to this height; default the model's own size). Interiors fill solid for prop-scale models and stay hollow above 6 m, matching `generate prop` — a solid-filled building is millions of wasted voxels. `--hollow` and `--solid` override that either way. Needs Chrome and a free port between 3000 and 3199. |
| `.glb --keep-glb` | `glb` | A polygon mesh, no voxelizing, no browser. `--height` sets `targetHeight` (default 2 m). |
| `.vxl` | `vxl` | Must be the binary VXL3 format (versions 3-9) the Creator and the engine export. Bounds, voxel size and voxel count are read from the file's own header. |
| `.png .jpg .jpeg .webp` | `image` | Uploaded as-is. |
| `.opus .ogg .m4a .mp3` | `audio` | Uploaded as-is. The engine plays all four; the Creator would transcode to `.opus` for a smaller download, and the command prints the `ffmpeg` line that does the same. |
| `.json` | `json` | The config-data assets `game/agent-docs/json-assets.md` describes. |
| `.gltf` | refused | Its buffers and textures live in files beside it, and the game loads one URL. Export a self-contained `.glb`. |
| `.vox .qb .fbx .spz .ply .sog .mp4` | refused | Each needs a conversion the Creator runs server-side (voxel-model import, FBX animation import, splat transcode, video faststart). Upload those through the Creator's Assets tab. | (An `.mp4` you want turned into an **animation** goes to `bitmagic generate animation --video` instead.)

The name defaults to the filename without its extension (`--name` overrides). The id is minted as
`asset_<timestamp>_<random>` unless `--asset-id` gives one — re-running with the same id **replaces**
the asset, which is how a `--asset-id` retry stays idempotent. A replacement **merges**: fields the
upload does not determine (`fitBox`, `vehicleFitment`, `collision`, `light`, …) survive it, and the
asset's cached thumbnail is dropped so the dev view re-renders. Replacing a still-referenced asset
with a *different* type is refused unless you pass `--force`, because every reference to it would
then resolve to something the engine cannot render. Every entry records
`production: { method: "uploaded", sourceUrl, sourceName }`.

Uploads are read with symlinks refused and capped at 512 MB: the destination is a permanent public
URL that nothing can delete, so the command will not follow a link to a file you did not mean to
publish.

The upload is refused before anything is sent when the project's `gameId` is not one the logged-in
account owns, and every failure before the write says `world.json is unchanged`. A `.glb` whose bake
fails after the upload prints where the GLB landed and the exact `--asset-id` to re-run with, so the
retry replaces that attempt instead of adding a second asset. While it runs, the upload shows in the
dev view's assets panel like any other job (`.bitmagic/jobs/`).

`bitmagic assets remove` edits `world.json` only — the file stays in storage, exactly as the
Creator's Remove leaves it (published games keep loading; every upload is immutable). It looks first
for anything in `world.json` still pointing at the asset — any `assetId`, `<thing>AssetId` or
`<thing>AssetIds` key, so placed objects, a level's `vwldAssetId`, a map, doors, key items and a
village's `buildingAssetIds` all count — and refuses while there is, listing where; `--force`
removes the asset and its placed instances in one write and reports the rest. `assets list` shows
the same total in its `REFS` column, so the two commands never disagree about what is in use. Game
code that names the id (`engine.playSound(...)`, `spawnFromAsset(...)`) is not checked, and neither
is a lookup by *name* — `loadJsonAsset` resolves the first asset with a matching name, so `add`
warns when a name is already taken.

## Generating a 3D model

`bitmagic generate model` turns a sentence into a new placeable asset: the Asset Forger builds a
mesh, the engine voxelizes it, and the result lands in `assets[]` under a fresh id.

```bash
bitmagic generate model --prompt "a mossy stone arch"
bitmagic generate model --prompt "a market stall" --name marketStall --height 3
bitmagic generate model --prompt "a cathedral" --height 30        # hollow above 6 m, automatically
```

Place it from game code by name, or drag it in from the dev view's Add-object palette.

**Things that move or glow** — a windmill, a ferris wheel, a swinging sign, a lamp post — can come
back as a *smart object*: the Forger analyses the generated voxels for moving parts and lights, the
bake writes them into the asset, and the engine animates them with no game code. Ask for it with
`--smart`, or say what should move in your own words, which is the biggest lever on what the
analysis finds:

```bash
bitmagic generate model --prompt "a wooden windmill with four large blades" --smart-hint "the blades spin about the hub"
bitmagic generate model --prompt "a fairground ferris wheel" --smart-hint "the wheel turns slowly and six cabins hang level from its rim"
bitmagic generate model --prompt "a victorian street lamp" --smart-hint "the lantern head glows warm amber"
```

`bitmagic generate prop` takes the same flags, and a placeholder that already declares its parts
(a forged windmill, an agent-built one) passes them on as the hint by itself. Voxel path only: a
mesh has no master to analyse. The analysis stores its verdict beside the master, and the free
retry a failed bake prints names it as `--smart-spec`, so a retry is not a second analysis.

**Which of the three 3D lanes you want** matters, because picking wrong wastes a generation:

| Command | Makes | Existing instances |
|---|---|---|
| `generate model --prompt "..."` | a **new** asset from a prompt | none yet — nothing is placed |
| `generate prop --asset <id> --prompt "..."` | replaces the mesh of an **existing** placeholder | **all of them upgrade at once** |
| `assets add <file>` | a new asset from a file you already have | none yet |

Like `prop` and `vehicle` it needs Chrome and a free port between 3000 and 3199 — only the engine
can voxelize, so the CLI drives its own headless browser. `--voxel-size` (default 0.1 m) and
`--max-voxel-size` (default 0.5 m) set the bake resolution; `--height` scales the model, and
interiors fill solid at or below 6 m and stay hollow above it (`--hollow` / `--solid` override),
because a solid-filled building is millions of wasted voxels.

It costs the same sparks as generating the same object in the web editor — both bill the Asset
Forger call under one price, so the lane you choose never changes what a creator pays.

**What gets generated: voxels, not a mesh.** The generator produces voxels directly, and the
`.vxl` master it returns is stored as the asset's source — around a megabyte, against the
50–100 MB textured mesh the old path bought only to voxelize and throw away. `--voxel-grid` sets
the grid that master is forged at (default 512, one of 0/32/64/128/256/512). It is worth
understanding once: a master can only ever be re-baked **coarser**, so the grid is the ceiling on
every later `bitmagic assets revoxelize`.

`--mesh` asks for the old path instead. The one thing it buys is that a mesh is
resolution-independent, so its stored source can be re-voxelized to any resolution at all — if you
know you will want more detail later than a 512³ master can hold, ask for the mesh. A project
whose vendored engine predates voxel masters takes the mesh path regardless, and says so; run
`bitmagic upgrade` to get the newer engine.

If the bake fails after the source exists, the error prints the exact command to retry the browser
step **without paying again** — `--master-url …` for voxels, `--glb-url …` for a mesh.

## Voxel materials

A generated voxel asset carries colours and nothing else, so every voxel shades identically: a
sword's steel blade responds to light exactly like its leather grip, and both read as flat paint.
Material classes fix that by saying what each colour is **made of**.

**You usually do not have to do anything.** `bitmagic generate prop` and `bitmagic generate model`
classify the asset right after the bake: a light model reads the same colour table shown below
together with the asset's prompt, and its verdicts are written into the `.vxl` before the command
returns (`Materials: metal, leather` in the output; `materials.classifier: "ai"` on the asset's
`world.json` entry). Pass `--no-materials` to skip it. A classification that cannot run — no model
credential, an engine too old to write classes — is a note, never a failed generation; the asset is
baked and matte, and `bitmagic assets materials <id> --auto` retries it later.

`bitmagic forge` does the same twice over: the level's own terrain gets classes the designer
authored per colour ("rock on steep slopes" bakes onto the stone tier), and every archetype
library asset the forge creates is classified right after the bake, in the same session. A matte
archetype is always retriable with `bitmagic assets materials <id> --auto`.

When the automatic answer is wrong, or for an asset added from a file (which records no prompt, so
nothing is classified automatically), `bitmagic assets materials` is two steps, and the CLI does not
guess at the middle one — **you do**, because you are the one who knows what the asset is meant to
be.

```bash
bitmagic assets materials sword-01              # what does this asset look like?
bitmagic assets materials sword-01 --set m.json # here is what those colours are
bitmagic assets materials sword-01 --auto       # or let the model decide again
```

The first command prints the asset's colour groups:

```
"Longsword"  signature 4f2a91cd (version 1)

20 voxels, 5 palette entries, bbox 1x20x1 voxels

id   colour   share  height(min-max)  extent(x,y,z)  radial  shape       parts
g0   #dddddd   70.0%        0.33-0.98 0.00,0.65,0.00    0.00  thin-long   1
g1   #998866   20.0%        0.03-0.18 0.00,0.15,0.00    0.00  blob        1
g2   #eecc66   10.0%        0.23-0.28 0.00,0.05,0.00    0.00  blob        1
```

Read with the asset's own prompt — printed underneath — that is enough to name the materials: g0 is
the steel blade (most of the model, upper two thirds, long and thin), g1 the leather grip at the
bottom, g2 a brass pommel between them. `share` is the fraction of the asset, `height` where it
sits from floor to top, `extent` its size along each axis relative to the whole, `radial` how far
from the centre axis, and `parts` how many separate pieces the colour forms — a colour scattered
across the whole model in many parts is dithering, not a material.

Then write the verdicts:

```json
{
  "signatureVersion": 1,
  "signatureHash": "4f2a91cd",
  "verdicts": [
    { "id": "g0", "class": "metal",   "confidence": 0.95 },
    { "id": "g1", "class": "leather", "confidence": 0.9 },
    { "id": "g2", "class": "gold",    "confidence": 0.6 }
  ]
}
```

The `signatureHash` binds the answer to the asset it describes: re-bake or re-voxelize the asset
and the hash changes, so a stale set of verdicts is refused rather than applied to geometry it
never saw.

**Classes:** `matte` (the default — no special shading), `metal`, `gold`, `chrome`, `gem`, `glass`,
`wood`, `stone`, `cloth`, `fur`, `leather`, `plastic`, `paint`. The first five reflect the sky; the
rest get a highlight from the scene's own lights, which keeps their colours exactly as they are.
`fur` is the odd one out — it reflects nothing, but picks up a soft halo towards its silhouette the
way a pelt or plush does. Use it for exactly that: a teddy bear, an animal, a fur trim. Anything
merely soft is `cloth`.

The engine also has three classes that EMIT light — `filament`, `neon`, `lava` — which is why you
may see one in a `.vxl` that this command never wrote. They are not on the list above and must not
be used here, deliberately: whether a surface is a light is not answerable from its colour, and a
wrongly glowing surface is worse than a wrongly shiny one because it also lights everything around
it. They are picked in the voxel editor, where a person selected the voxels and knows. A verdict
naming one is refused over a quarter of an asset like any precious material, and arrives with no
glow even when it is accepted.

**Rules worth following:**

- `matte` is the right answer for most groups, and costs nothing. Leave a group out entirely rather
  than inventing a material for it.
- The asset's prompt is the strongest evidence there is — it names the materials, and your job is
  only to decide which colour group is which.
- A material the prompt does **not** name needs strong evidence from the geometry. A precious
  material (`gold`, `gem`, `glass`) claimed over more than a quarter of the asset is refused
  outright unless the prompt says so: a wooden crate is never 40% gem, and a wrongly shiny surface
  is what a player notices.
- Use a low confidence rather than guessing. Below 0.45 a verdict is dropped, which is a better
  outcome than a surface that shines when it should not.
- An asset that records no prompt or description has nothing saying what it is. Prefer leaving it
  alone.

**Budget.** Four materials per asset. Each one is an extra draw call on every placed instance, so
over budget the smallest is folded into a related class (`gold` into `metal`, `gem` into `glass`)
rather than dropped — every fold is reported. Very small groups are dropped too, except for the
precious classes, where a fraction of a per cent is exactly the point.

Materials are written into the `.vxl` under the **same asset id**, so every placed instance changes
with it. `--clear` puts an asset back to the plain look — it strips the classes and keeps every
slot's glow, because taking a light away is the one thing this command must never do. A re-bake
(`bitmagic assets revoxelize`) produces a fresh `.vxl` whose palette the old verdicts no longer
describe, so it classifies again on its own — `--no-materials` there leaves the re-baked asset
plain. Nothing here generates anything or costs
sparks; what it costs is the headless browser bake, since only the engine can read and write a
`.vxl`. An asset whose base voxels glow (from the older per-colour glow that materials replaced) is
refused rather than having that glow silently traded for a shine.

## Drivable vehicles

`bitmagic generate vehicle` designs a vehicle and registers it as a **new** asset — unlike
`generate prop`, which upgrades an existing placeholder in place. An AI designer authors the shape,
paint and wrap art (including generated side art like "a poodle mural on a grooming van"); a
parametric builder produces the model; the engine voxelizes it and derives its physics fitment.

```bash
bitmagic generate vehicle --prompt "a rusty orange pickup with flame decals"
bitmagic generate vehicle --preset ?              # list the free presets
bitmagic generate vehicle --preset VAN            # a standard vehicle: skips the designer, free
```

Then spawn it from game code by name:

```js
engine.getVehicleSpawner().spawnAndEnterFromAsset({ x: 0, z: 0 }, '<assetName>', playerController);
```

Like `prop` it needs Chrome and a free port between 3000 and 3199 — only the engine can voxelize,
so the CLI drives its own headless browser. Headlights, taillights and emergency beacons are built
and lit automatically; there is no need to ask for them.

Costs sparks based on what the designer actually spends, so a retrying design costs more than a
clean one and `--preset` costs nothing. If it fails after the model exists, the error prints the
exact `--glb-url … --voxel-size …` command to retry the browser step without paying again.

If the finished asset reports that it will not drive, the project's vendored engine predates
vehicle-fitment derivation: run `bitmagic upgrade`, then regenerate.

## Editing a forged level

A forged level's design is kept on its record, so a detail can be changed without designing and
baking the world again:

```bash
bitmagic forge --edit "make the doors sliding bulkheads"
bitmagic forge --edit "warm amber lanterns instead of the lamps" --level "Freighter Mk IV"
bitmagic forge --edit "recolour the hull plating a rusty red" --level level_1788942022362_yujy5i
```

The change goes to the designer as one small call that returns the handful of edits to the stored
design — a fraction of a design's cost — and the level is then built from the edited design under
a new job. What that build re-makes is decided by comparing it with the original, object by object:

| The change touches | What is re-made |
|---|---|
| the door style | the door leaves and the key pickups |
| the light fixture (kind, colour, flicker) | the fixtures |
| a prop or wall-decor kit piece | that piece |
| a material colour, a wall/floor/ceiling kit piece, the rooms, the viewports, the terrain | the level bake and most objects |

Everything else — the baked level, every untouched object — is reused, so a door-style edit takes
about as long as baking three small objects. The run prints what it reused. The level is replaced
in place: same level id, doors, keys and placed objects rewritten, your other levels untouched. An
object you have since regenerated in high quality is kept as it is.

One thing it needs: the level must have been forged by a CLI that records the job on the level
and mirrors its bakes to the server — anything forged before that is re-forged once and editable
from then on, from any machine. (A forge keeps its resumable state in the project's
`.bitmagic/forge` and mirrors the browser-baked records to api-server as it goes, so `--resume`
and `--edit` both work from a clone that never ran the original forge; a mirror that fails is
said once and the run continues from the local copy. Once a forge or an edit has landed, the
finished jobs under `.bitmagic/forge` that no level names any more are removed — an edited level
keeps only its newest job; an unfinished job, the one `--resume` continues, is kept; the server's
copies are untouched.) `--level` takes the level's id or name (`bitmagic levels list`), and
can be left out when the project has one forged level. `--edit` takes no other forge flag; a 2D
side-view level cannot be edited yet.

## Managing levels

`bitmagic forge` bakes levels but could not manage them: only a game's *first* forge became the
level it boots into, so fixing a wrong start, renaming, or deleting one meant opening the web
Creator. These do it from here. They edit `src/work/world.json` directly — no network, no sparks,
no login — and journal each change to `.bitmagic/edit/events.jsonl` so your agent sees it.

```bash
bitmagic levels list
bitmagic levels set-start <levelId>
bitmagic levels rename <levelId> "Old Town"
bitmagic levels add <vwldAssetId> [--name "Cavern"] [--make-start]
bitmagic levels remove <levelId>
```

`list` shows each level's id, name, which one is the **START**, how many spawn points it owns and
how many placed objects belong to it — plus a warning when its baked `.vwld` asset has gone missing.

**`set-start` is the one that matters most.** `worldProfileData` keeps three *mirror* fields —
`voxelUrl`, `spawnPoints` and `playerSpawnPosition` — describing the start level, because published
games boot from those rather than from the level registry. This moves them together. Doing it by
hand and forgetting the mirrors gives you a game that renders the new level's objects and lighting
over the **old** level's terrain, with matching collision from a third place and no error anywhere.
It refuses when the level's `.vwld` asset is missing, since there would be nothing to boot.

**`add`** registers a `.vwld` that is already in the project (`bitmagic assets list` shows them) —
from a forge, or from `bitmagic assets add level.vwld`. It follows `forge`'s rule: a game's first
level becomes the start, later ones do not unless you pass `--make-start`. On a game that has a
world but no level registry yet, the existing world is registered as level 1 first, so it is not
stranded by the one you are adding.

**`remove`** takes the level's placed objects, doors and key items with it, and leaves the `.vwld`
asset alone — it may be large, and nothing here deletes uploaded bytes. It refuses two cases: the
game's only level, and the **start** level (move the start first, so you never end up with a game
booting one level's terrain under another's objects).

Runtime level switching is engine API — `engine.loadLevel(levelId)` from game code. These commands
manage the registry, not gameplay.

## Building a level in Blender

A hand-designed low-poly world — an interior, a station, a stylised set — can be written as a short
Python script against the `bmlevel` library that ships in this package (`assets/blender-level/`),
built by Blender into one GLB plus the collider / door / light / landmark JSON, audited with ray
casts and rendered for review, then registered with `bitmagic assets add … --keep-glb` and loaded
by the engine's `MeshLevel`. The scaffolded `building-levels-in-blender` skill walks through it;
its first section says when that path applies (a frontier model, and Blender installed) and sends
everything else to `bitmagic forge`.

## Importing a MetaHuman

An Epic MetaHuman (a UE 5.7 glTF export from the smorchj/metahuman-to-glb pipeline) can be the
game's playable high-res character without voxelization. The scaffolded `importing-metahumans` skill
carries the whole path — `bitmagic tools install metahuman` lands the GLB preparation scripts, the
runtime module (`src/work/metahuman/`), the lip-sync tool and a headless verification rig — and names
each failure a raw export produces (arms crossed into the chest, a torn shirt, hair left behind by a
head turn, tinted skin, black eyes) with the fix that resolved it.

## Art style

A 3D game is voxel-built unless `src/work/game.json` says otherwise:

```json
{ "gameName": "…", "artStyle": "low-poly" }
```

`artStyle` sits beside `physicsMode` and takes `voxel` (the default, and what an absent field
means) or `low-poly`. `bitmagic init --art-style low-poly` writes it and pre-fills
`GAME-DESIGN.md`'s Art direction heading; an existing project adds the line by hand. One field,
read by everything that used to assume cubes:

| Command | `voxel` | `low-poly` |
|---|---|---|
| `bitmagic cover`, `bitmagic reference make` | MagicaVoxel-style prompt lead and tail | flat-shaded low-poly lead and tail (same image model) |
| `bitmagic assets add x.glb` | voxelized in a headless browser | kept as a `glb` asset; `--voxelize` bakes it |
| `bitmagic generate model` / `prop` | voxel master, baked in the browser | mesh generated, re-uploaded into the project, registered as `glb` (`--height` / the placeholder's fitBox); `--voxelize` bakes it |
| `bitmagic character generate` | voxelized rig, library search first | the textured low-poly rig; its own library shelf, searched only where the server has enabled it; `--voxelize` overrides |
| `bitmagic character search` / `add` | searches the voxel shelf | searches the low-poly shelf |
| `bitmagic generate vehicle` | voxel | still voxel — the builder is voxel-native; the command says so |
| `bitmagic judge` with no `## Art direction` | grades the style it sees | judges within low-poly |
| the engine on load | sunset hemisphere light and a faint bloom | neither; a `MeshLevel` brings its own lighting |

### Characters

Characters are their own command group, because two different requests used to
share one command:

```
bitmagic character generate --name guard --prompt "..." [--fresh]
bitmagic character search   --prompt "..." [--limit 5] [--representation voxel|lowpoly]
bitmagic character add      --name guard  (--id <sid> | --prompt "...")
```

- **`generate`** forges a new body and costs sparks. It still consults the
  library first and substitutes a close-enough stored one where the server has
  that enabled — that is a cache hit on a generation you asked for, and it is
  what makes a character sometimes arrive in two seconds. `--fresh` turns it off
  and always forges. (`--no-library` is the deprecated spelling of `--fresh`.)
- **`search`** lists the closest ready-made bodies with their scores. Free,
  read-only, and it writes nothing to your game.
- **`add`** puts a stored body in the game. Free, instant, and it **never
  forges**: if the shelf has nothing usable the command fails and charges
  nothing, rather than quietly falling back to a paid generation.

`bitmagic generate character` still works and is a deprecated alias of
`bitmagic character generate`; `--library-only` on it is now
`bitmagic character add`.

#### Why `search` exists

Search is not a nicety here, it is the mechanism. Scored against the live index,
genuine matches land at 0.62–0.83 — and humanoid prompts the shelf *cannot*
answer land at 0.55–0.70, inside that range. "A beekeeper" fetches a humanoid bee
at 0.65; "a hazmat worker" a boy mechanic at 0.65. Both are confident, both are
wrong, and no threshold rejects them without also rejecting real matches. A
number cannot tell these apart. You can, in about a second:

```
$ bitmagic character search --prompt "a deep sea diver"
3 ready-made voxel character(s) for "a deep sea diver":

  #  id     match  auto  description
  1  b1877    57%     yes  a bearded pirate captain
  2  b0412    54%       -  a man in a rubber raincoat
  3  b2003    52%       -  an astronaut in a white suit

Add one:  bitmagic character add --id b1877 --name <name>
```

The **auto** column is the bar automatic substitution has to clear (55% on the
voxel shelf, 65% on the thinner low-poly one). It is shown rather than applied:
a body below it is often still the one you want, and knowing it is a stretch is
the useful part.

`add --id` reads the candidates the last `search` cached in
`.bitmagic/character-search.json`, so it needs only the id — no re-search, and no
retyping the prompt. `add --prompt` with no id takes the closest usable body
whatever it scored, which is the "any of these will do" request.

Neither `--head-scale` nor `--mesh-model` exists on `add`, and that is the point
of the split: proportions are baked into a stored body and a mesh model selects
how a character would be *forged*. The old `--library-only` had to accept the
first and refuse the second; a command that never forges simply does not offer
them.

The shelf follows the project's art style like every other character request: a
voxel game searches voxel bodies, a low-poly game searches low-poly ones, and an
empty result never falls back to the other medium. `--representation` overrides
it if you want to look at the other shelf.

#### Low-poly characters cost more to forge

In a low-poly game the textured rig IS the product, so `bitmagic character generate`
forges it with `runware-rodin-gen-2`: a quad remesh at a triangle count we choose,
with the concept sheet's own A-pose kept. **That bills 60 sparks and takes about
three minutes per character**, against 15 sparks and forty seconds for a voxel
body — pass `--mesh-model unified-trellis` to put one back on the cheap lane at
15 sparks. A voxel game does not get it: the body is rebuilt out of cubes, so the
remesh and the texture are thrown away.

Wall time is why the forge job budget is 20 minutes and api-server's in-handler
generation deadline is 29: at the old 8-minute and 280 s ceilings a slow mesh
model was killed about three seconds before it finished, after being billed.
Cloud Run's own 1800 s request timeout was already high enough and is set in
roleverse-infra.

Both commands print `Art style: … (game.json)` so a transcript shows which medium drove them, and a
value that is neither word is reported once and treated as voxel. The web Creator does not read
the field: its planner builds voxel games only.

## Publishing

`bitmagic publish` is two gated steps in one command:

1. **The verify gate.** Publish refuses to run unless `.bitmagic/verify/result.json` records a
   passing run of the **current** project state. "Current" is decided by a content fingerprint —
   a hash of your project's files — not a timestamp: editing any file after verifying makes the
   record stale immediately, while an unmodified project stays fresh no matter how much time has
   passed. There is no way to check freshness except running `bitmagic verify` again.
   `.git/`, `node_modules/`, `dist/`, `.bitmagic/` and `.vite-cache/` are excluded, so committing
   between `bitmagic verify` and `bitmagic publish` does **not** invalidate the verify. Neither
   does editing `GAME-DESIGN.md`, which no build step reads.
2. **The build.** If the project has changed since the last `.bitmagic/build/manifest.json`,
   publish rebuilds automatically; a prior manual `bitmagic build` is never required, only useful
   if you want to inspect `.bitmagic/build/index.html` before it ships.

**Publishing does not make a game public.** A new game starts `private` — reachable by URL, not
listed anywhere on bitmagic.ai. Pass `--visibility public` when you want it listed.

**Visibility is sticky.** `bitmagic publish` with no `--visibility` keeps the game exactly as it
is, so shipping an update to a public game leaves it public. You only need `--visibility` when you
want to *change* something: `public` to list it, `private` to take it out of service.

**Managing a published game** — changing its visibility, or taking it down for good — happens on
the web, at the **My games** page `publish` prints a link to (`bitmagic.ai/my-games/`). It is
the same page web-lane creators use, so there is one place to manage a game regardless of how it
was built. Withdrawing a game there unlists it and frees any custom `/play/` address; it does not
touch your project, and you can publish it again afterwards.

`--force` overrides a **stale or failed** verify verdict. It does **not** override a missing
verify record — a project with no cover art has only the verify screenshot to publish a thumbnail
from, so `bitmagic verify` must have produced at least one screenshot — `screenshot.png` or
`screenshot.mobile.png` — ever, before `--force` has anything to work with.

`--name` and `--description` apply to that one publish call only. They are never written back to
`src/work/game.json`, so pass them again on every future `bitmagic publish` if you want them to
stick — and note that writing them into `game.json` yourself would change the fingerprint and
invalidate the verify you just checked.

### Testing it on a phone

Publish draws the URL it just printed as a QR code, so you can point a phone at the terminal
instead of retyping a twelve-character game id into a mobile browser. Scan it and the game opens.

This is the pro lane's "try it on my phone" loop, and `--visibility private` is the whole of it: a
private game is *unlisted*, not access-controlled, so its URL works from any device while the game
stays off bitmagic.ai. Publish as often as you like and scan the code each time.

**When the code cannot be drawn, publish writes it as a file instead** — `.bitmagic/publish-qr.png`
— and names the path (`qrPath` in the `--json` result). That is the normal case for an agent-driven
publish: your agent's stdout is a pipe, not a terminal, so there is nothing to paint a code onto,
and the person who would scan it is on the other side of that pipe. The image is how it reaches
them. The same happens under `--json`, for `TERM=dumb`, and when the terminal is narrower than the
code (37 columns for a typical publish URL), since a wrapped code cannot be scanned anyway. A
terminal that draws the code successfully writes no file.

`.bitmagic/` is excluded from the publish fingerprint, so the image cannot invalidate a verify.

The code is extra, never a substitute — the URL is always printed as a plain line above it, and
everything still works if neither a drawing nor a file appears. `--no-qr` and `BITMAGIC_NO_QR`
suppress both.

`bitmagic dev --mobile` is the same loop without the publish — see **On a real phone** above. It
takes a detour to get there: the asset CDN serves assets only to `localhost` origins, so a phone
pointed straight at your machine's LAN address loads the game with no terrain and drops the player
through the floor. `--mobile` fetches those assets on the phone's behalf, which is what makes the
LAN address work at all. Publishing privately is still the way onto a device that is not on your
network, or into someone else's hands.

### Which platform the game is catalogued as

Publish syncs two indicators from your project files, so bitmagic.ai lists the game as what it
actually is:

| Indicator | Where you set it | Values |
|---|---|---|
| `primaryPlatform` | top level of `src/work/game.json` | `desktop` (templates ship this) or `mobile` |
| `mobileOrientation` | `worldProfileData` in `src/work/world.json` | `portrait` or `landscape`; absent means no lock |

Those files are the **source of truth**: each publish re-sends what they say, so a value changed
elsewhere (an admin edit, say) is replaced by your next publish, and removing `mobileOrientation`
from `world.json` clears the stored lock. A project whose `game.json` declares no
`primaryPlatform` at all leaves the stored value alone rather than forcing it to `desktop`.

`primaryPlatform` is not only a catalogue entry. It also decides what `bitmagic verify` boots and
`bitmagic judge` grades by default, and it turns the mobile-parity gap from a warning into a
failure — see **Verifying the mobile path**. The game itself is playable from a phone either way;
making it *good* on a phone is code, and the scaffolded `AGENTS.md` tells your agent how: bind every
action with `registerCustomAction`/`declareMobileActions` so each desktop key gets a touch button,
and never with a raw `keydown` listener.

### The source archive

Every publish also uploads a **source archive** — `.bitmagic/publish/source.zip` — alongside the
bundle. It carries your game's source (`src/work/`, `index.html`, the configs, `bitmagic.json`,
`GAME-DESIGN.md`, `mechanics-plan.md`) plus a manifest of file hashes. The vendored `engine/` tree
is hashed but not included: it is platform code, identical for everyone on your engine version, and
shipping ~14MB of it on every publish would serve nobody.

**The archive is uploaded to a publicly readable URL.** The address contains a random component, so
it cannot be guessed from your game's id or its play URL, but anyone given the link can download it.
Files that look like credentials — `.env*`, `*.pem`, `*.key`, `id_rsa*`, `.npmrc`,
`.git-credentials`, `*service-account*.json`, `.claude/settings.local.json` — are **never** put in
the archive, and publish prints a line naming any it left out. That list will never be exhaustive:
if your project directory holds a secret in some other shape, move it out before publishing.

Bitmagic uses the archive to sanity-check publishes: that the source matches the fingerprint the
verify gate checked, and that the engine you built against has not been modified. Findings are
advisory and printed as `warning:` lines — they never block a publish. The one thing that *is*
refused is a bundle that is not a Bitmagic engine build at all (exit 1).

### Exit codes

`bitmagic publish` and `bitmagic judge` use these to let a script or agent branch without parsing the message:

| Code | Meaning |
|---|---|
| 1 | Build failed, or the uploaded bundle was not a Bitmagic engine build (check `vite.publish.config.js`, or `vite.publish-standalone.config.js` for a `--single-file` build). Not retryable — republishing the same bytes gets the same refusal. |
| 2 | Not logged in |
| 3 | Verify missing, stale, or failed |
| 4 | `bitmagic judge --min-score`: the judge ran, but the overall score is below the bar. The scorecard is in `.bitmagic/judge.json`. |
| 5 | Not the game's owner, or the account/game is banned |
| 6 | Upload or finalize failed — usually transient (network/storage), but also covers permanent failures such as an unknown game or a rejected request. Retry once; if it fails identically, stop and report it. |
| 7 | The game published, but did not render when loaded the way the portal loads it. The game IS live — fix it and publish again rather than retrying the publish. Artifacts in `.bitmagic/smoke/`. |

`bitmagic build` alone exits 1 on a build failure and 0 on success.

## Upgrading

`bitmagic upgrade` refreshes the vendored `engine/`, the generated config files, `index.html`, and
**the skills this CLI ships** — in every skills directory the project holds — to the currently
published engine version. It
refuses to run against a dirty git working tree unless `--force`, so the upgrade always shows up as
its own reviewable diff. When the engine version actually moves it also checks npm for a newer CLI —
see [Staying current](#staying-current).

What is yours and never touched: `src/`, `game.json`, `package.json`, `CLAUDE.md`, `GAME-DESIGN.md`,
`.gitignore`, and any skill you wrote yourself. `world.json` is yours too, with one exception: the
default trees and rocks the platform itself scaffolded into a project before engine 3.1056 pointed at
legacy JSON `.vxl` files (converted in memory on every load, warned about on every load, no LOD
levels), and `upgrade` repoints those nine known URLs at their VXL3 versions and updates their
`size`. Nothing you added is looked at, and the command lists every record it moved.

`.claude/settings.json` is merged rather than rewritten — it is where your permissions and your own
hooks live. An upgrade adds the `bitmagic reload` Stop hook if it is missing and changes nothing
else; if it cannot parse the file it leaves it entirely alone and says so.

`AGENTS.md` is the one conditional case, and it can never lose your work. The CLI records a hash of
the `AGENTS.md` it writes. On a later upgrade:

- **Still byte-identical to what we wrote** — you never edited it, so refreshing it only replaces
  our words with our newer ones. It is refreshed, and the output says so.
- **Edited by you (or scaffolded before this existed)** — it is left exactly as it is. The output
  names the sections the current template has that yours does not, and writes the current template
  to `.bitmagic/AGENTS.md.latest` so you (or your agent) can diff and merge the parts you want.

That is why `bitmagic cover` could ship and stay invisible: an agent only knows what its `AGENTS.md`
tells it, and nothing was updating that file.

The shipped skills are on the refreshed side deliberately. They describe what the **CLI** can do,
not what your game is, so a project holding the version that came with its scaffold has an agent
that cannot discover anything added since — which is exactly what happened when `bitmagic cover`
shipped. The cost is that edits to *our* skill files are lost on upgrade; put your own guidance in
`AGENTS.md` or a skill of your own, both of which are left alone.

### Which agent your project is set up for

Agents do not agree on where project skills live, so `bitmagic init` writes the shipped set into
`.agents/skills/` — read by Cursor, Codex, GitHub Copilot, Cline and OpenCode — **and**
`.claude/skills/`, whatever agent you used. Both are written every time and both are refreshed by
`upgrade`, so switching agents costs you nothing.

Kiro is the exception, because an unasked-for `.kiro/` is just clutter: pass `--agent kiro` to
`init` (or to `upgrade` on an existing project) and it also writes `.kiro/skills/` and
`.kiro/steering/bitmagic.md`, which points Kiro at `AGENTS.md` — the one supported agent that does
not read it on its own. The choice is recorded as `agents` in `bitmagic.json`, so later upgrades
refresh the same set without the flag. `--agent` only ever ADDS; nothing is removed.

`--agent cursor` and `--agent codex` are accepted and reported as doing nothing, because both read
`AGENTS.md` and `.agents/skills/` and your project already has them.

For an agent with no entry of its own, install the skills into its directory yourself from inside
the project — `npx skills add .agents/skills -a <agent>` covers around seventy of them — bearing in
mind that `bitmagic upgrade` will not refresh what that puts there.

Where an upgrade leaves a project unable to use something the CLI can now do, it says so rather
than fixing it silently: a project with no `GAME-DESIGN.md` is told what to write and that
`bitmagic cover` needs it.

`upgrade` **reports** missing or outdated dependencies rather than installing them — it prints the
exact install command to run, using your project's own package manager (detected from
`pnpm-lock.yaml` / `yarn.lock`, defaulting to npm). A project scaffolded before a dependency was
added needs that command run once before `bitmagic build` (and therefore `bitmagic publish`) will
work.

A project scaffolded before the bundle config it needs existed cannot be bundled at all: `bitmagic
build` stops with a message naming `bitmagic upgrade`, which vendors the file without touching
your game code. This applies to `vite.publish-standalone.config.js` too, which arrived later than
`vite.publish.config.js` — so a project can have one and not the other, and the message names
whichever one your build actually needs. Run the printed install command afterwards if `upgrade`
reports missing dependencies too.

## Machine-readable output

Every command accepts `--json` except `dev`, which starts a server and never terminates with a result to report:

- **stdout** carries exactly one JSON document — the result, or on failure
  `{ "ok": false, "exitCode": N, "error": "..." }`
- **stderr** carries everything else: progress, warnings, and every update nudge

So `bitmagic publish --json > result.json` works while you still watch progress, and an agent
parses one stream whether the command succeeded or not. The exit code remains authoritative.

Without `--json` nothing changes: prose goes to stdout as before and no JSON is emitted.

`--json` suppresses `publish`'s *drawn* QR code, which uses terminal escape sequences: under
`--json` stdout carries the result document and nothing else. The image is still written, and
`qrPath` in the result names it — that is the only way an agent learns the file exists.

The result shape is per-command — `publish` reports
`url`/`visibility`/`publishVersion`/`manageUrl`, plus `qrPath` when it wrote a QR image, `init`
reports `gameId`/`root`/`dependenciesInstalled`/`packageManager`, `upgrade` reports `replaced` and any
`missingDependencies` as structured entries rather than only as a printed install command, plus
`cliUpdate` (`{ current, latest, updateAvailable }`, or `null` when the engine did not move and so
no check was made — which is not the same as "no update"), and `legacyAssetsRepointed` (the
`world.json` default-asset records it moved, each `{ id, name, from, to }`) with
`legacyAssetsSkipped` naming why `world.json` could not be checked, or `null`.

## Staying current

```bash
bitmagic self-update
```

updates the CLI itself. Use it rather than `npm install -g @bitmagic/cli`, which is only for the
first install — see [Why self-update and not npm](#why-self-update-and-not-npm) below.

It installs into the exact place this CLI is already installed, on the line it is already on. Inside
a project it uses that project's line instead (`environment` in `bitmagic.json`), so running it in a
`dev` project from a release build crosses you over; `--tag dev` / `--tag latest` overrides both. It
reads back the version on disk afterwards and fails if it did not move, so a successful-looking
update that changed nothing is reported rather than believed.

It refuses, with the path it looked at, when there is nothing for it to do: a run through `npx`
(nothing is installed), a source checkout (`git pull` and rebuild), or an install shape it does not
recognise — it will not guess at where to write.

### Why self-update and not npm

`npm install -g @bitmagic/cli` installs into whichever Node the `npm` on your `PATH` belongs to,
which is not necessarily the one this CLI is installed in. Under a version manager — nvm, fnm,
Volta — those two are routinely different, and a coding agent's non-interactive shell makes it
worse: it sources no profile, so nvm never loads and `npm` is whatever else is on the system path.
The install then succeeds, into a prefix nothing on your `PATH` points at, and `bitmagic --version`
never moves. The same applies to the two-root pnpm setup below, where a bare `npm i -g` writes to a
third place entirely.

`self-update` resolves its own install from the Node binary running it, so it cannot be aimed
anywhere else.

### The nudges

Three, all advisory — nothing is ever blocked because something is out of date.

- **The CLI itself.** Commands print a line when a newer `@bitmagic/cli` is on npm. The check reads
  a cache written by an earlier run, so it costs no time; the cache refreshes in the background at
  most once an hour on the release line, and every fifteen minutes on `dev` — that line is cut from
  every merge to `main` and can move several times an hour, so an older answer there is usually
  about a build several versions back. It asks about the line you are actually on — a build
  installed from `@dev` asks the `dev` dist-tag and hears about the next dev build, rather than
  being told the production release is "newer". An unreachable registry, a first-ever run, and a
  version with no recognisable numeric core all stay quiet. `bitmagic dev` also shows it as a
  banner in the browser, because that command runs for hours and the terminal line scrolls away.
- **The CLI, after an engine bump.** When `bitmagic upgrade` actually moves the engine version, it
  asks npm there and then instead of trusting that cache — which can be a whole interval stale, and
  so silent about the CLI published alongside the engine you just vendored. The CLI and the engine
  ship together, so a newer engine can need build config or a command only the newer CLI has, and
  finding that out as a failed `bitmagic build` is worse than being told here. It names the line the
  *project* is on rather than the one you happen to be running, so it says `bitmagic self-update
  --tag dev` where that is the crossing you need. No request is made when the engine version did not
  change, and an unreachable registry costs the hint and nothing else.
- **The vendored engine.** `bitmagic dev` and `bitmagic publish` say when a newer engine exists and
  suggest `bitmagic upgrade`. Publishing is never refused for an old engine — the prompt belongs
  where rebuilding is cheap, not at the moment you ship.

## Usage reporting

While you are logged in, each command tells Bitmagic how it went when it ends. That is: the command
name, whether it succeeded, its exit code and a failure *category* (`unreachable`, `pro_expired`,
`insufficient_sparks`, …), how long it took, and the CLI, Node and OS versions. Commands that talk to
Bitmagic anyway — `generate`, `forge`, `publish` — carry the same identification on the calls they
already make, which is how a spark charge can be attributed to the command that caused it.

**What is never sent:** your game's source, file names, paths, or command output. Failures are
reported as categories, never as messages, because a message can quote a path.

**What you type into a command is kept.** The text you pass to `bitmagic forge`, `bitmagic
generate`, `bitmagic cover`, `bitmagic reference make` and `bitmagic init --idea` — the
descriptions of the world, the asset, the game you want — is stored against your account so we can see what creators are trying to make
and where the generators fall short. It is the same thing the web editor already keeps for prompts
typed there, and it goes to the same place. None of it reaches Google Analytics, which only ever
receives counts, outcomes and timings.

**What you asked your agent for is kept too, when it passes it on.** The scaffolded project docs
tell your agent to add `--original-prompt "<your request, verbatim>"`, so on those commands the
wording *you* used — typically what you typed into Claude Code or Codex — is stored beside the
command's own text and as a record of its own. The CLI has no other view of your agent's
conversation: it sees only the arguments your agent chooses to pass it, and this is the one
argument that is your words rather than its paraphrase. It is optional, never required, and never
influences what gets generated.

Set `DO_NOT_TRACK=1` in your environment to switch the end-of-command report off. Nothing is
collected for it in that case — not even the file reads it would have needed. It also stops
`--original-prompt` being forwarded: the CLI drops it at the point every command reads its flags,
so no request carries it, whether or not your agent passed it.

It does **not** switch off the paragraph before that one. The text you pass to `forge`, `generate`,
`cover` and `init --idea` is not telemetry: it is the request, and the server cannot generate
anything without it. If you would rather it were not kept, the honest answer is that today there is
no flag for that — tell us and we will add one.

The report is deliberately unobtrusive but it is not free: it is capped at 1.5 seconds per request
(two at worst, when your access token is due for a refresh first), it never prints, and it can never
fail a command — but it does happen at exit, so a command can take that much longer to return on a
network that is dropping packets. It is skipped entirely when you are logged out.

## Licensing

`bitmagic init` vendors the engine into your project as readable source. It is Bitmagic's IP,
licensed to you — not assigned — under the [PolyForm Shield License
1.0.0](https://polyformproject.org/licenses/shield/1.0.0) plus an additional permission covering
games. The full text lands in your project as `engine/LICENSE.md`.

**You may** build games with it and sell them, modify the engine however your game needs, and ship
your games anywhere — bitmagic.ai, your own site, Steam, itch.io, a desktop build. Publishing
through Bitmagic is never a condition. Every engine version you obtained stays licensed to you
permanently; a lapsed subscription ends access to *new* versions and the hosted services, not your
right to keep shipping what you already built.

**You may not** provide the engine as the engine — distributing, selling, sublicensing or hosting
it, modified or not, free or paid, as a game engine, framework, SDK, library, starter kit or
game-creation tool.

One practical consequence: a published game contains compiled engine code, so the licence requires
the notice to travel with it. Both bundle configs carry a `banner` that does this automatically,
along with the attribution three.js, Rapier and the other libraries require — which a
`--single-file` build inlines outright, and a default build still ships loaders for. Leave it in
place. `engine/THIRD-PARTY-NOTICES.md` has the full texts.

## Development

```bash
pnpm run check    # lint + typecheck (from cli/, or from the repo root for every project)
pnpm test          # jest
```
