# tasks/ — Task CRUD, Queue, Progress

See @../../CLAUDE.md for project-wide patterns and @../common/CLAUDE.md for base client conventions.

## Status Lifecycle
```
grooming → {todo, cancelled}
todo → {in_progress, cancelled, grooming}
in_progress → {done, cancelled, timeout}
done → {} (TERMINAL — no transitions out)
cancelled → {todo}
timeout → {todo}
```

## Critical Validation Rules
- **'done' requires metrics**: Must provide `pr_url` OR non-zero `lines_added`/`lines_deleted`. Empty metrics without pr_url → 400.
- **'in_progress' checks dependencies**: All deps must be merged/cancelled or you get 409 Conflict.
- **Progress is monotonic**: Can only increase (0.0→1.0). Decreasing → 400 error.
- **Bulk update forbids 'done'**: Can only set done via single-task update (to enforce the metrics gate).

## next_task Concurrency
- Uses optimistic concurrency with max 10 retries and excluded_ids tracking
- SELECT finds candidate → UPDATE with `WHERE status = 'todo'` guards against races
- Post-claim re-check: after claiming, re-validates dependencies haven't changed; releases back to 'todo' if blocked
- Priority ordering: `critical > high > medium > low`, oldest first within same priority (CASE ORDER BY)

## Route Registration Order
CRITICAL: `/api/tasks/bulk-update` MUST be registered BEFORE `/{task_id}` or FastAPI matches "bulk-update" as a task_id.

## Enrichment
- `depends_on`: ALL dependency task IDs (historical)
- `blocked_by`: ONLY dependency IDs with status NOT IN (merged, cancelled) — what's actually blocking
- Feature names/branches are joined and populated in list/get responses

## Side Effects on Status Change
- `in_progress` → sets `started_at = CURRENT_TIMESTAMP`
- `done` → sets `completed_at`, checks if all sibling tasks in feature are done, triggers auto-merge re-evaluation via `asyncio.create_task`
- State changes emit webhook events (TASK_CREATED, TASK_COMPLETED, TASK_FAILED, TASK_TIMEOUT) via fire-and-forget

## Dependency System
- BFS cycle detection prevents circular dependencies
- UNIQUE constraint on `(task_id, depends_on_task_id)` with self-reference prevention
