agent-ecosystem/docs/team-management/research-cli-orchestration.md
2026-02-21 09:47:24 +02:00

493 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<SDKMessage> |
| 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<string | null> {
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/)