# 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.