docs(specs): add task-numbers and dependency-chain display designs
This commit is contained in:
@@ -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`.
|
||||||
@@ -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 `#<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.
|
||||||
Reference in New Issue
Block a user