diff --git a/tasks/multi-agent/DEV_TASK_ORCHESTRATOR_QG.md b/tasks/multi-agent/DEV_TASK_ORCHESTRATOR_QG.md new file mode 100644 index 0000000..57e0a62 --- /dev/null +++ b/tasks/multi-agent/DEV_TASK_ORCHESTRATOR_QG.md @@ -0,0 +1,403 @@ +# DEV TASK: Orchestrator — Quality Gates + Auto-launch агентов + +**Статус:** Ready for dev +**Проект:** multi-agent +**Фаза:** 2 +**BRD:** tasks/multi-agent/BRD.md +**Предыдущая задача:** tasks/multi-agent/DEV_TASK_ORCHESTRATOR_MVP.md (выполнена) + +--- + +## Цель + +> Orchestrator должен автоматически проверять Quality Gates и запускать Claude CLI агентов при наступлении событий (webhook от Plane/Gitea), без ручного вмешательства. + +## Архитектура + +Orchestrator уже развёрнут и принимает webhooks. Сейчас вся логика — заглушки (`pass`, `return True`). Нужно реализовать: +1. Реальные QG-проверки (наличие файлов в репо, CI status через Gitea API, reactions в Plane) +2. Автоматический запуск агентов при прохождении QG +3. Обновление stage задачи в БД при переходах +4. Уведомление (лог) при эскалации / ошибке + +Orchestrator работает в Docker-контейнере `orchestrator` на `orchestrator_default` сети. Доступ к Gitea и Plane — через localhost (порты 3000 и 8091). Claude CLI — на хосте, доступен через volume mount `/home/slin/repos:/repos:ro` и subprocess. + +**Важно:** Claude CLI запускается НЕ внутри контейнера, а на хосте. Orchestrator должен запускать его через `docker exec` на хосте или через отдельный механизм. Текущий launcher использует `subprocess.Popen` — это работает только если claude binary доступен внутри контейнера. Нужно проверить и при необходимости переделать на запуск через host. + +## Стек / Зависимости + +- Python 3.12 + FastAPI (уже есть) +- httpx (уже есть) — для вызовов Gitea/Plane API +- subprocess — для запуска Claude CLI +- SQLite — журнал + +--- + +## Инфраструктура + +| Параметр | Значение | +|----------|----------| +| Сервер | `slin@82.22.50.71` (mva154) | +| Рабочая директория | `/home/slin/repos/orchestrator/` | +| Контейнер | `orchestrator` (порт 8500) | +| Деплой | `cd /home/slin/repos/orchestrator && docker compose up -d --build` | +| Gitea API | `http://localhost:3000/api/v1` (token: в .env `ORCH_GITEA_TOKEN`) | +| Plane API | `http://localhost:8091/api/v1` (token: в .env `ORCH_PLANE_API_TOKEN`) | +| Claude CLI | `/usr/bin/claude` на хосте (НЕ в контейнере) | +| Repos | `/home/slin/repos/` на хосте, mount как `/repos:ro` в контейнере | + +--- + +## Файловая карта + +| Действие | Файл | Ответственность | +|----------|------|-----------------| +| Переписать | `src/qg/checks.py` | Реальные QG-проверки через Gitea/Plane API + filesystem | +| Переписать | `src/webhooks/plane.py` | Полная обработка events → QG → launch | +| Переписать | `src/webhooks/gitea.py` | Полная обработка push/PR/CI → QG → launch | +| Изменить | `src/agents/launcher.py` | Запуск на хосте (не в контейнере) | +| Создать | `src/notifications.py` | Логирование + будущие уведомления | +| Изменить | `src/db.py` | Добавить helper-функции (get_task_by_plane_id, update_stage) | +| Изменить | `src/config.py` | Добавить plane_project_id, default_repo | +| Изменить | `docker-compose.yml` | Mount docker socket или host network для запуска CLI | +| Создать | `tests/test_qg.py` | Тесты QG-проверок | +| Изменить | `tests/test_webhooks.py` | Расширить тесты | + +--- + +## Задачи + +### Task 1: Исправить запуск Claude CLI (host vs container) + +**Проблема:** Claude CLI установлен на хосте (`/usr/bin/claude`), но Orchestrator работает в контейнере. `subprocess.Popen` внутри контейнера не найдёт бинарник. + +**Решение:** Добавить docker socket mount + запускать через `docker exec` на хосте, ИЛИ дать контейнеру доступ к host network + PID namespace. Самый простой вариант — запускать через SSH на localhost (уже есть sshpass/ssh в образе) или через docker socket. + +**Рекомендуемый подход:** Добавить в docker-compose.yml volume `/var/run/docker.sock` и запускать Claude CLI через: +```bash +docker run --rm -v /home/slin/repos:/repos -w /repos/ claude --print ... +``` + +Или проще: mount `/usr/bin/claude` + необходимые зависимости в контейнер. + +**Шаги:** + +- [ ] **1.1** Проверить, доступен ли claude внутри контейнера: +```bash +docker exec orchestrator which claude 2>/dev/null || echo "NOT FOUND" +``` + +- [ ] **1.2** Если не доступен — выбрать один из подходов: + - **A)** Mount docker.sock + запуск через `docker run --rm` (изолированно) + - **B)** Mount `/usr/bin/claude` + зависимости в контейнер (проще, но хрупко) + - **C)** Запуск через SSH на localhost (надёжно, но overhead) + - **D)** Перевести orchestrator на `network_mode: host` + mount бинарника + +- [ ] **1.3** Обновить `docker-compose.yml` и `src/agents/launcher.py` соответственно + +- [ ] **1.4** Проверить: +```bash +# Из контейнера orchestrator должен уметь запустить claude +docker exec orchestrator python -c "import subprocess; print(subprocess.run(['claude', '--version'], capture_output=True, text=True).stdout)" +``` + +**Критерий готовности:** `launcher.launch("developer", "enduro-trails", "echo test")` успешно запускает Claude CLI процесс, run записывается в БД с output_path. + +--- + +### Task 2: Реальные QG-проверки + +**Файлы:** +- Переписать: `src/qg/checks.py` +- Изменить: `src/config.py` + +**Шаги:** + +- [ ] **2.1** Реализовать `check_analysis_complete(repo, work_item_id)`: +```python +# Проверяет наличие файлов в репо: +# docs/work-items//01-brd.md +# docs/work-items//02-trz.md +# docs/work-items//03-acceptance-criteria.md +# docs/work-items//04-test-plan.yaml +# Все 4 файла должны существовать в feature-ветке +``` + +- [ ] **2.2** Реализовать `check_architecture_done(repo, work_item_id)`: +```python +# Проверяет наличие: +# docs/work-items//06-adr/ (директория, хотя бы 1 файл) +# ИЛИ docs/work-items//07-infra-requirements.md +``` + +- [ ] **2.3** Реализовать `check_ci_green(repo, branch)`: +```python +# GET http://localhost:3000/api/v1/repos/{owner}/{repo}/commits/{branch}/status +# Headers: Authorization: token {ORCH_GITEA_TOKEN} +# Проверить: combined_status == "success" +``` + +- [ ] **2.4** Реализовать `check_review_approved(repo, pr_number)`: +```python +# GET http://localhost:3000/api/v1/repos/{owner}/{repo}/pulls/{pr_number}/reviews +# Проверить: хотя бы один review с state == "APPROVED" и 0 с state == "REQUEST_CHANGES" +``` + +- [ ] **2.5** Реализовать `check_tests_passed(repo, work_item_id)`: +```python +# Проверяет наличие файла: +# docs/work-items//13-test-report.md +# И что в нём есть строка "PASS" или "All tests passed" +``` + +- [ ] **2.6** Добавить в config.py: +```python +# Gitea +gitea_owner: str = "admin" +default_repo: str = "enduro-trails" + +# Plane +plane_project_id: str = "" +``` + +**Критерий готовности:** Каждая QG-функция делает реальный API-вызов или проверку файловой системы. Unit-тесты с мокированным httpx проходят. + +--- + +### Task 3: Полная обработка Plane webhooks + +**Файлы:** +- Переписать: `src/webhooks/plane.py` +- Изменить: `src/db.py` + +**Шаги:** + +- [ ] **3.1** Добавить в `db.py` helper-функции: +```python +def get_task_by_plane_id(plane_id: str) -> dict | None: ... +def update_task_stage(task_id: int, stage: str): ... +def get_task_by_repo_branch(repo: str, branch: str) -> dict | None: ... +``` + +- [ ] **3.2** Реализовать `handle_work_item_created`: +```python +# 1. Извлечь plane_id, name из payload +# 2. Сгенерировать work_item_id (например ET-003) — инкремент от последнего +# 3. Создать запись в tasks (plane_id, repo, stage="created", branch=f"feature/{work_item_id}-{slug}") +# 4. Создать ветку в Gitea через API: +# POST /api/v1/repos/{owner}/{repo}/branches +# {"new_branch_name": "feature/ET-003-slug", "old_branch_name": "main"} +# 5. Создать папку docs/work-items/{work_item_id}/ с 00-business-request.md +# 6. Обновить stage → "analysis" +# 7. Логировать: "Task created, waiting for analysis" +``` + +- [ ] **3.3** Реализовать `handle_comment` (`:approved:` detection): +```python +# 1. Определить к какому work_item относится комментарий +# 2. Получить текущий stage задачи из БД +# 3. В зависимости от stage: +# - stage="analysis" + :approved: → QG-1 check → если pass → launch Architect → stage="architecture" +# - stage="architecture" + :approved: → QG-2 check → launch Developer → stage="development" +# - stage="testing" + :approved: → merge PR → stage="done" +# 4. Если QG fail → логировать причину, не двигать stage +``` + +- [ ] **3.4** Добавить обработку `:rejected:`: +```python +# :rejected: → stage откатывается на предыдущий +# Логировать причину +``` + +**Критерий готовности:** При POST `/webhook/plane` с event `work_item.created` — создаётся ветка в Gitea. При `:approved:` comment — запускается следующий агент. + +--- + +### Task 4: Полная обработка Gitea webhooks + +**Файлы:** +- Переписать: `src/webhooks/gitea.py` + +**Шаги:** + +- [ ] **4.1** Реализовать `handle_push`: +```python +# 1. Извлечь branch name из ref (refs/heads/feature/ET-003-slug → feature/ET-003-slug) +# 2. Найти task по branch в БД +# 3. Если task.stage == "architecture" и push содержит файлы в docs/work-items/*/06-adr/: +# → QG-2 pass → launch Developer → stage="development" +# 4. Если task.stage == "development" и push содержит src/ файлы: +# → Ждать CI (не запускать reviewer сразу) +``` + +- [ ] **4.2** Реализовать `handle_ci_status`: +```python +# 1. Извлечь branch, state, context из payload +# 2. Если state == "success" и task.stage == "development": +# → QG-4 check (CI green) → launch Reviewer → stage="review" +# 3. Если state == "failure": +# → Логировать, уведомить (будущее) +``` + +- [ ] **4.3** Реализовать `handle_pr`: +```python +# 1. При action == "reviewed": +# - Если approved → QG-5 → launch Tester → stage="testing" +# - Если request_changes → back-to:dev, перезапуск Developer (max 3 раза) +# 2. При action == "closed" и merged: +# → stage="done" +``` + +**Критерий готовности:** Push в feature-ветку с ADR-файлами → автоматически запускается Developer. CI green → запускается Reviewer. + +--- + +### Task 5: Notifications + Stage machine + +**Файлы:** +- Создать: `src/notifications.py` +- Создать: `src/stages.py` + +**Шаги:** + +- [ ] **5.1** Создать `src/stages.py` — конечный автомат стадий: +```python +STAGE_TRANSITIONS = { + "created": {"next": "analysis", "agent": None}, + "analysis": {"next": "architecture", "agent": "architect", "qg": "check_analysis_complete"}, + "architecture": {"next": "development", "agent": "developer", "qg": "check_architecture_done"}, + "development": {"next": "review", "agent": "reviewer", "qg": "check_ci_green"}, + "review": {"next": "testing", "agent": "tester", "qg": "check_review_approved"}, + "testing": {"next": "deploy", "agent": "deployer", "qg": "check_tests_passed"}, + "deploy": {"next": "done", "agent": None}, + "done": {"next": None, "agent": None}, +} + +def advance_stage(task_id: int, current_stage: str) -> str | None: + """Try to advance to next stage. Returns new stage or None if QG fails.""" + ... +``` + +- [ ] **5.2** Создать `src/notifications.py`: +```python +import logging + +logger = logging.getLogger("orchestrator") + +def notify_stage_change(task_id: int, old_stage: str, new_stage: str, agent: str = None): + logger.info(f"Task {task_id}: {old_stage} → {new_stage}" + (f" (launching {agent})" if agent else "")) + +def notify_qg_failure(task_id: int, stage: str, check: str, reason: str): + logger.warning(f"Task {task_id}: QG failed at {stage}, check={check}: {reason}") + +def notify_agent_finished(run_id: int, agent: str, exit_code: int): + logger.info(f"Agent run {run_id} ({agent}) finished with exit code {exit_code}") +``` + +- [ ] **5.3** Интегрировать stages.py в webhook handlers (plane.py, gitea.py) + +**Критерий готовности:** Все переходы идут через `advance_stage()`. Логи показывают полный flow. + +--- + +### Task 6: Тесты + +**Файлы:** +- Создать: `tests/test_qg.py` +- Изменить: `tests/test_webhooks.py` + +**Шаги:** + +- [ ] **6.1** `tests/test_qg.py` — мокировать httpx, проверить каждую QG-функцию +- [ ] **6.2** Расширить `tests/test_webhooks.py`: + - Тест: plane work_item.created → task создан в БД, stage="analysis" + - Тест: plane comment :approved: при stage=analysis → stage меняется + - Тест: gitea push → stage advance + - Тест: gitea CI success → reviewer запускается + +- [ ] **6.3** Запустить тесты: +```bash +cd /home/slin/repos/orchestrator && pip install pytest httpx && pytest tests/ -v +``` + +**Критерий готовности:** `pytest tests/ -v` → all green + +--- + +### Task 7: Деплой + smoke test + +**Шаги:** + +- [ ] **7.1** Пересобрать и перезапустить: +```bash +cd /home/slin/repos/orchestrator && docker compose up -d --build +``` + +- [ ] **7.2** Smoke test — health: +```bash +curl -s http://localhost:8500/health | python3 -c "import sys,json; d=json.load(sys.stdin); assert d['status']=='ok'" +``` + +- [ ] **7.3** Smoke test — plane webhook: +```bash +curl -s -X POST http://localhost:8500/webhook/plane \ + -H "Content-Type: application/json" \ + -d '{"event":"work_item.created","data":{"id":"smoke-test-001","name":"Smoke test task","project":"proj-1"}}' | python3 -c "import sys,json; d=json.load(sys.stdin); assert d['status']=='accepted'" +``` + +- [ ] **7.4** Проверить что task создан и ветка создана в Gitea: +```bash +curl -s http://localhost:3000/api/v1/repos/admin/enduro-trails/branches \ + -H "Authorization: token c81227b0dee2217f9ab3d28c3642a4578a1b9772" | python3 -c "import sys,json; branches=[b['name'] for b in json.load(sys.stdin)]; print(branches)" +``` + +- [ ] **7.5** Проверить логи: +```bash +docker logs orchestrator --tail 20 +``` + +**Критерий готовности:** Webhook создаёт task + ветку. Логи показывают stage transitions. + +--- + +## Проверка (Acceptance) + +| # | Проверка | Команда / Действие | Ожидаемый результат | +|---|----------|-------------------|---------------------| +| 1 | Health | `curl localhost:8500/health` | `{"status":"ok"}` | +| 2 | Plane webhook creates task | POST work_item.created | task в БД, stage=analysis | +| 3 | Plane webhook creates branch | POST work_item.created | Ветка в Gitea | +| 4 | Approved advances stage | POST comment :approved: | stage analysis→architecture | +| 5 | QG check works | Вызвать check_analysis_complete для ET-001 | True (файлы есть) | +| 6 | QG check fails correctly | Вызвать check_analysis_complete для несуществующего | False | +| 7 | Agent launch works | launcher.launch("developer", "enduro-trails", "echo test") | run_id в БД, процесс запущен | +| 8 | Tests pass | `pytest tests/ -v` | All green | + +--- + +## Ограничения и контекст + +- ⚠️ Claude CLI на хосте, НЕ в контейнере — нужен механизм запуска (Task 1) +- ⚠️ Orchestrator в сети `orchestrator_default` — доступ к Gitea/Plane через localhost работает только если порты проброшены на хост (они проброшены: 3000, 8091) +- ⚠️ Gitea API owner = `admin`, repo = `enduro-trails` +- ⚠️ Plane workspace slug = `ag_proj` +- ⚠️ НЕ трогать существующие данные в БД (4 events, 2 tasks) — они тестовые, но пусть останутся +- ⚠️ НЕ менять порт 8500 +- ⚠️ НЕ менять формат .env (только добавлять новые переменные если нужно) +- 🚫 НЕ устанавливать Claude CLI в контейнер (он требует auth через браузер) +- 🚫 НЕ делать auto-merge PR без явного QG-прохождения + +--- + +## Деплой-чеклист + +- [ ] Код написан и тесты проходят +- [ ] docker-compose.yml обновлён (если нужен docker.sock mount) +- [ ] `docker compose up -d --build` успешен +- [ ] `curl localhost:8500/health` → ok +- [ ] Smoke test webhook → task + branch created +- [ ] `docker logs orchestrator --tail 30` — нет ошибок + +--- + +*Создано: 2026-05-21 | Автор ТЗ: Стрим | Исполнитель: Dev-агент*