‹ The toolbox

The ship plan — from a tested batch to users

Owner's ordering, 2026-07-29. A large batch sits on main that no user has seen — the sheets, the top bar, the pad, the board, sequence steps 1–5, the prop master, criterion 9. This plan takes it to production with the turn split on by default, in seven CC sessions. Statuses update here in the same commit as each session's ship (plans-live-in-room2). T5–T7 stay queued behind it — they gate criterion 1 only.

The order: the three mechanical closes (criteria 11 · 5 · 7) → the two dead paths → the UI glitch sweep → console dials (split · models · cost) → the thorough test → ship, split on → the criterion-2 human pass.
two rulesSessions run one at a time — session 1's free signal and session 2's sweep both edit room-ui.html, and 1b and 3 both edit admin.html; neither file is ever edited concurrently. And only session 5 deploys; every other session commits by pathspec and pushes to main only.

The sessions

#sessionservesstatusevidence
1The three closes — address never translated · the free signal · the result half11 · 5 · 7● done7 closed — the RESULT half was already a road; this is the audit that says so(every collecting kind cues once, gate_detach is the one-telling guard, the outcome channel never restates a result — 7 smoketest checks)plus one real defect found by reading the two strings side by side: the outcome note forbade「a result for a card that IS NOT ON THE TABLE」, and a settled card is not on the table, so on the one path this criterion is about the brief forbade what the criterion requires. Rescoped to「a card that NEVER LANDED」. Gate: the --quick set + s29 at N=8 → 6/6 at floor over 48 runs, s29 8/8. ⚠ And a blind spot, now session 1b: only the CLOCK's settle path reaches the panel in the exam — _life_fire carries a no-app-layer fallback and nothing else does, so every settle a human or the panel causes is untested.

5 closed — the free signal rides the turn: one delegated listener in the client, the iid resolved to a handle at drain, into the act brief and f's LOOKING AT row. s34(tap a card, say「这个关掉吧」)4/4 split — the same as s21's 4/4, which NAMES the card; controls 4/4 · 4/4 · 4/4, so it cost no restraint. Gate: 8/8 at floor over 32 runs.

11 closed — the room's addresses are masked out of the Layer B translator call and restored after it, a lost one rejects the translation(unmasked, the live translator turned「先手」into First move). 10 smoketest checks · new scenario s33 4/4 split · 2/4 un-split · --quick green. ⚠ and it found that Layer B reaches the kickoff turn and nothing else — an owner call, written up in the DoD §1
1bThe two dead paths — the exam's missing result cue, then Layer B back on the live turn with a console toggle7 · 11 · ops● donePART B done — Layer B runs on the live turn again, at the position criterion 11 rests on, with the addresses masked through it. Measured: 0.98s mean · 1.15s max · $0.02/1000 divergent turns — the money is nothing, the second is spent holding the ROOM LOCK, and a persona anchored to another language diverges every turn. So: a console switch(Settings → Room language, live, MAD_LAYER_B overrides), the timeout cut 60s → 12s, and every call booked as a translate event(it never was). 9 smoketest checks.

PART A doneScene mirrors the app's settle wiring, so a full card finally hands the panel its RESULT(only the CLOCK's path ever did). Re-baseline: 210 rooms · $0.63 · 93 min · 22/35 un-split · 32/35 split · 0 harness errorsthe table, and it is NOT item-comparable with the morning's — the prop master and the criterion-9 fix landed between). Attributable: one_door green in all 210, after-open hits +4 → +42, and criterion 7's live half(s35)askable at all, 3/3 both paths.

