---
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-путём.
