feat(staging): deterministic staging-runner replacing LLM deployer on deploy-staging (ORCH-115)
All checks were successful
CI / test (push) Successful in 1m8s
CI / test (pull_request) Successful in 1m8s

Replace the LLM `deployer` agent on the `deploy-staging` stage (self-hosting
orchestrator) with a deterministic staging-runner intercepted in launch_job
BEFORE _spawn (the deploy-finalizer / post-deploy-monitor reserved-agent
precedent). The runner executes the SAME staging suite, maps the exit-code to
`staging_status:` via the existing self_deploy.map_exit_code_to_status contract,
writes 15-staging-log.md, and initiates the UNCHANGED check_staging_status gate
exactly as a finished LLM-deployer would.

Invariant (NFR-1): this replaces only the *producer* of the artifact — the
artifact contract, the gate / _parse_staging_status / check_staging_status name,
STAGE_TRANSITIONS, the machine-verdict key `staging_status:` and the DB schema are
byte-for-byte unchanged. Additive, under a kill-switch + repo-scope CSV,
never-raise, fail-safe back to the LLM path.

Two-level outcome (D5, anti ORCH-110): suite executed -> verdict -> advance
(FAILED -> the existing deploy-staging -> development rollback + developer-retry,
same as a FAILED LLM verdict); tool-error (suite did not execute) -> bounded DEFER
-> fail-closed FAILED + alert on exhaustion (infra != code fault; never a silent
advance / false green).

First implemented slice of the LLM determinization roadmap (ORCH-118 A6,
replace-deterministic-now).

- New leaf src/staging_runner.py (never-raise; proc_group tree-kill + timeout)
- launch_job intercept + _run_staging_runner_job (mirror _run_deploy_finalizer_job)
- config: ORCH_STAGING_RUNNER_* keys (enabled/repos/timeout/infra-retry budget)
- GET /queue staging_runner observability block
- docs: llm-call-sites/roadmap/usage-policy (A6 implemented; machine blocks +
  single-transport invariant intact), deployer.md (LLM branch -> fallback),
  CLAUDE.md, CHANGELOG.md, overview (tech-pipeline/tech-agents/tech-quality-security),
  .env.example
- tests/test_orch115_staging_runner.py (TC-01..TC-13); LLM anti-drift green (TC-14)

Refs: ORCH-115

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 01:59:43 +03:00
parent f120e4bd8f
commit b50cf1dd08
16 changed files with 1235 additions and 7 deletions

View File

