Files
orchestrator/docs/architecture/README.md
claude-bot 6ddff5583d
All checks were successful
CI / test (push) Successful in 19s
CI / test (pull_request) Successful in 18s
fix(ORCH-058): parametrize staging_check in --build-staging + explicit staging target
Round-3 review follow-up on c53d625 (P1/P2):

- P1: --build-staging now runs staging_check via parametrized
  STAGING_CONTAINER / STAGING_CHECK_PATH / STAGING_CHECK_MODE (default
  orchestrator-staging / bind-mount path / stub) instead of hardcoding
  $TARGET_SERVICE + the script path. docker exec runs INSIDE the staging
  container (ORCH-048 canonical: B6 registry isolation), after health,
  before exit 0. Fail-closed: any non-zero -> exit 1. STAGING only (8501).
- P2a: rebuild_staging_image now passes the STAGING target EXPLICITLY
  (TARGET_SERVICE/TARGET_PORT/COMPOSE_PROFILE/STAGING_CONTAINER) so the
  self-rebuild can never drift onto prod 8500 if hook defaults change (AC-9).
- P2b: TC-09 caller<->hook contract tests assert the ssh command carries
  GIT_SHA + BUILD_CONTEXT + the staging target and never the prod 8500 one;
  no-ssh-host fails closed.
- P3: consolidated the three duplicate README footers into one.
- Docs (golden source): DEPLOY_HOOK.md step 4 + env rows, README footer,
  CHANGELOG, Dockerfile ARG GIT_SHA="" comment, .env.example freshness block.

Validates exactly the artefact later BUILD-ONCE retagged to prod (AC-4,
ADR-001 step 3). 632 tests pass, ruff clean, bash -n OK.

Refs: ORCH-058

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 09:24:38 +00:00

