AI Initiative logoAI Initiative
Reading room / Game Creation Studio

11. The game factory

Documents scaffolding, shared runtime and vector mathematics, automated checks, release preparation, two pilots, metrics and successive review corrections.

Creation-toolkit engineering case study
This is the retained source text, not a new validation of its claims. Numerical results and comparative claims require the accompanying source review before public reuse.

Read the connected case study or article →

Not stated in document opening · 2026-10-19

Provenance, authorship and editorial notes

11. The game factory

Id

game-platform-docs-11-game-factory

Project

game-platform

Source path

game-platform/docs/11-game-factory.md

Source sha256

acf00687738c9e53e6d7a6dce1416e3182dca52007e76109825755271912de94

Repo head

adfb7831984715cb220e4c3c8ba55a66fc7c54fa

Date

2026-10-19

Date provenance

First date near document opening; not independent publication date

Git last change

2026-10-20T02:34:55+01:00

Author

Not stated in document opening

Document status

Creation-toolkit engineering case study

Summary

Documents scaffolding, shared runtime and vector mathematics, automated checks, release preparation, two pilots, metrics and successive review corrections.

Topics
  • games
  • creation tools
  • AI workflow
  • mathematics
Related ids
  • game-platform-docs-02-architecture
  • game-platform-docs-09-multi-agent-workflow
  • game-platform-docs-04-quality-gates
  • game-platform-proposals-learn-from-play
Evidence concerns
  • A reusable builder toolkit, not an autonomous game-making product. Fast checks differ from full release preparation; historical pilot passes do not certify every game.
Word count

5884

Headings
  • 11. The game factory
  • 1. Quick start: create → build → check → release-prep → publish
  • 5 publish: a person / the coordinator (§5)
  • 2. What `create-game` writes
  • 3. The runtime: `games/_factory/kit2.js`
  • Verified maths: `games/_factory/vmath.js`
  • 4. The gates: `ci/game-check.mjs <id>`
  • 5. Release: `ci/release-prep.mjs <id>`, then the publish step
  • 6. Proof: two pilots and five templates (2026-10-19)
  • 7. Metrics: `ci/factory-metrics.json`
  • 8. Known gaps (honest list)
  • 9. Review of the factory itself
Publication status

Publication or peer-reviewed acceptance not established by discovery

Format

md

Private review scan
Known secret pattern hits

0

Customer or player record sets

None identified in selected narrative/design documents; no gameplay records, account databases or hand datasets copied.

Scope

Pattern scan plus document review; not a formal exhaustive privacy audit.

11. The game factory

"Efficiency of the game platform depends on how quickly and successfully we can build and launch the NEXT game."
(operator, decision 44, 2026-10-19)

The factory turns "a new game" into "a new game's gameplay". Everything the gates check (saves, text, input,
layout, pause, calm mode, dialogs, leaks) is owned by one small runtime and proven by one command, so a builder writes
only the rules of the game and a game is very likely to pass every gate the first time.

All commands run from MM/ (maneet-games/master-mind/). Node 18+ (Node 20 is fine: the scripts
add --experimental-websocket themselves), Python 3, Google Chrome.

1. Quick start: create → build → check → release-prep → publish

node ci/create-game.mjs star-catch --kind arcade --name "Star Catch" --emoji "⭐" --color "#ffb703" \
     --desc "Catch the falling stars." --cat arcade          # 1 create (never overwrites anything)
open games/star-catch/index.html                             # 2 build: replace the GAME part of the script
node ci/game-check.mjs star-catch                            # 3 check: every gate, ~1.5 min, table + logs
node ci/game-check.mjs star-catch --full                     #   release grade, ~6 min
node ci/release-prep.mjs star-catch                          # 4 release-prep: freeze + full gates + packet
#                                                              5 publish: a person / the coordinator (§5)