And both halves found a defect by being used. The re-baseline caught the leak scanner's pair rule firing on a GM's ordinary setup sentence(fixed to require one clause — after a first fix that suppressed the flagship leak and was thrown away); the live Layer B caught the address mask having nothing to mask on the turn that ARMS a card. Both fixed, both planted or asserted. ⚠ One thing left unexplained rather than explained away: s02 under the split went 2/3 → 0/3 and nothing in this session can reach it — written up on the exam page for session 4. Owner-ordered 07-29 off session 1's two findings
2The UI glitch sweep — every toolbox surface, owner's list firstquality◐ sweep done · 0a/0b with the ownerTHE OWNER'S TWO FLASHES ARE NOT THE RE-RENDER FAMILY. Measured at 375×812 in both regimes: opening the drawer and clicking the composer each change nothing outside their own surface — 0 DOM mutations, 0 geometry deltas over every element, 0 computed-style deltas(held 1.5s, covering the 160ms settle and the 250/300ms belt), no image destroyed, chat/composer/topbar node identity intact. Nothing is being rebuilt. ⚠ A before/after diff was the wrong instrument for 0b and said so by returning zeros: the owner's symptom is a composer that flies up AND COMES BACK, and a thing that returns to where it started has a zero delta. Transients need a time series.

0a · CLOSED(2d4a304), and the cause was smaller than the compositing story chased first. display cannot be transitioned, so .rx-scrim going display:none → flex laid its full rgba(0,0,0,.35) over the viewport in ONE frame — a screen-wide instantaneous darkening reads as a flash, not as a drawer. It was the last layer in the app still on the display switch: theme.css already fades every other scrim(.modal-scrim / .vp / .mep, opacity 0→1 over .18s, held by visibility). Converted to that pattern(owner's own call:「make the blacken effect gradient」). Two costs handled: both entrance animations moved onto .open(under visibility the sheet is always rendered, so a base-rule animation runs once at load and never again), and the always-laid-out scrim is confirmed not to intercept taps. ⚠ The earlier backdrop-filter/backdrop-root diagnosis was WRONG and is withdrawn — the owner ablated it and the flash persisted.

0b · STILL OPEN. Owner: 「the composer flied up and down in hundreds of ms, as if it wants to make way for the virtual keyboard for phone」— which names the shell-height machinery, not a repaint. Not reproducible in any regime a CC session can drive: in a real narrow desktop Chrome at 375 with a fine pointer, logging every --app-h/--composer-h write, body.kbd-up, and the composer rect on every scroll/resize/focus across a real click gives compTop 737 → 737, --app-h never set, kbd-up never toggled. The phone path cannot engage there, because IOS is a UA sniffroom-ui.html:5684)and FINE a pointer query — both true-to-desktop in the owner's regime(F12 DevTools docked, normal UA, mouse), which also rules out the emulation theory(preShrink bails on !IOS; body.kbd-up measured to move the composer 0px on desktop, safe-area being 0). Next: a rAF recorder the owner pastes in their own window — rAF runs there and nowhere available here, so their screen is the only instrument that can catch the transient.

The scoped sweep is green, with six real defects found and closed. Nine surfaces(five setting sheets · drawer · board · pad · clock)at 375×812 in light and dark: zero overflow, zero geometry issues, docScrollW===375 throughout. Top bar with three live gates: lvl0, both pills whole, no fold, and 0 element rebuilds over 6s(chip nodes 2/2 survived)— the strip obeys「don't re-render what didn't change」. Back-gesture coverage complete(44 registrations; the 9 apparent gaps are all body-class-registered or not layers). Dark-theme contrast: no real defects — the only two hits are a disabled commit button(exempt)and the designed OKLCH seat palette. ⚠ A first contrast probe reported four more and was wrong: it skipped translucent layers instead of compositing them, and the lifecycle bands are exactly that. Fixed: three missing [hidden] guards(.mep-row · .ci-actions · .vp-online — each beside an already-guarded sibling)and three hand-built <img> templates left on decoding="async".art-img · .my-poster · mdInline(), the last shared by chat bubbles AND the board). Both laws' style-guide entries updated with the sweep. smoketest PASS.
3Console dials — the split as a setting · per-call models · per-call cost + toolbox health · the three kill switchesops● doneAll five items landedturn-split §11 is the write-up). 1 · the split is a live console setting, read at the drain, ships OFF — which is what makes it the rollback: no restart, no deploy. The precedence is three-deep and each layer earns its place — MAD_TURN_SPLIT pins a box, the module global stays the harness pin exam/run.py sets with no DB at all, then the console. ⚠ A live switch whose effect lives in a CACHED prefix is only live if something re-stamps it: _sync_system re-builds a room's system block when the mode moves under it (one cache miss, said out loud in the UI copy), and the three dials are read once at the top of the drain so a flip landing mid-turn cannot produce a turn that ran half each way. 2 · per-call models — the act call's was the find: it silently inherited the room's PANEL model, the most expensive one in the room, on every split turn, carrying the manual, to fill in a form. Default stays「inherit」so shipping it changes nothing; its tally and its act_call event now name the model that actually ran, because billing it to the panel model hides exactly the saving the dial exists to make. No reasoning switch on any of the three — all send thinking:disabled, and a switch that changes only the price is a lie. 3 + 4 · two Status cards, in the exam's vocabulary so they are comparable: cost per call type (a turn is up to SIX provider round trips, and one total is the number that hides which grew) and toolbox health — arms by kind AI vs human, arms per turn, refusals, prop PROP/NONE with fail-open broken out. Arms count at _mint_handle, the one site every arm passes through; refusals are READ off the existing *_bad counters, never copied.

