The site
Releases and playtests
How a change reaches the family: a numbered release, a few days on its own branch, David’s playtest on his laptop and phone, his sign-off, and a note in What’s new.
On this page
The site ships in releases, one or two a week. Each has a version number, a note for the family in What’s new, a playtest by David on his laptop and his phone before anyone else sees it, and (unless it’s only fixes) a short video. Nothing reaches main, which deploys, until he’s played it and said ship it (ADR 0065).
Versions#
The site’s version is one line in VERSION. Each release has its notes beside it, in docs/releases/, named for its version. A version has three numbers, and which one goes up says what kind of release it is:
guide/releases.py KIND_MEANS · lines 44–49# What each kind means on a games site (David, 2026-11-05).
KIND_MEANS = {
"major": "a new game on the shelf, or a change the whole site feels",
"minor": "new features and improvements in games already there",
"patch": "fixes only",
}
So a new game on the shelf makes 1.4.2 into 2.0.0, a round of improvements makes it 1.5.0, and a fix between releases 1.4.3. The first release with a number, on 2026-11-05, was 1.0.0. Each shipped release is tagged in git (v1.4.0) and has a GitHub Release with its notes and its video attached.
A release, start to finish#
Every step is npm run release:<step>, which runs manage.py release <step> (playtest/management/commands/release.py). The release skill in .claude/skills/ drives them in order.
- Start.
release:start -- minorcutsrelease/v1.5.0frommain, writesVERSIONand an empty notes file, and pushes the branch. - Collect. Pull requests target the release branch, not
main, and merge into it as soon as they pass the gate (npm run check:pr). Each says what a player would notice under “For the family” (below);release:notesgathers those lines into the notes. - Freeze. When David’s ready to play it, new features wait for the next release. Only fixes land. A major release gets a design audit here, before he plays it.
- Playtest.
release:serveruns the release branch on his Mac for his laptop and his phone (below). What he finds becomes fixes on the branch. - Sign-off. David says ship it. Nothing else counts.
- Ship.
release:ship -- --signed-offchecks the notes strictly, runs the whole gate whatever changed (check:pr:all, then the suite’s lock tests on Postgres,check:postgres:_gate()), dates the notes, opens the release’s pull request tomainand merges it (which deploys), thenrelease:tagtags the merge, makes the GitHub Release, waits until the live What’s new shows the version, and lists what only the live site could show (the “After it ships” items, below) with how to check each.
A patch skips the freeze and the long playtest: one fix, David’s quick look, ship.
The notes#
A pull request says what the family would notice in a section of its own (.github/pull_request_template.md), one line each, New, Better or Fixed, then where, then what, and optionally where to try it:
## For the family
- Better · Millbury Mansion: The notebook marks who showed you a card. [Try it](/games/millbury-mansion/)
or “None” for a change no player would notice. guide/releases.py reads those lines (family_lines) and writes a release’s notes file (dump): front matter (version, kind, date, a one-line headline, the pull requests it covers), then up to three sections of items. date stays empty while the release is being tested, so What’s new marks it “Being tested” on the playtest server, and release:ship sets it.
check() holds every notes file to the site’s voice and to each other. It runs in the test suite, so the gate holds it too:
- a version is the next of its kind after the one before (1.4.2 then 1.5.0 for a minor), the file is named for it, and
VERSIONnames the newest; - only the newest may be undated, and a patch is fixes only;
- an item names a game by its title, or one of the site’s places (
SITE_PLACES), and is a sentence of at most 200 characters; the headline at most 140; - no pull request numbers and none of our words (
JARGON: “refactor”, “API”, “please”, “successfully” …).
While a release is being tested its headline and items may still be empty, as its pull requests are still landing; --strict, which shipping uses, requires both.
What’s new#
guide/views.py’s whats_new draws every release’s notes, newest first, at /guide/whats-new/, and the guide’s front page shows the newest above the search. It’s a guide page like any other: public, cached, no cookie. The live site lists only releases that have shipped (released(), the dated ones); one being tested shows, marked, only on a development or playtest server (shown()), so notes that reached main undated by mistake still never read as released. For search engines it previews as itself and joins the sitemap dated by the newest shipped release, so each release’s IndexNow ping names it (Architecture, ADR 0063). The GitHub Release’s text and the video’s closing card end with the live What’s new address and the release’s anchor (#v1.5.0): the repo is private, so they’re how the family finds the notes.
The playtest server#
David chose his own Mac, over Wi-Fi, for the playtest (2026-11-05): free, and the release running exactly as the agents test it. npm run release:serve gets it ready and runs it:
- its own SQLite database in
playtest-data/(gitignored), migrated and seeded, so playing it never touches anyone’s development tables; - uvicorn on every interface (
0.0.0.0, port 8300), answering to the Mac’s own Wi-Fi name (scutil --get LocalHostNameplus.local, which iPhones find by themselves), so the phone openshttp://davids-macbook-pro.local:8300/; UO_PLAYTEST=1, which turns on the Playtest page and keeps Feedback.
The phone reaches it by plain HTTP, so the share sheet and Copy fall back to what they do without a secure page; everything else is the release as it will ship. The Mac must be awake and on the same Wi-Fi; macOS may ask once to allow incoming connections.
settings.PLAYTEST is only ever true in development (obstacles/settings.py), and only then does obstacles/urls.py mount the Playtest page, so the live site has no /playtest/ at all.
The Playtest page#
/playtest/ (playtest/views.py) is the one page David starts from, on either device:
- Who you are: one tap to be a test player (
playtest/players.py: Quinn, Rosa, Sam and Tess, never a bot’s name), signed in with no password, so he can be one on the laptop and another on the phone. - The checklist (
playtest/checklist.py): every item in the release’s notes, linked to where to try it, then a route round the site and through every game on the shelf. Each is marked Works or Something’s off on the device it’s marked on (a coarse pointer is the phone), and a Something’s off asks what. An item’s playtest lines in the notes (PLAYTEST_KEYS, below) shape it. - Found so far: every note and every Feedback sent from any page, each with Done and Dismiss, so the list stays honest between rounds; done and dismissed ones fold away, and can be opened again.
- Start a table: any table game, him as host and bots in the other seats (up to three; none for a game only people play), in its waiting room. The seat is kept in the browser the way the room kit keeps one, then the table opens.
- Tables here: the tables on the server in the last few hours, to join from the other device.
Playtest lines#
A notes item can say what its playtest needs, on lines of its own beneath it that What’s new never shows (guide/releases.py):
guide/releases.py PLAYTEST_KEYS · lines 102–116# What a notes item may say about its playtest, on lines of its own beneath
# it (never shown in What's new; playtest/checklist.py reads them):
#
# - **Contract Rummy:** You can swap a joker off the table … [Try it](/games/contract-rummy/)
# - playtest: setup contract-rummy swap
# - playtest: checked Played the hand to the swap: it went down and kept the 4♥.
#
PLAYTEST_KEYS = {
"phone": "only on the phone (a phone's layout, a touch)",
"laptop": "only on the laptop",
"after-ship": "can only be checked on the live site (HTTPS, a link's preview, installing): how, in a line",
"preview": "a picture to show beside it on the Playtest page: a /static/ path",
"setup": "a Set it up button: a game and its stage tool's moments (tools/<game>/stage.py)",
"checked": "what Claude did to check it, where David can't easily (a moment no tool stages)",
}
So an item only a phone can show isn’t asked of the laptop; one only the live site can show (installing, needing its secure address; a link’s preview, needing its public address) goes under “After it ships”, isn’t marked, and release:ship lists it with how to check it once the version is live, while a picture it would show (the preview card) is drawn right on the Playtest page. An item that needs deep game state gets a Set it up button (setup in playtest/views.py): a table with David in seat 0 and bots, started, then set to each moment in turn by the game’s own stage tool, the one its browser check uses (tools/contract-rummy/stage.py CODE swap), before it opens. What no tool can stage, Claude checks before the playtest and says how, shown as “Checked by Claude”.
What you find#
playtest/store.py keeps the marks and what’s found in files, beside the release (playtest-data/<version>/): ticks.json, findings.jsonl, status.json (each finding marked done or dismissed: nothing is deleted) and any screenshot. Feedback on the playtest server goes there too, instead of to email (feedback/views.py). npm run release:findings reads back what’s still open, for whoever fixes it (--all, every one).
What the family sent#
Between releases, what players and guests send from the Feedback button on the live site waits in the owner’s inbox (Accounts, ADR 0068). npm run release:feedback runs manage.py feedback on the live site (heroku run). It lists what’s New since the last release shipped, grouped by game, as Markdown, for the next playtest round or release. npm run release:feedback -- mark 41 42 triaged marks items once they’re planned, and … shipped once their fix ships, in this version unless --in X.Y.Z. The /playtest-round and release skills start from that list.
The video#
A major release gets a video of up to 3½ minutes, a minor one a short cut of about a minute and a half, a patch none. release:video lays out a first manifest from the notes (a section a place, a scene an item, all pending), and the release-video skill makes it (Testing has how a take is recorded; docs/release-video.md the rest). Its music plays each game’s own theme in that game’s section, with the site’s theme at either end; npm run music:preview -- v1.0.0 hears the score before anything is drawn (ADR 0072).