summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/superpowers/specs/2026-07-15-linear-task-chains-design.md63
-rw-r--r--docs/superpowers/specs/2026-07-15-recurring-maintenance-buckets-design.md70
-rw-r--r--docs/superpowers/specs/2026-07-15-task-budgets-and-availability-design.md64
-rw-r--r--docs/superpowers/specs/2026-07-15-task-labels-and-projects-design.md69
4 files changed, 266 insertions, 0 deletions
diff --git a/docs/superpowers/specs/2026-07-15-linear-task-chains-design.md b/docs/superpowers/specs/2026-07-15-linear-task-chains-design.md
new file mode 100644
index 0000000..a66d828
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-15-linear-task-chains-design.md
@@ -0,0 +1,63 @@
+# Linear Task Chains with WIP Limits — Design
+
+**Status:** Backlog idea, refined via interview 2026-07-15. Not yet approved for implementation.
+
+## Context
+
+A chain is a fixed, ordered sequence of tasks where only one is ever actionable at a time — the next task appears only once its predecessor is finished. Use cases per the interview: training progressions, goal-oriented projects, test/cert prep. This is a curriculum/checklist model, not a general dependency graph.
+
+**Depends on:** [[doot-future-task-scheduling-ideas]] item 2 (task labels/projects) — per the interview, a chain is "effectively a project with strict sequential unlocking," reusing the Projects concept rather than inventing a parallel grouping mechanism.
+
+## Data Model
+
+```sql
+CREATE TABLE task_chains (
+ id TEXT PRIMARY KEY,
+ project_id TEXT NOT NULL, -- one project per chain (see labels-and-projects spec)
+ status TEXT DEFAULT 'active', -- 'active', 'paused', 'abandoned', 'completed'
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+);
+
+ALTER TABLE native_tasks ADD COLUMN chain_id TEXT DEFAULT '';
+ALTER TABLE native_tasks ADD COLUMN chain_position INTEGER DEFAULT 0; -- 0-indexed order within the chain
+ALTER TABLE native_tasks ADD COLUMN chain_unlocked BOOLEAN DEFAULT 0; -- true only for the single active task
+```
+
+WIP limit of exactly 1 (per the interview, not configurable): at most one `native_tasks` row per `chain_id` ever has `chain_unlocked = true`. Locked tasks (`chain_unlocked = false`) have no `due_date` — same "no date until it's actionable" convention as the maintenance-buckets spec's dormant items.
+
+Strictly linear, no branching (per the interview): `chain_position` is a simple integer ordering, and advancement always moves to `position + 1`.
+
+## Advancement Mechanism
+
+Completing the currently-unlocked task (via the existing `CompleteNativeTask` path) checks `chain_id`: if set, it finds the task at `chain_position + 1` in the same chain and flips it from locked to unlocked, setting its `due_date` at that point (e.g. `due_date = now`, since a chain task becomes due the moment it's unlocked — there's no cycle-length concept here the way buckets have one). If there is no `chain_position + 1` (the completed task was the last), the chain's `status` flips to `'completed'` instead — chains always terminate, per the interview, no loop-back.
+
+## Visibility
+
+Per the interview: **hidden in timelines, visible in the tasks list**.
+- `BuildTimeline`/`GetNativeTasksByDateRange`/the widget's `/api/widget` response exclude any task with `chain_id != '' AND chain_unlocked = 0` — locked tasks have no due date and aren't schedule-relevant, so they don't appear in any day-based view (web Today/Tomorrow, widget grid).
+- A separate, new "chain view" (`GET /api/widget/chains/{id}`) lists every task in the chain in order, locked and unlocked, so the full checklist is browsable — this is the "visible in the tasks list" requirement, met via a dedicated surface rather than the normal timeline.
+
+## Pause / Abandon
+
+Per the interview, a chain can be paused or abandoned partway through:
+- `'paused'`: the currently-unlocked task (if any) stays visible/actionable, but completing it does **not** auto-advance while paused — the chain must be explicitly resumed (`status` back to `'active'`) for the next task to unlock. This lets you finish what's in front of you without accidentally kicking off the next step of a chain you're stepping away from.
+- `'abandoned'`: a terminal state (like `'completed'` for bookkeeping purposes, but distinguishable in queries/reporting as "didn't finish").
+
+## API
+
+- `POST /api/widget/chains` — create a chain: name (creates its backing project automatically) + an ordered list of task titles/descriptions to seed every position. Position 0 is created unlocked with `due_date = now`; the rest are created locked with no due date.
+- `POST /api/widget/chains/{id}/pause`, `/resume`, `/abandon`.
+- `GET /api/widget/chains/{id}` — full ordered list (locked + unlocked) for the checklist view.
+- No new completion endpoint — `POST /api/widget/task/update`'s sibling `HandleWidgetComplete`/`CompleteNativeTask` gets extended to check `chain_id` and advance, the same way it already checks `recurrence_series_id` and creates a next iteration.
+
+## Testing
+
+Store-layer tests for: creating a chain seeds positions correctly (only position 0 unlocked/dated); completing an unlocked task advances to the next position; completing the last position marks the chain completed instead of advancing; pausing prevents auto-advance on completion, resuming re-enables it; locked tasks are excluded from date-range queries. Handler tests for the new endpoints mirroring existing widget-handler test style.
+
+## Out of Scope
+
+- Branching/non-linear chains, reordering after creation — strictly linear and fixed, per the interview.
+- Configurable WIP limits above 1 — always exactly 1, per the interview.
+- Looping chains for ongoing practice — always terminates, per the interview.
+- Interaction with maintenance buckets ([[doot-future-task-scheduling-ideas]] item 3) — independent concepts, not designed to compose.
+- Web UI specifics for the chain/checklist view.
diff --git a/docs/superpowers/specs/2026-07-15-recurring-maintenance-buckets-design.md b/docs/superpowers/specs/2026-07-15-recurring-maintenance-buckets-design.md
new file mode 100644
index 0000000..5072643
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-15-recurring-maintenance-buckets-design.md
@@ -0,0 +1,70 @@
+# 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.
diff --git a/docs/superpowers/specs/2026-07-15-task-budgets-and-availability-design.md b/docs/superpowers/specs/2026-07-15-task-budgets-and-availability-design.md
new file mode 100644
index 0000000..923ef54
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-15-task-budgets-and-availability-design.md
@@ -0,0 +1,64 @@
+# Task Budgets and Availability Times — Design
+
+**Status:** Backlog idea, refined via interview 2026-07-15. Not yet approved for implementation.
+
+## Context
+
+The goal is to know whether what's on your plate actually fits in the time you realistically have, without doot trying to auto-schedule your day for you. This is explicitly a **visibility** feature, not a **scheduling** one, per the interview.
+
+**Depends on:** [[doot-future-task-scheduling-ideas]] item 2 (task labels/projects) — availability tracking is opt-in per project/label, so that feature needs to exist first.
+
+## Scope
+
+doot-native tasks only. Opt-in: a project or label is marked as "budget-tracked," and only tasks under a tracked project/label count against any budget calculation. Untracked tasks are invisible to this whole feature — the default experience is unchanged.
+
+## Data Model
+
+**Availability** — a manual recurring weekly template, reduced by real calendar events:
+```sql
+CREATE TABLE availability_blocks (
+ id TEXT PRIMARY KEY,
+ weekday INTEGER NOT NULL, -- 0-6, Sun-Sat
+ start_time TEXT NOT NULL, -- "18:00"
+ end_time TEXT NOT NULL, -- "20:00"
+ label TEXT DEFAULT '' -- optional, e.g. "evening focus time"
+);
+```
+At computation time (see below), a day's available minutes = sum of that weekday's `availability_blocks` minus any overlapping existing calendar events (already fetched via `BuildTimeline` — no new calendar integration needed, just a subtraction pass over blocks the timeline already has).
+
+**Budget** — both per-task estimate and a period pool, per the interview:
+- `native_tasks` gets `estimated_minutes INTEGER DEFAULT 0` — user-entered or inferred (see below).
+- A period pool isn't a separate stored number — it's *computed* as the sum of `availability_blocks` for the period (today, this week), and compared against the sum of `estimated_minutes` for all budget-tracked, incomplete, due-in-period tasks. No new "pool" table; the pool is availability itself.
+
+**Estimate inference ("learned from history"):** requires tracking actual completion duration, which doot doesn't currently record (tasks have `due_date`/`completed_at` but no "started working on this" timestamp). The simplest version that doesn't require a new "time tracking" UI: when a task completes, if it had no `estimated_minutes` set, backfill nothing (no signal to learn from) — but if a *label* or *project* accumulates several tasks with user-entered `estimated_minutes`, use the average of the same-label/project tasks' user-entered estimates as the default for a *new* task under that label/project, rather than inferring from actual elapsed time. This sidesteps needing real time-tracking infrastructure while still delivering "the system learns typical durations for this kind of task." True elapsed-time-based learning is a larger, separate feature not designed here.
+
+## Computation and Surfacing
+
+No new background job — this is computed on read, at the same points `BuildTimeline`/`HandleWidgetGet` already run:
+1. For "today" and "this week," sum `estimated_minutes` across budget-tracked incomplete tasks due in that window.
+2. Sum available minutes from `availability_blocks` for the matching weekdays, minus overlapping calendar events already in the timeline.
+3. If load > availability, surface a flag — not a hard block, not a reorder. Likely surfaced as a small indicator in the web timeline (e.g. "6.5h scheduled / 4h available today") and, if there's room, a small badge on the widget's TODAY header. Exact placement is a UI-design decision for the implementation plan, not fixed here.
+
+Per the interview: **no auto-scheduling, no auto-reprioritization** — the flag is purely informational.
+
+## API
+
+- `GET/POST /api/widget/availability` — CRUD for `availability_blocks`.
+- `POST /api/widget/task/estimate` — `{id, estimated_minutes}`.
+- The overflow flag itself doesn't need a dedicated endpoint — it's computed as part of the existing `GET /api/widget` and the web timeline's existing data-building path, added as a new field on the response (e.g. `budget_status: {scheduled_minutes, available_minutes}` alongside `items`).
+
+## Recurrence Interaction
+
+`estimated_minutes` is task-level, not series-level — the interview didn't ask about this directly, but since it's meant to reflect "how long does *this kind* of task take," and `CreateNextIteration` already copies most fields forward, the natural default is to carry it forward like content/description/project/labels. Re-estimating a specific occurrence would just mean editing that occurrence's value directly (no override mechanism needed beyond the normal edit UI).
+
+## Testing
+
+Store-layer tests for availability CRUD and the availability-minus-calendar-events computation (a pure function, easy to unit test with fixture events). Handler tests for the new endpoints and for the `budget_status` field appearing correctly in `/api/widget`'s response only when budget-tracked tasks exist.
+
+## Out of Scope
+
+- Auto-scheduling tasks into specific time slots.
+- Auto-reprioritization or auto-deferral on overflow.
+- Real elapsed-time tracking (start/stop timers) — estimate inference uses same-label/project averaging of user-entered values instead.
+- Any UI beyond a basic indicator — exact visual treatment is an implementation-time decision.
+- Non-project/label-scoped (global) budgets — everything is opt-in per the interview.
diff --git a/docs/superpowers/specs/2026-07-15-task-labels-and-projects-design.md b/docs/superpowers/specs/2026-07-15-task-labels-and-projects-design.md
new file mode 100644
index 0000000..4144d5b
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-15-task-labels-and-projects-design.md
@@ -0,0 +1,69 @@
+# Task Labels and Projects — Design
+
+**Status:** Backlog idea, refined via interview 2026-07-15. Not yet approved for implementation.
+
+## Context
+
+doot-native tasks have had a `Labels []string` field on `models.Task` (`internal/models/types.go`) since early in the project — it's fully wired at the store layer (persisted as a JSON column, scanned back out by `scanNativeTasks`) but never surfaced in any UI, web or widget. There's no editing, no display, no filtering.
+
+There's also no real "project" concept for doot-native tasks today. `Task.ProjectID`/`ProjectName` exist on the struct and `ProjectName` is a real column in `native_tasks`, but a repo-wide search turns up **zero** places that ever set `ProjectName` to a non-empty value when constructing a native task — it's vestigial, always empty in practice. This is likely a leftover from the Todoist-sync era (Todoist tasks did have real project names; the field just never got repurposed after native tasks replaced Todoist in commit `945c345`). So this feature isn't "finish wiring up projects" — it's building projects from scratch, alongside finally wiring up labels.
+
+This is a foundational feature: [[doot-future-task-scheduling-ideas]] items 1 (budgets/availability) and 4 (task chains) both reference "projects" as a dependency. Building this first unblocks both.
+
+## Scope
+
+doot-native tasks only, matching every other feature built this session. Trello/Google Tasks cards keep whatever labeling concept they already have server-side (Trello lists, etc.) — this feature does not touch them.
+
+## Data Model
+
+**Labels** — reuse the existing field, no schema change needed:
+- `Task.Labels []string`, already a JSON column on `native_tasks`.
+- Free-form text tags, many-to-many with tasks (a task can have any number of labels).
+- User-assignable color per label. Since labels are currently just strings with no identity beyond their text, color needs a home: a new small `labels` table (`name TEXT PRIMARY KEY, color TEXT`) mapping label text → color hex. A label typed on a task that doesn't yet exist in this table gets a default color (or prompts for one) the first time it's colored.
+
+**Projects** — new, lightweight:
+```sql
+CREATE TABLE projects (
+ id TEXT PRIMARY KEY,
+ name TEXT NOT NULL,
+ color TEXT NOT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ archived BOOLEAN DEFAULT 0
+);
+```
+- `native_tasks` gets a new `project_id TEXT DEFAULT ''` column (a real FK-shaped reference, not the vestigial `project_name` string column — that column stays as dead weight for now since dropping SQLite columns needs a table-rebuild migration, out of scope for this feature; it just stops being read).
+- Exactly one project per task (`project_id` is a single string, empty = unassigned), per the interview answer. Labels handle the cross-cutting many-to-many need instead.
+- A project is not itself an atom/task — it's pure metadata (name + color) that a task points to.
+
+## Recurrence Interaction
+
+Both `project_id` and `Labels` are **series-level metadata**: `CreateNextIteration` (in `internal/store/native_tasks.go`) already copies `content`, `description`, `project_name`, `priority`, and `labels` onto every new occurrence — `project_id` joins that same copy-forward list, no new mechanism needed. This matches the interview's "inherit automatically" answer exactly.
+
+## API
+
+New endpoints, doot-only, matching the existing `/api/widget/task/*` pattern:
+- `GET /api/widget/projects` — list all non-archived projects (id, name, color), for populating a picker.
+- `POST /api/widget/projects` — create a project (name, color).
+- `POST /api/widget/task/project` — `{id, project_id}`, sets or clears a task's project.
+- `POST /api/widget/task/labels` — `{id, labels: []string}`, replaces a task's label set (matches `UpdateNativeTask`'s replace-whole-value style rather than an add/remove-single-label API).
+- Labels don't need their own CRUD endpoint in the same way projects do — a label is just a string until someone assigns it a color, so `POST /api/widget/task/labels` implicitly "creates" any new label text. A separate `POST /api/widget/labels/color` — `{name, color}` — sets a label's color.
+
+## UI
+
+**Widget (Android):** per the interview, minimal — a small color mark on the time-grid row (project color takes priority for the row's accent; if labels need visual representation too, a second smaller dot, but start with just project color to avoid clutter). No filtering UI on the widget itself.
+
+**Task detail popup (`TaskDetailActivity`):** full display and editing — a project picker (chip showing current project, tap to open a picker/create-new dialog) and a label editor (chip-entry, similar in spirit to the recurrence weekday chips but free-text with autocomplete against existing label names), both using the color-coding.
+
+**Web:** out of scope for this spec's UI section — the interview didn't ask about web display specifically; assume parity with the widget's popup-level editing is desirable but not designed here.
+
+## Testing
+
+Standard TDD per this project's convention: store-layer tests for `CreateProject`, `GetProjects`, `SetTaskProject`, `SetTaskLabels`, `SetLabelColor`; handler tests for the new endpoints (mirroring `widget_test.go`'s style); a `CreateNextIteration` test confirming `project_id` and `Labels` both carry forward to the next occurrence.
+
+## Out of Scope
+
+- Project hierarchies (sub-projects, nesting) — flat list only.
+- Multi-project tasks — exactly one project per task, per the interview.
+- Archived-project handling in the UI beyond hiding them from the picker (no dedicated archive-management screen designed here).
+- Web UI specifics (noted above).
+- Migrating/backfilling the vestigial `project_name` column — it's simply superseded, not cleaned up, in this spec.