5 · the three kill switches, and the load-bearing half is what they do NOT do. The human refusal is one line in room_tool_access() — so「open cards still settle」is not a special case but a consequence of where it lives: the arm routes ask that predicate and the settle routes (vote_tap·roll_tap·seal_put·gate_reveal·roll_go·gate_release) never do. The AI door drops only the blocks that put a NEW card down; a reveal, a close, a take-down, an <act> and a <react> still land. ⚠ The speech prompt needed a THIRD state, not a flag — chat-only is not hands-off-minus-the-manual: the hands-off note tells a host the room HAS dice and that they are set up before it speaks, which in a tool-free room is a promise nothing can keep, so the host offers a die that never arrives. 5c is the CHAT's(owner's placement, same day): a Tools pill in chat info, third of four, right of Mute, mirrored in the ⋯ menu, persisted in state.json, audited like every other room setting and pushed over SSE so every member's window follows in the same frame. ⚠ The first cut made it an account setting with a「chat-only once EVERY human has opted out」rule; the pill replaces the setting AND the arithmetic, because the gate does that work instead — it answers to the floor producer's gate(admin · a private chat's OWNER · never a plain member, never anyone in a live chat), so「one member must not end another's game」holds by construction rather than by counting.

Gates. 42 new smoketest checks (default · override · persistence for every dial, both doors, the three-state prompt, the counters) — smoketest PASS · i18n_audit OK · --selftest PASS. Manual probes on :8016 with each switch off: the composer button measured display:none at 375, the human arm route 403 and 200 again on restore, the three console pages rendering (the health card's counters showed the ballot the probe itself armed), and for 5c the whole loop — the pill third of four with docScrollW 375, its label and struck-through glyph flipping, the composer door closing in the same frame, the server persisting it, the ⋯ mirror reading「Tools on」, the tools SSE event arriving at a listening client, and the ballot already on the table still there with the toast saying so(「nothing new opens; 1 card already up stays」). The split regression set with tools ON: 6/7 at floor, all four controls 4/4 — and the switches' idleness was proved directly rather than inferred: the three model-facing prompts (speech un-split · speech split · the act-call manual) are BYTE-IDENTICAL to the previous commit.

⚠⚠ ONE RED, AND IT IS NOT THIS SESSION'S — s20's no_leak is failing on main. It is a floor-1.0 invariant and s20 is in the --quick gate set, so this is a ship blocker for row 4/5. Three arms, same shape every time(a role flipped in speech —「翻牌:Ben是预言家」): this build in the 7-scenario set 3/4, this build alone 2/4, and HEAD as a controlled arm 3/4. Byte-identical prompts plus a red control is as close to attribution as the instrument gets. Criterion 8's territory — handed to session 4
4The thorough test — full exam both paths · latency + cost · a real phonereadiness◐ GO · phone owedGO — the ship configuration is the best-measured state the exam has recordedthe verdict, exam §7). Under the split: 33/35 at floor(was 32)· 100/105 clean runs(was 97)· controls bare 9/9 · 0 harness errors in 210 rooms, $0.576, 85 min. The gate agrees — --quick at N=8 is 5/5 at floor over 40 runs, 0 hard leaks. Static gates all green(smoketest PASS · i18n_audit OK · --selftest PASS), and the parse-health corpus replays through today's parser with 0 crashes over 516 prod turns.

The un-split path did not move, and the churn is the point. 82/105 clean against 83/105 — but 15 cells crossed a floor, 7 down and 8 up. At N=3 on a ~0.78 base rate that is the coin toss the exam's own honest-limit box predicts; the aggregate is the statistic that holds still, and it is flat. No named regression is owed because nothing regressed.

SESSION 3'S SHIP BLOCKER DOES NOT REPRODUCE. s20's no_leak, measured at N=8 on the path session 3 saw it fail: 0 hard leaks in 8 runs, and 3/3 in the full run. The same shape DID fire once in three SPLIT runs, so it is real — but it is intermittent(~1 run in 10), older than this batch(the re-baseline read s20 split 1/3; it is 2/3 now), and session 3's prompts were proved byte-identical. It ships as a known criterion-8 red, owner-carried, and it is the first thing T5–T7 should be pointed at. ⚠ And the raw leak TOTAL misleads: 13 hard leaks, but 9 are s32, the complier scenario, where no_leak is a watch by design.

THE PRICE, off the event records over 863 panel turns. A split turn is 5.53s · $0.000754 against un-split's 5.09s · $0.000497+0.45s(+9%)and +52%, or $0.50 → $0.75 per 1000 turns. The latency is nearly free where it matters: with the floor producer on(the default for every new room)the act call runs concurrent with f and outlasts it on only 5 of 112 turns — +0.01s mean. It is only with the FP off that it runs serial and costs its full 2.25s. Components(split): prop 0.98s/$0.000041 · f 3.16s/$0.000229 · act 2.02s/$0.000510 · speech 2.64s/$0.000368.

AND THE ROW-3 PREMISE FOR THE ACT-MODEL DIAL IS WRONG. This prompt said「the act call no longer rides the panel's Pro pricing」— it does. _act_model() defaults to "" → inherit(run_room.py:3618), exactly as session 3 intended(「default stays inherit so shipping it changes nothing」). So the numbers above ARE the shipping price, and the act call is 68% of a split turn's spend — the biggest cost lever in the ship configuration, carrying the ~5.6k-token manual at panel rates to fill in one form. Moving it to flash projects ~$0.60/1000, but that is arithmetic off the rate card, not a measurement, and it is an act-surface change that owes a --split 1 gate. A post-ship session.

THE PHONE HALF IS NOT DONE AND A CC SESSION CANNOT DO IT. The LAN server is up and verified correct(0.0.0.0:8011, dev wiring, v655, login rendering at 375×812 with docScrollW 375), but the virtual keyboard, real touch and mobile Safari need a hand holding a device. Owner's pass, with #kbdebugthe canon): composer focus without a page shove · body.kbd-up while typing · .chat bottom never exceeding innerHeight on a plain swipe · the toolbox surfaces arm→close→reveal end to end
5Ship — split default ON · BUILD/CACHE bump · deploy-box · post-probeseverything○ queued
6Criterion 2 — the funnel + the hallway test, on real users2○ queued

The prompts

Each is written for a cold session: it names its gates and its memories, and the session updates this page's status row in the same change as its ship.

Session 1 · the three closes

Toolbox DoD: close criteria 11, 5 and 7 — three small mechanical items, in this order,
committing by pathspec + pushing after EACH one (NO deploy). Read docs/toolbox-dod.html and
docs/exam.html first; the host-moves-study and prop-master-gate memories orient you. The exam
gates every change, and THE GATE MUST MATCH THE SURFACE (exam/run.py's docstring): manual or
marker edits → python exam/run.py --quick; act-brief or act-call edits →
python exam/run.py s20 s21 s07 s02 s18 s30 s31 --n 4 --split 1 --yes.

1 · CRITERION 11 — enforce「an address is never translated」(DoD §1 ruling). Matching and
targeting run on the AUTHORED string only; Layer B translation is display-only. Add the
enforcement wherever a title/handle could pass through translation, a smoketest check, and a
new exam scenario s33: a card titled in 中文 addressed from English speech, and the reverse.

2 · CRITERION 5 — the free signal (DoD §4): when a human taps a card and then types, the
client knows which card the finger was on — ride it with the turn(「the guest is looking at
deal#2」)into the act call's brief and f's signals. Client half in lib/room-ui.html.
⚠ A brief edit is a regression event: if any reach number moves, A/B against the previous
commit at N=8 in a worktree before concluding anything.

3 · CRITERION 7 — the RESULT half: armed/refused ride the outcome channel once; a card's
RESULT still reaches the panel only as a cue. Read the DoD cell and make the MINIMAL close —
if the cue path already satisfies「the persona reads the outcome of its own move」, the close
is evidence + a status flip, not code. Guard the channel's rides-once property; never
double-tell a result.

Update the DoD cells and docs/ship-plan.html row 1 in the same commits.

Session 1b · the two dead paths

Two dead paths session 1 found and left for an owner call. Both are now GO. Do them in
this order — the exam first, because it is the instrument the second half is measured with —
committing by pathspec + pushing after EACH part (NO deploy). Read docs/exam.html (the box
above the run history) and docs/toolbox-dod.html §1's crit-11 finding first; the
host-moves-study and room-language-handling memories carry both findings.

⚠ THE GATE MUST MATCH THE SURFACE (exam/run.py's docstring). Part A changes the RUNNER, so
its gate is a re-baseline, not a pass/fail. Part B changes server behaviour but no
model-facing string, so its gate is python lib/smoketest.py + python exam/run.py --quick.
⚠ --quick is IGNORED when you also name scenarios (main() lets named keys win and the run
stops being a gate) — to add one, list the five QUICK keys explicitly.

⚠ A PARALLEL SESSION IS BUILDING THE TIMER CARD (docs/timer-card.html) and owes one small
lib/run_room.py change — the clock ring event gaining by_uid, around :9687/:9725/:14148, plus
card rendering in lib/room-ui.html. NONE of that is a region you touch (yours are ~:7090
Layer B, ~:13600 drain_turn, and the console-settings block), so work in parallel — but:
· :8011 IS THEIRS. If part B's console toggle wants a server, use mad-preview-8016 from
  .claude/launch.json — same dev-data wiring, same SW bypass.
· Commit by explicit pathspec (git commit -- ) and git pull --rebase before pushing;
  one working copy, two sessions (remote-moves-via-parallel-sessions).
· And the one REAL conflict is A2: a --full re-baseline is an hour of measurement, so a
  run_room.py change landing mid-run gets attributed to you. Do A1, A3 and A4 freely, then
  WAIT for the timer card to be pushed, rebase, and only then run A2.

PART A · FOUR OF THE EXAM'S FIVE SETTLE PATHS NEVER REACH THE PANEL
_gate_finish — the settle that turns a full card into a RESULT and queues the one cue the
panel reads it by — lives inside build_app's route layer (lib/run_room.py ~16274), and exam's
Scene drives the Room directly. THE ONE EXCEPTION IS THE CLOCK: _life_fire (~:11704) carries
an explicit `elif acts:` fallback —「no app layer — still land the record + cue」— so a card
whose deadline runs out finishes itself. That is why s28 is the ONLY scenario in exam/runs/
with a settle event to its name (11 vote_result) and the other 33 have none between them.
Verify that yourself before you start; it is the measurement the rest of this rests on.

So what is untested is every settle a HUMAN or the PANEL causes, which is every ordinary way
a round ends: an all-in tap, a full reveal, the panel's own , an arm-time
replacement, a face-up deal.

1 · Give Scene the ROOM half of _gate_finish — vote_finish · seal_finish · roll_finish ·
    deal_finish · gate_finish, no hub calls. _life_fire's fallback is the pattern, one level
    up. Wire it everywhere the route wires it, and mirror the route rather than re-deriving
    it — a check that owns a second copy of a rule is a second rule (criterion 9's lesson):
      · vote_tap / roll_tap / seal_put coming back `complete` → detach → the kind's finish
        (「everyone has voted」·「everyone has rolled」·「everyone has answered」)
      · gate_reveal's `full` → `gate_detached` →「revealed」; its ROW path has three different
        endings by kind (~:20014) — copy them, they are not the same call
      · user_vote / user_seal_open / user_roll_open `released` (a unique arm replacing an
        incumbent) and user_deal's `settled`
      · after drain_turn: gate_applied `released`, roll_applied `settled` / `released`,
        seal_applied `settled` / `released` / `revealed`, acts_applied `released` / `revealed`,
        rolls_applied `deal_settle`, notes_applied `deal_settled` — the app's own post-turn
        sweep is ~:16150–16230 and it is the list to copy
2 · RE-BASELINE: python exam/run.py --full --n 3 --yes (both paths, ~$0.9 now that cards
    settle). Diff every scenario against docs/exam.html's 07-29 run. Cards that used to sit
    open now settle, so survivor · distinct · one_door · no_orphan will move — say which
    moved and why, per scenario. A floor may be RAISED freely; lowering one is a deliberate,
    dated act. ⚠ one_door going red is the change EARNING ITS KEEP, not a regression — chase
    it, it means a result was told twice.
3 · A scenario that scores the reading: the panel opens a ballot, humans vote, the panel
    closes it with its OWN act, and the next turn must state the count. The count exists only
    in speech, so it is a `watch` prose heuristic that PRINTS what it matched (ghost_narration
    is the precedent), never a gate. This is criterion 7's live half.
4 · Rewrite docs/exam.html: the blind-spot box becomes a finding with numbers, the run table
    and history gain the re-baseline row, §2's check vocabulary gains the new check.

PART B · LAYER B REACHES THE KICKOFF TURN AND NOTHING ELSE
_translate_divergent is called only from Room._call, whose callers are the kickoff and
Room.say() — and Room.say() has had no callers since the batched turn became the door. The
live path is enqueue_human → drain_turn, which resolves the turn without ever asking it. So
the translate-on-divergence net validated 2026-06-21 (docs/room-language.html) has reached
one turn per room ever since.

1 · Call it in drain_turn at the SAME position it holds in _call — after _resolve_turn, before
    the history append, re-serializing `hist` so the panel's memory keeps the room-language
    version. ⚠ THAT ORDERING IS CRITERION 11: every matcher (the act call's target,
    , the uniqueness invariant) must have read the AUTHORED reply first.
    The address mask is already built — translate_to(text, target, keep) with
    self.room_addresses() — so pass it, and the smoketest's crit-11 block will hold you to it.
2 · A CONSOLE TOGGLE (owner asked): lib/admin.html + lib/run_room.py, following the existing
    console-Settings rows and the cost.py apply_overrides pattern. Ships ON. Env override
    beside it. Smoketest: default · override · persistence · and that OFF leaves the reply
    untouched. It already fails open (any translator error keeps the original) — assert that.
3 · MEASURE WHAT IT COSTS, because that is why it was an owner call and not a fix: the added
    wall clock on a turn that diverges, from the event records. A Chinese persona in an
    English room pays it EVERY turn. Put the number in docs/room-language.html and in the
    setting's own UI copy (t()/i18n; python lib/i18n_audit.py after).
4 · Watch s33 — an English room whose persona replies in 中文 is exactly the shape that now
    starts translating, so it is the scenario most likely to move.

Update docs/room-language.html, docs/exam.html, the DoD cells the numbers move, and
docs/ship-plan.html row 1b in the same commits.

Session 2 · the UI glitch sweep

UI glitch sweep after the toolbox wave(sheets v634 · top bar v635 · pad v636 · board
v642–644 · icons v616 · naming v647). THE OWNER'S LIST COMES FIRST — two reported flashes,
both smelling of the re-render family (the re-render-flash memory):
  0a · tapping the room TOOL BUTTON flashes the whole screen once. Opening a drawer must not
       repaint the chat — find what is being rebuilt that did not change.
  0b · DESKTOP browser, narrow mode: clicking the COMPOSER zone flashes the screen once.
       Reproduce in a real narrow Chrome window (the preview pane does not composite) — a
       focus handler is likely rebuilding or relayouting something on desktop that only a
       phone keyboard should trigger.
Diagnose both at PAINT level before fixing; ask me for any further glitches before your own
sweep. Then sweep every toolbox surface on :8011
(scripts\restart8011.bat; MAD_DEV_NOSW is on, plain reload paints) at 375×812 in light AND
dark: the drawer, all five setting sheets, the top-bar pills and the width ladder, the board
card + editor + pen flow, dice/spinner/deal/vote/deposit through arm→close→reveal, reactions,
and the back gesture on every dismissible layer.

Use the iframe-at-375 + claude-in-chrome recipe (preview-mcp-sse-screenshot memory) for real
paints. Fix under the presentation laws: the re-render flash (decoding=sync; never rebuild
what didn't change) · never relayout under a finger · never destroy a focused node or a
loaded image · [hidden] loses to your own display rule · a wrapper steals adjacency gaps ·
every layer registers for the back gesture (universal-back-stack). Verify each fix at PAINT
level in the user's viewport on a fresh shell (visual-bug-verification memory).

Sync docs/style-guide.html in the same change. python lib/smoketest.py, and
python lib/i18n_audit.py after any copy change. Commit per coherent batch by pathspec, push,
NO deploy. Update docs/ship-plan.html row 2.

Session 3 · console dials

Admin console: toolbox-era toggles and dials (lib/admin.html + lib/run_room.py; follow the
existing console-Settings rows and the cost.py apply_overrides pattern). Read the
prop-master-gate memory and docs/turn-split.html first.
⚠ OWNER RULING (2026-07-29): the console never CHANGES a room's content — no force-close, no
force-reveal, no admin hand on any card, a stuck room included. Observability yes, valves no.

1 · THE TURN SPLIT as a console setting — console default (ships OFF; the ship session flips
it) with the MAD_TURN_SPLIT env as override. It is read per-turn at the drain, so make the
console value LIVE (no restart) — it is also the rollback story — and say so on the setting's
own UI.
2 · MODELS PER CALL: the act call's model (today it silently inherits the room's panel model
— a real cost lever) · the prop master's model (MAD_PROP_GATE_MODEL) + on/off (MAD_PROP_GATE)
· autotitle (hardcoded flash, lib/run_room.py ~2719) — beside the existing FP-arm and
panel-model choices.
3 · COST PER CALL TYPE on admin Status — speech / FP / act / prop master / dispatch.
prop_call and act_call events already carry cost+secs; surface them.
4 · TOOLBOX HEALTH panel on admin Status — the production mirror of the exam's axes: arms by
kind · refusals by reason (the *_bad counters + the outcome channel) · prop master PROP/NONE
ratio + its fail-open count · arms-per-turn (restraint, live). The counters mostly exist;
this is surfacing, not building.
5 · THE TOOL KILL SWITCHES, three layers (owner-specified 2026-07-29):
   a · GLOBAL console toggle「tools off」— no human or AI sees tools: the composer tool
       button hidden, BOTH arm doors refuse, the act call + prop master skipped, the tool
       manual leaves the prompts. Existing open cards stay visible and can still settle —
       never destroy content mid-game; nothing NEW arms.
   b · A console VISIBILITY CHECKBOX for the composer's room-tool button alone — the human
       door can be hidden while AI arming stays (the inverse of a's spirit).
   c · A user-end「tools off」button in the Me sheet's Light/Dark toggle group (meTheme,
       lib/room-ui.html ~5230) — the user's own safety net against tool misfires.
       Semantics (recommended; confirm with the owner at session start): in rooms where they
       are the ONLY human, fully chat-only — no arming, no button, no manual. In multi-human
       rooms it hides THEIR tool door; the room goes chat-only only when EVERY human in it
       has tools off — one member must not kill another's game, and shared cards stay
       visible to all regardless.

Every new setting: smoketest checks (default · override · persistence) · new copy through
t()/i18n (python lib/i18n_audit.py) · BUILD bump per the dev-loop. ⚠ 5a and 5c touch prompt
assembly — the gate must match the surface: run the split regression set
(python exam/run.py s20 s21 s07 s02 s18 s30 s31 --n 4 --split 1 --yes) with tools ON to
prove the switches changed nothing while idle, and one manual probe with each switch OFF.
Commit by pathspec, push, NO deploy. Update docs/ship-plan.html row 3.

Session 4 · the thorough test

Pre-ship test pass on current main — MEASURE, don't fix (file findings; fix only trivial
breaks). Read docs/ship-plan.html and docs/exam.html first.

1 · python lib/smoketest.py · python lib/i18n_audit.py · python lib/run_room.py --selftest.
2 · The exam, full: python exam/run.py --full --n 3 --yes (both paths, props on) plus
    --quick at its N=8 default. Diff every scenario against docs/exam.html's run history —
    any drop from the last full run needs a NAMED cause before we ship.
3 · The ship configuration's price: wall clock + per-turn cost of a split+prop-master turn
    vs un-split, from the event records (prop_call secs/cost · act_call secs · fp secs) —
    and re-check it with session 3's act-model dial at its new default, since the act call
    no longer rides the panel's Pro pricing.
4 · A real phone: scripts\serve-lan-8011.bat, the toolbox surfaces end to end, plus the
    composer/keyboard basics (the virtual-keyboard-war canon points).
5 · python rooms-dev/_probe/parse_health_pull.py --replay on the stored corpus.
6 · The exam's settle paths are session 1b's job, not yours — confirm its re-baseline is the
    number you diff against, not the 2026-07-29 one.

Write the results into docs/exam.html as a「pre-ship run」history row plus a one-paragraph
ship-readiness verdict; update any DoD cell the numbers move. Commit, push, NO deploy.
Update docs/ship-plan.html row 4.

Session 5 · ship

The ship session. Read the sg-deployment and deploy-policy memories FIRST — the traps that
make a GOOD deploy look broken are listed there. Pre-flight: docs/ship-plan.html rows 1–4
are green and docs/exam.html's pre-ship verdict says go.

1 · Flip the turn split default ON via session 3's console setting; the prop master stays on.
2 · The ship ritual: BUILD + sw.js CACHE bump · final python exam/run.py --quick +
    python lib/smoketest.py · commit by pathspec · push.
3 · scripts\deploy-box.ps1 (HEAD check → ff-only pull → systemctl restart mad-room →
    re-probe with retries). Expect the ~90s restart, a benign timeout/status=9/KILL, and a
    000/502 bind window — re-probe 3s apart before alarming.
4 · Post-deploy: a real turn in a real room on chat.xbbapp.com with an instrument armed ·
    admin Status parse health · prop_call events present with sane cost · watch the first
    hour's spend.
5 · Docs in the same push: exam.html history「shipped」row · DoD statuses · the dev-loop
    page if the default changed · docs/ship-plan.html row 5.

Rollback is settings, not a redeploy: split off from the console; MAD_PROP_GATE=0 (restart)
for the prop master.

Session 6 · criterion 2, on humans

Criterion 2 —「every dial human-reachable; the sheet makes sense cold」— is the one DoD
criterion the exam structurally cannot measure: it needs humans. Post-ship, two instruments:

1 · THE FUNNEL: instrument sheet-open → armed vs sheet-open → abandoned, per tool
    (server-side counters beside the existing _tool_used; no new per-user tracking). A sheet
    that opens often and arms rarely does not make sense cold. Surface the funnel on admin
    Status.
2 · THE HALLWAY TEST: prepare a task-card list(「open a roll where each player sees only
    their own」·「collect sealed answers that open together」·「put a score where everyone
    can see it」…)plus a one-page observation sheet. Running 2–3 first-time users through
    it is MINE — prepare the materials, don't simulate the humans.

Fold findings into sheet copy fixes (i18n discipline), update DoD criterion 2 with the
evidence, and docs/ship-plan.html row 6. Commit, push; deploy only if I say so.
The ship plan · cut 2026-07-29 from the owner's ordering · precedent: host-moves-plan.html · scoreboard: the DoD · net: the exam. Statuses move in the same commit as each session's ship.