Holding Pool Exit Lanes

Built: 2026-08-03 (session 68) — entirely-api 2811636 + b418b1a, entirely-portal 3bb8690 Asked by: Dustin — “release from pool” was the only action; some holds never reschedule, some have promised start dates, some finished without us.

The four lanes

Every held project now has four first-class exits from the pool page, each with notes, all logged to timeline + Comm Log + audit:

  1. Release from Pool (existing) — PATCH /portal/holding-pool/{id}/reschedule. Admin/owner.
  2. Follow-UpPOST /{id}/follow-up. Scheduler/triage/admin/owner. Sets projects.hold_follow_up_date + hold_follow_up_note (migration 030, promoted via SUPABASE_EXTRA_COLUMNS). Project stays in the pool; page bands it (Due / Scheduled / Waiting). Pipeline Shepherd rule hold_follow_up_due notifies the scheduler role daily once the date arrives (queries projects directly — the columns are NOT in projects_enriched). {date: null} clears.
  3. Convert to Desk ReviewPOST /{id}/convert-to-desk-review. Admin/owner. For work completed without a reschedule: sets review_type → Desk Review (the load-bearing move — 2x post-production billing, branch-owned cert routing, PPDR/field calendar split all key off it), releases the hold, stamps ppdr_conversion marker in portal_state, notifies scheduler to request post-production evidence (human step by design — no auto-RFI).
  4. Close Out (stale)POST /{id}/close-out. Admin/owner, note required. Status → Closed (forward move, no backward gate) + permanent holding_pool_stale_closeout marker in portal_state (days_held, hold_reason, note, aged_90_plus). 90 days is a highlight, not a gate (Dustin’s call) — the pool shows a “Stale 90d+” chip but any hold can be closed.

Clean break + paper trail (close-out re-intake)

  • Intake check-duplicates already excluded Closed projects from blocking matches, so a resubmission at the address opens as a NEW project for free.
  • New: dropped terminal matches carrying the stale marker return as informational stale_closeouts — the wizard shows a non-blocking history banner.
  • New: execute_confirm calls _stamp_stale_closeout_lineage after every project creation (server-side, covers wizard/triage/queue-approval paths uniformly): exact permit/address lookup among Closed projects with the marker → prior_stale_closeout back-pointer in the new project’s portal_state + Comm Log entry on the old project.

Shared invariant

_HOLD_EXIT_CLEAR in routes/portal_holding_pool.py — every pool exit (release, close-out, convert) clears on_hold, hold_since, hold_reason, AND both follow-up fields, or released projects would keep re-firing the Shepherd follow-up rule. Pinned by tests/test_holding_pool_actions.py (13 tests).