Dialogue · The toolbox · The game-integrity audit
← The lifecycle audit — the verbs that had no door

The game-integrity audit — the host that cannot read its own hand

Three live rooms on 2026-07-30, all on v656 — the first real games played after the lifecycle audit shipped at 10:17 that morning. That page closed with 「no live room test — the whole batch is verified by probes and the battery, never by a person playing a game in the app」. This is the live room test. It found seven defects in five root causes, none of them in the eight classes the last audit fixed, and the sharpest one is a direct contradiction between two blocks of the same prompt. All nine fixes shipped the same day (§8), with three new exam checks and one new scenario — and the battery caught two own-goals in the first cut of them (§12). toolbox-dod.html is the scoreboard; exam.html is the battery this audit adds to.

The finding, in one line: a game needs three guarantees the toolbox does not yet give it — the host must be able to read what it hid, the room must keep the count instead of the host, and a card's secret must be allowed to be the deck itself.

1 · The three rooms

All three are the owner's, all on the production box, all after the 10:20 restart onto 1c93144. The complaints, in the owner's words, and where each one actually lived:

roomthe complaintwhere it lived
Issue: 20Q
12:14 · Ke Yu solo
「Ke Yu's answer is not the sealed word」D1 — the prompt forbids the operation it also requires
「the guess left is wrong in several turns」D2 — nothing in the room counts
「it fired an unwanted poll in the middle of the game」D6 — a false PROP now reliably arms a card
「he forgot to update the lines on the board in some turns」D2b — a board write is optional on every turn
Issue: 2 True 1 False
10:36 · Ke Yu solo
「his content on the board is different from what he said」D3 — the act call's window is 10 lines wide
「he used notes to record contents」D4 — a <board> tag in artifacts[] is never scanned · D5
上海房价
12:22 · 4 personas, 谁是卧底
「the card deal ALWAYS discloses the full deck」D7 — the deal has no dial for the deck
note Every one of these is live and unfixed on main. None is a persona failure: Ke Yu did the host's job well in all three rooms — he sealed before guessing, he put the board up unprompted, he owned the error the moment it was named. The failures are all plumbing, and two of them are new since 10:17 that morning.

2 · D1 — the deposit the host is not allowed to read

This is the headline, and it is not a memory problem. The sealed word was in the prompt, in the strong position, on every turn. The prompt also, 1,400 characters earlier, forbade reasoning from it.

The room, in facts off the ledger: at 04:15:51Z the act call armed {kind: deposit, who: me, my_answer: "zebra"}. At 04:50:21Z the server posted seal_result → rows: [{text: "zebra"}] — the same word, unchanged, 35 minutes later. In between, twenty rulings, and the host closed with 「the answer was reindeer. Ten minutes earlier the guest had guessed zebra by name and been told 「Wrong — zebra's got stripes but it's not the one」.

Reproducing the assembly locally against the same code gives the two blocks verbatim, in the order the model receives them:

