--- verdict: APPROVED work_item: ORCH-115 stage: review author_agent: reviewer status: approved created_at: 2026-06-16 model_used: claude-opus-4-8 type: review work_item_id: ORCH-115 version: 1 --- # Review ORCH-115 — детерминированный staging-раннер вместо LLM-деплойера ## Summary Замена LLM-агента `deployer` на стадии `deploy-staging` (self-hosting `orchestrator`) детерминированным leaf `src/staging_runner.py`, перехватываемым в `launch_job` **до** `_spawn` (прецедент `deploy-finalizer`/`post-deploy-monitor`). Реализация строго соблюдает корневой инвариант NFR-1 — это замена **продюсера** артефакта `15-staging-log.md`, а **не** гейта: `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_staging_status`/`_parse_staging_status`, machine-key `staging_status:` и схема БД — байт-в-байт не тронуты. Все FR-1…FR-7 реализованы, все AC-1…AC-12 выполнены, документация обновлена в том же PR на всех требуемых поверхностях, полный регресс зелёный (**2105 passed**), новый сьют (24 теста) зелёный, анти-дрейф LLM-карты зелёный. **Вердикт: APPROVED** — P0/P1 findings отсутствуют. ## Проверка по осям ### 1. Соответствие ТЗ (`02-trz.md`) / критериям (`03-acceptance-criteria.md`) - **FR-1 / AC-1** — перехват в `launch_job` до `_spawn` при `agent=="deployer"` + `should_intercept` (стадия `deploy-staging` + `applies`); возвращает `None`, `_spawn` не вызывается, `agent_runs` не создаётся, `jobs`-строка ведётся `mark_job` через `_run_staging_runner_job` (зеркало `_run_deploy_finalizer_job`). ✅ (TC-05) - **FR-2 / AC-9 / AC-8** — та же каноническая staging-сюита через `proc_group.run_in_process_group` (tree-kill, таймаут). Команда из config (без host-хардкодов). ✅ (TC-11/TC-12) - **FR-3 / AC-3** — exit-код → `staging_status:` через **единый** контракт `self_deploy.map_exit_code_to_status` (`0→SUCCESS`, иначе/None/нечисло→`FAILED`), второй маппинг не введён. ✅ (TC-02) - **FR-4 / AC-2** — `15-staging-log.md` с `staging_status: SUCCESS|FAILED` (UPPERCASE) + полной 52c-схемой (`work_item`/`stage: deploy-staging`/`author_agent: staging-runner`/`status`/ `created_at`/`model_used: n/a`); INFRA-WAIVED копируется в тело; best-effort commit/push в **фичеветку** (не в `main`), гейт читает worktree первым (`check_staging_status` lookup order подтверждён в `src/qg/checks.py:627`). ✅ (TC-03/TC-04) - **FR-5 / AC-4** — после вердикта вызывается существующий `advance_stage(current_stage="deploy-staging", finished_agent="deployer")` — параметры в точности соответствуют сигнатуре `stage_engine.advance_stage:191` и зеркалят `run_deploy_finalizer:2092`; SUCCESS → под-гейты + Phase A; FAILED → существующий откат `deploy-staging → development` (`stage_engine.py:932`, ветка `agent=="deployer" and qg=="check_staging_status"`). Новой ветви маршрутизации нет. ✅ (TC-07/TC-10) - **FR-6 / AC-6** — kill-switch `staging_runner_enabled` + скоуп `staging_runner_repos` (пусто → self-hosting only через `is_self_hosting_repo`); `applies` проверяется первым, при выключенном флаге `_spawn` LLM-deployer'а байт-в-байт. ✅ (TC-01/TC-08) - **FR-7 / AC-10** — read-only блок `staging_runner` в `GET /queue` + один структурный лог-вердикт на прогон, различающий `code-pass`/`code-fail`/`tool-error`. ✅ (TC-13) - **AC-7 (never-raise)** — все публичные функции изолированы; tool-error (spawn-error/таймаут/ `returncode None`) даёт bounded DEFER, затем fail-closed FAILED; никогда тихий advance/ложный green. ✅ (TC-10) - **AC-12** — `pytest tests/ -q` зелёный (2105 passed); новый `tests/test_orch115_staging_runner.py` зелёный (24 теста, покрытие leaf 83%; непокрытые строки — исключительно defensive `except`-ветви never-raise). ✅ ### 2. Соответствие ADR (`06-adr/ADR-001` + сквозной `adr-0048`) - D1–D11 реализованы как зафиксировано: точка диспетчеризации (перехват до `_spawn`, дискриминатор по **стадии задачи**, не по имени роли — защита от перехвата прод-`deploy` deployer'а, TC-06), чистота leaf'а (на импорте только `config`/`proc_group`; `db`/`git_worktree`/`stage_engine`/ `qg.checks`/`self_deploy`/`notifications` — лениво), переиспользование `proc_group` и единого маппинга, **двухуровневый исход D5 (анти-ORCH-110)** — инфра-сбой ≠ код-фейл, restart-safe маркер-счётчик в `task_content` без правки схемы БД, push только в фичеветку, инициация существующего гейта, бюджет времени без правки `reaper_max_running_s`, наблюдаемость. - **Инвариант NFR-1 / AC-5 соблюдён байт-в-байт** — анти-дрейф TC-09 подтверждает неизменность `STAGE_TRANSITIONS["deploy-staging"]`, наличие `check_staging_status` в `QG_CHECKS`, отсутствие новых таблиц/колонок, неизменность machine-key `staging_status:`. - **Граница O1 (трассировка маркеров)** — код ORCH-114 (`transition_lease`) и ORCH-112 (`checkout_hygiene`) не модифицируется; lease берётся **внутри** `advance_stage`, раннер его не трогает — зафиксированный инвариант цепочки ORCH-110/111/112/113/114 не сломан. ### 3. Качество кода - Docstrings на всех публичных функциях; следование установленным паттернам (`self_deploy`/ `proc_group`/`merge_gate`); классификация `suite_ran = returncode is not None and not timed_out` корректно трактует timeout-kill (returncode может быть `-9`, но `timed_out=True`) как tool-error, а не код-фейл. - Сверены все интеграционные сигнатуры: `ProcResult`, `run_in_process_group(grace_s/tree_kill)`, `enqueue_job(available_at_delay_s)`, `mark_job`, `get_worktree_path`, `link_for`/`send_telegram`, `is_self_hosting_repo`/`SELF_HOSTING_REPO` — все совпадают. - **Self-hosting safety (AC-8/BR-7)** — в командной строке нет рестарта 8500 / `docker compose up` / `--build` / force-push / правок `.env`; git push строго в фичеветку, никогда в `main`. ✅ (TC-12) - Багфикс-трек (BR-4) неприменим: ORCH-115 — полный цикл (метка не `Bug`, прошёл `architecture`), регресс-тест-фиксатор не требуется; покрытие обеспечено новым сьютом. ### 4. Документация (обязательная проверка) `src/` изменён → документация обновлена **в том же PR** на всех требуемых поверхностях: - `CLAUDE.md` (раздел-паспорт ORCH-115), `CHANGELOG.md` (`[Unreleased]`). - `docs/architecture/README.md` (компонент Staging-runner + adr-link), `internals.md` (детерминированные перехваты `launch_job`). - Норматив сопровождения ORCH-118: `llm-call-sites.md` (A6 — срез реализован, машинный `ORCH-118-INVENTORY-BLOCK` сохраняет deployer `avoidable=yes`/`axis=C` как fallback), `llm-determinization-roadmap.md` (rank 1 — ✅, инвариант «ровно один `first_slice`» цел), `llm-usage-policy.md` (§5 — единственный транспорт S0 не нарушен). Анти-дрейф `tests/test_llm_call_site_inventory.py` / `test_llm_determinization_docs.py` зелёные (TC-14). - **Витрина системы `docs/overview/` (ORCH-011)** — обновлены `tech-pipeline.md`, `tech-agents.md`, `tech-quality-security.md`; `tests/test_system_docs.py` зелёный. - `.openclaw/agents/deployer.md` (LLM-ветвь `deploy-staging` — fallback), `.env.example` (5 ключей), ADR work-item + сквозной `adr-0048`. - **README «Известные ограничения» (ORCH-079)** — корректно не тронут: ни один из трёх открытых пунктов (Telegram 48h / intra-repo deps / batch-autonomy) не закрывается ORCH-115; ложного «решённое за открытое» нет. ## Findings ### P0 — Blocker - Нет. ### P1 — Must fix - Нет. ### P2 — Should fix - Нет. ### P3 — Nice-to-have - [ ] `run_staging_gate` при отсутствии `work_item_id`/`branch` (повреждённая строка `tasks` — задача на `deploy-staging` без branch) делает ранний `return`; обёртка `_run_staging_runner_job` затем помечает джоб `done`, оставляя задачу «висеть» на `deploy-staging` до ре-драйва reconciler/reaper. Это крайний кейс (любая задача на `deploy-staging` имеет branch), логируется как ERROR и согласуется с never-raise-контрактом, но опционально можно сделать defensive-defer вместо `done`-без-эффекта. Не блокирует приёмку. ## Документация Обновлена полностью и в том же PR. Изменение `src/` сопровождено синхронным обновлением паспорта, чейнджлога, архитектурных доков, LLM-карты/roadmap/политики (+ зелёные анти-дрейф тесты), витрины `docs/overview/` (ORCH-011), промпта `deployer.md`, `.env.example` и обоих ADR. Обзорная витрина README не требует правок (нет закрытого ORCH-115 пункта). Документационная ось — **PASS**.