--- work_item: ORCH-118 stage: analysis author_agent: analyst status: ready-for-review created_at: 2026-06-15 model_used: claude-opus-4-8 --- # 02 — ТЗ (TRZ): ORCH-118 — replace avoidable LLM control paths with deterministic implementations Work Item: **ORCH-118** · Repo: **orchestrator** · Стадия: analysis > ТЗ описывает **что** должно измениться и **где** (артефакты/контракты/тесты), выведено из BRD и > фактического кода. **Как** (точная структура/размещение документов карты, формат классификации, > схема структурных тестов) — решает архитектор в `06-adr/`. ТЗ фиксирует требования и границы. > > ⚠️ **Скоуп — inventory + классификация + roadmap + политика + структурные тесты.** Реализация > детерминированных раннеров — **вне скоупа** (FR-7). Это **docs + tests only**: `src/**`-рантайм не > меняется. > > 📌 **Follow-up'ы — по роли, без выдуманных Plane-ID** (R3, NFR-6). Конкретные ID (напр. > `ORCH-115`/`ORCH-116`) в артефактах **не фиксируются** — этих work item нет в репо/подтверждённом > backlog; ID присваивается при заведении задачи. > > 🔁 **R4 — блокер R3-ревью (закрыт).** Единица инвентаря/инварианта — **LLM-консультация** (control-path > потребляет суждение LLM), **не** «спавн процесса / существование Claude CLI». Claude CLI-subprocess > через `_spawn` — лишь **текущий транспорт**; «процесс существует» ≠ «LLM консультирован» > (capability ≠ consultation). Развёрнуто — BRD §0; затрагивает FR-1/FR-3/FR-6. > > 🔁 **R5 — единственный оставшийся блокер R4-ревью.** Артефакты разводили «консультация ≠ транспорт/ > слот», но **не делали явной control-path-ось** — самую важную для названия задачи: среди реальных > консультаций **не различались** (C) control-path (LLM-вердикт потребляется потоком управления — > `check_*` ветвится на нём) и (P) artifact-producer (детерминированный гейт судит артефакт; суждение > LLM в control flow не входит); и нигде **не был определён** термин **«avoidable LLM control path»**. > R5 добавляет ось + определение (BRD §0-bis) и тянет их в FR-1/FR-2 + новый **FR-8**, в инвентарь > §1 (новая колонка), в тесты FR-6 (**TC-13/TC-14**). Содержательная классификация не меняется — > R5 **выводит** её из `src/qg/checks.py`. --- ## 1. Сводка изменения Выпустить **доказательную карту** всех мест вызова LLM в оркестраторе с классификацией и упорядоченным roadmap'ом детерминированных замен, а также нормативную **политику использования LLM**; прибить инварианты карты к коду набором **структурных тестов**. Реализация замен не входит. Все рантайм-контракты (`STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict-ключи / схема БД) — **не меняются**. Опорный факт инвентаризации (ground-truth кода на момент задачи; line-привязки уточняет карта): > Единица — **LLM-консультация** (потребление суждения LLM), а не «спавн процесса» (R4, BRD §0). > R5: дополнительно размечается **ось (C) control-path / (P) artifact-producer** и **потребитель > вывода** (`check_*`/`_parse_*`), доказывающий ось. Колонка `Консультирует LLM?` помечает транспорт/ > слот vs факт консультации; колонка `Control path?` помечает, входит ли LLM-вывод в поток управления. | # | Call-site | Где | Что делает | Консультирует LLM? | Потребитель вывода (control-flow consumer) | Control path? (C/P) | Avoidable LLM control path? | |---|-----------|-----|------------|--------------------|--------------------------------------------|---------------------|-----------------------------| | S0 | **Единственный транспорт LLM-консультации** | `src/agents/launcher.py::_spawn` (`CLAUDE_BIN --print … --system-prompt "$(cat …)"` + `Popen`) | реализует консультацию для любой из 6 ролей | транспорт (capability) | — | — | — (транспорт, не call-site решения) | | A1 | analyst | `.openclaw/agents/analyst.md`, стадия `analysis` | анализ → 01–04 | да (через S0) | `check_analysis_complete` (`src/qg/checks.py:33`) — **наличие файлов** | **P** (artifact-producer) | **нет** (не control path) → keep-LLM | | A2 | architect | `.openclaw/agents/architect.md`, стадия `architecture` | архитектура → 06-adr | да (через S0) | `check_architecture_done` (`checks.py:62`) — **наличие** 06-adr/07 | **P** | **нет** → keep-LLM | | A3 | developer | `.openclaw/agents/developer.md`, стадия `development` | реализация + PR | да (через S0) | `check_ci_green` (`checks.py:82`) + `check_branch_mergeable` (`checks.py:657`) — **CI/merge** | **P** | **нет** → keep-LLM | | A4 | reviewer | `.openclaw/agents/reviewer.md`, стадия `review` | ревью → `12-review.md` (`verdict:`) | да (через S0) | `check_reviewer_verdict` (`checks.py:336`) читает **`verdict:`** → REQUEST_CHANGES-откат | **C** (control path) | **нет** (вердикт НЕ деривируем — настоящее суждение) → keep-LLM | | A5 | tester | `.openclaw/agents/tester.md`, стадия `testing` | `pytest`+smoke → `13-test-report.md` (`result:`) | да (через S0) | `check_tests_passed` (`checks.py:182`) → `_parse_tests_verdict` (`checks.py:226`) читает **`result:`** | **C** | **ДА** (вердикт = exit-code `pytest`/smoke) → needs-hybrid-fallback | | A6 | deployer | `.openclaw/agents/deployer.md`, стадии `deploy-staging`/`deploy` | `staging_check.py`/exit-code → `15`/`14` логи | да (через S0) | `check_staging_status` (`checks.py:599`)→`_parse_staging_status` (`checks.py:538`) **`staging_status:`**; `check_deploy_status` (`checks.py:473`)→`_parse_deploy_status` (`checks.py:413`) **`deploy_status:`** | **C** | **ДА** (вердикт = `staging_check.py`/exit-code; прод уже детерминирован Phase A/B/C) → replace-deterministic | | D1 | deploy-finalizer | `launch_job` перехват **до** `_spawn` (`launcher.py:389`) | детерминированный (LLM не консультируется) | **нет** (слот агента, перехват до `_spawn`) | — | — (нет консультации) | — (уже детерминирован) | | D2 | post-deploy-monitor | `launch_job` перехват **до** `_spawn` (`launcher.py:394`) | детерминированный (LLM не консультируется) | **нет** (слот агента, перехват до `_spawn`) | — | — (нет консультации) | — (уже детерминирован) | > **Чтение таблицы (R5).** Три ортогональных факта: (1) `Консультирует LLM?` — транспорт/слот vs факт > консультации (R4 §0); (2) `Control path?` — входит ли вывод роли в **поток управления** (C: `check_*` > ветвится на LLM-вердикте; P: детерминированный гейт судит артефакт независимо); (3) `Avoidable …?` — > двухбитный предикат BRD §0-bis (C **И** вердикт деривируем из tool-сигналов). Итог: **avoidable LLM > control paths = {tester, deployer}**; control-path-но-keep = `{reviewer}`; не-control-path = > `{analyst, architect, developer}`. > > Не-агентские control-path'ы (маршрутизация `advance_stage`, `QG_CHECKS`/`check_*`/`_parse_*`, > `error_classifier`, `serial_gate`/`merge_gate`/`coverage_gate`/`security_gate`/`staging_verdict`/ > `review_parse`/`frontmatter`, `self_deploy` Phase A/B/C) — **уже детерминированы** (FR-3). ## 2. Задействованные модули / пути | Путь | Действие | |------|----------| | `src/agents/launcher.py` | **читать** (инвентарь S0/D1/D2; `_spawn`, `launch_job:389/394`, `AGENT_CONFIGS`, `resolve_agent_model/effort`) — **не менять** | | `.openclaw/agents/{analyst,architect,developer,reviewer,tester,deployer}.md` | **читать** (инвентарь 6 ролей) — **не менять** | | `src/qg/checks.py` | **читать** (доказательство control-path-оси: кто потребляет вывод каждой роли — `check_analysis_complete:33`/`check_architecture_done:62`/`check_ci_green:82`/`check_reviewer_verdict:336`/`check_tests_passed:182`+`_parse_tests_verdict:226`/`check_staging_status:599`+`_parse_staging_status:538`/`check_deploy_status:473`+`_parse_deploy_status:413`; `QG_CHECKS`-реестр) — **не менять** | | `src/stages.py`, `src/stage_engine.py` | **читать** (доказать детерминизм маршрутизации) — **не менять** | | `src/{serial_gate,merge_gate,coverage_gate,security_gate,staging_verdict,review_parse,error_classifier,frontmatter,self_deploy,post_deploy,transition_lease,reconciler,job_reaper}.py` | **читать** (детерминированные leaf'ы — доказательная база) — **не менять** | | `src/usage.py`, `src/db.py` (`agent_runs`) | **читать** (источник оценок экономии токенов/времени) — **не менять** | | `docs/architecture/llm-call-sites.md` *(имя — пример; финально решает архитектор)* | **создать**: карта call-site'ов + классификация + **control-path-разметка** (FR-1/FR-2/FR-3/FR-8) | | `docs/architecture/llm-determinization-roadmap.md` *(имя — пример)* | **создать**: упорядоченный roadmap (FR-4) | | `docs/architecture/llm-usage-policy.md` *(имя — пример)* | **создать**: нормативная политика + определение «avoidable LLM control path» (FR-5/FR-8) | | `docs/work-items/ORCH-118/06-adr/ADR-001-*.md` | **создать** (архитектор): фиксация карты/таксономии/control-path-оси/первого среза как ADR | | `tests/test_llm_call_site_inventory.py` *(имя — пример)* | **создать**: структурные анти-дрейф тесты (FR-6), включая control-path-инварианты | | `docs/architecture/README.md`, `docs/overview/*`, `CHANGELOG.md` | **обновить** (ссылка на карту/политику; норматив golden-source) | > Документы карты/политики целесообразно разместить в `docs/architecture/` (durable, сквозное), а не > только в `docs/work-items/ORCH-118/` — окончательное размещение/формат решает архитектор. ## 3. Функциональные требования ### FR-1 — Полнота и привязка инвентаря LLM-консультаций (BR-1) Единица — **LLM-консультация** (control-path потребляет суждение LLM), а не «спавн процесса» (R4, BRD §0). Карта перечисляет **каждый** call-site: `S0` (единственный транспорт `_spawn`), `A1…A6` (6 ролей, консультируют через S0), `D1/D2` (job-роли, **занимают слот агента, но НЕ консультируют LLM** — перехват в `launch_job` до `_spawn`; включены как доказательство паттерна). Поля записи: `id`, `location` (`file:line`), `trigger`, `stage/owner`, `output artifact`, `machine-verdict key` (если есть), **`output consumer` (`check_*`/`_parse_*` с `file:line` — кто потребляет вывод роли)**, `est. tokens/runtime`, **`consults-LLM`** (consultation vs LLM-capable transport/slot, §0.3), **`axis` (C control-path / P artifact-producer, §0-bis)**, `classification`, `rationale`, `dependency`, `risk`. Каждый `file:line` обязан резолвиться в реальный код. Инвариант (транспорт-агностичный, двусторонний): единственный транспорт LLM-консультации в `src/**` — `S0`; **иного LLM-транспорта нет** — тесты FR-6(a)+(f). ### FR-2 — Таксономия классификации (BR-2) Ровно 4 взаимоисключающих класса с определениями: - `keep-LLM` — нужно настоящее суждение; **обязательно назвать** конкретное суждение. - `replace-deterministic-now` — безопасная детерминированная замена сейчас. - `replace-later/risky` — замена возможна, но позже / с риском (нужны предпосылки). - `needs-hybrid-fallback` — детерминированное ядро + LLM-фолбэк только на суждение. Каждому call-site присвоен **ровно один** класс, и класс **выводится из control-path-оси** (§0-bis, FR-8), а не постулируется: - **P (artifact-producer)** → `keep-LLM` — детерминированный гейт судит артефакт; авторская работа требует суждения: `analyst`, `architect`, `developer`. - **C + НЕ-деривируемый вердикт** → `keep-LLM` — control path, но суждение настоящее: `reviewer` (назвать суждение: «приемлемость кода/решения», не сводится к exit-коду). - **C + деривируемый вердикт** → avoidable → `replace-*` / `needs-hybrid-fallback`: `deployer → replace-deterministic-now`/`replace-later/risky` (staging = exit-code-маппинг; прод self-hosting уже детерминирован Phase A/B/C), `tester → needs-hybrid-fallback` (детерминированный прогон `pytest`+smoke даёт PASS/FAIL; LLM-суждение только на триаж падений / маппинг TC↔критерии). - `deploy-finalizer/post-deploy-monitor → already-deterministic` (вне таксономии замен, эталон). > 📌 **Follow-up'ы — по роли, без Plane-ID (R3, NFR-6).** Кандидаты обозначаются ролью > (deployer-замена, tester-гибрид), а не конкретными ID. ### FR-3 — Подтверждение детерминизма не-агентских путей (BR-3) Карта отдельным разделом фиксирует, с `file:line`-доказательством, что НЕ консультируют LLM (не зависят от суждения LLM ни через `_spawn`, ни иным транспортом): маршрутизация (`advance_stage`/ `STAGE_TRANSITIONS`), все `QG_CHECKS`/`check_*`, парсеры вердиктов (`_parse_*` через `frontmatter.parse_frontmatter`), `error_classifier` (regex), под-гейты (security/merge/coverage/ image-freshness), `self_deploy` Phase A/B/C, reconciler/reaper/serial-gate/transition-lease. ⚠️ Эти модули **спавнят subprocess'ы** (`git`/`pytest`/`docker`/`ssh`/сканеры) — это детерминированные инструменты, **не** LLM-консультации (§0): доказательство детерминизма — отсутствие *LLM*-транспорта, а не отсутствие *subprocess*. Любая найденная неожиданная LLM-консультация добавляется в инвентарь (FR-1) и классифицируется (FR-2). ### FR-4 — Упорядоченный roadmap замен (BR-4) Ранжированный список кандидатов (названных **по роли**); для каждого: зависимости (предпосылки/блокеры), **оценка** экономии токенов/времени (из `agent_runs`), риск безопасности, нужен ли hybrid-fallback, ожидание kill-switch/обратимости, и **тип будущей follow-up задачи по роли** (без конкретного Plane-ID — заводится отдельно, NFR-6). Явно: **рекомендованный первый срез** + обоснование (самый низкорисковый, «чисто деривируемый» control path, опирающийся на существующий прецедент D1/D2). ### FR-5 — Политика использования LLM (BR-5) Нормативный durable-документ: принцип «LLM — только где нужно настоящее суждение»; критерии решения keep vs replace, **сформулированные через ось §0-bis** (является ли путь control path — ветвится ли `check_*` на LLM-выводе; деривируем ли вердикт из tool-сигналов; обратимость; влияние на автономность); требование к новым/изменённым control-path'ам обосновывать любое использование LLM против политики. Может включать рекомендацию reviewer-оси (как ORCH-079) — **как требование, не как реализацию гейта** (новый QG не вводится, FR-6 §QG). ### FR-6 — Структурные анти-дрейф тесты (BR-6) Новый offline-тест-файл (без сети/LLM/subprocess-к-модели), проверяющий инварианты карты. ⚠️ **R4 — инвариант формулируется вокруг LLM-консультации/транспорта, а не «существования процесса Claude CLI».** Дискриминатор тестов — **«консультирует LLM», а не «спавнит subprocess»**; десятки прочих subprocess (`git`/`pytest`/`docker`/`ssh`/сканеры/`staging_check.py`) явно исключаются из матчинга. - **(a) Единственный транспорт.** В `src/**` ровно **одна** точка сборки/запуска Claude CLI (матчинг по совокупности признаков LLM-транспорта: `CLAUDE_BIN` + `--system-prompt` + `Popen`/`bash -c`), и это `launcher._spawn`. *(Необходимое, но не достаточное — дополняется (f).)* - **(f) Отсутствие иного LLM-транспорта (R4).** В `src/**` **нет** альтернативного транспорта LLM-консультации: ни импорта `anthropic`/`openai`/иного LLM-SDK, ни прямого HTTP-эндпоинта Anthropic/Claude (`api.anthropic.com`, `/v1/messages` и т.п.), ни второго model-invoking subprocess-сборщика. Allowlist единственного разрешённого транспорта = `S0`. - **(b) Нет консультации в детерминированных путях.** Перечисленные детерминированные модули и обработчики `D1/D2` **не** консультируют LLM (ни `_spawn`-транспорта, ни альтернативного по (f)). - **(c)** Карта перечисляет ровно те промпт-файлы, что физически лежат в `.openclaw/agents/`. - **(d)** Классификация покрывает каждый перечисленный call-site **ровно один раз** (тотальность). - **(e) Capability ≠ consultation.** `D1/D2` перехватываются в `launch_job` **до** `_spawn` (`launcher.py:389/394`) → занимают слот, но консультации нет. - **(g) Control-path-разметка верна (R5, TC-13).** Для каждой роли поле `axis` (C/P) карты **согласовано с фактическим потребителем** в `src/qg/checks.py`: P-роли (`analyst`/`architect`/`developer`) потребляются детерминированными гейтами (`check_analysis_complete`/`check_architecture_done`/ `check_ci_green`), C-роли (`reviewer`/`tester`/`deployer`) — verdict-парсерами (`check_reviewer_verdict`/`_parse_tests_verdict`/`_parse_staging_status`/`_parse_deploy_status`). Дрейф (роль переразмечена или потребитель в коде сменил природу) → красный. - **(h) Avoidable-набор зафиксирован (R5, TC-14).** Множество «avoidable LLM control path» в карте = ровно `{tester, deployer}`; `reviewer` помечен control-path-но-keep; `analyst`/`architect`/ `developer` — не control path. Любое добавление/удаление без обновления карты → красный. > ❌ **Не вводить** тест, прибивающий карту к конкретным follow-up Plane-ID → ✅ тесты проверяют > только инварианты, резолвящиеся в код/файлы репозитория (R3, NFR-6). ### FR-7 — Скоуп-гард (BR-7) Раннеры замен **не реализуются** в ORCH-118. Карта/roadmap явно помечают кандидатов **по роли** (deployer-замена, tester-гибрид) как follow-up, старт которых гейтится утверждением карты. Тест/диф не должны содержать новых детерминированных раннеров tester/deployer. **Конкретные follow-up Plane-ID не фиксируются** ни в одном артефакте (NFR-6). ### FR-8 (R5) — Явная control-path-ось и определение «avoidable» (BR-8/BR-9) 1. **Разметка оси.** Карта несёт для каждой консультации поле `axis` ∈ {C, P} (§0-bis) + доказательство (поле `output consumer` — `check_*`/`_parse_*` с `file:line`). Разметка проверяема (FR-6g/TC-13). 2. **Определение термина.** Карта/политика дают **нормативное определение** «avoidable LLM control path» — двухбитный предикат: (i) C-консультация (LLM-вердикт потребляется потоком управления) **И** (ii) вердикт деривируем из tool-сигналов (exit-code `pytest`/smoke/`staging_check.py`/деплоя). 3. **Поимённый целевой набор.** Карта/roadmap **явно** называют `{tester, deployer}` как avoidable LLM control paths, явно отделяя их от `reviewer` (C, но keep — суждение не деривируемо) и от `analyst/architect/developer` (P — не control path). Набор проверяем (FR-6h/TC-14). 4. **Связь с классификацией.** Класс из FR-2 **выводится** из (C/P)-типа и деривируемости (а не наоборот). ## 4. Изменения API Нет. (Опциональная read-only наблюдаемость в `GET /queue`/`GET /metrics` — **вне скоупа** ORCH-118; если архитектор сочтёт полезным — отдельная аддитивная врезка, но не требуется этой задачей.) ## 5. Изменения схемы БД Нет. (Источник оценок экономии — существующие колонки `agent_runs`: `model`/`effort`/токены/стоимость/ время; только чтение.) ## 6. Требования к новым/изменённым QG checks Нет. `QG_CHECKS` / `check_*` / `_parse_*` / machine-verdict-ключи — **байт-в-байт**. Структурные тесты FR-6 (включая control-path TC-13/TC-14) — обычные `pytest`-тесты, **не** Quality Gate и **не** стадия. Политика LLM (FR-5) — нормативный документ, а не машинный гейт. ⚠️ Control-path-ось — **аналитическая разметка карты**, читающая `check_*` как ground-truth; она ничего в `src/qg/checks.py` не меняет. ## 7. Совместимость / регресс - **Docs + tests only:** рантайм `src/**` не меняется → нулевая регрессия; enduro-trails не затронут; kill-switch не нужен (нет рантайм-поведения), как в ORCH-077/079/101/102/103/011. - **Обратимость:** артефакты — документы и тесты; откат = удаление/правка docs (рантайм-риска нет). - **Анти-дрейф:** структурные тесты держат карту синхронной с кодом, включая control-path-ось (TC-13/TC-14); норматив сопровождения — «менял места вызова LLM или потребителя вердикта в `src/qg/checks.py` → обнови карту/разметку и политику в том же PR». - **Анти-фабрикация (R3):** артефакты фиксируют только проверяемые ссылки; follow-up'ы — по роли, без выдуманных Plane-ID (NFR-6). - **Self-hosting:** не деплоит/не рестартит прод/не трогает `main` — безопасно для общего инстанса.