11 KiB
Kanban Design
Flow
TODO → IN PROGRESS → DONE ───────────────→ APPROVED
│ ↑
└→ REVIEW ───────────┘
│
└→ pending + needsFix
Колонки
| Колонка | Source | Кто двигает | Описание |
|---|---|---|---|
| TODO | task.status = pending | Автоматически | Задачи ожидающие исполнителя |
| IN PROGRESS | task.status = in_progress | Автоматически | Агент работает |
| DONE | task.status = completed | Автоматически | Агент завершил |
| REVIEW | kanban-state.json | Юзер/UI actions | На проверке |
| APPROVED | kanban-state.json | Юзер/UI actions | Одобрено |
Kanban State Storage
Почему свой файл, а не task metadata
Metadata в task-файлах может быть перезаписан агентом при TaskUpdate. Semantics (merge vs replace) НЕДОКУМЕНТИРОВАН. Если replace — наш kanbanColumn исчезнет.
Формат kanban-state.json
Хранится в директории app config (не в ~/.claude/tasks/).
{
"teamName": "my-team",
"reviewers": ["agent-review-1"],
"tasks": {
"12": {
"column": "review",
"reviewStatus": "pending",
"reviewer": "agent-review-1",
"movedAt": "2026-02-17T15:30:00.000Z"
},
"15": {
"column": "approved",
"movedAt": "2026-02-17T16:00:00.000Z"
},
"18": {
"column": "review",
"reviewStatus": "error",
"reviewer": null,
"errorDescription": "Не обработан edge case с пустым массивом",
"movedAt": "2026-02-17T16:30:00.000Z"
}
}
}
Column Mapping Logic
Task → Column:
1. kanban-state.tasks[task.id] exists? → use .column
2. task.status === "pending" → TODO
3. task.status === "in_progress" → IN PROGRESS
4. task.status === "completed" → DONE
5. task.status === "deleted" → не показываем
GC kanban-state (важно: порядок операций)
При очистке устаревших записей (задачи которых больше не существуют) ОБЯЗАТЕЛЬНО сначала полностью загрузить все tasks, и только потом запускать GC:
// ПРАВИЛЬНО:
const tasks = await getAllTasks(teamName); // 1. Сначала все tasks
const kanban = await getKanbanState(teamName); // 2. Затем kanban
const validIds = new Set(tasks.map(t => t.id));
const cleaned = Object.fromEntries(
Object.entries(kanban.tasks).filter(([id]) => validIds.has(id))
); // 3. Только потом GC
// НЕПРАВИЛЬНО (race condition при startup):
const kanban = await getKanbanState(teamName);
gcStaleEntries(kanban); // ← task-файлы ещё не прочитаны, удалим валидные записи
const tasks = await getAllTasks(teamName);
Review Flow
⚠️ Этот файл описывает текущий продуктовый contract review flow. Исторические iteration-доки могут расходиться с ним.
Manual actions from DONE
Из DONE сейчас есть два валидных пользовательских сценария:
- Request Review - отправить задачу в
REVIEW - Approve - сразу перевести задачу в
APPROVEDкак manual shortcut
REVIEW нужен, когда пользователь хочет отдельный шаг проверки на доске, включая reviewer-driven flow или ручную проверку через UI. Но REVIEW не является обязательным промежуточным шагом для каждого manual approval.
Перемещение DONE → REVIEW
- Юзер переводит карточку из DONE в REVIEW через UI action
- Проверяем
kanbanState.reviewers[] - Есть ревьюверы:
- Берём первого свободного (round-robin с балансировкой по количеству активных ревью)
- Записываем в kanban-state:
{ column: "review", reviewStatus: "pending", reviewer: "agent-name" } - Отправляем inbox ревьюверу:
{ "from": "user", "text": "Please review task #12: Rename package in pubspec.yaml\n\nDescription: Change name: dartdoc to name: dartdoc_vitepress", "summary": "Review request for task #12", "timestamp": "...", "read": false }
- Нет ревьюверов:
- Записываем в kanban-state:
{ column: "review", reviewStatus: "pending" } - Юзер сам ревьювит через UI (кнопки Approve / Request Changes)
- Записываем в kanban-state:
Прямое DONE → APPROVED
Юзер может сразу нажать Approve на карточке в DONE:
- kanban-state:
{ column: "approved" } - отдельный заход в
REVIEWне требуется - это manual shortcut и текущее допустимое поведение UI
Review Result
В UI каждая карточка в REVIEW показывает ReviewBadge:
- Pending (yellow) — ждёт ревью
- Approved (green) — проверено, всё хорошо → кнопка Approve → переходит в APPROVED
- Changes Requested (red) — найдены замечания → кнопка Request Changes с опциональным комментарием
Approve → APPROVED
Юзер нажимает кнопку Approve на карточке в REVIEW:
- kanban-state:
{ column: "approved" }
Request Changes → Fix
- Юзер нажимает кнопку Request Changes на карточке в REVIEW
- Появляется ReviewDialog — textarea для описания проблемы (опционально)
- Юзер нажимает "Отправить"
- Действия:
- kanban-state: удаляем запись для этой задачи
- task file:
status = "pending" - reviewState становится
needsFix - в UI задача возвращается в TODO/backlog path с маркером Needs Fixes
- Inbox к исходному owner:
{ "from": "user", "text": "Task #12 needs fixes:\n\nНе обработан edge case с пустым массивом\n\nPlease fix and mark as completed when done.", "summary": "Fix request for task #12", "timestamp": "...", "read": false }
Примечание: reviewHistory (история раундов ревью) и round-robin балансировка ревьюверов — в Phase 2, не MVP.
MVP vs Phase 2
MVP: Click-to-Move
Для текущего UI переходы между review-колонками делаются через card actions на карточке. Отдельный DnD сейчас используется для перестановки задач внутри колонки, а не для review state transitions.
[Task Card]
Subject: Rename package in pubspec.yaml
Owner: worker-1
[Approve] [Request review]
Разрешённые review-переходы через UI actions:
| Откуда → Куда | Действие |
|---|---|
| DONE → REVIEW | kanban-state: review + reviewStatus: pending. Inbox ревьюверу если есть |
| DONE → APPROVED (Approve) | kanban-state: approved |
| REVIEW → APPROVED (Approve) | kanban-state: approved |
| REVIEW → TODO/Needs Fixes (Request Changes) | Dialog → task: pending + needsFix, kanban: remove, inbox к owner |
| APPROVED → DONE | kanban-state: remove (возвращается в DONE по status) |
Не разрешено:
- TODO → IN PROGRESS (агент берёт сам через TaskUpdate)
- IN PROGRESS → DONE (агент завершает сам через TaskUpdate)
Phase 2: Полноценный D&D для state transitions
@dnd-kit уже используется для ordering. В Phase 2 можно добавить drag-and-drop и для самих state transitions, если это понадобится по UX.
Синхронизация при изменении task status
Агент может поменять статус задачи пока мы показываем Kanban. Через file watcher мы узнаём об изменении.
Сценарии
Task в REVIEW, но agent поставил status = in_progress:
- Конфликт: kanban-state говорит REVIEW, task file говорит in_progress
- Действие: показать warning badge на карточке
Task в REVIEW, agent поставил status = completed:
- Агент завершил повторно (после fix)
- Действие: обновить reviewStatus на pending (новый раунд ревью)
Новая задача (status = pending):
- Нет записи в kanban-state
- Действие: автоматически в TODO
Reviewer Assignment
Конфигурация
Список ревьюверов хранится в kanban-state.json → reviewers: string[].
Источники ревьюверов:
- Роль при создании команды: юзер в промпте указывает "создай agent-review с ролью reviewer"
- В UI: кнопка "Назначить ревьювером" рядом с участником в MemberList
Round-Robin с балансировкой
function pickReviewer(reviewers: string[], kanbanState: KanbanState): string | null {
if (reviewers.length === 0) return null;
// Считаем активные ревью у каждого
const reviewCounts = new Map<string, number>();
for (const reviewer of reviewers) {
reviewCounts.set(reviewer, 0);
}
for (const task of Object.values(kanbanState.tasks)) {
if (task.column === 'review' && task.reviewStatus === 'pending' && task.reviewer) {
const count = reviewCounts.get(task.reviewer) || 0;
reviewCounts.set(task.reviewer, count + 1);
}
}
// Берём того у кого меньше всего
return [...reviewCounts.entries()]
.sort((a, b) => a[1] - b[1])[0][0];
}