# Архитектура 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. Модель/эффорт каждого агента резолвятся из config (`resolve_agent_model`/`resolve_agent_effort`, ORCH-41), а не из frontmatter промпта. **ORCH-74:** имя модели валидируется форматом `^claude-…$` (`is_valid_model`) перед `--model`; невалидное → лог + откат на следующий уровень/CLI-дефолт (never-break, как `VALID_EFFORTS` для эффорта). Тот же предикат гардит inline-чтение `--fallback-model`. - **Queue** (`src/queue_worker.py`, ORCH-1) — персистентная очередь задач (SQLite `jobs`), atomic claim, max_concurrency, ретраи, restart-safe. **ORCH-026:** `claim_next_job` гейтит задачи с незавершёнными зависимостями (`job_deps`, `NOT EXISTS`) без занятия слота; декларации/циклы — leaf `src/task_deps.py`. - **Job-reaper** (`src/job_reaper.py`, ORCH-065 — [adr-0011](adr/adr-0011-job-reaper-lease-reclaim.md)) — фоновый daemon-поток (каркас `reconciler`), стартует/останавливается в `main.lifespan` (после `reconciler.start()` / перед `worker.stop()`). Детектирует «мёртвый» `running`-job **без рестарта** процесса (Tier-1 мёртвый `jobs.pid` после `reaper_dead_ticks` тиков; Tier-2 `agent_runs.exit_code` записан, а job ещё `running`; Tier-3 backstop `reaper_max_running_s`) и приводит строку к корректному статусу через те же контракты (`_try_advance_stage`/`_finalize_job`, gate-driven; exit≠0/неизвестно → `attempts `ORCH_AGENT_MODEL_`/`ORCH_AGENT_EFFORT_` > `*_default` > CLI-дефолт (без флага)**. **Эффорт (ORCH-081):** ниже `*_default` добавлен непустой **per-role floor** — class-default поля `agent_effort_` из `config.py` (его пустой env перебить не может). Floor — строго последний уровень (ниже default) и срабатывает ТОЛЬКО когда все уровни пусты, поэтому пустые прод-`ORCH_AGENT_EFFORT_*=` (которые pydantic трактует как явное `''` и обнуляют дефолт) больше не приводят к запуску без `--effort`: каждая роль получает свой канонический пол (developer=`xhigh`, tester/deployer=`medium`, прочие=`high`). Непустой явный конфиг по-прежнему побеждает floor; опечатка вне `VALID_EFFORTS` дропается валидацией ДО floor (never-break, не маскируется). См. `docs/work-items/ORCH-081/06-adr/ADR-001-effort-resolution-floor.md`. frontmatter `model:` в `.openclaw/agents/*.md` **удалён** (ORCH-74 G1) — он был мёртвой/лживой декларацией (launcher его не читает); config — единственный источник правды о модели. Model-routing (G3) НЕ включён — все 6 агентов на `claude-opus-4-8`. | Агент | Модель | Эффорт | |-------|--------|--------| | analyst | claude-opus-4-8 | high | | architect | claude-opus-4-8 | high | | developer | claude-opus-4-8 | xhigh | | reviewer | claude-opus-4-8 | high | | tester | claude-opus-4-8 | medium | | deployer | claude-opus-4-8 | medium | **Валидация (ORCH-74 G2, never-break):** резолвенное имя модели проходит формат-чек `is_valid_model` (`^claude-[a-z0-9.-]+$`) перед попаданием в `--model`. Невалидное (опечатка, `gpt-4`, пустое) → `logger.warning` + откат на следующий валидный уровень (в пределе — без `--model`, CLI-дефолт); мусор **никогда** не уезжает в CLI и запуск не падает. Форма — формат-чек, а не статичный allowlist: forward-compatible (будущие `claude-*` проходят без правки кода). Тот же предикат гардит inline-чтение `--fallback-model` (`agent_fallback_model` читается мимо резолва — TRZ §4). Эффорт валидируется множеством `VALID_EFFORTS` (`low|medium|high|xhigh|max`). Fallback (G4) НЕ включён (`agent_fallback_model=""`). Детали — `docs/work-items/ORCH-074/06-adr/ADR-001-model-name-validation.md`. ### Условный 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). ### Толерантность staging-вердикта к инфра-FAIL (ORCH-061 — design) Self-hosting зацикливался на `deploy-staging`: `scripts/staging_check.py` давал ложный FAILED на C9a/C9b (ветка в sandbox / analyst-job в очереди), вызванный **отсутствием sandbox-настроек** (bot-аккаунты не члены SANDBOX-проекта), а не регрессом кода → откат `deploy-staging → development` → петля. ORCH-061 классифицирует проверки suite на **REAL** (pipeline) и **SANDBOX_INFRA** (узкий allowlist `{C9a, C9b}`) и делает вердикт толерантным к инфра-FAIL, сохраняя fail-closed для реальных проверок: - Чистая логика — leaf-модуль `src/staging_verdict.py` (`classify_check`, `compute_staging_verdict`, never-raise). Упала хоть одна REAL → FAILED/exit1; упали ТОЛЬКО SANDBOX_INFRA и толерантность вкл → SUCCESS/exit0 (waived); waiver применяется только когда все REAL (вкл. C7/C8) зелёные. - `scripts/staging_check.py` помечает проверки категориями, считает вердикт через `staging_verdict`, печатает `INFRA-WAIVED` (наблюдаемость). - Kill-switch `staging_infra_tolerance_enabled` (env `ORCH_STAGING_INFRA_TOLERANCE_ENABLED`, дефолт `true`, в `.env.staging`); `false` → 1:1 прежнее строгое поведение. - `check_staging_status` / `_parse_staging_status` / `STAGE_TRANSITIONS` / реестр `QG_CHECKS` — **без изменений** (новый QG-чек не вводится); условность ORCH-35 и схема БД сохранены. - Инвариант: «no changes to commit» на action-стадиях (`deploy-staging`/`deploy`) не есть недовыполнение — продвижение определяется exit0 + гейт-вердиктом (launcher не откатывает; добавлена observability-строка). Подробнее: [adr-0009](adr/adr-0009-staging-infra-tolerance.md), детально — `docs/work-items/ORCH-061/06-adr/ADR-001-staging-infra-tolerance.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`. - **Безусловный pre-merge rebase (ORCH-026, A-2):** при `premerge_rebase_always` (дефолт `True`, скоуп `merge_gate_repos`) short-circuit `branch_is_behind_main` пропускается — `auto_rebase_onto_main` вызывается **всегда** под лизом. На актуальной ветке это no-op (`rebase` не меняет HEAD, `push --force-with-lease` → «Everything up-to-date», CI не триггерится); на отстающей — реальный догон. Детерминированный структурный анти-фантом на уровне планировщика (дополняет рубежи ORCH-073, не заменяет). Kill-switch `premerge_rebase_always=False` → прежнее поведение (ребейз только при behind). - **Re-test:** `python -m pytest` (`merge_retest_target`, дефолт `tests/`) в worktree догнанной ветки, тайм-аут `merge_retest_timeout_s`. Красный/тайм-аут → откат на `development`. - **Сериализация (merge-lock):** файловый **merge-lease** на репо (`/.merge-lease-.json`), живёт от гейта до фактического merge. Acquire **неблокирующий** (anti-deadlock при `max_concurrency=1`): busy → **defer** (повторная постановка deployer'а на `deploy-staging` с задержкой через `available_at`), а не откат. Release — на PR-merged вебхуке / `deploy→done` / откате / по возрасту (crash-реклейм). Restart-safe; без изменения схемы БД. **ORCH-026 (A-1):** это окно = «merge → main-updated» (для self `done` ⇔ SHA-in-main, ORCH-073) — пока A не в `main`, B того же репо получает `merge-lock busy` → defer. Окно сериализации per-repo НЕ переписывается; кросс-репо параллелизм сохранён (лиз — per-repo файл). - **Условность (как 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`. Безусловный pre-merge rebase + связь с зависимостями задач — [adr-0015](adr/adr-0015-task-deps-and-merge-serialization.md) (ORCH-026). ### Зависимости задач: B ждёт A (ORCH-026, Уровень B) Плоская очередь ORCH-1 (FIFO по `id` + `available_at` + `max_concurrency`) не выражала логических зависимостей. ORCH-026 вводит декларативные связи «задача B не стартует, пока не готовы её depends-on» — без новой стадии и без изменения `STAGE_TRANSITIONS`/`QG_CHECKS`. - **Источник истины планировщика — БД** (аддитивная таблица `job_deps(task_id, depends_on_task_id)`): claim в горячем цикле обслуживает очередь ВСЕХ проектов и обязан быть offline-устойчив (сетевой Plane на каждый claim = встанет очередь всех проектов). Источник **декларации** настраивается `task_deps_source = db|plane|hybrid` (дефолт `db`; `plane`/`hybrid` читают Plane relations в `handle_work_item_created` и кэшируют в `job_deps`). - **Гейт планировщика (`claim_next_job`)** — условие `NOT EXISTS (job_deps d JOIN tasks t … WHERE d.task_id=j.task_id AND t.stage!='done')` при `task_deps_enabled`: задача с незавершённой зависимостью **не выбирается** (агент не запускается, слот `max_concurrency` не занимается). Инертно при пустой `job_deps` → нулевая регрессия; kill-switch `task_deps_enabled=False` → запрос 1:1 как ORCH-1. - **Детект дедлоков** — DFS-цикл-детектор (leaf `src/task_deps.py::detect_cycle`) при вставке связи + backstop в `reconciler`; цикл → `set_issue_blocked` + alert (Telegram/Plane) с перечислением цикла. Поток остальных задач не блокируется. - **Видимость** — строка «⏳ ждёт ORCH-NNN» в Telegram-карточке (`update_task_tracker`, never-raise); Plane `Blocked` — на дедлоке (не на нормальном коротком ожидании, чтобы не флаппить). Инвариант «одна карточка на задачу» сохранён. - **Совместимость:** `reconciler` F-1 пропускает dep-заблокированные задачи (`is_task_ready`, паттерн ORCH-060); `reaper` сканирует только `running` → dep-блок остаётся `queued`, не трогается. Зависимости — только intra-repo (v1). - **Наблюдаемость:** блок `task_deps` в `GET /queue` (заблокированные задачи, держатель merge-lease, defer-счётчики, обнаруженные циклы) — read-only. Подробнее: [adr-0015](adr/adr-0015-task-deps-and-merge-serialization.md), детально — `docs/work-items/ORCH-026/06-adr/ADR-001-merge-serialization-and-task-deps.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 + запрос перевести задачу в статус **«Confirm Deploy»** (ORCH-059; Plane-коммент + Telegram). Перехват в `advance_stage` ПОСЛЕ `check_staging_status` и merge-gate. - **Фаза B (Plane → `Confirm Deploy`, ORCH-059)** — `advance_stage(deploy, finished_agent=None, confirm_deploy=True)` запускает **detached host-процесс** (ssh + setsid → хук с прод-параметрами + build-once retag `SOURCE_IMAGE`) и ставит детерминированный **finalizer-job**; маркер `initiated` — идемпотентность. Возврат БЕЗ advance (вердикта ещё нет). Обычный `Approved` на `deploy` (`confirm_deploy=False`) — детерминированный no-op (не деплоит и не откатывает). - **Фаза C (finalizer)** — новый контейнер после рестарта читает sentinel `result` (exit-code хука), маппит `0→SUCCESS / иначе→FAILED`, пишет `14-deploy-log.md`, вызывает `advance_stage(deploy, finished_agent="deployer")` → существующие контракты: `SUCCESS → done`, `FAILED → откат БАГ-8 на development`. Триггер прод-деплоя = смена статуса Plane на `Confirm Deploy` (ORCH-059; status-only verdict model; комментарии не управляют конвейером). `Approved` остаётся исключительно человеческим гейтом конвейера и прод-деплой не запускает. На старте — обязательный ручной 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-файлы (`/.deploy-state-//`), без миграции БД. Подробнее: [adr-0007](adr/adr-0007-executable-self-deploy.md), детально — `docs/work-items/ORCH-036/06-adr/ADR-001-executable-self-deploy.md`. #### Выделенный статус-триггер прод-деплоя «Confirm Deploy» (ORCH-059 — реализовано) Перегрузка: один Plane-статус `Approved` служил И человеческим гейтом BRD на `analysis` (`check_analysis_approved`), И триггером Фазы B прод-деплоя на `deploy` — привычный жест approve молча запускал прод-рестарт (групповой self-hosting риск). ORCH-059 разделяет жесты: вводится отдельный логический статус `confirm_deploy` («Confirm Deploy»), который триггерит **ТОЛЬКО** Фазу B на `deploy`; `Approved` остаётся исключительно гейтом конвейера. - `_PLANE_NAME_TO_KEY` += `"Confirm Deploy" → "confirm_deploy"`; в `_DEFAULT_STATES` ключ НЕ добавляется (нет UUID для enduro/fallback) → **fail-closed**: нет статуса → нет деплоя, без `KeyError` (доступ через `.get`). - `handle_issue_updated` маршрутизирует `Confirm Deploy` → `handle_confirm_deploy` (гард `stage=="deploy"`) → `_try_advance_stage(..., confirm_deploy=True)`. - `advance_stage` получает kwarg `confirm_deploy: bool=False`; блок Фазы B (`deploy`+`finished_agent is None`+self-hosting) деплоит ТОЛЬКО при `confirm_deploy=True`, иначе (обычный `Approved`) — **no-op** (`check_deploy_status` не запускается → нет ложного отката БАГ-8). - CTA Фазы A (`_handle_self_deploy_phase_a`) просит «Confirm Deploy», не «Approved». - Условность как ORCH-35/36 (только `orchestrator`); Фазы A/C, `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_deploy_status`, merge-gate, схема БД — без изменений. - Эксплуатация: в Plane-проекте ORCH создать статус «Confirm Deploy» + сброс кэша состояний (`docs/work-items/ORCH-059/07-infra-requirements.md`). Детально — `docs/work-items/ORCH-059/06-adr/ADR-001-confirm-deploy-status.md` (уточняет/триггер Фазы B относительно adr-0007). #### Merge-в-main + пост-деплой верификация как условие `done` (ORCH-071 — фикс фантомного merge) **Фантомный merge** (CRITICAL, постмортем `docs/history/LESSONS_2026-06-08_phantom-merge.md`): на self-hosting пути `deploy` агент `deployer` НЕ запускается, а фактический merge PR в `main` исторически делал ТОЛЬКО он → детерминированный путь (`_handle_self_deploy_phase_b → initiate_deploy → run_deploy_finalizer`) **не содержал шага merge-в-main вообще**. Detached host-деплой лишь retag'ал образ + рестартил 8500; `done` достигался по `deploy_status: SUCCESS` без верификации `main`. Зелёный деплой (образ из рабочей ветки) маскировал отсутствие merge → следующая задача срезала ветку от устаревшего `main` и теряла код предшественника (накопительно потеряны ORCH-022/059/066/068). ORCH-071 вводит **детерминированный merge-актор + пост-merge верификацию** как **под-гейт ребра `deploy → done`** (симметрично edge-под-гейтам `deploy-staging → deploy`), только для self-hosting: - **Врезка `_handle_merge_verify` в `advance_stage`** (`current_stage=="deploy"` и `next_stage=="done"`, ПОСЛЕ зелёного `check_deploy_status`, ДО `update_task_stage`). Гейтит **ВСЕ** пути к `done` единообразно (`run_deploy_finalizer` Phase C, reconciler F-1, job-reaper — все идут через `advance_stage`), закрывая дыру обхода merge. - **Merge в Phase C (после рестарта), НЕ в Phase B** — finalizer restart-surviving (claim воркером нового контейнера, re-drive reaper'ом), merge физически строго ПОСЛЕ рестарта прода → рестарт его не убивает (G3 «шаг, переживающий рестарт»; постмортем-урок №3). - **Merge-актор `merge_gate.merge_pr`** — `pr_already_merged` (idempotency no-op повтор) → иначе Gitea `POST /repos/{owner}/{repo}/pulls/{index}/merge`. Выбор PR строго по `head.ref==branch` И `base.ref=="main"`. Никогда push/force-push в `main`. - **Верификатор `merge_gate.verify_merged_to_main` (семантика ORCH-073, FR-1):** подтверждение — **ТОЛЬКО** `git merge-base --is-ancestor origin/main` (`validated_revision` — якорь ORCH-058). PR-флаг `pr_already_merged` **больше НЕ подтверждает merge** (удалён из verify): он понижен до idempotency-guard `merge_pr` и засчитывает merged PR лишь при `head.ref==branch` И `base.ref=="main"` (исключает авто docs-PR). Пустой SHA / git-ошибка → `False` (fail-closed), never-raise. - **Регресс-гард целостности `main` (ORCH-073, FR-5):** `merge_gate.check_main_regression` в `_handle_merge_verify` ПОСЛЕ подтверждённого SHA-в-main и ДО `done` проверяет, что `origin/main` содержит декларативный набор маркеров ранее-merged задач (`MAIN_REGRESSION_MARKERS`, `git grep -c origin/main -- ` > 0). Маркер отсутствует → alert «main regressed» + HOLD (НЕ `done`, ALERT-only). Fail-open на git-ошибке грепа (регресс — только при `count==0`). Kill-switch `regression_guard_enabled`; non-self → no-op. Набор — append-only константа, значимая задача дописывает свой маркер. - **Не подтверждено → alert «deploy succeeded but not merged» (Telegram+Plane) + HOLD** (`set_issue_blocked`, задача НЕ `done`, БЕЗ авто-отката на `development` — not-merged есть инфра-дефект, реакция ALERT-only как ORCH-021 self-hosting). Подтверждено → штатный `deploy → done` + `merged_to_main: true` во frontmatter `14-deploy-log.md` (`deploy_status:` нетронут). - **Защита от CHANGELOG-затирания (ORCH-073, FR-4):** корневой `.gitattributes` с `CHANGELOG.md merge=union` → правки `## [Unreleased]` авто-сливаются при `auto_rebase_onto_main` без конфликта, ветка не откатывается в `development` и не тащит устаревший код-сосед. `docs/**` под union НЕ ставится (union только для append-only). - **Условность как ORCH-35/43/58:** `merge_verify_enabled` (kill-switch, дефолт `true`) + `merge_verify_repos` (пусто → только self-hosting); non-self — no-op, merge остаётся за `deployer`. never-raise; идемпотентность по **SHA-в-main** (INV-4, не «любой merged PR»); ручной approve сохранён (`Confirm Deploy`). - **Инварианты:** `STAGE_TRANSITIONS`, `check_deploy_status`/`_parse_deploy_status`, реестр `QG_CHECKS` (под-гейт — врезка в `advance_stage`, НЕ новый зарегистрированный QG), схема БД, БАГ-8, terminal-sync, merge-gate, image-freshness, exit-коды хука — **без изменений**. Диагностика фантома — runbook `docs/operations/PHANTOM_MERGE_RUNBOOK.md` (4 проверки постмортема). Подробнее: [adr-0013](adr/adr-0013-merge-verify-gate.md) + [adr-0014](adr/adr-0014-merge-verify-sha-source-of-truth.md) (amends 0013 — SHA-в-main как единственный критерий + регресс-гард, ORCH-073); детально — `docs/work-items/ORCH-071/06-adr/ADR-001-merge-verify-gate.md`, `docs/work-items/ORCH-073/06-adr/ADR-001-merge-verify-sha-truth-and-regression-guard.md`. #### Гарантированный код-PR перед merge-verify (ORCH-082 — фикс ложного HOLD «no open PR») Под-гейт merge-verify (ORCH-071/073) детерминированно мержит **открытый** код-PR ветки в `main` (`merge_pr`, фильтр `head.ref==branch` И `base.ref=="main"`). Но конвейер **не гарантировал**, что к моменту merge у ветки этот PR есть: PR создаётся единственной `launcher._ensure_pr` **только** на developer-пути и **только** при свежем worktree-коммите. На деплое ORCH-074 (08.06, первая задача после ручных восстановлений `main`) у ветки не оказалось открытого код-PR → `merge_pr` вернул `("False", "no open PR")` → защита ORCH-073 верно удержала задачу (HOLD, не ложный `done`), но это лечило следствие. ORCH-082 закрывает **отсутствующий инвариант** «к merge-verify у ветки есть открытый код-PR» аддитивно, внутри того же под-гейта, не трогая машину стадий: - **Новый leaf-актор `merge_gate.ensure_open_pr(repo, branch) -> (status, detail)`** (never-raise): `GET …/pulls?state=open` с фильтром `head.ref==branch` И `base.ref=="main"` (**идентичен** `merge_pr`/ORCH-073 FR-3 — авто-docs-PR `base != main` НЕ код-PR) → `("existed", N)`; иначе `POST …/pulls` → `("created", N)`; гонка «PR exists»/409/422 → повторный GET → `existed` (без дублей); любая иная ошибка → `("failed", reason)`. - **Врезка в `_handle_merge_verify`** ПОСЛЕ резолва `validated_revision` и **ПЕРЕД** `merge_pr`: `created|existed` → штатно к `merge_pr` → `verify_merged_to_main`; `failed` → честный HOLD+alert через новый helper `_hold_pr_create_failed` (текст «PR создать не удалось» — отличим от not-merged HOLD; `result.note="pr-create-failed-hold"`), задача остаётся на `deploy`, БЕЗ отката на development. - **Защита ORCH-073 неприкосновенна и приоритетна:** подтверждение merge остаётся ТОЛЬКО `verify_merged_to_main` (SHA-в-main) + `check_main_regression`; `ensure_open_pr` устраняет лишь **ложный** HOLD «no open PR», но не маскирует реально невлитый код (тот → HOLD как прежде). - **`launcher._ensure_pr`** рекомендуется делегировать в `ensure_open_pr` (единый код создания PR), сохранив прежний триггер «только developer-путь». - **Условность как ORCH-35/43/58/71:** kill-switch `merge_verify_autocreate_pr_enabled` (дефолт `true`); область — `merge_verify_applies(repo)` (self-hosting / `merge_verify_repos`); non-self — no-op. `False` → поведение ORCH-074 1:1. Идемпотентность из Gitea (наличие открытого PR), **без миграции БД** (restart-safe). `STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД, `check_deploy_status`, exit-коды хука, merge-gate, image-freshness — без изменений; `main` не push/force-push. Подробнее: [adr-0016](adr/adr-0016-ensure-open-pr-before-merge-verify.md) (amends 0013/0014); детально — `docs/work-items/ORCH-082/06-adr/ADR-001-ensure-open-pr-before-merge-verify.md`. ### Post-deploy наблюдение прода + реакция на деградацию (ORCH-021 — реализовано) Конвейер заканчивался на `deploy → done` и **забывал про прод**: «успех» = health-check в момент рестарта (~60с). Класс «зелёный деплой, красный прод» (прецедент ET-8 — деградация через минуты под трафиком, health `200 ok`, фича сломана). ORCH-021 продлевает ответственность **ЗА** `done`: для применимого репо после терминального перехода армится наблюдение окна `post_deploy_window_s` (~15 мин) с интервалом `post_deploy_interval_s`; деградация фиксируется по детерминированным порогам, при подтверждении — реакция. Механизм — **reserved-agent job `post-deploy-monitor`** (калька `deploy-finalizer`, НЕ стадия и НЕ daemon): арм в `advance_stage` в блоке `next_stage == "done"` (`post_deploy.arm_monitor`, sentinel `armed` = идемпотентность); тик перехватывается в `launcher.launch_job` ДО `_spawn` → `stage_engine.run_post_deploy_monitor` (один опрос → append в `series` → классификация → перепостановка с задержкой ИЛИ реакция+артефакт+`done`). Чистая логика — новый leaf-модуль `src/post_deploy.py` (never-raise): `post_deploy_applies`, `probe_signals` (`/health` 200+`{"status":"ok"}` + доля 5xx на `/status`,`/queue`), `classify` (HEALTHY|DEGRADED — главный предмет юнит-тестов), `decide_action`, sentinel-state, `write_post_deploy_log`. - **Пороги (BR-3):** `DEGRADED` ⇔ `≥ post_deploy_fail_threshold` ПОСЛЕДОВАТЕЛЬНЫХ провалов health ИЛИ доля 5xx `> post_deploy_5xx_threshold`; одиночный глюк → HEALTHY (нет ложных откатов). - **Реакция:** self-hosting (`orchestrator`) — ВСЕГДА `ALERT_ONLY` (Telegram+Plane, ручной approve; тик НИКОГДА не откатывает/рестартит прод-контейнер); не-self + `post_deploy_auto_rollback=true` → хук `--rollback` (`0→ROLLBACK_OK`, `1/2→ROLLBACK_FAILED`+алерт); дефолт → `ALERT_ONLY`. - **Артефакт** `16-post-deploy-log.md` (YAML-frontmatter `post_deploy_status`/ `action_taken`/…) — машиночитаемо для петли уроков ORCH-8; best-effort. - **Наблюдаемость** — блок `post_deploy` в `GET /queue` (образец `reconcile`). - **Инварианты:** `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_deploy_status`, terminal-sync, merge-gate, exit-коды хука (0/1/2), схема БД — НЕ меняются. Restart-safe (sentinel `.post-deploy-state-//` + jobs-очередь). Kill-switch `post_deploy_monitor_enabled`, область `post_deploy_repos` (пусто → self-hosting). Условность как ORCH-35/36/43/58. Подробнее: [adr-0010](adr/adr-0010-post-deploy-monitor.md), детально — `docs/work-items/ORCH-021/06-adr/ADR-001-post-deploy-monitor.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=`, 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`. ### Security-гейт: secret-scanning + dependency audit перед мержем (ORCH-022 — реализовано) Автономный конвейер вливал ветку в `main` без проверки на утёкший секрет (ключ/токен/пароль/ приватный ключ) и уязвимую зависимость (CVE); для self-hosting один секрет/CVE через одну задачу уезжал в общий прод всех проектов (CLAUDE.md §8). ORCH-022 вводит детерминированный (без LLM) **security-гейт как под-гейт ребра `deploy-staging → deploy`**, рядом с merge-gate (ORCH-043) и image-freshness (ORCH-058), исполняемый **ПЕРВЫМ** среди edge-под-гейтов (ДО merge-gate). Паттерн соседей: leaf `src/security_gate.py` (never-raise) + тонкая обёртка `check_security_gate` в `QG_CHECKS` + врезка `_handle_security_gate` в `advance_stage`. `STAGE_TRANSITIONS` и схема БД — **без изменений**. - **Secret-scanning (`gitleaks`, offline):** скан `origin/main..HEAD`; любой секрет вне аллоулиста `.gitleaks.toml` → вклад в FAIL. Offline → гарантия «секрет всегда блокирует» не зависит от сети (безусловна). - **Dependency audit (`pip-audit`, OSV/PyPI):** severity ≥ `security_dep_block_severity` (дефолт `HIGH`) → FAIL; ниже / UNKNOWN → warning. Недоступность фида → **fail-open + громкий warning** (анти-петля ORCH-061; флаг `security_dep_audit_fail_closed` для строгого режима). best-effort при доступности фида. - **ПЕРВЫМ, ДО merge-gate:** дёшево фейлить до дорогих rebase/rebuild; скан ветки ДО rebase не «обвиняет» задачу в CVE из обновившегося `main`; до захвата merge-lease → при FAIL lease освобождать не нужно. - **Артефакт `17-security-report.md`** (YAML-frontmatter `security_status`/`secrets_found`/ `deps_blocking`/`deps_warning`/`deps_audit_degraded`); вердикт читается ТОЛЬКО из frontmatter (гейт пишет → читает обратно через `parse_security_status` → возвращает: единый источник истины), negative-токен авторитетен, битый/нет → fail-closed. - **FAIL → откат на `development`** + developer-retry (общий `_developer_retry_count`, cap 3, затем `set_issue_blocked` + Telegram); `task_desc` несёт дословные находки (ORCH-046). - **Условность как ORCH-35/43/58:** `security_gate_enabled` + `security_gate_repos` (пусто → только self-hosting); never-raise; таймаут `security_scan_timeout_s`; гейт не деплоит/не рестартит прод. v1 — Python-only; SAST/мульти-стек — follow-up (BR-14). Подробнее: [adr-0012](adr/adr-0012-security-gate.md), детально — `docs/work-items/ORCH-022/06-adr/ADR-001-security-gate.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` не реконсилируется. **Skip escalated / Blocked / Needs-Input (ORCH-060):** ДО оценки гейта F-1 пропускает (молча, без advance/нотификаций) задачи, которые ждут человека — (1) исчерпавшие лимит developer-ретраев (`developer_retry_count(task_id) >= MAX_DEVELOPER_RETRIES`, детерминированно, без сети — закрывает bounce-петлю ET-013) и (2) в явном Plane-статусе **Blocked** / **Needs Input** (Вариант A — запрос Plane API, без миграции БД; never-raise → консервативный skip). Гард retry-count проверяется первым (дёшево, локальный SQL). **ORCH-086 (закрытие F-1-пробела ORCH-068):** терминал-исключение и `state_uuid`-dedup (изначально только F-2) распространены на F-1. После дешёвых локальных гардов F-1 делает **один** резолв Plane-статуса задачи на тик (общий fetch для Guard 2 + терминал-скипа + `_note_unblock`); терминальная задача (группа Plane `completed`/`cancelled`, fallback — логические ключи `done`/`cancelled`, ЛИБО стадия в БД орка ∈ `{done, cancelled}`) → **безусловный** ранний скип (`skipped_terminal_total++`, без `advance`/уведомления; не подчинён `reconcile_skip_blocked_enabled`). Вызов `_note_unblock` на F-1 теперь передаёт `state_uuid` → in-memory dedup работает на обоих путях (страховка от повтора после рестарта). Лечит периодическое ложное «ET-002 done разблокирована (потерян webhook)» для терминальных в Plane задач (enduro/orchestrator), сохраняя легитимный unblock реально застрявшей не-терминальной задачи. `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД/сигнатуры/новые флаги — без изменений. Детали — `docs/work-items/ORCH-086/06-adr/ADR-001-reconciler-f1-terminal-skip-and-dedup.md`. - **F-2 plane-side:** опрос Plane API per-project → `handle_status_start` / `handle_verdict` из `webhooks/plane.py` (логика не дублируется). **ORCH-068 (livelock-fix):** (1) задачи в **терминальной группе** Plane (`state.group ∈ {completed, cancelled}`, fallback — логические ключи `done`/`cancelled`) исключаются из actionable-выборки per-issue — проектно-независимо, устойчиво к UUID-алиасингу после переименований статусов (ORCH-066); (2) `_note_unblock` (лог + Telegram + `unblocked_total`) вызывается ТОЛЬКО при **подтверждённом state change** (сравнение стадии задачи до/после `_dispatch`; no-op dispatch → тишина), плюс in-memory дедуп по `issue_id→state`. Восстанавливает инвариант silence-when-in-sync (AC-9/AC-10). Детали — `docs/work-items/ORCH-068/06-adr/ADR-001-reconciler-terminal-exclusion-and-cache-ttl.md`. - **F-3:** усиление `sha→branch` в `handle_ci_status` (БД-fallback по единственной development-задаче repo; неоднозначность → не резолвим). - **F-4 observability:** при разблокировке — лог-строка `reconciler: разблокирована (потерян webhook)` + Telegram (`reconcile_notify_unblock`); снимок состояния в `GET /queue` (блок `reconcile`). **ORCH-068** добавляет в снимок счётчики `skipped_terminal_total` (исключённые терминалы) и `deduped_total` (подавленные повторные нотификации). Реализация: `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`. ### Job-reaper + проактивный реклейм merge-lease (ORCH-065 — design) Финализация статуса job (`done`/`queued`/`failed`) выполняется ТОЛЬКО в `launcher._monitor_agent → _finalize_job` внутри живого процесса. Смерть monitor-потока/процесса между `proc.wait()` и `_finalize_job` (краш, OOM, self-restart во время deploy) оставляла строку `jobs` навсегда `running`; при `max_concurrency=1` одна зомби-строка блокирует claim всех job → встаёт конвейер ВСЕХ проектов (инциденты 07.06: jobs 236/239/242/254). `requeue_running_jobs()` спасал ТОЛЬКО на старте процесса. Симметрично залипал merge-lease (ORCH-043): реклейм был лениво-по-TTL и только при чужом `acquire`, liveness держателя по pid не проверялся. Это последняя ручная точка автономного self-deploy (блокер ORCH-54). ORCH-065 вводит фоновый watchdog, чтобы смерть процесса/потока на любой стадии НЕ оставляла навсегда захваченных ресурсов: - **Job-reaper** (`src/job_reaper.py`) — daemon-поток по образцу `reconciler`, работает **без рестарта**. Трёхуровневая liveness: Tier-1 мёртвый `jobs.pid` (новая колонка) после `reaper_dead_ticks` подряд тиков (анти-ложноположительность — живой долгий агент не реапится); Tier-2 `agent_runs.exit_code` записан, а job ещё `running` — но это окно неоднозначно (живой monitor пишет exit_code ПЕРВЫМ, затем git push/PR/Plane-комментарии), поэтому Tier-2 реапит только после finalization-grace `reaper_finalize_grace_s` (живой финализирующий monitor НЕ реапится); Tier-3 backstop по потолку `reaper_max_running_s` (> max agent_timeout+grace). Действие переиспользует контракты по принципу **claim-before-act**: для exit0 канонический QG оценивается read-only ПЕРЕД атомарным claim, затем claim `done` ПЕРВЫМ и только победитель claim делает `_try_advance_stage` (advance+enqueue) — проигравший claim (поздний monitor / стартовый requeue) не выполняет побочных эффектов (нет дубль-advance/-enqueue); источник истины — канонический QG, не факт «exit0»; гейт красный или exit≠0/ неизвестно → `attempts`: ``` {ICON} {RoleName} — {описание стадии} [Verdict|Status: VALUE] # reviewer/tester/deployer, из YAML-frontmatter артефакта [Длительность: 4m 12s] # явный duration_s от launcher, либо fallback из agent_runs Документы: [8.5M in / 45.8k out · $7.29] # тех-хвост 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); колонка `pid` (ORCH-065) — pid агентского процесса для liveness-детекции зомби job-reaper'ом - `job_deps` — декларативные зависимости задач (ORCH-026, Уровень B): `(task_id, depends_on_task_id)`, аддитивная; источник истины планировщика для гейта «B ждёт A» ## Изоляция (git worktree, ORCH-2) Каждая задача исполняется в отдельном git worktree, ветки не пересекаются. Репозитории проектов разделены под `/repos/`. ## API | Method | Path | Описание | |--------|------|----------| | GET | `/health` | health check | | GET | `/status` | активные задачи (stage != done) | | GET | `/queue` | очередь: counts + max_concurrency + resilience + reconcile (ORCH-053) + reaper (ORCH-065) + post_deploy (ORCH-021) + последние 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//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-060 (F-1 skip escalated/Blocked/Needs-Input, `docs/work-items/ORCH-060/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-060 (Guard 1 `developer_retry_count>=MAX_DEVELOPER_RETRIES` + Guard 2 `plane_sync.fetch_issue_state` Blocked/Needs-Input, флаг `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED`); 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); ORCH-061 (толерантность staging-вердикта к инфра-FAIL C9a/C9b, adr-0009, `docs/work-items/ORCH-061/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-061 (обновлять также при изменении src/staging_verdict.py, scripts/staging_check.py, флаг staging_infra_tolerance_enabled); ORCH-021 (post-deploy наблюдение прода + реакция на деградацию, adr-0010, `docs/work-items/ORCH-021/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-021-post-deploy-rollback (reserved-agent job `post-deploy-monitor`: арм в src/stage_engine.py блок `next_stage == "done"`, тик `run_post_deploy_monitor` + перехват в src/agents/launcher.py ДО _spawn; чистая логика src/post_deploy.py never-raise; флаги `post_deploy_*` в src/config.py; блок `post_deploy` в `/queue`; артефакт 16-post-deploy-log.md; self-hosting всегда ALERT_ONLY — тик не рестартит прод; обновлять также при изменении src/post_deploy.py / арм-блока / launcher-перехвата); ORCH-065 (job-reaper + проактивный реклейм merge-lease + идемпотентная финализация merge, adr-0011, `docs/work-items/ORCH-065/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-065 (новый daemon-поток src/job_reaper.py + старт/стоп в src/main.py lifespan; колонка `jobs.pid` через _ensure_column + проставление в src/agents/launcher.py `_spawn`; функции реклейма lease `pid_alive`/`reclaim_stale_lease` + guard `pr_already_merged` в src/merge_gate.py (консультируется merge-актором — промпт `.openclaw/agents/deployer.md`); флаги `reaper_*`/`lease_reclaim_*` в src/config.py; блок `reaper` в `/queue`; обновлять также при изменении этих мест); ORCH-022 (security-гейт: secret-scanning gitleaks + dependency audit pip-audit как под-гейт ребра `deploy-staging → deploy` ПЕРВЫМ, adr-0012, `docs/work-items/ORCH-022/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-022-security-secret-scanning (leaf src/security_gate.py never-raise + check_security_gate в src/qg/checks.py `QG_CHECKS` + врезка _handle_security_gate в src/stage_engine.py блок `current_stage == "deploy-staging"` ПЕРВОЙ; флаги `security_*` в src/config.py; gitleaks (pinned) в Dockerfile, pip-audit в requirements.txt, `.gitleaks.toml` в корне; артефакт 17-security-report.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-060 (F-1 skip escalated/Blocked/Needs-Input, `docs/work-items/ORCH-060/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-060 (Guard 1 `developer_retry_count>=MAX_DEVELOPER_RETRIES` + Guard 2 `plane_sync.fetch_issue_state` Blocked/Needs-Input, флаг `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED`); 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); ORCH-061 (толерантность staging-вердикта к инфра-FAIL C9a/C9b, adr-0009, `docs/work-items/ORCH-061/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-061 (обновлять также при изменении src/staging_verdict.py, scripts/staging_check.py, флаг staging_infra_tolerance_enabled); ORCH-021 (post-deploy наблюдение прода + реакция на деградацию, adr-0010, `docs/work-items/ORCH-021/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-021-post-deploy-rollback (reserved-agent job `post-deploy-monitor`: арм в src/stage_engine.py блок `next_stage == "done"`, тик `run_post_deploy_monitor` + перехват в src/agents/launcher.py ДО _spawn; чистая логика src/post_deploy.py never-raise; флаги `post_deploy_*` в src/config.py; блок `post_deploy` в `/queue`; артефакт 16-post-deploy-log.md; self-hosting всегда ALERT_ONLY — тик не рестартит прод; обновлять также при изменении src/post_deploy.py / арм-блока / launcher-перехвата); ORCH-065 (job-reaper + проактивный реклейм merge-lease + идемпотентная финализация merge, adr-0011, `docs/work-items/ORCH-065/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-065 (новый daemon-поток src/job_reaper.py + старт/стоп в src/main.py lifespan; колонка `jobs.pid` через _ensure_column + проставление в src/agents/launcher.py `_spawn`; функции реклейма lease `pid_alive`/`reclaim_stale_lease` + guard `pr_already_merged` в src/merge_gate.py (консультируется merge-актором — промпт `.openclaw/agents/deployer.md`); флаги `reaper_*`/`lease_reclaim_*` в src/config.py; блок `reaper` в `/queue`; обновлять также при изменении этих мест); ORCH-059 (выделенный статус-триггер прод-деплоя «Confirm Deploy», ADR `docs/work-items/ORCH-059/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-059 (маппинг `"Confirm Deploy"→"confirm_deploy"` в src/plane_sync.py `_PLANE_NAME_TO_KEY`, НЕ в `_DEFAULT_STATES` = fail-closed; ветка `handle_confirm_deploy` + fail-closed `.get("confirm_deploy")` в src/webhooks/plane.py `handle_issue_updated`; keyword-only `confirm_deploy` в src/stage_engine.py `advance_stage` — Фаза B деплоит ТОЛЬКО при `confirm_deploy=True`, иначе `Approved`-на-`deploy` = no-op; CTA Фазы A просит «Confirm Deploy»; эксплуатация — статус доски «Confirm Deploy» в Plane-проекте ORCH, `docs/work-items/ORCH-059/07-infra-requirements.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-060 (F-1 skip escalated/Blocked/Needs-Input, `docs/work-items/ORCH-060/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-060 (Guard 1 `developer_retry_count>=MAX_DEVELOPER_RETRIES` + Guard 2 `plane_sync.fetch_issue_state` Blocked/Needs-Input, флаг `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED`); 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); ORCH-061 (толерантность staging-вердикта к инфра-FAIL C9a/C9b, adr-0009, `docs/work-items/ORCH-061/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-061 (обновлять также при изменении src/staging_verdict.py, scripts/staging_check.py, флаг staging_infra_tolerance_enabled); ORCH-021 (post-deploy наблюдение прода + реакция на деградацию, adr-0010, `docs/work-items/ORCH-021/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-021-post-deploy-rollback (reserved-agent job `post-deploy-monitor`: арм в src/stage_engine.py блок `next_stage == "done"`, тик `run_post_deploy_monitor` + перехват в src/agents/launcher.py ДО _spawn; чистая логика src/post_deploy.py never-raise; флаги `post_deploy_*` в src/config.py; блок `post_deploy` в `/queue`; артефакт 16-post-deploy-log.md; self-hosting всегда ALERT_ONLY — тик не рестартит прод; обновлять также при изменении src/post_deploy.py / арм-блока / launcher-перехвата); ORCH-065 (job-reaper + проактивный реклейм merge-lease + идемпотентная финализация merge, adr-0011, `docs/work-items/ORCH-065/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-065 (новый daemon-поток src/job_reaper.py + старт/стоп в src/main.py lifespan; колонка `jobs.pid` через _ensure_column + проставление в src/agents/launcher.py `_spawn`; функции реклейма lease `pid_alive`/`reclaim_stale_lease` + guard `pr_already_merged` в src/merge_gate.py (консультируется merge-актором — промпт `.openclaw/agents/deployer.md`); флаги `reaper_*`/`lease_reclaim_*` в src/config.py; блок `reaper` в `/queue`; обновлять также при изменении этих мест); ORCH-066 (осмысленная статусная модель Plane — слой B, `docs/work-items/ORCH-066/06-adr/ADR-001-plane-status-model.md`) — реализовано в ветке feature/ORCH-066-plane (только Plane-индикация: новые ключи `to_analyse`/`analysis`/`code_review`/`awaiting_deploy`/`deploying`/`monitoring` в `_PLANE_NAME_TO_KEY`/`_DEFAULT_STATES` + project-relative `_STATE_ALIAS_FALLBACK` в get_project_states + `_STAGE_TO_STATE_KEY` analysis/review + 5 новых `set_issue_*` в src/plane_sync.py; триггер `in_progress`→`to_analyse` и `set_issue_analysis` в src/webhooks/plane.py; Phase A→Awaiting Deploy / Phase B→Deploying / terminal-sync split monitoring↔done / post-deploy monitor HEALTHY→Done DEGRADED→Blocked в src/stage_engine.py; F-2 триггер `to_analyse` + Guard 2 skip-set с вычитанием base_working в src/reconciler.py; `STAGE_TRANSITIONS`/QG/схема БД НЕ трогаются; без kill-switch — раскат гейтится созданием 6 Plane-статусов оператором, `docs/work-items/ORCH-066/07-infra-requirements.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-060 (F-1 skip escalated/Blocked/Needs-Input, `docs/work-items/ORCH-060/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-060 (Guard 1 `developer_retry_count>=MAX_DEVELOPER_RETRIES` + Guard 2 `plane_sync.fetch_issue_state` Blocked/Needs-Input, флаг `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED`); 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); ORCH-061 (толерантность staging-вердикта к инфра-FAIL C9a/C9b, adr-0009, `docs/work-items/ORCH-061/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-061 (обновлять также при изменении src/staging_verdict.py, scripts/staging_check.py, флаг staging_infra_tolerance_enabled); ORCH-021 (post-deploy наблюдение прода + реакция на деградацию, adr-0010, `docs/work-items/ORCH-021/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-021-post-deploy-rollback (reserved-agent job `post-deploy-monitor`: арм в src/stage_engine.py блок `next_stage == "done"`, тик `run_post_deploy_monitor` + перехват в src/agents/launcher.py ДО _spawn; чистая логика src/post_deploy.py never-raise; флаги `post_deploy_*` в src/config.py; блок `post_deploy` в `/queue`; артефакт 16-post-deploy-log.md; self-hosting всегда ALERT_ONLY — тик не рестартит прод; обновлять также при изменении src/post_deploy.py / арм-блока / launcher-перехвата); ORCH-065 (job-reaper + проактивный реклейм merge-lease + идемпотентная финализация merge, adr-0011, `docs/work-items/ORCH-065/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-065 (новый daemon-поток src/job_reaper.py + старт/стоп в src/main.py lifespan; колонка `jobs.pid` через _ensure_column + проставление в src/agents/launcher.py `_spawn`; функции реклейма lease `pid_alive`/`reclaim_stale_lease` + guard `pr_already_merged` в src/merge_gate.py (консультируется merge-актором — промпт `.openclaw/agents/deployer.md`); флаги `reaper_*`/`lease_reclaim_*` в src/config.py; блок `reaper` в `/queue`; обновлять также при изменении этих мест); ORCH-068 (livelock-fix reconciler F-2: терминал-исключение по группе состояния + `_note_unblock` только при подтверждённом state change + дедуп; TTL `_STATES_CACHE`, `docs/work-items/ORCH-068/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-068 (D1 терминал-гард по группе `_is_terminal_state` + `get_project_state_groups` в src/plane_sync.py; D2 сравнение стадии до/после `_dispatch` + дедуп-словарь в src/reconciler.py; TTL-запись `_STATES_CACHE` + флаг `plane_states_ttl_s` в src/config.py; счётчики `skipped_terminal_total`/`deduped_total` в `/queue`; обновлять также при изменении src/reconciler.py F-2, src/plane_sync.py `get_project_states`/`get_project_state_groups`/`_STATES_CACHE`).*