summaryrefslogtreecommitdiff
path: root/docs/superpowers/plans/2026-07-17-linear-task-chains.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/superpowers/plans/2026-07-17-linear-task-chains.md')
-rw-r--r--docs/superpowers/plans/2026-07-17-linear-task-chains.md98
1 files changed, 98 insertions, 0 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`).