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 inrust-toolchain.toml; rustup installs it). Crates:serde,serde_jsonandminiz_oxideonly. - Test the program:
cargo test(no failures) andcargo fmt --check - Test the repo's tooling:
npm test(builds the program, then runsscripts/test/; must show# fail 0) - CLI:
target/release/pixellab help;pixellab --version - CLI reference:
npm run docs:cliregeneratesdocs/cli.mdfrompixellab help --markdown. Run it after changing a command; never editdocs/cli.mdby hand. - Developer docs site:
npm run docs:sitebuildsbuild/docs-site/from the README files anddocs/(scripts/docs-site/); it fails on a broken link, anchor or image..github/workflows/docs.ymlbuilds it on every PR and deploysmainto GitHub Pages. Adding a page:docs/contributing.md, "The docs site". - App:
swift build --package-path app, thenscripts/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"(orPIXELLAB_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; alsoexpect scenes|objects|zoom|theme|build|outputs|publishdir|document|documentkind <v>andexpect 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 (withPIXELLAB_CLAUDE_HOMEandPIXELLAB_CLAUDE_INSTALLERonscripts/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 placeis 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 orname@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.pixellibasks 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 inapp/Sources/PixelLab/Support/ScriptRunner.swift). It refuses to run withoutPIXELLAB_DOCorPIXELLAB_SITE, or against the real checkout or anything inside it, logs each step to stderr, and exits 0 only if every step andexpectpassed. Make the temp copy withpixellab document copy --to <temp>.pixellab, or copy the site'ssrc/,scripts/,package.jsonandpackage-lock.jsonfor 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 ofexamples/):scripts/acceptance.sh [out-dir]; its scripts are inscripts/acceptance/. It migrates its temp site copy withpixellab document migratefirst;migrate.txtruns on a format-1 copy of the site's document (git archiveof a site revision, read-only) andactions.txton a temp harbor copy. Itsexpectlines pin the site's art as it was on 2026-10-21; update them when the art changes. Never run the owner's realclaudefrom a test or script: use the fake (scripts/fake-claude/README.md).scripts/acceptance.sh --only <name>[,<name>...](orPIXELLAB_ACCEPT_ONLY) runs only those stages (a stage file's name inscripts/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 theclaude mcp addcommand;--dest <dir>for a test target,--no-build). Never install into/Applicationsand never runclaude mcp addfor the owner. - Release: pushing a tag
v<major>.<minor>.<patch>onmainruns.github/workflows/release.yml, which tests on Linux and macOS, builds, smoke-tests and publishes a GitHub Release withPixelLab-darwin-arm64.dmgand.zip(the app, with the program atContents/Helpers/pixellab),pixellab-darwin-arm64.tar.gz,pixellab-linux-x64.tar.gz(scripts/package-program.sh) andSHA256SUMS. The tag is the version: the workflow stamps it into the app bundle and the program (PIXELLAB_VERSION), and the repo'spackage.jsonstays0.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 byscripts/make-examples.mjs;npm run examplesrewrites them (commit both together;npm testchecks 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 inexamples/. - App icon:
node scripts/make-icon.mjs(renders with the program) rewritesapp/Sources/PixelLab/Resources/AppIcon.png(build-app.sh makes the .icns)
Verifying your work#
- Program:
cargo fmt --check,cargo testwith no failures, andnpm testwith# fail 0. Never skip or delete a failing test. - App, when
app/changed:swift build --package-path appwith no errors, andscripts/test-app.shwith every test passing. - End to end, when
app/,crates/orscripts/acceptance/changed:scripts/build-app.sh, thenscripts/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:clileavesgit statusclean apart from your own edits. - Docs: after changing a doc or a README,
npm run docs:sitebuilds with no broken links. - UI: drive the change with the script hook and its
expectlines, and snapshot each changed view in dark and light (snapshot <png>in a script, orscripts/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 pinnedpixellabrelease. 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()incrates/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 -sor change system settings. UseDEVELOPER_DIR=/Applications/Xcode.app/Contents/Developerper 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 inAppModel);@Observable,@Environment,@AppStorage,@FocusStateare 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 inregistry.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.buildisn'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:
intent.md: what's wanted and why, in the owner's words. Use thewrite-intentskill. The owner accepts it by merging.spec.md: numbered requirements and design from the accepted intent, with areas of concern flagged. Usewrite-spec. The owner approves it by merging.plan.md: files, order of work, workflow, risks and proof, written before any code. Usewrite-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 updateplan.mdin the same commit when the work departs from it.- 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"). - Independent verification, when the plan calls for it: a fresh session, given only "Use the
verify-changeskill 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/maintracks it, so a baregit pushtargetsmainand the hook blocks it. Push withgit push -u origin <branch>. scripts/acceptance.shpins today's art; a failingexpectafter the site's art changed means updating the script, not the app.