# Multi-Agent Orchestrator

> См. [CLAUDE.md](CLAUDE.md) (паспорт проекта) и [docs/architecture/README.md](docs/architecture/README.md) (архитектура).

FastAPI-сервис для оркестрации мульти-агентного пайплайна разработки. Принимает webhooks от Plane и Gitea, управляет жизненным циклом задач через Quality Gates, запускает Claude CLI агентов на каждой стадии.

## Архитектура

```
Plane (task mgmt) ──webhook──┐
                              ├──► Orchestrator (FastAPI) ──► Quality Gates ──► Agent Launcher
Gitea (git events) ─webhook──┘         │                                            │
                                        ▼                                            ▼
                                   SQLite DB                                   Claude CLI
                                (events, tasks,                            (analyst, architect,
                                 agent_runs)                              developer, reviewer, tester)
```

## Стадии пайплайна

```
created → analysis → architecture → development → review → testing → deploy-staging → deploy → done
                          ↑                          │
                          └───── REQUEST_CHANGES ─────┘  (max 3 retries)
```

| Стадия | Агент | Quality Gate (выход) | Триггер перехода |
|--------|-------|---------------------|------------------|
| created | — | — | Plane webhook (work_item.created) |
| analysis | analyst | Файлы BRD/TRZ/AC/TestPlan | Push docs/ |
| architecture | architect | ADR или infra-requirements | Push docs/ |
| development | developer | check_tests_local (орк сам гоняет `make test`) | Auto-advance после developer |
| review | reviewer | check_reviewer_verdict (`verdict:` во frontmatter 12-review.md) | Auto-advance после reviewer |
| testing | tester | check_tests_passed (test-report.md) | Auto-advance после tester |
| deploy-staging | deployer | check_staging_status (15-staging-log.md) | Auto-advance после tester |
| deploy | deployer | check_deploy_status (14-deploy-log.md) | Auto-advance после staging |
| done | — | — | — |

## API Endpoints

| Method | Path | Описание |
|--------|------|----------|
| GET | `/health` | Health check |
| GET | `/status` | Активные задачи (stage != done) |
| GET | `/queue` | Очередь задач (ORCH-1): counts по статусам + max_concurrency + последние 10 jobs |
| POST | `/webhook/plane` | Plane webhook receiver |
| POST | `/webhook/gitea` | Gitea webhook receiver |

## Структура проекта

```
src/
├── main.py              # FastAPI app, lifespan (orphan recovery)
├── config.py            # Pydantic settings (env vars)
├── db.py                # SQLite: init, get_db, update_task_stage
├── stages.py            # State machine (transitions, agents, QG)
├── notifications.py     # Уведомления (логирование)
├── plane_sync.py        # Синхронизация статусов с Plane API
├── queue_worker.py      # ORCH-1: фоновый воркер очереди (claim → launch_job)
├── agents/
│   └── launcher.py      # AgentLauncher: launch/launch_job, monitor, watchdog, auto-advance
├── webhooks/
│   ├── plane.py         # Plane webhook handler
│   └── gitea.py         # Gitea webhook handler (push, PR, CI status)
└── qg/
    └── checks.py        # Quality Gate checks (filesystem + Gitea API)
data/
├── orchestrator.db      # SQLite database
└── runs/                # Agent output logs ({run_id}.log)
docs/
├── PRODUCT_VISION.md            # Видение продукта
├── architecture/
│   ├── README.md                # Обзор архитектуры, компоненты, API
│   ├── internals.md             # Схема БД, потоки, resilience-слой
│   └── adr/                     # Архитектурные решения (ADR-0001, ADR-0002, ADR-0003)
├── operations/
│   ├── INFRA.md                 # Топология, порты, env, self-hosting риски
│   ├── DEPLOY_HOOK.md           # Деплой-хук
│   ├── STAGING.md               # Staging-окружение
│   ├── STAGING_CHECK.md         # Проверки staging
│   └── SETUP_WEBHOOKS.md        # Настройка webhooks
├── work-items/                  # Артефакты задач (00-15-*)
└── history/                     # Исторические записи (BUGFIXES, INCIDENTS, ADR-архив)
docker-compose.yml       # Deployment config
Dockerfile               # Python 3.12 + Docker CLI + tini
```

## Запуск

### Docker (production)

```bash
docker compose up -d --build
```

### Dev

```bash
pip install -r requirements.txt
uvicorn src.main:app --reload --port 8500
```

## Конфигурация

Все переменные с префиксом `ORCH_`:

| Переменная | Описание | Default |
|-----------|----------|---------|
| `ORCH_PLANE_API_URL` | Plane API URL | `http://localhost:8091` |
| `ORCH_PLANE_API_TOKEN` | Plane API token | — |
| `ORCH_PLANE_WEBHOOK_SECRET` | Webhook secret | — |
| `ORCH_PLANE_WORKSPACE_SLUG` | Workspace slug | — |
| `ORCH_PLANE_PROJECT_ID` | Project UUID | — |
| `ORCH_GITEA_URL` | Gitea URL | `http://localhost:3000` |
| `ORCH_GITEA_TOKEN` | Gitea API token | — |
| `ORCH_GITEA_WEBHOOK_SECRET` | Gitea webhook secret | — |
| `ORCH_GITEA_OWNER` | Gitea repo owner | `admin` |
| `ORCH_DEFAULT_REPO` | Default repository (fallback) | `enduro-trails` |
| `ORCH_PROJECTS_JSON` | Multi-repo реестр (JSON-массив, ORCH-6) | `""` → дефолт в `src/projects.py` |
| `ORCH_CLAUDE_BIN` | Путь к Claude CLI | `/opt/claude-code/bin/claude.exe` |
| `ORCH_REPOS_DIR` | Repos dir (container) | `/repos` |
| `ORCH_HOST_REPOS_DIR` | Repos dir (host) | `/home/slin/repos` |
| `ORCH_DB_PATH` | SQLite path | `/app/data/orchestrator.db` |
| `ORCH_MAX_CONCURRENCY` | Сколько jobs воркер запускает параллельно (ORCH-1) | `1` |
| `ORCH_QUEUE_POLL_INTERVAL` | Период опроса очереди воркером, сек (ORCH-1) | `2.0` |
| `ORCH_PREFLIGHT_CACHE_TTL` | Кэш preflight (CLI/net), сек (ORCH-1 resilience) | `45` |
| `ORCH_BACKOFF_BASE_SECONDS` | База exp-backoff для transient (429) | `10` |
| `ORCH_BACKOFF_MAX_SECONDS` | Потолок backoff | `600` |
| `ORCH_TRANSIENT_MAX_ATTEMPTS` | Ретраи для 429/недоступности | `5` |
| `ORCH_BREAKER_THRESHOLD` | transient подряд до открытия breaker | `3` |
| `ORCH_BREAKER_PAUSE_SECONDS` | Пауза при открытом breaker | `300` |
| `ORCH_RECONCILE_ENABLED` | Kill-switch sweeper потерянных webhook (ORCH-053) | `true` |
| `ORCH_RECONCILE_PLANE_ENABLED` | Отдельный флаг F-2 (опрос Plane API) | `true` |
| `ORCH_RECONCILE_INTERVAL_S` | Период фонового прохода reconciler, сек | `120` |
| `ORCH_RECONCILE_GRACE_DEFAULT_S` | Порог «застряла» по `tasks.updated_at`, сек | `600` |
| `ORCH_RECONCILE_GRACE_OVERRIDES_JSON` | Per-stage пороги, напр. `{"development":300}` | `""` |
| `ORCH_RECONCILE_NOTIFY_UNBLOCK` | Telegram при разблокировке застрявшей задачи | `true` |
| `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED` | F-1 Guard 2 (ORCH-060): пропуск задач в Plane-статусе Blocked / Needs Input; `false` глушит только сетевой Guard 2 (Guard 1 escalated всегда активен) | `true` |
| `ORCH_QG0_TITLE_MAX` | Верхний лимит длины заголовка QG-0 (вход `_qg0_errors`); невалидное/пустое значение → дефолт (ORCH-069) | `200` |

## Очередь задач (ORCH-1 / F-2b)

Webhook-хэндлеры больше не спавнят claude-агентов синхронно в процессе uvicorn.
Вместо этого они кладут **job** в персистентную SQLite-таблицу `jobs`
(`enqueue_job`, мгновенный ответ), а фоновый воркер (`src/queue_worker.py`)
забирает jobs с учётом `ORCH_MAX_CONCURRENCY` и запускает агента (`launch_job`,
та же Popen-логика, что и раньше).

Преимущества:
- **Рестарт-safe.** При старте jobs со статусом `running` возвращаются в `queued`
  (queue-recovery в lifespan) — работа не теряется.
- **Лимит параллелизма.** Воркер не превышает `ORCH_MAX_CONCURRENCY`.
- **Ретраи.** Упавший job (exit≠0) ретраится пока `attempts < max_attempts`,
  потом `failed` + Telegram-нотификация.

Статусы job: `queued → running → done | failed`. Наблюдаемость — через `GET /queue`.

**Resilience-слой:** дешёвый preflight (CLI/net, кэш, без токенов) гейтит claim;
429/overload детектится по логу (transient vs permanent), transient ретраится с
exp-backoff (`available_at`, Retry-After); circuit breaker паузит воркер после N
transient подряд. Подробности: `docs/history/ORCH-1_JOB_QUEUE.md`.

