17 KiB
Итерация 06 — Team Provisioning (Create Team из UI через CLI + File Monitoring)
Эта итерация добавляет создание команды из UI: пользователь задаёт параметры команды, приложение запускает CLI-процесс, затем верифицирует результат через файлы в ~/.claude/teams/** и сообщает прогресс в UI.
Основание:
docs/team-management/research-cli-orchestration.md(v2) — итоговая схема spawn + monitoring, PATH на macOSdocs/team-management/README.md— важные ограничения формата (config.jsonmembers неполный)- Текущая реализация Team Management (итерации 01–04):
team:list,team:getData,team:change,team:sendMessage,team:requestReview,team:updateKanban
Цель итерации
- В
Teamsпоявляется кнопка Create Team. - Открывается диалог создания команды:
teamName(канонический ID = имя директории)displayName(пишется вconfig.json.name, по умолчанию =teamName)description(опционально)members(минимум 1 имя; роли опциональны — только для prompt)cwd(рабочая директория проекта; выбирается через нативный folder picker)
- По нажатию Create:
- main process валидирует входные данные и находит путь к CLI бинарнику (macOS PATH fix).
- запускает CLI в одноходовом режиме (
-p) с envCLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1. - UI получает стрим прогресса:
validating → spawning → monitoring → verifying → ready/failed/cancelled.
- Команда считается созданной (готовой к просмотру в UI), когда:
~/.claude/teams/{teamName}/config.jsonсуществует и является валидным JSON, содержащимname: string.team:listначинает возвращать эту команду.
Не-цели (строго вне scope)
- Создание/управление задачами через provisioning (TaskCreate, seed tasks) — только в prompt как best-effort, без жёсткой верификации.
- Гарантия, что teammates “точно запущены” и отвечают — MVP provisioning считает успехом создание команды (config), остальное — последующие итерации/полировка.
- Поддержка provisioning в SSH-context (remote) — только local context.
- Парсинг stdout CLI (
stream-json, NDJSON) — не делаем, чтобы исключить backpressure/encoding/64KB буферы. - Авто-удаление существующих директорий команды.
- Интеграция
leadSessionIdс session viewer (это отдельная задача).
Допущения и ограничения (явно)
- Provisioning работает только в local context.
- CLI provisioning не гарантирует, что все участники сразу “живые” и отвечают. MVP-критерий успеха — созданная команда (валидный
config.json). config.json.membersне является источником полного списка участников. Полный roster будет восстанавливаться как и сейчас:union(config members + inbox filenames + task owners).- Любые частичные артефакты на диске после
cancelled/failedне удаляются автоматически.
Контракт итерации (Main → Preload → Renderer)
IPC каналы (flat export const в src/preload/constants/ipcChannels.ts)
TEAM_CREATE = 'team:create'TEAM_PROVISIONING_STATUS = 'team:provisioningStatus'TEAM_PROVISIONING_PROGRESS = 'team:provisioningProgress'(event main → renderer)
Shared types (src/shared/types/team.ts)
Добавляем:
export type TeamProvisioningState =
| 'idle'
| 'validating'
| 'spawning'
| 'monitoring'
| 'verifying'
| 'ready'
| 'failed'
| 'cancelled';
export interface TeamProvisioningMemberInput {
name: string;
role?: string;
}
export interface TeamCreateRequest {
teamName: string; // canonical ID (directory name)
displayName?: string; // config.json.name (default = teamName)
description?: string;
members: TeamProvisioningMemberInput[];
cwd: string; // project working directory
}
export interface TeamCreateResponse {
runId: string;
}
export interface TeamProvisioningProgress {
runId: string;
teamName: string;
state: Exclude<TeamProvisioningState, 'idle'>;
message: string;
startedAt: string; // ISO
updatedAt: string; // ISO
pid?: number;
error?: string;
warnings?: string[];
}
Семантика IPC (без двусмысленностей)
team:create(request):- валидирует
teamName,cwd,members(см. правила валидации ниже) - если команда уже существует (
~/.claude/teams/{teamName}/config.jsonсуществует) →IpcResulterror (“Team already exists”) - если provisioning уже запущен для этого
teamName→IpcResulterror (“Provisioning already running”) - иначе стартует provisioning и возвращает
{ runId }
- валидирует
team:provisioningStatus(runId):- возвращает текущий snapshot прогресса (или ошибку “Unknown runId”)
team:provisioningProgress(event):- отправляется при каждом переходе state machine (и при важных milestones внутри шага)
- payload всегда включает
runId, чтобы UI мог безопасно фильтровать
Правила валидации (без двусмысленностей)
teamName:- строка, trim не пустой
- только
[a-z0-9-], длина 1..64 - не может начинаться/заканчиваться на
- - не может содержать
..,/,\, пробелы
cwd:- абсолютный путь
- должен существовать и быть директорией
members[].name:- строка, trim не пустой
- валидируем тем же правилом, что и для inbox member name (чтобы любые best-effort проверки файлов не открывали путь traversal)
- дубликаты по имени запрещены
Архитектура provisioning (main)
State machine
idle
→ validating
→ spawning
→ monitoring
→ verifying
→ ready
↘ failed
↘ cancelled
Binary resolution (macOS PATH проблема)
Добавляем ClaudeBinaryResolver (main):
- сперва пробуем
claudeв текущем PATH (на случай запуска из терминала) - затем common paths (
/usr/local/bin/claude,/opt/homebrew/bin/claude,~/.npm-global/bin/claude, nvm paths, Windows.cmd) - если не найдено → понятная ошибка для UI (“Claude CLI не найден; установите или укажите путь”)
- запрещено использовать
shell: trueдля любых вызовов
Spawn contract (одноходовый, без stdout)
Запуск:
- args:
['-p', prompt, '--output-format', 'text'] - env:
...process.env+CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 cwd = request.cwdstdio = ['ignore', 'ignore', 'pipe']- stdin ignored (неинтерактивно)
- stdout ignored (не читаем и не рискуем deadlock’ом)
- stderr piped (только для диагностики)
Важно (без двусмысленностей): stderr pipe обязателен к drain’у.
- мы всегда читаем
child.stderr.on('data')и:- складываем в ring-buffer (например, последние 64KB), чтобы не держать бесконечный лог в памяти
- используем для
failed.error(обрезанный, безопасный текст)
Таймауты и зависания (без двусмысленностей)
- Если процесс не завершился за
T_TOTAL(например 2 минуты) → переводим вfailedс ошибкой “Timed out waiting for CLI”. - В тексте ошибки обязателен хинт:
- CLI может требовать интерактивную авторизацию/онбординг
- нужно запустить
claudeодин раз из терминала и завершить онбординг, затем повторить provisioning
Cancel semantics (без двусмысленностей)
- Cancel доступен только в состояниях:
spawning | monitoring | verifying. - При cancel:
- если child process ещё существует → посылаем завершение (best-effort)
- state становится
cancelled - любые созданные файлы/директории не удаляем
Monitoring + verifying (через файлы)
Monitoring триггерится после exit code === 0 (или сразу, если config появляется раньше).
Проверки:
config.jsonсуществует по пути~/.claude/teams/{teamName}/config.json- JSON валиден
config.name— непустая строка
Оговорка: config.json.members не является источником полного состава команды (см. docs/team-management/README.md). Поэтому эта итерация не требует полного roster в config как критерий успеха.
Дополнительная best-effort верификация (не блокирует ready):
- если prompt включает “bootstrap DM” каждому участнику, то ожидаем появление
inboxes/{member}.json - при таймауте:
ready+warnings: ['Some inboxes not created yet']
Разрешение конфликтов сигналов (без двусмысленностей)
- Если
config.jsonстал валидным и прошёл verify, но процесс CLI завершился с non-zero:- итоговое состояние:
ready warnings: добавить строку вида “CLI exited with code X after config was created”
- итоговое состояние:
- Если процесс CLI завершился
0, ноconfig.jsonне появился/невалиден до таймаута verify:- итоговое состояние:
failed - error: “TeamCreate did not produce a valid config.json”
- итоговое состояние:
Изменения в UI (renderer)
Точка входа
- В
TeamListViewдобавляем кнопку Create Team. - В browser-mode и SSH-context:
- кнопка disabled + подпись “Доступно только в local Electron-режиме”.
CreateTeamDialog (минимальный UX)
- Валидация на клиенте:
teamName: kebab-case[a-z0-9-](сообщение об ошибке рядом с полем)members: минимум 1; имена не пустыеcwd: обязателен
- После старта provisioning:
- показываем шаги прогресса
- показываем кнопку Cancel (если процесс ещё в
spawning/monitoring/verifying) - при
ready: кнопка “Open Team” (открывает team tab) - при
failed: показываем error + “Retry” (повторно вызывает create; если команда уже создана — предлагаем открыть)
Store contract (Zustand)
Добавляем в teamSlice:
provisioningRuns: Record<runId, TeamProvisioningProgress>activeProvisioningRunId: string | null- actions:
createTeam(req): Promise<string /* runId */>cancelProvisioning(runId): Promise<void>(если делаем cancel в этой итерации)- subscribe/unsubscribe на
team:provisioningProgress
Выходные изменения (файлы)
Новые файлы (ожидаемо)
src/main/services/team/ClaudeBinaryResolver.tssrc/main/services/team/TeamProvisioningService.tssrc/shared/types(расширениеteam.tsprovisioning-типами)src/renderer/components/team/dialogs/CreateTeamDialog.tsx
Изменяемые файлы (ожидаемо)
src/preload/constants/ipcChannels.ts(новые каналы)src/shared/types/api.ts(TeamsAPI:createTeam,getProvisioningStatus,onProvisioningProgress)src/preload/index.ts(bridge)src/main/ipc/teams.ts(+ handlers)src/main/ipc/guards.ts(валидацияcwdи members, если нужно)src/renderer/components/team/TeamListView.tsx(кнопка + диалог)src/renderer/store/slices/teamSlice.ts(state + actions)
Порядок работ (runbook) с контрольными точками
CP0 — типы и IPC компилируются
- Расширить shared types (provisioning contract).
- Добавить IPC channels constants.
pnpm typecheck
CP1 — main provisioning skeleton (без UI)
ClaudeBinaryResolver(безshell: true).TeamProvisioningService:- lock по
teamName - state machine
- spawn contract (
stdio+ stderr drain + timeout) - monitoring/verifying по
config.json
- lock по
- IPC handlers
team:create,team:provisioningStatus, progress event. pnpm test+pnpm typecheck
CP2 — preload bridge + renderer store
TeamsAPIрасширение + preload bridge.teamSliceactions + подписка на progress event.pnpm typecheck
CP3 — UI dialog
CreateTeamDialog+ интеграция вTeamListView.- Ручная проверка (см. ниже).
pnpm test+pnpm typecheck
Manual regression checklist (обязательно)
- Local mode: Create Team → прогресс до
ready, команда появляется в списке. - Ошибка “CLI не найден” на macOS Finder-запуске воспроизводится → UI показывает понятное сообщение.
- Cancel во время
spawning/monitoring:- UI показывает
cancelled - процесс не остаётся “висячим”
- UI показывает
- Повторный create с тем же
teamName, когда команда уже существует → понятная ошибка без перезаписи. - После
readyможно открыть Team tab и увидеть данные (пусть даже roster заполнится постепенно).
Appendix A — One-shot prompt template (одноходовый bootstrap)
Ниже шаблон prompt, который должен отработать в одном turn в режиме -p.
Переменные (подставляет приложение):
{teamName}— directory name{displayName}— UI name{description}— описание (может быть пустым){members}— список участников (строки), где каждый элемент:name(валидирован)role(опционально)
Шаблон:
You are running in a non-interactive CLI session. Do not ask questions. Do everything in a single turn.
Goal: Provision a Claude Code agent team.
Constraints:
- Use ONLY these tools: TeamCreate, SendMessage, TaskCreate.
- Do not use Read/Edit/Bash.
- Do not output large logs. Keep assistant text minimal.
Steps (must be executed in this order):
1) Call TeamCreate to create team "{teamName}" with:
- display name: "{displayName}"
- description: "{description}"
- members: the list below (include roles if present)
2) For each member in the list, call SendMessage to send a short bootstrap DM:
- summary: "Bootstrap"
- text: "Team \"{displayName}\" is ready. Reply with 'OK' when you are available."
3) (Best-effort) Create 1 initial task via TaskCreate:
- subject: "Bootstrap check"
- description: "Confirm team provisioning succeeded. Each member replied OK."
- owner: leave empty (unassigned)
Members:
{members}
Примечания:
- Bootstrap DM нужен, чтобы с высокой вероятностью создались
inboxes/{member}.json. - TaskCreate — best-effort; отсутствие задач НЕ блокирует
ready.