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.

2
playable games · was 1
31
PRs merged · Aug 13–20
627
tests · was 463
5
admin views · the ops console
2
bench items cleared · of 5
12
landings on main

Set in Battleship's own CRT theme — the one dispatch that doesn't wear the house colors.

Green CRT host wall reading star BEE RULES THE SEA over final standings: BEE 07 points, ANN 04 points, with a New Game control
The second game, played to a finish. Host wall — fleet command, 1982.

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.

Phone deployment screen: DINGHY and SLOOP buttons, a 3 by 3 grid with two ships placed, Lay Down and Shuffle controls, a Ready button
Deploy — the shipyard.
Phone targeting screen: opponent BEE's fog-of-war grid above the player's own fleet strip, prompt reading PICK A TARGET CELL
Target — pick a cell, then fire.
Volley result modal over the targeting screen: HIT +1, a log reading YOU to BEE A1 HIT and BEE to YOU A1 SUNK, one OK key
Resolve — a modal over the live console, not a screen swap.

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.

Green CRT host wall during Volley 1: two side-by-side 3 by 3 fleet grids labelled ANN and BEE, both reading ALL SHIPS AFLOAT
The wall splits per fleet — party-scale boards, fog of war, both fleets still afloat.

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

platform owns

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.

a game owns

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.

the hook

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.

the font

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.

Trivia host screen in plum: Round 1, a 20-second countdown ring, the question Who wrote Romeo and Juliet, four labelled answer tiles
One platform, plum — trivia, the quiz-show broadcast.
Battleship host wall in green CRT: two fleet grids for ANN and BEE, all ships afloat
One platform, phosphor — Battleship, fleet command. Same shell, two worlds.

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.

Pizza admin overview: stat tiles for rooms, players and dropped events, and a funnel from created to joined to started to finished
Overview and the funnel — where do rooms leak: created-never-joined is a QR that didn't scan.

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.

Admin room timeline: eleven events over 1.8 seconds, two round spans on a swimlane under a connection lane
Room timeline — one game's whole trace on a time axis.
Admin live ops: one room active, six connections open, zero events dropped, a running event ticker
Live ops — rooms, connections, drops, and errors, live over 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 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.

Trivia host final-results screen captured after a page reload: trophy, a winner, standings, New game control
A finished game survives a host reload — same winner, no dead lobby.
Player waiting screen after a reload and rejoin: You're in, still in the room, waiting for the host
A player reloads and lands back in the same room, seat intact.

Loose ends

Still on the bench

Still open, tracked by number:

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.