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