# Recurring Maintenance Buckets — Design **Status:** Backlog idea, refined via interview 2026-07-15. Not yet approved for implementation. ## Context For longer-term items that need doing "every so often" but don't each deserve their own due date (e.g. "clean the gutters," "check the water heater," "review the emergency kit") — a bucket holds a pool of such items, and every M days/weeks the system activates N of them as real, actionable tasks. Completing (or deferring) an active item returns it to the pool; the interview's own framing calls for "room for fanciness" in the selection. **Depends on:** the task-recurrence-and-detail-editing feature (already shipped) for its periodic-scheduler infrastructure, and optionally on [[doot-future-task-scheduling-ideas]] items 1 (availability) and 2 (labels) for scoring inputs, per the interview. ## Architecture Note: Different Shape Than Due-Date Recurrence The existing recurrence system (shipped 2026-07-14) creates a **new row per iteration** — each occurrence is a distinct, permanent history entry. That model fits a task that repeats indefinitely on its own schedule. A bucket is different: it's a **finite, fixed pool of items** that get **repeatedly activated and deactivated** — the item itself doesn't need a fresh row each cycle, it needs a state flip. So this feature reuses the *scheduler infrastructure* (a periodic background check, same pattern as `scheduler.RunRecurrenceCheck`) but not the *new-row-per-iteration* mechanism. This is the "new recurrence variant layered onto the existing system" from the interview — same periodic-check shape, different iteration semantics. ## Data Model ```sql CREATE TABLE maintenance_buckets ( id TEXT PRIMARY KEY, name TEXT NOT NULL, cycle_days INTEGER NOT NULL, -- M, expressed in days (a "weekly" bucket = 7, "every 3 weeks" = 21) pick_n INTEGER NOT NULL, -- N items activated per cycle last_cycle_at DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); ALTER TABLE native_tasks ADD COLUMN bucket_id TEXT DEFAULT ''; ALTER TABLE native_tasks ADD COLUMN bucket_state TEXT DEFAULT ''; -- '', 'dormant', 'active' ALTER TABLE native_tasks ADD COLUMN bucket_last_active_at DATETIME; -- drives staleness scoring ``` Bucket items are ordinary `native_tasks` rows (so they appear in existing lookups/UI naturally) with `bucket_id` set and `bucket_state` cycling between `'dormant'` (in the pool, no `due_date`, invisible to normal task views) and `'active'` (selected this cycle, has a computed `due_date`, shows up everywhere a normal task would). Per the interview: no fixed due dates on dormant items — the cycle length is the only timing concept. Exactly one bucket per item (`bucket_id` is a single value, not a list). ## Selection Mechanism Every `cycle_days`, a periodic check (extending the existing scheduler) for each bucket: 1. Finds all `'dormant'` items with that `bucket_id`. 2. Scores each — staleness (time since `bucket_last_active_at`; never-activated items score highest), task priority, and (if the item has a label/project with an availability budget, per [[doot-future-task-scheduling-ideas]] item 1) how well it fits current capacity. Exact scoring formula is an implementation-time decision, not fixed here — the interview confirmed "weighted/scored," not a specific weighting. 3. Selects the top `pick_n`, flips them to `'active'`, sets `due_date = now + cycle_days` (giving the full cycle window to complete it), sets `bucket_last_active_at = now`. 4. Updates `last_cycle_at` on the bucket. **Deferring** an active item (a new "Defer" action distinct from complete/reschedule): flips it back to `'dormant'` with `bucket_last_active_at` left at its prior value (so it's still relatively stale and likely to be reselected soon), then **immediately triggers a fresh selection** for that bucket to backfill the freed slot — matching the interview's "defer one and get a new selection" requirement. **Completing** an active item: flips it back to `'dormant'` too, but sets `bucket_last_active_at = now` (just done, so it naturally cools down in the staleness scoring before being picked again) — distinct from deferring, where nothing was actually accomplished. This distinction wasn't explicitly asked in the interview; it's a reasonable design choice to revisit if it doesn't match intuition in practice. ## API - `GET/POST /api/widget/buckets` — CRUD for buckets (name, cycle_days, pick_n). - `POST /api/widget/buckets/{id}/items` — add an existing or new task to a bucket's pool (sets `bucket_id`, `bucket_state = 'dormant'`). - `POST /api/widget/task/defer` — `{id}`, the new defer action (distinct from complete/reschedule) for active bucket items. - No dedicated "trigger selection" endpoint needed for normal operation (the periodic check handles it) — but the defer action needs to synchronously trigger a re-selection for its bucket, reusing the same selection logic as a library function, not a new job. ## UI Not designed in detail here — the interview didn't cover UI specifics for buckets. At minimum: a bucket needs a management view (create bucket, add/remove pool items, see cycle settings) and active bucket items need a visible "Defer" action alongside the existing Complete action, likely in the task-detail popup ([[doot-future-task-scheduling-ideas]] item 2's popup, or a bucket-specific variant). ## Testing Store-layer tests for the selection algorithm (given a pool with known staleness/priority values, confirm the expected N are selected), the defer-triggers-reselection flow, and complete-vs-defer's differing `bucket_last_active_at` handling. Scheduler test for the periodic per-bucket cycle check, mirroring the existing `AdvanceDueRecurringTasks` test style. ## Out of Scope - Exact scoring formula/weights — left as an implementation-time decision. - UI design. - Multi-bucket item membership (exactly one bucket per item, per the interview). - Any interaction with the linear-task-chains feature ([[doot-future-task-scheduling-ideas]] item 4) — buckets and chains are independent concepts, not designed to compose.