src/frontmatter.py grows from a single-key reader into the full machine
contract: reader (read_frontmatter_value, unchanged), one parse primitive
(parse_frontmatter), writer (render/write_frontmatter), schema validator
(validate_schema/REQUIRED_FIELDS, warning-only by default) and a shared
strip_frontmatter helper. The five verdict gates (check_reviewer_verdict,
_parse_tests_verdict, _parse_deploy_status, _parse_staging_status,
parse_security_status) now read through the single parse_frontmatter point
instead of duplicated ad-hoc YAML logic; review_parse._strip_frontmatter and
security_gate.extract_security_findings reuse the shared helper.
Strictly backward compatible + never-raise: STAGE_TRANSITIONS, the QG_CHECKS
composition, verdict semantics (incl. ORCH-047 three-field tester + negative
token priority), reason-strings and worktree->origin/main fallback are 1:1.
The schema validator never influences a gate verdict by default; hard-fail is
reserved behind the frontmatter_validation_strict kill-switch (default False).
New formal handoff spec docs/_standards/HANDOFF_PROTOCOL.md ("stage -> required
output" + required frontmatter schema), aligned 1:1 with PIPELINE_DOCS.md.
Tests: test_frontmatter.py (TC-01..07), test_qg_verdicts.py (TC-08..15),
test_security_gate.py (TC-12), test_stages_invariants.py (TC-16). Full
tests/ green (1212).
Refs: ORCH-076
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Multi-Agent Orchestrator
См. CLAUDE.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)
docker compose up -d --build
Dev
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_RUNS_DIR |
Базовый каталог per-run логов агентов (<runs_dir>/{run_id}.log, ORCH-087) |
/app/data/runs |
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 |
Как добавить новый проект
-
Убедись, что gitea-репо уже клонировано в
/repos/<repo>(авто-clone — отдельно). -
Узнай Plane project uuid (из URL проекта в Plane или через Plane API).
-
Добавь запись в
ORCH_PROJECTS_JSONв.env(JSON-массив). Важно: если задаёшьORCH_PROJECTS_JSON, он полностью заменяет дефолт — перечисли все нужные проекты (включая enduro-trails и orchestrator):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":"<имя>"} ]' -
Пересобери:
docker compose up -d --build. -
Проверь резолв:
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/developmentpull_request*(wildcard) → review approved/rejected, PR mergestatus→ (legacy) Gitea CI; С-1: больше не authoritative,failureлогируется на debug и не блокирует/не алертит (QG развития = локальныйcheck_tests_local)
Тесты
pytest tests/ -v
Известные ограничения
- Single-task / shared
/reposcheckout — одновременно безопасно обрабатывается одна задача: все агенты иcheck_tests_localделаютgit checkoutв одном/repos/<repo>→ гонки при параллельных задачах. Исправление — git worktree per task (S-4, отдельно). - Plane sync — маппинг issue ID может быть некорректным (P3, в работе)
- In-process daemon-потоки — агенты живут в потоках uvicorn; при рестарте ловит orphan-recovery. Целевое — очередь задач (F-2b)
- Gitea CI не настроен — тесты гоняет сам оркестратор локально
- Tester timeout — e2e тесты с Playwright могут занимать >25 мин на тяжёлых фичах
- No retry on API errors — httpx вызовы к Gitea/Plane без retry logic