0109 — Voice Triage Front-Door Doctrine

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)

  1. 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.
  2. An explicit ask for a human transfers immediately. No deflection, no "let me see if I can help first."
  3. 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:

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

  1. Adopt the three doctrine points above as the voice front-door architecture — this ADR is their document of record.
  2. 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. handled survives 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).
  3. 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).
  4. Re-align the routing rubric and eval expectations to the doctrine in the same PR as the prompt change (F7).
  5. Re-adjudicate the ~4 rubric-biased replay units before acting on any model-swap decision (F8).
  6. 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.
  7. 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

Verification plan