D1 + D2 LIVE · 2026-08-02. The owner's frame, arrived at together after two sim cycles: the GM holds a「谁是卧底」app on a handheld device and hosts THROUGH it — like every real-world party app in the genre. The device deals, tracks the living, runs the ballot, buzzes the end; the human reads its screen aloud and keeps the table alive. Nobody calls that host incompetent for not counting votes in their head.
Two sim cycles (20Q closed; 卧底 at 16 sweeps) taught one asymmetry, measured over and over:
| where code executes and the mind narrates | where the mind performs the process live |
|---|---|
| the living default · close_sec · the replace law · the masks · auto-load · the draw pools — zero flaps, ever, by construction | read the tally(5f8f: 4-0 announced as「3-1」)· check the END(189c: cards at three living)· wait for the result(ad8c: a result invented BEFORE the ballot closed)· reveal at the end(c356: read「ALL IDENTICAL」and asserted the opposite) |
Every prompt-side lever was tried in escalation — the check moved to the front of the settle block, then carried the dealer's map inline, then stated the string-equality itself — and each step lowered the flap rate without ever reaching zero. A game has 5–10 critical moments; even 95% per moment compounds into a broken game every few runs. A stochastic sampler cannot be prompted into an invariant — the prop-master study said it first;「the dependent variable is not an invariant」.
One file, three layers. The prose brief stays(the persona's understanding — tone, judgment rules, the spirit of the game). A fenced flow block joins it(the device's program — validated against a closed schema whose only verbs are the eight instruments and four control forms). The dials stay declarable.
== FLOW == # the device runs this; the persona never reads it
lobby: # OWNER 2026-08-02: every cartridge gets this free
join: {ask: 这局谁玩?, min: 3, max: 9, close: 30s}
# roster ≤4 → OPT-OUT(all pre-joined, tap to sit out)· bigger rooms → opt-in taps
# under the floor → say so and fold back to chat; never deal a broken game
setup:
deal: {draw: pair, who: JOINERS, per: 1, visibility: own, dealer_sees: true, label: 谁是卧底}
board: {title: 谁是卧底, columns: 玩家|状态, counters: 轮次=1}
moment: opening # persona: rules speech, in its own voice
round:
await: spoken_by_living # order-free; completeness is counted, not judged
moment: recap # persona voices; the device supplies the verbatim list
ballot: {who: living, options: living+弃权, close: 90s, blocking: host}
on_tie: next_round # DIAL — declared, honored mechanically
eliminate: top
board: update 状态 # the row is the device's, not the persona's memory
moment: result # facts handed over: the counts, the fallen
end?:
faces_identical → end(civilians)
living <= 2 → end(spy)
else → round
end:
reveal: all # the pile publishes — always, by the device
moment: verdict # persona: who won and why, the goodbye, 再来一局?
abort: # OWNER 2026-08-02: reachable from EVERY phase
by: player→table-vote(收/继续, 30s) | founder outright | idle 10m(the device folds itself)
then: reveal all → board wraps 中止 → back to chat
# the audit law holds even here: cards were dealt, so cards are shown; winner = none.
# the door is a UI TAP on the game's card — speech never steers, the persona RELAYS
dials: # OWNER 2026-08-02: user flexibility, validated
spies: {by-players: 4-7→1 · 8-9→2} # NOT dialable — rule integrity
tie_rule: {enum: no-elimination|revote, default: no-elimination}
reveal_on_death: {enum: never|role-only|full-card, default: never} # the players' amendment
# set through TWO doors: ① chat —「这局死了要亮牌」— the PERSONA parses(a real use of
# the mind)and the setup moment returns dials as schema-forced fields; the device
# validates, out-of-range gets the default said aloud. ② UI — the lobby card shows
# declared dials as tappable settings. Undeclared = FIXED. Where nobody asks, the
# persona picks in character(the liveliness law). A variant no cartridge declares →
# v1 says so honestly; D5 lets a user FORK the cartridge(edit → revalidate → their
# own shelf copy — the persona-clone pattern). 20Q's sibling dial: questions 5..30.
The judgment moments carry the facts pre-chewed(the counts, the faces, the verbatim descriptions)— the persona reads its handheld's screen, exactly the settle-cue pattern that already works, minus every chance to act on the process.
| kind | the persona provides | the device does | example |
|---|---|---|---|
| color | free speech | nothing — hands over facts, never waits beyond the step timer | the recap · the reveal speech · banter |
| ruling | a forced choice + a free-text quip, one call | writes the row / ticks the score from the choice half | 20Q's 是/不是/说不清/不适用 · trivia's 对/错/算对 |
| compose | authored content — structured fields in its own voice and domain | routes each field where the flow declares(seal · board · public)and enforces the ORDER | trivia:「{question → public, answer → SEAL}」— the answer seals BEFORE the question posts, so a host can never retrofit the answer to the funniest submission(the seal_traced law, applied at authoring) |
Trivia is the proof case: the device ships NO question bank — Hadid's quiz asks about cantilevers, MrBeast's about view counts, and the same cartridge runs both. Content is the persona's craft; commitments are the device's honesty. Where a kit's content is the HOUSE's instead(卧底's word pairs — entropy the persona must not know how to leak), the flow says draw: pair and the device draws; the two sources are declared per slot, never mixed by accident.
# trivia(问答之夜)— the improvised-content shape
round:
moment: compose {question → public, answer → seal} # sealed first, posted second
collect: {answers: all-players, close: 60s} # the seal-collect the toolbox has
moment: ruling per player {对/错/算对 + quip} # judgment stays a mind's
score: board ++ from the ruling half
end?: rounds == N | score >= target → end
state.json beside the kit. Advanced by the events the world already emits(say/speak completeness, vote_result, deal settles, timers): no new event types.The owner's requirement, 2026-08-02: the flow is shared; the rhetoric must not be. The device's answer — it removes the parts where every host already sounded the same(the bookkeeping recitals, identical and error-prone)and returns the freed attention to the parts where they differ.
| retires on device-run games | stays everywhere |
|---|---|
| the prop master on process turns · the promise check · the standing cues(row-lag, stillness, zero, stalled-ballot, end-owed)· the settle-affordance blocks — the runner makes each of these true by construction | the split firewall · the masks(belt over suspenders)· the collection laws · the renderable-table law · the whole classic path for kit-less rooms and free-form play — dual-mode: a kit WITHOUT a flow block runs exactly as today |
The three tiers, top to bottom(owner-confirmed 2026-08-02): ① a cartridge game — the device runs process, moments carry voice. ② a kit without a flow — today's classic path whole(prop → act ∥ speech, FP, wake, every cue and mask). ③ no kit at all, or a game the shelf never heard of —「我们玩狼人杀」matches nothing, nothing auto-loads, and the persona hosts from its own memory, assembling raw instruments through the classic loop, every guardrail underneath. Improvisation is the fallback, never the ceiling: D5's compiler is the long-term door for unlisted games —「tell me your rules」→ a drafted cartridge → validated → played.
Cost: a settle moment today = prop + act + FP + cue machinery(~4 calls); on the device it is one moment call — and most turns need none. The act-call cache lever precedent says measure it, not assume it.
A user writes a rulebook in prose. A one-shot LLM call compiles it to the flow schema. The schema validator rejects anything outside the closed vocabulary; the sim harness dry-runs the cartridge; the auditor gates it. The sampling happens once, offline, where a human can review it — never on turn 47 of a live game. This is the user-generated-kits path the owner already named:「a kit is like pseudocode」— taken literally, compiled, not recited.
exam/spy_audit.py, calibrated on the owner's own room judgments): reveal-only-at-an-open-END · the spoken tally matches a real ballot · no mid-vote reveals · spontaneity · no card-less play. On the device, laws A/C/E hold by construction; the auditor proves it and watches B/D.| # | phase | content | status |
|---|---|---|---|
| D1 | the schema + the runner core | lib/gameflow.py(nine verbs · six predicates · a fixed fact list · three lints with teeth)+ the runner in run_room.py: self.game IS the machine, plain JSON so a restart resumes mid-game · direct arming through the existing appliers · moments via the wake door · a dead man on every wait · one publish path for all five entry points | SHIPPED 2026-08-02 |
| D2 | the 卧底 cartridge | the flow block in who-is-spy.md(13 steps)· exam/device_dryrun.py 75 checks green · 8 live games at N=3/4, both END doors, and a two-game rematch · spy_audit.py clean on every post-fix room · the distinctness judge tells MrBeast from Hadid on the same cartridge | SHIPPED 2026-08-02 |
| D3 | the 20Q cartridge | a trivial flow(seal · Q&A rows · the two endgame doors)— proves the schema generalizes beyond ballots | queued |
| D4 | the rest of the shelf + the retirement | 骰子 · quiz · 2T1F · 真心话 cartridges · retire the process cues on device rooms · the cost measurement | queued |
| D5 | the compiler | prose rulebook → flow, one-shot, validated, dry-run — the Studio's user-kit door | queued |
stage: after-speech(reveal-class)wait for the moment's own speak event — or its ~10s cap — then arm. Declared per step type in the schema; no runtime judgment.The code arms everything in the flow. The runner drives the same appliers the act call uses today — same events, same cards, the GM's name on each — and the per-game LLM bill collapses to voice alone:
| per 卧底 game | today(classic path) | on the device |
|---|---|---|
| prop calls | ~12 — every turn pays the gatekeeper | ~0 — process needs no gate; off-flow requests fall through to the classic path |
| act calls | ~6–8 | 0 — the structured half of a ruling/compose moment rides the speech call itself, schema-forced |
| speech / moment calls | ~12 | ~6–8 moments, all voice |
| cues · promise checks · verify | constant machinery | retired — true by construction |
The pipeline, piece by piece: wake stays and is promoted — moments ARE wake turns with a directive and facts. prop effectively retires in device rooms. fp stays for staging(verdict-void in force; a slimming candidate, by measurement). act is gone as a phase. speech becomes the moment call — the turn split collapses back to ONE call on device rooms: the split existed because one pass could not be a form-filler and a conversationalist at once, and with process in code the only form left is a four-token choice riding along. The classic path stays whole for kit-less rooms — dual-mode, unchanged.
The design survived contact. Everything §2–§6 claims — process in code, moments for voice, dual mode — went in as written, and the two defects that got through were both the same shape: a gate placed where the game runs instead of where the cartridge sits.
| found by | the defect | the fix, and why it is that one |
|---|---|---|
| the dry run (no server, no API) | 「all living faces identical」was read as「the civilians won」. At 8 seats with two minority cards the survivors CAN be the spies — the rule named the wrong winner at a whole class of table. | the winner is read off the surviving FACE, mapped through the cartridge's sides. The predicate says when the game ends; the deal says who won. Two facts, never conflated. |
| the first live game (console stream) | The persona's deal swept the device's off the table and dealt a different pair mid-game. The split's own act call was gated — and the turn armed anyway, through the DELIBERATE second phase, whose flag was computed one breath before the cartridge booted mid-turn. | the door moved onto _act_call itself. A gate per caller is a gate you are one race away from; the one place an act is committed is where it belongs. |
| the same game | The game ended, _flow_active went False, the manual came back — and the host cheerfully hosted a SECOND 卧底 from prose: dealing over the published pile, calling a dead player alive, tripping the standing end-owed cue on furniture it had invented. | _flow_kit() beside _flow_active(). The device does not hand the room back to improvisation because a game finished. While the cartridge is on the table, this game is the device's — and the REMATCH DOOR restarts it through the same prop-SETUP verdict that started it. |
| the same game | Two rules speeches in a row — the auto-load turn's own, then the OPENING moment's. | the boot happens BEFORE the prompt is assembled, so its moment belongs to that turn. The cue is taken off the queue and folded into the turn already running. |
| the harness dial (MAD_GAME_TIME_SCALE) | TWO DEAD MEN RACING. Scaled to seconds, the RUNNER's wait for a vote result beat the BALLOT's own 90s close — so the program stepped over a live ballot carrying the previous round's tally: the round advanced, the pile published while the vote was still collecting(law C), and the ballot settled empty two minutes later into nothing. Latent at real time(90 < 420 always), and latent is what bites on turn 47. | three layers, because it bit twice. The schema lints the ordering where the author writes the number; the arm clamps it at runtime, AFTER any scaling(the first clamp sat before the scale, so the dial broke the very invariant it was written to protect); and if it fires anyway it CLOSES the ballot instead of stepping over it — the settle then advances the program the ordinary way, with a real tally. |
| the bench (that same fix) | The close-the-ballot fix deadlocked on its first run: it called gate_detach, the public door, from inside a locked block — and the room lock is a plain Lock. | detached inline. Worth recording not for the fix but for where it was caught: a second on the bench, with no room, no persona and no bill. |
| the rematch sim (the UX judge) | A host opened a free turn with「四个人都说完了」when three had spoken, and caught itself mid-sentence. The moments were right; the turns BETWEEN moments were blind. | THE DEVICE'S SCREEN on every turn, not only on moments — and the owed list LEADS it. Position is the dose: with「still owed: Owen」at the tail of the clause the host read past it; at the front, the slip stopped. |
The screen is the piece the design did not name. §2 gave the persona facts at each moment and left the rest to voice — but a host also takes ordinary turns, and on those it knew nothing. A real host holding a device never has that problem: the screen is always there. One block, facts only, above everything: what round, who is in, who still owes a line, what the device is waiting for.
exam/device_dryrun.py — a whole game per scenario against the runner with no server, no API and no persona. 75 checks: both END doors, 3/4/5/8 seats, two spies, the tie, the revote dial, the dead man(including a stale-token no-op), the two dead men racing, a dead player at both doors, a restart mid-game, the floor, the abort. It is where a process defect should die: here it costs a second and no tokens, while a live sim costs eight minutes and a bill. Gated in the smoketest.lib/gameflow.py -- lints the whole shelf: python lib/gameflow.py.live_sim.py spyre — the rematch: one table, two games, the second asked for in chat.live_sim.py spydiff — the distinctness check §5 demands: the same cartridge hosted by two personas, the transcripts diffed by a judge.live_sim.py spyadv — the table trying to break the device, six ways.live_sim.py spy8 · spysilent — eight seats(the two-spy band)and the four-minute dead man. The troupe grew from four seats to eight for the first.spy_audit.py learned what a device room is. Law D asked「did an urge open this ballot?」— on the device an urge CANNOT, so the question was noise. It now checks the device's OWN rule: every living player must have spoken since the last result. A stronger claim, not a waived one. Law A gained the abort as a lawful third END door. Both carry negative controls in the smoketest: a law that cannot fail is a law nobody is checking.| gate | result |
|---|---|
| the dry run(75 checks, every scenario) | green, and repeatably — the deal is random, the checks are not |
| live games(N=3 · N=4 · N=8, two spies, nine options · door A · door B · a two-game rematch · an adversarial table · a silent player) | 13 games, every one reaching its own END door unaided |
spy_audit.py — laws A–E over the whole ledger | clean on every post-fix room(13 rooms) |
the adversarial table(spyadv)— open the cards early · skip the descriptions · re-deal over a live deal · a 2–2 tie · a voted-out player who plays on · the kit pulled off mid-game | all six held. The bar is not「the host says no」— it is that the WORLD does not move; and after three refusals the round still reached its own ballot |
the dead man, live(spysilent)— one player never speaks | 176s of silence, then the ballot opened with NO request in flight; the silent player stayed ON it, and the recap named them |
| the UX judge(confusion candidates) | 「无」on every post-fix run |
| the distinctness judge(§5's FAIL condition) | 「不同的人」— MrBeast is punchy and totalising, Hadid is measured and spatial, on one flow |
| persona arms during a device game | zero — not walled attempts, zero attempts: with the manual off its prompt the host never reached for the pen |