1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
# Linear Task Chains with WIP Limits — Design
**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
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.
## Deferred: Android chain-checklist screen
Not built as of 2026-07-17. What exists today: a row-level "N/M" badge on the currently-unlocked task (`WidgetRows.kt`'s `TaskRow`), and the data layer to fetch the full ordered list (`WidgetRepository.fetchChain(id)` → `ChainDetail{chain, tasks}`, hitting `GET /api/widget/chains/{id}`; `Chain`/`ChainTask` models already defined in `WidgetItem.kt`). What's missing is the actual screen: tapping the badge does nothing yet (no `clickable`/`actionStartActivity` wired on it).
Rough shape for whoever picks this up:
- A new Activity (matching the existing pattern of `TaskDetailActivity`/`EventDetailActivity`-style popups — check current detail-popup activities for the `taskAffinity` fix mentioned in the 2026-07-12 worklog entry, so it doesn't reopen behind Settings) launched via `actionStartActivity` from the chain badge, passing the chain id.
- On launch: call `WidgetRepository.fetchChain(id)`, render `tasks` in `chain_position` order — locked items dimmed/non-interactive (no due date, not completable from here), the one `chain_unlocked` item shown with its existing Complete affordance.
- Chain-level actions (pause/resume/abandon) as a small menu or buttons — repository methods `pauseChain`/`resumeChain`/`abandonChain` already exist and are ready to call.
- No automated Compose UI test harness in this project (established convention) — verify by building and a manual on-device/emulator check, same as every prior widget UI task this session.
- Web gets nothing here per the original spec ("Web UI specifics... out of scope") — this is Android-only follow-up work.
|