198 lines
22 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.
# Архитектура Orchestrator
## Обзор
Мульти-агентный оркестратор разработки. Принимает webhooks от Plane (управление задачами) и Gitea (git-события), ведёт задачи по конвейеру стадий через Quality Gates, на каждой стадии запускает Claude CLI агента. Поддерживает несколько проектов (multi-repo) и self-hosting (дорабатывает сам себя).
## Компоненты
- **Webhook Receivers** (`src/webhooks/plane.py`, `gitea.py`) — приём событий, HMAC-проверка, дедупликация (`_dedup.py`). Роуты: `POST /webhook/plane`, `POST /webhook/gitea`.
- **State Machine** (`src/stages.py`) — `STAGE_TRANSITIONS`: переходы, агент и QG каждой стадии. Хелперы: `get_next_stage`, `get_agent_for_stage`, `get_qg_for_stage`, `get_previous_stage`.
- **Stage Engine** (`src/stage_engine.py`) — исполнение переходов, диспетчеризация QG (`_run_qg`), откаты, синхронизация с Plane.
- **Review/Test Parsers** (`src/review_parse.py`, ORCH-046) — defensive-извлечение дословного must-fix текста из артефактов для встраивания в `task_desc` заворота: `extract_review_findings` (P0/P1 из `12-review.md`), `extract_test_failures` (фрагмент тела `13-test-report.md`). Контракт «never raise»: любая ошибка → `""`.
- **Quality Gates** (`src/qg/checks.py`) — проверки выхода со стадии, реестр `QG_CHECKS`.
- **Agent Launcher** (`src/agents/launcher.py`) — запуск Claude CLI агентов в изолированном git worktree, мониторинг, auto-advance.
- **Queue** (`src/queue_worker.py`, ORCH-1) — персистентная очередь задач (SQLite `jobs`), atomic claim, max_concurrency, ретраи, restart-safe.
- **Reconciler** (`src/reconciler.py`, ORCH-053 — реализовано, [adr-0007](adr/adr-0007-reconciler.md)) — фоновый daemon-поток (паттерн `queue_worker`), стартует/останавливается в `main.lifespan` (после `worker.start()` / перед `worker.stop()`). Реконсилирует рассинхрон «источник истины ≠ стадия задачи» при потерянном webhook. F-1 gate-side (продвигает застрявшую стадию по локальной БД через штатный `advance_stage(..., finished_agent=None)`), F-2 plane-side (опрос Plane API → `handle_*` из `plane.py`), F-3 (БД-fallback `sha→branch` в `handle_ci_status`). Источник истины — гейт/Plane, не событие; идемпотентность (active-job guard + atomic-claim + grace); kill-switch `ORCH_RECONCILE_ENABLED`. `analysis` F-1 не трогает (человеческий гейт). Наблюдаемость — блок `reconcile` в `GET /queue`.
- **Project Registry** (`src/projects.py`, ORCH-6) — Plane project id → repo + prefix; фильтрация вебхуков по проекту.
- **Plane Sync** (`src/plane_sync.py`) — синхронизация статусов/комментариев в Plane.
## Конвейер и Quality Gates
```
created → analysis → architecture → development → review → testing → deploy-staging → deploy → done
↑ │
└──── REQUEST_CHANGES ──────┘ (откат на development, max 3 retries)
```
| Стадия | Агент (выход) | Quality Gate | Артефакт |
|--------|---------------|--------------|----------|
| created | analyst | — | — |
| analysis | architect | `check_analysis_approved` | 01-brd / 02-trz / 03-acceptance-criteria / 04-test-plan.yaml |
| architecture | developer | `check_architecture_done` | 06-adr/ |
| development | reviewer | `check_ci_green` | код + PR |
| review | tester | `check_reviewer_verdict` | 12-review.md (`verdict:`) |
| testing | deployer | `check_tests_passed` | 13-test-report.md |
| deploy-staging | deployer | `check_staging_status` | 15-staging-log.md (`staging_status:`) |
| deploy | — | `check_deploy_status` | 14-deploy-log.md (`deploy_status:`) |
| done | — | — | — |
**Реестр QG** (`QG_CHECKS`): check_analysis_approved, check_analysis_complete, check_architecture_done, check_ci_green, check_review_approved, check_tests_passed, check_reviewer_verdict, check_tests_local, check_deploy_status, check_staging_status, check_branch_mergeable (ORCH-043), check_staging_image_fresh (ORCH-058).
**Канон гейтов:** машинные вердикты читаются ТОЛЬКО из YAML-frontmatter, никогда из прозы. Лог-файлы мержатся в `origin/main` отдельным PR; гейт читает из `origin/main`.
### Условный staging-гейт (ORCH-35)
`check_staging_status` реален только для self-hosting (`is_self_hosting_repo(repo)``orchestrator`); для остальных проектов → no-op `(True, "Staging gate N/A")`. Для orchestrator парсит `staging_status:` из `15-staging-log.md`; FAILED → откат на `development`. Подробнее: [ADR-0003](adr/adr-0003-staging-gate.md).
### Merge-gate: догон `main` + re-test + сериализация слияний (ORCH-043)
Детерминированный под-гейт (`check_branch_mergeable`, без LLM) на ребре **`deploy-staging → deploy`**: исполняется ПОСЛЕ `check_staging_status` и ДО запуска deployer'а, который вливает PR в `main` (deployer мержит в начале стадии `deploy`). Стадии (`STAGE_TRANSITIONS`) НЕ меняются — это «под-гейт» ребра, а не отдельная стадия (триггер — то же событие «staging-deployer завершился»).
Назначение: ветка валидируется относительно того `main`, из которого создана; параллельная задача могла уйти вперёд → семантический конфликт слияния (зелёная ветка ломает обновлённый `main`). Merge-gate гарантирует проверку против **актуального** `origin/main` перед слиянием:
- **Догон:** ветка отстаёт (⇔ `origin/main` не предок HEAD) → `rebase origin/main` в worktree + `push --force-with-lease` (ТОЛЬКО ветка задачи; `main` — никогда). Текстовый конфликт → `rebase --abort` → откат на `development`.
- **Re-test:** `python -m pytest` (`merge_retest_target`, дефолт `tests/`) в worktree догнанной ветки, тайм-аут `merge_retest_timeout_s`. Красный/тайм-аут → откат на `development`.
- **Сериализация (merge-lock):** файловый **merge-lease** на репо (`<repos_dir>/.merge-lease-<repo>.json`), живёт от гейта до фактического merge. Acquire **неблокирующий** (anti-deadlock при `max_concurrency=1`): busy → **defer** (повторная постановка deployer'а на `deploy-staging` с задержкой через `available_at`), а не откат. Release — на PR-merged вебхуке / `deploy→done` / откате / по возрасту (crash-реклейм). Restart-safe; без изменения схемы БД.
- **Условность (как ORCH-35):** реален для `orchestrator`; прочие репо — no-op. Флаги `merge_gate_enabled` / `merge_gate_repos` — поэтапный раскат. Контракт **never-raise**.
Подробнее: [adr-0006](adr/adr-0006-merge-gate.md), детально — `docs/work-items/ORCH-043/06-adr/ADR-001-merge-gate.md`.
### Исполняемый самодеплой стадии `deploy` (ORCH-36)
`deploy` перестаёт быть «бумажной»: для self-hosting (`is_self_hosting_repo`) стадия
РЕАЛЬНО деплоит прод (8500) через хост-хук `scripts/orchestrator-deploy-hook.sh`,
а `deploy_status: SUCCESS` означает доказанный health-ok, не декларацию LLM. Три фазы
(детерминированно, без LLM в критическом пути self-restart):
- **Фаза A (вход в `deploy`)** — при `deploy_require_manual_approve=true` вместо запуска
прод-deployer выставляется approval-pending статус Plane + запрос approve
(Plane-коммент + Telegram). Перехват в `advance_stage` ПОСЛЕ `check_staging_status`
и merge-gate.
- **Фаза B (Plane → `Approved`)** — `advance_stage(deploy, finished_agent=None)`
запускает **detached host-процесс** (ssh + setsid → хук с прод-параметрами +
build-once retag `SOURCE_IMAGE`) и ставит детерминированный **finalizer-job**;
маркер `initiated` — идемпотентность. Возврат БЕЗ advance (вердикта ещё нет).
- **Фаза C (finalizer)** — новый контейнер после рестарта читает sentinel `result`
(exit-code хука), маппит `0→SUCCESS / иначе→FAILED`, пишет `14-deploy-log.md`,
вызывает `advance_stage(deploy, finished_agent="deployer")` → существующие контракты:
`SUCCESS → done`, `FAILED → откат БАГ-8 на development`.
Approve = смена статуса Plane на `Approved` (status-only verdict model; комментарии
не управляют конвейером). На старте — обязательный ручной approve (флаг `true`); полный
авто — отдельная задача (ORCH-54). Условность как ORCH-35: реально для `orchestrator`,
прочие репо — прежний синхронный ssh-деплой агентом. Контракты не меняются:
`STAGE_TRANSITIONS`, реестр QG, `check_deploy_status`/`_parse_deploy_status`, БАГ-8,
terminal-sync, merge-gate, exit-code-контракт хука. Restart-safe состояние —
sentinel-файлы (`<repos_dir>/.deploy-state-<repo>/<wi>/`), без миграции БД.
Подробнее: [adr-0007](adr/adr-0007-executable-self-deploy.md), детально —
`docs/work-items/ORCH-036/06-adr/ADR-001-executable-self-deploy.md`.
### Свежесть артефакта BUILD-ONCE: провенанс staging-образа (ORCH-058 — реализовано)
BUILD-ONCE retag (ORCH-36) промоутит `SOURCE_IMAGE=orchestrator-orchestrator-staging` в прод
**без rebuild**, полагаясь на «staging-образ свеж и провалидирован». Этой гарантии нет:
конвейер нигде не пересобирает staging-образ из провалидированного коммита → retag мог тихо
промоутнуть УСТАРЕВШИЙ образ (инцидент LESSONS_ORCH-036 п.4 — зелёный деплой молча
откатывал прод). ORCH-058 обеспечивает инвариант `INV-FRESH` **двумя слоями** (defense in
depth), только для self-hosting:
- **A — пересборка (liveness):** детерминированный QG-под-чек `check_staging_image_fresh` на
ребре `deploy-staging → deploy` ПОСЛЕ merge-gate и ДО Phase A пересобирает
`orchestrator-orchestrator-staging` из worktree валидированного коммита
(`--build-arg GIT_SHA=<sha>`, OCI-лейбл `org.opencontainers.image.revision`), пересоздаёт
8501 и прогоняет `staging_check` против свежего образа → валидируем и промоутим один
артефакт. FAIL → откат на `development` (как merge-gate). Сборки/recreate — ТОЛЬКО staging.
- **B — fail-closed guard (safety):** хук шагом 2b ПЕРЕД `docker tag` сверяет лейбл `revision`
у `SOURCE_IMAGE` с `EXPECTED_REVISION` (пробрасывает `build_deploy_command`). Несовпадение
/ пустой лейбл / пустой ожидаемый SHA / ошибка inspect → `exit 1` → FAILED (БАГ-8 откат),
прод не трогается. Делает тихий промоут устаревшего образа структурно невозможным даже при
отключённой/проигравшей гонку A.
Якорь «провалидированного коммита» — `git rev-parse HEAD` worktree ПОСЛЕ merge-gate (один
helper `validated_revision` питает и штамп A, и `EXPECTED_REVISION` B). Единый kill-switch
`image_freshness_enabled` включает A+B **как целое** (нет «B без A» = вечного fail-fast);
`image_freshness_repos` (пусто → self-hosting). `STAGE_TRANSITIONS`, exit-code хука (0/1/2),
`check_deploy_status`, БАГ-8, merge-gate, схема БД — НЕ меняются (под-гейт ребра + лейбл
образа, без миграций). Подробнее: [adr-0008](adr/adr-0008-staging-image-provenance.md),
детально — `docs/work-items/ORCH-058/06-adr/ADR-001-staging-image-provenance.md`.
### Reconciler: реконсиляция потерянных webhook (ORCH-053 — реализовано)
Конвейер продвигается только входящими webhook; потерянное событие (502 на ребилде,
нет ретраев у Plane/Gitea, неразрезолвленный `sha→branch`) → задача застревает молча
(инцидент ORCH-044). Фоновый поток `reconciler` периодически (`reconcile_interval_s`)
находит застрявшие задачи и доигрывает пропущенный переход **через те же штатные
гейты/обработчики**, что и webhook:
- **F-1 gate-side:** для задач со `stage∉{done}`, без активного job и
`age(updated_at) ≥ grace_for_stage(stage)` — read-only пред-оценка канонического QG;
зелёный → `stage_engine.advance_stage(..., finished_agent=None)`; красный →
тишина (спам нотификаций структурно невозможен). `analysis` не реконсилируется.
- **F-2 plane-side:** опрос Plane API per-project → `handle_status_start` /
`handle_verdict` из `webhooks/plane.py` (логика не дублируется).
- **F-3:** усиление `sha→branch` в `handle_ci_status` (БД-fallback по единственной
development-задаче repo; неоднозначность → не резолвим).
- **F-4 observability:** при разблокировке — лог-строка `reconciler: <wi> <stage>
разблокирована (потерян webhook)` + Telegram (`reconcile_notify_unblock`); снимок
состояния в `GET /queue` (блок `reconcile`).
Реализация: `src/reconciler.py` (daemon-поток по образцу `queue_worker`), стартует в
`main.lifespan` **после** `worker.start()`, останавливается в `finally` **перед**
`worker.stop()`.
Инварианты: источник истины — гейт/Plane, не событие; идемпотентность (active-job
guard + atomic-claim на создании под process-wide Lock + grace + `max_concurrency=1`);
never-raise на единицу работы; тишина при синхронности; restart-safe; kill-switch
`ORCH_RECONCILE_ENABLED` (+ `ORCH_RECONCILE_PLANE_ENABLED` гасит только F-2). Схема БД
и реестры (`STAGE_TRANSITIONS`/`QG_CHECKS`) не меняются. Подробнее:
[adr-0007](adr/adr-0007-reconciler.md), детально — `docs/work-items/ORCH-053/06-adr/ADR-001-stuck-task-reconciler.md`.
## Откаты
- Reviewer REQUEST_CHANGES → откат на `development` + retry (`MAX_DEVELOPER_RETRIES = 3`).
- Tester `check_tests_passed` FAIL → откат на `development` + retry.
- Deploy / deploy-staging FAILED → откат на `development`.
- Merge-gate FAIL (конфликт rebase / красный re-test, ORCH-043) → откат на `development` + retry; `merge-lock busy` → **defer** (не откат, dev-retry не тратится).
- `get_previous_stage` использует порядок ключей `STAGE_TRANSITIONS`.
### Обогащение `task_desc` при заворотах (ORCH-046)
При откате на `development` `task_desc` (попадает в `.task-dev.md` developer-агента) несёт **дословный must-fix текст**, а не только ссылку — чтобы агент видел суть претензий сразу и не повторял ту же ошибку:
- **reviewer REQUEST_CHANGES** → дословные пункты P0/P1 из секции `## Findings` файла `12-review.md` (`extract_review_findings`);
- **tester `check_tests_passed` FAIL** → `reason` гейта + фрагмент тела `13-test-report.md` (приоритет: `## Вывод pytest` → FAIL-строки `## Результаты` → `## Итог`; `extract_test_failures`).
Ссылка на полный файл-артефакт сохраняется всегда («Полный контекст»). Парсеры `src/review_parse.py` — defensive (never-raise); при отсутствующем/битом артефакте `task_desc` graceful-фоллбэк на прежнюю ссылку-строку, последовательность отката и retry-счётчик не меняются (ADR `docs/work-items/ORCH-046/06-adr/ADR-001-embed-findings-in-task-desc.md`).
### Plane Sync: единый status-коммент агентов (ORCH-016)
Все агенты (analyst / architect / developer / reviewer / tester / deployer) пишут финальный коммент через **один хелпер** `usage.build_status_comment(...)` (ADR `docs/work-items/ORCH-016/06-adr/ADR-001-unified-status-comment.md`). Формат HTML, разделители `<br>`:
```
{ICON} {RoleName} — {описание стадии}
[Verdict|Status: VALUE] # reviewer/tester/deployer, из YAML-frontmatter артефакта
[Длительность: 4m 12s] # явный duration_s от launcher, либо fallback из agent_runs
<b>Документы:</b><ul><li><a href="…">label</a></li>…</ul>
[<sub>8.5M in / 45.8k out · $7.29</sub>] # тех-хвост usage; опускается при нулях
```
- **Длительность** считается launcher'ом (`_monitor_agent`) и пробрасывается в `_post_usage_comments`; для analyst (коммент строится в `stage_engine`) используется DB-фоллбэк `usage.get_agent_duration(task_id, agent)`.
- **Vердикт-парсер** — `src/frontmatter.read_frontmatter_value(...)` (defensive, никогда не raise). Машинные ключи: reviewer → `verdict:` (12-review.md); **testing-гейт `check_tests_passed` (13-test-report.md) → любое из трёх равноправных: `result:` (канон промпта тестера), `verdict:`, `status:`** (ORCH-047, ADR-001); deployer → `deploy_status:` (14-deploy-log.md), `staging_status:` (15-staging-log.md). Negative-токен в любом поле авторитетен (перебивает positive).
- Формат коммента **не** меняет реестр гейтов и стадий; коммент — отображение, не управление.
## База данных (SQLite)
- `events` — входящие вебхуки (дедуп)
- `tasks` — задачи и их стадии
- `agent_runs` — запуски агентов (run_id, usage, cost)
- `jobs` — очередь задач (ORCH-1)
## Изоляция (git worktree, ORCH-2)
Каждая задача исполняется в отдельном git worktree, ветки не пересекаются. Репозитории проектов разделены под `/repos/<project>`.
## API
| Method | Path | Описание |
|--------|------|----------|
| GET | `/health` | health check |
| GET | `/status` | активные задачи (stage != done) |
| GET | `/queue` | очередь: counts + max_concurrency + resilience + reconcile (ORCH-053) + последние jobs |
| POST | `/webhook/plane` | Plane webhook |
| POST | `/webhook/gitea` | Gitea webhook (push, PR, CI status) |
## Деплой и эксплуатация
Топология, контейнеры, порты, env-карта, self-hosting риски — [docs/operations/INFRA.md](../operations/INFRA.md). Деплой-хук — [DEPLOY_HOOK.md](../operations/DEPLOY_HOOK.md). Staging — [STAGING.md](../operations/STAGING.md).
## ADR
Сквозные архитектурные решения — [adr/](adr/). Per-work-item решения — `docs/work-items/<id>/06-adr/`.
## Детали реализации
Схема БД, потоки данных, resilience-слой, детали Dockerfile — [internals.md](internals.md).
---
*Актуально на 2026-06-07. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. Статусы доработок: ORCH-036 (исполняемый самодеплой `deploy`, adr-0007) — реализовано; ORCH-043 (merge-gate, adr-0006) — design, ветка feature/ORCH-043; ORCH-053 (reconciler, adr-0007, src/reconciler.py) — реализовано; ORCH-058 (провенанс staging-образа: check_staging_image_fresh + staging_check свежего образа + хук-guard, adr-0008) — реализовано в ветке feature/ORCH-058 (обновлять также при изменении src/image_freshness.py, scripts/orchestrator-deploy-hook.sh, Dockerfile).*