## Multi-repo: реестр проектов (ORCH-6)

Оркестратор обслуживает несколько репозиториев через реестр проектов
(`src/projects.py`), ключ = **Plane project id**. Plane-webhook фильтрует события
по проекту (неизвестный проект → `ignored`) и резолвит `repo` / `work_item_prefix` /
Plane-проект из маппинга.

По умолчанию (если `ORCH_PROJECTS_JSON` пуст) зарегистрированы два проекта:

| Проект | Plane project id | repo | prefix |
|--------|------------------|------|--------|
| enduro-trails | `7a79f0a9-5278-49cd-9007-9a338f238f9c` | `enduro-trails` | `ET` |
| orchestrator | `8da6aa25-a60e-44d6-a1e2-d8ae59aa7d6a` | `orchestrator` | `ORCH` |

### Как добавить новый проект

1. Убедись, что gitea-репо уже клонировано в `/repos/<repo>` (авто-clone — отдельно).
2. Узнай Plane project uuid (из URL проекта в Plane или через Plane API).
3. Добавь запись в `ORCH_PROJECTS_JSON` в `.env` (JSON-массив). **Важно:** если
   задаёшь `ORCH_PROJECTS_JSON`, он полностью заменяет дефолт — перечисли **все**
   нужные проекты (включая enduro-trails и orchestrator):

   ```bash
   ORCH_PROJECTS_JSON='[
     {"plane_project_id":"7a79f0a9-5278-49cd-9007-9a338f238f9c","repo":"enduro-trails","work_item_prefix":"ET","name":"enduro-trails"},
     {"plane_project_id":"8da6aa25-a60e-44d6-a1e2-d8ae59aa7d6a","repo":"orchestrator","work_item_prefix":"ORCH","name":"orchestrator"},
     {"plane_project_id":"<новый-uuid>","repo":"<новый-repo>","work_item_prefix":"<PREFIX>","name":"<имя>"}
   ]'
   ```

4. Пересобери: `docker compose up -d --build`.
5. Проверь резолв:
   ```bash
   docker exec orchestrator python3 -c "from src.projects import get_project_by_plane_id as g; print(g('<новый-uuid>'))"
   ```

Поля `name` опционально (по умолчанию = `repo`). Подробности — `docs/architecture/internals.md`.

## Ключевые механизмы

### Auto-advance
После успешного завершения агента (exit_code=0), `_try_advance_stage()` проверяет QG и автоматически продвигает задачу + запускает следующего агента.

### Review bounce
При REQUEST_CHANGES от reviewer задача откатывается в development, developer перезапускается (до 3 попыток). При исчерпании — эскалация.

### Orphan recovery (M-1)
При старте контейнера каждый run с `finished_at IS NULL` старше 35 минут помечается exit_code=-1, логируется per-run warning и отправляется Telegram-уведомление «нужна ручная проверка/перезапуск» (не молча).

### Запись task-файлов (B-1)
Task-файлы `.task-*.md` пишутся **прямой записью в смонтированный volume `/repos/<repo>/`** (без docker). При ошибке записи — RuntimeError (не молчит). В `.gitignore` проекта.

### Логи агентов (B-2)
stdout/stderr агента перенаправляются СРАЗУ в `/app/data/runs/{id}.log` на уровне ОС (без PIPE). monitor-поток делает `proc.wait()` → реальный exit_code, нет зомби.

### Watchdog
Каждый агент имеет timeout 30 минут. При превышении — SIGKILL + запись exit_code=-9.

### Event routing
Gitea events роутятся по типу:
- `push` → проверка файлов, advance architecture/development
- `pull_request*` (wildcard) → review approved/rejected, PR merge
- `status` → (legacy) Gitea CI; С-1: больше не authoritative, `failure` логируется на debug и не блокирует/не алертит (QG развития = локальный `check_tests_local`)

## Тесты

```bash
pytest tests/ -v
```

## Известные ограничения

1. **Single-task / shared `/repos` checkout** — одновременно безопасно обрабатывается одна задача: все агенты и `check_tests_local` делают `git checkout` в одном `/repos/<repo>` → гонки при параллельных задачах. Исправление — git worktree per task (S-4, отдельно).
2. **Plane sync** — маппинг issue ID может быть некорректным (P3, в работе)
3. **In-process daemon-потоки** — агенты живут в потоках uvicorn; при рестарте ловит orphan-recovery. Целевое — очередь задач (F-2b)
4. **Gitea CI не настроен** — тесты гоняет сам оркестратор локально
3. **Tester timeout** — e2e тесты с Playwright могут занимать >25 мин на тяжёлых фичах
4. **No retry on API errors** — httpx вызовы к Gitea/Plane без retry logic
