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:
- Release from Pool (existing) —
PATCH /portal/holding-pool/{id}/reschedule. Admin/owner. - Follow-Up —
POST /{id}/follow-up. Scheduler/triage/admin/owner. Setsprojects.hold_follow_up_date+hold_follow_up_note(migration 030, promoted viaSUPABASE_EXTRA_COLUMNS). Project stays in the pool; page bands it (Due / Scheduled / Waiting). Pipeline Shepherd rulehold_follow_up_duenotifies the scheduler role daily once the date arrives (queriesprojectsdirectly — the columns are NOT inprojects_enriched).{date: null}clears. - Convert to Desk Review —
POST /{id}/convert-to-desk-review. Admin/owner. For work completed without a reschedule: setsreview_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, stampsppdr_conversionmarker in portal_state, notifies scheduler to request post-production evidence (human step by design — no auto-RFI). - Close Out (stale) —
POST /{id}/close-out. Admin/owner, note required. Status → Closed (forward move, no backward gate) + permanentholding_pool_stale_closeoutmarker 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-duplicatesalready 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_confirmcalls_stamp_stale_closeout_lineageafter every project creation (server-side, covers wizard/triage/queue-approval paths uniformly): exact permit/address lookup among Closed projects with the marker →prior_stale_closeoutback-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).
Related
- trip-charge-closeout — the marker/portal_state pattern this reuses (and its opposite re-intake behavior: reactivate SAME project)
- ppdr-calendar-split — desk-review predicate the conversion feeds
- absow-stamp-clear-centralized — write-service invariant style