A live, isometric pixel-art "management-sim" that turns your real local AI coding sessions — Claude Code, Codex CLI, and friends — into a cozy monastery of working monks. Bosses, workers, and clerks glide between desks, take breaks by the koi pond, and deliver work in real time, because they are your real work: every sprite is backed by an actual running session on this machine. It's a game you play as your own work environment.
Alongside the diorama there's a full build cockpit — a live finance/payroll ledger, a mission desk that triages and dispatches briefs to contractor CLIs, a Forge for commissioning real apps from inside the game, and a shared space where real people can drop in, chat, and spectate together.
Pixel Atelier reads local files only:
~/.claude/projects/**/*.jsonl— Claude Code session transcripts (including subagent transcripts nested undersubagents/).~/.codex/sessions/**/rollout-*.jsonl— Codex CLI rollout files.- A periodic
pssweep to notice other known AI CLIs running as guest processes (ollama, aider, gemini, goose, Cursor, ...).
All of it is read-only and stays on this machine. Nothing is sent anywhere except when you explicitly use the concierge/CFO LLM layer (which calls whatever provider you've configured) or the optional remote-access features (Cloudflare Tunnel / Tailscale, which you install and run yourself). The server does not phone home on its own.
- Python 3.9+, standard library only — no
pip installneeded, anywhere. - Optional, for the native macOS app: Xcode Command Line Tools (
xcode-select --install) soswiftcis available. - Optional, for local session data to show up at all: you need to have
actually run
claudeorcodexat least once on this machine, so their transcript files exist.
git clone <this-repo-url> pixel-atelier
cd pixel-atelier
python3 server.pyThen open http://127.0.0.1:4560 in any browser, on any OS. On first run
the server creates its own local data files (see below) and prints a
32-character access token, writing it to a secret.token file (chmod 600)
in the repo directory — see Security below.
Change the port with the PORT environment variable, e.g. PORT=8080 python3 server.py.
Browsers only allow microphone access (getUserMedia) in a secure context:
https://, http://localhost, or a Tailscale/Cloudflare-tunneled origin.
Plain http://<lan-ip> will not work for voice, even though chat and
presence work fine over plain HTTP.
python3 server.py --httpsThis starts a second server on port 4561 over HTTPS with a self-signed
cert (generated once into a local certs/ folder). Colleagues click through
the "not trusted" warning once — expected for a local self-signed cert.
If you're on macOS and have Xcode Command Line Tools installed:
cd native
./build-app.shThis builds native/PixelAtelier.app — a small native Swift/Cocoa+WebKit
shell that launches the server for you, lives in the Dock and menu bar, and
points at the server.py in this same repo checkout no matter where you move
the built .app. Open PixelAtelier.app and it auto-starts the server on
first launch.
Nothing personal ships in this repo. The first time you run server.py (or
touch a given feature), it creates, in the repo directory: secret.token,
keys.env, config.json, roster.json, finance.json, ledger.json,
prices.json, cfo-tasks.json, missions.json, and builds.json — each
seeded with a sane empty/default structure. All of these (plus logs/ and
certs/) are covered by .gitignore, so your local data and keys never get
committed. config.sample.json ships as a reference starting point for
config.json.
Provider API keys you add to keys.env (or environment variables) and the
access token in secret.token never leave this machine except as part
of a request you make to that provider's API.
The rooms are now fully enclosed (back walls, sheared windows with drifting clouds, wall clocks, framed art, posters, book shelves) and the patio has been rebuilt as a living garden: a koi pond with ducks, lily pads, reeds and ripples; a great blossoming tree that sheds petals and casts god rays by day; flower beds with butterflies; fireflies and lit string lights at night; stone paths, garden benches and a bird feeder. Smoking is gone — agents on break feed the koi, scatter crumbs (the birds actually converge on them), watch the fountain or rest under the great tree, and stalled agents drift out to the pond's edge until their transcript moves. Agents no longer walk: they hover, gliding at double speed with a levitation aura and a trail of sparks.
The office is now a full build cockpit:
- The Forge (
F) — commission a real app from inside the game: name, brief, stack (static/node/python), builder model. A real Claude Code session is spawned on this machine in its own workspace under~/AtelierBuilds/<slug>with a reserved preview port, a production-grade BRIEF.md (dev server on its port, live STATUS.md, steering protocol), and the builder glides into the office like any other colleague. - The Gallery (
G) — live iframes of every build's dev server: watch the app visibly grow while the builder works. Status lamp (up/down), file count, last-touched, the builder's own STATUS.md headline, and per-build REFRESH / OPEN / WATCH BUILDER / STEER (drops a note into the workspace's STEERING.md that the builder reads between steps). For static builds the Forge supervises the preview server itself, so there is always something to look at as soon as an index.html exists. - The Activity River (
R) — a realtime dock streaming every agent's latest tool call and utterance across all platforms, color-coded by provider; click any entry to jump into that agent's live transcript.
Server endpoints: POST /api/forge, GET /api/builds,
POST /api/forge/steer (all token-gated). Restart server.py after
updating to load the new forge.py module.
The diorama is for feel; the Ops Board is for running the fleet. The ⌖
button (or the O key) opens a live command table over every session the
adapters can see — Claude, Codex, and guest processes side by side. Summary
chips (click to filter) count the fleet per platform and status; a pulsing
attention queue pins stalled agents to the top with how long they've been
stuck and their last tool; the table below shows every agent's platform,
project, status with time-since-activity, model, last action, and jobs
delivered, with a free-text filter across all of it. Every row has one-click
WATCH (live CRT transcript), STEER (dossier with a steering note that
lands in the agent's loop), and LOCATE (fly the camera to the sprite).
It refreshes every 2 seconds while open; Esc closes it.
- Claude adapter — same tail-read/cache/status logic as Phase 1, now
factored into
adapters.scan_claude(). Also extracts per-messageusageblocks for payroll. - Codex adapter — parses
~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl(session_meta,turn_context,response_item,event_msgline types). Codex agents render withprovider:"codex",tier:"gpt5", teal/white in the UI, seated in a distinct Atelier desk cluster. Token deltas come fromevent_msg.payload.info.last_token_usageontoken_countevents. - Process-presence adapter — every 10 seconds runs
ps -axo comm=and matches against a known-AI-process table (codex-tui,Codex,ollama,lms,aider,gemini,goose,Cursor). Matches with no transcript data become "guest" sprites in the Humans Lounge (ollama · guest). - Real-time push — the scan loop runs every 1 second (stat-only; the
heavier
pssweep is throttled to its own 10s cadence). Any agent diff (new agent, status change, newlastText/lastTool) is broadcast as an"agents"SSE event on/api/events; the frontend consumes SSE first and falls back to polling/api/stateif the stream drops.
Every agent gets a stable, deterministic identity keyed by
provider+project+tier+kind, persisted in roster.json in the repo
directory: {name, hiredAt, jobsDelivered, quirk}. Names are picked
deterministically (hash of the key) from a built-in ~60-name list; quirk
similarly biases idle animation (plant lover / coffee addict / window
gazer). jobsDelivered increments whenever an agent transitions working →
done. The CFO (below) is a permanent roster entry (cfo::permanent).
Click WATCH in an agent's dossier to open a full-screen pixel CRT overlay (scanlines, phosphor-green text, chunky bezel, agent name + project in the title bar). Three modes:
- summary — assistant text + tool names only, ~200 chars each.
- verbose — parsed
{t, kind, body}entries:thinking(dim/italic, 💭 prefix),text,tool_use(⚙ prefix),tool_result(truncated ~500 chars),user. - raw — raw jsonl lines.
Works across Claude main sessions, Claude subagents, and Codex rollouts.
POST /api/watch {sessionId, on} toggles server-side watching; watched
sessions push "transcript" SSE events (newest verbose entries) whenever the
scanner sees the file grow. Watched agents show a blinking "REC ●" on their
nameplate. All transcript reads are validated to resolve under
~/.claude/projects or ~/.codex/sessions (adapters.is_path_allowed).
- Payroll — every Claude
usageblock and Codextoken_countdelta is accumulated intofinance.json, deduped by request/message id so tail re-reads never double-count. Cache reads cost 0.1× the input rate; cache writes cost 1.25× the input rate. - Prices (
prices.json) — seeded from Anthropic's price sheet (fable-5 $10/$50 per MTok, opus $5/$25, sonnet $3/$15 — $2/$10 intro until 2026-08-31, haiku $1/$5, gpt5/codex $1.25/$10 marked "unverified estimate").fableis locked ("locked": true) and never overwritten. A daily background job (andPOST /api/finance/refresh-pricesfor manual refresh) fetches BerriAI/litellm's public price sheet and updates non-locked tiers if the new rate passes a sanity check (0 < $/MTok < 1000). - Subscriptions ledger (
ledger.json) — editable list of{name, monthlyCost, renewalDay, note}; the ticker warns when a renewal is ≤3 days out. - CFO tasks (
cfo-tasks.json) — a simple add/complete checklist. - FINANCE OFFICE panel — weekly burn headline, a hand-drawn 7-day bar
chart (canvas, no chart library), top-5 most expensive "colleagues" by
roster name, per-project spend, a subscriptions table + add form, the
tasks checklist, "prices as of
<date>·<source>" + a refresh button, and an "ASK CFO" button that runs a real LLM completion over the live finance JSON. - Costs are estimates, not billing truth. Every dollar figure here is a token-count estimate against a price sheet that may lag actual invoiced rates (especially for subscription-covered usage, which isn't metered at all in reality). Treat it as a relative signal, not an invoice.
- First run generates a 32-hex token into
secret.token(chmod 600) in the repo directory; printed at startup.python3 server.py --rotate-tokenregenerates it. - Requests from 127.0.0.1 only bypass auth. Every other client — LAN,
Tailscale, tunnel — must supply
?token=<token>(which then sets anHttpOnlycookie,Securewhen behind a detected HTTPS proxy) or an existing valid cookie. Token comparisons usehmac.compare_digest. Unauthenticated remote requests get a pixel-styled 401 page with a token input; failing auth 5×/minute from one IP gets a 429 page. - Security headers on every response:
X-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy: no-referrer. - Path safety:
/api/steermay only writeSTEERING.mdinside the agent's own resolved realcwd(or the repo'ssteering/fallback); transcript reads are restricted to~/.claude/projectsand~/.codex/sessions;secret.tokenandkeys.envcan never be served as static files even though they live in the same directory asindex.html.
-
python3 server.py --info— prints your LAN IP, Tailscale IP (if the Tailscale.app CLI is reachable and signed in), and the token URL. -
python3 server.py --tunnel— spawns~/.local/bin/cloudflared tunnel --url http://127.0.0.1:4560, parses thehttps://*.trycloudflare.comURL from its output, and prints it with the token appended. -
python3 server.py --tunnel-named <name>— runscloudflared tunnel run <name>and (re)writesDEPLOY.mdwith exact one-time steps for a stable domain of your choosing, a second LaunchAgent to keep the tunnel itself alive, token-as-password guidance, and the Tailscale alternative. See the generatedDEPLOY.mdfor the full walkthrough. -
Always-on (macOS):
python3 server.py --install-autostartwrites acom.pixelatelier.serverLaunchAgent (RunAtLoad, KeepAlive, logs to a locallogs/folder) and loads it vialaunchctl bootstrap gui/$UID <plist>(falling back tolaunchctl load -won older macOS).--uninstall-autostartreverses it.This is implemented but never executed automatically. Run
python3 server.py --install-autostartyourself, once, when you want the office to survive reboots/logouts.
complete(prompt, system=None, role="concierge") tries providers in this
order, skipping unconfigured ones and falling back on error/timeout (hops
are logged to the ticker):
- claude-cli (
claude -p ... --output-format text, your Claude subscription) — 120s timeout. Output/stderr is scanned for daily-limit language ("limit", "rate limit", "usage limit", "out of ... quota", or a nonzero exit code); on a hit, claude-cli is marked "cooling" for 30 minutes and the chain continues. - codex-cli (
codex exec ..., your Codex subscription) — 300s timeout. - grok —
api.x.ai/v1/chat/completions,XAI_API_KEY. - openai —
api.openai.com/v1/chat/completions,OPENAI_API_KEY. - gemini —
generativelanguage.googleapis.com,GEMINI_API_KEY.
Keys live in keys.env (chmod 600, auto-created with a commented template —
note a Google Workspace Gemini plan does not give you an API key for
this file; get one from
aistudio.google.com/apikey) with a
process-env fallback. Keys are never sent to the frontend —
GET /api/providers only returns {name, configured, model, lastStatus, cooling}.
HARNESS.md (in the repo directory) is loaded fresh from disk on every
completion call when system isn't explicitly overridden (a one-line
fallback is used if it's missing) — edits apply instantly, no restart
needed. This app never modifies HARNESS.md.
UI: a CONCIERGE chat panel (answers questions using live office context — recent agents, ticker, finances — plus whichever provider actually answered and how long it took), a settings gear with a live PROVIDERS status list (configured/cooling/error, per-provider server-side TEST button) and a read-only HARNESS tab, and an "ASK CFO" button in the finance panel that runs a completion over the finance JSON.
The chain of command: Fable is the studio's CEO (your own sessions — this app never invokes it), Opus is the standing judge, and Codex/Grok/Claude workers are contractors.
POST /api/mission {title, brief} triggers:
- Judge step — always forced through
claude-cli --model opus(falling back through the chain if Opus is cooling), system = HARNESS.md, asked to reply with strict JSON:{"route": "codex"|"grok"|"claude"|"needs-ceo", "reason", "prompt"}. Parsing tolerantly extracts the first{...}block. - Dispatch —
codex/clauderoutes run their CLI in a background thread withcwd = missions/<slug>/inside the repo directory;grokcalls the API directly;needs-ceoparks the mission with the judge's reason ("run this in your own Fable session"). Each mission getsMISSION.md(title/brief/route/reason) andRESULT.mdon completion. - Lifecycle events (routed / running / done / parked / failed) go to the ticker and SSE.
GET /api/missions lists everything with live status. The Mission Desk
panel has a new-mission form, a status-chip list, and the judge's one-line
reasoning per mission.
Agent visualization
GET /api/state— agents (Claude + Codex, roster-enriched) + project summaries + guest sprites + recent ticker.POST /api/steer—{sessionId, note}→ appends toSTEERING.md.GET /api/agent/transcript?sessionId=&mode=summary|verbose|raw&cursor=— tail-read a transcript from a byte cursor.POST /api/watch—{sessionId, on}→ toggles live transcript SSE push.
Finance
GET /api/finance→{payroll, subscriptions, tasks, prices}.POST /api/finance/refresh-prices,/api/finance/subscription,/api/finance/task,/api/finance/task/complete,/api/finance/ask-cfo.
LLM layer
GET /api/providers,POST /api/providers/test {name}.GET /api/harness(read-only).POST /api/concierge {question}.
Missions
GET /api/missions,POST /api/mission {title, brief}.
Humans (presence / chat / voice signaling) — unchanged from Phase 1:
GET /api/events (SSE), POST /api/human/{join,heartbeat,move,chat,leave, signal}.
- working — transcript touched in the last 120 seconds.
- break — touched between 2 and 30 minutes ago.
- done — touched between 30 minutes and 3 hours ago.
- Older than 3 hours — dropped entirely (not shown).
Claude tiers come from message.model (fable/opus/sonnet/haiku);
Codex sessions are tier gpt5. Room placement: fable/opus bosses →
Direction; sonnet/gpt5 workers (and any other boss) → Atelier (Codex in its
own desk cluster); haiku workers → Archives; unmatched guest processes →
Humans Lounge.
Subscriptions first, credits second. Fable is the CEO — reserved for
architecture and final calls, never invoked by this app itself. Opus is the
standing judge for routing/triage/review. Claude (Sonnet/Haiku),
Codex, and Grok are contractors, matched to the mission by cost/fit. Daily
subscription limits are the budget; metered API keys are the fallback, never
the first choice. See HARNESS.md for the full doctrine (never edited by
this app — only loaded as the LLM system prompt).
The role hierarchy (who counts as CEO, judge, worker, clerk, or contractor)
is driven entirely by roles.json — not by code. When a new model
launches, you only ever need to edit roles.json; no Python or frontend
changes are required.
Each role is a match array of lowercase substrings checked against a
model's id (or, as a fallback, its tier string). The first role whose
match list contains a substring of the model id wins, checked in the
order the roles appear in the file. If nothing matches, the model/agent
is assigned the built-in unassigned role.
{
"_note": "User-editable. Add a model-id substring to a role's match array (or add a new role) when a new frontier model ships. No code changes needed.",
"ceo": {"match": ["fable", "mythos"], "note": "most capable frontier model"},
"judge": {"match": ["opus"], "cli_model": "opus"},
"worker": {"match": ["sonnet"]},
"clerk": {"match": ["haiku"]},
"contractor": {"match": ["gpt", "codex", "grok", "gemini"]}
}match(required) — array of substrings; a case-insensitive substring match against the model id (or tier) assigns that role.cli_model(optional) — a CLI-facing model alias used by code that needs to invoke that role directly (currently onlyjudge, used bymissions.py's_judge()to pick which model triages a mission brief).note(optional) — free-text documentation, not read by any code.
The built-in roles, in the order the app expects to see them:
| Role | Meaning |
|---|---|
ceo |
Most capable frontier model — direction-setting, never auto-invoked by missions |
judge |
Triages/routes/reviews mission briefs (missions.py) |
worker |
Standard Claude workers (e.g. Sonnet-tier) |
clerk |
Cheap/fast tier, seated in the Archives room (e.g. Haiku-tier) |
contractor |
Non-Claude CLIs (Codex, Grok, Gemini, etc.) |
unassigned |
(implicit — not in roles.json) Any model id that matches no role's match list. Rendered with a chrome/silver sprite and a "★ NEW" badge, surfaced in the Settings → Unassigned tab, and announced once on the ticker so you know to add it to roles.json. |
To onboard a new model: add its id substring to the right role's
match array (e.g. add "opus-5" to judge.match when a new Opus ships),
or add a brand-new role block if it doesn't fit an existing one. Save
roles.json — roles.py loads it once at process start, so restart
server.py to pick up the change (a normal server restart, not a code
change). Nothing in server.py, adapters.py, missions.py,
finance.py, or index.html needs to change.
- Reads only local transcripts/process lists on this machine — no cross-machine aggregation.
- Subagent detection relies on the current
<session-id>/subagents/agent- *.jsonllayout; a Claude Code layout change would need a parser update. - Steering notes are best-effort (see the STEERING.md convention) — no confirmation a running agent actually reads the note.
- The self-signed HTTPS cert always shows a browser warning.
- Human presence is entirely in-memory — restarting the server drops connected humans/voice sessions.
- Voice is a full mesh — fine for a small team, not built for large calls.
- Finance figures are token-count estimates, not billing truth — especially for anything covered by a flat subscription rather than metered API usage.
- The Codex/Grok/OpenAI/Gemini legs of the LLM chain only activate if that
CLI/key is actually present —
GET /api/providersshows what's live. - Remote access (
--tunnel,--tunnel-named, Tailscale) and autostart are fully implemented but never self-installed — you run those commands yourself. See the generatedDEPLOY.md.
BSD-3-Clause. See LICENSE.