# Research: CLI Orchestration for Team Provisioning ## Status: RESEARCH COMPLETE (v2) Результаты исследования 10 агентов (3 + 4 + 3) по запуску Claude CLI для создания команд. Раунд 3 — верификация и финальная архитектура. --- ## 1. Варианты запуска Claude CLI ### 1.1 Claude Agent SDK (`@anthropic-ai/claude-agent-sdk`) **Пакет**: `@anthropic-ai/claude-agent-sdk` v0.2.44 (npm) **Repo**: `github.com/anthropics/claude-agent-sdk-typescript` **Статус**: Official, maintained by Anthropic, pre-1.0 #### API ```typescript import { query } from "@anthropic-ai/claude-agent-sdk"; for await (const msg of query({ prompt: "Create a team to work on X", options: { allowedTools: ["TeamCreate", "TaskCreate", "SendMessage", "Read", "Edit", "Bash"], model: "opus", maxTurns: 250, cwd: "/path/to/project", permissionMode: "acceptEdits", // или "bypassPermissions" } })) { console.log(msg); // SDKMessage — structured } ``` #### Характеристики | Параметр | Значение | |----------|----------| | Startup overhead | ~12 сек per query() call | | Hot process reuse | НЕТ (issue #34 в SDK repo) | | Internal mechanism | Спавнит Claude Code CLI как subprocess | | Team tools | Доступны с `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` | | Streaming | AsyncGenerator | | Multi-turn | Stateless — каждый query() = новая сессия | | Node.js | 18+ | | Electron compatible | Да (main process only) | #### V2 Preview (simpler API) ```typescript import { unstable_v2_prompt } from "@anthropic-ai/claude-agent-sdk"; const result = await unstable_v2_prompt("What is 2 + 2?"); ``` #### Проблемы - **12 сек startup** — непригоден для real-time UX - **No hot reuse** — каждый вызов = new process (issue #34) - **Pre-1.0** — API может измениться - **Team features experimental** — документированные issues: - Session resumption breaks team state - Task status can lag - Shutdown slow - One team per session ### 1.2 CLI с `--output-format stream-json` **Флаги**: | Флаг | Опции | Описание | |------|-------|----------| | `--output-format` | `text`, `json`, `stream-json` | Формат stdout | | `--input-format` | `text`, `stream-json` | Формат stdin | | `-p "prompt"` | string | Non-interactive (print mode) | | `--verbose` | flag | Подробный вывод | #### Stream-JSON Protocol (NDJSON) Каждая строка — полный JSON object: ```json {"type":"init","session_id":"abc123","timestamp":"2024-01-01T00:00:00Z"} {"type":"message","role":"assistant","content":[{"type":"text","text":"Analyzing..."}]} {"type":"tool_use","name":"TeamCreate","input":{"team_name":"my-team","description":"..."}} {"type":"tool_result","output":"Team created successfully"} {"type":"result","status":"success","duration_ms":1234} ``` #### Можно детектить tool_use events ```bash claude -p "Create a team" --output-format stream-json | \ jq 'select(.type == "tool_use" and .name == "TeamCreate")' ``` #### Проблемы - **Баг #5034**: `--input-format stream-json` дублирует entries в session .jsonl при multi-turn - **Stateful parsing**: tool_result не имеет поля `previous_event` → нужна корреляция вручную - **stdio buffer**: 64KB platform limit → deadlock если не drain'ить - **Encoding**: UTF-8 multi-byte может порваться на chunk boundary - **Backpressure**: если не читаем stdout → subprocess зависает ### 1.3 CLI с file-based monitoring (без парсинга stdout) ```bash spawn(claudePath, [prompt], { stdio: ['ignore', 'ignore', 'pipe'], # stdout ИГНОРИРУЕМ, stderr для ошибок }) ``` Мониторинг через FileWatcher: ``` ~/.claude/teams/{name}/config.json → team created/updated ~/.claude/teams/{name}/inboxes/*.json → messages ~/.claude/tasks/{name}/*.json → tasks ~/.claude/projects/{path}/{id}.jsonl → session data (already parsed by app) ``` #### Преимущества - JSONL = stable API contract (не меняется между версиями CLI) - FileWatcher уже есть в проекте (retry, SSH polling, debounce) - Crash-safe: файлы на диске сохраняются - No buffer overflow: читаем файлы, не pipes - Масштабируется: один FileWatcher для N агентов --- ## 2. Проблема PATH на macOS ### Суть проблемы Когда Electron запускается из macOS Finder (не из терминала), `process.env.PATH` минимальный: ``` /usr/bin:/bin:/usr/sbin:/sbin ``` **Отсутствуют**: `/usr/local/bin`, `~/.npm/bin`, `~/.nvm/versions/...`, `/opt/homebrew/bin` `spawn('claude', [...])` → **ENOENT** ("command not found") ### Где Claude CLI устанавливается | Метод | Путь бинарника | |-------|----------------| | npm global | `/usr/local/bin/claude` (symlink) | | npm global (user) | `~/.npm-global/bin/claude` | | nvm + npm | `~/.nvm/versions/node/v20/bin/claude` | | Homebrew (Intel) | `/usr/local/bin/claude` | | Homebrew (Apple Silicon) | `/opt/homebrew/bin/claude` | | Windows | `C:\Users\{user}\AppData\Roaming\npm\claude.cmd` | ### Решение: ClaudeBinaryResolver ```typescript async function resolveClaudeBinary(): Promise { const candidates = [ // 1. Try current PATH first (works in terminal) 'claude', // 2. Common npm global paths path.join(os.homedir(), '.npm-global', 'bin', 'claude'), path.join(os.homedir(), '.npm', 'bin', 'claude'), // 3. System paths '/usr/local/bin/claude', '/opt/homebrew/bin/claude', // Apple Silicon // 4. nvm — find active version ...await findNvmClaudePaths(), // 5. Windows ...(process.platform === 'win32' ? [`${process.env.APPDATA}\\npm\\claude.cmd`] : []), ]; for (const candidate of candidates) { try { await fs.promises.access(candidate, fs.constants.X_OK); return candidate; } catch { continue; } } // Fallback: `which claude` try { const { stdout } = await execFileAsync('which', ['claude'], { timeout: 2000, shell: true }); return stdout.trim() || null; } catch { return null; } } ``` ### Существующие паттерны в проекте - `config.ts:570-604` — пробует несколько editor candidates (cursor, code, subl, zed) - `SshConnectionManager.ts:325` — использует ПОЛНЫЙ путь: `/bin/launchctl` - `fix-path` / `shell-path` НЕ используются в проекте --- ## 3. stdio vs File Monitoring ### Сравнительная матрица | Критерий | stdio Pipes | File Monitoring | |----------|-------------|-----------------| | **Потеря данных** | ВЫСОКИЙ РИСК (buffer overflow 64KB) | НЕТ РИСКА (файлы на диске) | | **Latency** | ~0ms (pipe events) | 0-100ms (fs.watch), 3-30s (polling) | | **Structured data** | Нужен парсинг (NDJSON или raw text) | JSONL уже structured | | **Crash recovery** | Потеря буфера | Файлы сохранены | | **Memory** | Накапливает в буферах | Читаем инкрементально | | **Complexity** | State machine + backpressure + timeouts | Уже реализовано (FileWatcher) | | **Fragility** | Ломается при смене формата CLI | JSONL = stable contract | | **Масштаб** | 10 agents × stdio = resource explosion | Один watcher на все файлы | ### Конкретные риски stdio 1. **Buffer overflow**: stdout buffer = 64KB. Claude team session легко генерит > 64KB → subprocess deadlock 2. **Partial JSON reads**: chunk boundary может порвать JSON object → нужен stateful line parser 3. **Backpressure**: если не drain'ить pipe → Claude CLI зависает навсегда 4. **Encoding**: multi-byte UTF-8 рвётся на chunk boundaries 5. **ANSI escapes**: Claude может слать цветные коды (даже в pipe mode) ### Вердикт **Hybrid**: stdio ТОЛЬКО для lifecycle (started/stopped/crashed), файлы для ВСЕХ данных. --- ## 4. Сравнение подходов | | **SDK query()** | **CLI + stream-json** | **CLI + file monitoring** | |---|---|---|---| | Startup | ~12 сек (!) | ~2-3 сек | ~2-3 сек | | Structured output | SDKMessage | NDJSON events | JSONL files | | TeamCreate detection | Да | Да (tool_use event) | Да (config.json appears) | | Multi-turn | Stateless per query | Баг #5034 | Работает | | stdio risks | Hidden in SDK | Buffer/encoding/deadlock | Нет (файлы) | | Crash recovery | Потеря | Потеря | Автоматическое | | PATH resolution | SDK сам резолвит | Нужен resolver | Нужен resolver | | API stability | Pre-1.0 | Experimental | JSONL stable | | Hot reuse | Нет (issue #34) | N/A | N/A | --- ## 5. Env Variable для Team Tools Team tools **выключены по умолчанию**. Для включения: ```bash CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 ``` Или в `~/.claude/settings.json`: ```json { "env": { "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1" } } ``` Доступные team tools: - `TeamCreate` — создание команды - `TaskCreate` — создание задачи - `TaskUpdate` — обновление статуса задачи - `TaskList` — список задач - `TaskGet` — получение задачи по ID - `SendMessage` — отправка сообщения участнику - `TeamDelete` — удаление команды --- ## 6. Ограничения Agent Teams (из официальной документации) 1. `/resume` и `/rewind` ломают team state 2. Task status может отставать — teammates иногда не отмечают tasks completed 3. Shutdown медленный — teammates завершают текущий request перед выходом 4. Один team per session — нельзя управлять несколькими командами из одного lead 5. Нет nested teams — teammates не могут создавать свои команды 6. Token cost высокий — каждый teammate = отдельное context window 7. Split-pane mode fragile — требует tmux или iTerm2 --- ## 7. Верификация (раунд 3, 3 агента) ### 7.1 Вердикт по SDK: НЕТ **Финальный вердикт**: SDK query() НЕ использовать. Причины: 1. SDK **внутри спавнит CLI как subprocess** — нет выигрыша по сравнению с прямым spawn 2. ~12 сек cold start per query() call — неприемлемо для UX 3. Pre-1.0: API может измениться без предупреждения 4. SDK **не решает** проблему PATH resolution — всё равно нужен resolver 5. Team features experimental — задокументированные баги с session resumption, task lag, slow shutdown 6. Никакой добавленной ценности: FileWatcher уже делает всё, что мог бы дать SDK streaming ### 7.2 Верификация file monitoring: ОК с оговорками **Находка**: CLI flag `-p` (print mode) **выходит после первого ответа**. Для multi-turn team creation: - Вариант A: `--continue --resume $SESSION_ID` для продолжения сессии - Вариант B: **Одноходовый промпт** — Claude за один turn вызывает TeamCreate + TaskCreate + спавнит teammates - **Рекомендация**: Вариант B (одноходовый) — достаточно для bootstrap, проще и надёжнее **Session JSONL parsing**: В JSONL session файле видны: - `tool_use` с `name: "TeamCreate"` → команда создана - `tool_result` с `teammate_spawned` → участник запущен - SubagentResolver в проекте **уже умеет** детектить `teammate_spawned` **Edge cases**: - Mid-creation crash → retry safe (TeamCreate идемпотентен — перезаписывает config.json) - Concurrent creation attempts → лок на teamName (validateTeamName + state machine) - Dir exists without config.json → graceful skip (уже исправлено в v7, баг #37) - Tasks dir ENOENT → return [] (уже исправлено в v7, баг #38) ### 7.3 Финальная архитектура (верифицирована) #### Новые файлы (7) | Файл | Описание | |------|----------| | `src/main/services/teams/ClaudeBinaryResolver.ts` | Поиск бинарника Claude CLI | | `src/main/services/teams/TeamProvisioningService.ts` | State machine для создания команды | | `src/main/services/teams/types.ts` | Типы для provisioning | | `src/main/ipc/teams.ts` | IPC handlers для provisioning | | `src/preload/constants/teamChannels.ts` | IPC channel constants | | `src/renderer/hooks/useTeamProvisioning.ts` | React hook | | `src/renderer/components/teams/CreateTeamDialog.tsx` | UI диалог создания | #### Модификации (5) | Файл | Изменение | |------|-----------| | `src/main/ipc/handlers.ts` | + registerTeamProvisioningHandlers | | `src/preload/index.ts` | + api.teams.create, api.teams.getProvisioningStatus | | `src/renderer/store/slices/teamSlice.ts` | + provisioning state | | `src/main/services/infrastructure/FileWatcher.ts` | Уже добавлен в v7 (teamsWatcher) | | `src/shared/types/ElectronAPI.ts` | + provisioning types | #### State Machine ``` idle → validating → spawning → monitoring → ready ↘ failed → idle (retry) ``` - `idle` — начальное состояние - `validating` — проверка teamName, поиск Claude binary - `spawning` — spawn CLI process - `monitoring` — ожидание config.json через FileWatcher - `ready` — команда создана, participants bootstrapped - `failed` — ошибка (с retry) #### IPC Contract ```typescript // Channels export const TEAM_CREATE = 'team:create'; export const TEAM_PROVISIONING_STATUS = 'team:provisioning-status'; export const TEAM_PROVISIONING_PROGRESS = 'team:provisioning-progress'; // event // Request interface TeamCreateRequest { teamName: string; description: string; members: Array<{ name: string; role: string }>; cwd: string; // project working directory } // Response (via event stream) interface ProvisioningProgress { teamName: string; state: 'validating' | 'spawning' | 'monitoring' | 'ready' | 'failed'; message: string; error?: string; sessionId?: string; } ``` #### Spawn Strategy ```typescript const claudePath = await ClaudeBinaryResolver.resolve(); if (!claudePath) throw new Error('Claude CLI not found'); const child = spawn(claudePath, [ '-p', prompt, // print mode (single turn) '--output-format', 'text', // НЕ парсим stdout ], { cwd: projectPath, env: { ...process.env, CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1', }, stdio: ['ignore', 'ignore', 'pipe'], // stdout ИГНОРИРУЕМ }); // Только stderr для ошибок child.stderr.on('data', (data) => { stderrBuffer += data; }); // Lifecycle child.on('exit', (code) => { if (code === 0) transitionTo('monitoring'); else transitionTo('failed', stderrBuffer); }); child.on('error', (err) => transitionTo('failed', err.message)); ``` #### Bootstrap Detection ```typescript // FileWatcher УЖЕ следит за ~/.claude/teams/ // При появлении config.json → проверяем содержимое function onTeamConfigCreated(teamName: string) { const config = await readTeamConfig(teamName); if (config && config.members?.length > 0) { transitionTo('ready'); emitProgress({ state: 'ready', sessionId }); } } ``` #### Idempotency 1. `validateTeamName()` проверяет формат (a-z, 0-9, -) 2. Если dir уже существует с config.json → **ошибка** (не перезаписываем чужую команду) 3. Если dir существует БЕЗ config.json → **очищаем и продолжаем** (неудачная предыдущая попытка) 4. PID tracking для active processes → нельзя запустить два provisioning для одного teamName --- ## 8. Итоговая рекомендация ### Подход: CLI spawn + FileWatcher (hybrid) ``` [UI] → [IPC: team:create] → [TeamProvisioningService] ↓ [ClaudeBinaryResolver.resolve()] ↓ [spawn(claudePath, ['-p', prompt], { env: { CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1' }, stdio: ['ignore', 'ignore', 'pipe'] })] ↓ [FileWatcher: ~/.claude/teams/{name}/config.json] ↓ [IPC event: team:provisioning-progress → 'ready'] ``` ### Почему НЕ другие варианты | Вариант | Причина отказа | |---------|----------------| | SDK query() | Внутри = тот же spawn, +12с overhead, pre-1.0, не решает PATH | | CLI + stream-json | stdio fragile (64KB buffer), баг #5034, нужен stateful parser | | CLI + stdout pipe | Buffer overflow, encoding, backpressure — все те же риски | | tmux/iTerm2 | Не подходит для Electron (нет терминала) | --- ## 9. Sources ### Official Documentation - [CLI reference](https://code.claude.com/docs/en/cli-reference) - [Run Claude Code programmatically](https://code.claude.com/docs/en/headless) - [Orchestrate teams](https://code.claude.com/docs/en/agent-teams) - [Agent SDK overview](https://platform.claude.com/docs/en/agent-sdk/overview) - [Agent SDK TypeScript](https://platform.claude.com/docs/en/agent-sdk/typescript) ### NPM / GitHub - [npmjs.com/@anthropic-ai/claude-agent-sdk](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk) - [github.com/anthropics/claude-agent-sdk-typescript](https://github.com/anthropics/claude-agent-sdk-typescript) ### Known Issues - [#5034 — Duplicate entries in stream-json](https://github.com/anthropics/claude-code/issues/5034) - [#1920 — Missing Final Result Event in Streaming JSON](https://github.com/anthropics/claude-code/issues/1920) - [#34 — Hot process reuse for SDK](https://github.com/anthropics/claude-agent-sdk-typescript/issues/34) - [#15511 — Stream partial JSON tokens](https://github.com/anthropics/claude-code/issues/15511) ### Community - [Building agents with Claude Agent SDK — Anthropic Engineering](https://www.anthropic.com/engineering/building-agents-with-the-claude-agent-sdk) - [Agent Teams with Claude Code — Medium](https://kargarisaac.medium.com/agent-teams-with-claude-code-and-claude-agent-sdk-e7de4e0cb03e) - [Claude Code Multi-Agent tmux Setup — Dariusz Parys](https://www.dariuszparys.com/claude-code-multi-agent-tmux-setup/)