summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-07-17 22:22:37 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-07-17 22:22:37 +0000
commitb007fee8fb5b39a5f9b369c59af71ac9e795ceaf (patch)
treea5b999b8f4c23a52ae9249675753a92a77993008 /docs
parent70e6dd75130e70f2db83096c23eaa75326b183a2 (diff)
Implement linear task chains and recurring maintenance buckets
Backend, web timeline, and Android widget wiring for the last two unimplemented items from doot-future-task-scheduling-ideas. Chains: task_chains table + chain_id/chain_position/chain_unlocked on native_tasks (migration 026), WIP-limit-1 advancement hooked into CompleteNativeTask, locked tasks excluded from all date-based queries, 5 new /api/widget/chains* endpoints, an N/M position badge on web and Android widget rows. Buckets: maintenance_buckets table + bucket_id/bucket_state/ bucket_last_active_at on native_tasks (migration 027), staleness-then-priority selection scoring, a new RunBucketCycleCheck scheduler loop, 5 new endpoints including the distinct Defer action, a Defer button on web and Android widget rows. Also corrected stale "not yet approved" status headers on the two already-shipped specs this work depended on (labels/projects, budgets/ availability) -- their headers were never updated after implementation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
Diffstat (limited to 'docs')
-rw-r--r--docs/superpowers/plans/2026-07-17-linear-task-chains.md98
-rw-r--r--docs/superpowers/plans/2026-07-17-recurring-maintenance-buckets.md106
-rw-r--r--docs/superpowers/specs/2026-07-15-linear-task-chains-design.md2
-rw-r--r--docs/superpowers/specs/2026-07-15-recurring-maintenance-buckets-design.md2
-rw-r--r--docs/superpowers/specs/2026-07-15-task-budgets-and-availability-design.md2
-rw-r--r--docs/superpowers/specs/2026-07-15-task-labels-and-projects-design.md2
6 files changed, 208 insertions, 4 deletions
diff --git a/docs/superpowers/plans/2026-07-17-linear-task-chains.md b/docs/superpowers/plans/2026-07-17-linear-task-chains.md
new file mode 100644
index 0000000..ebb576a
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-17-linear-task-chains.md
@@ -0,0 +1,98 @@
+# Linear Task Chains — Implementation Plan
+
+> Implements `docs/superpowers/specs/2026-07-15-linear-task-chains-design.md`.
+> Backend (Go), web timeline indicator, and Android widget chain view — full stack, per user decision 2026-07-17.
+
+**Goal:** doot-native tasks can belong to a `task_chains` sequence with a WIP limit of exactly 1 — only one task in the chain is ever unlocked/actionable, and completing it unlocks the next. Locked tasks are hidden from date-based views; a dedicated checklist view shows the whole chain.
+
+## Global Constraints
+
+- Doot-native tasks only (matches every prior feature this session). Chains reuse the `projects` table (one project per chain) rather than a parallel grouping mechanism.
+- Strictly linear, fixed at creation: no branching, no reordering, WIP limit not configurable (always 1), no looping.
+- Locked tasks have no `due_date` and are excluded from `GetNativeTasksByDateRange`/`GetOverdueNativeTasks`/`GetUndatedNativeTasks`/the widget's `/api/widget` response.
+
+---
+
+### Task 1: Migration
+
+**File:** `migrations/026_task_chains.sql`
+
+```sql
+CREATE TABLE task_chains (
+ id TEXT PRIMARY KEY,
+ project_id TEXT NOT NULL,
+ status TEXT NOT NULL 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;
+ALTER TABLE native_tasks ADD COLUMN chain_unlocked BOOLEAN DEFAULT 0;
+
+CREATE INDEX IF NOT EXISTS idx_native_tasks_chain ON native_tasks(chain_id);
+```
+
+Also add the same tables/columns to the store test-schema helpers (`internal/store/native_tasks_test.go`'s inline `CREATE TABLE native_tasks`, plus a new `task_chains` table) and `internal/store/sqlite_test.go`'s `native_tasks` schema.
+
+### Task 2: Models
+
+**File:** `internal/models/types.go`
+
+- Add to `Task`: `ChainID string`, `ChainPosition int`, `ChainUnlocked bool` (json `chain_id,omitempty` / `chain_position,omitempty` / `chain_unlocked,omitempty`).
+- New `Chain` struct: `ID, ProjectID, Status string`, `CreatedAt time.Time`.
+
+### Task 3: Store — chain CRUD and advancement
+
+**File:** `internal/store/chains.go` (new)
+
+- `CreateChain(name string, taskTitles []string) (*models.Chain, error)` — creates a backing project (reuse `CreateProject`), inserts a `task_chains` row, then inserts one `native_tasks` row per title: position 0 gets `chain_unlocked=1, due_date=now`; the rest get `chain_unlocked=0, due_date=NULL`. All in one transaction.
+- `GetChain(id string) (*models.Chain, error)` — `ErrNativeTaskNotFound` if missing (reuse the shared sentinel).
+- `GetChainTasks(chainID string) ([]models.Task, error)` — every task in the chain, ordered by `chain_position`, locked and unlocked both (backs the checklist view; the "visible in the tasks list" requirement from the spec).
+- `SetChainStatus(id, status string) error` — used by pause/resume/abandon.
+- `advanceChain(tx, chainID string, completedPosition int) error` — unexported, called from `CompleteNativeTask`: if chain status is `paused`, no-op (per spec: paused chains don't auto-advance). Otherwise find the row at `chain_position = completedPosition + 1`; if found, set it `chain_unlocked=1, due_date=now`; if not found (completed task was last), set the chain's `status='completed'`.
+
+**File:** `internal/store/native_tasks.go`
+
+- Add `chain_id, chain_position, chain_unlocked` to all 5 SELECT queries and `scanNativeTasks`.
+- `GetNativeTasksByDateRange`, `GetOverdueNativeTasks`, `GetUndatedNativeTasks`: add `AND (chain_id = '' OR chain_unlocked = 1)` to the WHERE clause — locked chain tasks never appear in date-based views regardless of (null) due date.
+- `CompleteNativeTask`: after the existing recurrence-advancement block, add: if `task.ChainID != ""`, call `s.advanceChain(task.ChainID, task.ChainPosition)`.
+- `CreateNativeTask`: no chain fields needed (chain creation goes through `CreateChain`, not this path).
+
+**Tests:** `internal/store/chains_test.go` (new) — creating a chain seeds positions correctly (only position 0 unlocked/dated, rest locked/undated); completing an unlocked task advances the next position (unlocked + due_date set); completing the last position marks the chain `completed` instead of advancing; pausing prevents auto-advance on completion; resuming re-enables advancement on the *next* completion (not retroactively); `GetNativeTasksByDateRange`/`GetOverdueNativeTasks`/`GetUndatedNativeTasks` all exclude locked tasks.
+
+### Task 4: HTTP handlers
+
+**File:** `internal/handlers/widget.go`
+
+- `HandleWidgetChainsCreate` (`POST /api/widget/chains`) — body `{name string, tasks []string}`; calls `store.CreateChain`; returns `{id string}`.
+- `HandleWidgetChainsPause` / `HandleWidgetChainsResume` / `HandleWidgetChainsAbandon` (`POST /api/widget/chains/{id}/pause|resume|abandon`) — call `SetChainStatus`; 404 via `ErrNativeTaskNotFound` if the chain id doesn't exist.
+- `HandleWidgetChainGet` (`GET /api/widget/chains/{id}`) — returns `{chain: Chain, tasks: []Task}` ordered by position, for the checklist view.
+
+**File:** `cmd/dashboard/main.go` — register the 5 routes under the existing `widgetAuth`-gated block, next to the availability routes.
+
+**Tests:** `internal/handlers/chains_test.go` (new) mirroring `widget_test.go`'s style — create, pause/resume/abandon (including 404 on unknown id), get.
+
+### Task 5: Web timeline wiring
+
+A task's chain membership is visible as a small "N/M" position badge (e.g. "3/12") next to the existing due-date/project chip on the web Today/Tomorrow timeline rows, same visual slot pattern as the budget "scheduled/available" badge from `2026-07-16-task-budgets-and-availability.md` Task 11. Locked tasks never reach the timeline template (excluded at the store layer), so no "locked" state needs rendering there — only the currently-unlocked task ever shows, with its position.
+
+**Files:** the timeline template(s) already touched by the budget-indicator work (search for that commit's template diff — `6651dc1`) and the corresponding `TimelineItem`/Go template-data struct, adding `ChainPosition`/chain total count (compute total via `len(GetChainTasks)` or store it as `chain_total` if simpler — implementation-time call).
+
+No standalone web checklist page — per spec, "web UI specifics for the chain/checklist view" are out of scope; the Android widget gets the dedicated checklist surface (Task 6).
+
+### Task 6: Android widget — chain checklist view
+
+**Files:**
+- `android/app/src/main/java/org/terst/doot/widget/data/WidgetItem.kt` — add `chainId`, `chainPosition`, `chainUnlocked` (or equivalent) fields, mirroring how `budget_status` was added.
+- `android/app/src/main/java/org/terst/doot/widget/data/DataStore.kt` — new repository methods hitting `GET /api/widget/chains/{id}`, `POST /api/widget/chains/{id}/pause|resume|abandon`.
+- `android/app/src/main/java/org/terst/doot/widget/ui/WidgetRows.kt` — small chain-position badge on rows for tasks with `chainId` set (same slot as the budget badge).
+- New Compose screen/dialog for the chain checklist (locked + unlocked items in order) — reachable by tapping the chain badge. No automated Compose UI test harness in this project (established convention); verify by building and a manual on-device/emulator check.
+
+### Task 7: Docs
+
+- Flip this spec's status line from "Backlog idea... Not yet approved" to reflect implementation, once Tasks 1-6 are done and tests pass.
+- Update `.agent/worklog.md`.
+
+## Testing
+
+`go test ./...` after Tasks 1-4. Android: `./gradlew assembleDebug` after Task 6 (debug build only, per existing widget-build convention — do not build/publish the release APK without a separate explicit go-ahead, since that touches `/site/static.terst.org`).
diff --git a/docs/superpowers/plans/2026-07-17-recurring-maintenance-buckets.md b/docs/superpowers/plans/2026-07-17-recurring-maintenance-buckets.md
new file mode 100644
index 0000000..c78783e
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-17-recurring-maintenance-buckets.md
@@ -0,0 +1,106 @@
+# Recurring Maintenance Buckets — Implementation Plan
+
+> Implements `docs/superpowers/specs/2026-07-15-recurring-maintenance-buckets-design.md`.
+> Backend (Go), web wiring, and Android widget wiring — full stack, per user decision 2026-07-17.
+
+**Goal:** a bucket holds a pool of doot-native tasks that don't each deserve their own due date. Every `cycle_days`, the top `pick_n` (scored by staleness/priority/availability-fit) flip from dormant to active with a computed due date. Completing or deferring an active item returns it to the pool; deferring immediately triggers a backfill selection for that bucket.
+
+## Global Constraints
+
+- Doot-native tasks only. Exactly one bucket per item.
+- Reuses the periodic-scheduler shape from `internal/scheduler/recurrence.go`, not its new-row-per-iteration mechanism — bucket items are state-flipped in place, never duplicated.
+- Scoring formula is an implementation-time call (spec leaves it open) — implement staleness (days since `bucket_last_active_at`, nulls score as +infinity/highest) as primary key, task `priority` as tiebreaker. Availability-budget fit (item 1) is a nice-to-have third factor if `estimated_minutes`/budget-tracked project data is easily available at selection time; skip it if it adds real complexity rather than blocking on it.
+
+---
+
+### Task 1: Migration
+
+**File:** `migrations/027_maintenance_buckets.sql`
+
+```sql
+CREATE TABLE maintenance_buckets (
+ id TEXT PRIMARY KEY,
+ name TEXT NOT NULL,
+ cycle_days INTEGER NOT NULL,
+ pick_n INTEGER NOT NULL,
+ 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 '';
+ALTER TABLE native_tasks ADD COLUMN bucket_last_active_at DATETIME;
+
+CREATE INDEX IF NOT EXISTS idx_native_tasks_bucket ON native_tasks(bucket_id);
+```
+
+Add matching columns/table to the store test-schema helpers (`internal/store/native_tasks_test.go`, `internal/store/sqlite_test.go`), same as the chains plan's Task 1.
+
+### Task 2: Models
+
+**File:** `internal/models/types.go`
+
+- Add to `Task`: `BucketID string`, `BucketState string`, `BucketLastActiveAt *time.Time` (all `omitempty`).
+- New `MaintenanceBucket` struct: `ID, Name string`, `CycleDays, PickN int`, `LastCycleAt *time.Time`, `CreatedAt time.Time`.
+
+### Task 3: Store — bucket CRUD, selection, defer/complete hooks
+
+**File:** `internal/store/buckets.go` (new)
+
+- `CreateBucket(name string, cycleDays, pickN int) (*models.MaintenanceBucket, error)`.
+- `GetBuckets() ([]models.MaintenanceBucket, error)`.
+- `AddBucketItem(bucketID, taskID string) error` — sets `bucket_id`, `bucket_state='dormant'` on an existing task row (task must already exist — created via the normal `CreateNativeTask`/widget-add path first, matching "add an existing or new task" from the spec's API section: creating-then-adding covers both cases without a separate code path).
+- `RemoveBucketItem(taskID string) error` — clears `bucket_id`/`bucket_state`/`bucket_last_active_at`.
+- `selectBucketCycle(bucketID string, now time.Time) (int, error)` — unexported: loads dormant items for the bucket, scores (staleness DESC — NULL `bucket_last_active_at` first — then `priority` DESC as tiebreaker), flips the top `pick_n` to `active` with `due_date = now + cycle_days` and `bucket_last_active_at = now`, updates the bucket's `last_cycle_at = now`. Returns count activated.
+- `RunBucketCycles(now time.Time) (int, error)` — exported, scheduler entry point: for every bucket where `last_cycle_at IS NULL OR last_cycle_at <= now - cycle_days`, calls `selectBucketCycle`. Mirrors `AdvanceDueRecurringTasks`'s shape.
+- `DeferNativeTask(id string) error` — sets the task `bucket_state='dormant'`, due_date=NULL, leaves `bucket_last_active_at` untouched, then synchronously calls `selectBucketCycle` for its bucket to backfill the freed slot. Returns `ErrNativeTaskNotFound` if the task isn't an active bucket item.
+
+**File:** `internal/store/native_tasks.go`
+
+- Add `bucket_id, bucket_state, bucket_last_active_at` to all 5 SELECT queries and `scanNativeTasks`.
+- `GetNativeTasksByDateRange`/`GetOverdueNativeTasks`/`GetUndatedNativeTasks`: dormant bucket items (`bucket_state = 'dormant'`) have no due date already, so the existing `due_date IS NOT NULL` filters naturally exclude them from the dated queries — no WHERE-clause change needed there. `GetUndatedNativeTasks` (`due_date IS NULL`), however, would otherwise surface dormant bucket items as if they were ordinary undated tasks; add `AND bucket_state != 'dormant'` to its WHERE clause so the pool stays invisible until activated, per spec.
+- `CompleteNativeTask`: after the chain-advancement block (or recurrence block if chains plan hasn't landed yet — check `chain_id`/order doesn't matter, they're independent per both specs), add: if `task.BucketID != ""`, flip it back to `dormant` with `bucket_last_active_at = now` (distinct from Defer, which leaves the timestamp alone) and clear `due_date`.
+
+**Tests:** `internal/store/buckets_test.go` (new) — selection picks the top-N by staleness then priority; never-activated (`NULL` timestamp) items outrank ever-activated ones; `RunBucketCycles` respects `cycle_days` (no-op if not due yet); completing an active item returns it to dormant with `bucket_last_active_at = now`; deferring returns it to dormant with the *prior* timestamp and triggers a fresh selection that backfills the slot; dormant items excluded from `GetUndatedNativeTasks`.
+
+### Task 4: Scheduler
+
+**File:** `internal/scheduler/buckets.go` (new) — `RunBucketCycleCheck(ctx, s, interval)`, same ticker shape as `RunRecurrenceCheck`, calling `s.RunBucketCycles(config.Now())`.
+
+**File:** `cmd/dashboard/main.go` — `go scheduler.RunBucketCycleCheck(schedulerCtx, db, 15*time.Minute)` next to the existing recurrence-check goroutine.
+
+### Task 5: HTTP handlers
+
+**File:** `internal/handlers/widget.go`
+
+- `HandleWidgetBucketsGet` (`GET /api/widget/buckets`).
+- `HandleWidgetBucketsCreate` (`POST /api/widget/buckets` — `{name, cycle_days, pick_n}`).
+- `HandleWidgetBucketItemsAdd` (`POST /api/widget/buckets/{id}/items` — `{task_id}`).
+- `HandleWidgetBucketItemsRemove` (`POST /api/widget/buckets/{id}/items/remove` — `{task_id}`).
+- `HandleWidgetTaskDefer` (`POST /api/widget/task/defer` — `{id}`) — the new action distinct from complete/reschedule.
+
+**File:** `cmd/dashboard/main.go` — register under the `widgetAuth` block.
+
+**Tests:** `internal/handlers/buckets_test.go` (new), mirroring `widget_test.go` style.
+
+### Task 6: Web + task-detail popup wiring
+
+- Task-detail popup (the same one that already handles project/labels/recurrence, per `2026-07-15-task-labels-and-projects.md`) gets a bucket assignment control and, for active bucket items, a "Defer" button next to Complete.
+- Bucket management view: minimal list (name, cycle, pick_n, pool contents) — reuses the existing projects-list page pattern if one exists, otherwise a simple new template under the settings area.
+
+### Task 7: Android widget wiring
+
+**Files:**
+- `WidgetItem.kt` — add `bucketId`, `bucketState` fields.
+- `DataStore.kt` — repository methods for the 5 new endpoints, plus defer.
+- `WidgetRows.kt` — a "Defer" swipe/button action alongside the existing Complete action for rows where `bucketState == "active"`.
+- No dedicated bucket-management screen on Android (per spec, UI design is explicitly out of scope beyond the minimum) — management stays a web-only surface; the widget only needs to complete/defer active items it already displays.
+
+### Task 8: Docs
+
+- Flip the spec's status line once Tasks 1-7 are done and tests pass.
+- Update `.agent/worklog.md`.
+
+## Testing
+
+`go test ./...` after Tasks 1-5. Android: `./gradlew assembleDebug` after Task 7 (debug build only — do not build/publish the release APK without a separate explicit go-ahead).
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
index a66d828..98198b2 100644
--- a/docs/superpowers/specs/2026-07-15-linear-task-chains-design.md
+++ b/docs/superpowers/specs/2026-07-15-linear-task-chains-design.md
@@ -1,6 +1,6 @@
# Linear Task Chains with WIP Limits — Design
-**Status:** Backlog idea, refined via interview 2026-07-15. Not yet approved for implementation.
+**Status:** Implemented 2026-07-17. Backend (migration 026, store, HTTP API), web timeline position badge, and Android widget data layer + row badge all shipped with tests (`go test ./...` green). See `docs/superpowers/plans/2026-07-17-linear-task-chains.md` for the task breakdown. Not built: a dedicated Android checklist screen for browsing a full chain (locked + unlocked) -- the row-level badge and `GET /api/widget/chains/{id}` endpoint exist, but the standalone browsable view described in "Visibility" below is deferred, consistent with this spec's own "Web UI specifics for the chain/checklist view" being out of scope.
## Context
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
index 5072643..2bfb5ba 100644
--- a/docs/superpowers/specs/2026-07-15-recurring-maintenance-buckets-design.md
+++ b/docs/superpowers/specs/2026-07-15-recurring-maintenance-buckets-design.md
@@ -1,6 +1,6 @@
# Recurring Maintenance Buckets — Design
-**Status:** Backlog idea, refined via interview 2026-07-15. Not yet approved for implementation.
+**Status:** Implemented 2026-07-17. Backend (migration 027, store, scheduler, HTTP API), web timeline Defer action, and Android widget data layer + row Defer button all shipped with tests (`go test ./...` green). See `docs/superpowers/plans/2026-07-17-recurring-maintenance-buckets.md` for the task breakdown. Not built: a dedicated bucket-management UI (create bucket / add-remove pool items screen) on either web or Android -- the CRUD API exists and is tested, but per this spec's own "Not designed in detail here" on UI, the management surface itself is deferred.
## Context
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
index 923ef54..5cf2c04 100644
--- 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
@@ -1,6 +1,6 @@
# Task Budgets and Availability Times — Design
-**Status:** Backlog idea, refined via interview 2026-07-15. Not yet approved for implementation.
+**Status:** Implemented (migration 025, shipped 2026-07-17). Status header and the implementation plan's checkboxes were never updated after implementation -- corrected 2026-07-17.
## Context
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
index 4144d5b..9c13094 100644
--- 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
@@ -1,6 +1,6 @@
# Task Labels and Projects — Design
-**Status:** Backlog idea, refined via interview 2026-07-15. Not yet approved for implementation.
+**Status:** Implemented (migration 024, shipped 2026-07-15). Status header was never updated after implementation -- corrected 2026-07-17 during the linear-task-chains/maintenance-buckets work, which depends on this being done.
## Context