159 lines
16 KiB
Markdown
159 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
|
||
---
|
||
|
||
# 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/<work_item_id>/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) как доказанный паттерн перехвата + маппинга.
|