Pixel Labdeveloper docs

Reference

CLAUDE.md#

Pixel Lab for Mac: pixellab, one Rust program that is the pixel-art engine and compiler, the CLI and the MCP server (crates/engine, crates/pixellab), and a SwiftUI Mac app that bundles it, for isometric pixel art kept in a .pixellab document (a folder in format 2: manifest.json, palette.json, objects/*.json, scenes/*.json; pixellab document migrate converts format 1). The owner's document lives in the site repo ~/Projects/jlawcordova.github.io, which takes only the program's output: self-contained SVGs (docs/site-contract.md). Read docs/intents/2026-10-mac-app/plan.md first: it is the architecture and design record, including the look (mood board 03, "Field notebook"); docs/intents/2026-10-pixellab-document/ is the change that moved the engine here and made the document. README.md has the user-facing tour, shortcuts and MCP setup; docs/cli.md is the command reference. docs/intents/2026-10-rust-engine/ is the change that ported the engine and CLI to Rust and made the art self-contained; its plan's "Departures" record what was built differently.

Commands#

  • Build the program: cargo build --release → target/release/pixellab (the toolchain is pinned in rust-toolchain.toml; rustup installs it). Crates: serde, serde_json and miniz_oxide only.
  • Test the program: cargo test (no failures) and cargo fmt --check
  • Test the repo's tooling: npm test (builds the program, then runs scripts/test/; must show # fail 0)
  • CLI: target/release/pixellab help; pixellab --version
  • CLI reference: npm run docs:cli regenerates docs/cli.md from pixellab help --markdown. Run it after changing a command; never edit docs/cli.md by hand.
  • Developer docs site: npm run docs:site builds build/docs-site/ from the README files and docs/ (scripts/docs-site/); it fails on a broken link, anchor or image. .github/workflows/docs.yml builds it on every PR and deploys main to GitHub Pages. Adding a page: docs/contributing.md, "The docs site".
  • App: swift build --package-path app, then scripts/build-app.sh → build/Pixel Lab.app
  • App snapshot (no screen recording needed; read-only): scripts/snapshot.sh <out.png> [scene:hero-island] [dark|light] [zoom]
  • App unit tests (Swift Testing, works with the Command Line Tools): scripts/test-app.sh
  • App test hook (drives the model headlessly, no UI automation): PIXELLAB_SCRIPT=<file> PIXELLAB_DOC=<temp copy of a document> PIXELLAB_STATE=<temp dir> "build/Pixel Lab.app/Contents/MacOS/PixelLab" (or PIXELLAB_SITE=<temp site copy>). One action per line (open scene:hero-island, select 145.0, click <x> <y>, library pine, hover 3 -1 1, place 3 -1 1, key opt+up, drag 1 0 hold, mouse down|drag|up <x> <y> [opt], save, publish [<dir>], saveas <path.pixellab>, time 9400, snapshot <png>, expect at 4,-1,1; object mode: mode object, color ink, stroke 3,4 9,9, mousepath 3,4 6,6 9,9, erase 5,5, px 5 5 opt|shift, fill 7 23, pick 30 5, layer add|up|hide 0, frame next|add, animate wf, onion on, new object rock 12x8, new scene meadow, expect pixel 7,7 roof-1; autosave and versions: flush, wait saved, revertto lastopened, versions open|close, version select 2|newest|next|none, version restore [doc], version undo [<n>], version answer yes|no, ghost on|off, expect savestate|subtitle|undolabel|redolabel|owner|versions|ghost|ghostchanges|ghosttime|retired <v>, expect version <n>|newest author|edits|files <v>, expect versionresult <text>; Agent window: agent open, snapshot agent <png>, menus; files changing on disk: cli <args…> (the bundled CLI on the script's document, from its own state dir: an outside edit), outside <file> <field.path> <json>, stale reload|keep, expect stale|mergealert true|false; one writer: cli-owned <args…> (through the app), mcpcall <tool> <json>, secondapp; also expect scenes|objects|zoom|theme|build|outputs|publishdir|document|documentkind <v> and expect scenepixel x,y #RRGGBB, …; scene layers: select layer:train 12.0, select add <target>, click <x> <y> shift, dblclick <x> <y>, pointer <x> <y>, rightclick <x> <y>, rightclick row layer:train, menu group|ungroup|inside|hide|lock|close, group, ungroup, inside <id>|done|top, crumb 1, hide|show|lock|unlock|expand|collapse <id>, rename <name>, movedby 1 0, layernudge downright, placeinto, layerskey shift+down, sidebar layers|library, expect selection|inside|scenelayers|layername|itemlayer|hiddenlayers|lockedlayers|into|crumbs|rows|movedby|menu|tab|inspector|faded|hover <v>; the Agent chat (with PIXELLAB_CLAUDE_HOME and PIXELLAB_CLAUDE_INSTALLER on scripts/fake-claude/, never the owner's Claude Code): righttab agent, setup install|signin|cancel|signout|wait <state>, chat new|list|open 2|type <t>|send <t>|stop|wait, chatkey tab|return|shift+return|esc, details on, steps expand 1, choose 1, link pine 14.0, chip pin|unpin|remove <target>, savenow, card undo|show, altclick <x> <y> [shift], selectuses, activity chats, askclaude, agentwindow, expect setup|righttab|chats|chattitle|messages|steps|steplabel|chatquestion|chosen|cards|cardstatus|cardtitle|chips|pinned|unsaved|contextline|suggestion|placeholder|draft|running|claudeinfo|lastnote|activityrows|banner <v>, expect lastreply|context <text>; depth: pinch in|out [<x> <y>], pinchby <factor> [<x> <y>] (a smooth pinch), zoomkey in|out, library open|close (the Library popover; library <name> picks an object and starts placing; tool place is gone), librarydrop <name> <col> <row> <level>, editobject [<name>], back, hide|show|lock|unlock <item path>|selected, expect anchor|library|floatingbar|toolbar|inspector shown|returnscene|hiddenitems|lockeditems <v>; depth front|back|auto (the inspector's Depth control), groupdepth <path>|selected front|back|auto, sort, footprint <w>,<d> (the open sprite), expect depth|level <v>, expect groupdepth <path>|selected <v>, expect order pine@0,1,0 walls (entries in list order: a path, #id, selected, an object name or name@col,row,level), expect footprint <w>,<d>, expect warning|nowarning out_of_order, expect cli <text>; a format-1 document: migrate, answer migrate|cancel, expect migrate|phase|problem <v>; ambient loops and actions: play <action> [<item>], playat <action> <ms>, playend, reducemotion on|off|system, expect ambient|timeline|actions|playing <v>, expect lastplay|announced <text>; object libraries and palette files: category Nature/Trees|none (the open object's Category field), librarytab <name>|all|others, libraryquery <text>, import library|palette <path>, importsheet convert <missing> <existing>|keep <color>|theirs <color>|add <color>|remove <color>|import|cancel, export library <path> [category], export palette <path>, openfile <path> (a .pixellib asks first; answer offer import|cancel), dropfile <path>, expect tabs|libtab|headings|libraryobjects|libraryorder|category|categorydraft|categoryerror|importundo|offer <v>, expect objectcategory <name> <path|none>, expect palettecolor <name> <#hex|none>, expect importsheet sheet|renames|differs|missing|nearest|new|over|count|button|canimport|error <v>; the full list is in app/Sources/PixelLab/Support/ScriptRunner.swift). It refuses to run without PIXELLAB_DOC or PIXELLAB_SITE, or against the real checkout or anything inside it, logs each step to stderr, and exits 0 only if every step and expect passed. Make the temp copy with pixellab document copy --to <temp>.pixellab, or copy the site's src/, scripts/, package.json and package-lock.json for a temp site.
  • Acceptance pass (end to end on a temp site copy: scene edit → save → publish, the Agent chat against the fake Claude Code in scripts/fake-claude/ (setup, chats, context, change cards undone), object paint → save, an MCP agent's edits applied and a version taken back, a change on disk merged into a pending save and a clash reloaded or kept, autosave and the Versions panel, one writer (the CLI and an MCP client through the app, a second copy refused), save as → reopen the copy → publish into a folder, scene layers made, picked, gone inside, hidden, locked, moved and ungrouped with the hero island's SVG byte-identical, then the example documents' layers on temp copies of examples/): scripts/acceptance.sh [out-dir]; its scripts are in scripts/acceptance/. It migrates its temp site copy with pixellab document migrate first; migrate.txt runs on a format-1 copy of the site's document (git archive of a site revision, read-only) and actions.txt on a temp harbor copy. Its expect lines pin the site's art as it was on 2026-10-21; update them when the art changes. Never run the owner's real claude from a test or script: use the fake (scripts/fake-claude/README.md).
    • scripts/acceptance.sh --only <name>[,<name>...] (or PIXELLAB_ACCEPT_ONLY) runs only those stages (a stage file's name in scripts/acceptance/ without its extension; the shared setup always runs). Running the stages a change touches is enough; the full pass is optional.
  • Install for the owner: scripts/install.sh (builds, copies to ~/Applications, prints the claude mcp add command; --dest <dir> for a test target, --no-build). Never install into /Applications and never run claude mcp add for the owner.
  • Release: pushing a tag v<major>.<minor>.<patch> on main runs .github/workflows/release.yml, which tests on Linux and macOS, builds, smoke-tests and publishes a GitHub Release with PixelLab-darwin-arm64.dmg and .zip (the app, with the program at Contents/Helpers/pixellab), pixellab-darwin-arm64.tar.gz, pixellab-linux-x64.tar.gz (scripts/package-program.sh) and SHA256SUMS. The tag is the version: the workflow stamps it into the app bundle and the program (PIXELLAB_VERSION), and the repo's package.json stays 0.0.0-dev, so never bump a version in a PR. Steps are in README.md, "Releases". Don't push a release tag unless the owner asks.
  • Examples: examples/ holds two documents with scene layers (harbor.pixellab, town.pixellab), made by scripts/make-examples.mjs; npm run examples rewrites them (commit both together; npm test checks they match). scripts/try-example.sh [harbor|town] opens the built app on a temp copy of one. Test against a copy, never the files in examples/.
  • App icon: node scripts/make-icon.mjs (renders with the program) rewrites app/Sources/PixelLab/Resources/AppIcon.png (build-app.sh makes the .icns)

Verifying your work#

  • Program: cargo fmt --check, cargo test with no failures, and npm test with # fail 0. Never skip or delete a failing test.
  • App, when app/ changed: swift build --package-path app with no errors, and scripts/test-app.sh with every test passing.
  • End to end, when app/, crates/ or scripts/acceptance/ changed: scripts/build-app.sh, then scripts/acceptance.sh --only <stages> exits 0 for the stages that cover the change (and any stage file you changed); name them in the PR. The full pass (no --only) isn't required.
  • Commands: after changing one, npm run docs:cli leaves git status clean apart from your own edits.
  • Docs: after changing a doc or a README, npm run docs:site builds with no broken links.
  • UI: drive the change with the script hook and its expect lines, and snapshot each changed view in dark and light (snapshot <png> in a script, or scripts/snapshot.sh). Look at the snapshots before reporting.

Run these before reporting any task complete, and paste the output. If a test fails, fix the code, not the test. Something you couldn't run is "not verified", never passed.

Rules#

  • The document is the source of truth for the art, and the engine lives here, in Rust (crates/engine); the site takes only output. A change to the engine is a change here, released by tag, and the site bumps its pinned pixellab release. Never write to the real site checkout, or the real document inside it, from tests or scripts: use a temp copy (make_document(), make_site() in crates/pixellab/tests/common/). A change to the site's art is a change in the site repo, under its own workflow.
  • No new crates, npm or Swift dependencies without asking.
  • Never use xcode-select -s or change system settings. Use DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer per command if full Xcode is needed.
  • The app builds with the Command Line Tools, which lack the SwiftUI macro plugin: don't use @State (keep view state in AppModel); @Observable, @Environment, @AppStorage, @FocusState are fine.
  • Colors in the app come from the board 03 tokens in app/Sources/PixelLab/Theme/Theme.swift. Pixel art is drawn nearest-neighbor at integer zoom.
  • Every action is a command in the registry (crates/pixellab/src/commands/, listed in registry.rs), so the CLI, the app (serve --stdio) and agents (serve --mcp) can all take it. Agents' edits apply as they make them, through the app while it owns the document (docs/intents/2026-10-autosave/); every write is a version in the state dir's history, which the owner can browse and take back. Publishing stays a person's: art.build isn't an MCP tool.
  • Keep VoiceOver labels, visible focus, comfortable hit targets and Reduce Motion support on anything interactive or animated.
  • Commit messages end with Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>. PR bodies end with 🤖 Generated with [Claude Code](https://claude.com/claude-code).
  • No secrets, tokens or personal data in commits, PRs or comments. The repo is public, and so are the art it edits (published by the site) and the docs site.

Architecture#

The record has the full picture (docs/architecture.md). In short: crates/engine/ is the engine (iso, depth, motion, ambient for ambient loops and actions, engine to validate, resolve, render and composite, edit, svg, compile with the optimizer, lossless check and budgets, preview, format for canonical JSON, migrate for format 1, jsnum for JavaScript number parity). crates/pixellab/ is the program (cli.rs; registry.rs and commands/ hold every command; document.rs resolves the document, or the one inside a site checkout; session.rs holds open docs with undo and redo; scene_ops.rs, layer_ops.rs and motion_ops.rs are the scene editing logic; server_stdio.rs and server_mcp.rs the two servers; watch.rs polls for changes; state.rs the state dir and the activity log; history.rs the version history; owner.rs and client.rs one writer per document), with suites in crates/*/tests/ and the fixture in fixtures/. app/ is the SwiftPM package (Server/ talks JSON-RPC to the bundled pixellab serve --stdio; Model/ holds AppModel and the editors; Views/ the windows; Theme/ the tokens; Support/ScriptRunner.swift the test hook), with Swift Testing suites in app/Tests/. scripts/ builds, installs, packages, snapshots and runs the acceptance pass; Node is only repo tooling there.

How changes flow#

Planned work follows Anthropic's AI-native SDLC playbook, the same way as the site repo. Each stage commits an artifact, and the next stage starts by reading it:

  1. intent.md: what's wanted and why, in the owner's words. Use the write-intent skill. The owner accepts it by merging.
  2. spec.md: numbered requirements and design from the accepted intent, with areas of concern flagged. Use write-spec. The owner approves it by merging.
  3. plan.md: files, order of work, workflow, risks and proof, written before any code. Use write-plan. Its workflow says who runs each step (the main session or subagents), which steps pause for the owner's review, and whether any independent review runs. Implement only against an approved plan, and update plan.md in the same commit when the work departs from it.
  4. Code and tests, following the plan's workflow, then a PR that cites the plan and pastes the verification output. Name tests after the requirement they prove (R3: …). Run the steps back to back. Hand over to the owner only at a review the plan names, or when blocked (write-plan, "Workflow and reviews").
  5. Independent verification, when the plan calls for it: a fresh session, given only "Use the verify-change skill on PR #<n>.", checks the PR against its intent, spec and plan and reports as a PR comment. It never pushes or commits. Fix its blocking findings before the PR merges. Without one in the plan, the implementer's checks in the PR are the verification. The owner can still ask for a run on any PR.

A small, self-contained fix can go straight to a PR into main.

Planned work runs on a main-session branch, feat/<slug>, where every slice PR (feat/<slug>-<slice>) merges, and reaches main in one final PR (write-plan, "Branches"). A one-slice change may go straight to main. Agents may merge slice PRs into feat/<slug> once their checks pass, after any review the plan names for them. Only the owner merges into main, because it's what Pixel Lab is installed from; a hook in .claude/settings.json blocks agents from doing it.

Each change lives in docs/intents/YYYY-MM-<slug>/ (the month its intent was created), holding its intent.md, spec.md, plan.md and any notes. Don't create top-level docs/specs/ or docs/plans/ folders.

  • Link within a change by sibling path (spec.md), and to another change with ../YYYY-MM-<slug>/intent.md.
  • When a doc moves, update every mention of it, including code comments.

If documents disagree, the intent wins over the spec and the spec over the plan. Fix the lower one. 2026-10-mac-app predates these templates (it has only a plan); don't rewrite it to match. Its follow-ups list is a good source of intents.

Things Claude gets wrong#

When Claude makes the same mistake twice, add the correction here.

  • Large pushes (screenshots) can fail on GitHub's default buffer: use git -c http.postBuffer=157286400 push.
  • A branch cut from origin/main tracks it, so a bare git push targets main and the hook blocks it. Push with git push -u origin <branch>.
  • scripts/acceptance.sh pins today's art; a failing expect after the site's art changed means updating the script, not the app.

Source: CLAUDE.md · built by npm run docs:site