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):
--kind | Surface | What the template is | Typical next game |
|---|---|---|---|
arcade | 2D canvas, real-time | catch the stars, dodge the clouds; keys + pad + drag + gamepad | catchers, dodgers, runners |
puzzle | DOM board, turn-based | lights-on grid; arrows/d-pad move, tap/Enter/A flips | grids, matching, logic |
word | DOM board, turn-based | spell the word from its letters; kid-safe answer list | spelling, anagrams, vocabulary |
ordie | DOM board, timed rounds | an "…or Die" quick-fire round on OrDie.run (lives, tiers, streaks) | quiz/party rounds |
3d | WebGL (three r128) | steer and collect; renderer in try, shared assets, per-round disposal | small 3D arcade |
maths | DOM board, turn-based | 8-question rounds from two verified generators (equivalent fractions/decimals/percent; a missing number), answers compared by VALUE | any maths game (operator, 2026-10-19: verified maths by default) |
2. What create-game writes
| File | What it is |
|---|---|
games/<id>/index.html | the 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.js | the 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.md | the builder's notes while building (plan, kid-safety plan, questions) |
factory/<id>/<id>.test.mjs | the game's own checks, run by the probe through the test hook (add your rules: scoring, win/lose, what is saved) |
ci/factory-metrics.json | the 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:
| Area | What Kit2 does | Lesson / gate |
|---|---|---|
| Saves | one 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 result | D42, S19, SB1–SB3, Anime Punch 12, seq895 #1–#3, seq900, P11, P13, P13b/c, P16, P22–P25 |
| Events | quest 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 |
| Players | a hub player switch abandons the round (never saved into the new player), clears G.mem, closes dialogs, releases input, repaints the title | SB2/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 out | every 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 everything | D40, D45, S20, F01, docs/06 L8, P03, P20 |
| Input | keyboard 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 switch | S07, T2, Anime Punch 3–6, P06–P08 |
| Layout | stage inside the safe areas; canvas re-fitted on resize, rotate and visualViewport changes; backing = CSS × min(DPR, 2); never stretched | S10, B03, R1–R8, P14 |
| Time | rAF 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 round | S08, S11, guide §2.5 (stale timers), P04 |
| Calm mode | one 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 second | Anime Punch 9–10, docs/07 §6, P10 |
| Dialogs | title, 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 ignored | S12, Anime Punch 14–15, P05 |
| Controller and keys | the 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 phones | seq900, P27, P28 |
| Resources | G.page / G.scope own listeners, timers and disposables; Kit2.disposeTree for 3D; Kit2.resources() counts what is live | S09, Anime Punch 17, P09 |
| Standalone | works in the hub iframe and from a file or tab; without the SDK file it runs device-only | B01, guide §2 |
| Test hook | Kit2.test: state, play, end, pause, resume, quit, hold/release, resources, calm, save copy, log | P01–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.
| Gate | Scope | fast | --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 mode | estate | ✔ | ✔ |
| display sink suite D40 (the …or Die file list) | estate | ✔ | ✔ |
| save boundary SB1–SB4 | game | ✔ | ✔ |
| rotation R1–R8 (fast: 390×844 only; full: both pairs) | game | ✔ | ✔ |
| pass + touch T1–T6 | game | ✔ | ✔ |
| 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 | ✔ | |
| pipeline | game (estate when it is red only in its stage-1a sweep on files outside the game) | --quick | full (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 ownfactory/<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 shipsgames/_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.
| Pilot | Kind | What it is | Custom code | Platform code in the game |
|---|---|---|---|---|
lantern-glide 🏮 Lantern Glide | arcade | steer 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 stub | none: saves, input (keys, 4-button pad, drag, gamepad stick and d-pad), fit, pause, calm, dialogs, sink all from kit2 |
word-garden 🌻 Word Garden | word | spell 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 stub | none |
Timings and first-pass results (MM/ci/factory-metrics.json; wall-clock, Mac under load ≈ 15–19):
| Pilot | create-game | first all-green run ends | time to first green | first-run result | first full (release-prep) result |
|---|---|---|---|---|---|
| lantern-glide | 17:03:29Z | 17:05:55Z | 2.4 min (fast run 95 s) | all 10 gates PASS | FAIL: 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-garden | 17:06:21Z | 17:08:29Z | 2.1 min (fast run 91 s) | all 10 gates PASS | PASS (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 firstgame-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):
| Gate | Scope | Lantern Glide | Word Garden |
|---|---|---|---|
| factory static F01–F08 | game | ✔ | ✔ |
| static S01–S20 | game | ✔ 0 errors, 0 warnings | ✔ 0 errors, 0 warnings |
| kid-safe S16 | game | ✔ | ✔ |
| verified maths | advisory | N/A (no maths) | N/A (no maths) |
| sink poison D45 (P1 planted save/typed text; P4 N/A, proposal P14) | game | ✔ | ✔ |
| source controls | estate | ✔ | ✔ |
| estate sweep G1–G4 | estate | ✖ 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–B07 | game | ✔ 10 runs, 0 errors | ✔ 10 runs, 0 errors |
| save boundary SB1–SB4 | game | ✔ 6 pass | ✔ 6 pass |
| rotation R1–R8 | game | ✔ 60/60 | ✔ 42/42 |
| pass + touch | game | ✔ 15/15 | ✔ 11/11 |
| factory probe P01–P36 + game test | game | ✔ 44/44 | ✔ 43/43 |
| gate self-tests | estate | ✔ | ✔ |
| factory self-tests | game | ✔ | ✔ |
| full pipeline | game | ✔ PIPELINE PASSED | ✔ PIPELINE PASSED |
| verdict on the frozen bytes | GAME GREEN, estate red (hub viewport), no drift | GAME GREEN, estate red (hub viewport), no drift | |
| second-family review | Codex 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):
~/.cache/claude-scratch/game-factory/releases/lantern-glide-2026-10-20T00-40-13-90976-gymvm/~/.cache/claude-scratch/game-factory/releases/word-garden-2026-10-20T01-07-28-10782-0n8bm/
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)
- Real-device walk-off: not done for either pilot; Lantern Glide moves an avatar, so it needs a phone row and a
desktop row before release (docs/04 §7). Headless input is not proof of feel.
- The probe drives rounds through the test hook and real input, but it does not play a game to its natural end in
every way a child would; deep-play behaviour is the game's own test stub's job.
- F01 is a source scan, a heuristic by design. Codex r21 still bypassed it with contrived injected code: a re-assignment
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.
_ordie/core.js(77 KB) loads with every factory game until proposal P1.- The factory runtime is new shared code: its first release needs the Codex review (§5). A pre-release Codex review
of the factory itself was run on 2026-10-19 (§9).
- Two tabs of one game and player can overwrite each other's saves (an SDK limitation in every SDK game; proposal P13
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).
- Not run by game-check: the hub suites (the hub is not changed by a new game), Code Editor override versions, fun.
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.
| # | Finding | Fix | Proof | |||
|---|---|---|---|---|---|---|
| P1-1 | F01 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 checked | 6 new planted cases in factory-static --selftest | |||
| P1-2 | single-quoted / attribute-first kit2.js tag turned both factory gates into N/A | isFactoryGame and scriptSrcs parse any quoting, attribute order and query string | selftest case | |||
| P1-3 | a 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 reported | P13b: fails without the fix (device and cloud overwritten), passes with it | |||
| P1-4 | G.commit() reported success when the device could not store (SDK writes asynchronously) | commit also checks the device can store; the player is told | P16 | |||
| P1-5 | winning a round from a gamepad handler crashed to "Something broke" | every phase of a frame re-checks that the round is still live | reproduced on the puzzle template: now results, no errors | |||
| P1-6 | release-prep could reuse an old game-check.json PASS | refuses a non-empty --out; deletes the result file first; child exit status must match the result | code | |||
| P2-7 | a drag survived pause (Lantern Glide steered by a dead finger) | releaseAll ends the drag and tells the game ({type:"up", cancel:true}) | P18 | |||
| P2-8 | gameplay input (gamepad, custom keys) worked behind dialogs | kit input ignored while a dialog is open; G.modalOpen for custom handlers; word template + Word Garden guard | code + P05 | |||
| P2-9 | a pause inside a timer did not stop the rest of that frame | Scope.tick(dt, halt) stops when the round pauses or ends | P17 | |||
| P2-10 | a stale Settings promise paused a new round | run-identity check on the continuation | code | |||
| P2-11 | the word template scored a word twice (undo during the transition) | run.busy guard (Word Garden already had it) | code | |||
| P2-12 | P03 scanned only the title, never attributes | P03c scans text and text attributes on title, play and results, also upper-cased | selftest fixture caught on all three screens | |||
| P2-13 | P09 missed a raw window.addEventListener per round | raw window/document/visualViewport listeners counted by identity | selftest fixture now leaks one per round and is caught | |||
| P2-14 | freeze manifest missed gate helpers/config, new game files, odd script URLs | all `ci/*.mjs | sh | py | json + ci/factory/**` + hub page frozen; new files in the game folder count as drift | code |
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:
| Item | Finding | Fix | Proof |
|---|---|---|---|
| #1 (r2) | const label=player.name; n.append(label); Math.round(1) && name | identifiers resolved to their initialiser; &&/?? split; numeric calls must be the whole expression | 2 more selftest cases (Codex r3: FIXED) |
| #3 (r2, r3) | newer save overwritten via pause/resume, then via quit or replay after a mid-round edit | applyIncoming() (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 save | after each commit the device copy is read back (lastSaveOk); the player is told | P16b (Codex r3: FIXED) |
| N3 (r3) | the retry used store.saveSoon (bypassing the incoming-save check) | retries go through G.commit(), at most 3 | code |
| #12, N4 (r2, r3) | canvas text not scanned; a bounded capture could drop an unsafe draw; fail-closed check ignored canvas | every fillText/strokeText captured, violations kept apart from the bounded log; P03b also checks canvas words | selftest + all games green |
| N1, N5 (r2, r3) | once listeners: counted forever, then never counted | a once listener counts until its event fires | code |
| #14, N2, N6 (r2, r3) | config drift hidden by load tuning, or changed between copy and hash | the baseline is the copied file's hash, taken before tuning | code (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:
| Source | Finding | Fix | Check |
|---|---|---|---|
| r4 | N7/N8 canvas words and duplicate once listeners could slip the scans; N9 an immediate save failure left lastSaveOk=true | violations kept outside the bounded log; once tracking per registration; status set on every failure | probe |
| r5 | N10–N12 listener re-registration, title/alt/placeholder in the fail-closed check, the no-SDK retry budget | fixed | probe P03b, P09 |
| r6 | N13 a refused Reset claimed success | real result + read-back + retry | P23 |
| seq895 #1–#4 | Reset 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's | quarantine (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 |
| seq900 | the 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 counter | kit refuses every write while quarantined (SDK fix P10); P() prefs; Escape-only pause; controller navigation; touch-action by surface; saveOnQuit; G.rebuild; pilot counter | P25, P22, P27, P28, P29 |
| D45 | text must be filtered where it ENTERS, too | G.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 |
| r7 | another 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 unreported | device 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.commit | P32, P30, P31, P20, selftests |
| r8 | N1 adopting the newer copy rewrote the SDK's record; N2 same-second runs shared a folder | quarantine is the kit's own flag, nothing copied into the SDK; unique run folders | P32 |
| r9 | N3 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 passed | first 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 CI | P33, P34, factory-maths --selftest |
| r10 | N3/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 uniqueness | storage-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–r12 | the 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 cleanup | P32 (heads-up), P34, F01 selftest |
| r13 | N26 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 signal | a 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 tree | P36, F01 selftest |
| r14–r19 | N26 (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 gates | a 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 tree | P36, F01 selftest (each reproduction is a planted case) |
| r21–r22 | N33/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 saved | F01 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 |
| coordinator | Chrome 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 P11 | manual 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.