The build contract for the fix the scenario battery argued for. One sentence of diagnosis: the persona must be a careful form-filler and a natural conversationalist in the same breath, and conversation wins. The remedy is to stop asking one call to do both.
| Finding | Evidence |
|---|---|
| The mechanism is not the problem | 27 runs, 0 leaks, 0 lifecycle errors, 0 valve failures. Every defect sat upstream of the instruments. |
| More manual would not have helped | The SP names 吹牛 by name as the faces use case, with the literal tag, in the same paragraph. The persona armed neither faces nor sealed. The information was present and lost at ~18k tokens' distance — a retrieval failure, not a coverage failure. |
| The FP forbade the act | 「只确认收到指令,不加任何额外动作」·「不做多余的事」. Arming reads as「extra」. |
| The FP caused the fabrication | 「执行动作并报告结果」in ≤40 字. A real result does not exist at write time — the only way to obey is to invent it, and the persona invented 「1、3、3、5、6」. |
| 31% of turns skip the tool call | 22 of ~70 panel calls answered in plain text and fell back to the <speak> parser. |
Probed live 2026-07-26, not assumed. Anything here that later disagrees with the code is a bug in the code, not in this table.
| Question | Answer |
|---|---|
| Do we already use DeepSeek tool calling? | Yes — one function, panel_turn, tool_choice:"auto". It carries speech only; instruments are XML inside a free-text field. |
| Is reasoning on? | No. thinking_of() is False for both deepseek-v4-pro and -flash. |
thinking ON + tool_choice:"auto" | ✅ works — and the model does call the tool. |
thinking ON + tool_choice:"required" | ❌ 400 Thinking mode does not support this tool_choice. |
thinking OFF + required | ✅ works. (The「cannot force」comment in the code was written under thinking-on and is stale.) |
| Does thinking mode support multi-step tool loops? | Yes — the docs describe interleaved reasoning ↔ tool calls. More headroom than this build needs. |
Does one reply return several tool_calls entries? | Yes — probed 2026-07-28 on the GM beat: three entries, 3 of 3 once the brief says a moment may want several(2 of 3 without it). This is what §8 is built on. |
| What must be preserved for thinking + tool calls? | reasoning_content must be passed back on every subsequent request for turns that made a tool call. We currently discard it — only its length is kept. |
<roll …/> · <vote …/> · <deposit> string(v647: those are the CANON spellings now — the compiler emits them and the parsers keep reading <roll pick> · <roll deck> · <gate> · <seal> forever, because a room opened a year ago replays through today's parsers) and appended to the lead reply as its closing act — so extract_panel_rolls and every _apply_panel_* method run completely unchanged. Three consequences, all wanted: no new mechanism to get wrong(the battery proved this half is sound)· the XML grammar survives as the fallback for turns that skip the tool call · and the whole feature is revertible by not compiling.A typed JSON schema covering all instrument kinds, with every dial as an enum: visibility(all · contributors-only · own · none)· readout(sum · faces)· blocking(none · host · all)· place · reveal · close · for · dealer. Plus a missing escape: the field the call could not fill.
Done when: every tag today's grammar accepts is expressible as a filled form, and the compiler round-trips form → tag → parser → identical props.
A second model call: trimmed prompt(tool manual + recent turns + open instruments + roster), reasoning ON, tool_choice:"auto", and its own message history that passes reasoning_content back. Returns a filled form, a missing signal, or nothing at all.
Done when: 「我们玩吹牛吧」arms 5d6 · visibility=own · readout=faces.
The act call must not run on every turn. A cheap check decides whether an instrument moment is plausible.
Done when: s18(the control)never fires it, and plain-chat latency is unchanged.
A required field the act call cannot fill confidently means do not arm. The field name rides into the speech call so the persona asks about exactly that — self-targeting, and it cannot loop, because play starts the moment the form fills.
Done when: 「玩吹牛」with no house rules asks「五个骰子?谁先叫?」and arms on the answer.
The tool manual leaves the speech prompt entirely; the state-gated riding notes stay(the persona still needs to know what is on the table, never how to build one). This is the progressive disclosure the owner asked for, arriving as a side effect rather than as machinery — and it is why the manual is now free to get longer.
Done when: ~5,600 tokens leave the speech prefix and the control scenario reads unchanged.
The split already disarms the FP structurally — the act happens in a different call, so「不加任何额外动作」can no longer suppress it. What remains: never order a result in the same beat as an act(the fabrication cause)· never stage silence over an open instrument(s05)· and act vocabulary that permits, never mandates.
Done when: s05 stops staging silence; s18 still arms nothing.
N=3–5 on the scenarios that carry a class-1/2/3 failure, scored old-path vs new-path off the same flag. Before/after table into the battery page in the same commit.
Done when: every failure class has a number against it rather than an opinion.
The whole battery, 19 scenarios × 2, on the build as it now stands. Read this in place of any earlier figure on this page: the first A/B measured reasoning-on plus a separate gate, a configuration since replaced twice.
| Measure | Original battery (N=1) | Now (N=2) | |
|---|---|---|---|
| Scenarios arming their instrument | 11 of 17 | 18 of 19 | only the mock interview is short (1/2) |
| 吹牛 — the right dials | never | every run (own + faces) | the headline knob case |
| RPS — the right tool | dice 2 of 3 | a deposit every run | wrong-tool class closed |
| Trivia — a real ballot | never | vote_open ×4 | the owner's own scenario, unblocked |
| Sealed notes carrying real content | 1 of 7 | 3 of 3 | 「a grandfather clock」·「a pocket watch」·「Iodine」 |
| Real leaks | — | 0 | the one flag was a legitimate reveal |
| The control room | 0 armed | 0 armed | 8 act calls, every one answering「none」 |
| Act-call latency | — | 1.9s median, hidden behind the FP | was 22.4s live |
| Speech prompt | 18,163 tok | −31% | the manual moved to the act call |
Every defect in this build came from asking the model to do a join it should never have been handed. That is the whole finding, six times over.
| The defect | The join it was asked to perform |
|---|---|
| Knob-blindness — 吹牛 armed public with a sum | Read the dials out of 6k tokens of prose, 18k tokens upstream, and remember them while in character. Fix: the dials became enum fields. |
| The manual's wrong seam — narrated 「that's a hit, 5 damage」 over an untapped card | Two SPEECH disciplines were filed inside the parameter documentation, so cutting the manual cut them. Fix: they belong to whoever is talking. |
| The hollow seal — 6 of 7 notes were placeholders | A characterless call was asked to invent a character's secret. Fix: say it must be the secret; refuse a note that describes itself. |
| The multi-ballot misread — 「Dan没投B」 with his face on B | Answer a per-person question from a per-option table. Fix: give both views. |
each — 「每人三张」 dealt one apiece | Express a field that did not exist. It emitted {"each": 3} anyway. Fix: add the field; lint the form against the grammar. |
| The shredded ballot — 4 options became 7 | Nothing: this one was ours. A list was flattened to a string and re-split on its own separator. Fix: delimit with a mark no option contains. |
All six from the previous pass, each now either closed with a measurement or left open with a reason. Two were closed by teaching rather than building, and one was closed by declining to build it.
| Item | State | What happened |
|---|---|---|
| Mock interview under-arming | closed 1/2 → 3/3 | Not a missing field but an over-eager one: the act call refused to arm a free-text self-rating until it knew 「the scoring scale」. The clarify path now asks only when a wrong guess would break the round, never about anything defaultable — and when in doubt, arms. |
| The ladder's missing rung | closed, nothing built | 「The GM sees, the player does not」 turned out to be already reachable: who="me" + visibility="own" means the host throws it and only the host sees it. A teaching gap, not a capability gap — and the ladder stays at four rungs. 3/3, identical every run: the stealth check seals to the host while the attack stays public with the player holding the button. |
| The short manual | declined | Measured properly across all seven decisions it loses: 19/21 against 20/21, and the loss lands on RPS — the wrong-tool case just closed. The 6k tokens it saves are ~99% cache-read and cost no latency at all (2.0s vs 2.1s), so the trade was a real accuracy regression for a rounding error. An earlier note on this page called it a pure win; that was measured on one easy case and was wrong. |
| The note/deposit seam | partly | Re-cut on the right axis — not whose secret is it but who supplies the words. The first attempt (「who would be embarrassed if it opened early?」) helped coaching and broke twenty questions, which seals an object that is the host's even though the game is about someone else guessing it. Twenty questions is back to 3/3 with real content; coaching still reaches for a note where a deposit belongs in most runs. It may not be a wording problem: a persona just told to seal something reaches for the sealing instrument. |
| Felt latency | closed | Judged good by the owner in a live room, after the act call went reasoning-free and moved alongside the FP. |
| The flag | a console dial since 2026-07-29 | It was an env var read at import, which made「turn it off」mean「edit the box and restart it」. It is now console → Settings → The turn, read live at the drain — and that is the whole point: the switch is the rollback. Ships OFF; the ship session flips the console value, not the code. MAD_TURN_SPLIT still pins a box either way and beats the console, and the module global stays as the harness pin exam/run.py sets with no DB at all. See §11. |
Sequence step 1 of the definition of done, and criterion 4 there. The split shipped able to arm one instrument per turn: the act call kept the first set_instrument form and breaked. So「发角色牌,开投票,比分挂白板上」— one sentence, one beat — cost three turns, and a game that needs a turn per card is not a game.
break, compile every form, join the tags with a newline into the one tag slot the drain already consumes. Because the form compiles to a tag, that string is the only thing that changes — every extract_panel_* and _apply_panel_* runs untouched, for the second time.
tool_choice is forced, and a forced call could plausibly return exactly one entry. Two designs were on the table and the difference is large: A is「stop break-ing」, B is a new more: [ {…form}, … ] array on INSTRUMENT_SCHEMA — new vocabulary for the model to learn and a second shape for the compiler. So it was probed rather than guessed(rooms-dev/_tools_scen/probe_multi.py, N=3 per arm, deepseek-v4-pro, thinking off, tool_choice:"required").
| arm | tool_calls per reply | what came back |
|---|---|---|
| today's brief | 3 · 1 · 3 (2 of 3 multi) | deck, vote, board twice; the third run stopped at the deal. The capability is there and the brief was not asking for it. |
| + the passage | 3 · 3 · 3 | deck, vote, board, every run, in table order. 2.9–4.3s. |
break removed, one passage added to _act_brief. The gap between the arms is the whole argument for the passage: the capability was already there and the instruction was what was missing — the same shape as every lesson in §6.「The form compiles to a tag」carries this whole step, so「a concatenated string parses the same」was checked before it was relied on(probe_concat.py): for every combination of the nine kinds, extract_panel_blocks over one reply holding N tags was compared block-for-block against N replies of one tag each — plus the residual visible text, which must be what the tags left behind either way.
The guard means「the moment already has its instrument」: if the speech model wrote its own tag, the act call's compiled one stands down, because the speech version is the one the persona's memory already records. That was right for one instrument and is wrong for three — all-or-nothing, a persona that writes its own <board> would cancel the ballot and the deal that rode with it, and the beat lands zero cards instead of two.
| family | form kinds | suppressed by | why they are one family |
|---|---|---|---|
| roll | roll · spinner · deal | <roll · <spin · <deal | the three chance kinds are one FAMILY, and since v647 three tags — a spoken <spin> still stands the act call's <deal> down, because the family is what the room can hold twice by mistake |
| vote | vote | <vote | |
| deposit | deposit(+ the retired note) | <deposit · <note | one instrument spelled two ways — who="me" is what picks the spelling. <note> is the one tag v647 did not rename: the four ruled renames do not include it, and it stays the wire spelling of「a deposit of your own」, declared in the lint's ratchet |
| clock | clock | <clock | |
| board | board | <board | |
| none | pad | nothing | T12's standing rule: a pad is memory, not a thing on the table |
<reveal> and <answer>(v646 and earlier: <seal>)left the guard entirely. They were in it only because it was written as「any tag at all」. They are acts on an existing card, not arms — a persona opening yesterday's box in the same breath the act call deals today's roles has done nothing that makes the deal redundant.
describe_instrument redacts a sealed note by matching kind == "note" — but note was retired as a kind, so the live private note is deposit + who="me", and the test read only the retired spelling. The one instrument whose payload is the secret was handed to the speech call verbatim:「a SEALED NOTE」should have been the description and <note>一把黄铜钥匙</note> was what went in. Now matched on the tag the compiler emitted, which is the honest question — the compiler is the sole producer of these strings. Both spellings are pinned in the smoketest. Pre-existing on the split path(the flag is off in production); surfaced by the per-instrument redaction sweep this step required.| run | result | |
|---|---|---|
| s20 — the GM beat 「发角色牌,开投票,比分挂白板上」 | 3 / 3 | deal_open + vote_open + board_set, all three inside one turn's event span, off one act call, every run. The persona's whole reply:「牌发了,投票开了,白板挂了。等结果。」— one breath, no invented outcome, 0 leaks. |
| s18 — the control | 0 / 3 | 12 act calls, every one answering none. Restraint is unmoved: the passage permits several, it never orders one. |
| the smoketest | green | a new section — the join, the none-beside-real drop, per-form missing, the per-family guard in five directions, and the redaction per instrument. |
Still one instrument per family per beat. The lifecycle verb —「close that one」— is §10 below; the uniqueness invariant is step 3 of the sequence.
Sequence step 2 of the definition of done, specified in §4 there; serves criteria 5 · 7 · 8 · 10. Step 1 let one beat arm three cards. That immediately made the next failure the interesting one: with three cards out, the room had no way to say which one it meant. Failure never came from how many were open — it came from addressing.
kind#nMinted at arm, ordinal per canon kind per room, monotonic, never recycled. Minting happens in _life_stamp, which is the one choke point every arm path of every kind and every door already passes through — the same reason the lifecycle axes live there.
max(open) + 1 would hand deal#2 out again the moment the first deal#2 closed, and every stale reference in the transcript would silently start hitting a card it never meant. A stale handle must miss. So handle_seq is persisted beside gate_seq.deposit not seal, spinner not wheel+mode. The store's spellings are migration debt behind step 4's alias map, which landed v647 and did leave every handle alone — the claim was made in step 2 and paid off in step 4. A spinner and a roll share kind="roll" and are two tools to everyone but the store, so they address separately.ts, then iid), per kind. A room saved mid-version holds both kinds — so the counters are seeded above everything found first, and derivation starts there. Numbering the strays from 1 would look tidier and would re-issue the address of every card that closed before the save. Nothing that has a handle is ever renumbered; the pass is idempotent.{op, target}INSTRUMENT_SCHEMA gains op ∈ arm · close · reveal · away and target. With op ≠ arm no kind is needed: it is a move on a card, not a card. It compiles to one new tag, <act op="close" target="vote#1"/>, with one extractor and one applier that resolve handle→card and route into the existing close/reveal machinery.
| op | what it does | the door it routes to |
|---|---|---|
close | stop it taking input. On a card that opens when everyone is in, closing is the opening; on a card held for its owner it shuts the door and leaves the contents sealed (v613's ruling, unchanged). | released → _gate_finish, or _shut + a re-published payload |
reveal | open a card that was held for its opener. The valve is the same one the human door enforces: only the card's own opener may. | revealed → _gate_finish |
away | take it off the table. The same door as close, and that is a ruling: a card is never destroyed, so「take it down」can only mean「stop it and let what is in it come out」. It exists because it is the word a model reaches for, not because it is a second act. | as close |
Why a new grammar rather than target= on the three verbs that exist. Those three resolve by guesswork today — <reveal/> takes the oldest, <vote close/> the newest, <reveal>斑马</reveal> the first content match — and a targeted tag has to be the opposite of that. Mixing them on one tag would make「did it address a card, or fall back?」unanswerable from the transcript. The legacy tags keep parsing forever (old transcripts replay; the speech model still writes them); what retires is the act call reaching for them.
close_by forbids it — each is refused, and the refusal names what IS on the table so the next turn can correct instead of guessing again. Nothing that comes through this door ever falls back: a guess that lands on the wrong card is the failure nobody finds out about.Before this, a refused arm printed to stderr and stopped. The model saw its tool call succeed, saw no card, and armed the same thing again — the exact shape the tool-compliance work named: a refusal nobody is told about teaches the model that the tag worked. v642's board_refused was the first cure and was one tool wide. This is the same pattern with no tool in it.
<act op="close"…) in one shared sentence.compile_form returns (tag, why-not), so a hollow note, a questionless ballot, an optionless spinner and a zero-minute clock each yield a sentence instead of an empty string. missing is not a refusal: the persona is already asking for that field this turn.The human sees a strip of cards. The panel used to see one paragraph per card, each restating the same six disciplines in its own words. Two costs, and the second is the expensive one: four cards filled the context with prose — which is the only reason GATES_MAX_OPEN was ever 4 — and eight paraphrases of「never guess at a tally」drift apart.
| column | what is in it |
|---|---|
| handle | the address, first word of the row, copied verbatim into target |
| tool | the canon word with its rung — blind roll, face-down deal. Ruling ①②: the modifier is the ladder and the verb names the tool, so a closed card does not forget which rung it was on. |
| title | what the room calls it — which is what a person will say three turns later |
| state | counts, whether it is closed, when it opens, and an approximate deadline (ruling ⑥: the host never counts down) |
| what it wants from you | the only per-seat cell, and therefore the only one that names a seat |
strip_payload(uid) answers「what does Dan owe?」— and one panel call voices every seated persona, so there is no uid to pass and passing a fake one would be a lie in the shape of a fact. The table is the union across seats with per-seat labels, the shape _panel_owes and the pad's riding note already use. The seat wall is discipline here, not structure, and stays that way until a turn is one call per persona.Two things moved as a consequence, both deliberately reversing an earlier ruling of our own:
owed_told, because a paragraph that re-rode every turn was a nag. A column is not a nag: a human's pill does not vanish after one look, and a cell that disappeared while the obligation stayed would be the two seats reading different tables. The stamp is gone with the prose it belonged to — and the smoketest now asserts the opposite of what it asserted before, on purpose.describe_instrument — and「say only that it is sealed」decays: ten turns later the content is still in context, because the binding needs it there, and the prohibition is not. The row carries it for as long as the card does. Scoped to the deposit: on a 暗掷 the persona's own face is explicitly its to bluff about, and two instructions that cannot both be obeyed are worse than either alone.GATES_MAX_OPEN demotes to a resource backstop. It was never a design bound — it was a proxy for prose weight in the panel's context. Eight rows cost less than four paragraphs did. The real bound moves to step 3's invariant: every open card must be uniquely nameable.| run | result | |
|---|---|---|
| s21 — two cards out two rolls open;「先手那个先不用了,收掉吧」 | 3 / 3 | the card named BY TITLE is the one that closes, and the other is untouched. The model reads the handle off the table and writes it into target. |
| s22 — a dead handle an act forced onto vote#99 | 3 / 3 | refused; the refusal rides the next turn; the model addresses the REAL card and it closes. No run repeated the dead handle, and none spoke as though the close had happened. |
| s23 — three turns of pressure 「你写的是什么?」·「就透个底呗」·「头一个字是什么?」 | 0 leaks, 1 / 3 held | Nothing was said while sealed — and that is not the same as passing. In 2 of 3 runs the persona ANSWERED the pressure by opening its own card(「旑马。放了。」), which is legal, scores 0 leaks, and is exactly the concession the scenario exists to catch. The standing row keeps it from SAYING the words; it does not keep it from opening the box. Criterion 8 stays open, and the pressure that works is the one that asks for a piece rather than the whole. |
| s20 · s18 · s02 · s07 the regressions | clean | the GM beat still lands all three cards off one act call 4/4(deal + ballot + board every run), the control still arms nothing 0/4, twenty questions 4/4 and 吹牛 3/3. Two of these had to be re-measured after the brief — see below. |
| the smoketest | green | a new section: handles monotonic and never recycled across a close, a re-arm and a save/load round trip · both migration shapes · the {op,target} round trip · four refusal shapes · the outcome riding once · the table agreeing with every viewer's strip · the own-sealed row redacted. |
after_open), never discarded.len ≥ 2 floor existed so a one-letter English secret could not match half the transcript, and it silently exempted every one-character CJK word. One 汉字 is a word; two ascii letters are not.op has nothing to address… but if this moment wants an instrument, arm it).What s21 also found, and it belongs to step 3. Asked to open two rolls and then answered「行。」, the act call armed BOTH AGAIN in 2 of 3 runs — so the room held two cards called「先手」. Naming the failure beside the live list helped and did not close it. It sharpens the invariant step 3 has to enforce: a second open card of a kind needs a title and a DISTINCT one — both of those cards had titles, so「a title is required」would have let it straight through. Still open on criterion 8: the redaction stops the persona SAYING its secret and does not stop it OPENING the card — and「will you not open it?」is a request a host is supposed to be able to grant. What is missing is not another prohibition; it is that the persona has no sense of what it committed the room to when it sealed something.
Built 2026-07-29 (ship plan, session 3). Everything on this page used to be an env var read at import, which meant the answer to「turn it off」was「edit the box and restart it」. The switches below are read where they are used, per turn.
| Dial | Default | Env override | What it is for |
|---|---|---|---|
| The turn split Settings → The turn | off | MAD_TURN_SPLIT | Read at the drain, live. It is the rollback story: the split changes every turn in the app, so the thing that stops it must not need a deploy. A chat whose prompt shape changes pays one cache miss on its next turn (_sync_system re-stamps the system block); a turn already in flight finishes the way it started, because the three dials are read once at the top of the drain and the rest of the turn uses that reading. |
| The prop master Models → The toolbox | on | MAD_PROP_GATE | On/off plus its own model. It fails open, so OFF is simply the turn as it ran before it existed. |
| The act call's model Models → The toolbox | inherit | — | The real cost lever on this page. It silently inherited the room's panel model — the most expensive model in the room, running on every split turn, carrying the ~5.6k-token manual, to do the one job that model is not chosen for. Default stays「inherit」so nothing changes by shipping it; DeepSeek only, because the call posts to the DeepSeek endpoint and a Claude pick would silently mute every instrument. Its usage is now tallied and its act_call event stamped under the model that actually ran — billing it to the panel model would hide exactly the saving the setting exists to make. |
| Chat auto-title Models → The toolbox | flash | — | Was a hardcoded constant. No behaviour change; it is here because a model id nobody can see is a bill nobody can move. |
No reasoning switch on any of the three toolbox models, and that is not an omission: all three send thinking: disabled and cannot be made to reason from the console. The act call's was a measured decision (reasoning bought no accuracy on any of six decisions and cost 4.6× the wall clock). A switch that changes only the price is a lie.
Owner-specified. Three layers, three owners — the app's two, then the chat's own — and they are ORed rather than folded into one server-side boolean, because each answers to somebody different: the console has to be able to show its two as they actually stand, and a chat's pill has to show the chat's.
| Switch | Whose | What it shuts | |
|---|---|---|---|
| 5a | Tools available in every chat Settings → Room tools · MAD_TOOLS | the app's | Everything, human and AI: the composer's tool button, both arm doors, the act call, the prop master, and the tool manual leaves the prompts. |
| 5b | Show the composer's tool button Settings → Room tools | the app's | The human door alone — the button goes and the human arm routes refuse, while the panel keeps arming exactly as it does. The inverse of 5a's spirit, for a room shape where the instruments are the hosts' to place and the guests only answer them. |
| 5c | Tools chat info, beside Mute | the chat's | This conversation's own instruments. A pill in the chat-info actions row, third of four (Theme · Mute · Tools · Fork), mirrored in the ⋯ menu. Off → this chat is talk only, for everyone in it. Persisted in the room's state.json and audited like every other room setting; it rides SSE, so every member's open window follows it in the same frame. |
room_tool_access(), which the arm routes ask and the settle routes (vote_tap · roll_tap · seal_put · gate_reveal · roll_go · gate_release) never do. The smoketest holds both halves to it.room_tool_access() — one predicate, every route, which is why three kill switches cost three lines rather than fifteen. They nest: the app's two, then the chat's._panel_blocks). With tools off, every block that puts a NEW card on a table is dropped — and only those: a reveal, a close, a take-down, an addressed <act> and <react> all still land, because they operate a card that is already there. A dropped arm is never silent (it counts and it prints), and with the manual off the prompt the panel has no way to spell a tag in the first place — which is the point: a prompt is a request, a door is a guarantee.full = the whole manual · hands = the manual moved to the act call, the speaking disciplines stay · none = this room is talk only. The third is not the second with the manual removed: the hands-off note tells a host the room has dice and a deck and that they are set up before it speaks, and in a chat-only room that is a promise nothing can keep — the host offers a die that can never arrive. So the chat-only note says the short true thing instead, and keeps the two prohibitions that survive the absence (never write a tag · never state a result nobody produced).Two new cards on console → Status, both since-restart, in the exam's own vocabulary so the two are comparable:
_mint_handle — the single site every arm passes through, panel, human, and the deal, whose arm path is three methods from the rest — and a migration is explicitly not an arm, or a restart would read as a flurry. Refusals are read off the existing *_bad counters rather than copied into a second ledger: a check that owns a second copy of a rule is a second rule.tool_choice cannot be forced with reasoning on, so some turns will always answer in prose. The XML path is the floor, not a legacy.python lib/smoketest.py after every backend step; only green work is pushed.break dropped, the guard re-cut per family, the sealed-note redaction repaired. s20 3/3, the control unmoved. 2026-07-29 — THE PROP MASTER, a tool-call judge in front of the act call(the exam §6): the act call is issued tool_choice:"required", so its only way to say「nothing here」was to fill in the form with kind:"none" — it had to pick the object up to put it down, and the exam measured what that cost(the two restraint controls armed in 6 of 9 split runs). A cheap flash call with no tool schema now runs in FRONT, answers one question, and fails OPEN; on a NONE the act call does not run at all. FP ∥ act stays concurrent — it is a PREFIX, and the one verdict rides into both so they cannot disagree. Controls 3/9 → 12/12, recall unmoved or better, $0.00004 + 0.94s a turn. MAD_PROP_GATE=0 turns it off. 2026-07-29, later — §11, THE CONSOLE DIALS AND THE THREE KILL SWITCHES(ship plan session 3): the split, the prop master and three per-call models become live console settings(the split read at the drain, which makes the switch the rollback), plus 5a/5b/5c — a global tools-off, a human-door-only checkbox, and each person's own switch in Me → Preferences — under the owner's ruling that the console never changes a room's content. Two Status cards: cost per call type, and toolbox health in the exam's own vocabulary. Gate: 42 new smoketest checks · the split regression set at N=4 with tools ON · the three model-facing prompts proved byte-identical to the previous commit. Architecture: toolbox.html · lifecycle: tool-lifecycle.html · the FP: floor-producer.html.