@@ -58,7 +58,7 @@
| **A3** | `.openclaw/agents/developer.md` | стадия `development` | developer | код + PR | — | `check_ci_green:82` (+ `check_branch_mergeable:657`) — CI/merge | ~150400k / 1040 мин | да (через S0) | **P** | `keep-LLM` | написание кода — настоящее суждение; гейт судит CI/merge, не самоотчёт |
| **A4** | `.openclaw/agents/reviewer.md` | стадия `review` | reviewer | `12-review.md` | `verdict:` | `check_reviewer_verdict:336` (`verdict:`) | ~100300k / 525 мин | да (через S0) | **C** | `keep-LLM` | control path, но вердикт «приемлемость кода/решения» **НЕ деривируем** из exit-кода — настоящее суждение |
| **A5** | `.openclaw/agents/tester.md` | стадия `testing` | tester | `13-test-report.md` | `result:` | `check_tests_passed:182``_parse_tests_verdict:226` (`result:`) | ~60150k / 520 мин | да (через S0) | **C** | `needs-hybrid-fallback` | **avoidable**: PASS/FAIL = exit-code `pytest`+smoke (деривируем); LLM нужен лишь на триаж падений / маппинг TC↔критерии |
| **A6** | `.openclaw/agents/deployer.md` | стадии `deploy-staging` / `deploy` | deployer | `15-staging-log.md` / `14-deploy-log.md` | `staging_status:` / `deploy_status:` | `check_staging_status:599``_parse_staging_status:538` (`staging_status:`); `check_deploy_status:473``_parse_deploy_status:413` (`deploy_status:`) | ~40120k / 315 мин | да (через S0) | **C** | `replace-deterministic-now` | **avoidable**: staging-вердикт = маппинг exit-кода `staging_check.py`; прод уже детерминирован Phase A/B/C (ORCH-036) |
| **A6** | `.openclaw/agents/deployer.md` | стадии `deploy-staging` / `deploy` | deployer | `15-staging-log.md` / `14-deploy-log.md` | `staging_status:` / `deploy_status:` | `check_staging_status:599``_parse_staging_status:538` (`staging_status:`); `check_deploy_status:473``_parse_deploy_status:413` (`deploy_status:`) | ~40120k / 315 мин | да (через S0; для in-scope репо на `deploy-staging`**нет**, перехват до `_spawn`) | **C** | `replace-deterministic-now` | **avoidable, СРЕЗ РЕАЛИЗОВАН (ORCH-115):** на `deploy-staging` для self-hosting `orchestrator` вердикт `staging_status:` производит детерминированный `src/staging_runner.py` (перехват в `launch_job` до `_spawn`, как D1/D2) — маппинг exit-кода `staging_check.py`; прод уже детерминирован Phase A/B/C (ORCH-036). LLM-ветвь остаётся fallback'ом под выключенным флагом / для не-self репо |
| **D1** | `src/agents/launcher.py:389` (перехват в `launch_job` **до** `_spawn`; «Not an LLM spawn» `407`) | post-deploy edge | deploy-finalizer | jobs-row | — | — | — (детерминированный) | **нет** (слот, перехват до `_spawn`) | — | `already-deterministic` (эталон) | Занимает слот агента, но LLM не консультируется — рабочий прецедент замены |
| **D2** | `src/agents/launcher.py:394` (перехват в `launch_job` **до** `_spawn`; «Not an LLM spawn» `428`) | post-deploy observation | post-deploy-monitor | jobs-row | — | — | — (детерминированный) | **нет** (слот, перехват до `_spawn`) | — | `already-deterministic` (эталон) | Тик наблюдения; LLM не консультируется |
@@ -147,8 +147,14 @@
- **Docs + tests only (ORCH-118).** `STAGE_TRANSITIONS` / реестр и имена `QG_CHECKS`/`check_*` /
machine-verdict-ключи (`verdict:`/`result:`/`staging_status:`/`deploy_status:`/`security_status:`/
`coverage_status:`) / схема БД — **байт-в-байт не тронуты**. Раннеры замен **не** реализованы
(см. roadmap — follow-up'ы по роли, без Plane-ID).
`coverage_status:`) / схема БД — **байт-в-байт не тронуты** (это инвариант самой карты).
- **Реализованные срезы.** Первый срез roadmap'а**deployer (staging-status)** — реализован
**ORCH-115** (`src/staging_runner.py`, перехват в `launch_job` до `_spawn`): на `deploy-staging`
для in-scope репо вердикт `staging_status:` производит детерминированный код, не LLM. Это
`replace-deterministic-now` без ввода второго транспорта (раннер LLM не зовёт) — карта/инвариант
«единственный транспорт S0» соблюдены. Машинный `ORCH-118-INVENTORY-BLOCK` сохраняет deployer как
`avoidable=yes`/`axis=C` (LLM-ветвь жива как fallback под выключенным флагом / для не-self репо).
Второй кандидат (`tester`) остаётся follow-up'ом по роли.
- **Анти-дрейф тесты:** `tests/test_llm_call_site_inventory.py` (TC-01…TC-06, TC-09, TC-12, TC-13,
TC-14) и `tests/test_llm_determinization_docs.py` (TC-07/08/11). Дискриминатор всех проверок —
**«консультирует LLM», а не «спавнит subprocess»**.

View File

@@ -15,7 +15,17 @@
---
## 1. Рекомендованный первый срез — **deployer (staging-status)**
## 1. Рекомендованный первый срез — **deployer (staging-status)** — ✅ РЕАЛИЗОВАН (ORCH-115)
> **Статус: реализовано.** Срез выполнен в **ORCH-115** — `src/staging_runner.py` (перехват в
> `launch_job` до `_spawn`, как `D1`/`D2`): на стадии `deploy-staging` для self-hosting `orchestrator`
> вердикт `staging_status:` производит детерминированный код (маппинг exit-кода `staging_check.py`
> через `self_deploy.map_exit_code_to_status`), а не LLM. Под kill-switch `staging_runner_enabled` +
> скоуп `staging_runner_repos` (пусто → self-hosting only); LLM-ветвь остаётся fallback'ом.
> Контракт артефакта/гейта `check_staging_status`/`STAGE_TRANSITIONS`/схема БД — не тронуты. Детали —
> `docs/work-items/ORCH-115/06-adr/ADR-001-deterministic-staging-runner.md`, сквозной
> `docs/architecture/adr/adr-0048-deterministic-staging-runner.md`. Запись `rank 1` в машинном блоке
> §4 сохраняется (`first_slice = yes`, инвариант карты) как историческая фиксация первого среза.
Обоснование (самый низкорисковый «чисто деривируемый» control path):

