Field notes · the same private repo, the week after platform week · August 2026
Two games on the board
Platform week ended by pointing at v0.3 — the milestone where Claude calls in. The week that followed didn't chase it. It cleared the bench instead: Battleship went from a server with no client to a game you can actually play, in its own Apple II green-phosphor skin; the write-only event spine grew a five-view console that reads itself back; and the platform drew a hard line between the chrome it owns and the theme a game brings. That's v0.2's real goal — a second, genuinely playable game — reached.
Set in Battleship's own CRT theme — the one dispatch that doesn't wear the house colors.
The week
Not the shiny thing
The honest shape of the week: it deferred its own headline. v0.3 — an MCP server over the question
pool and a generate-then-verify content pipeline — is still filed, still unwritten, zero code under
src/. Every one of the 31 PRs that merged was v0.2 work, and they landed the way stacked work lands:
31 pull requests consolidated into 12 arrivals on main, two of them umbrella trains that each landed
a whole stack of branches as one — Battleship (#239) and the observability spine (#267).
Two of the five items left on last dispatch's bench got cleared outright: Battleship has no face (#163) and telemetry is write-only (#218, #219). Nothing about that is glamorous. It's the week you finish the things you already started instead of starting the thing you already teased.
Battleship
A server with a face
Last week Battleship was a server module with wire tests and a README that was mostly a list of what it wasn't yet. This week it grew both halves of a client and closed #163. It's a real game now — proven by Playwright driving two phones through placement, volleys, and a finish, with sentinels asserting no ship glyph ever leaks onto a screen it shouldn't (ADR-0017's per-seat secrecy, checked by machine).
The play arc lives on the phone. Round zero is the shipyard — tap to anchor each ship, LAY flips it
across or down, SHUFFLE throws the whole fleet, READY commits; a seat that never commits gets
auto-placed at close, so one AFK admiral never wedges the war. Then it's select-then-FIRE, the same
lock-in trivia uses. A single-hue glyph language carries everything: fog is ·, a miss ○, a hit a
glowing ✶, a sunk cell ✶ on inverse video — a burning wreck is the brightest thing on the board.
Two decisions were dragged out of a real playtest, on the record. The result screen used to be a full swap between every firing — "disruptive," the commit says — so #242 turned it into a modal that rides over the still-mounted console and auto-dismisses at the next volley; the board you were reading is still there underneath. And the round timer used to close volleys "under people mid-thought," so #241 gave Battleship a declared pacing of none: a round ends only when every living, connected admiral has fired. Drop your phone and the war waits for you; there is no countdown to render.
"You win Battleship by outliving, not out-scoring."
#235 — the final board ranks by survival order, applied at broadcast and at resync alike, so a reload can never re-rank the war
Design
Where the platform ends and a game begins
Battleship's CRT is nothing like trivia's quiz-show plum, and that forced a question the repo had never answered on the record: may a game own a complete visual theme, or must it inherit the platform look? ADR-0021 answers yes — and draws the line precisely. It's the section that made this page possible: the dispatch you're reading is set in Battleship's own theme, as proof the seam holds.
The chrome before any game mounts. The game picker, the join shell, the shared tokens. Since the #240 rescope it's deliberately plain — neutral --pf-* tokens, system fonts — until a real landing theme is designed.
Every pixel once it mounts. Its own namespaced tokens (--bs-*), its own self-hosted fonts, even opting out of the platform's light theme — a CRT has no light mode — and its own contrast and motion obligations.
One mechanism joins the two. The platform stamps data-game-theme on <html> when the UI commits to a game. A game may restyle platform chrome only inside a [data-game-theme="its-id"] guard — greppable, scoped, the CSS twin of a composition-root carve-out.
A theme may bring its own face. Self-hosted, verbatim, credited — PrintChar21's license forbids derivatives, so it ships unmodified. The old system-fonts-only rule is now scoped to platform chrome alone.
Trivia moved too: its plum stopped being the platform's look and retreated into trivia as a themed game like any other (#246). The difference is the hook. Trivia doesn't use it — the landing stays plain even after you pick it. Battleship does, so the picker and join screens flip to CRT green the moment you commit, and joining a Battleship room feels like booting into a different machine.
Observability
The platform can watch itself
Last week's event spine only wrote — a JSONL log that nothing read back. This week the read side arrived
as three derived models over that one write path, with the writers untouched: live counters at /metrics
in Prometheus text, permanent daily rollups kept forever behind a 30-day raw horizon, and a five-view
/admin console — the third client entry beside host and player, ESLint-fenced so it can't even
type-import a game.
The headline is the room timeline — a distributed-trace view of a party game, each round a span coloured by how it closed, each seat a lane, disclosures marked on the axis. The others answer questions the product could only guess at before: round health audits the close policy, and the disclosure view finally makes the pub-round cadence measurable instead of shipped on faith. Live ops updates over the games' own WebSocket transport — no polling, the console just watches the wire.
The store is still files: JSONL today, SQLite named as the drop-in swap for the day it earns a native dependency. The read side shipped as an interface from the first commit, so that day changes no callers — the same files-over-database move the project keeps making.
Receipts
Receipts, lightly
A fast week still leaves marks, and the ledger stays honest:
- The app bound every interface. A missing host argument on
listen()served the real host secret in plaintext, past the TLS and the auth (#222). The first fix used??, which waves an empty-string through; review caught it and swapped in||, with the test that proves it. - One
git add -Afrom a leaked secret. Rotating that secret withsed -i.bakleft a.env.bakcopy the ignore rule didn't cover (#224). Caught and closed the same day. - The reviewer reviewed nothing. The Claude Review action posted zero comments on the Battleship PR across three tries — 52 permission denials, $4.28 spent, a green check and silence — before #249 → #253 widened its tools and made posting the verdict a stated must.
- A caps discipline learned the hard way. GitHub's own infra hung the Playwright install for up to 52 minutes; neither CI job had set a timeout, so both were exposed to the 6-hour default kill (#243, #245).
The process kept trimming its own bill, too — Monitor's cron went from every 15 minutes to every 6 hours (~24× fewer unattended job-minutes), and the subagent routing got pinned to a specific model id after the bare "Opus" alias silently began resolving to a newer, pricier model (#228).
And one bug class finally graduated into a rule. "Every new stateful phase ships its resync" is now
line three of the review bar (#164), worded against the round engine's actual trio —
roundSnapshot / standingsSnapshot / gameOverSnapshot. It earned the promotion honestly: #257 found
that the auto-advance dwell between rounds had no resync at all, so a host refresh mid-dwell dropped a
live game back to the pre-game lobby. The same PR that promoted the rule also walked back an over-broad
claim about what survives a restart — the reference implementation doesn't meet it in one mode, so the gap
is named rather than asserted away.
Loose ends
Still on the bench
Still open, tracked by number:
- The bot drives the wire, not the browser client (#221). The smoke bot plays through the protocol, not the real browser client — untouched this week.
- Stale parallel-agent worktrees under
.claude/remain — ignored by git, not yet swept.
Next
v0.3 — Claude calls in
With v0.2's goal met, the version bump from 0.1.0 to 0.2.0 follows in a release branch (#281). Next is
v0.3, where Claude calls in: an MCP server wrapping the question pool and game stats, with a
generate-then-verify pipeline behind it — the platform's first AIService consumer.
The platform is now shaped for that caller: two registered games, typed round rules, per-seat views, and an event spine with a console to watch it happen. It can register a second game, hide a fleet from the seat beside it, and read its own history back.