176 lines
16 KiB
Markdown
176 lines
16 KiB
Markdown
---
|
||
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-путём.
|