Files
orchestrator/docs/work-items/ORCH-118/02-trz.md
claude-bot 70171eb1c1
All checks were successful
CI / test (push) Successful in 1m7s
analyst(ET): auto-commit from analyst run_id=726
2026-06-15 23:42:44 +03:00

235 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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` | анализ → 0104 | да (через 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` — безопасно для общего инстанса.