feat(team-management): implement initial Team Management feature with Kanban support
- Added core documentation for the Team Management feature, including iteration goals, implementation plans, and design specifications. - Introduced new files for team configuration, IPC channels, and UI components for team listing and Kanban board. - Established a clear definition of done and scope for the first iteration, focusing on visible results and graceful degradation in the UI. - Documented the messaging and task file formats, ensuring robust communication between team members. - Implemented a Kanban design with defined states and transitions for task management.
This commit is contained in:
parent
bd088ec71c
commit
e7d9e82ce8
9 changed files with 3684 additions and 0 deletions
14
docs/iterations/README.md
Normal file
14
docs/iterations/README.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Итерации реализации
|
||||
|
||||
Здесь лежат планы реализации фич итерациями (vertical slices), чтобы двигаться **постепенно** и каждый шаг давал видимый результат.
|
||||
|
||||
## Список
|
||||
|
||||
- [Итерация 01 — Core Foundation + Team List](./iteration-01-core-team-list.md)
|
||||
|
||||
## Принципы
|
||||
|
||||
- **Vertical slice**: в каждой итерации доводим минимум “end-to-end” (types → main → IPC → preload → renderer → UI)
|
||||
- **Чёткий scope**: у каждой итерации есть цели и не‑цели
|
||||
- **Definition of Done**: заранее фиксируем критерии готовности и ручную проверку
|
||||
|
||||
350
docs/iterations/iteration-01-core-team-list.md
Normal file
350
docs/iterations/iteration-01-core-team-list.md
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
# Итерация 01 — Core Foundation + Team List
|
||||
|
||||
Эта итерация реализует **первый вертикальный срез** Team Management: от файлов `~/.claude/teams/*` до UI, где открывается вкладка со списком команд.
|
||||
|
||||
База для решений и порядка работ:
|
||||
- `docs/team-management/implementation.md` (v7)
|
||||
- `~/.claude/plans/goofy-napping-river.md` (summary v7)
|
||||
- `docs/team-management/research-*.md` + `docs/team-management/kanban-design.md`
|
||||
|
||||
---
|
||||
|
||||
## Цель итерации
|
||||
|
||||
Сделать **видимый результат** без “backend-first” провала:
|
||||
|
||||
- В приложении есть **вкладка `Teams`** (тип таба `teams`, **singleton в рамках активной pane**, как `Notifications`)
|
||||
- В ней отображается **список команд** из `~/.claude/teams/{teamName}/config.json`
|
||||
- Если команд нет (или директория отсутствует) — корректный **empty state**
|
||||
- Ошибки чтения/парсинга не валят UI: **graceful degradation**
|
||||
|
||||
---
|
||||
|
||||
## Не-цели (строго вне scope)
|
||||
|
||||
- Team detail (страница конкретной команды)
|
||||
- Members/Tasks/Messages/Kanban UI
|
||||
- Любые write-path (inbox write, kanban update, task status update)
|
||||
- FileWatcher расширение (teamsWatcher/tasksWatcher) и live refresh по `team-change`
|
||||
- Tests/fixtures (это отдельная итерация 5), кроме минимальных правок тестов, если они ломаются из‑за добавления нового `tab.type`
|
||||
|
||||
---
|
||||
|
||||
## Контракт итерации (что именно считаем “готово”)
|
||||
|
||||
### API-контракт (Main → Preload → Renderer)
|
||||
|
||||
**IPC channel**
|
||||
- `TEAM_LIST` = `'team:list'` (flat `export const`, без `IPC_CHANNELS.*`)
|
||||
|
||||
**Response**
|
||||
- Возвращает `TeamSummary[]`
|
||||
- Никаких side effects (только чтение)
|
||||
- Должен быть безопасен при отсутствии `~/.claude/teams`
|
||||
|
||||
**TeamSummary (на итерации 1)**
|
||||
- `name: string` — отображаемое имя команды (из `config.json`)
|
||||
- `description: string` — описание (из `config.json` или `''`)
|
||||
- `memberCount: number` — `config.members.length` или 0
|
||||
- `taskCount: number` — **0** (пока не читаем tasks)
|
||||
- `lastActivity: string | null` — **null** (пока не читаем inbox)
|
||||
|
||||
### Definition of Done
|
||||
|
||||
- **UI**: вкладка `Teams` открывается, показывает список/empty state, без падений при:
|
||||
- отсутствии `~/.claude/teams`
|
||||
- наличии “мусорных” директорий без `config.json` (например `default/`)
|
||||
- наличии битого `config.json` (пропускаем, не падаем)
|
||||
- **IPC**: есть канал `TEAM_LIST` (`'team:list'`), который возвращает `TeamSummary[]`
|
||||
- **Типы**: `IpcResult<T>` дедуплицирован в `src/shared/types/ipc.ts`
|
||||
- **Качество**: проходит `pnpm typecheck`, приложение запускается в dev, вкладка доступна из UI
|
||||
|
||||
---
|
||||
|
||||
## Главные решения, которые сохраняем
|
||||
|
||||
- **Vertical slice**: делаем end-to-end минимум (types → main → IPC → preload → renderer → UI)
|
||||
- **Flat IPC constants**: только `export const TEAM_LIST = 'team:list'` и прямые импорты (никаких `IPC_CHANNELS.*`)
|
||||
- **Graceful skip**: `listTeams()` пропускает директории без валидного `config.json` (v7 fix #37)
|
||||
- **Global TeamDataService**: сервис создаётся глобально (по аналогии с UpdaterService), не привязан к ServiceContext (подготовка к следующим итерациям, v6/v7 решение)
|
||||
|
||||
---
|
||||
|
||||
## Выходные изменения (файлы), только для итерации 1
|
||||
|
||||
Цель — чтобы по итогам итерации было понятно: **что добавили**, и что менять не пришлось.
|
||||
|
||||
### Новые файлы (ожидаемо)
|
||||
|
||||
- `src/shared/types/ipc.ts`
|
||||
- `src/shared/types/team.ts` (минимальный набор типов для list)
|
||||
- `src/main/services/team/TeamConfigReader.ts`
|
||||
- `src/main/services/team/TeamDataService.ts` (минимум `listTeams()`; остальное — позже)
|
||||
- `src/main/ipc/teams.ts` (минимум: `TEAM_LIST`)
|
||||
- `src/renderer/utils/unwrapIpc.ts`
|
||||
- `src/renderer/store/slices/teamSlice.ts` (минимум: `fetchTeams`)
|
||||
- `src/renderer/components/team/TeamListView.tsx`
|
||||
- `src/renderer/components/team/TeamEmptyState.tsx`
|
||||
|
||||
### Изменяемые файлы (ожидаемо)
|
||||
|
||||
- `src/shared/types/index.ts` (реэкспорты)
|
||||
- `src/preload/constants/ipcChannels.ts` (добавить `TEAM_LIST`)
|
||||
- `src/shared/types/api.ts` (добавить `TeamsAPI.list`)
|
||||
- `src/preload/index.ts` (проброс `api.teams.list`)
|
||||
- `src/main/ipc/handlers.ts` (wiring: init + register `TEAM_LIST`)
|
||||
- `src/main/index.ts` (создание global `teamDataService` и проброс в `initializeIpcHandlers`)
|
||||
- `src/renderer/types/tabs.ts` (добавить `type: 'teams'`)
|
||||
- `src/renderer/store/types.ts`, `src/renderer/store/index.ts` (подключение teamSlice)
|
||||
- layout-компоненты табов (`PaneContent`, `TabBar`, `SortableTab`) — чтобы появился вход в Teams tab
|
||||
|
||||
---
|
||||
|
||||
## Порядок работ (runbook)
|
||||
|
||||
Ниже — “как делаем” в точном порядке, чтобы на каждом шаге был компилируемый код и минимальный риск сломать существующие фичи.
|
||||
|
||||
### Контрольные точки (чтобы не тащить поломки дальше)
|
||||
|
||||
Контрольные точки привязаны к реально “широким” изменениям (типы/IPC/таб‑routing):
|
||||
|
||||
- **CP0**: после 1.1 — `pnpm typecheck`
|
||||
- **CP1**: после 2.5 — `pnpm typecheck`
|
||||
- **CP2**: после 4.2 — `pnpm typecheck`
|
||||
- **CP3**: после 4.4 — `pnpm dev` + ручная проверка
|
||||
|
||||
---
|
||||
|
||||
### 1) Shared foundation (Phase 0)
|
||||
|
||||
#### 1.1 Дедупликация `IpcResult<T>` (Step 0.1)
|
||||
|
||||
- Создать `src/shared/types/ipc.ts` с `export interface IpcResult<T = void> { success; data?; error? }`
|
||||
- Удалить дубликаты:
|
||||
- `src/main/ipc/config.ts`: заменить `ConfigResult<T>` → `IpcResult<T>` из `@shared/types`
|
||||
- `src/preload/index.ts`: заменить локальный `IpcResult` → импорт из `@shared/types` и обновить `invokeIpcWithResult<T>()` на этот тип
|
||||
- Обновить `src/shared/types/index.ts`: реэкспорт `IpcResult`
|
||||
|
||||
**CP0:** `pnpm typecheck` (на этом шаге обязателен, чтобы не тащить дальше ошибки типов).
|
||||
|
||||
---
|
||||
|
||||
### 2) Backend: чтение команд (Main Process)
|
||||
|
||||
#### 2.1 Типы для Teams (Step 1 — частично)
|
||||
|
||||
В этой итерации нужны минимум:
|
||||
|
||||
- `TeamConfig` (чтобы распарсить `config.json`)
|
||||
- `TeamSummary` (чтобы вернуть данные в UI)
|
||||
|
||||
Создать/расширить `src/shared/types/team.ts` и реэкспорт в `src/shared/types/index.ts`.
|
||||
|
||||
Важно:
|
||||
- `TeamSummary.lastActivity` на итерации 1 можно держать `null` (не считаем активность без inbox/tasks).
|
||||
|
||||
#### 2.2 Path helpers (Step 2 — по минимуму)
|
||||
|
||||
Добавить `getTeamsBasePath()` в `src/main/utils/pathDecoder.ts` рядом с `getProjectsBasePath()` / `getTodosBasePath()`:
|
||||
|
||||
- `getTeamsBasePath(): string` → `path.join(getClaudeBasePath(), 'teams')`
|
||||
|
||||
Эта функция **обязательна** уже в итерации 1, чтобы не раскидывать `'teams'` как литерал по коду.
|
||||
|
||||
**Важно (без двусмысленностей):** `TeamConfigReader` **не имеет права** “захардкодить” путь в конструкторе так, чтобы он перестал учитывать `setClaudeBasePathOverride()`.
|
||||
|
||||
- `getClaudeBasePath()` в этом репо поддерживает override (через настройки Claude root).
|
||||
- Поэтому путь к `~/.claude/teams` должен вычисляться **на каждый вызов** `listTeams()` через `getTeamsBasePath()`.
|
||||
|
||||
#### 2.3 `TeamConfigReader.listTeams()` (Step 3 — частично)
|
||||
|
||||
Создать `src/main/services/team/TeamConfigReader.ts` с поведением:
|
||||
|
||||
- Если `~/.claude/teams` отсутствует → вернуть `[]`
|
||||
- `readdir(teamsDir, { withFileTypes: true })` → обрабатывать **только директории** (Dirent.isDirectory), остальные entries игнорировать
|
||||
- Для каждой директории:
|
||||
- пытаться прочитать `{dir}/config.json`
|
||||
- если файла нет / JSON битый / схема не подходит → **continue** (v7 fix #37)
|
||||
- собрать `TeamSummary` (name, description, memberCount, taskCount=0, lastActivity=null)
|
||||
|
||||
**Нюанс:** в логах можно оставлять `debug` (но не спамить и не падать).
|
||||
|
||||
#### 2.4 `TeamDataService.listTeams()` (Step 5 — частично)
|
||||
|
||||
Создать каркас `TeamDataService` так, чтобы в итерации 1 он поддерживал минимум:
|
||||
|
||||
- `listTeams(): Promise<TeamSummary[]>` → делегирует в `TeamConfigReader`
|
||||
|
||||
Остальные методы (`getTeamData`, kanban/inbox/tasks) — не трогаем или оставляем на следующую итерацию, чтобы не расползаться.
|
||||
|
||||
#### 2.5 Глобальная инициализация сервиса (Step 11 — частично)
|
||||
|
||||
В `src/main/index.ts` создать **один** instance `teamDataService` (global), не в `ServiceContext`.
|
||||
|
||||
**Цель итерации 1:** чтобы IPC handler мог вызвать `teamDataService.listTeams()` без привязки к workspace.
|
||||
|
||||
**CP1:** `pnpm typecheck` (до IPC лучше убедиться, что main компилируется).
|
||||
|
||||
---
|
||||
|
||||
### 3) IPC + Preload: TeamsAPI.list()
|
||||
|
||||
#### 3.1 IPC channels (Step 6)
|
||||
|
||||
В `src/preload/constants/ipcChannels.ts` добавить:
|
||||
|
||||
- `export const TEAM_LIST = 'team:list';`
|
||||
|
||||
#### 3.2 IPC handler (Step 7 — частично)
|
||||
|
||||
Создать `src/main/ipc/teams.ts` в стиле проекта:
|
||||
|
||||
- module-level state + `initializeTeamHandlers(service)`
|
||||
- `registerTeamHandlers(ipcMain)` регистрирует `TEAM_LIST`
|
||||
- `wrapTeamHandler()` возвращает `IpcResult<T>`
|
||||
- `handleListTeams()` вызывает `service.listTeams()`
|
||||
|
||||
Важно:
|
||||
- Использовать **flat imports** каналов (`TEAM_LIST`) (v7 fix #36)
|
||||
- Не завязываться на `ServiceContextRegistry` (данные Teams читаются из `~/.claude`, это локальный глобальный источник)
|
||||
|
||||
#### 3.3 Wiring handlers.ts (v7 fix #48 — частично для list)
|
||||
|
||||
В этом проекте единая точка регистрации IPC — `src/main/ipc/handlers.ts` (`initializeIpcHandlers()`):
|
||||
|
||||
- Добавить импорт `initializeTeamHandlers/registerTeamHandlers/removeTeamHandlers` из `./teams`
|
||||
- Расширить сигнатуру `initializeIpcHandlers(...)` новым параметром `teamDataService`
|
||||
- Внутри `initializeIpcHandlers()`:
|
||||
- вызвать `initializeTeamHandlers(teamDataService)`
|
||||
- вызвать `registerTeamHandlers(ipcMain)`
|
||||
- В `removeIpcHandlers()`:
|
||||
- вызвать `removeTeamHandlers(ipcMain)`
|
||||
|
||||
#### 3.4 Preload bridge (Step 9 — частично)
|
||||
|
||||
В `src/shared/types/api.ts` добавить интерфейс `TeamsAPI` минимум с:
|
||||
|
||||
- `list(): Promise<TeamSummary[]>`
|
||||
|
||||
И в `ElectronAPI` добавить поле:
|
||||
|
||||
- `teams: TeamsAPI`
|
||||
|
||||
В `src/preload/index.ts`:
|
||||
|
||||
- Реализовать `api.teams.list()` через существующий `invokeIpcWithResult<T>()` (он уже используется для `config/ssh/context/httpServer`)
|
||||
- Пробросить в `contextBridge.exposeInMainWorld()`
|
||||
|
||||
**Быстрая проверка до UI (делаем всегда):**
|
||||
- В renderer DevTools выполнить `await window.electronAPI.teams.list()` и убедиться, что вернулся массив.
|
||||
|
||||
---
|
||||
|
||||
### 4) Renderer: Tabs + store + UI
|
||||
|
||||
#### 4.1 `unwrapIpc<T>()` (Step 12)
|
||||
|
||||
Создать `src/renderer/utils/unwrapIpc.ts`:
|
||||
|
||||
- оборачивает вызов `api.teams.list()`
|
||||
- не делает “double unwrap” (preload уже бросает ошибку при `success: false`)
|
||||
|
||||
#### 4.2 Tabs + UI entry (строго в рамках текущей модели табов)
|
||||
|
||||
**Важно:** в текущем коде `Tab` — это один интерфейс с optional `sessionId/projectId` и `type`-switch в местах использования. Полная миграция на discriminated union — отдельная refactor‑итерация (иначе итерация 1 перестанет быть “тонкой” и потеряет фокус на Team List).
|
||||
|
||||
В итерации 1 делаем **ровно** следующее:
|
||||
|
||||
- В `src/renderer/types/tabs.ts`:
|
||||
- расширить union `Tab['type']`, добавив `'teams'`
|
||||
- ничего больше не меняем (никаких refactor’ов структуры Tab, никаких новых optional полей)
|
||||
|
||||
- В `src/renderer/components/layout/PaneContent.tsx`:
|
||||
- добавить ветку `tab.type === 'teams'` → рендер `TeamListView`
|
||||
|
||||
- В `src/renderer/components/layout/SortableTab.tsx`:
|
||||
- добавить иконку для `teams` в `TAB_ICONS`
|
||||
|
||||
- В `src/renderer/components/layout/TabBar.tsx`:
|
||||
- добавить кнопку “Teams” (иконка `Users` из `lucide-react`) в ряд action‑кнопок
|
||||
- обработчик должен вызывать `openTeamsTab()` (см. следующий пункт)
|
||||
|
||||
- В store:
|
||||
- добавить action `openTeamsTab()` как **per-pane singleton** (строго по паттерну `openNotificationsTab()`):
|
||||
- если в focused pane уже есть таб `type: 'teams'` → активируем его
|
||||
- иначе → `openTab({ type: 'teams', label: 'Teams' })` (лейбл фиксируем как `Teams`, как у `Dashboard/Notifications`)
|
||||
|
||||
**CP2:** `pnpm typecheck` после всех правок выше (до того, как писать TeamList UI).
|
||||
|
||||
#### 4.3 `teamSlice.fetchTeams()` (Step 14 — частично)
|
||||
|
||||
Добавить `src/renderer/store/slices/teamSlice.ts` с минимумом:
|
||||
|
||||
- `teams: TeamSummary[]`
|
||||
- `teamsLoading / teamsError`
|
||||
- `fetchTeams()`: вызывает `unwrapIpc('team:list', () => api.teams.list())`
|
||||
- `openTeamsTab()`: per-pane singleton (описано в Step 4.2)
|
||||
|
||||
Никаких `selectedTeamData`, refresh generation и т.п. — это следующая итерация.
|
||||
|
||||
Подключить slice в `src/renderer/store/types.ts` и `src/renderer/store/index.ts`.
|
||||
|
||||
#### 4.4 Tab integration + Teams view (Step 15 + Step 16 — частично)
|
||||
|
||||
- В `TabBar` добавить кнопку “Teams” → вызывает `openTeamsTab()` (per-pane singleton)
|
||||
- В `PaneContent` добавить ветку `tab.type === 'teams'` → рендер `TeamListView`
|
||||
|
||||
UI-компоненты (минимум для итерации 1):
|
||||
|
||||
- `src/renderer/components/team/TeamListView.tsx`
|
||||
- на mount вызывает `fetchTeams()`
|
||||
- показывает loading/error/empty
|
||||
- рендерит список карточек/строк команд
|
||||
- `src/renderer/components/team/TeamEmptyState.tsx`
|
||||
|
||||
---
|
||||
|
||||
## Ручная проверка (обязательная для итерации 1, CP3)
|
||||
|
||||
1) Запуск:
|
||||
|
||||
- `pnpm dev`
|
||||
- Открыть вкладку `Teams`
|
||||
|
||||
1.1) Singleton-поведение вкладки:
|
||||
|
||||
- Нажать кнопку “Teams” 2–3 раза
|
||||
- Ожидание: не создаются новые табы, фокус остаётся на существующем `Teams` табе (per-pane singleton)
|
||||
|
||||
2) Empty state:
|
||||
|
||||
- Переименовать (временно) `~/.claude/teams` или протестировать на чистой машине
|
||||
- Ожидание: “Команды не найдены” (без ошибок в UI)
|
||||
|
||||
3) Skip dirs без config:
|
||||
|
||||
- Создать папку `~/.claude/teams/default/` без `config.json`
|
||||
- Ожидание: она **не** появляется в списке, UI не падает
|
||||
|
||||
4) Happy path:
|
||||
|
||||
- Убедиться что реальная команда (где есть `config.json`) отображается
|
||||
|
||||
---
|
||||
|
||||
## Риски и как их гасим в рамках итерации
|
||||
|
||||
- **Добавление нового `tab.type`**: обязательно обновить `PaneContent` + `SortableTab.TAB_ICONS`, иначе получим runtime/TS ошибки. CP2 гарантирует, что всё “сшилось”.
|
||||
- **Разный формат `config.json` / мусорные директории**: `TeamConfigReader.listTeams()` делает `try/catch` и `continue`, не кидает исключения наружу.
|
||||
- **Отсутствие `~/.claude/teams`**: `listTeams()` возвращает `[]` без ошибок.
|
||||
- **Стабильность UI**: в `TeamListView` должны быть 4 состояния: loading → data / empty / error.
|
||||
|
||||
---
|
||||
|
||||
## Выходные артефакты (что появится после итерации)
|
||||
|
||||
- Документированный канал `TEAM_LIST` и `TeamsAPI.list()`
|
||||
- Минимальный backend reader для списка команд
|
||||
- Вкладка `Teams` и список команд в UI (или empty state)
|
||||
- База для следующих итераций без переархитектуривания
|
||||
|
||||
100
docs/team-management/README.md
Normal file
100
docs/team-management/README.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# Team Management Feature
|
||||
|
||||
Интерфейс для управления командами тиммейтов Claude Code внутри claude-devtools (Electron).
|
||||
|
||||
## Что делает
|
||||
|
||||
- Видеть состав команды и роли участников
|
||||
- Kanban-доска с 5 колонками (TODO → IN PROGRESS → DONE → REVIEW → APPROVED)
|
||||
- Отправка сообщений тиммейтам через inbox-файлы
|
||||
- Review flow: автоматическое назначение ревьюверов или ручное ревью
|
||||
- Live updates через file watcher
|
||||
|
||||
## Документация
|
||||
|
||||
| Файл | Содержание |
|
||||
|------|-----------|
|
||||
| [research-inbox.md](./research-inbox.md) | Формат inbox-файлов, race conditions, atomic write, доставка сообщений |
|
||||
| [research-tasks.md](./research-tasks.md) | Формат task-файлов, .lock, .highwatermark, конкурентный доступ |
|
||||
| [research-messaging.md](./research-messaging.md) | Сравнение подходов (inbox vs SDK vs CLI), почему выбрали inbox |
|
||||
| [kanban-design.md](./kanban-design.md) | Kanban flow, колонки, review mechanism, kanban-state.json |
|
||||
| [implementation.md](./implementation.md) | Техплан: файлы, шаги, verification |
|
||||
| [research-worktrees.md](./research-worktrees.md) | Git worktrees + teams, запуск Claude процессов из UI (Phase 2) |
|
||||
|
||||
## Ключевые решения
|
||||
|
||||
### 1. Messaging: Inbox-файлы
|
||||
Единственный способ общаться с **запущенными** тиммейтами. SDK и CLI создают новые сессии, а не подключаются к существующим. Подробности: [research-messaging.md](./research-messaging.md)
|
||||
|
||||
### 2. Kanban Storage: Собственный файл
|
||||
Kanban-позиция (REVIEW, APPROVED) хранится в `kanban-state.json`, а не в task metadata. Причина: metadata может быть перезаписан агентом при TaskUpdate. Подробности: [kanban-design.md](./kanban-design.md)
|
||||
|
||||
### 3. Review Flow: Approve / Request Changes
|
||||
- Есть ревьюверы в команде → автоматическое назначение через inbox
|
||||
- Нет ревьюверов → ручное ревью юзером (Approve / Request Changes в UI)
|
||||
- При Request Changes → юзер описывает проблему (опционально) → задача к исходному owner
|
||||
|
||||
### 4. Atomic Write
|
||||
Все записи через tmp + rename для предотвращения corrupted JSON.
|
||||
|
||||
### 5. Sender Identity
|
||||
Отправляем `from: "user"`. Fallback на `from: "team-lead"` если не работает.
|
||||
|
||||
## Финальные решения после ревью
|
||||
|
||||
По итогам 3 раундов ревью (13 экспертов) приняты следующие решения:
|
||||
|
||||
### Inbox: Atomic write + messageId verify
|
||||
- Atomic write (tmp + rename) предотвращает corrupted JSON
|
||||
- После записи читаем файл обратно и проверяем наличие нашего `messageId`
|
||||
- Полный CAS/retry-цикл — не нужен на MVP: проверка при следующем read достаточна
|
||||
- Риск race condition с агентом реален, но вероятность низкая
|
||||
|
||||
### Kanban: kanban-state.json с безопасным GC
|
||||
- GC устаревших записей kanban-state выполняется ТОЛЬКО ПОСЛЕ полной загрузки tasks
|
||||
- Иначе при startup возможна race condition: GC удаляет запись до того как task-файл прочитан
|
||||
|
||||
### Review Flow: Approve / Request Changes
|
||||
- Кнопки переименованы: **Approve** (вместо OK) и **Request Changes** (вместо Error)
|
||||
- Комментарий при Request Changes — опционален
|
||||
- `reviewHistory` и round-robin балансировка → Phase 2, не MVP
|
||||
|
||||
### Members: полный список через union
|
||||
- `union(config members + inbox filenames + task owners)` — единственный способ получить полный список
|
||||
- `owner` в task-файлах — опционален (агент может не иметь owner до назначения)
|
||||
|
||||
### Graceful Degradation
|
||||
- `try/catch` везде в TeamDataService — при ошибке чтения возвращаем безопасные дефолты
|
||||
- 3 состояния участника: `ACTIVE` / `IDLE` / `TERMINATED`
|
||||
- `ACTIVE`: idle < 5 минут
|
||||
- `IDLE`: idle > 5 минут
|
||||
- `TERMINATED`: получен `shutdown_response` с `approve: true`
|
||||
|
||||
### @dnd-kit: click-to-move для MVP
|
||||
- MVP: выбор колонки через select/dropdown (click-to-move) — проще и надёжнее
|
||||
- Phase 2: полноценный D&D через `@dnd-kit`
|
||||
|
||||
---
|
||||
|
||||
## Открытые вопросы
|
||||
|
||||
- **FileWatcher расширение**: FileWatcher.ts уже 900+ строк — добавление teams/tasks watchers нетривиально, требует отдельного спайка
|
||||
- **Windows atomic rename**: `fs.renameSync` на Windows бросает `EXDEV`/`EBUSY` при кросс-устройственном rename — нужна обёртка
|
||||
- **leadSessionId интеграция**: config.json содержит `leadSessionId`, но интеграция с session viewer (переход к сессии лида) — открытый вопрос
|
||||
- **Hard Interrupt**: сообщения доставляются между turns (1-30с задержка). В будущем нужен способ прервать mid-turn
|
||||
- **Архивация**: inbox не чистится автоматически, нужна кнопка "Архивировать"
|
||||
|
||||
## Файловая структура Claude Code
|
||||
|
||||
```
|
||||
~/.claude/
|
||||
├── teams/{teamName}/
|
||||
│ ├── config.json # Конфиг команды (members НЕПОЛНЫЙ!)
|
||||
│ └── inboxes/{memberName}.json # Inbox каждого участника
|
||||
└── tasks/{teamName}/
|
||||
├── {id}.json # Файл задачи
|
||||
├── .lock # Lock-файл (0 байт)
|
||||
└── .highwatermark # Последний ID задачи
|
||||
```
|
||||
|
||||
**ВАЖНО**: config.json members содержит только team-lead. Полный список участников реконструируем из inbox-файлов.
|
||||
2145
docs/team-management/implementation.md
Normal file
2145
docs/team-management/implementation.md
Normal file
File diff suppressed because it is too large
Load diff
232
docs/team-management/kanban-design.md
Normal file
232
docs/team-management/kanban-design.md
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
# Kanban Design
|
||||
|
||||
## Flow
|
||||
|
||||
```
|
||||
TODO → IN PROGRESS → DONE → [юзер] → REVIEW → [юзер] → APPROVED
|
||||
↑ |
|
||||
└── Fix (error) ←──┘
|
||||
```
|
||||
|
||||
## Колонки
|
||||
|
||||
| Колонка | Source | Кто двигает | Описание |
|
||||
|---------|--------|-------------|----------|
|
||||
| **TODO** | task.status = pending | Автоматически | Задачи ожидающие исполнителя |
|
||||
| **IN PROGRESS** | task.status = in_progress | Автоматически | Агент работает |
|
||||
| **DONE** | task.status = completed | Автоматически | Агент завершил |
|
||||
| **REVIEW** | kanban-state.json | Юзер (drag-and-drop) | На проверке |
|
||||
| **APPROVED** | kanban-state.json | Юзер (drag-and-drop) | Одобрено |
|
||||
|
||||
---
|
||||
|
||||
## Kanban State Storage
|
||||
|
||||
### Почему свой файл, а не task metadata
|
||||
|
||||
Metadata в task-файлах может быть **перезаписан** агентом при TaskUpdate. Semantics (merge vs replace) НЕДОКУМЕНТИРОВАН. Если replace — наш kanbanColumn исчезнет.
|
||||
|
||||
### Формат kanban-state.json
|
||||
|
||||
Хранится в директории app config (не в `~/.claude/tasks/`).
|
||||
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
|
||||
```typescript
|
||||
// ПРАВИЛЬНО:
|
||||
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
|
||||
|
||||
### Перемещение DONE → REVIEW
|
||||
|
||||
1. Юзер перетаскивает карточку из DONE в REVIEW
|
||||
2. Проверяем `kanbanState.reviewers[]`
|
||||
3. **Есть ревьюверы**:
|
||||
- Берём первого свободного (round-robin с балансировкой по количеству активных ревью)
|
||||
- Записываем в kanban-state: `{ column: "review", reviewStatus: "pending", reviewer: "agent-name" }`
|
||||
- Отправляем inbox ревьюверу:
|
||||
```json
|
||||
{
|
||||
"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
|
||||
}
|
||||
```
|
||||
4. **Нет ревьюверов**:
|
||||
- Записываем в kanban-state: `{ column: "review", reviewStatus: "pending" }`
|
||||
- Юзер сам ревьювит через UI (кнопки OK / Error)
|
||||
|
||||
### 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
|
||||
|
||||
1. Юзер нажимает кнопку **Request Changes** на карточке в REVIEW
|
||||
2. Появляется ReviewDialog — textarea для описания проблемы (опционально)
|
||||
3. Юзер нажимает "Отправить"
|
||||
4. Действия:
|
||||
- kanban-state: удаляем запись для этой задачи (вернётся в IN PROGRESS по status)
|
||||
- task file: `status = "in_progress"` (atomic write)
|
||||
- Inbox к исходному owner:
|
||||
```json
|
||||
{
|
||||
"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
|
||||
|
||||
Для MVP вместо drag-and-drop используется **click-to-move**: каждая карточка имеет кнопку или select-dropdown для смены колонки. Это проще реализовать и достаточно для первой версии.
|
||||
|
||||
```
|
||||
[Task Card]
|
||||
Subject: Rename package in pubspec.yaml
|
||||
Owner: worker-1
|
||||
[Move to: REVIEW ▼] ← dropdown или кнопка
|
||||
```
|
||||
|
||||
Разрешённые переходы через click-to-move:
|
||||
| Откуда → Куда | Действие |
|
||||
|----------------|----------|
|
||||
| DONE → REVIEW | kanban-state: review + reviewStatus: pending. Inbox ревьюверу если есть |
|
||||
| REVIEW → APPROVED (Approve) | kanban-state: approved |
|
||||
| REVIEW → DONE (Request Changes) | Dialog → task: in_progress, kanban: remove, inbox к owner |
|
||||
| APPROVED → DONE | kanban-state: remove (возвращается в DONE по status) |
|
||||
|
||||
Не разрешено:
|
||||
- TODO → IN PROGRESS (агент берёт сам через TaskUpdate)
|
||||
- IN PROGRESS → DONE (агент завершает сам через TaskUpdate)
|
||||
|
||||
### Phase 2: Полноценный D&D через @dnd-kit
|
||||
|
||||
`@dnd-kit` уже есть в зависимостях проекта (используется для перетаскивания табов). В Phase 2 добавить drag-and-drop для всех разрешённых переходов.
|
||||
|
||||
---
|
||||
|
||||
## Синхронизация при изменении 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[]`.
|
||||
|
||||
Источники ревьюверов:
|
||||
1. **Роль при создании команды**: юзер в промпте указывает "создай agent-review с ролью reviewer"
|
||||
2. **В UI**: кнопка "Назначить ревьювером" рядом с участником в MemberList
|
||||
|
||||
### Round-Robin с балансировкой
|
||||
|
||||
```typescript
|
||||
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];
|
||||
}
|
||||
```
|
||||
242
docs/team-management/research-inbox.md
Normal file
242
docs/team-management/research-inbox.md
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
# Research: Inbox-файлы Claude Code
|
||||
|
||||
## Формат
|
||||
|
||||
**Путь**: `~/.claude/teams/{teamName}/inboxes/{memberName}.json`
|
||||
|
||||
**Структура**: JSON-массив объектов (весь файл = массив)
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"from": "contracts-cleaner",
|
||||
"text": "Готово. Вот что было удалено из packages/core/...",
|
||||
"timestamp": "2026-02-09T17:12:32.316Z",
|
||||
"color": "blue",
|
||||
"read": true,
|
||||
"summary": "Cleanup complete"
|
||||
},
|
||||
{
|
||||
"from": "team-lead",
|
||||
"text": "{\"type\":\"shutdown_request\",\"from\":\"team-lead\",\"timestamp\":\"...\"}",
|
||||
"timestamp": "2026-02-09T17:25:43.886Z",
|
||||
"read": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Поля
|
||||
|
||||
| Поле | Обязательно | Тип | Описание |
|
||||
|------|:-----------:|-----|----------|
|
||||
| `from` | YES | string | Имя отправителя (зарегистрированный agent) |
|
||||
| `text` | YES | string | Текст сообщения или JSON-строка |
|
||||
| `timestamp` | YES | string | ISO 8601 |
|
||||
| `read` | YES | boolean | Прочитано ли Claude Code |
|
||||
| `summary` | NO | string | Краткое описание для UI |
|
||||
| `color` | NO | string | Цвет агента (blue, green, red, yellow, purple, cyan, orange, pink) |
|
||||
|
||||
### text: plain vs structured
|
||||
|
||||
`text` может быть:
|
||||
- **Plain text**: обычное сообщение
|
||||
- **JSON-строка**: структурированное сообщение (парсится из text)
|
||||
|
||||
Типы структурированных:
|
||||
```
|
||||
idle_notification — агент ушёл в idle (idleReason: "available")
|
||||
shutdown_request — запрос на завершение работы
|
||||
shutdown_approved — подтверждение завершения
|
||||
message — обычное DM (content, summary)
|
||||
task_completed — задача завершена (taskId)
|
||||
```
|
||||
|
||||
### read поведение
|
||||
|
||||
- Claude Code ставит `read: true` после обработки
|
||||
- Сообщения НЕ УДАЛЯЮТСЯ — остаются навечно
|
||||
- Нет автоматической чистки inbox
|
||||
- 106 сообщений = ~256 KB
|
||||
|
||||
---
|
||||
|
||||
## Race Condition (КРИТИЧНЫЙ РИСК)
|
||||
|
||||
### Сценарий
|
||||
|
||||
```
|
||||
T=0ms: App читает inbox [msg1, msg2]
|
||||
T=1ms: Claude Code читает inbox [msg1, msg2]
|
||||
T=5ms: App пишет [msg1, msg2, msg3_app]
|
||||
T=6ms: Claude Code пишет [msg1, msg2, msg3_agent]
|
||||
→ msg3_app ПОТЕРЯНО
|
||||
```
|
||||
|
||||
### Почему происходит
|
||||
|
||||
Inbox — JSON-массив. Append = read whole array → add element → write whole file. Два процесса читают одну версию, каждый добавляет своё, последний перезаписывает первого.
|
||||
|
||||
### Вероятность
|
||||
|
||||
**НИЗКАЯ** — записи в inbox происходят нечасто:
|
||||
- Юзер шлёт 1-2 сообщения в минуту
|
||||
- Агенты шлют idle_notification раз в 5-30 секунд
|
||||
- Коллизия = оба пишут в ОДИН файл в пределах ~10ms
|
||||
|
||||
### Митигация
|
||||
|
||||
1. **Atomic write** (предотвращает partial writes):
|
||||
```typescript
|
||||
const tmpPath = targetPath + '.tmp.' + process.pid;
|
||||
fs.writeFileSync(tmpPath, JSON.stringify(messages, null, 2));
|
||||
fs.renameSync(tmpPath, targetPath); // atomic на macOS/Linux
|
||||
```
|
||||
|
||||
2. **Retry с проверкой** (обнаруживает потерю):
|
||||
```typescript
|
||||
// После записи — перечитать и проверить
|
||||
const written = JSON.parse(fs.readFileSync(inboxPath, 'utf8'));
|
||||
const found = written.some(m => m.messageId === ourMessageId);
|
||||
if (!found) {
|
||||
// Наше сообщение потеряно — retry
|
||||
await appendToInbox(inboxPath, message);
|
||||
}
|
||||
```
|
||||
|
||||
3. **Уникальный messageId**: добавлять `messageId: uuid()` в наши сообщения
|
||||
|
||||
4. **Debounce**: не писать чаще раз в 500ms
|
||||
|
||||
### Что НЕ решает
|
||||
|
||||
Atomic write предотвращает corrupted JSON, но НЕ предотвращает overwrite race. Retry с проверкой — best effort, но не 100%.
|
||||
|
||||
---
|
||||
|
||||
## Доставка сообщений
|
||||
|
||||
### Между turns, НЕ real-time
|
||||
|
||||
```
|
||||
Цикл тиммейта:
|
||||
1. Читает inbox (видит новые сообщения с read: false)
|
||||
2. Обрабатывает
|
||||
3. Вызывает инструменты (Bash, Edit, Read...)
|
||||
4. Turn заканчивается → шлёт idle_notification → IDLE
|
||||
5. Ждёт...
|
||||
6. Новый turn → читает inbox
|
||||
```
|
||||
|
||||
**Задержка**: 1-30 секунд (зависит от длительности turn)
|
||||
|
||||
### Нельзя прервать mid-turn
|
||||
|
||||
Если агент уже вызвал Edit/Bash — инструмент будет выполнен. Сообщение "стоп, не трогай файл X" придёт ПОСЛЕ.
|
||||
|
||||
### Idle → Active
|
||||
|
||||
Сообщение idle-агенту пробуждает его при следующем цикле проверки inbox.
|
||||
|
||||
### Hard Interrupt (будущее)
|
||||
|
||||
Возможные подходы:
|
||||
- `kill -SIGINT` процесса (жёсткое)
|
||||
- Файловый flag `.interrupt-{member}`
|
||||
- Ждать API от Anthropic
|
||||
|
||||
---
|
||||
|
||||
## from: "user" — валидация
|
||||
|
||||
### Факты
|
||||
|
||||
- В реальных inbox-файлах видны ТОЛЬКО зарегистрированные agent names
|
||||
- Нет примеров `from: "user"` в 256+ KB данных
|
||||
- Неизвестно, валидирует ли Claude Code поле `from` по config.json members
|
||||
|
||||
### Решение
|
||||
|
||||
Пробуем `from: "user"`. Если агент не получает сообщение → fallback на `from: "team-lead"` (всегда есть в config.json).
|
||||
|
||||
### Тест
|
||||
|
||||
При первой реализации:
|
||||
1. Запустить команду с 1 тиммейтом
|
||||
2. Записать сообщение с `from: "user"` в inbox тиммейта
|
||||
3. Проверить — получит ли тиммейт сообщение
|
||||
4. Если нет — повторить с `from: "team-lead"`
|
||||
|
||||
---
|
||||
|
||||
## Размер и масштабирование
|
||||
|
||||
| Метрика | Значение |
|
||||
|---------|----------|
|
||||
| Размер сообщения | ~2.4 KB |
|
||||
| 100 сообщений | ~240 KB |
|
||||
| 1000 сообщений | ~2.4 MB |
|
||||
| Парсинг 1000 сообщений | <10ms |
|
||||
| Реальный inbox (106 msgs) | 256 KB |
|
||||
|
||||
Проблема начнётся при 10000+ сообщений — JSON.parse будет заметно медленнее. Для долгоживущих команд нужна архивация.
|
||||
|
||||
---
|
||||
|
||||
## Финальное решение (после 3 раундов ревью)
|
||||
|
||||
### Подход: Atomic write + messageId verify
|
||||
|
||||
Выбрана комбинация atomic write с постфактум-верификацией через `messageId`:
|
||||
|
||||
```typescript
|
||||
// 1. Генерируем уникальный ID для нашего сообщения
|
||||
const messageId = crypto.randomUUID();
|
||||
const message: InboxMessage = {
|
||||
from: 'user',
|
||||
text,
|
||||
timestamp: new Date().toISOString(),
|
||||
read: false,
|
||||
summary,
|
||||
messageId,
|
||||
};
|
||||
|
||||
// 2. Читаем текущий inbox
|
||||
const existing = JSON.parse(fs.readFileSync(inboxPath, 'utf8'));
|
||||
|
||||
// 3. Добавляем сообщение
|
||||
const updated = [...existing, message];
|
||||
|
||||
// 4. Atomic write (tmp + rename)
|
||||
const tmpPath = inboxPath + '.tmp.' + process.pid;
|
||||
fs.writeFileSync(tmpPath, JSON.stringify(updated, null, 2));
|
||||
fs.renameSync(tmpPath, inboxPath);
|
||||
|
||||
// 5. Verify: перечитываем и проверяем messageId
|
||||
const written = JSON.parse(fs.readFileSync(inboxPath, 'utf8'));
|
||||
const found = written.some((m: InboxMessage) => m.messageId === messageId);
|
||||
if (!found) {
|
||||
// Потеря обнаружена — показать warning в UI, не silent fail
|
||||
throw new Error(`Message ${messageId} lost during write`);
|
||||
}
|
||||
```
|
||||
|
||||
### Решения по итогам ревью
|
||||
|
||||
- **Полный CAS не нужен на MVP**: verify при следующем read достаточен для обнаружения потерь
|
||||
- **messageId проверяется сразу после записи** (а не только при следующем read)
|
||||
- **Не silent fail**: если сообщение потеряно — UI показывает предупреждение пользователю
|
||||
- **Retry не автоматический**: потеря крайне редка, ручная отправка достаточна на MVP
|
||||
|
||||
### Риски
|
||||
|
||||
| Риск | Вероятность | Митигация |
|
||||
|------|-------------|-----------|
|
||||
| Race condition: агент пишет одновременно | Низкая | Atomic write + verify |
|
||||
| Потеря при race | Очень низкая | messageId verify → warning в UI |
|
||||
| Corrupted JSON | Практически 0 | Atomic write (tmp + rename) |
|
||||
|
||||
### Что не входит в MVP
|
||||
|
||||
- Автоматический retry при потере (добавить в Phase 2 при необходимости)
|
||||
- Debounce записи (не нужен при редкой записи)
|
||||
- Полный CAS с блокировкой (избыточно для данной частоты записей)
|
||||
189
docs/team-management/research-messaging.md
Normal file
189
docs/team-management/research-messaging.md
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
# Research: Подходы к отправке сообщений тиммейтам
|
||||
|
||||
## Сравнение 3 подходов
|
||||
|
||||
| Критерий | Inbox-файлы | Agent SDK | CLI subprocess |
|
||||
|----------|:-----------:|:---------:|:--------------:|
|
||||
| Скорость | ~5ms | ~12с | 10-15с |
|
||||
| Стоимость | $0 | $0.01-0.08/msg | токены |
|
||||
| Работает с запущенными | **YES** | NO | NO |
|
||||
| Прерывает mid-turn | NO | NO | NO |
|
||||
| Требует API ключ | NO | YES | NO |
|
||||
| Расход памяти | 0 | 0 | 100-320MB |
|
||||
|
||||
---
|
||||
|
||||
## 1. Inbox-файлы (ВЫБРАНО)
|
||||
|
||||
### Как работает
|
||||
|
||||
Прямая запись JSON в файл `~/.claude/teams/{team}/inboxes/{member}.json`. Claude Code мониторит эти файлы через fs.watch и доставляет сообщения агентам между turns.
|
||||
|
||||
### Плюсы
|
||||
|
||||
- **Мгновенная запись** (~5ms)
|
||||
- **$0** — никаких API вызовов
|
||||
- **Единственный** способ общаться с запущенными тиммейтами
|
||||
- Работает с idle и active агентами (но доставка между turns)
|
||||
|
||||
### Минусы
|
||||
|
||||
- Race condition при одновременной записи (см. [research-inbox.md](./research-inbox.md))
|
||||
- Формат недокументирован (internal API)
|
||||
- Доставка между turns, не real-time
|
||||
- from: "user" может не работать
|
||||
|
||||
### Формат сообщения
|
||||
|
||||
```json
|
||||
{
|
||||
"from": "user",
|
||||
"text": "Не трогай файл auth.ts, я его сам изменю",
|
||||
"timestamp": "2026-02-17T15:30:00.000Z",
|
||||
"read": false,
|
||||
"summary": "Do not modify auth.ts",
|
||||
"messageId": "uuid-for-retry-check"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Agent SDK (ОТВЕРГНУТ)
|
||||
|
||||
### Как работает
|
||||
|
||||
```typescript
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
const client = new Anthropic();
|
||||
const response = await client.messages.create({
|
||||
model: 'claude-opus-4-6',
|
||||
messages: [{ role: 'user', content: 'Send message to teammate...' }],
|
||||
tools: [/* SendMessage, TaskUpdate, etc. */]
|
||||
});
|
||||
```
|
||||
|
||||
### Почему отвергнут
|
||||
|
||||
1. **Создаёт НОВУЮ сессию** — не подключается к работающему тиммейту. SendMessage и TaskCreate — это инструменты модели, не программные вызовы
|
||||
2. **~12 секунд** на каждый вызов (полный API round-trip)
|
||||
3. **Стоит токены** — $0.01-0.08 за сообщение
|
||||
4. **Нужен API ключ** — отдельная оплата, а не подписка Claude
|
||||
|
||||
### Когда может пригодиться
|
||||
|
||||
- Создание новых команд программно
|
||||
- Автоматизация workflow (вне real-time UI)
|
||||
|
||||
---
|
||||
|
||||
## 3. CLI subprocess (ОТВЕРГНУТ)
|
||||
|
||||
### Как работает
|
||||
|
||||
```bash
|
||||
claude --message "Send message to teammate-1: stop working on X"
|
||||
```
|
||||
|
||||
### Почему отвергнут
|
||||
|
||||
1. **Новый процесс** — не инжектится в работающего тиммейта
|
||||
2. **10-15 секунд** холодный старт
|
||||
3. **100-320MB памяти** на процесс
|
||||
4. Каждый вызов стоит токены
|
||||
|
||||
---
|
||||
|
||||
## Доставка: Timing и ограничения
|
||||
|
||||
### Цикл тиммейта
|
||||
|
||||
```
|
||||
Turn N:
|
||||
1. Читает inbox → видит новые (read: false)
|
||||
2. Обрабатывает сообщения/задачи
|
||||
3. Вызывает инструменты
|
||||
4. Reasoning
|
||||
5. Output
|
||||
→ idle_notification → IDLE
|
||||
|
||||
... ожидание ...
|
||||
|
||||
Turn N+1:
|
||||
1. Пробуждение (новое сообщение в inbox / назначение задачи)
|
||||
2. Читает inbox → видит новые
|
||||
...
|
||||
```
|
||||
|
||||
### Задержка
|
||||
|
||||
- **Idle agent**: получит при следующем пробуждении (доли секунды если inbox-change triggers)
|
||||
- **Active agent (mid-turn)**: получит только после завершения текущего turn (1-30 секунд)
|
||||
|
||||
### Нельзя прервать
|
||||
|
||||
Если агент уже вызвал Edit/Bash — инструмент выполнится. Наше сообщение придёт ПОСЛЕ.
|
||||
|
||||
**Пример**:
|
||||
```
|
||||
17:12:30 — Agent начинает Edit на auth.ts
|
||||
17:12:31 — Мы шлём "Не трогай auth.ts"
|
||||
17:12:32 — Agent завершает Edit (auth.ts изменён)
|
||||
17:12:33 — Agent читает inbox, видит наше сообщение
|
||||
→ Поздно, файл уже изменён
|
||||
```
|
||||
|
||||
### Hard Interrupt (будущее)
|
||||
|
||||
Возможные подходы:
|
||||
1. **kill -SIGINT** процесса тиммейта (жёсткое прерывание, потеря контекста)
|
||||
2. **Файловый flag** `.interrupt-{member}` (нужна поддержка в Claude Code)
|
||||
3. **API от Anthropic** (если появится)
|
||||
|
||||
Текущее решение: задержка приемлема, hard interrupt — в будущем.
|
||||
|
||||
---
|
||||
|
||||
## Финальное решение (после 3 раундов ревью)
|
||||
|
||||
### Поле from
|
||||
|
||||
- Используем `from: "user"` — интуитивно и описывает источник
|
||||
- Fallback `from: "team-lead"` если агент не реагирует (team-lead всегда есть в config.json members)
|
||||
- Практический тест необходим при первой реализации (см. [research-inbox.md](./research-inbox.md))
|
||||
|
||||
### messageId — обязателен в каждом сообщении
|
||||
|
||||
Каждое исходящее сообщение включает `messageId: crypto.randomUUID()`:
|
||||
|
||||
```json
|
||||
{
|
||||
"from": "user",
|
||||
"text": "Please review task #12",
|
||||
"timestamp": "2026-02-17T15:30:00.000Z",
|
||||
"read": false,
|
||||
"summary": "Review request for task #12",
|
||||
"messageId": "550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
```
|
||||
|
||||
### Verify: проверка сразу после записи
|
||||
|
||||
- После atomic write читаем inbox и ищем наш `messageId`
|
||||
- Если не найден — потеря обнаружена → warning в UI (не silent fail)
|
||||
- Не автоматический retry на MVP
|
||||
|
||||
### 3 состояния offline-участника
|
||||
|
||||
| Состояние | Условие | Отображение |
|
||||
|-----------|---------|-------------|
|
||||
| `ACTIVE` | idle < 5 минут | Зелёный dot |
|
||||
| `IDLE` | idle > 5 минут | Жёлтый dot |
|
||||
| `TERMINATED` | Получен `shutdown_response` с `approve: true` | Серый dot, "Завершён" |
|
||||
|
||||
Определение состояния по timestamp последнего события в inbox (idle_notification, любое сообщение). TERMINATED — исключительно по явному `shutdown_response`.
|
||||
|
||||
### Что не входит в MVP
|
||||
|
||||
- Автоматический retry при потере сообщения
|
||||
- `from: "user"` validation через config.json members (проверяем практически)
|
||||
- Hard Interrupt (kill -SIGINT, файловый flag) — Phase 2
|
||||
158
docs/team-management/research-tasks.md
Normal file
158
docs/team-management/research-tasks.md
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
# Research: Task-файлы Claude Code
|
||||
|
||||
## Формат
|
||||
|
||||
**Путь**: `~/.claude/tasks/{teamName}/{id}.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "48",
|
||||
"subject": "Rename package in pubspec.yaml",
|
||||
"description": "Change name: dartdoc to name: dartdoc_vitepress",
|
||||
"activeForm": "Updating pubspec.yaml",
|
||||
"owner": "senior-1-rename",
|
||||
"status": "completed",
|
||||
"blocks": [],
|
||||
"blockedBy": [],
|
||||
"metadata": { "_internal": true }
|
||||
}
|
||||
```
|
||||
|
||||
### Поля
|
||||
|
||||
| Поле | Обязательно | Тип | Описание |
|
||||
|------|:-----------:|-----|----------|
|
||||
| `id` | YES | string | Числовой ID в виде строки ("1", "48") |
|
||||
| `subject` | YES | string | Краткий заголовок задачи |
|
||||
| `description` | NO | string | Детальное описание |
|
||||
| `activeForm` | NO | string | Present continuous для спиннера ("Updating...") |
|
||||
| `owner` | NO | string | Имя агента-владельца |
|
||||
| `status` | YES | string | pending / in_progress / completed / deleted |
|
||||
| `blocks` | NO | string[] | ID задач, которые зависят от этой |
|
||||
| `blockedBy` | NO | string[] | ID задач, от которых зависит эта |
|
||||
| `metadata` | NO | object | Произвольный объект |
|
||||
|
||||
### Статусы
|
||||
|
||||
```
|
||||
pending — задача создана, ждёт исполнителя
|
||||
in_progress — агент работает над задачей
|
||||
completed — агент завершил
|
||||
deleted — задача удалена
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## .highwatermark
|
||||
|
||||
**Путь**: `~/.claude/tasks/{teamName}/.highwatermark`
|
||||
**Содержимое**: число (последний выданный ID)
|
||||
|
||||
### Как используется
|
||||
|
||||
```
|
||||
1. TaskCreate → читает .highwatermark (например, "48")
|
||||
2. Новый ID = 49
|
||||
3. Создаёт 49.json
|
||||
4. Записывает "49" в .highwatermark
|
||||
```
|
||||
|
||||
### Риск при внешней записи
|
||||
|
||||
Если мы создаём задачу с ID=50, но .highwatermark = 48:
|
||||
- Следующий TaskCreate создаст 49.json (ОК)
|
||||
- Ещё один — 50.json → **ПЕРЕЗАПИШЕТ нашу задачу**
|
||||
|
||||
**Решение**: Не создавать задачи напрямую. Мы только ЧИТАЕМ задачи и модифицируем status/metadata.
|
||||
|
||||
---
|
||||
|
||||
## .lock файл
|
||||
|
||||
**Путь**: `~/.claude/tasks/{teamName}/.lock`
|
||||
**Содержимое**: пустой файл (0 байт)
|
||||
|
||||
### Поведение
|
||||
|
||||
- Обновляется (touch) при обращении к tasks
|
||||
- Механизм блокировки НЕДОКУМЕНТИРОВАН
|
||||
- Вероятно используется как advisory lock (наличие = кто-то работает)
|
||||
|
||||
### Наш подход
|
||||
|
||||
Не трогаем .lock. Он для Claude Code. Мы делаем atomic write (tmp + rename) что безопасно без lock.
|
||||
|
||||
---
|
||||
|
||||
## Видимость для агентов
|
||||
|
||||
| Инструмент | Видит status | Видит metadata | Видит owner | Видит blockedBy |
|
||||
|------------|:---:|:---:|:---:|:---:|
|
||||
| TaskList | YES | **NO** | YES | YES |
|
||||
| TaskGet | YES | YES | YES | YES |
|
||||
| TaskUpdate | Меняет | Меняет | Меняет | Меняет |
|
||||
|
||||
**Следствие**: Агент через TaskList НЕ увидит наш kanbanColumn в metadata. Поэтому хранить kanban-состояние в metadata бессмысленно для агентов, и рискованно (может быть перезаписано).
|
||||
|
||||
---
|
||||
|
||||
## metadata: merge vs replace
|
||||
|
||||
### Проблема
|
||||
|
||||
**НЕДОКУМЕНТИРОВАНО.** Неизвестно что происходит при:
|
||||
```typescript
|
||||
// Текущее состояние:
|
||||
{ metadata: { kanbanColumn: "REVIEW", _internal: true } }
|
||||
|
||||
// Агент делает TaskUpdate:
|
||||
TaskUpdate({ metadata: { owner_note: "done" } })
|
||||
|
||||
// Результат ???
|
||||
// MERGE: { kanbanColumn: "REVIEW", _internal: true, owner_note: "done" }
|
||||
// REPLACE: { owner_note: "done" } ← kanbanColumn ПОТЕРЯНО
|
||||
```
|
||||
|
||||
### Решение
|
||||
|
||||
Не полагаемся на metadata. Kanban-состояние храним в собственном файле `kanban-state.json`.
|
||||
|
||||
---
|
||||
|
||||
## Конкурентный доступ
|
||||
|
||||
### Сценарий
|
||||
|
||||
```
|
||||
T=0: App читает task.json: { status: "completed", metadata: {} }
|
||||
T=1: Agent читает тот же файл: { status: "completed", metadata: {} }
|
||||
T=5: App пишет: { status: "in_progress", metadata: {} } (Fix)
|
||||
T=6: Agent пишет: { status: "completed", metadata: { note: "done" } }
|
||||
→ Наш status: "in_progress" ПОТЕРЯН
|
||||
```
|
||||
|
||||
### Вероятность
|
||||
|
||||
**СРЕДНЯЯ** — мы пишем в task только при Fix (status → in_progress). Но агент может одновременно обновлять тот же файл.
|
||||
|
||||
### Митигация
|
||||
|
||||
1. **Atomic write** (tmp + rename)
|
||||
2. **Verify after write**: перечитать файл, проверить что status = наш
|
||||
3. **File watcher**: если status изменился обратно → показать warning в UI
|
||||
|
||||
### Что пишем в task-файлы
|
||||
|
||||
Минимум — только `status` при Fix:
|
||||
```typescript
|
||||
// Читаем текущий task
|
||||
const task = JSON.parse(fs.readFileSync(taskPath));
|
||||
// Меняем только status
|
||||
task.status = 'in_progress';
|
||||
// Atomic write
|
||||
const tmp = taskPath + '.tmp.' + process.pid;
|
||||
fs.writeFileSync(tmp, JSON.stringify(task, null, 2));
|
||||
fs.renameSync(tmp, taskPath);
|
||||
```
|
||||
|
||||
Всё остальное (kanbanColumn, reviewStatus) — в нашем kanban-state.json.
|
||||
254
docs/team-management/research-worktrees.md
Normal file
254
docs/team-management/research-worktrees.md
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
# Research: Git Worktrees + Agent Teams + Process Launch
|
||||
|
||||
## Статус: Phase 2 (после базовой реализации Team Management)
|
||||
|
||||
---
|
||||
|
||||
## ЧАСТЬ 1: Текущая ситуация
|
||||
|
||||
### Agent Teams работают в ОДНОМ каталоге
|
||||
|
||||
Все тиммейты шарят один `cwd` — нет встроенной поддержки worktrees для команд.
|
||||
|
||||
```
|
||||
Lead + Teammate-1 + Teammate-2 → все в /project/
|
||||
→ Два агента могут редактировать один файл → перезапись
|
||||
→ "Решение" Claude Code: лид вручную назначает разные файлы разным агентам
|
||||
```
|
||||
|
||||
### Worktrees в Claude Code — отдельный механизм
|
||||
|
||||
Worktrees существуют как способ **ручного параллелизма пользователя**:
|
||||
```bash
|
||||
git worktree add ../project-feature-a -b feature-a
|
||||
cd ../project-feature-a
|
||||
claude # Новая независимая сессия
|
||||
```
|
||||
|
||||
Это НЕ связано с Agent Teams. Каждый worktree = отдельный процесс claude, никакой координации.
|
||||
|
||||
### У Task tool нет параметра cwd
|
||||
|
||||
При спавне тиммейта через Task нельзя указать рабочую директорию. Тиммейт наследует `cwd` лидера.
|
||||
|
||||
---
|
||||
|
||||
## ЧАСТЬ 2: Существующая инфраструктура в claude-devtools
|
||||
|
||||
### Уже реализовано (можно переиспользовать)
|
||||
|
||||
| Компонент | Файл | Что делает |
|
||||
|-----------|------|-----------|
|
||||
| `GitIdentityResolver` | `src/main/services/parsing/GitIdentityResolver.ts` | Определяет worktree vs main repo, извлекает branch, remote URL |
|
||||
| `WorktreeGrouper` | `src/main/services/discovery/WorktreeGrouper.ts` | Группирует проекты по git repository |
|
||||
| `WorktreeBadge` | `src/renderer/components/common/WorktreeBadge.tsx` | UI badge для типа worktree (8 типов) |
|
||||
| `repositorySlice` | `src/renderer/store/slices/repositorySlice.ts` | Store: repos, worktrees, grouped/flat view |
|
||||
| `worktreePatterns` | `src/main/constants/worktreePatterns.ts` | 8 типов: vibe-kanban, conductor, auto-claude, 21st, claude-desktop, ccswitch, git, unknown |
|
||||
| `SidebarHeader` | `src/renderer/components/layout/SidebarHeader.tsx` | Dropdown с repo → worktree навигацией |
|
||||
|
||||
### config.json содержит cwd у members
|
||||
|
||||
```json
|
||||
{
|
||||
"members": [
|
||||
{
|
||||
"name": "team-lead",
|
||||
"cwd": "/Users/belief/dev/projects/modularity" // Все members имеют ОДИНАКОВЫЙ cwd
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Поле есть, но все members указывают одну директорию.
|
||||
|
||||
---
|
||||
|
||||
## ЧАСТЬ 3: Идея — Запуск Claude Process из UI
|
||||
|
||||
### Суть
|
||||
|
||||
Пользователь хочет не просто **просматривать** команды, но и **запускать** Claude Code процессы прямо из Electron UI. С предконфигурацией:
|
||||
|
||||
1. Выбрать рабочую директорию (может быть worktree)
|
||||
2. Настроить agent teams заранее
|
||||
3. Запустить claude процесс с нужными параметрами
|
||||
4. Worktree создаётся автоматически перед запуском
|
||||
|
||||
### Сценарии использования
|
||||
|
||||
#### Сценарий 1: Запуск одиночного Claude
|
||||
|
||||
```
|
||||
UI: [Выбрать директорию] → [Запустить Claude]
|
||||
→ spawn('claude', [], { cwd: '/selected/path' })
|
||||
```
|
||||
|
||||
#### Сценарий 2: Запуск с предсозданным worktree
|
||||
|
||||
```
|
||||
UI: [Выбрать репо] → [Создать worktree для branch X] → [Запустить Claude в worktree]
|
||||
→ git worktree add ../project-branch-x -b branch-x
|
||||
→ spawn('claude', [], { cwd: '../project-branch-x' })
|
||||
```
|
||||
|
||||
#### Сценарий 3: Запуск команды с worktrees для каждого тиммейта
|
||||
|
||||
```
|
||||
UI: [Настроить команду]
|
||||
- Lead: /project (main)
|
||||
- Teammate-1: /project-wt1 (branch auth)
|
||||
- Teammate-2: /project-wt2 (branch api)
|
||||
|
||||
→ git worktree add ../project-wt1 -b auth
|
||||
→ git worktree add ../project-wt2 -b api
|
||||
→ spawn('claude', ['--prompt', 'Create team... Teammate-1 works in /project-wt1...'], { cwd: '/project' })
|
||||
```
|
||||
|
||||
#### Сценарий 4: Worktree per-task
|
||||
|
||||
```
|
||||
UI: [Создать задачу] → [Auto-create worktree] → [Assign to teammate]
|
||||
→ git worktree add ../project-task-15 -b task-15
|
||||
→ Prompt: "For task #15, work in /project-task-15"
|
||||
```
|
||||
|
||||
### Технические вопросы
|
||||
|
||||
#### Как запустить claude из Electron?
|
||||
|
||||
```typescript
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
const claude = spawn('claude', ['--prompt', initialPrompt], {
|
||||
cwd: workingDirectory,
|
||||
env: {
|
||||
...process.env,
|
||||
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1', // Если нужны teams
|
||||
},
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
// Слушаем stdout для отслеживания прогресса?
|
||||
claude.stdout.on('data', (data) => { /* ... */ });
|
||||
```
|
||||
|
||||
**Вопросы**:
|
||||
- claude запускается как interactive CLI. Можно ли передать начальный промпт через args?
|
||||
- Есть ли headless/non-interactive режим? (`claude --message "..."` — да, но одноразовый)
|
||||
- Как мониторить процесс после запуска?
|
||||
- Как связать запущенный процесс с нашим UI?
|
||||
|
||||
#### Claude CLI аргументы (известные)
|
||||
|
||||
```bash
|
||||
claude # Interactive REPL
|
||||
claude "prompt here" # One-shot (non-interactive? needs check)
|
||||
claude --resume <session-id> # Resume session
|
||||
claude --continue # Continue last session
|
||||
claude -p "prompt" # Print mode (non-interactive, output to stdout)
|
||||
```
|
||||
|
||||
**Нужно исследовать**:
|
||||
- Как запустить interactive сессию с начальным промптом
|
||||
- Можно ли передать agent teams конфигурацию через аргументы
|
||||
- Как получить session ID запущенного процесса
|
||||
|
||||
#### Worktree lifecycle
|
||||
|
||||
```
|
||||
Создание: git worktree add <path> -b <branch>
|
||||
Удаление: git worktree remove <path>
|
||||
Список: git worktree list
|
||||
```
|
||||
|
||||
**Вопросы**:
|
||||
- Когда удалять worktree? После завершения задачи/команды?
|
||||
- Как мержить изменения из worktree обратно в main?
|
||||
- Что если worktree "зависает" (процесс упал)?
|
||||
|
||||
---
|
||||
|
||||
## ЧАСТЬ 4: Варианты архитектуры
|
||||
|
||||
### A: Prompt Injection (простой, хрупкий)
|
||||
|
||||
```
|
||||
1. UI создаёт worktrees
|
||||
2. В .claude/rules/worktrees.md: "Teammate X must cd to /path"
|
||||
3. Агент (надеемся) делает cd
|
||||
```
|
||||
|
||||
**Compliance**: ~70-80%
|
||||
**Сложность**: низкая
|
||||
**Риск**: агент забудет cd, или cd обратно
|
||||
|
||||
### B: Отдельные сессии (надёжный, без Teams)
|
||||
|
||||
```
|
||||
1. UI создаёт worktrees
|
||||
2. Запускает N отдельных claude процессов в разных worktrees
|
||||
3. Координация через наш UI (Kanban, inbox messaging)
|
||||
4. Нет встроенных Teams инструментов
|
||||
```
|
||||
|
||||
**Isolation**: 100%
|
||||
**Сложность**: высокая (наша координация вместо Teams)
|
||||
**Плюс**: полный контроль
|
||||
|
||||
### C: Гибрид (3 режима)
|
||||
|
||||
```
|
||||
shared — один каталог (текущее поведение Teams)
|
||||
per-teammate — worktree на каждого тиммейта
|
||||
per-task — worktree на каждую задачу
|
||||
```
|
||||
|
||||
Юзер выбирает режим при создании команды. UI создаёт worktrees и инжектирует правила.
|
||||
|
||||
### D: Только визуализация (безопасный)
|
||||
|
||||
```
|
||||
Не управляем worktrees
|
||||
Только показываем какой тиммейт в каком каталоге
|
||||
Группировка через repositorySlice
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ЧАСТЬ 5: Открытые вопросы для Phase 2
|
||||
|
||||
1. **Как запустить claude с начальным промптом из Electron?**
|
||||
- Нужно исследовать CLI аргументы подробнее
|
||||
- Есть ли способ передать initial team config?
|
||||
|
||||
2. **Можно ли контролировать cwd тиммейта?**
|
||||
- Через промпт lead'а: "spawn teammate in /path/to/worktree"
|
||||
- Через config.json manipulation?
|
||||
- Compliance?
|
||||
|
||||
3. **Worktree cleanup**
|
||||
- Автоматическое удаление при завершении команды?
|
||||
- UI кнопка "Cleanup worktrees"?
|
||||
- Что делать с uncommitted changes?
|
||||
|
||||
4. **Merge strategy**
|
||||
- Как мержить worktree branches обратно?
|
||||
- Кнопка "Merge all" в UI?
|
||||
- Conflict resolution?
|
||||
|
||||
5. **Process monitoring**
|
||||
- Как отслеживать запущенный claude процесс?
|
||||
- Как получить его session ID?
|
||||
- Как определить что процесс завершился?
|
||||
|
||||
6. **Integration с Team Management**
|
||||
- Worktree mode как часть team creation flow?
|
||||
- Или отдельная фича?
|
||||
- Как связать worktree с конкретным тиммейтом/задачей?
|
||||
|
||||
---
|
||||
|
||||
## Решение
|
||||
|
||||
**Phase 2** — после базовой реализации Team Management (Kanban, messaging, members).
|
||||
Требует дополнительного ресёрча по claude CLI аргументам и process management.
|
||||
Loading…
Reference in a new issue