23 KiB
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-строки ~60–150k токенов / 5–20 мин на прогон; точное число —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) из frontmatter13-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-hostingorchestratorконтракт известен по умолчанию. - Раннер активируется через перехват в
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-closedFAIL+ 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.
- маппинг exit-кода), без консультации LLM. Happy-path
- 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 (тот же путь и capMAX_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, не трогаетmainforce-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_grouptree-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/новый репо «застрянет» без продюсера отчёта.