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

19 KiB
Raw Blame History

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 через:

docker run --rm -v /home/slin/repos:/repos -w /repos/<repo> <image-with-claude> claude --print ...

Или проще: mount /usr/bin/claude + необходимые зависимости в контейнер.

Шаги:

  • 1.1 Проверить, доступен ли claude внутри контейнера:
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 Проверить:

# Из контейнера 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):
# Проверяет наличие файлов в репо:
# 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):
# Проверяет наличие:
# 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):
# 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):
# 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):
# Проверяет наличие файла:
# docs/work-items/<work_item_id>/13-test-report.md
# И что в нём есть строка "PASS" или "All tests passed"
  • 2.6 Добавить в config.py:
# 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-функции:
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:
# 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):
# 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::
# :rejected: → stage откатывается на предыдущий
# Логировать причину

Критерий готовности: При POST /webhook/plane с event work_item.created — создаётся ветка в Gitea. При :approved: comment — запускается следующий агент.


Task 4: Полная обработка Gitea webhooks

Файлы:

  • Переписать: src/webhooks/gitea.py

Шаги:

  • 4.1 Реализовать handle_push:
# 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:
# 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:
# 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 — конечный автомат стадий:
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:
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 Запустить тесты:

cd /home/slin/repos/orchestrator && pip install pytest httpx && pytest tests/ -v

Критерий готовности: pytest tests/ -v → all green


Task 7: Деплой + smoke test

Шаги:

  • 7.1 Пересобрать и перезапустить:
cd /home/slin/repos/orchestrator && docker compose up -d --build
  • 7.2 Smoke test — health:
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:
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:
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 Проверить логи:
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-агент