From 31a9e87b5661fda76c99be2d9087265fd27e9054 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Tue, 11 Aug 2026 09:56:26 +0200 Subject: [PATCH] docs(specs): add task-numbers and dependency-chain display designs --- ...6-08-11-dependency-chain-display-design.md | 73 +++++++++++ .../specs/2026-08-11-task-numbers-design.md | 116 ++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-11-dependency-chain-display-design.md create mode 100644 docs/superpowers/specs/2026-08-11-task-numbers-design.md diff --git a/docs/superpowers/specs/2026-08-11-dependency-chain-display-design.md b/docs/superpowers/specs/2026-08-11-dependency-chain-display-design.md new file mode 100644 index 00000000..73bfcbce --- /dev/null +++ b/docs/superpowers/specs/2026-08-11-dependency-chain-display-design.md @@ -0,0 +1,73 @@ +# Dependency chain display — Design + +**Date:** 2026-08-11 +**Status:** approved, not implemented + +## Problem + +`DependsOnTaskId` is invisible in the UI. `TaskRowViewModel` has no `DependsOnTaskId` property +and `TaskRowView.axaml` renders nothing for it — a chained task looks exactly like an unrelated +one. The execution order the user declared is not readable anywhere in the list. + +## Target + +A chain renders as a group: the head full width, its dependents indented behind a vertical rail, +each carrying a small circular step badge on the rail. + +``` +┌──────────────────────────────────────────┐ +│ chain head (full width, no badge) │ +└──────────────────────────────────────────┘ + ╷ ┌────────────────────────────────────┐ + (1)│ first dependent │ + ╷ └────────────────────────────────────┘ + ╷ ┌────────────────────────────────────┐ + (2)│ second │ + ╷ └────────────────────────────────────┘ + ╷ ┌────────────────────────────────────┐ + (2)│ parallel to the second │ + ╷ └────────────────────────────────────┘ + ╷ ┌────────────────────────────────────┐ + (3)│ third │ + └────────────────────────────────────┘ +``` + +## Model reading + +- **Step number = hop distance from the chain head**, not a running counter. `DependsOnTaskId` is + a single FK, so several tasks may share one predecessor → same depth → **same number**. Two + rows showing `2` means "these two are both unblocked by step 1", which is the intended reading. +- **Chain head** = a task with no `DependsOnTaskId` that at least one other task points at. A task + with no dependents and no dependency is not a chain and renders exactly as today. +- Cycles are impossible (`TaskStateService.SetDependsOnAsync` rejects them), so the depth walk + always terminates. Still cap the walk defensively. + +## Decisions + +**One indent level only — parent wins.** The 24 px indent track already exists for planning +children (`TaskRowView.axaml:22-28`, gated on `ShowAsChild`). A planning child that *also* has a +`DependsOnTaskId` keeps the parent indent and shows its chain membership as a small inline +chip (`after #123`) instead of a second indent level. No nesting, no 48 px rows. + +**The group is pulled together; the head carries it.** Chain members are re-ordered to sit +directly under their head regardless of `SortOrder`, so displayed order always equals execution +order. Dragging the head moves the whole group; members are not individually draggable. + +**Head not in view → no orphan rails.** Exact precedent exists: `ParentInView` /`ShowAsChild` +(`TaskRowViewModel.cs:75`, computed in `Regroup` at `TasksIslandViewModel.cs:543`). Same +treatment — if the head is filtered out (other list, My Day, completed group), the row renders +flat with the `after #123` chip instead of a dangling rail. + +**Badge has no `#`.** If task numbers (`2026-08-11-task-numbers-design.md`) also land, a row +would show a rail badge and a `#412` title prefix. The rail badge is a step position, not an +identity — render it bare (`2`), never `#2`. + +## Slices + +| # | Slice | Depends on | +|---|---|---| +| 1 | VM: `DependsOnTaskId` + depth/head computation in `Regroup`, group pull-together, drag semantics | — | +| 2 | View: rail, step badge, `after …` chip, locale keys | 1 | + +Slice 1 exposes the contract Slice 2 binds to: `ShowAsChainMember`, `ChainStep`, +`ChainAfterLabel`. diff --git a/docs/superpowers/specs/2026-08-11-task-numbers-design.md b/docs/superpowers/specs/2026-08-11-task-numbers-design.md new file mode 100644 index 00000000..634a7b71 --- /dev/null +++ b/docs/superpowers/specs/2026-08-11-task-numbers-design.md @@ -0,0 +1,116 @@ +# Task Numbers (`#123`) — Design + +**Date:** 2026-08-11 +**Status:** approved, not implemented + +## Problem + +Tasks are only addressable by GUID. Every report from Claude reads +"task `0ef5ga…` is done", which is unreadable and untraceable for the user. A short, +stable, human-speakable handle is needed. + +## Decision + +Add a **global, monotonically increasing integer** `TaskEntity.Number`, displayed as `#123`. + +- **Global, not per list.** Per-list numbering would make `#123` ambiguous across the ~15 + lists and would force a list argument into every lookup. Global costs nothing and is + unambiguous. +- **Alias, not identity.** The GUID stays the primary key and stays in branch names + (`claudedo/{id}`), worktree paths, and the `ClaudeDo-Task:` commit trailer. `Number` is + purely a display + lookup alias. +- **Immutable, never reused.** A number that appeared in a log must never later point at a + different task. Deleting tasks leaves gaps — that is correct and intended. + +## Allocation + +`MAX(number) + 1` is **wrong**: deleting the newest task frees its number for reuse. +Use a persistent counter instead. + +- `app_settings.next_task_number` (INTEGER NOT NULL, singleton row — same table as + `MaxTurnsCeiling` etc.). +- Allocation is one statement, atomic under SQLite's single-writer model: + `UPDATE app_settings SET next_task_number = next_task_number + 1 + WHERE id = 1 RETURNING next_task_number - 1` + executed in the **same transaction** as the task insert. +- `tasks.number` gets a **UNIQUE index** (`idx_tasks_number`) so a collision is a DB error, + not silent corruption. On a unique violation, retry the allocation (bounded, 5 attempts). + +Only **two** insert sites exist and both must allocate — every other creation path +(UI, `add_task`, `batch_add_tasks`, Online Inbox sync, merge-helper handler task, +planning children) routes through one of them: + +- `TaskRepository.AddAsync` (`src/ClaudeDo.Data/Repositories/TaskRepository.cs:20`) +- `TaskRepository.AddChildAsync` (same file, ~line 297) + +Do **not** reuse `SortOrder` — it is per list and mutable via drag & drop. + +## Migration + backfill + +One migration, created **alone** (never in parallel with another migration — sibling +migrations off the same parent silently drop each other's columns on a SQLite table rebuild): + +1. `ALTER TABLE tasks ADD COLUMN number INTEGER NOT NULL DEFAULT 0` +2. Backfill in creation order: + `UPDATE tasks SET number = (SELECT COUNT(*) FROM tasks t2 + WHERE t2.created_at < tasks.created_at OR (t2.created_at = tasks.created_at AND t2.id <= tasks.id))` + (or the equivalent `ROW_NUMBER()` window function). +3. `ALTER TABLE app_settings ADD COLUMN next_task_number INTEGER NOT NULL DEFAULT 1` + then set it to `(SELECT COALESCE(MAX(number), 0) + 1 FROM tasks)`. +4. Create the unique index **after** the backfill. + +⚠️ Tests use `EnsureCreated`, which bypasses migrations — the backfill needs a test that +explicitly runs `Migrate()` against a DB seeded with pre-migration rows. + +## Resolution: `#123` → GUID + +New `TaskIdResolver` in `src/ClaudeDo.Worker/External/`: + +- Input `#123` or bare `123` (a GUID is never all-digits, so this is unambiguous) → look up + by number. +- Anything else → passed through as a GUID. +- Unknown number → a clear MCP error (`no task with number 123`), never a silent null. + +Wired at the top of every MCP tool taking a task id (~44 parameters across `External/`), +including the id arrays of the `batch_*` tools. + +## Output surface + +Two central mappers cover most tools: + +- `ExternalMcpService.ToDto` (line ~1629) → `TaskDto` +- `ExternalMcpService.GetTaskRefAsync` (line ~335) → `TaskRefDto` + +Add `Number` to both. Then the DTOs that carry a bare task id and bypass those mappers: +`RunTaskNowResult`, `MergeTaskResultDto`, `PossibleDuplicateDto`, `SubsetRelationDto`, +`FileOverlapDto`, the `BatchMcpTools` results, `TaskWaitMcpTools`, `QueueStateMcpTools`, +`RunHistoryMcpTools`. + +`McpToolDocs` gets a shared clause instructing the agent to **refer to tasks as `#` +when reporting to the user**. Without this the number is present in the payload but never +spoken — this clause is the actual point of the feature. + +## UI surface + +- `TaskRowViewModel.Number` → dim `#123` in the row, tokenized (no inline literals). +- Detail pane header. +- `HubBroadcaster.WorkerLog` business events in `TaskRunner` / `TaskMergeService` / + `TaskResetService` say `#123` instead of the GUID. + +## Out of scope (possible follow-up) + +Branch names and worktree folder names (`claudedo/123-`, `123-`). Cheap once +numbers exist, but a separate change — it touches merge, self-heal, and the merge helper. + +## Slices + +| # | Slice | Depends on | +|---|---|---| +| 1 | Schema, counter, allocator, backfill migration + tests | — | +| 2 | MCP output: `Number` in all task-carrying DTOs | 1 | +| 3 | MCP input: `TaskIdResolver` + `McpToolDocs` reporting clause | 2 | +| 4 | UI: row/detail display + worker-log messages | 1 | +| 5 | Docs: `CLAUDE.md` (Data, Worker), explore-notes | 3, 4 | + +2 and 3 both edit `ExternalMcpService.cs` heavily, so 3 waits for 2 rather than running +beside it. 4 touches disjoint files and can run beside 2.