diff --git a/docs/work-items/ORCH-115/01-brd.md b/docs/work-items/ORCH-115/01-brd.md new file mode 100644 index 0000000..8fe81fd --- /dev/null +++ b/docs/work-items/ORCH-115/01-brd.md @@ -0,0 +1,175 @@ +--- +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-путём. diff --git a/docs/work-items/ORCH-115/02-trz.md b/docs/work-items/ORCH-115/02-trz.md new file mode 100644 index 0000000..f64cee0 --- /dev/null +++ b/docs/work-items/ORCH-115/02-trz.md @@ -0,0 +1,158 @@ +--- +work_item: ORCH-115 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-16 +model_used: claude-opus-4-8 +--- + +# 02 — ТЗ (TRZ): ORCH-115 — детерминированный staging-раннер вместо LLM-деплойера + +Work Item: **ORCH-115** · Repo: **orchestrator** · Стадия: analysis + +> ТЗ описывает **конкретные изменения к реализации**, выведенные из BRD и фактического кода. +> Архитектурное обоснование (точный механизм перехвата, размещение раннера, способ инициации +> `advance_stage`, лестница таймаутов) — задача архитектора (`06-adr/`). Здесь — требования и +> привязка к реальным модулям `src/`. + +## 1. Сводка изменения + +Заменить **LLM-агента `deployer`** на стадии `deploy-staging` (для self-hosting `orchestrator`) +**детерминированным staging-раннером**, перехватываемым в `launch_job` **до `_spawn`** (прецедент +`deploy-finalizer`/`post-deploy-monitor`, `src/agents/launcher.py:389/394`). Раннер исполняет ту же +staging-сюиту, что исполнял LLM (`docker exec orchestrator-staging python3 +scripts/staging_check.py …`), маппит exit-код в `staging_status:` (`0→SUCCESS`, иначе `FAILED`), +пишет `15-staging-log.md`, best-effort мержит лог в `main`, затем инициирует **существующую** оценку +exit-гейта `check_staging_status` ровно как завершившийся LLM-deployer. Контракт артефакта, гейт, +`STAGE_TRANSITIONS`, схема БД — **неизменны**. Под kill-switch + скоуп-CSV; never-raise; fail-closed. + +## 2. Задействованные модули / пути + +| Путь | Действие | Назначение | +|------|----------|------------| +| `src/staging_runner.py` *(новый leaf)* | создать | Детерминированный раннер: `applies(repo)` (kill-switch + скоуп), исполнение staging-сюиты, маппинг exit-кода, запись `15-staging-log.md`, best-effort merge, снапшот для `/queue`. Leaf-чистота по образцу `self_deploy.py`/`staging_verdict.py`: импортирует только `config`/`git_worktree` (+ лениво `qg.checks.is_self_hosting_repo`), never-raise. | +| `src/agents/launcher.py` | изменить | В `launch_job` добавить перехват **до `_spawn`** (рядом с D1/D2): если джоб — `deployer` на стадии задачи `deploy-staging` и `staging_runner.applies(repo)` → исполнить раннер синхронно в воркер-треде, инициировать `advance_stage` и пометить джоб (как `_run_deploy_finalizer_job`); вернуть `None` (нет `agent_runs`-строки). | +| `src/config.py` | изменить | Добавить ключи `staging_runner_enabled: bool = True` (env `ORCH_STAGING_RUNNER_ENABLED`) и `staging_runner_repos: str = ""` (env `ORCH_STAGING_RUNNER_REPOS`; пусто → self-hosting only) + опц. `staging_runner_timeout_s` (см. FR-5). Дефолты = боевое; паттерн `coverage_gate_enabled`/`coverage_gate_repos`/`self_deploy_*`. | +| `src/stage_engine.py` | (потенциально) точечно | Если архитектор решит инициировать гейт из stage_engine, а не из launcher — добавить тонкий хелпер (вызов существующего `advance_stage(finished_agent="deployer")`). **Без** правки `STAGE_TRANSITIONS`/exit-гейтов. | +| `src/main.py` (`GET /queue`) | изменить | Read-only блок `staging_runner` (флаг/скоуп/счётчики исходов) — наблюдаемость BR-6. | +| `.openclaw/agents/deployer.md` | изменить (docs) | Отметить, что на `deploy-staging` для in-scope репо стадию ведёт детерминированный код (зеркало формулировки prod-Phase A/B/C); LLM-ветвь `deploy-staging` остаётся как fallback под выключенным флагом / для не-self репо. | +| `docs/architecture/llm-call-sites.md`, `llm-determinization-roadmap.md`, `llm-usage-policy.md` | изменить (docs) | Норматив сопровождения ORCH-118 (NFR-6): отразить реализацию A6 (deployer staging-status) — обновить инвентарь/политику/roadmap в том же PR; синхронно поправить `tests/test_llm_call_site_inventory.py` / `tests/test_llm_determinization_docs.py`. | +| `CLAUDE.md`, `CHANGELOG.md`, `docs/overview/` | изменить (docs) | Паспорт/чейнджлог/витрина — правило для агентов №2. | +| `tests/test_orch115_staging_runner.py` *(новый)* | создать | Покрытие (см. `04-test-plan.yaml`). | + +> **Не трогать (NFR-1):** `src/stages.py::STAGE_TRANSITIONS`; имена/семантику `QG_CHECKS`/`check_*`/ +> `_parse_*` в `src/qg/checks.py`; `src/staging_verdict.py` (переиспользуем как есть); `src/self_deploy.py` +> прод-путь; `src/transition_lease.py` (ORCH-114); `src/checkout_hygiene.py` (ORCH-112); схему БД. + +## 3. Функциональные требования + +### FR-1 — Детерминированный перехват на `deploy-staging` (без `_spawn`) +В `launch_job` (`src/agents/launcher.py`) **до** вызова `_spawn`, по образцу D1/D2: если +`job.agent == "deployer"` **и** стадия задачи (`tasks.stage` по `job.task_id`) == `deploy-staging` +**и** `staging_runner.applies(job.repo)` истинно → не вызывать `_spawn`, а исполнить раннер +синхронно. Контракт: возвращает `None` (нет `agent_runs`), сам ведёт `jobs`-строку +(`mark_job(done|failed|queued)`) как `_run_deploy_finalizer_job`. +- Дискриминатор «staging vs prod» — **стадия задачи**, не имя роли (роль `deployer` общая для + `deploy-staging` и `deploy`). Для self-hosting прод-ребро не запускает `deployer` (Phase A), поэтому + коллизии нет; гард по стадии — защита от перехвата не того джоба (R-1). +- `applies(repo)`: `staging_runner_enabled=False` → `False` (откат к LLM-пути); непустой + `staging_runner_repos` → membership; пустой CSV → `is_self_hosting_repo(repo)`. Никакой сети, + проверяется **первым** (нулевой оверхед при выключенном флаге). Never-raise → `False` при ошибке + (fail-safe к прежнему LLM-пути). + +### FR-2 — Исполнение staging-сюиты +Раннер исполняет ту же канонную команду, что исполнял LLM-deployer +(`.openclaw/agents/deployer.md` step 1): +`docker exec orchestrator-staging python3 /repos/orchestrator/scripts/staging_check.py +--base-url http://localhost:8501 --mode stub` (точные аргументы/таргет — из config, не хардкодить +host-специфику; ORCH-101). Захватывает exit-код (и stdout для observability/тела лога). infra-tolerance +(ORCH-061) уже **внутри** `staging_check.py` → раннер вердикт повторно не судит (BR-4). + +### FR-3 — Маппинг exit-кода → `staging_status:` +`0 → "SUCCESS"`, любой ненулевой / отсутствие кода / ошибка запуска → `"FAILED"` (fail-closed, +никогда ложный green). Зеркало уже существующего `self_deploy.map_exit_code_to_status` (pure, +unit-tested) — переиспользовать общий контракт, не плодить второй маппинг. + +### FR-4 — Запись и merge `15-staging-log.md` +Раннер пишет `docs/work-items//15-staging-log.md` в worktree фичеветки с frontmatter: +`staging_status: SUCCESS|FAILED` + обязательная 52c-схема (`work_item`/`stage=deploy-staging`/ +`author_agent`/`status`/`created_at`/`model_used`) — зеркало `self_deploy.build_deploy_log` для +`14-deploy-log.md`. `author_agent`/`model_used` отражают **детерминированный** продюсер (например +`author_agent: staging-runner`, `model_used: n/a` или платформенный литерал — финализирует архитектор; +ключи и имя `staging_status:` не меняются). При INFRA-WAIVED-строке от `staging_check.py` — скопировать +её в тело (observability, как требовал prompt). Best-effort `git add/commit/push` лога в `main` +(зеркало `self_deploy.write_deploy_log`, тот же git-identity-паттерн ORCH-101); гейт всё равно +читает worktree → origin/main fallback (`check_staging_status` lookup order, `src/qg/checks.py:627-638`). + +### FR-5 — Инициация существующего гейта после вердикта +После записи (и best-effort merge) раннер инициирует ту же оценку exit-гейта, что триггерил +завершившийся LLM-deployer: `advance_stage(task_id, current_stage="deploy-staging", repo, +work_item_id, branch, finished_agent="deployer")` (через `_try_advance_stage`-эквивалент). Это +запускает `check_staging_status` и — на SUCCESS — под-гейты security→merge→coverage→image-freshness +(ORCH-022/043/027/058) и Phase A (ORCH-036); на FAILED — существующий rollback +(`src/stage_engine.py:932`). **Никакой новой ветви маршрутизации.** Lease ORCH-114 берётся внутри +`advance_stage` как сейчас — раннер его не трогает (граница задачи). +- Таймаут раннер-subprocess — выделенный ключ `staging_runner_timeout_s` с дефолтом, согласованным + со сквозным бюджетом ORCH-065/109/110 (NFR-4); малформ/непозитив → дефолт + WARNING (never-break). + +### FR-6 — Kill-switch и скоуп (обратимость) +`staging_runner_enabled=False` → перехват не срабатывает → на `deploy-staging` запускается прежний +LLM-deployer (`_spawn`) **байт-в-байт** как до ORCH-115. `staging_runner_repos` ограничивает скоуп +(пусто → только `orchestrator`); не-self репо никогда не перехватываются (для них staging-гейт и так +N/A, `src/qg/checks.py:620`). + +### FR-7 — Наблюдаемость +- Read-only блок `staging_runner` в `GET /queue`: `enabled`, `repos`, счётчики `success`/`failed`/ + `tool_error`/`runs`. +- Один структурный лог-вердикт на прогон (`work_item`/`repo`/`exit_code`/`status`/`duration_s`), + различающий «код упал» (`FAILED` от staging-сюиты) и «инструмент недоступен» (tool-error). + +## 4. Изменения API + +- **`GET /queue`** — добавить read-only ключ `staging_runner` (наблюдаемость). Существующие поля + ответа не меняются. +- Опционально (на усмотрение архитектора, по образцу `POST /coverage/baseline`): нет обязательного + нового мутирующего эндпоинта. Откат — через env-флаг. +- Новых вебхуков нет. + +## 5. Изменения схемы БД + +**Нет.** Раннер использует существующие таблицы (`tasks` для стадии, `jobs` для статуса джоба) и +sentinel/worktree-механику. Никаких новых таблиц/колонок/миграций (NFR-1). Счётчики `/queue` — +in-process (паттерн `_MERGE_GATE_COUNTERS`, ORCH-110), не БД. + +## 6. Требования к новым/изменённым QG checks + +**Нет новых QG и нет изменений существующих.** `check_staging_status` / `_parse_staging_status` / +ключ `staging_status:` (`src/qg/checks.py:538/599`) и состав `QG_CHECKS` — **байт-в-байт неизменны**. +ORCH-115 меняет только *продюсера* `15-staging-log.md` (детерминированный код вместо LLM); гейт, +читающий артефакт, остаётся прежним. Это критический инвариант (NFR-1) — reviewer ловит любое +изменение имени/семантики гейта как finding ≥P1. + +## 7. Совместимость / регресс + +- **Обратная совместимость:** `staging_runner_enabled=False` → прежний LLM-deployer-путь + байт-в-байт; не-self репо → 1:1 (N/A-pass либо LLM, в зависимости от скоупа). enduro-trails не + затронут (NFR-5). +- **Kill-switch / область раската:** один флаг `staging_runner_enabled` + CSV `staging_runner_repos` + (пусто → self-hosting only). Откат = `ORCH_STAGING_RUNNER_ENABLED=false`. +- **Обратимость:** полностью обратимо флагом; артефакт и гейт неизменны, так что переключение туда-сюда + не оставляет несовместимого состояния. +- **never-raise / fail-safe (NFR-2):** ошибка раннера → `FAILED` (fail-closed) или штатный requeue, + не «тихий advance»; сбой не клинит очередь. Self-hosting safety (BR-7): никаких рестартов 8500 / + force-push в `main` / правок инфры. +- **Граница (О1):** код ORCH-112 (checkout hygiene) и ORCH-114 (transition lease) не модифицируется. +- **Норматив сопровождения (NFR-6):** в том же PR обновить `docs/architecture/llm-call-sites.md` / + `llm-determinization-roadmap.md` / `llm-usage-policy.md` + соответствующие анти-дрейф тесты; + `CLAUDE.md` / `CHANGELOG.md` / `docs/overview/`. + +## 8. Phase 2 (forward-looking, вне приёмки ORCH-115) + +Зафиксировано для преемственности — **не реализуется в этой задаче**, заводится отдельным follow-up: +- **Project deploy contract** для не-self репо (enduro-trails): декларативный per-repo контракт + `deploy` / `rollback` / `healthcheck` (команды + ожидаемые коды/эндпоинты), исполняемый тем же + детерминированным раннер-паттерном (run → map exit code → verdict → artifact → healthcheck). +- LLM остаётся допустим только как **off-control-path** debug/triage-аналитик после + детерминированного провала (NFR-7) — не как продюсер вердикта. +- Зависимость: устойчивый Phase 1 (этот work item) как доказанный паттерн перехвата + маппинга. diff --git a/docs/work-items/ORCH-115/03-acceptance-criteria.md b/docs/work-items/ORCH-115/03-acceptance-criteria.md new file mode 100644 index 0000000..ebc49dc --- /dev/null +++ b/docs/work-items/ORCH-115/03-acceptance-criteria.md @@ -0,0 +1,166 @@ +--- +work_item: ORCH-115 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-16 +model_used: claude-opus-4-8 +--- + +# 03 — Критерии приёмки (Acceptance Criteria): ORCH-115 — детерминированный staging-раннер + +Work Item: **ORCH-115** · Repo: **orchestrator** · Стадия: analysis + +Формат: каждый критерий имеет **PASS** (что должно быть истинно для приёмки) и **FAIL** +(что считается провалом). Любой машинный/ручной reviewer проверяет их буквально по файлам +репозитория. + +--- + +## AC-1 — Детерминированный перехват на `deploy-staging` (нет `_spawn`/LLM) + +**Условие:** При включённом флаге и in-scope репо джоб `deployer` на стадии `deploy-staging` +обрабатывается раннером, а не LLM-агентом. +- **PASS:** `launch_job` (`src/agents/launcher.py`) перехватывает джоб **до** `_spawn` (рядом с + D1/D2) при `agent=="deployer"` + стадия задачи `deploy-staging` + `staging_runner.applies(repo)`; + `_spawn` не вызывается; не создаётся строка `agent_runs`; джоб ведётся `mark_job(...)` самим + раннером. Тест воспроизводит это без живого Claude CLI. +- **FAIL:** На `deploy-staging` для in-scope репо при включённом флаге всё ещё вызывается `_spawn` / + создаётся `agent_runs`-строка LLM-deployer'а. + +--- + +## AC-2 — Контракт артефакта `15-staging-log.md` неизменен + +**Условие:** Раннер пишет тот же артефакт с тем же machine-key, что читает гейт. +- **PASS:** Создаётся `docs/work-items//15-staging-log.md` с frontmatter + `staging_status: SUCCESS|FAILED` (UPPERCASE) + обязательная 52c-схема + (`work_item`/`stage: deploy-staging`/`author_agent`/`status`/`created_at`/`model_used`). + `_parse_staging_status` читает его и возвращает корректный вердикт **без изменения** парсера. +- **FAIL:** Изменено имя/регистр ключа `staging_status:`, отсутствует frontmatter, либо вердикт + записан только прозой; либо парсер `_parse_staging_status` пришлось менять. + +--- + +## AC-3 — Корректный exit-code → verdict маппинг + +**Условие:** Exit-код staging-сюиты детерминированно маппится в вердикт. +- **PASS:** `0 → SUCCESS`; любой ненулевой / None / ошибка запуска → `FAILED` (fail-closed). + Маппинг — pure-функция, переиспользующая контракт `self_deploy.map_exit_code_to_status` (или + эквивалентный единый), покрыта unit-тестом на каждый класс входа. infra-tolerance (ORCH-061) не + пересуживается раннером. +- **FAIL:** Ненулевой код даёт `SUCCESS`; ошибка/None даёт `SUCCESS` (ложный green); раннер вводит + второй несогласованный маппинг. + +--- + +## AC-4 — Эквивалентность маршрутизации (SUCCESS / FAILED) + +**Условие:** После вердикта конвейер ведёт себя ровно как при завершившемся LLM-deployer'е. +- **PASS:** SUCCESS → раннер инициирует `advance_stage(finished_agent="deployer")`, далее + отрабатывают под-гейты security→merge→coverage→image-freshness (ORCH-022/043/027/058) и Phase A + (ORCH-036) — теми же путями. FAILED → существующий откат `deploy-staging → development` с + инкрементом developer-retry (`src/stage_engine.py:932`), тот же исход, что у FAILED-вердикта LLM. +- **FAIL:** Задача зависает на `deploy-staging` (гейт не инициирован); или FAILED не откатывает / + откатывает иначе; или появляется новое ребро/исход. + +--- + +## AC-5 — Инвариант скоупа: гейты/стадии/схема БД не тронуты (анти-дрейф) + +**Условие:** Изменена только сторона *продюсера*, не контракт конвейера. +- **PASS:** `git diff` не затрагивает `src/stages.py::STAGE_TRANSITIONS`; имена/семантику + `QG_CHECKS`/`check_*`/`_parse_*` в `src/qg/checks.py`; machine-verdict-ключи + (`staging_status:`/`deploy_status:`/`verdict:`/`result:`/`security_status:`/`coverage_status:`); + схему БД (нет новых таблиц/колонок/миграций). Анти-дрейф-тест это подтверждает. +- **FAIL:** Любой из перечисленных артефактов изменён по имени/семантике/структуре. + +--- + +## AC-6 — Kill-switch и скоуп (обратимость) + +**Условие:** Флаг возвращает прежнее поведение; скоуп ограничивает раннер. +- **PASS:** `staging_runner_enabled=False` → на `deploy-staging` запускается прежний LLM-deployer + через `_spawn` (байт-в-байт до ORCH-115). Пустой `staging_runner_repos` → раннер активен только для + `orchestrator`; не-self репо никогда не перехватываются. Покрыто тестом для обоих значений флага. +- **FAIL:** При выключенном флаге раннер всё равно перехватывает; либо не-self репо перехватывается. + +--- + +## AC-7 — never-raise / fail-safe (инструмент недоступен) + +**Условие:** Любая ошибка раннера приводит к безопасному детерминированному исходу. +- **PASS:** Недоступность docker/`orchestrator-staging`, таймаут, I/O-ошибка → раннер не роняет + воркер; исход — `FAILED` (fail-closed) **или** штатный requeue/defer, **никогда** тихий advance/ + ложный green. Все публичные функции `staging_runner.py` — never-raise; `applies()` при ошибке → `False`. +- **FAIL:** Ошибка раннера роняет воркер/клинит очередь; либо ошибка/таймаут даёт `SUCCESS`. + +--- + +## AC-8 — Self-hosting safety + +**Условие:** Раннер на `deploy-staging` не выполняет опасных для прода действий. +- **PASS:** Раннер не рестартит контейнер 8500, не выполняет `docker compose up -d orchestrator`/ + `--build`, не пушит force в `main`, не правит `.env`/`.env.staging`/`docker-compose.yml`. Merge + лога идёт штатным PR/artifact-merge-путём (как `self_deploy.write_deploy_log`). Подтверждается + ревью кода раннера + (где применимо) тестом, что в командах раннера нет запрещённых литералов. +- **FAIL:** Раннер содержит любой путь, рестартящий 8500 / force-push в `main` / правящий инфру. + +--- + +## AC-9 — Изоляция процесса и таймаут + +**Условие:** docker-subprocess ограничен по времени и не оставляет сирот. +- **PASS:** Раннер запускает staging-сюиту с ограниченным таймаутом + (`staging_runner_timeout_s`, согласован со сквозным бюджетом ORCH-065/109/110, не правя + `reaper_max_running_s`); малформ/непозитив таймаут → дефолт + WARNING; завершение чистое (без + осиротевших docker/pytest-процессов, согласовано с `proc_group`/tree-kill ORCH-110). +- **FAIL:** Нет таймаута / зависший subprocess клинит воркер; остаются сироты процессов. + +--- + +## AC-10 — Наблюдаемость + +**Условие:** Исход раннера виден и различим. +- **PASS:** `GET /queue` содержит read-only блок `staging_runner` (`enabled`/`repos`/счётчики + `success`/`failed`/`tool_error`/`runs`); на каждый прогон — один структурный лог-вердикт + (`work_item`/`repo`/`exit_code`/`status`/`duration_s`), различающий код-фейл и tool-error. +- **FAIL:** Нет блока в `/queue`; исход раннера не логируется/не различим. + +--- + +## AC-11 — Норматив сопровождения LLM-карты/политики/витрины + +**Условие:** Документация обновлена в том же PR (правило агентов №2 + норматив ORCH-118). +- **PASS:** `docs/architecture/llm-call-sites.md` (строка A6) / `llm-determinization-roadmap.md` / + `llm-usage-policy.md` отражают реализацию детерминированного deployer-staging; соответствующие + анти-дрейф-тесты (`tests/test_llm_call_site_inventory.py`, `tests/test_llm_determinization_docs.py`) + зелёные; `.openclaw/agents/deployer.md`, `CLAUDE.md`, `CHANGELOG.md`, `docs/overview/` обновлены. +- **FAIL:** Карта/политика/roadmap/витрина не обновлены; анти-дрейф-тесты красные (reviewer: ≥P1). + +--- + +## AC-12 — Полный регресс зелёный + +**Условие:** Существующий конвейер не сломан. +- **PASS:** `pytest tests/ -q` зелёный; новый `tests/test_orch115_staging_runner.py` зелёный; + staging-smoke (`scripts/staging_check.py`) на 8501 проходит штатно. +- **FAIL:** Любой ранее зелёный тест становится красным; новые тесты падают. + +--- + +## Сводная матрица AC ↔ FR/BR +| AC | Покрывает | +|----|-----------| +| AC-1 | BR-1 / FR-1 | +| AC-2 | BR-2 / FR-4 | +| AC-3 | BR-4 / FR-2 / FR-3 | +| AC-4 | BR-3 / FR-5 | +| AC-5 | NFR-1 / FR-6 | +| AC-6 | BR-5 / FR-6 | +| AC-7 | NFR-2 / FR-1 | +| AC-8 | BR-7 | +| AC-9 | NFR-3 / NFR-4 / FR-5 | +| AC-10 | BR-6 / FR-7 | +| AC-11 | NFR-6 | +| AC-12 | NFR-5 / NFR-1 | diff --git a/docs/work-items/ORCH-115/04-test-plan.yaml b/docs/work-items/ORCH-115/04-test-plan.yaml new file mode 100644 index 0000000..5ff798e --- /dev/null +++ b/docs/work-items/ORCH-115/04-test-plan.yaml @@ -0,0 +1,104 @@ +work_item: ORCH-115 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-16 +model_used: claude-opus-4-8 +title: "Детерминированный staging-раннер вместо LLM-деплойера (deploy-staging)" +framework: pytest +scope: > + Покрывает Phase 1: перехват deployer-джоба на deploy-staging до _spawn, маппинг + exit-кода в staging_status:, запись/merge 15-staging-log.md, инициацию существующего + гейта check_staging_status, kill-switch/скоуп, never-raise/fail-safe, изоляцию + процесса/таймаут, наблюдаемость, и анти-дрейф инвариант (STAGE_TRANSITIONS/QG_CHECKS/ + схема БД не тронуты). Вне покрытия: Phase 2 (project deploy contract для не-self репо), + прод-ребро deploy (ORCH-036), живой Claude CLI и живой staging-стенд (мокируются). +notes: > + Тесты не требуют живого Claude CLI, docker или сети: subprocess/docker-exec и + advance_stage мокируются; пьюр-маппинг тестируется напрямую. Полный регресс tests/ + должен оставаться зелёным. Анти-дрейф (TC-09) защищает критический инвариант NFR-1. + +tests: + - id: TC-01 + type: unit + description: "applies(repo): enabled=False -> False (откат к LLM); пустой CSV -> True только для orchestrator; непустой CSV -> membership; not-self репо -> False; ошибка -> False (never-raise, fail-safe)." + module: tests/test_orch115_staging_runner.py + expected: PASS + + - id: TC-02 + type: unit + description: "Маппинг exit-кода: 0 -> SUCCESS; 1/2/любой ненулевой -> FAILED; None/нечисло/ошибка запуска -> FAILED (fail-closed). Согласован с self_deploy.map_exit_code_to_status." + module: tests/test_orch115_staging_runner.py + expected: PASS + + - id: TC-03 + type: unit + description: "Рендер 15-staging-log.md: frontmatter содержит staging_status: SUCCESS|FAILED (UPPERCASE) + 52c-схему (work_item/stage=deploy-staging/author_agent/status/created_at/model_used); INFRA-WAIVED строка из stdout копируется в тело." + module: tests/test_orch115_staging_runner.py + expected: PASS + + - id: TC-04 + type: integration + description: "Сгенерированный раннером 15-staging-log.md читается НЕИЗМЕНЁННЫМ _parse_staging_status -> корректный (bool, reason) для SUCCESS и FAILED (контракт артефакта/гейта неизменен)." + module: tests/test_orch115_staging_runner.py + expected: PASS + + - id: TC-05 + type: integration + description: "launch_job перехватывает deployer-джоб на стадии deploy-staging для in-scope репо ДО _spawn (как D1/D2): _spawn НЕ вызывается, agent_runs не создаётся, возвращается None, jobs-строка ведётся mark_job. _spawn мокирован." + module: tests/test_orch115_staging_runner.py + expected: PASS + + - id: TC-06 + type: integration + description: "Дискриминатор стадии: deployer-джоб на стадии deploy (не deploy-staging) НЕ перехватывается раннером (для self-hosting прод-ребро идёт через Phase A; для не-self остаётся прежний путь)." + module: tests/test_orch115_staging_runner.py + expected: PASS + + - id: TC-07 + type: integration + description: "После SUCCESS-вердикта раннер инициирует advance_stage(finished_agent='deployer') ровно как завершившийся LLM-deployer (advance_stage мокирован/наблюдается); после FAILED — тот же путь, что у FAILED LLM (откат deploy-staging -> development)." + module: tests/test_orch115_staging_runner.py + expected: PASS + + - id: TC-08 + type: integration + description: "Kill-switch: staging_runner_enabled=False -> на deploy-staging для orchestrator вызывается _spawn (прежний LLM-путь байт-в-байт), раннер не активируется." + module: tests/test_orch115_staging_runner.py + expected: PASS + + - id: TC-09 + type: unit + description: "Анти-дрейф NFR-1: STAGE_TRANSITIONS (src/stages.py) и реестр/имена QG_CHECKS + ключ staging_status: неизменны; в схеме БД нет новой таблицы/колонки от ORCH-115. Структурная проверка." + module: tests/test_orch115_staging_runner.py + expected: PASS + + - id: TC-10 + type: integration + description: "never-raise/fail-safe: docker exec бросает/таймаутит/возвращает ненулевой -> раннер не падает, исход FAILED (fail-closed) или штатный requeue, никогда тихий advance/ложный green; воркер/очередь не клинятся." + module: tests/test_orch115_staging_runner.py + expected: PASS + + - id: TC-11 + type: unit + description: "Таймаут: staging_runner_timeout_s применяется к subprocess; малформ/непозитив -> дефолт + WARNING (never-break); завершение чистое (tree-kill согласован с proc_group ORCH-110)." + module: tests/test_orch115_staging_runner.py + expected: PASS + + - id: TC-12 + type: unit + description: "Self-hosting safety: в командной строке раннера нет запрещённых литералов (рестарт 8500 / docker compose up orchestrator / --build / force-push main / правки .env)." + module: tests/test_orch115_staging_runner.py + expected: PASS + + - id: TC-13 + type: integration + description: "Наблюдаемость: GET /queue содержит блок staging_runner (enabled/repos/счётчики success/failed/tool_error/runs); на прогон пишется один структурный лог-вердикт, различающий код-фейл и tool-error." + module: tests/test_orch115_staging_runner.py + expected: PASS + + - id: TC-14 + type: integration + description: "Анти-дрейф LLM-карты: llm-call-sites.md (A6)/roadmap/policy обновлены под реализацию; tests/test_llm_call_site_inventory.py и tests/test_llm_determinization_docs.py остаются зелёными после правок." + module: tests/test_llm_call_site_inventory.py + expected: PASS