5.1 KiB
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
#123ambiguous 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 theClaudeDo-Task:commit trailer.Numberis 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 asMaxTurnsCeilingetc.).- 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 - 1executed in the same transaction as the task insert. tasks.numbergets 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):
ALTER TABLE tasks ADD COLUMN number INTEGER NOT NULL DEFAULT 0- 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 equivalentROW_NUMBER()window function). ALTER TABLE app_settings ADD COLUMN next_task_number INTEGER NOT NULL DEFAULT 1then set it to(SELECT COALESCE(MAX(number), 0) + 1 FROM tasks).- 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
#123or bare123(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) →TaskDtoExternalMcpService.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 #<number>
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#123in the row, tokenized (no inline literals).- Detail pane header.
HubBroadcaster.WorkerLogbusiness events inTaskRunner/TaskMergeService/TaskResetServicesay#123instead of the GUID.
Out of scope (possible follow-up)
Branch names and worktree folder names (claudedo/123-<slug>, 123-<slug>). 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.