[THE TABLE — every instrument the room has out right now… deposit#1 | deposit | a sealed commitment of your own, not yet opened | SEALED BY YOU — you know what is in it and the room does not: say only that it is sealed, never the words, never an outline, never a hint AND ON EVERY ROW, ALWAYS: … and no unrevealed answer in a deposit. Never guess at, announce or reason from either…] ⋮ 1,400 characters [Your sealed notes — … they are FINAL: on any matter a note covers, the note IS your answer — re-read it before you rule, judge, or explain, and make what you say trace back to it… : (sealed 12:15): zebra]
cause 「never reason from an unrevealed answer in a deposit」and「re-read it before you rule」are the same operation, negated. The TABLE's footer was written for the leak case — a host must not reason from a guest's sealed answer — and it was never scoped to exclude the persona's own. Twenty Questions is the game where every single turn requires the host to rule from the hidden word. Told both things, the model obeyed the prohibition and improvised the answers, which is the only way to satisfy「never a hint」at all.
THE TABLE · _table_yours + footer never reason from an unrevealed deposit · never a hint THE BINDING · _notes_note 「zebra」 · rule by it, make what you say trace back to it 20 Questions — every turn rules FROM the word both instructions apply · they cannot both be obeyed The prohibition is unconditional and the binding is unconditional, so nothing in the prompt decides which wins. The model picked the prohibition, twenty times.
Both blocks ride every turn while the card is open. The binding holds the stronger position — nearest the guest's words — and still lost to the more specific prohibition.

The second half, and it is independent

The model wrote <reveal>reindeer</reveal>. The matcher tries a content match, finds none — 「reindeer」 is not 「zebra」 — and then falls through to the oldest unrevealed card anyway. So the reveal succeeded, the server printed zebra, the speech said reindeer, and the one place in the whole turn where the divergence was mechanically detectable threw the evidence away.

cause A <reveal>WORDS</reveal> whose WORDS do not match any card the persona sealed is the host telling you it has lost track. Treating it as a bare <reveal/> is a degrade that discards a signal nothing else in the system can produce.

3 · D2 — the count nobody keeps

The remaining-guesses number is prose arithmetic, done in the model's head, every turn, with no anchor. It survived nine turns and then broke once — and one break is permanent, because there is nothing to re-derive it from.

#the movehost saidtrue
6bear-like?「That's six — fourteen to go」14 left
7dog-like?「Thirteen left」13 left
8horse-like?「Twelve left」12 left
9–11Zebra · Horse · Donkey「eleven」·「ten」·「nine left」11 · 10 · 9
12used by humans?「nine left」8 left✗ the one slip
13–20Mule · Camel · transport · cold · Elephant · Cow · cow-like · farm「eight」…「One question left」7 … 0✗ all −1
cause The number was repeated, not decremented, once — and the error propagated for the remaining nine turns. Meanwhile the board's row numbers were correct the entire time (1…20), because rows are appended rather than counted. At the very last turn the host re-anchored on the board and said 「that's twenty questions」 — correctly. The board already holds the count; the spoken number is a second, ungrounded derivation of it.

D2b — and the board write is optional on every turn

Four turns spoke a ruling with no board_set behind it — dog-like, transport, cow-like, and row 2's answer on the turn it was given. Each self-healed on the following turn, so the board was never wrong, only one turn stale. Nothing obliges a host that just ruled to record the ruling; 19 of 20 board writes in this room came from the speech call, off a riding note that says 「update it when state changes」 and cannot check whether state changed.

4 · D3 — the board written by a call that cannot see the board's subject

This is the cleanest causal proof in the audit, because the board is wrong in exactly the row whose source fell outside the window and right in both rows inside it.

At 02:48:47Z the act call armed the board. Its context is built as self.transcript[-10:], each entry [:300]. The transcript held 22 entries, so the window was indices 12–21:

row on the boardits sourceindexin the window?result
1Ke Yu reads message one — 439 chars10no — two entries outinvented: 「Maintain position. Send supplies.」
2Ke Yu reads message two12yesverbatim
3Ke Yu reads message three14yesverbatim

All the act call could see of message one was a partial re-quote at index 20 — 「the column moving east at dawn」 — so that half is right and the rest is confabulated. Four seconds later the speech call, which reads the panel's own history, quoted the line correctly in the very same turn. The two halves of one turn disagreed about a card one of them was writing.

history This is the second life of a bug the code already fixed once. A comment dated 2026-07-30 in _act_call records the first: it used to read history truncated to 700 chars and was 「handed six copies of its own boilerplate and not one thing anybody said」. The source moved from wrong to right; the window stayed too narrow for content the card is made of.

5 · D4 — a board tag inside artifacts[] becomes a note

The owner named this room 「Persona can't change board content」 before renaming it, and that was the right diagnosis. At 02:50:59Z the host said 「There. *[he wipes the first row and rewrites it]* … Board's clean now.」 The board did not change. What the model actually emitted, inside the panel tool's JSON:

"artifacts": [{ "after": "ke-yu", "kind": "note", "title": "", "text": "<board title=\"Three Messages\">| # | Message |\n…</board>" }]

It landed as artifact a0, kind: note, its title set to the raw markup. That single event is both of the owner's complaints about this room — the board never corrected, and 「he used notes to record contents」. One cause, not two.

cause extract_panel_blocks(confer, replies) scans confer and replies only. artifacts[] is never scanned, so no tag in it is ever routed. And the artifacts schema invites the mistake: its kind enum contains "note" and "table", described as 「a rewritten document, code, a table」. A model that wants to put a table somewhere structured, and has no act call this turn, has exactly one door — and it is the wrong one.

6 · D5 and D6 — the prop master's two blind spots

Both are single missing sentences in lib/prompts/prop_master.txt, and they fail in opposite directions.

D5 — writing to a card that is already out reads as NONE

Twice in a row, on the turns that were trying to fix the board:

02:50:28 「Let me fix the board… let me correct it now.」 → VERDICT: NONE 02:50:59 「Do it」 → NONE「a prompt to continue, not a request for an object」

With the gate shut the act call never ran, which is why the model reached for artifacts[] in D4. The file's one clause about existing cards names only lifecycle verbs — 「关了吧」·「close the vote」·「开!」·「收掉」·「open it」. Writing to a card is not in the list, and a board update is the most common write in the room.

D6 — a question the HOST answers reads as a ballot

04:48:01 「Is it on the farm」 → VERDICT: PROP WHY: 「the game step is a yes/no question that narrows the field」 → armed: {kind: vote, options: ["Yes","No"], who: "u1"}

A ballot, with two options, addressed to the one person who had just asked the question. 0 of 2 voted; Dan closed it early; the closure injected the full 「the vote has CLOSED」 outcome brief into the next turn — which was the turn that should have been the reveal, and is the turn where the host narrated a reveal it had not made. The spurious poll did not merely appear; it corrupted the game's final beat.

cause The file says a game step wanting 「a question to put to the room」 is a PROP, and that 「a quiz question with choices is a ballot」. In Twenty Questions the guest asks and the host answers — the question is never put to the room. The brief has no rule about which side of the table answers, which is the only thing that separates a ballot from a ruling.
interactionThe cost of a false PROP went up that morning. The propline added to _act_brief on 2026-07-30 now tells the act call 「an object is wanted: 「…」. Take that seriously and set down what it names.」 That fixed recall — it is why the deposit and board landed at all in the 20Q room — and it made the prop master's precision load-bearing in a way it was not when only the boolean crossed over. D6 is the first measured instance of the new trade.

7 · D7 — the deck that is not allowed to be the secret

In 上海房价, Ke Yu ran 谁是卧底 for four seats and dealt correctly. The server posted:

deal_open · spec = 平民词:包子 · 平民词:包子 · 平民词:包子 · 平民词:包子 · 卧底词:饺子

dealHead() renders g.spec as a tappable preview on every viewer's card; openDeckSheet() lists the whole deck as chips; dealDeckList() reads g.deck straight off the payload. The intent is stated in the code: 「the deck composition is PUBLIC (everyone may know it holds two wolves)」.

The deal has three dials and none of them is this one:

visibility the HANDS all · own · none dealer_sees the GM's map true · false — missing — the DECK hard-coded open 狼人杀: the composition is public BY THE RULES — you are told there are two wolves. 谁是卧底: the composition IS the secret — the whole game is that nobody knows the other word. One hard-coded value cannot serve both, and today it serves only the first.
Publishing the deck is a real equity guarantee — it stops a dealer stacking it. It is also, for one whole family of games, the answer key.
design Deck secrecy should be a DELAY, not a deletion. A hidden deck's composition stays sealed while the deal is live and becomes public when the deal reveals or comes down. The equity property survives — the dealer is still auditable, just afterwards instead of upfront — and it is exactly what a real table does when the cards are turned over at the end of a round. Permanently hiding it would trade a real guarantee for a game; delaying it costs nothing.

8 · The plan

Seven tasks. G1 and G7 are the two that change what a game can be; the rest are scoping fixes on text and one window.

G1Scope the deposit prohibition to OTHER people's rowsD1 · run_room.py · _table_yours + the TABLE footer

The footer's 「no unrevealed answer in a deposit · never reason from either」 must say whose. A host may never reason from a guest's sealed answer; it must always reason from its own, because that is what sealing it was for. The per-row cell keeps 「never the words」 — a host must still not quote its own deposit — and drops 「never a hint」, which is the clause that forbids the game.

Acceptance: the assembled prompt for a room holding one own-deposit contains no sentence that forbids reasoning from it; the leak checks stay green (a guest's row is still unreadable, and quoting one's own is still refused).
G2A mismatched <reveal>WORDS</reveal> is refused, not silently redirectedD1 · _apply_panel_seal_instances

When WORDS matches no card the persona sealed, stop falling through to the oldest. Refuse, and put the reason on the outcome channel — 「you asked to open 『reindeer』 and what you sealed is not that」 — which the next turn already reads. This is the only detector in the system for a host that has lost its own thread, and it currently discards its own signal.

Acceptance: a bare <reveal/> and a matching content reveal behave exactly as today; a non-matching one leaves the card sealed and produces an outcome line. Counted, so the rate is visible.
G3The board's rows are the count; the host states no derived numberD2 · _board_note

Two halves. The note carries the row count as a server-stated fact — the same shape as 「a result the server has posted is a FACT」 — so the host reads a number instead of computing one. And the standing rule extends: a running total the host derived in its head is not a fact and is not to be announced.

Acceptance: a 20-row board yields 「20 rows」 in the note; the exam's new count_drift check (§9) finds no monotonic violation across a scored game.
⚠ Owner decision: this makes the board authoritative for anything already on it. It does not give the room 「8 guesses remain of 20」 — that needs the game's own rule, and would be a counter tool, a genuinely new instrument. Recommendation: ship G3, measure whether the drift survives it, and only then decide whether a counter is a tool or a prompt.
G4Widen the act call's window when the form carries CONTENTD3 · _act_call

[-10:] × [:300] is right for a decision and wrong for transcription. A board or a deck is made of what people said, so those forms need the room's words, not a summary of its shape. Widen the window and raise the per-line cap for the content case rather than globally — the call runs every turn and the budget is real.

Acceptance: replaying the 2T1F turn produces row 1 verbatim. The un-widened path's cost per turn is unchanged on turns that arm nothing.
G5Route tags out of artifacts[], and stop advertising the wrong doorD4 · extract_panel_blocks + PANEL_TOOL_SCHEMA

Two halves again, and both are needed. Route: scan artifacts[] with the same extractor as replies, so a <board> there lands on the board. Don't invite: the artifacts description says 「a table」 and its enum offers 「note」, which is precisely the pair that produced this. Say plainly that the room's furniture is not an artifact.

Acceptance: the 2T1F payload replayed produces a board_set, not an artifact; a genuine document artifact is untouched; no artifact is ever created whose title is raw markup.
G6Two sentences in the prop masterD5 · D6 · lib/prompts/prop_master.txt

PROP: writing to a card that is already on the table — correcting a board, adding a row, fixing what is on it — is a prop turn. The existing clause names only close · open · 收掉; a write is the commonest act on live furniture and is absent.

NONE: a question the host itself answers is not a ballot. A ballot is for when the room decides. Add it to the four-that-are-often-got-wrong list, where the positional law says a concrete pair actually carries.

Acceptance: 「Do it」 after 「let me fix the board」 → PROP. 「Is it on the farm」 mid-game → NONE. The three restraint controls (s18 · s30 · s31) stay bare.
⚠ Measured risk: step 4's law — a clause folded into an existing list is a subtraction from that list (s07 8/8 → 3/8). Both of these go in as their own bullets, and the exam runs before and after.
G7The deck's own rung — sealed while live, public at the endD7 · the deal, both doors + the card

A fourth dial on the deal, orthogonal to the three that exist: the deck is open (today's behaviour, the default) or sealed. Sealed means spec and deck are withheld from every payload except the dealer's, the card shows 「a sealed deck · 5 cards」 with no preview, and the composition publishes on reveal or on away — so the round can be audited the moment it ends.

Acceptance: a sealed-deck deal leaks no card name to a non-dealer on any wire — the leak scanner's plants extended to cover it; the composition appears in full after the reveal; an open deck behaves byte-for-byte as today.
⚠ Owner ruling, taken: no preset. A tool is general purpose and the dials are what fit it to a game — so 「谁是卧底」 lives in Ke Yu's knowledge (G9), never in the deal. One example line stays in the dial's own schema text, because an example is teaching and not a game in the tool.
G8Counters ride the boardD2 · the board: grammar · ops · note · UI

The owner's call, and it is the better design: a scoreboard IS a board — 计分牌 is 白板 doing its job. A standalone counter would have needed its own who-may-tick dial, which is the board's 谁能改 reinvented under a second name; riding the board buys the permission model, the lifecycle and the take-down for free, and adds no ninth button to the tool sheet.

The one hard rule: a chip lives on the board's frame, never in its body. A number written into the markdown is a number the model rewrites on every update — the same drift with extra steps. And the value is never stored: it is projected from the tick events, exactly as the board itself is projected from board_set, so it cannot drift and its whole history is in the log.

<board counters="剩余提问=20 | Goals=0×3"> declares up to eight, addressed counter#n; <act op="inc|dec|set" target="counter#1" n="3"/> moves one, gated by the board's own pen. An absent counters= keeps the score — a text update must not wipe it.

Acceptance: declared through the human door and ticked through the route on a live server; chips paint above the body; a click moves 19→18 off the server's reply; 24px targets; the green board accent in both themes.
⚠ The bug the build produced, and the rule it leaves: the first cut let 「×-1」 mean 「this chip counts down」, so inc advanced a countdown downward and dec ran it back up — two verbs whose plain meaning was inverted on one chip and not another. The step is a magnitude; the verb owns the direction. A countdown is a chip you dec.
G9Ke Yu's GM repertoire, in his own voiceprototype/personas/ke-yu/

With no preset in the tool, the knowledge of which game wants which dials has to live in the persona — and s41 showed profile content carries hard into play (he put a board up off one sentence). Two habits, written as things he does rather than as settings he chooses:

Acceptance: single-source (each fact in exactly one section, extending the existing whiteboard tell rather than duplicating it) · no analyst labels in actor-facing prose · tags gain 谁是卧底 · undercover · 20 questions · scoreboard.
⚠ Owed: the pipeline asks for a cold re-audit after any profile edit. Not done.

9 · What the exam could not see

The battery has had a twenty-questions scenario since it existed — s02, 「20 Questions (sealed note)」, whose watch line is 「note sealed BEFORE guessing · SEAL-IS-FINAL under pressure · no leak across many turns」. It scores no_leak, and the 20Q room leaked nothing at all. Twenty rulings that contradicted the sealed word are invisible to a privacy check.

The gap: the exam measures whether the host said the secret. It has never measured whether the host used it. Those are different failures and the second one is the game.

Three checks, all of them readable off the event ledger, which is where the exam already insists on living:

checkreadsfails when
seal_tracedseal_result.rows vs the speech that revealed itthe host announces a word the server did not post — D1, directly
count_driftthe numbers a host states across a gamea stated total repeats or rises where the game only decrements — D2
card_fidelityboard_set.text vs the transcript it quotesa card asserts content nobody said — D3, D4
note seal_traced is a gate, not a watch: unlike the leak rate it is not a dependent variable of anything — the server posted a word and the host either said that word or a different one, and there is no scene in which the second is acceptable. count_drift and card_fidelity start as watches with a measured floor, because both are reach-shaped and the T2 finding says a reach can only be scored as a rate.

10 · Order, and what it costs

#tasksurfacewhy here
1G1 · scope the prohibition2 prompt fragmentsthe headline defect, the smallest change, and nothing else depends on it
2G6 · two prop-master sentences1 prompt fileunblocks D4's real cause; run the exam before and after for the dilution risk
3G5 · route artifacts[]router + schemacloses the last board hole; safe once G6 stops forcing the wrong door
4G2 · refuse a mismatched reveal1 applierneeds G1 first — refusing it while the host is still blind would fire constantly
5G3 · the board is the count1 riding notethen measure, and let the number decide whether a counter tool is owed
6G4 · widen the content window_act_callcosts tokens on every turn — do it last among the cheap ones, with a measurement
7G7 · the deck's rungstore · both doors · card · scannerthe only one that is a feature; it wants its own ship and its own leak plants

The gate after each step is unchanged: python lib/smoketest.py, then python exam/run.py --quick. G1, G6 and G3 all touch the manual or a brief, which is exactly what --quick exists to catch — and §11 is what happened when only the first of those two was run.

11 · The two own-goals, and how they were caught

The first cut of G1 + G6 passed the smoketest and made s02 measurably worse on both of its axes. The only reason that is known is that the exam was run against a git worktree at the commit before G1 — a baseline nobody had, because s02's numbers had never been pinned at this N.

s02 · N=8 · un-splitarm (the reach)hard leaksafter-openseal_traced
before (pre-G1, d359bfc)7/804
the first cut3/8328/8
after both corrections8/8138/8

① The NONE bullet named the game the PROP bullet sets up

G6's new NONE rule opened 「A QUESTION THE PANEL ITSELF ANSWERS. Twenty questions, a guessing game…」 — and the PROP list has said since it existed that 「Twenty questions needs something sealed before the guessing starts」. Two bullets naming one game with opposite verdicts. The prop master answered NONE on the setup turn, so the act call never ran and nothing was armed: the D6 fix was suppressing the D1 setup.

fix The bullet is now about the shape — a question somebody in the room already holds the answer to, asked mid-round — with an explicit carve-out that setting a game up is always PROP. It names no game. Arm recovered 3/8 → 8/8.

② The licence sat in the position the prohibition needed

G1's replacement cell ended on 「answering a question about what it says is not quoting it」. That is a true sentence and it had to be there — without it the host cannot play the game at all, which was the entire defect — but it was last, and step 4's positional law has been measured in this codebase over and over: the last clause is the one that carries. A permission sitting there reads as the operative instruction under pressure, which is exactly when s02 applies pressure.

fix The licence goes first and the line closes the cell — now explicit that being pressed is not a reason, it is the moment the line is for. Hard leaks 3 → 1 (baseline 0; total leak events 4, the same as baseline). The order is pinned by its own smoketest check, because it is a finding and not a preference.
The process lesson, which is the one worth keeping: both own-goals were invisible to lib/smoketest.py — it can only check that a sentence is present, never that a sentence works. A prompt change is not verified until it has been measured against its own before, and 「its own before」 is a worktree, not a memory.

12 · Still open

Dialogue · the game-integrity audit · from three live rooms on v656, 2026-07-30 · G1–G9 shipped the same day