0060 — Stale-lead call list eligibility is the human phone layer, not Clara's bot gates

Context

PropFlow ships a weekly stale-lead call list — a prioritized email to a property's leasing contact telling a human PM who to phone (src/lib/domain/leasing/stale-lead-digest/). It ships disabled behind three independent gates: no vercel.json cron entry, a global env arm (LEASING_DIGEST_AUTONOMOUS_SENDING=armed), and a per-property flag (Property.autonomousLeasingDigestEnabled). The requirement came out of the 2026-06-15 cofounder standup: PMs assume "the AI did its job" and stop making the closing calls, and occupancy is trending down — so the human still needs a ranked, contextual call sheet for the leads Clara's automation can't close on its own.

This list sits downstream of Clara's automated cadence, and that boundary is the whole point. Clara's only automated touch for a prospect is the first-reply follow-up (first-reply-followup.ts): she replies on inquiry (claraEngagedAt), and ~48h later — if the lead hasn't answered and is still at INQUIRY — sends one more nudge. That path is gated to online-listing sources (WEBSITE/ZILLOW/APARTMENTS_COM) and deduped by firstReplyFollowUpSentAt. After that single nudge, automated outreach stops. The call list's 7-day recency floor is deliberately the handoff line: days 0–7 are Clara's automated window; days 7–30 are the human's.

Three things are unresolved:

  1. The eligibility principle is only an inline comment. On 2026-06-17 we decided the human list is source-agnostic and must not inherit Clara's automated-texting gates (isStageEligible, firstReplyFollowUpSentAt). Those gates answer "when may a bot text?" — a different question from "who should a human call?". That decision lives only as a code comment in build-call-list.ts (Cohort C). A future contributor can silently re-couple them.
  2. No phone-required gate. buildStaleLeadCallList includes leads with no resolvable phone; the email renders a "No phone on file" row with no call icon. On a call list, a lead you cannot dial is noise.
  3. No consent posture is written down. A lead who texted STOP (SmsConsentRecord.revokedAt) still appears with a tel: link, and there is no doNotCall/doNotContact concept anywhere in the codebase — the only opt-out signal that exists is SMS-keyed.

Decision

The stale-lead call list is the human phone layer. Its eligibility is its own — defined by cohort + recency + callability, and deliberately decoupled from the gates that govern when Clara's automation may text a lead.

A lead is eligible for the call list when all hold:

  1. Cohort membership (deduped, hottest reason wins: A → C → B):
    • A — toured, not applied: a completed (past), non-cancelled tour and no application.
    • C — ghosted after first reply: Clara engaged (claraEngagedAt), no tour, not applied, then went quiet.
    • B — inquired, not toured: has a prospect row, no non-cancelled tour, pre-terminal stage.
  2. Recency window: last contact is between minDaysSinceContact (default 7) and maxDaysSinceContact (default 30) days ago.
  3. Not terminal: stage is not applied/approved/lease_signed/rejected/not_interested, and no application is detected (appliedAt/signedAt/af.rentalApplicationId).
  4. Callable: the lead has a dialable phone number. A lead with no resolvable phone is not eligible — you cannot call them. (They remain visible on the /leasing/prospects pipeline; they are simply not on a phone list.)
  5. Not suppressed: the lead is not on a genuine do-not-contact list.

Three rules a future change must not violate:

This ADR proposes no new persisted entities. suppressPersonIds is a function parameter, not an entity, so the entity-classification subsection is omitted.

Consequences

Alternatives considered