0149 — An emergency is never a mode question: one rule in the mode layer, the on-call chain at the moment of confidence, the work order after the call, and a caller who can cancel

Context

The mode layer (src/lib/domain/modules/module-mode.ts, Gera 2026-10-28) answers what each of the four modes — Off, Preview, Supervised, Live — lets Clara do. It said in a comment that "emergencies are not a mode question", but nothing outside tests read the capabilities object, so it enforced nothing. The bypass that did exist was spelled three ways at the call sites: the emergency always-on kind on a send declaration (always-on.ts), lifeSafety: true on an email or a page, and the tool-side 'always_on'. Meanwhile seven gates still held an emergency back by mode:

Each gate had its own idea of what an emergency is and whether it passes. Gera, 2026-10-29: "Make sure the code is clean around it, don't just hack."

Decision

  1. An emergency is never a mode question, and that is ONE rule, in the mode layer. capabilitiesFor(mode, subject) takes the mode and the act's subject ({ emergency: boolean }) and answers the Live row's sends, writes and team alerts for an emergency subject in every mode — Off, Preview, Supervised, Live and unresolved alike — and never holds it for approval. takesCalls and recordsToPanel stay the mode's own: an emergency at Supervised is recorded to the panel and acted on, never recorded instead of acted on. The resolver (resolveModuleEntitlement, resolveModuleEntitlementForOrg, resolveModuleStateForOrg, holdOrSendStateOf) and every refusal door (refuseIfModuleSwitchedOff*, the Maintenance wrappers) take the same opts.subject and answer the passthrough before any read: an unreadable company or building cannot hold an emergency. This is the one place the mode layer fails open; for a routine subject it fails closed as before. The answer carries passthrough: 'emergency' and logs one line, so an on-call reading an Off building's work order sees why it was filed. The subject is built from a fact the caller already holds — a work order's priority === 'emergency', the content check's verdict.outcome === 'emergency', isLifeSafetyUrgency(urgency) on an escalation — never from a string compared by name at a gate. A ratchet (emergency-literal-readers.drift.test.ts) pins every file that still decides an emergency by name (module: 'emergency', lifeSafety: true, reason === 'maintenance_module_off' beside a carve-out); each later slice deletes its row; a new site is red.
  2. Emergencies pass through in every mode, the emergency WORK ORDER included. The work order is filed and written to the PMS at Off, Preview, Supervised and Live. Preview's "recorded, never written" and Off's "nothing" do not apply to it. Supervised on a call is Live on the call: Clara files; only her follow-up messages are held.
  3. The moment Clara is confident on a call that it is an emergency, the on-call chain starts — mid-call — from the escalation signal itself. Calls, texts and emails to the on-call list go out at once, for every tier (gas, fire, police, a leak, no heat), from escalate_to_human. The first notice is interim — in Gera's words, "we just got a message, we're gathering more context, it seems like an emergency: XYZ". Clara keeps collecting details afterwards. No work order is filed mid-call: the work order and the PMS write come after the call, from the sweep, with the full context, and the sweep's follow-up to everyone contacted carries that context. startsAlertChainMidCall and heldChain (the 2026-10-20 hold) are deleted, not widened. This reverses ADR-0145's 2026-10-20 amendment: the founders now judge the first ring worth more than the extra context, and the follow-up carries the context.
  4. A caller CAN cancel. "Never mind, it's a test" after the flag is honoured: if the chain has not started, it does not start; if it is mid-chain, it stops — nobody further is rung — and everyone already contacted (on-call texts, the office email, anyone who was called) gets a follow-up: "That was a test, please disregard the previous message." (Clarified 2026-10-30, Gera ~00:20Z: "everyone" is EVERY recipient of the alert — the people the chain alerted after the post-call follow-up's claim as much as before it (they are kept on a second list, alertedAfterFollowup; on the 2026-10-29 test call hops 1 and 2 were texted after a 20 s call's claim and never told), the office inbox that got the exhaust note (as a reply in that email's thread, emailOfficeEmergencyDisregard), and #emergency — on the live-chain path and on the expired-ticket path alike.) No emergency work order is filed after the call; the withdrawal is recorded on the conversation as a fact. Clara never claims "I've closed that out" without a tool behind the claim. The withdrawal reaches the chain through an explicit in-call signal, never by parsing the escalation's reason text. (This reverses the ruling of earlier the same evening that a retraction still rings.)
  5. There is always a follow-up after an emergency alert. Normally it carries the full context from the finished call (and the work order's id). When the caller cancelled, it carries the disregard note instead. The interim notice and the follow-up are the same conversation's rows.
  6. The sweep's after-call emergency check runs at every building; the chain rings for the pinned or confirmed home, never for the dialled line's row. The gate that ran it only where the line's Maintenance row read Off is gone (§1: a mode never decides an emergency), so the one extra read of the transcript is paid on every voice call the sweep files nothing for. The "declined on the call: Maintenance is off here" label goes; what the on-call person hears is built from facts (what was reported, that no work order was filed, and "the caller then said it was a test" when so — the latter from withdraw_emergency, #23443). (Amended 2026-10-29 by part 4a: the first wording kept a gate on the home's mode, which §1 forbids.)
  7. After-hours wording for an emergency on a call: "I'm calling the on-call team now." The voice lane owns the prompt copy; this ADR owns the mechanism.

A piece of code violates this ADR if a module mode, a Supervised hold, a Preview record, or an unresolved read holds back, downgrades or refuses an emergency act anywhere; if a gate decides an emergency by a name instead of the subject; if a confirmed emergency on a call rings nobody before hang-up; if a work order is filed mid-call; or if a caller's cancellation does not stop the chain and reach everyone already contacted.

Consequences

Alternatives considered

Amendment 2026-10-29 (evening) — "Emergency calls go to": Clara, or the company's call center

Ruling (Gera, 2026-10-29 ~18:25Z, revised ~19:30Z): "anything that's maintenance or emergency is gonna go straight to MCC" — then, the same evening, made two explicit, independent choices per company, entered by Propflow (not the customer) next to the gatekeeper and module switches:

  1. "Maintenance calls go to" — Clara, or the call center. This is maintenance.intake_route plus the Maintenance switch, unchanged.
  2. "Emergency calls go to" — Clara, or the call center. New: the registry key emergency.intake_route (org / group / property, nearest wins, absentMeans Clara), read in ONE place, resolveEmergencyIntakeRoute (src/lib/domain/escalation/emergency-intake-route.ts), written by the canonical script scripts/set-company-emergency-route.ts (dry run by default).

They are independent: a company may send ordinary maintenance to Clara and emergencies to the call center, or any other combination. The default is Clara everywhere, so no company changes until Propflow writes its row (hot rule 8: one visible setting, default unchanged).

What "the call center" means for an emergency on a call. The moment Clara confirms the emergency she fires escalate_to_human (decision 3), and at a call-center company that tool call is the forward: the handler moves the live call to the stored call-center number as the caller (no caller id: an outside service that serves the tenant directly shows the tenant, handoff-caller-id.ts), at once — no grace for the spoken 911 line (amended 2026-10-30, Gera ~00:20Z: the call center handles the safety instructions; the 6 s grace let the 2026-10-29 test call, hung up at 19 s, miss the move with Twilio 21220 and ring the company's own techs), before the tool answers (emergency-call-center-forward.ts). It is server-side and keyed on the setting, never on a prompt branch, so a life-safety caller reaches the human the company chose whatever the model does next. A move Twilio refuses falls through to today's path, unchanged. A move that went through skips only the mid-call chain start; the team is still paged, and the post-call chain and the one-person relay are untouched by this slice — they may ring on top of the center, never instead of it. Standing them down on the forward's outcome (the center answered / the dial ended unanswered) is the follow-up (#23529's design read), never on the setting alone. The emergency work order is still filed after the call (decision 2 stands).

Decisions 1–7 stand, and "an emergency always reaches a human" still holds: at a company that chose the call center, that human IS the call center. The spoken 911 / evacuate line (Triage's and Step 2's) is no longer waited for: the forward goes the moment the tool fires, and the center gives the safety instructions (2026-10-30; the prompt may still start the line, the move cuts it).

Why one key and not "Maintenance Off + a call center". The first wording tied emergencies to the Maintenance switch; Gera's revision the same evening wants the two choices visible and separately settable, because a customer may want Clara on repairs and the call center on emergencies (or the reverse). A derived rule would have made one of those combinations unreachable.

A piece of code violates this amendment if an emergency at a company whose "Emergency calls go to" is the call center is not forwarded there on a live call, shows the line's number instead of the caller's on that forward, silences a fallback on the setting alone, or reads the choice anywhere but the one resolver.

Amendment 2026-10-30 (~00:20Z) — at a call-center company the techs never ring; a failed forward becomes Clara's call to the center

Incident: the 2026-10-29 test call at a call-center company (conv_voice_c14ad0b9-…). The caller hung up at 19 s, the move was refused (Twilio 21220), and "today's path" rang three of the company's own techs — the people the setting exists to spare.

Rulings (Gera): (1) forward at once, no 911-line grace (amended in place above); (2) if the forward fails — the caller hung up, or Twilio refused the move — Clara places a call to the call center herself, with the caller's details, and never rings the customer's on-call techs; (3) the disregard reaches every recipient (decision 4, clarified in place).

Mechanism for (2), one door. startEmergencyAlertChain reads the one resolver (resolveEmergencyIntakeRoute) before its switch and its list. At a company whose "Emergency calls go to" is the call center, the chain IS one call from Clara to the center — the emergency relay agent (emergency-relay-call.ts), dialled from the company's own line, briefed with the caller's name, unit, callback number and what was reported — and no hunt ticket, no hop leg and no on-call text exists. Every starter goes through that door (the escalation mid-call, the company-line start, the sweep after the call, a work order, a texted emergency), so no caller has to know the setting. The chain switch does not matter there; emergencyAlertChainWillRing answers true. A withdrawn emergency places nothing; one call per conversation per day; a call that could not be placed is told to the team at once (the exhaust note) and the escalation's own page still goes out. The emergencyChainStartedAt stamp stands the hang-up relay down, so the center is not called twice. #emergency reads "nobody on the on-call list: emergencies at this company go to its call center…".

This reverses three sentences of the 2026-10-29 evening amendment: "a move Twilio refuses falls through to today's path" (it falls through to the center); "they may ring on top of the center, never instead of it" (the post-call chain and the one-person relay never ring the techs at such a company); and "standing them down on the forward's outcome … never on the setting alone" (the on-call list is stood down on the setting; what the setting does NOT silence is the center itself, which is always reached). The violation clause "silences a fallback on the setting alone" is withdrawn with it.

Scope decision, recorded here rather than escalated: the ruling was framed around calls, and this door is company-wide — a texted emergency at a call-center company also reaches the center, by Clara's call, instead of the techs. The alternative (gate on the channel) would leave the texted case ringing the very people the company chose not to ring.

A piece of code violates this amendment if anything at a call-center company places a leg, a text or an email to the on-call list, reads the choice anywhere but the one resolver, or lets a refused forward end with nobody calling the center.

Amendment 2026-10-30 — the scripted maintenance texts have no exception; maintenance.always_send_texts is retired

Ruling (Gera, 2026-10-30): "Supervised holds every message… If there's something contradicting that, then that needs to be updated so we don't get confused." Hot rule 10 (#23507) is the standard: under Supervised every message is held (residents, prospects, vendors and the team) except an emergency (every mode) and a caller's message or callback request to the team; PMS writes and live calls go.

What contradicted it. maintenance.always_send_texts (#23084, writer script #23572; Gera 2026-10-28 with Fede 2026-10-27) let three scripted maintenance texts (wo_number, wo_created_notice, rating_prompt) skip the Supervised hold: each was written as a hold already APPROVED by system:always-send, posted as "Approved automatically", and sent at once, and a text absent from the stored value counted as ON. On 2026-10-30 01:31Z that texted "Your work order #… is in" to a resident at a customer whose Maintenance is Supervised.

Decision: the setting is retired, not defaulted off. It could change no outcome outside Supervised — reviewAutomatedText (rerun-text-review.ts) asks decideHold first and answers send under Live before autoApprove is read; Off refuses at the send door; Preview records — so once Supervised stops honouring it, it is a switch nothing reads (ADR-0144 bucket d: unused, deleted; hot rule 8: a fix to live behaviour has no switch). The three senders (notify-tenant-of-pms-creation.ts, pms-work-order-number-follow-up.ts, rating.ts) now go through reviewMaintenanceText / reviewRerunText with nothing but the mode; the registry key, its absentMeans, the writer script, the atlas provenance row and the Clara-paths settings are gone with it. The dead auto-approve door (recordAutoApprovedText, settleAutoApprovedText, AUTO_APPROVED_BY, renderAutoSentCard, the autoApprove inputs) is deleted in the follow-up PR. The two stored rows (written 2026-10-28 and 2026-10-30, every text false) are unread from here on; no registered reader is left for that slot.

Part 2 (the dead door) and the fate of the rows it left. The auto-approve door is deleted whole: recordAutoApprovedText, settleAutoApprovedText, AUTO_APPROVED_BY, autoApprovedSend, autoApproveText, renderAutoSentCard and the autoApprove inputs. autoApprovedSend used to re-send, on a retry, a hold row the setting had already approved; without it a retry answers held on an approved row, which would strand a text if a retry could still reach one. Census before the deletion (read only, scripts/ops/count-auto-approved-holds.ts, prod, 2026-10-30): 32 rows approved by system:always-send. 9 finished (send stamp on the work order). 20 on the test building whose work orders no longer exist (every sender refuses before the hold). 2 on a test-building order that never promised a number (never due again). 1 rating ask at a real company on an order completed a week earlier, whose only triggers are the one-shot completion activity and a person's approve re-run, and whose card already says Not sent. No retry path reaches any of them, so none is re-opened or expired, nothing is written to prod, and the branch goes. A new row of that shape cannot be written: no code path writes system:always-send any more (other system approvers remain by design: the rollout release, system:rollout, and the draft-expiry sweep).

The one rule stands unchanged: capabilitiesFor(mode, subject). A scripted text is a routine subject. The only passthroughs are the emergency subject (every mode) and the caller-message subject (Supervised only); "scripted", "low risk" and "the customer already agreed" are not subjects.

A piece of code violates this amendment if any message to a resident, prospect, vendor or the team skips the Supervised hold by a company setting, a sender name or a judgement about the text, rather than by the subject rule in module-mode.ts.

Amendment 2026-10-30 (afternoon) — the canonical mode model; emergencies and caller notes become locked paths

Rulings (Gera, 2026-10-30):

  1. The four modes. OFF is entirely off. PREVIEW writes nothing anywhere (no PMS write, no email, no text) and saves a DRAFT response wherever Clara could have responded. SUPERVISED is on, but every message to the team, residents, prospects and vendors is held; PMS writes go; outbound calls are held; only emergencies go. LIVE is on, with an optional "also log to Slack" field.
  2. What always goes is a path, not an exception. "So it actually becomes a path, not an exception." Emergencies and "caller left a note / callback" become their own paths on the Paths page, LOCKED (grayed, cannot be changed), defaulting to Live with Slack logging on.
  3. The emergency path is the on-call chain, not vendors. "For emergencies we only want to worry about the on-call chain dispatch, not vendors and handymen." The emergency path is the on-call chain, the team emergency alert, the 911 line to the resident and the emergency work order. A vendor's or the in-house handyman's emergency page is held under Supervised like any vendor text.
  4. Supervised holds Clara's reply to a teammate's own text too. "If there's a path we always want on, we set it on, not supervised."
  5. An unreadable mode fails closed (for a routine act; the emergency path is not a mode question).
  6. Supervised holds all outbound calls except emergencies; Maintenance PMS writes always go.

What this changes in this ADR.

What is being built. The two locked rows and their resolution (Clara's paths lane). Live's optional Slack-logging field (checked against the Preview/Live Slack switch work, #23533). A callback Clara files MID-CALL (escalate_to_human) routes through the caller-note path by a typed callback fact on the tool input (today only the two after-call backstops mark a caller's message). The write-gate lane brings writesHeldForApproval to ruling 6.

A piece of code violates this amendment if anything always goes by a check at a door rather than through its locked path's subject, if a vendor's or handyman's page goes unheld under Supervised because it is an emergency, or if an unreadable mode lets a routine act out.

Amendment 2026-10-31 — a renewal under Preview pauses; it does not fail

Ruling (Gera, 2026-10-31): under Preview a renewal PAUSES quietly with its draft saved, instead of ending as escalated / prepare_failed. It resumes when the building's Renewals mode moves to Supervised or Live. One path, no switch; no renewals-board "needs attention" entry for a Preview pause.

What main did before. Ruling 1 above made Preview refuse the renewal prepare at the chokepoint (triggerRenewalSync, code renewals_preview), and the workflow treated every refusal as final: exitEscalated('prepare_failed', 'PMS prepare failed: PREPARE_REFUSED (renewals_preview) — …'). The renewal ended, the board listed it under needs-you ("restart it"), and nothing of what Clara would have filled was kept.

What is built (#24218). The workflow parks the renewal in a quiet paused_preview phase and re-asks through the same dispatch activity on a durable 12-hour timer (patched('renewal_preview_pause_v1')); the chokepoint re-reads the mode before any side effect, so that re-dispatch IS the re-check. Supervised or Live queues the prepare and the renewal continues on its normal path (Supervised then holds its messages under ruling 1). A different refusal (Renewals switched Off) still ends it; a PM decision or tenant settle while paused goes through the terminal ladder. The pause is bounded by lease end + 1 day: a pause that outlives the lease ends quietly (no page, nothing sent) and never resumes the start-time offer for a tenant already month-to-month. The draft (terms and dates only) is saved once on the renewal's own timeline. The board shows "Paused (Preview)" as a working row, and the stuck-saga sweep treats the pause as a pause, not a stuck saga. No cron, no sweeper, no mode-change signal.

A piece of code violates this amendment if a Preview refusal of a renewal write ends, escalates or pages, or if a renewal paused by Preview needs a person to restart it once the mode moves.

Amendment 2026-11-02 — a teammate's declared test suppresses the emergency path

Ruling (Gera, 2026-11-02; design: docs.propflowai.co/a/test-call-mode-2026-11): when a registered PropFlow teammate (config/phone-registry.json, kind: human, benchAdmin: true) says in plain words that this is a test, on any line at any company, THAT ONE conversation is draft-only: Clara answers and runs the whole process, and nothing real happens, the emergency path included (no on-call chain, no team emergency alert, no emergency work order). This is the first time a single conversation changes the mode, and the only case where an emergency IS a mode question. An emergency without a declared test is unchanged, and anyone else saying "test" changes nothing.

Where it lives. One rule, as before: capabilitiesFor answers a DRAFT row (Clara runs and takes the call; nothing sends, writes, holds or pages) for a subject carrying declaredTest, in every mode and BEFORE the emergency and caller-note paths. The turn arms a frame (declared-test-frame.ts, which lists the doors) and each door asks draftedByDeclaredTest(subject). Built in slices: (1) the rule, the frame and the doors, with nothing arming the frame yet, so nothing changes on merge; (2) the trigger on a text (the phone is the gate, the words only start or end it; a text test lasts until the teammate says it is over or ten minutes pass (two hours until Gera's 2026-11-03 "it should be simple"); the window is fixed, not sliding: a text after it runs for real, and saying it again restarts the ten minutes), armed before the deterministic text lanes.

What is being built. The one TEST notice to the team (email through sendOrHoldTeamNotice plus the Review & log card, marked TEST, saying what Clara would have done), the TEST flag on /conversations with the thread left out of grading and metrics, and the phone-call half (a test lasts for the call). Customer staff declaring a test at their own buildings is part 2 of the design.

Scope of a text test (Gera, 2026-11-03). A text test covers ONLY THE LINE the teammate texted: a real emergency texted to another line of the same company pages for real. The running test is an expiring marker keyed by (teammate phone, line), reused from the idempotency store (declared-test-lanes.ts), read before every lane in dispatchInbound and again by the agent turn. It is never a field on whichever conversation a lane lands on, which three review rounds on #24677 showed a lane can miss. Conversation.declaredTest is the display copy for /conversations.

A piece of code violates this amendment if a declared test lets an act reach a real person, a PMS or the on-call chain, if the declaration can be set by a model's or a caller's claim rather than the server's phone check, or if a declared test changes anything for a sender who is not a registered teammate.

Built 2026-11-03, the test reply (part 2b). Clara's reply to the teammate who declared the running test, on that teammate's own phone, is the locked test_reply path (locked-paths.ts, a grayed row on the Paths page, team.test_reply). It is Live under Preview, Supervised and Live, and follows the module under Off and an unreadable mode, so nothing goes there ("OFF = entirely off", the test included). capabilitiesFor resolves its subject (TEST_REPLY_SUBJECT) BEFORE the declared test's DRAFT row; every other act in the test stays drafted. The subject is set only by testReplySubject (declared-test-frame.ts). Inside the turn's frame that means the declaring teammate. After the turn, it means a conversation carrying a running test, whose own phone is to, and which is a registered teammate's. The asks that hold or refuse a text pass it to the one rule: the row line, the routed backstop and the text door's backstop (shouldHoldClaraReplyRuled, rule subject), and the text door's Preview refusal (previewSendRefusal). The Supervised carve-out this names is narrow on purpose: only that teammate's own phone, only while their test runs.

Built 2026-11-03, the TEST notice (part 2). The notice is a locked path, test_notice (locked-paths.ts): Live in every mode, Slack logging on, a grayed row on the Paths page (team.test_notice). capabilitiesFor resolves its subject (TEST_NOTICE_SUBJECT) through that path BEFORE the declared test's own DRAFT row, so the notice goes from inside the test's frame by its own subject and nothing else in the frame is un-drafted. It is one plain note to the building's office inbox through sendOrHoldTeamNotice (which, given the subject, asks the one rule at the Team notices mode and sends Live, declaring team_alert to the email door) plus one Slack line through the Review & log poster (postSendLine), both marked TEST and listing the doors' "would have" lines. One per turn: emitted at the text dispatcher's turn boundary (dispatchInbound), keyed on the turn's event; the agent turn's own decision (armed or stood down in its child slot) is written back to the turn's slot for it. declared-test-notice.ts is channel-agnostic, for the call half.

Addendum 2026-11-03 — the phone-call half, after the call (part 4a). The voice tool webhooks receive a tool name, its arguments and the call's dynamic variables, never the caller's words; the first place the server hears them is the post-call webhook. So a call's declaration is read there today (declared-test-voice.ts, called by /api/voice/call-ended): when the telephony caller id is a registered teammate and one of the caller's own turns declares a test, the webhook's slot is armed and every post-call act runs drafted (the work order filed from the transcript, which is where voice work orders are filed; the team notice; the emergency relay, now stopped at the outbound-call door initiateOutboundCall, and the emergency no-answer backstop that texts the emergency contact and alerts ops, now a door of its own; the relay leg's own post-call webhook, a call we placed, never arms), and the test is stored closed with endedBy: 'call_ended'. A drafted post-call create is its own outcome, never a refusal, and the promise ledger skips any conversation that carried a declared test (it runs outside the frame, in a Temporal worker). What this does NOT cover yet: acts Clara takes DURING the call through a tool (a page, an escalation, an on-call chain started mid-call, a transfer) still go out for real. Drafting those needs the declaration stored during the call, which needs a prompt or tool change proven with voice:fastlane; that waits for test-property testing to reopen.

Addendum 2026-11-03 — WHO may declare a text test (Gera, 2026-11-03). The registry's benchAdmin list is no longer the gate on a text line. One pure rule, mayDeclareTextTest (src/lib/domain/staff/inbound-resolution.ts), reads the line company's own staff answer (decideLineStaffAnswer's order, fenced to the line's company). It admits a Propflow admin (the platform-administrator roster) on every line, the line company's own TEST_DECLARER_TIERS (org_admin, pm, assistant_property_manager, regional_manager) at that company's buildings, and a registered bench phone on a test company. Maintenance, handymen, read-only and leasing staff, residents, prospects, vendors and strangers never declare. Read the 2026-11-02 violation sentence's "registered teammate" as "a sender mayDeclareTextTest admits". When a marker read fails, only a bench phone's turn is drafted (fail closed); a client manager's turn stays real. WHO is asked on every text while no test runs; once a test runs, the marker is the authority, so a restart renews the window without a second WHO read. A call's test still gates on the registry (#24882).

Addendum 2026-11-03 — WHAT starts a text test is read by meaning, never a phrase list (Gera, 2026-11-03: "a smart intelligent layer, not regex or hard-coded checks"). The dispatcher's line arm asks WHO first, so a resident never pays for a model call. Only a sender WHO admits, or one whose test is already running, gets one small model question: is this person declaring that this conversation is a test, or that it is over (classifyTestDeclaration, declared-test-classify.ts)? The verdict is logged with a reason. The turn that ENDS a test runs real, so "test's over, for real now, water in 2A" pages. The call makes one attempt under a hard deadline. If the model cannot answer, a running test continues; with none running, a bench phone's turn is drafted and no window opens, and a customer staff turn runs real. The phrase list (matchTestDeclaration) is no longer a text arming path. It survives only for the call's post-call read, until the voice declare tool replaces it, and as the "old rule" comparison column in the text replay and eval, to be removed from those by 2026-12-01. Clara's own start_test_mode / end_test_mode tools on text (the voice lane's catalog rows, with WHO re-checked server-side) follow in a second PR.

Addendum 2026-11-03 — the phone-call half, during the call (part 4b, #24882). Test mode is model-decided (Gera 2026-11-03: a tool call, never a regex, on every channel). A registered teammate's call carries a TEST MODE standing order, rendered into the existing teammate-only {{bench_codes_rule}} value on any line (every other caller still gets the empty string). When they say the call is a test, Clara calls start_test_mode (end_test_mode when it is over); the handler re-checks the platform caller id and stores the test on the call's conversation, and both voice tool routes run every later tool webhook inside the declared-test frame, re-checked on every call, so acting tools return the tool door's draft. On a call the call is the window (no ten-minute cap), with a two-hour stale-row backstop. An unreadable row never arms (an emergency must not be stopped by it). ElevenLabs' native transfer never reaches the server, so the standing order tells Clara not to transfer during a test.

Addendum 2026-11-04 — the phrase list is gone (Gera: never a regex). matchTestDeclaration and its per-conversation stepper are deleted. A call is a test only when Clara's start_test_mode stored one; the post-call webhook no longer reads the caller's words, so if Clara never switched test mode on, the post-call work is real. The text replay and eval lose their "old rule" comparison column.