---
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/<work_item_id>/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 |
