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:
iliya 2026-02-17 21:10:15 +02:00
parent bd088ec71c
commit e7d9e82ce8
9 changed files with 3684 additions and 0 deletions

14
docs/iterations/README.md Normal file
View 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**: заранее фиксируем критерии готовности и ручную проверку

View 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” 23 раза
- Ожидание: не создаются новые табы, фокус остаётся на существующем `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)
- База для следующих итераций без переархитектуривания

View 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-файлов.

File diff suppressed because it is too large Load diff

View 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];
}
```

View 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 с блокировкой (избыточно для данной частоты записей)

View 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

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

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