0100 — PM-decided month-to-month conversion (the no-renewal path)

Context

A renewal cycle has always had three PM-visible endings: the tenant renews, the tenant leaves, or the tenant transfers. There is a fourth real-world ending with no product path today: the PM decides not to offer a renewal and instead lets the tenancy continue month-to-month.

Where this bites, concretely (Camellia, 2026-07):

Founder rulings that bind this design (2026-07-15/16/17):

  1. Month-to-month is a settled outcome with its own yellow designation on the board — never a top-level bucket, never an action item once decided.
  2. Outcomes name the real disposition (Renewed / Month-to-month / Did Not Renew / Transferred) — never the mechanism ("handled in AppFolio", "holdover scan", etc.). Provenance goes in the activity log.
  3. No tenant comms on the MTM decision. Recording the decision sends nothing. (The ADR-0046 reminder-ladder copy already tells tenants the MTM-with-premium consequence during outreach; that is the only place the tenant hears about MTM, and it fires only while an offer is live.)
  4. Minimize human touches: one decision, one click, row settles. No confirmation lanes, no bookkeeping steps.
  5. Colorado legal guardrails (§38-12-701 60-day notice before a rent increase, §38-12-702 one increase per 12 months, HB24-1098 for-cause non-renewal) are documented, not enforced, in v1 — explicit founder deferral 2026-07-16.

Decision

One new PM decision — "Move to month-to-month" — captured in the revived review drawer, recorded as a first-class renewal outcome, executed in AppFolio by the existing ADR-0046 holdover converter at lease end. Five coordinated pieces, each naming what it reuses:

1. Decision capture: the review drawer, two options

The Requires-action rows whose "What's needed" is a decisionflagged (playbook says a PM approves) and held_review (an eligibility-engine hold, e.g. balance) — render their label as an in-place button that opens the revived RenewalDrawer (side panel; #3878 removed its caller, not the component). The drawer shows: tenant · unit, lease end, current → proposed rent with delta/%, the MTM figure with the property's premium (computeMtmFallback + renewalPolicy.mtmPremium), the specific hold reason in plain English, and Clara's recommendation. Two primary actions:

Chore rows (add_contact) and take-over rows (escalated, prepare_failed) are out of scope here — they have no one-click decision (see ADR-0046 / task-20 escalation work).

2. Recording: a first-class outcome, settled immediately

convert_mtm writes a PmRenewalDecision (who/when/reason) to the saga and resolves the cycle at decision time, not at lapse:

3. Workflow: wrap up, don't abandon

If the renewal workflow is RUNNING (offer prepared, outreach queued), the decision must stop it the same way an external renewal does — gracefully. Reuse the ADR-0091 addendum machinery verbatim in shape: a synchronous Temporal Update (pmDecision, sibling of externalResolution, gated by workflow.patched('pm_mtm_decision_v1')) that:

No running workflow (pre-saga or already terminal): the decision records directly; nothing to wrap up.

4. Execution in AppFolio: the ADR-0046 converter, now with two triggers

Nothing converts in AppFolio at decision time — the lease is still live until its end date, and AppFolio stays source of truth (ADR-0039). At lease end the existing daily-scan holdover branch performs the conversion (end-cap PATCH, recurring rent = proposed rent + mtmPremium, next-month charge), with two changes:

The autonomous no-decision lapse path is unchanged. Both paths end in the same HOLDOVER terminal state; decisionSource is the only difference.

5. What v1 explicitly does not do

Consequences

Positive. The Review lane finally closes its loop — every decision row lands somewhere a decision can actually be made, and the second-most-common real ending (no renewal, tenant stays) becomes one click instead of "do nothing and let the row rot." The autonomous and PM-decided MTM paths share one converter, one terminal state, one outcome vocabulary — no drift between "the scan converted them" and "I converted them." Wrap-up semantics are uniform across every way a cycle ends early (external actor, PM decision): one Update pattern, one conflict rule, no dropped tenant decisions.

Negative / accepted. The premium may legally need to wait out the 60-day notice window that v1 does not track — accepted by explicit founder deferral; the phase-2 note above is the mitigation path. A PM can choose MTM while a tenant is mid-conversation with Clara about renewing; the conflict check only blocks on received decisions (signed / NTV), not on chatter — consistent with the 2026-07-17 deletion of the conversation-recency guard.

Testing gate. Per the standing founder rule, the end-to-end chain ships with Willows regression-harness coverage in the same PR: decision → saga settles MTM (yellow, board leaves Requires action) → workflow wrap-up cancels outreach + voids the offer → converter authorizes on decisionSource: 'pm' → conflict path (tenant already signed) surfaces and changes nothing.

Rollout

  1. Land drawer + decision recording + wrap-up Update behind the normal PR flow (no flag — the action is PM-initiated and sends nothing).
  2. Converter gate change rides the next daily-scan deploy; first PM-decided conversion at a real lapse gets a manual post-conversion verification against AppFolio (rent, charge, end date) before the path is called proven — same canary discipline ADR-0046 prescribed.

Addendum — Outreach exhaustion: quiet wrap-up (2026-07-17)

Ruling (founder-approved, minimal change). When the full outreach ladder — initial offer + reminders + the 30-day and 15-day expiration nudges — runs and the tenant never responds, the renewal workflow no longer treats that as a PM escalation. Outreach running out is a non-event for the PM: the tenancy simply rolls month-to-month. So the terminal exhaustion branch now exits quietly instead of raising a "Review" row.

What was removed (terminal exhaustion branch only). The exhaustion branch no longer calls escalateToPM (no PM email, no PM SMS) and no longer records an escalated outcome. It now exits exactly the way the external-resolution and PM-decided wrap-ups do: it cancels pending outreach/timers, sets phase='done', and records no terminal outcome. Because outcome stays null, the board projection falls to the phase rung — phase='done', outcome=null → PREPARED ("Offer sent", Upcoming) — so the row keeps its last outreach state and never projects to needs_you. escalateToPM itself is untouched and still serves its other callers (prepare_failed and the awaiting-countersign timeout).

What already covers the rest. Nothing else changes. At lease lapse the ADR-0046 holdover pipeline detects the holdover, end-caps the lease in AppFolio, closes the saga to HOLDOVER (the board settles yellow "Month-to-month"), sends the D1 office heads-up (notifyHoldoverToOffice). It makes no next-month re-offer — that ADR-0046 D2 step was superseded 2026-09-05 (Fede: "i dont want to offer anything, just roll mtm"); month-to-month is the end state. The converter's proof-of-warning gate is satisfied by the 15-day nudge receipt (mtmWarning-15d) — which is exactly why the nudges are left in place and untouched by this change.

Replay gating. The quiet exit is gated behind workflow.patched('exhausted_quiet_wrapup_v1'). In-flight pre-patch histories return false and keep the old escalation behavior, so a workflow that recorded no patch marker replays deterministically.

Testing gate. Ships in the same PR with the exhaustion-exit workflow-body test (quiet exit, pre-patch escalation preserved, and tenant-sign / NTV / intent-to-vacate / external-resolution all winning over the quiet exit) and a Willows-harness permutation matrix (X-15) walking the full non-responder → holdover chain across the exhaustion exit, the converter gates, and the board projection.