0109 — Voice Triage Front-Door Doctrine
- Status: Accepted (ratified by Fede 2026-07-23)
- Date: 2026-07-19
- Deciders: Fede
- Related: ADR-0029 (role-prompt-tool router — the text-side identity→capability architecture; the voice sibling topology is its ElevenLabs-hosted cousin), ADR-0040 (identity-anchored reasoning for operational users), ADR-0101 (tour-day arrivals transfer the live call), ADR-0080 (read-injection doctrine), PRs #4146 (turn config as code), #4148 (personalized triage greeting + identity de-steer), #4149 (Spanish layer 1), #4155 (per-call language override allowlist).
- Evidence base: the 2026-07-19 corpus replay — every historical
Clara — Triagecall (828 calls → 434 distinct scenarios) replayed across 5 model arms, with all 168 history-vs-arm disagreements individually adjudicated, then re-inspected under Fede's locked doctrine rulings (this document). Raw data: session 170c4eb1 scratchpad,harness-build/routing-replay/(REPORT.md,verdicts.json,replay_results.jsonl).
Context
PropFlow's production voice topology is a set of standalone ElevenLabs sibling agents: Triage answers every inbound call and hands off to a specialist — Leasing, Maintenance (Tenant), Maintenance (Handyman), Lease & Billing, Renewal Inbound/Outbound, Turnover Intake, Unknown Caller — or to a human via transfer_to_number.
This topology has no founding ADR. It grew out of ADR-0029's text-side router, the #1990-era specialist split, and per-feature ADRs (0048, 0057, 0080, 0101). Its operating doctrine exists only as comments in agents/clara/lib/voice-agents/triage.ts and accumulated gauntlet lessons. When the 2026-07-19 corpus replay's adjudicator needed a rubric, the ambiguities in that implicit doctrine produced rulings that contradict the founder's intent — which is how we discovered the doctrine was never written down.
This ADR codifies the doctrine, records what the full-history replay found when measured against it, and proposes fixes.
The doctrine (Fede, locked 2026-07-19)
- Triage is the front door. Its only job is to transfer. The point of the sibling topology is small, specialized agents: issue X gets routed to specialist Y. Triage greets, classifies, hands off. It never resolves the caller's need itself.
- An explicit ask for a human transfers immediately. No deflection, no "let me see if I can help first."
- A prospect asking about the property is a lead, not a trivia question. "Do you have a pool?" routes to Leasing — Leasing answers and captures the guest card, pitches the tour. Answering amenity questions at the front door discards revenue signal.
Point 3 overturns the current prompt's rule 4.5, which does the opposite by design: triage carries a LOADED PROPERTY FACTS block "ONLY so you can answer a quick 'do you have a pool / gym / are you near the light rail?' yourself (rule 4.5) without a transfer" and explicitly instructs "do NOT pitch a tour." The prompt's own mission statement ("You never answer substantively. You greet, classify, hand off") already agrees with the doctrine — rule 4.5 is a carve-out that contradicts the mission two paragraphs above it.
The architecture today
flowchart LR
C[Caller dials property line] --> T1[Twilio]
T1 --> P["/api/voice/personalization
(pre-first-word context)"]
P -->|"greeting (name-aware #4148)
language override (es, #4155)
tenant/prospect context
upcoming tour · renewal saga"| TR
TR{{"Clara — Triage
(front door)"}}
TR -->|transfer_to_agent| L[Leasing]
TR -->|transfer_to_agent| MT["Maintenance (Tenant)"]
TR -->|transfer_to_agent| MH["Maintenance (Handyman)"]
TR -->|transfer_to_agent| LB[Lease & Billing]
TR -->|transfer_to_agent| RI[Renewal Inbound]
TR -->|transfer_to_agent| TO[Turnover Intake]
TR -->|transfer_to_agent| UC[Unknown Caller]
TR -->|transfer_to_number| H([Human — PM phone])
TR -->|"rule 4.5: 'handled'
⚠ answers inline"| X[["call ends at the front door
(no specialist, no guest card)"]]
style X fill:#7a2222,color:#fff
style TR fill:#1f4e79,color:#fff
The red terminal is the doctrine violation: an outcome where the front door was the destination.
Findings
F1 — The handled carve-out contradicts the mission and drives the largest live misroute class
The corpus replay adjudicated 168 disagreements between historical routing and the model arms. Re-inspected under the doctrine:
- 29 historical calls were real misroutes (issue X reached the wrong specialist Y).
- Re-run against today's model + prompt (the control arm), 17 of the 29 are already fixed, 12 still misroute today — 7 repeating the exact historical mistake, 5 picking a different wrong door.
- A ×3 reproducibility pass (2026-07-20, faithful full-config copy) grades those 12: 8 deterministic misroutes (wrong on 3/3 runs — one caller got three different wrong doors across reps), 2 flaky (wrong 2/3), and 2 measured correct on all re-runs (the original single-run catches were stochastic flukes; both in the solicitor class). The confirmed-live set is 10 misroutes (8 stable + 2 flaky); the pinned regression suite deliberately keeps all 12 — the 2 single-run flukes included — because a fluke that misrouted once can misroute again, and a scenario that stays green is free. Raw data
still-live-repro-x3.json. - The dominant still-live pattern is
handledoveruse: solicitors, robocalls, bare-name callers ("Erica." / "Joanna."), a caller asking for a specific person ("is Rico there?"), and garbled maintenance asks all terminated at the front door instead of reaching Unknown Caller / Maintenance / a human.
The 4 remaining adjudicated cases (amenity-curious prospects sent to Leasing) were ruled correct under doctrine point 3 — the adjudicator's rubric wanted them answered inline and was wrong.
flowchart TD
subgraph still["The 12 still-live misroutes (today's model + prompt)"]
direction LR
A["Solicitors / robocalls / bare names /
'is Rico there?' — 7 calls"] -->|"today: handled or human"| UC2[should be: Unknown Caller]
B["'my work orders?' · water-shutoff text — 2"] -->|"today: Lease & Billing"| MT2[should be: Maintenance Tenant]
D["'why did you message my cell?' complaint — 1"] -->|today: handled| H2[should be: Human]
E["delivery driver at the door — 1"] -->|today: Unknown Caller| H3["should be: Human (live access need)"]
F2A["tenant says 'turnover on unit 204' — 1"] -->|today: Turnover Intake| RI2["should be: Renewal Inbound
(tenant move-out ≠ PM turnover)"]
end
F2 — Unknown Caller is under-used
12 of the 29 historical misroutes should have gone to Unknown Caller. It is the correct door for every unidentified caller with no routable need — solicitors, robocalls, message-leavers, people asking for a named staff member — because it is the only sibling designed to capture without acting (no tenant/leasing/work-order tools by construction). Today's prompt reserves it well in rule 5c but the model still leaks these callers to handled and human.
F3 — "My work orders" reads as billing to the model
Three historical cases (two still live) routed a tenant's own work-order status question to Lease & Billing. Rule 4 already says "anything maintenance-flavored is rule #2, NOT here" — but "can you tell me about my work orders?" and "I got a text the water's being shut off" don't pattern-match "maintenance-flavored" strongly enough. The boundary needs the explicit counter-examples.
F4 — Vendor/delivery callers have no clean lane
Appliance deliveries were routed to Lease & Billing, Unknown Caller, and (correctly, once) Maintenance Handyman. A delivery driver standing at the door needs a human (message capture can't open a door — the vendor-flavored cousin of ADR-0101's arrival finding). This overlaps the vendor-communications lane proposal (PR #4087); whichever lands, triage's rules need one unambiguous vendor branch.
F5 — The immediate-human rule exists; execution is the gap
Doctrine point 2 is already rule 6 verbatim (explicit human ask → transfer_to_number, one warm word, no destination-naming). But the replay caught a live execution miss ("can I talk to someone real quick?" → answered at the front door). This is a regression-scenario gap, not a prompt gap.
F6 — Pre-first-word context is fetched but under-exploited (the tour-caller question) — RESOLVED: see Decision 6, shipped in #4372
The personalization webhook already resolves, before Clara says a word: caller identity (name-aware greeting since #4148), stored language preference (#4155), tenant/prospect context, any upcoming tour, and renewal-saga state. Yet the greeting is doctrinally neutral ("never presume a topic") — a prospect with a tour booked in 3 hours gets the same "what can I help you with?" as a stranger.
The neutrality rule exists for good reason (the old "are you calling about your maintenance request?" greeting was a real offender — wrong guesses are costly and creepy). But a scheduled tour inside a tight window is a categorically stronger prior than "has ever filed a work order":
| Signal | Prior that the call is about it | Wrong-guess cost |
|---|---|---|
| Has an open work order (old offender) | Low | High — feels surveilled |
| Has a tour booked within ~48h | High | Low — "Also, I see you have a tour tomorrow at 2" reads as competent, not creepy |
Recommendation (approved by Fede 2026-07-22, shipped in #4372 — extended to fresh open work orders): keep the neutral greeting, but when a scheduled tour falls within a window (48h), append one proactive clause — "Hi Dan! What can I help you with — are you calling about your tour tomorrow at 2?" — and treat any yes/tour-adjacent reply as an immediate transfer to Leasing behind one first-person handoff line. Outside the window, stay fully neutral. This is a greeting-copy + routing-hint change on top of plumbing that already exists.
F7 — The routing rubric teaches the anti-doctrine
The rubric used for adjudication (and mirrored in eval scenarios) uses "do you have a pool?" as its worked example of answer-it-inline. Under the doctrine that example is exactly backwards. Every artifact that encodes routing truth — the triage prompt, the routing rubric, the eval scenario expectations — must be re-aligned in the same change, or the next replay/eval will re-flag correct behavior as bugs (and vice versa).
F8 — Model-comparison verdicts inherit the rubric bias
The replay's headline (Sonnet 5 at 94.9% vs live 94.7% — a tie) credited arms for handled answers the doctrine now rules wrong. Only ~4 distinct units are affected, but the Sonnet-5-vs-live gap (0.2 pts) is inside that bias. The tie is not trustworthy until the affected units are re-adjudicated under the corrected rubric. (Qwen 3.6-35B's disqualification gets worse under doctrine — its signature failure was answering callers inline instead of routing.)
Decision
- Adopt the three doctrine points above as the voice front-door architecture — this ADR is their document of record.
- Retire rule 4.5 (
handled) as a routing outcome for prospect-shaped questions. Amenity/location questions from prospects route to Leasing behind one short first-person handoff line ("Let me check that for you") — per the spoken-handoff doctrine shipped 2026-07-20 (#4193/#4197/#4198): never a bare or wordless handoff, never wording that announces a transfer or names a team.handledsurvives only for zero-content interactions (pure pleasantries, wrong-number hangups) where no specialist has anything to capture. The LOADED PROPERTY FACTS block shrinks accordingly (it remains useful for disambiguation, not for answering). - Tighten the misroute boundaries with the replay's counter-examples: the 12 still-live cases become named regression scenarios in the triage suite (solicitor→unknown_caller ×7 class, work-order-status→maintenance_tenant, complaint→human, delivery-at-door→human, tenant-says-turnover→renewal_inbound).
- Re-align the routing rubric and eval expectations to the doctrine in the same PR as the prompt change (F7).
- Re-adjudicate the ~4 rubric-biased replay units before acting on any model-swap decision (F8).
- Tour-proactive greeting (F6): RESOLVED — Fede approved 2026-07-22 and it shipped in PR #4372 (extended to fresh open work orders). Composed in
triage-greeting.ts: confirmed tour ≤48h or fresh open WO turns the ask context-aware, always open-ended; fail-closed to the neutral open. Live-verified via real EL calls on the prod bench. - Vendor lane (F4): defer to the vendor-communications ADR (PR #4087); add triage's delivery-at-door→human rule regardless of which lane design lands.
Proposed end state
flowchart LR
C[Caller] --> P["personalization
(identity · language · tour · saga)"]
P --> TR{{"Triage — front door
ONLY transfers"}}
TR -->|"prospect anything
(incl. amenity questions = leads)"| L[Leasing]
TR -->|"maintenance-flavored
(incl. 'my work orders', shutoff notices)"| MT["Maintenance (Tenant)"]
TR -->|vendor on existing WO| MH[Handyman]
TR -->|verified tenant · lease + money only| LB[Lease & Billing]
TR -->|renewal keywords · tenant move-out| RI[Renewal Inbound]
TR -->|PM turnover context| TO[Turnover Intake]
TR -->|"solicitors · robocalls · bare names ·
asking for a person · unclear after 1 question"| UC[Unknown Caller]
TR -->|"explicit human ask — IMMEDIATE
delivery/access at the door
complaints needing judgment"| H([Human])
TR -.->|"tour within 48h:
proactive clause (shipped #4372)"| L
style TR fill:#1f4e79,color:#fff
style H fill:#2e5e2e,color:#fff
No red terminal: every call ends at a specialist or a human. The only inline responses left at the front door are the AI-disclosure answer (legally mandated) and zero-content pleasantries.
Consequences
- Positive: every prospect interaction produces a guest card; solicitors stop consuming
handled/human paths; the routing rubric, prompt, and evals stop disagreeing about ground truth; the sibling topology finally has a document of record; regression scenarios pin the 12 known-live misroutes. - Negative / accepted: amenity questions gain one transfer's latency (~1s post-#4126, covered by the spoken handoff line) versus an inline answer — accepted as the cost of lead capture; the tour-proactive greeting (as shipped in #4372) reintroduces a bounded topic presumption, mitigated by the 48h window, the always-open-ended phrasing, and the low wrong-guess cost.
- Neutral:
handledremains in telemetry as a valid label for zero-content calls, so historical dashboards don't break.
Verification plan
- Prompt/rubric change ships with the 12 regression scenarios + the existing 28-scenario triage gauntlet (bar: 12/12 new green, no regression in the 28).
- Re-adjudication of the rubric-biased units re-scores the 5-arm table; the Sonnet 5 decision card updates with corrected numbers.
- Post-deploy, a 1-week sample of
handled-labeled calls is spot-checked: target zero prospect-shaped questions terminating at the front door.