--- work_item: ORCH-115 stage: analysis author_agent: analyst status: ready-for-review created_at: 2026-06-16 model_used: claude-opus-4-8 --- # 01 — BRD (бизнес-требования): ORCH-115 — заменить LLM-деплойера детерминированным staging-раннером Work Item: **ORCH-115** · Repo: **orchestrator** · Стадия: analysis ## 1. Бизнес-контекст и проблема Стадия `deploy-staging` сейчас исполняется **LLM-агентом `deployer`** (`src/stages.py:18`, `get_agent_for_stage("testing") = "deployer"`). Фактическая работа агента на этой стадии — **чисто детерминированная**: запустить staging-сюиту (`docker exec orchestrator-staging python3 scripts/staging_check.py --base-url http://localhost:8501 --mode stub`), смаппить **exit-код** в вердикт (`0 → SUCCESS`, ≠0 → `FAILED`), записать `15-staging-log.md` с frontmatter `staging_status:` и смержить лог в `main` (`.openclaw/agents/deployer.md`, шаги 1–4). Это **avoidable LLM control path** по нормативной политике (`docs/architecture/llm-usage-policy.md` §3): (i) это C-консультация — её вердикт `staging_status:` потребляется гейтом `check_staging_status` (`src/qg/checks.py:599`), и (ii) вердикт **полностью деривируем** из exit-кода `staging_check.py`. Карта вызовов (`docs/architecture/llm-call-sites.md`, строка **A6**) классифицирует deployer как **`replace-deterministic-now`**, а roadmap (`docs/architecture/llm-determinization-roadmap.md`, машинный блок) ставит его **rank 1** с `first_slice = yes`, `hybrid_needed = no`. Эта задача — **первый срез** реализации того roadmap. **Боль / риск, который закрываем:** - **Недетерминизм в потоке управления.** Решение «advance или rollback» на `deploy-staging` зависит от LLM-сессии (стоимость, латентность, риск галлюцинации команд), хотя сводится к одному exit-коду. - **Стоимость и латентность.** Каждый прогон deployer'а на staging тратит токены/время opus-агента (оценка по `agent_runs`: deployer-строки ~40–120k токенов / 3–15 мин на прогон; точное число — `GET /metrics`) ради действия, которое выполняется тремя shell-строками. - **Класс инцидентов «LLM принял решение, которое есть исполнение фиксированных команд + маппинг результата»** — тот же RCA-трек, что ORCH-110/111/112/113/114/117. Установленные факты (не изобретать): - Пьюр-логика вердикта уже существует и юнит-тестируема: `src/staging_verdict.py::compute_staging_verdict` (ORCH-061) считает infra-tolerant вердикт **внутри** `staging_check.py`; раннеру остаётся доверять exit-коду (как уже делает LLM-deployer — `deployer.md` step 2). - Детерминированный прецедент замены агента уже работает: `launch_job` перехватывает зарезервированные роли `deploy-finalizer` (D1, `src/agents/launcher.py:389`) и `post-deploy-monitor` (D2, `:394`) **до `_spawn`** и исполняет их как no-LLM-джобы. - Прод-ребро `deploy` для self-hosting уже детерминировано (`src/self_deploy.py` Phase A/B/C, ORCH-036) — LLM в критическом self-restart-пути нет. Срез не трогает критический прод-путь. ## 2. Объём (scope) ### В объёме (Phase 1) - **Детерминированный staging-раннер** для `deploy-staging` репо `orchestrator` (self-hosting): исполняет staging-сюиту, маппит exit-код в `staging_status:`, пишет `15-staging-log.md`, мержит в `main` — **без** запуска LLM-агента `deployer`. - Раннер активируется через **перехват в `launch_job` до `_spawn`** (прецедент D1/D2), **без правки `src/stages.py`/`STAGE_TRANSITIONS`** (роль `deployer` в словаре остаётся; меняется лишь *кто* обрабатывает джоб на стадии `deploy-staging` для in-scope репо). - После выпуска вердикта раннер инициирует **существующую** оценку exit-гейта `check_staging_status` ровно так, как это делал завершившийся LLM-deployer (`_try_advance_stage` → `advance_stage( finished_agent="deployer")`) — все нижестоящие под-гейты (security → merge → coverage → image-freshness, ORCH-022/043/027/058) и Phase A (ORCH-036) ведут себя идентично. - Kill-switch + скоуп-CSV (паттерн ORCH-022/027/043/089/090): `*_enabled` (откат к LLM-пути) и `*_repos` (пусто → self-hosting only). - Наблюдаемость: read-only блок в `GET /queue` + структурный лог вердикта. ### Вне объёма (явно НЕ делаем в ORCH-115) - **Phase 2 — «project deploy contract» для не-self репо** (например `enduro-trails`): конфигурируемый контракт deploy/rollback/healthcheck для произвольных репо. Описан как **forward-looking follow-up** (см. §6 и `02-trz.md` §8); **в приёмку ORCH-115 не входит**. Для не-self репо `deploy-staging` сейчас — мгновенный pass (`check_staging_status` → N/A, `src/qg/checks.py:620`), поэтому Phase 1 их не затрагивает. - **Прод-ребро `deploy`** (Phase A/B/C self-deploy, ORCH-036) — уже детерминировано; не трогаем. - **LLM debug/triage-аналитик после детерминированного FAILED** — `replace-deterministic-now` без гибрида (roadmap `hybrid_needed = no`). В этом срезе LLM на `deploy-staging` отсутствует и в happy-path, и в fail-path; опциональный off-control-path debug-аналитик оставлен как будущее улучшение и **требованиями не запрещён** (см. NFR-7). - **Любая правка `STAGE_TRANSITIONS` / реестра и имён `QG_CHECKS` / семантики `check_*` / machine-verdict-ключей / схемы БД** (см. NFR-1). - **ORCH-112 (checkout hygiene) и ORCH-114 (transition lease)** — по явной границе задачи не смешиваем: раннер вызывает `advance_stage`, который уже владеет lease ORCH-114; сам lease/гигиену не модифицируем. ## 3. Заинтересованные стороны - **Заказчик / Owner** (`homenet542@gmail.com`) — инициатор детерминизации LLM-control-path'ов. - **Платформа orchestrator (self-hosting)** — прямой потребитель: дешевле/быстрее/детерминированнее собственный `deploy-staging`. - **Другие проекты на общем инстансе** (enduro-trails) — НЕ затронуты в Phase 1 (скоуп self-hosting), выигрывают позже от Phase 2. - **Reviewer / Tester / Deployer-роли конвейера** — принимают результат через неизменные гейты. ## 4. Бизнес-требования (BR) - **BR-1 — Детерминированный staging без LLM.** На `deploy-staging` для in-scope репо вердикт `staging_status:` производится детерминированным кодом (исполнение `staging_check.py` + маппинг exit-кода), **без** консультации LLM. Happy-path `deploy-staging` не вызывает `_spawn`. - **BR-2 — Контракт артефакта неизменен.** Раннер пишет тот же `15-staging-log.md` с тем же frontmatter-ключом `staging_status: SUCCESS|FAILED`, который читает `check_staging_status`/ `_parse_staging_status`. Гейт байт-в-байт не меняется. - **BR-3 — Эквивалентность маршрутизации.** SUCCESS → продвижение на `deploy` через те же под-гейты и Phase A; FAILED → существующий откат `deploy-staging → development` (тот же путь, что у FAILED-вердикта LLM-deployer'а, `src/stage_engine.py:932`). Никаких новых рёбер/исходов. - **BR-4 — Переиспользование существующей пьюр-логики.** Раннер использует уже существующий exit-code→verdict маппинг (тривиальный `0→SUCCESS`/иначе`FAILED`, зеркало `self_deploy.map_exit_code_to_status`); infra-tolerance (ORCH-061) остаётся **внутри** `staging_check.py` — раннер ему доверяет, повторно не судит. - **BR-5 — Обратимость одним флагом.** Глобальный kill-switch возвращает прежний LLM-deployer-путь на `deploy-staging` байт-в-байт; скоуп-CSV ограничивает раннер in-scope репо (пусто → только `orchestrator`). - **BR-6 — Наблюдаемость.** Исход раннера (запущен / SUCCESS / FAILED / ошибка инструмента) виден в `GET /queue` и в структурном логе; деградации (например staging-инстанс недоступен) различимы от «код упал». - **BR-7 — Self-hosting safety.** Раннер на `deploy-staging` **никогда** не рестартит прод-контейнер 8500, не трогает `main` force-push'ем, не правит `.env`/`docker-compose.yml`. Он лишь читает, исполняет staging-сюиту (порт 8501), пишет лог и мержит лог штатным PR/artifact-merge-путём. ## 5. Нефункциональные требования (NFR) - **NFR-1 — Скоуп-инвариант (анти-дрейф).** `STAGE_TRANSITIONS` (`src/stages.py`), реестр и имена `QG_CHECKS`/`check_*`/`_parse_*` (`src/qg/checks.py`), machine-verdict-ключи (`staging_status:`/`deploy_status:`/`verdict:`/`result:`/`security_status:`/`coverage_status:`), схема БД — **байт-в-байт не тронуты**. Это замена *продюсера* артефакта, не гейта. - **NFR-2 — never-raise / fail-safe.** Любая ошибка раннера (docker недоступен, таймаут, I/O) → безопасный детерминированный исход без падения воркера: либо `FAILED` (fail-closed, никогда ложный green), либо штатный requeue/defer — не «тихий advance». Сбой раннера не клинит очередь всех проектов. - **NFR-3 — Изоляция процесса / таймаут.** Спавненный subprocess (`docker exec …`) имеет ограниченный таймаут и чистое завершение дерева процессов (согласовано с прецедентом ORCH-110 `proc_group`/tree-kill); сирот pytest/docker не оставляет. - **NFR-4 — Сквозные бюджеты времени.** Таймаут раннера согласован со сквозным инвариантом ORCH-065/109/110 (`reaper_max_running_s` > Σ(работ на ребре deploy-staging) + grace) — без правки `reaper_max_running_s`. - **NFR-5 — Совместимость с не-self репо.** Для репо вне скоупа `deploy-staging` ведёт себя 1:1 как до ORCH-115 (LLM-deployer либо мгновенный N/A-pass). enduro-trails не затронут. - **NFR-6 — Соответствие политике LLM.** Изменение снимает LLM-консультацию A6; карта `docs/architecture/llm-call-sites.md` и политика/roadmap обновляются **в том же PR** (норматив сопровождения ORCH-118): строка deployer переходит из «consults_llm: yes» в реализованное детерминированное состояние. - **NFR-7 — Не запрещать будущий debug-fallback.** Архитектура раннера не должна архитектурно исключать опциональный off-control-path LLM debug-аналитик после FAILED (будущее улучшение); но в ORCH-115 он не реализуется. ## 6. Допущения и ограничения - **Допущение А1.** staging-инстанс `orchestrator-staging` (8501) поднят и доступен на хосте; его недоступность раннер трактует детерминированно (fail-closed `FAILED` или defer — решает архитектор, AC-7). - **Допущение А2.** `scripts/staging_check.py` остаётся источником истины набора проверок и exit-кода (включая infra-tolerance ORCH-061). ORCH-115 его логику не меняет. - **Допущение А3.** Перехват «до `_spawn`» по имени джоб-роли + стадии задачи — достаточный механизм диспетчеризации (как D1/D2); конкретный механизм финализирует архитектор (06-adr). - **Ограничение О1.** Граница задачи: не смешивать с ORCH-112/ORCH-114 (их код не модифицируется). - **Ограничение О2.** Phase 2 (project deploy contract) — отдельный follow-up; ORCH-115 закрывает только Phase 1. ## 7. Критерии успеха `deploy-staging` для `orchestrator` проходит без запуска LLM-агента `deployer`: детерминированный раннер исполняет staging-сюиту, пишет корректный `15-staging-log.md` (`staging_status:`), мержит его в `main`, и конвейер продвигается/откатывается ровно как раньше — при неизменных `STAGE_TRANSITIONS`/`QG_CHECKS`/гейтах/схеме БД, под kill-switch с откатом к прежнему поведению. Детальные PASS/FAIL — `03-acceptance-criteria.md`. ## 8. Риски Краткий перечень (детали — `10-tech-risks.md`, заполняет архитектор): - **R-1** — точка диспетчеризации «до `_spawn`» должна корректно отличать staging-deployer от прод-deployer (по стадии задачи), иначе можно перехватить не тот джоб. - **R-2** — после выпуска вердикта нужно надёжно инициировать `advance_stage`, иначе задача зависнет на `deploy-staging` (нет «финиша агента», который раньше триггерил гейт). - **R-3** — таймаут/изоляция docker-subprocess; утечка процессов (ср. инцидент ORCH-110). - **R-4** — взаимодействие с transition-lease (ORCH-114) и serial-gate (ORCH-088) на side-effectful ребре — не сломать владение. - **R-5** — корректность отката FAILED (developer-retry cap) — должна совпасть с LLM-путём.