Kinds (six; each is a small working game that already passes every gate; change it, don't start from blank):

--kindSurfaceWhat the template isTypical next game
arcade2D canvas, real-timecatch the stars, dodge the clouds; keys + pad + drag + gamepadcatchers, dodgers, runners
puzzleDOM board, turn-basedlights-on grid; arrows/d-pad move, tap/Enter/A flipsgrids, matching, logic
wordDOM board, turn-basedspell the word from its letters; kid-safe answer listspelling, anagrams, vocabulary
ordieDOM board, timed roundsan "…or Die" quick-fire round on OrDie.run (lives, tiers, streaks)quiz/party rounds
3dWebGL (three r128)steer and collect; renderer in try, shared assets, per-round disposalsmall 3D arcade
mathsDOM board, turn-based8-question rounds from two verified generators (equivalent fractions/decimals/percent; a missing number), answers compared by VALUEany maths game (operator, 2026-10-19: verified maths by default)

2. What create-game writes

FileWhat it is
games/<id>/index.htmlthe game. Loads _sdk/mm-sdk.js, _sdk/mm-store-v1.js, _shared/kidsafe.js, _ordie/core.js (the display sink), _factory/kit2.js, then one Kit2.game({...})
factory/<id>/hub-entry.jsthe three hub lines (GAMES, LOGOS, QPFX) for the hub owner to paste. The factory never edits index.html. The quest prefix is the id without dashes (the hub's QEV_RE is [a-z0-9]+); QPOOL is generated from QPFX, so the game gets the standard t3/p2/t8/t15 quests
factory/<id>/REVIEW-PACKET.mdthe builder's notes while building (plan, kid-safety plan, questions)
factory/<id>/<id>.test.mjsthe game's own checks, run by the probe through the test hook (add your rules: scoring, win/lose, what is saved)
ci/factory-metrics.jsonthe creation time (for time-to-first-green)

factory/ is outside games/, so none of it is shipped by the pipeline.

3. The runtime: games/_factory/kit2.js

One file (v2.4.0, ~610 lines, no build step) plus vmath.js (VMath 1.1.0); API reference with examples: MM/games/_factory/README.md. What a game
gets without writing it, and the lesson behind each:

AreaWhat Kit2 doesLesson / gate
Savesone MM.store("<id>-v1", {v,…}); migrations edit the old object in place (unknown fields survive); a newer-version save is quarantined (checked before every write, never overwritten or reset, 🔒 badge); a failed device write shows ⚠️ "Not saved yet" until a real retry is read back from the device; Reset reports its real result; nothing is written before MM.ready; store.hold() during a round, and any waiting save is applied before the round's resultD42, S19, SB1–SB3, Anime Punch 12, seq895 #1–#3, seq900, P11, P13, P13b/c, P16, P22–P25
Eventsquest events and play time belong to the player who started the round; after a hub player switch nothing of A's is sent as B's (the kit's own play clock replaces MM.gameplayStart/Stop)seq895 #4, P21
Playersa hub player switch abandons the round (never saved into the new player), clears G.mem, closes dialogs, releases input, repaints the titleSB2/SB3, hub-xplayer lessons, P12
Text in (D45)text is filtered where it ENTERS: G.ingest (= OrDie.ingest) for AI, party, import and URL text; G.textInput for typed text; the save's known fields after every load; the hub player's name. Unknown save fields are untouched (D42)D45, F08, P19
Text outevery kit and game text goes through OrDie.show/safe/safeAttr (normalise → upper → word-cut → kid filter last, fail-closed): DOM text, attributes (title, aria-*, alt, placeholder, value), canvas words (Kit2.safe), document.title (G.title); no CSS text-transform ({upper:true} instead); a late filter repaints everythingD40, D45, S20, F01, docs/06 L8, P03, P20
Inputkeyboard map; on-screen pad with pointer-id ownership (setPointerCapture, multi-touch, 64 px); standard gamepad (buttons + axes; a vanished pad releases); everything released on blur, hide, pause and switchS07, T2, Anime Punch 3–6, P06–P08
Layoutstage inside the safe areas; canvas re-fitted on resize, rotate and visualViewport changes; backing = CSS × min(DPR, 2); never stretchedS10, B03, R1–R8, P14
TimerAF clock, dt ≤ 0.1 s; hidden or blurred → paused (dialog, audio suspended), resume with no time jump; game-time timers G.after/G.every stop while paused and die with the roundS08, S11, guide §2.5 (stale timers), P04
Calm modeone flag for flashing, shaking and CSS animation; saved per player as true/false/null, where null (and a missing field in an old save) follows the OS reduced-motion setting; flashes capped at 2 a secondAnime Punch 9–10, docs/07 §6, P10
Dialogstitle, pause, settings (sound, calm, two-tap reset), results, error card; focus moves in, Tab is trapped, Escape cancels, focus returns, the page behind is inert; a stale dialog answer after a new round is ignoredS12, Anime Punch 14–15, P05
Controller and keysthe d-pad moves focus on title, dialogs, results and DOM boards; A presses, B cancels, Start pauses; Escape pauses (P is a letter, not pause); G.rebuild keeps focus when a board is rebuilt; DOM boards scroll on small landscape phonesseq900, P27, P28
ResourcesG.page / G.scope own listeners, timers and disposables; Kit2.disposeTree for 3D; Kit2.resources() counts what is liveS09, Anime Punch 17, P09
Standaloneworks in the hub iframe and from a file or tab; without the SDK file it runs device-onlyB01, guide §2
Test hookKit2.test: state, play, end, pause, resume, quit, hold/release, resources, calm, save copy, logP01–P18, test stubs

Why _ordie/core.js is loaded by every factory game: it holds THE display sink. Wrapping it (not copying it) keeps
one sink rule for the whole platform. Cost: 77 KB per game. Proposal P1 moves the sink to _shared/display-sink.js
(proposals (../proposals/2026-10-19-game-factory-shared-changes.md)).

Verified maths: games/_factory/vmath.js

Operator, 2026-10-19 (mnp-review/analysis/matiz-games-mapping.md recommendation 1). Exact BigInt rationals and answer
equivalence (1/2 = 0.5 = 2/4 = 50%), and validateQuestion(q): one accepted answer (identities and no-solution items
rejected), an independent recomputation, distinct options with exactly one equal in value to the answer, clean display,
no parity giveaway on prime questions. A maths game declares pure generators in <script id="k2-maths"> and shows items only
through VMath.draw. ci/factory-maths.mjs runs every declared generator over 10,000 seeds in game-check (advisory:
shown in the table and metrics, not in the verdict). The module is standalone so ci/math-question-test.mjs (another
builder, for Whetstone and Color Monster Academy) can import it (loadVMath in ci/factory-maths.mjs). API: MM/games/_factory/README.md.
The maths template's two generators: 10,000 / 10,000 valid each.

4. The gates: ci/game-check.mjs <id>

One command, a staged copy (~/.cache/claude-scratch/game-factory/runs/<id>/<stamp>/), a pass/fail table, a log per
gate, the sha256 of the tested bytes, and an entry in ci/factory-metrics.json. Staging means other builders'
in-flight edits cannot change a result half-way, the hashes say exactly what was tested, and the pipeline's dist/
and reports never touch the shared tree. The copy (only) gets longer Chrome timeouts (≥ 180 s instead of 60 s; one
Chrome at a time above half load), so a slow hub page on a busy machine does not turn into a red gate; the report says so.

GateScopefast--full
factory static F01–F08 (ci/factory-static.mjs)game✔✔
static S01–S20 for this game (hub findings listed separately)game✔✔
kid-safe S16 (ci/kidsafe-check.mjs)game✔✔
verified maths, 10,000 seeds per generator (ci/factory-maths.mjs; games with a k2-maths block)advisory✔✔
estate sweep G1–G4, copy modeestate✔✔
display sink suite D40 (the …or Die file list)estate✔✔
save boundary SB1–SB4game✔✔
rotation R1–R8 (fast: 390×844 only; full: both pairs)game✔✔
pass + touch T1–T6game✔✔
browser B01–B07 for this game (run directly, so an estate-red pipeline cannot hide them)game✔ (phone + monkey)✔ (4 sizes)
factory probe P01–P36 + the game's test stub (ci/factory-probe.mjs)game✔✔
gate self-tests (html_meta, S17–S19 fixtures, SB leak fixture)estate✔
factory self-tests (F-rule fixtures, probe broken fixture)game✔
pipelinegame (estate when it is red only in its stage-1a sweep on files outside the game)--quickfull (B01–B07, hub save HS1–HS6, SB, package)

Chrome lifecycle: every run is guarded (ci/factory/chrome-guard.mjs): exit/signal handlers and a detached watchdog
(chrome-watchdog.mjs) stop the run's Chromes and orphaned gate processes if game-check is killed (tested: SIGKILL
mid-gate, 11 → 0 processes in 15 s), and leftovers after each gate are stopped and noted. It only ever touches processes
inside the run folder it owns, or whose cdp profile names a gate pid it spawned. node ci/cleanup-orphans.mjs lists
Chromes whose owner pid is dead (--all-cdp for any cdp run, --kill to stop them). The upstream fix for ci/cdp.mjs
is proposal P11.

Exit 0 = all green; 1 = a game gate failed; 3 = only estate gates (shared files the game cannot fix) failed.
Options: --full, --in-place, --keep, --no-metrics, --json <file>, --root <master-mind>,
--remote oldisgold.local (stages here, rsyncs to ~/game-factory/runs/ there, runs with that host's Chrome
~/mm-runner/bin/chrome, copies the logs back).

Factory static F01–F08 (stricter than the platform rules on purpose: the kit owns every text write):
F01 no dynamic text write outside the sink (DOM properties, {textContent:}, insertAdjacentHTML, createTextNode,
document.write, text attributes, append of strings, canvas fillText/strokeText), with a / sink-ok: reason /
escape hatch; also every aria-, value, document.title and ariaLabel · F08 (D45) every input boundary in the
game file (.value reads, message events, fetch/XHR/WebSocket, clipboard, URL, MM.data reads, <input>/<textarea>/
contenteditable) goes through G.ingest/G.textInput · F02 every string in a /
kit2:answers */ list passes kidsafe.js (also upper-cased); word and
…or Die games must have one · F03 script load order · F04 no storage writes outside the kit · F05 versioned, stable
save key · F06 no CSS text-transform · F07 (warn) raw timers and listeners. --selftest plants a bug for every rule
and checks every template is clean.

Factory probe P01–P36 (real Chrome, the game in a fake hub iframe, real CDP touch and keys): P01 boot in the hub ·
P02 Play by touch · P03a sink masks blocked words (also after upper-casing) · P03b sink fails closed when the
…or Die core is missing · P03c no blocked word in any shown text or text attribute on the title, play and results
screens (also upper-cased) · P04 hidden → paused with game-time frozen · P05 dialog focus trap and Escape · P06 keys,
blur releases · P07 two-finger pad ownership · P08 gamepad press and release · P09 5 rounds leave no leaked kit
listeners, timers, scopes, disposables, raw intervals or raw window/document listeners · P10 calm follows OS
reduced-motion unless the player chose · P11 a round's result reaches the cloud, survives reload, keeps an unknown
field · P12 a player switch mid-round leaks nothing · P13 a newer-version save present at boot is never overwritten ·
P13b one arriving from another device mid-round (through the SDK's real cloud merge) is kept on the device and in
the cloud · P14 rotate re-fits · P15 no uncaught errors · P16 a device that cannot store is told (never a silent
loss) · P17 a pause inside a game-time timer halts that frame · P18 pausing mid-drag ends the drag; then the game's own
factory/<id>/<id>.test.mjs. Added for D45 and the review rounds: P19 plant-and-render (unsafe text planted in save
fields, the player name, typed text, AI/party/import/URL text, document.title and attributes never reaches the save or
the screen; unknown fields stay byte-identical) · P20 a late filter (placeholders, then everything repainted) · P21 a
mid-round player switch reports nothing of A's as B's · P22 Reset never replaces a quarantined newer save · P23 a refused
Reset is reported, retried and lands · P24 a failed save is retried with a real write until the device copy reads back ·
P25 a newer save arriving outside a round is not overwritten by a Settings change · P27 P is not pause, Escape is · P28
controller navigation · P29 saveOnQuit. --selftest runs a deliberately broken fixture (raw unsafe text, pause disabled, a timer
and a window listener leaked per round) and fails unless P03c, P04 and P09 catch it.

What the static heuristics cannot see in a factory game: S07, S08, S10 and S11 scan only the game file, and the loop,
input and DPR cap live in kit2.js, so for a factory game they pass without proving anything. The probe is the proof
(P04, P06–P08, P14). docs/04 says so too.

5. Release: ci/release-prep.mjs <id>, then the publish step

release-prep never publishes, never writes the db, never posts and never edits the hub. It:

1. freezes the tree to ~/.cache/claude-scratch/game-factory/releases/<id>-<stamp>/freeze/master-mind and writes
FREEZE.sha256 (the game's files, the shared files it loads, the gate code, the test stub);
2. runs game-check --full --in-place on the frozen bytes with the frozen gate code;
3. re-hashes the source tree and reports any drift since the freeze (re-run if any);
4. writes REVIEW-PACKET.md (filled: hashes vs live, gate table, scope, gaps), CODEX-PROMPT.md + CODEX-COMMAND.sh
(the exact read-only Codex review of the frozen copy), PRE-PUBLISH-POST.txt (channel text, not sent),
RELEASE-CHECKLIST.md (prefilled), PUBLISH-STEP.md, hub-entry.js.

Scope: if a shared platform file (games/_*) differs from the live version (ci/published.json), the packet says a
second-family (Codex) review is required (decision 22). The first release of any factory game ships
games/_factory/kit2.js for the first time, so it needs one.

The publish step (human / coordinator only): gates green on the frozen bytes or a gate exception recorded first ·
reviews done and every finding dispositioned · the hub owner pastes hub-entry.js and re-runs the hub gates ·
PRE-PUBLISH post (status OK + seq) · publish the frozen shipped files, read back, compare with FREEZE.sha256 ·
npm run ci:mark-published -- <version> · POST-PUBLISH post · Board row done · lessons to docs/10.
Details: 05-release-process.md (05-release-process.md).

6. Proof: two pilots and five templates (2026-10-19)

Two small original pilots were made with the factory only (no copyrighted characters; not added to the hub; hub
snippets in MM/factory/<id>/hub-entry.js), plus every kind template in a scratch tree.

PilotKindWhat it isCustom codePlatform code in the game
lantern-glide 🏮 Lantern Glidearcadesteer a paper lantern up through the night, catch fireflies, drift past clouds; five in a row = GLOW (cloud-proof)~93 lines of gameplay + drawing, 3 rule checks in its test stubnone: saves, input (keys, 4-button pad, drag, gamepad stick and d-pad), fit, pause, calm, dialogs, sink all from kit2
word-garden 🌻 Word Gardenwordspell garden words from their letters; every word plants a flower that stays in your saved garden; hint / undo / skip~99 lines, 26-word kid-safe answer list, 2 rule checks in its test stubnone

Timings and first-pass results (MM/ci/factory-metrics.json; wall-clock, Mac under load ≈ 15–19):

Pilotcreate-gamefirst all-green run endstime to first greenfirst-run resultfirst full (release-prep) result
lantern-glide17:03:29Z17:05:55Z2.4 min (fast run 95 s)all 10 gates PASSFAIL: full pipeline, HUB load@phone/tablet B02 "Chrome timed out after 60 s" under load (the hub page, not the game). Fixed in the factory (copies get ≥ 180 s Chrome timeouts, recorded in the report); the same failure recurred once at load ≈ 11, under the first threshold, which is why the longer timeout is now unconditional
word-garden17:06:21Z17:08:29Z2.1 min (fast run 91 s)all 10 gates PASSPASS (all 13 gates, 341 s)

The time to first green includes writing the gameplay (the builder was an AI agent; a person will take longer to
write the game, not to clear the gates). Neither pilot needed a single fix to pass a gate. The one red result was
infrastructure (Chrome timeouts on the hub page under machine load), and one remote run of word-garden failed because
of a factory bug (Node 20 on oldisgold did not get --experimental-websocket; fixed, re-run green, 101 s).

Templates (create-game --kind <k> with no changes, scratch tree): all five reached all-green on their first
game-check (puzzle, word, ordie, 3d and arcade --full; arcade fast first), then again after the Codex fixes on
oldisgold (fast, ~95–105 s each). The 3d template first showed an S09 warning (removed objects never disposed); the
template now creates and disposes one per-round material, and the warning is gone. Template "time to first green"
in the scratch log (5–31 min) is queue time: all five were created at once and checked one after another.

Final gate table (both pilots, release-prep, full mode, on the frozen bytes, after all review fixes):

Kit2 2.4.0 + VMath 1.1.0, oldisgold.local, 2026-10-20 00:40–01:30 (release-prep --remote oldisgold.local):

GateScopeLantern GlideWord Garden
factory static F01–F08game✔✔
static S01–S20game✔ 0 errors, 0 warnings✔ 0 errors, 0 warnings
kid-safe S16game✔✔
verified mathsadvisoryN/A (no maths)N/A (no maths)
sink poison D45 (P1 planted save/typed text; P4 N/A, proposal P14)game✔✔
source controlsestate✔✔
estate sweep G1–G4estate✖ the hub page index.html has no viewport meta (hub builder's file)✖ same
display sink D40 (…or Die files)estate✔ 18/18✔ 18/18
browser B01–B07game✔ 10 runs, 0 errors✔ 10 runs, 0 errors
save boundary SB1–SB4game✔ 6 pass✔ 6 pass
rotation R1–R8game✔ 60/60✔ 42/42
pass + touchgame✔ 15/15✔ 11/11
factory probe P01–P36 + game testgame✔ 44/44✔ 43/43
gate self-testsestate✔✔
factory self-testsgame✔✔
full pipelinegame✔ PIPELINE PASSED✔ PIPELINE PASSED
verdict on the frozen bytesGAME GREEN, estate red (hub viewport), no driftGAME GREEN, estate red (hub viewport), no drift
second-family reviewCodex r20, r21, r22: SHIP (P10–P13 out of scope)Codex r20, r21, r22: SHIP

Templates (fresh scratch tree, fast mode on oldisgold, kit2 2.4.0): arcade, puzzle, word, ordie, maths GAME GREEN; 3d
GAME RED only on the sink-poison test, which gives no output for a WebGL page (the page renders; the test's recorder
never reports): a harness limit, added to proposal P14. The 3d template's probe P03c/P19/P20 text checks pass. The
maths template's generators: 10,000 / 10,000 valid each.

Release folders (review packet, Codex command, pre-publish text, checklist, publish step):

Metrics (MM/ci/factory-metrics.json): first all-green run 2.4 min (Lantern Glide) and 2.1 min (Word Garden) after
create-game, both on the first run with every gate passing; 17 and 15 recorded runs since (most are the review rounds).

7. Metrics: ci/factory-metrics.json

One entry per factory game: kind, createdAt (create-game), every gate run (at, endAt, mode, host,
seconds, pass, per-gate ok), firstRun (the first-pass gate results), firstGreenAt, runsToFirstGreen,
timeToFirstGreenMin and timeToFirstFullGreenMin. Time-to-first-green is wall-clock from create-game to the end
of the first all-green run, so it includes the builder's own build time; that is the number to drive down.
Remote and infrastructure failures are recorded as runs too (the 2026-10-19 Node 20 flag bug shows up as one red
remote run of word-garden).

8. Known gaps (honest list)

desktop row before release (docs/04 §7). Headless input is not proof of feel.

every way a child would; deep-play behaviour is the game's own test stub's job.

split over lines, parenthesised or commented ((label) ||= …), x. append( with a space, and a "node" initialiser
concatenated with text. None occurs in the pilots or templates. The runtime layers catch such text when it is shown:
probe P03c (DOM, attributes, canvas), P19 (plant-and-render) and the sink-poison test.

of the factory itself was run on 2026-10-19 (§9).

has the fix direction and a regression test). The kit tells the player and never makes it worse; it cannot fix it from
outside the SDK (two attempts, both withdrawn after review).

9. Review of the factory itself

A read-only Codex review of the whole factory (kit2, scaffolder, templates, gates, release-prep, both pilots) ran on
2026-10-19 (~/.cache/claude-scratch/game-factory/codex-r1/codex-review.md): verdict DON'T SHIP, 6 P1 + 8 P2.
Triage: raised 14 · verified against the code 14 · folded 14 · rejected 0.

#FindingFixProof
P1-1F01 accepted let re-assignment, G.safe("x")+name, append(name), n["textContent"]=, setAttribute (constants = const literals only; a sink call must be the whole expression; bracket writes, spaced calls, append/prepend/before/after/replaceWith of non-nodes checked6 new planted cases in factory-static --selftest
P1-2single-quoted / attribute-first kit2.js tag turned both factory gates into N/AisFactoryGame and scriptSrcs parse any quoting, attribute order and query stringselftest case
P1-3a newer-version save arriving mid-round was overwritten by the round's result (device and cloud; the future field lost)G.end releases the store hold BEFORE the game writes its result, so the newer save loads, blocks writes and is reportedP13b: fails without the fix (device and cloud overwritten), passes with it
P1-4G.commit() reported success when the device could not store (SDK writes asynchronously)commit also checks the device can store; the player is toldP16
P1-5winning a round from a gamepad handler crashed to "Something broke"every phase of a frame re-checks that the round is still livereproduced on the puzzle template: now results, no errors
P1-6release-prep could reuse an old game-check.json PASSrefuses a non-empty --out; deletes the result file first; child exit status must match the resultcode
P2-7a drag survived pause (Lantern Glide steered by a dead finger)releaseAll ends the drag and tells the game ({type:"up", cancel:true})P18
P2-8gameplay input (gamepad, custom keys) worked behind dialogskit input ignored while a dialog is open; G.modalOpen for custom handlers; word template + Word Garden guardcode + P05
P2-9a pause inside a timer did not stop the rest of that frameScope.tick(dt, halt) stops when the round pauses or endsP17
P2-10a stale Settings promise paused a new roundrun-identity check on the continuationcode
P2-11the word template scored a word twice (undo during the transition)run.busy guard (Word Garden already had it)code
P2-12P03 scanned only the title, never attributesP03c scans text and text attributes on title, play and results, also upper-casedselftest fixture caught on all three screens
P2-13P09 missed a raw window.addEventListener per roundraw window/document/visualViewport listeners counted by identityselftest fixture now leaks one per round and is caught
P2-14freeze manifest missed gate helpers/config, new game files, odd script URLsall `ci/*.mjsshpyjson + ci/factory/**` + hub page frozen; new files in the game folder count as driftcode

Round 2 (same reviewer, verifying): 9 fixed, 5 partly, 2 new P2s. Round 3: 4 of 7 fixed, 3 partly, 4 new
(N3–N6). Each round's items were verified at file:line before fixing; none was rejected. What rounds 2–3 added:

ItemFindingFixProof
#1 (r2)const label=player.name; n.append(label); Math.round(1) && nameidentifiers resolved to their initialiser; &&/?? split; numeric calls must be the whole expression2 more selftest cases (Codex r3: FIXED)
#3 (r2, r3)newer save overwritten via pause/resume, then via quit or replay after a mid-round editapplyIncoming() (load the waiting save) before every commit and every release (end, quit, replay, error)P13b and P13c: both fail without the fix, pass with it
#4 (r2)storage that accepts a tiny probe but refuses the real saveafter each commit the device copy is read back (lastSaveOk); the player is toldP16b (Codex r3: FIXED)
N3 (r3)the retry used store.saveSoon (bypassing the incoming-save check)retries go through G.commit(), at most 3code
#12, N4 (r2, r3)canvas text not scanned; a bounded capture could drop an unsafe draw; fail-closed check ignored canvasevery fillText/strokeText captured, violations kept apart from the bounded log; P03b also checks canvas wordsselftest + all games green
N1, N5 (r2, r3)once listeners: counted forever, then never counteda once listener counts until its event firescode
#14, N2, N6 (r2, r3)config drift hidden by load tuning, or changed between copy and hashthe baseline is the copied file's hash, taken before tuningcode (Codex r3: N2 FIXED)

Known limit kept honest: the SDK skips identical re-writes and writes the device copy asynchronously, so after a
failed device write the kit can tell the player and re-commit (which applies any incoming save first), but the device
copy is only re-written by the SDK on hide/pagehide or by the next change. Proposal P9 asks the SDK for a write status.
Rounds 4–9 (same reviewer, verifying each fix and hunting new ones), plus the coordinator's reviews of the frozen pilot
candidates (master-mind-games seq895, seq900) and two operator decisions (D45 ingest at the source; verified maths). Every
item was checked at file:line before fixing; none was rejected. Main items and their regression checks:

SourceFindingFixCheck
r4N7/N8 canvas words and duplicate once listeners could slip the scans; N9 an immediate save failure left lastSaveOk=trueviolations kept outside the bounded log; once tracking per registration; status set on every failureprobe
r5N10–N12 listener re-registration, title/alt/placeholder in the fail-closed check, the no-SDK retry budgetfixedprobe P03b, P09
r6N13 a refused Reset claimed successreal result + read-back + retryP23
seq895 #1–#4Reset downgraded a v99 save; a failed reset claimed success; retries never really re-wrote; a mid-round player switch reported A's play time as B'squarantine (read-only, never reset); read-back; a forced write (k2w) with backoff and a "Not saved yet" badge; events and play time bound to the round's player (kit clock)P22, P23, P24, P21
seq900the SDK's unheld path overwrote v99; prefs:null crashed Settings; P paused word games; no controller navigation; DOM boards could not scroll; leaving lost the round; focus lost on rebuild; 7/6 counterkit refuses every write while quarantined (SDK fix P10); P() prefs; Escape-only pause; controller navigation; touch-action by surface; saveOnQuit; G.rebuild; pilot counterP25, P22, P27, P28, P29
D45text must be filtered where it ENTERS, tooG.ingest, G.textInput, G.attr, G.title, save fields ingested on load, late-filter repaint; F01 (+aria-*, value, title) and F08 (input boundaries)P19, P20, F08
r7another tab's newer save; cleanup could reach other runners; F01/F08 gaps (["value"], Object.assign, Reflect.set, message via the kit); Word Garden empty after a late filter; Start resumed behind Settings; recovery after "fail"; boot writes unreporteddevice copy checked; path-boundary + launch-time + orphan-only cleanup with explicit --owned; rules widened; onFilter hook + lazy pool; resume guarded; filter watched for the page's life; boot through G.commitP32, P30, P31, P20, selftests
r8N1 adopting the newer copy rewrote the SDK's record; N2 same-second runs shared a folderquarantine is the kit's own flag, nothing copied into the SDK; unique run foldersP32
r9N3 the SDK's own flush rewrote its stale record (sibling key lost); N4 quarantine leaked to the next player; N5–N8 VMath (sampled uniqueness, primality not checked, 1/00 threw, ÷ 0 passed); N9 maths options' accessible names; N10 an empty maths block passedfirst a snapshot/restore guard (withdrawn in r10: it could roll back a genuinely newer record, N12); now every write of an SDK record is MERGED with the device copy, newest entry per key by timestamp (byte-identical in one tab; SDK fix P13); quarantine per player; VMath grid semantics (exhaustive, bounded, answer on the grid, off-grid fractions d ≤ 12 searched unless the prompt names the answer set), real primality, never throws, glyph-normalised display check; option value = accessible name; strict maths CIP33, P34, factory-maths --selftest
r10N3/N11/N12 cross-tab loss on an immediate pagehide or a player switch, and the restore guard's rollback; N13 SIGTERM exited without stopping the active gate; N14 ordie choices unnamed for screen readers; N15 an identifier alias passed F01; N5 sampled uniquenessstorage-boundary merge (above) + quarantine on every storage event; signal handler ordered first and gate pid recorded at spawn; picture names; alias chains followed; a grid-checked maths item must name its answer set in the prompt or list every solution (solveAll)P34, P35, F01 selftest, maths selftest
r11–r12the storage merge was not sound (equal timestamps, lost dirty flags, a read-merge-write cannot be atomic across tabs: N16–N19); the writing-tab lease that replaced it added its own loss paths (expired lease + stale memory, take-over reload, copied sessionStorage ids, cloud writes from a read-only tab, the no-SDK fallback: N21–N25)both withdrawn. Lesson: the kit must not intercept the SDK's device writes. Cross-tab concurrency is an SDK limitation shared by every SDK game (proposal P13). The kit now only tells the player (G.otherTab, BroadcastChannel, no write interference) and quarantines a newer save; N15 parameters/destructuring flagged; N20 the runner waits for a killed gate before cleanupP32 (heads-up), P34, F01 selftest
r13N26 the kit's durability retry could write an older value over a newer same-version save another tab made just after (worse than a plain SDK game); N15 parameters/destructuring sharing a name with a declared node; N27 nested pipeline gates survived a signala different device value written after our commit is another writer's: no retry (checked again before each retry); names bound as parameters or destructured anywhere are unknown; the runner kills the active gate's whole descendant treeP36, F01 selftest
r14–r19N26 (variants): the kit's forced retry could still replace another tab's newer save (an unchanged commit, a failed probe, a skip that adopted the other value); N29/N34 a reset or commit right after a save could drop the earlier check and abandon recovery; F01 gaps (alias of a parameter, spread, insertAdjacentText, outerText, logical assignments); N28 a gate timeout left nested gatesa retry may only replace the exact value THIS tab last saw on the device, and never after yielding to another writer; only a commit that really writes is verified, against the value captured at that write; a superseded check only moves the baseline; a pending check is completed, never dropped; F01 widened each time; timeouts kill the gate's whole treeP36, F01 selftest (each reproduction is a planted case)
r21–r22N33/N35/N36 F01 gaps (probe. data, {data:…}, logical re-assignment); a real-Chrome P24 failure: the SDK's own later flush landed our value and the retry called it "another writer"F01 widened (whitespace, object keys, every value of a name); a device already holding exactly our current value counts as savedF01 selftest, P24; Codex r21 and r22: SHIP for both pilots
r20—SHIP for both pilots within scope (P10–P13 out of scope); three P2 static-gate gaps found by injection (none in the pilots)closed in r21
coordinatorChrome orphans (oldisgold: 13, one at ~700 % CPU for 4 days)ci/factory/chrome-guard.mjs + watchdog + ci/cleanup-orphans.mjs; async gates so SIGTERM stops a run (tested: SIGKILL → 11→0 processes in 15 s; SIGTERM → stopped in 3 s, 0 left); upstream patch P11manual tests, P11 test

Each round's verdict and the round-10 result are in §6 and the hand-back. Review files:
~/.cache/claude-scratch/game-factory/codex-r1 … codex-r10/codex-review.md.

Continue exploring