View File

@@ -94,3 +94,8 @@ Call-site является **avoidable LLM control path** тогда и толь
`launcher._spawn` (S0). Ввод второго транспорта (новый `_spawn`, импорт `anthropic`/`openai`/иного
LLM-SDK, прямой HTTP Anthropic/Claude, второй model-invoking subprocess) запрещён без явного ADR;
прибито тестами TC-01/TC-12.
- **Реализованный срез (ORCH-115).** Снятие C-консультации с деривируемым вердиктом — это разрешённое
`replace-deterministic-now`, а не ввод новой LLM-консультации. ORCH-115 снял A6/staging-status:
детерминированный `src/staging_runner.py` производит `staging_status:` без `_spawn` (перехват до
него, как `D1`/`D2`) — раннер **LLM не зовёт** и **второй транспорт не вводит**, поэтому инвариант
«единственный транспорт S0» соблюдён (TC-12 зелёный). Это образец для последующих срезов roadmap'а.

View File

@@ -48,6 +48,12 @@ Machine-verdict ключи читаются гейтами **только из Y
Особенность: промпт `deployer` сознательно на английском (самый safety-critical — несёт
запреты self-hosting в видной рамке); остальные пять — на русском.
Особенность (ORCH-115): на стадии `deploy-staging` для self-hosting `orchestrator` LLM-`deployer`
заменён **детерминированным staging-раннером** (`src/staging_runner.py`) — его работа была чисто
механической (запуск staging-сюиты + маппинг exit-кода). LLM-промпт `deployer` остаётся fallback'ом
под выключенным флагом / для не-self репо и продолжает вести прод-стадию `deploy`. Подробнее —
[конвейер](tech-pipeline.md) и [карта LLM-консультаций](../architecture/llm-call-sites.md).
## Человек как седьмая роль
Человек не пишет артефакты конвейера, но принимает два решения, которые не делегированы

View File

@@ -34,6 +34,16 @@ created → analysis → architecture → development → review → testing →
| `done` | — | — | терминал |
| `cancelled` | — | — | терминал (сток отмены) |
> **Детерминированный staging-раннер (ORCH-115).** На стадии `deploy-staging` для self-hosting
> `orchestrator` работу ведёт **детерминированный код** (`src/staging_runner.py`), а не LLM-агент
> `deployer`: он перехватывается в `launch_job` до запуска агента (как Phase A/B/C прод-деплоя),
> исполняет ту же staging-сюиту, маппит exit-код в `staging_status:` и инициирует **тот же** гейт
> `check_staging_status`. Это замена *продюсера* артефакта, а не гейта: контракт `15-staging-log.md`,
> имя/семантика `check_staging_status`, `STAGE_TRANSITIONS` — не изменились. Под kill-switch
> `staging_runner_enabled` (скоуп `staging_runner_repos`, пусто → self-hosting only); при выключении
> на стадии снова работает LLM-`deployer` байт-в-байт. Это первый реализованный срез
> determinization-roadmap (см. `docs/architecture/llm-determinization-roadmap.md`).
## Под-гейты деплойного ребра — врезки, не стадии
На переходе `deploy-staging → deploy` исполняются четыре под-гейта в нормативном порядке

View File

@@ -50,7 +50,10 @@ LLM, и **нормативную политику** «LLM — только та
control-path и его вердикт деривируем из exit-кодов (тогда консультацию можно заменить
детерминированным раннером). Поимённо: avoidable = `{tester, deployer}`; настоящее суждение
сохраняется у `{analyst, architect, developer, reviewer}`. Карта — снимок, прибитый структурными
анти-дрейф тестами; реализация замен — отдельные follow-up'ы по роли.
анти-дрейф тестами. **Первый срез реализован (ORCH-115):** на `deploy-staging` для self-hosting
`orchestrator` LLM-`deployer` заменён детерминированным `src/staging_runner.py` (вердикт
`staging_status:` = маппинг exit-кода staging-сюиты); LLM-ветвь остаётся fallback'ом, гейт
`check_staging_status` не тронут. Замена второго кандидата (`tester`) — follow-up по роли.
- Карта вызовов LLM: [`../architecture/llm-call-sites.md`](../architecture/llm-call-sites.md)
- Нормативная политика: [`../architecture/llm-usage-policy.md`](../architecture/llm-usage-policy.md)