Files
ClaudeDo/docs/superpowers/specs/2026-08-11-task-numbers-design.md

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