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

20 KiB
Raw Blame History

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

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)

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:

{"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

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)

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

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 выключены по умолчанию. Для включения:

CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1

Или в ~/.claude/settings.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

// 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

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

// 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

NPM / GitHub

Known Issues

Community