Skip to content

Repository files navigation

Pixel Atelier

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.

How it sees your sessions

Pixel Atelier reads local files only:

  • ~/.claude/projects/**/*.jsonl — Claude Code session transcripts (including subagent transcripts nested under subagents/).
  • ~/.codex/sessions/**/rollout-*.jsonl — Codex CLI rollout files.
  • A periodic ps sweep 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.

Requirements

  • Python 3.9+, standard library only — no pip install needed, anywhere.
  • Optional, for the native macOS app: Xcode Command Line Tools (xcode-select --install) so swiftc is available.
  • Optional, for local session data to show up at all: you need to have actually run claude or codex at least once on this machine, so their transcript files exist.

Quick start

git clone <this-repo-url> pixel-atelier
cd pixel-atelier
python3 server.py

Then 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.

Voice calls need --https

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 --https

This 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.

macOS native app

If you're on macOS and have Xcode Command Line Tools installed:

cd native
./build-app.sh

This 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.

First run creates your own local data

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.

What's new in Phase 3 — the living garden

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 Forge, the Gallery, and the Activity River (F / G / R)

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 Ops Board (press O)

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.

What's new in Phase 2

1. Multi-AI source adapters (adapters.py)

  • Claude adapter — same tail-read/cache/status logic as Phase 1, now factored into adapters.scan_claude(). Also extracts per-message usage blocks for payroll.
  • Codex adapter — parses ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl (session_meta, turn_context, response_item, event_msg line types). Codex agents render with provider:"codex", tier:"gpt5", teal/white in the UI, seated in a distinct Atelier desk cluster. Token deltas come from event_msg.payload.info.last_token_usage on token_count events.
  • 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 ps sweep is throttled to its own 10s cadence). Any agent diff (new agent, status change, new lastText/lastTool) is broadcast as an "agents" SSE event on /api/events; the frontend consumes SSE first and falls back to polling /api/state if the stream drops.

2. Roster (roster.py)

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).

3. Watch Mode (transcript.py, /api/agent/transcript, /api/watch)

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).

4. Finance / CFO (finance.py, /api/finance*)

  • Payroll — every Claude usage block and Codex token_count delta is accumulated into finance.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"). fable is locked ("locked": true) and never overwritten. A daily background job (and POST /api/finance/refresh-prices for 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.

5. Auth + security (auth.py)

  • First run generates a 32-hex token into secret.token (chmod 600) in the repo directory; printed at startup. python3 server.py --rotate-token regenerates it.
  • Requests from 127.0.0.1 only bypass auth. Every other client — LAN, Tailscale, tunnel — must supply ?token=<token> (which then sets an HttpOnly cookie, Secure when behind a detected HTTPS proxy) or an existing valid cookie. Token comparisons use hmac.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/steer may only write STEERING.md inside the agent's own resolved real cwd (or the repo's steering/ fallback); transcript reads are restricted to ~/.claude/projects and ~/.codex/sessions; secret.token and keys.env can never be served as static files even though they live in the same directory as index.html.

6. Remote access + always-on (remote.py) — implemented, not installed

  • 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 the https://*.trycloudflare.com URL from its output, and prints it with the token appended.

  • python3 server.py --tunnel-named <name> — runs cloudflared tunnel run <name> and (re)writes DEPLOY.md with 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 generated DEPLOY.md for the full walkthrough.

  • Always-on (macOS): python3 server.py --install-autostart writes a com.pixelatelier.server LaunchAgent (RunAtLoad, KeepAlive, logs to a local logs/ folder) and loads it via launchctl bootstrap gui/$UID <plist> (falling back to launchctl load -w on older macOS). --uninstall-autostart reverses it.

    This is implemented but never executed automatically. Run python3 server.py --install-autostart yourself, once, when you want the office to survive reboots/logouts.

7. Multi-provider LLM layer (llm.py)

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):

  1. 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.
  2. codex-cli (codex exec ..., your Codex subscription) — 300s timeout.
  3. grokapi.x.ai/v1/chat/completions, XAI_API_KEY.
  4. openaiapi.openai.com/v1/chat/completions, OPENAI_API_KEY.
  5. geminigenerativelanguage.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.

8. Mission Desk (missions.py, /api/mission*)

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:

  1. 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.
  2. Dispatchcodex/claude routes run their CLI in a background thread with cwd = missions/<slug>/ inside the repo directory; grok calls the API directly; needs-ceo parks the mission with the judge's reason ("run this in your own Fable session"). Each mission gets MISSION.md (title/brief/route/reason) and RESULT.md on completion.
  3. 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.

API summary

Agent visualization

  • GET /api/state — agents (Claude + Codex, roster-enriched) + project summaries + guest sprites + recent ticker.
  • POST /api/steer{sessionId, note} → appends to STEERING.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}.

Status rules (how agent state is derived)

  • 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.

Chain of command (doctrine summary)

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).

When a new frontier model ships

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 only judge, used by missions.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.jsonroles.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.

Known limits

  • Reads only local transcripts/process lists on this machine — no cross-machine aggregation.
  • Subagent detection relies on the current <session-id>/subagents/agent- *.jsonl layout; 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/providers shows 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 generated DEPLOY.md.

License

BSD-3-Clause. See LICENSE.

About

A cozy isometric management-sim that turns your real local Claude Code / Codex sessions into a village of leveling-up monks. Native macOS app, BSD-3 licensed.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages