Files
orchestrator/docs/work-items/ORCH-116/01-brd.md
claude-bot 50c48c2c03
All checks were successful
CI / test (push) Successful in 1m8s
analyst(ET): auto-commit from analyst run_id=738
2026-06-16 02:29:50 +03:00

23 KiB
Raw Blame History

work_item, stage, author_agent, status, created_at, model_used
work_item stage author_agent status created_at model_used
ORCH-116 analysis analyst ready-for-review 2026-06-16 claude-opus-4-8

01 — BRD (бизнес-требования): ORCH-116 — заменить LLM-тестера детерминированным test-раннером

Work Item: ORCH-116 · Repo: orchestrator · Стадия: analysis

1. Бизнес-контекст и проблема

Стадия testing сейчас исполняется LLM-агентом tester (src/stages.py:17, STAGE_TRANSITIONS["review"]["agent"] = "tester" — tester запускается при входе в testing). Фактическая работа агента на этой стадии в happy-path — в основном детерминированная (.openclaw/agents/tester.md): прогнать регресс pytest tests/ -v --tb=short в worktree ветки, сделать read-only smoke (/health, /status, /queue + наличие блока serial_gate), собрать exit-код, записать 13-test-report.md с машинным frontmatter result: PASS|FAIL. Гейт check_tests_passed (src/qg/checks.py:182_parse_tests_verdict:226) читает только ключ result: из frontmatter — не прозу и не покрытие TC.

Это avoidable LLM control path по нормативной политике (docs/architecture/llm-usage-policy.md): (i) это C-консультация — вердикт result: потребляется гейтом check_tests_passed, и (ii) вердикт PASS/FAIL деривируем из exit-кода pytest + smoke (детерминированные сигналы). Карта вызовов (docs/architecture/llm-call-sites.md, строка A5) классифицирует tester как needs-hybrid-fallback, а roadmap (docs/architecture/llm-determinization-roadmap.md, машинный блок §4) ставит его rank 2 с hybrid_needed = yes, first_slice = no. Это второй срез determinization-roadmap — после реализованного первого среза ORCH-115 (детерминированный staging_runner, deployer на deploy-staging).

Гибридная природа (ключевое отличие от ORCH-115). Tester — needs-hybrid-fallback, не replace-deterministic-now: его PASS/FAIL-ядро полностью детерминируемо (exit-код pytest + smoke), но часть работы прежнего промпта — триаж падений, анализ пробелов покрытия AC, сопоставление TC ↔ критерии приёмки и человекочитаемая диагностика — это настоящее суждение. ORCH-116 выносит из потока управления только PASS/FAIL-исполнителя (его делает детерминированный код); опциональный LLM-аналитик допустим как off-control-path триаж/ диагностика после детерминированного провала, но никогда как первичный исполнитель вердикта гейта (BR-8 / NFR-7).

Боль / риск, который закрываем:

  • Недетерминизм в потоке управления. Решение «advance на deploy-staging или rollback на development» на стадии testing зависит от LLM-сессии (стоимость, латентность, риск галлюцинации/неверного вердикта), хотя сводится к exit-коду pytest + smoke.
  • Стоимость и латентность. Каждый прогон tester'а тратит токены/время opus-агента (оценка по agent_runs: tester-строки ~60150k токенов / 520 мин на прогон; точное число — GET /metrics) ради действия, которое выполняется одним прогоном pytest + несколькими read-only GET.
  • Избыточность с уже-зелёным сигналом. Регресс уже прогоняется в CI (check_ci_green гейтит development → review, src/qg/checks.py:82), повторно в merge-gate re-test (ORCH-043/110) и в coverage-gate (ORCH-027). Повторный прогон pytest на стадии testing — подтверждение факта, а не суждение → естественный кандидат на детерминизацию.
  • Класс инцидентов «LLM принял решение, которое есть исполнение фиксированных команд + маппинг результата» — тот же RCA-трек, что ORCH-110/111/112/113/114/117/115.

Установленные факты (не изобретать):

  • Гейт check_tests_passed/_parse_tests_verdict читает только result: (плюс legacy-поля verdict:/status:, ORCH-047) из frontmatter 13-test-report.md; покрытие TC / сопоставление с AC гейтом не парсится (это была нагрузка промпта, не требование гейта). Значит замена продюсера result: детерминированным кодом сохраняет контракт гейта байт-в-байт.
  • Детерминированный прецедент замены агента до _spawn уже работает и проверен: launch_job перехватывает deploy-finalizer (D1, src/agents/launcher.py:397), post-deploy-monitor (D2, :402) и staging-runner (ORCH-115, :405-408) до _spawn; src/staging_runner.py — готовый эталон leaf-раннера (two-level outcome, never-raise, kill-switch, proc_group).
  • Изоляция спавненного pytest уже решена: src/proc_group.py::run_in_process_group (ORCH-110) даёт tree-kill (os.killpg, каскад SIGTERM→grace→SIGKILL) — корень CPU-голодания от осиротевших pytest (инцидент ORCH-109/111) закрыт; раннер обязан использовать его.
  • Маппинг exit-кода — тривиальная pure-функция (0 → PASS, иначе FAIL), зеркало self_deploy.map_exit_code_to_status (но в токенах PASS/FAIL, а не SUCCESS/FAILED).

2. Объём (scope)

В объёме (Phase 1)

  • Детерминированный test-раннер для стадии testing репо orchestrator (self-hosting): исполняет «тест-контракт» (сконфигурированные test/smoke-команды) в worktree ветки задачи, маппит exit-код в result: PASS|FAIL, пишет 13-test-report.md, инициирует существующий гейт check_tests_passedбез запуска LLM-агента tester.
  • Тест-контракт — сконфигурированный набор команд: обязательная регресс-команда (pytest tests/, переиспользуя конвенцию merge_retest_target) + опциональные read-only smoke-проверки (зеркало шага 3 промпта tester: /health, /status, /queue + наличие блока serial_gate). Для self-hosting orchestrator контракт известен по умолчанию.
  • Раннер активируется через перехват в launch_job до _spawn (прецедент D1/D2/ORCH-115), без правки src/stages.py/STAGE_TRANSITIONS (роль tester в словаре остаётся; меняется лишь кто обрабатывает джоб на стадии testing для in-scope репо с тест-контрактом).
  • После выпуска вердикта раннер инициирует существующую оценку exit-гейта check_tests_passed ровно как завершившийся LLM-tester (advance_stage(finished_agent="tester")): PASS → deploy-staging (и далее под-гейты ORCH-022/043/027/058 на ребре deploy-staging → deploy); FAIL → существующий откат testing → development + developer-retry (src/stage_engine.py:849).
  • Two-level outcome (анти-ORCH-110, по образцу staging_runner): сюита исполнилась → вердикт → advance; сюита не исполнилась (tool-error: spawn-error/таймаут/returncode None) → bounded DEFER, на исчерпании → fail-closed FAIL + advance + alert. Инфра-сбой не жжёт developer-retry.
  • Kill-switch + скоуп-CSV + тест-контракт (паттерн ORCH-022/027/043/089/090/115): *_enabled (откат к LLM-пути), *_repos (пусто → self-hosting only), backward-compat: репо без тест-контракта → раннер не применяется → прежний LLM-tester.
  • Наблюдаемость: read-only блок в GET /queue + структурный лог вердикта.

Вне объёма (явно НЕ делаем в ORCH-116)

  • ORCH-115 (детерминированный deploy/staging-раннер)по явной границе задачи не смешиваем: ORCH-116 не модифицирует src/staging_runner.py и не трогает ребро deploy-staging/deploy.
  • LLM-роли reviewer и developerостаются без изменений (граница задачи). reviewer — control-path-но-keep (вердикт verdict: не деривируем из tool-сигнала, llm-call-sites.md).
  • Реализация опционального off-control-path LLM-триажа/диагностики после FAIL — не делается в этом срезе (forward-looking, §6 BRD и §8 TRZ). Архитектура раннера не должна запрещать её (NFR-7), но и не реализует.
  • Сопоставление TC ↔ критерии приёмки / анализ пробелов покрытия AC как условие гейта — гейт check_tests_passed его не требует (читает только result:); в потоке управления его нет. Это off-control-path диагностика (см. выше).
  • Любая правка STAGE_TRANSITIONS / реестра и имён QG_CHECKS / семантики check_tests_passed / _parse_tests_verdict / machine-verdict-ключей (result:/verdict:/status:) / схемы БД (см. NFR-1).
  • Замена/правка check_ci_green / merge-gate re-test / coverage-gate — они продолжают работать как есть; ORCH-116 меняет только продюсера 13-test-report.md.

3. Заинтересованные стороны

  • Заказчик / Owner (homenet542@gmail.com) — инициатор детерминизации LLM-control-path'ов.
  • Платформа orchestrator (self-hosting) — прямой потребитель: дешевле/быстрее/детерминированнее собственная стадия testing.
  • Другие проекты на общем инстансе (enduro-trails) — НЕ затронуты в Phase 1 (скоуп self-hosting + backward-compat для репо без тест-контракта); выигрывают позже от Phase 2 (project test contract).
  • Reviewer / Developer-роли конвейера — принимают результат через неизменные гейты; их LLM-роли не трогаются.

4. Бизнес-требования (BR)

  • BR-1 — Детерминированный PASS/FAIL без LLM. На стадии testing для in-scope репо с тест-контрактом вердикт result: производится детерминированным кодом (исполнение тест-контракта
    • маппинг exit-кода), без консультации LLM. Happy-path testing не вызывает _spawn.
  • BR-2 — Контракт артефакта неизменен. Раннер пишет тот же 13-test-report.md с тем же frontmatter-ключом result: PASS|FAIL, который читает check_tests_passed/_parse_tests_verdict. Гейт и парсер байт-в-байт не меняются.
  • BR-3 — Эквивалентность маршрутизации. PASS → продвижение на deploy-staging через существующий путь; FAIL → существующий откат testing → development + инкремент developer-retry (тот же путь и cap MAX_DEVELOPER_RETRIES, что у FAIL-вердикта LLM-tester'а, src/stage_engine.py:849). Никаких новых рёбер/исходов.
  • BR-4 — Переиспользование существующей инфраструктуры. Раннер исполняет pytest через proc_group.run_in_process_group (ORCH-110, tree-kill), переиспользует exit-code→verdict-маппинг (зеркало self_deploy.map_exit_code_to_status, в токенах PASS/FAIL) и конвенцию таргета (merge_retest_target/tests/). Не плодит второй несогласованный маппинг/механизм.
  • BR-5 — Обратимость одним флагом. Глобальный kill-switch возвращает прежний LLM-tester-путь на testing байт-в-байт; скоуп-CSV ограничивает раннер in-scope репо (пусто → только orchestrator).
  • BR-6 — Наблюдаемость. Исход раннера (запущен / PASS / FAIL / tool-error / defer) виден в GET /queue и в структурном логе; «код упал» (детерминированный FAIL) и «инструмент недоступен» (tool-error) различимы.
  • BR-7 — Self-hosting safety. Раннер на testing никогда не рестартит прод-контейнер 8500, не трогает main force-push'ем, не правит .env/docker-compose.yml. Он лишь читает, исполняет тест-контракт в worktree (pytest) и read-only smoke против 8500, пишет лог и best-effort пушит лог в фичеветку (merge в main — штатным merge-gate-путём).
  • BR-8 — Гибрид: LLM только off-control-path. Детерминированный раннер — единственный исполнитель вердикта result:. LLM на стадии testing допустим лишь как опциональный off-control-path триаж/диагностика после детерминированного FAIL и не выносит/не переопределяет машинный вердикт гейта. В Phase 1 он не реализуется (NFR-7), но архитектурно не запрещён.
  • BR-9 — Backward-compatibility для репо без тест-контракта. Репо, для которого тест-контракт не сконфигурирован/не резолвится, раннер не перехватывает → стадию testing ведёт прежний LLM-tester (fail-safe). enduro-trails и любые будущие репо без контракта — 1:1 как до ORCH-116.

5. Нефункциональные требования (NFR)

  • NFR-1 — Скоуп-инвариант (анти-дрейф). STAGE_TRANSITIONS (src/stages.py), реестр и имена QG_CHECKS/check_tests_passed/_parse_tests_verdict (src/qg/checks.py), machine-verdict-ключи (result:/verdict:/status:/staging_status:/deploy_status:/security_status:/coverage_status:), схема БД — байт-в-байт не тронуты. Это замена продюсера артефакта, не гейта.
  • NFR-2 — never-raise / fail-safe. Любая ошибка раннера (pytest не запустился, таймаут, I/O) → безопасный детерминированный исход без падения воркера: либо FAIL (fail-closed, никогда ложный green), либо штатный bounded requeue/defer — не «тихий advance». Сбой раннера не клинит очередь всех проектов. Tool-error ≠ code-fail: инфра-сбой не жжёт developer-retry (анти-ORCH-110).
  • NFR-3 — Изоляция процесса / таймаут. Спавненный pytest исполняется через proc_group (tree-kill, ORCH-110); сирот pytest не оставляет; ограниченный таймаут.
  • NFR-4 — Сквозные бюджеты времени. Таймаут раннера согласован со сквозным инвариантом ORCH-065/109/110 (reaper_max_running_s > Σ(работ на ребре) + grace) — без правки reaper_max_running_s. Ребро testing отдельно от ребра deploy-staging; бюджет ≤ окна, которое раннер замещает (прежний tester шёл под agent_timeout_seconds).
  • NFR-5 — Совместимость с не-self репо. Для репо вне скоупа / без тест-контракта testing ведёт себя 1:1 как до ORCH-116 (LLM-tester). enduro-trails не затронут.
  • NFR-6 — Соответствие политике LLM. Изменение снимает LLM-консультацию A5 из потока управления; карта docs/architecture/llm-call-sites.md (A5) / llm-determinization-roadmap.md (rank 2) / llm-usage-policy.md и анти-дрейф-тесты обновляются в том же PR (норматив сопровождения ORCH-118).
  • NFR-7 — Не запрещать будущий off-control-path LLM-триаж. Архитектура раннера не должна архитектурно исключать опциональный LLM debug/triage-аналитик после детерминированного FAIL (будущее улучшение); в ORCH-116 он не реализуется.

6. Допущения и ограничения

  • Допущение А1. Регресс-сюита orchestrator (pytest tests/) исполняема в worktree ветки задачи и её exit-код — авторитетный сигнал PASS/FAIL (как уже трактуют CI / merge-gate re-test / coverage-gate).
  • Допущение А2. Для self-hosting orchestrator тест-контракт известен по умолчанию (pytest + read-only smoke против 8500). Для прочих репо контракт отсутствует, пока не сконфигурирован (Phase 2) → раннер их не перехватывает (BR-9).
  • Допущение А3. Перехват «до _spawn» по имени джоб-роли (tester) + стадии задачи (testing) — достаточный механизм диспетчеризации (как D1/D2/ORCH-115); конкретный механизм финализирует архитектор (06-adr).
  • Ограничение О1. Граница задачи: не смешивать с ORCH-115 (его код не модифицируется); LLM-роли reviewer/developer не трогаются; код ORCH-112/114 не модифицируется.
  • Ограничение О2. Phase 2 (project test contract для не-self репо + опциональный off-control-path LLM-триаж) — отдельный follow-up; ORCH-116 закрывает только Phase 1.

7. Критерии успеха

Стадия testing для orchestrator проходит без запуска LLM-агента tester: детерминированный раннер исполняет тест-контракт (pytest + smoke), пишет корректный 13-test-report.md (result: PASS|FAIL), инициирует неизменный гейт check_tests_passed, и конвейер продвигается (testing → deploy-staging) / откатывается (testing → development + developer-retry) ровно как раньше — при неизменных STAGE_TRANSITIONS/QG_CHECKS/гейтах/схеме БД, под kill-switch с откатом к прежнему LLM-поведению, с backward-compat для репо без тест-контракта. Детальные PASS/FAIL — 03-acceptance-criteria.md.

8. Риски

Краткий перечень (детали — 10-tech-risks.md, заполняет архитектор):

  • R-1 — точка диспетчеризации «до _spawn» должна корректно отличать testing-tester от любого иного джоба (по роли + стадии задачи), иначе можно перехватить не тот джоб.
  • R-2 — после выпуска вердикта нужно надёжно инициировать advance_stage(finished_agent="tester"), иначе задача зависнет на testing (нет «финиша агента», который раньше триггерил гейт).
  • R-3 — таймаут/изоляция pytest-subprocess; утечка процессов (корень инцидента ORCH-109/110/111) — обязателен proc_group tree-kill.
  • R-4 — корректность two-level outcome: tool-error не должен жечь developer-retry (анти-ORCH-110), но и не давать ложный green/тихий advance.
  • R-5 — корректность отката FAIL (developer-retry cap, встраивание extract_test_failures) — должна совпасть с LLM-путём src/stage_engine.py:849.
  • R-6 — гибрид: не протащить LLM обратно в поток управления вердикта (BR-8); off-control-path триаж — отдельная роль/джоб, не выносящая result:.
  • R-7 — backward-compat: репо без тест-контракта обязаны откатываться на LLM-tester (BR-9), иначе enduro/новый репо «застрянет» без продюсера отчёта.