auto-sync: 2026-05-21 15:10:01
This commit is contained in:
403
tasks/multi-agent/DEV_TASK_ORCHESTRATOR_QG.md
Normal file
403
tasks/multi-agent/DEV_TASK_ORCHESTRATOR_QG.md
Normal file
@@ -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/<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-агент*
|
||||
Reference in New Issue
Block a user