Files
wiki/tasks/multi-agent/DEV_TASK_ORCHESTRATOR_QG.md
2026-05-21 15:10:03 +03:00

404 lines
19 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.
# 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/<repo> <image-with-claude> 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/<work_item_id>/01-brd.md
# docs/work-items/<work_item_id>/02-trz.md
# docs/work-items/<work_item_id>/03-acceptance-criteria.md
# docs/work-items/<work_item_id>/04-test-plan.yaml
# Все 4 файла должны существовать в feature-ветке
```
- [ ] **2.2** Реализовать `check_architecture_done(repo, work_item_id)`:
```python
# Проверяет наличие:
# docs/work-items/<work_item_id>/06-adr/ (директория, хотя бы 1 файл)
# ИЛИ docs/work-items/<work_item_id>/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/<work_item_id>/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-агент*