---
work_item: ORCH-118
stage: architecture
author_agent: architect
status: accepted
created_at: 2026-06-15
model_used: claude-opus-4-8
---

# ADR-001: Карта LLM-консультаций, control-path-ось «avoidable» и roadmap детерминизации

Work Item: **ORCH-118** — replace avoidable LLM control paths with deterministic implementations
Стадия: **architecture**
Сквозная регистрация: **`docs/architecture/adr/adr-0047-llm-usage-policy-and-call-site-map.md`**
(решение кросс-каттинговое — вводит нормативную политику использования LLM для всего оркестратора и
снимок-карту, прибитую к коду тестами).

## Статус
Accepted

## Контекст

ORCH-118 — **зонтичная inventory/architecture-задача** (RCA-трек ORCH-114/117 и предшественники
110/111/112/113): корневым классом инцидентов было отсутствие **единого детерминированного владения**
у side-effectful и решающих control-path'ов — местами решение принималось LLM-агентом «потому что
удобно», хотя по сути это исполнение фиксированных команд и маппинг результата. Задача **не**
реализует детерминированные раннеры (это follow-up'ы); её выход — **доказательная карта** всех мест
вызова LLM + классификация + roadmap + нормативная политика, защищённые структурными тестами.

ТЗ (02-trz) оставляет **архитектору** решить «как»: структуру/размещение/формат документов карты,
схему классификации, дизайн структурных тестов, рекомендованный первый срез. Этот ADR это фиксирует.

**Факты, сверенные с кодом на момент задачи (ground-truth):**

- **Единственный транспорт LLM-консультации в `src/**`** — `src/agents/launcher.py::_spawn` (`def` —
  `launcher.py:472`; сборка CLI `f'{self.CLAUDE_BIN} --print … --system-prompt "$(cat {system_prompt})"'`
  — `launcher.py:610-614`; парс токенов из CLI-JSON — `_monitor_agent`, `launcher.py:838`). Им
  пользуются ровно **6 ролей** (`.openclaw/agents/{analyst,architect,developer,reviewer,tester,
  deployer}.md` — подтверждено `ls .openclaw/agents/`). **Иного LLM-транспорта нет:**
  `grep -rnE "import (anthropic|openai)|api\.anthropic\.com|/v1/messages" src/ watchdog/` → пусто;
  `CLAUDE_BIN` вне `_spawn` встречается только в `src/preflight.py` (проверка `os.path.exists`, **не**
  инференс) и `src/config.py:65` (литерал дефолта пути). Это критично для дизайна теста (D5).
- **Потребитель вывода каждой роли** (`src/qg/checks.py`, все `file:line` резолвятся):
  `check_analysis_complete:33` (наличие файлов), `check_architecture_done:62` (наличие 06-adr/07),
  `check_ci_green:82` + `check_branch_mergeable:657` (CI/merge), `check_reviewer_verdict:336`
  (`verdict:`), `check_tests_passed:182` → `_parse_tests_verdict:226` (`result:`),
  `check_staging_status:599` → `_parse_staging_status:538` (`staging_status:`),
  `check_deploy_status:473` → `_parse_deploy_status:413` (`deploy_status:`).
- **Перехват D1/D2 до `_spawn`:** `launch_job:377` возвращает рано для `agent=="deploy-finalizer"`
  (`launcher.py:389`) и `agent=="post-deploy-monitor"` (`launcher.py:394`) — код прямо помечает «Not an
  LLM spawn» (`launcher.py:407,428`). Слот агента занят, но консультации LLM нет — **рабочий прецедент
  детерминированной замены агента**.
- **Не-агентские control-path'ы уже детерминированы** (LLM-консультации не несут — подтверждено
  наличием модулей): `src/{stages,stage_engine,staging_verdict,self_deploy,error_classifier,
  frontmatter,serial_gate,merge_gate,coverage_gate,security_gate,post_deploy,transition_lease,
  reconciler,job_reaper}.py`. Их subprocess-вызовы (`git`/`pytest`/`docker`/`ssh`/сканеры) —
  детерминированные **инструменты**, а не LLM.

Без архитектурной фиксации «как» развести **три ортогональных факта** (транспорт/слот ≠ консультация
≠ control-path) и без нормативного определения «avoidable LLM control path» карта осталась бы
субъективной, а тесты — тривиальными (корень R4/R5-блокеров BRD).

## Решение

### Сводка

Фиксируем: (D1) набор и размещение durable-документов; (D2) схему записи инвентаря; (D3) три
ортогональных оси и **нормативное определение** «avoidable LLM control path»; (D4) таксономию и
правило **вывода** класса из осей с поимённой канонической таблицей ролей (= «фиксация карты»);
(D5) дизайн структурных анти-дрейф тестов; (D6) рекомендованный первый срез roadmap'а; (D7)
скоуп-гард. ORCH-118 — **docs + tests only**: `STAGE_TRANSITIONS` / реестр и имена `QG_CHECKS`/
`check_*` / machine-verdict-ключи / схема БД — **байт-в-байт не тронуты**; раннеры замен **не**
реализуются; конкретные follow-up Plane-ID **не** фиксируются (NFR-6).

### D1 — Набор и размещение документов (BR-1/BR-4/BR-5; FR-1/FR-4/FR-5; AC-1/AC-4/AC-5)

Три durable-документа размещаются в **`docs/architecture/`** (сквозные, переживают задачу, читаются
будущими follow-up'ами) — НЕ только в `docs/work-items/ORCH-118/`:

| Файл | Роль | Жизненный цикл |
|------|------|----------------|
| `docs/architecture/llm-call-sites.md` | **Карта** call-site'ов: инвентарь + control-path-разметка + классификация (D2/D3/D4) | **Снимок**, прибит тестами (D5); обновляется при дрейфе кода |
| `docs/architecture/llm-determinization-roadmap.md` | **Roadmap** замен: порядок, экономия, риски, первый срез (D6) | Транзиентный план; обновляется по мере закрытия follow-up'ов |
| `docs/architecture/llm-usage-policy.md` | **Политика**: принцип + критерии keep/replace через ось §0-bis + **определение «avoidable LLM control path»** (D3) | Нормативный durable-документ |

**Решение о разделении на 3 файла** (а не один): у них разные аудитория и жизненный цикл — карта
машинно-сверяется и есть снимок; roadmap транзиентен; политика нормативна и стабильна. Слияние
размыло бы тестируемость карты и стабильность политики.

> **Авторство.** Содержательное «как» (структура, поля, оси, классификация, дизайн тестов, первый
> срез) фиксирует **этот ADR** (он и есть «фиксация карты» по TRZ §2). Физическое создание трёх
> `docs/architecture/llm-*.md` + тест-файла + синхронизация golden-source (README/overview/CHANGELOG)
> — деливерабл **стадии development** строго по этому ADR. Канонические таблицы D3/D4 ниже —
> **source of truth**, которую развёрнутые документы и тесты **зеркалят** без расхождений.

### D2 — Схема записи инвентаря (BR-1/BR-8; FR-1/FR-8; AC-1)

Каждая строка карты несёт **обязательные поля** (порядок — нормативный, тест D5 проверяет наличие):

`id` · `location (file:line)` · `trigger` · `stage/owner` · `output artifact` · `machine-verdict key`
(если есть) · **`output consumer`** (`check_*`/`_parse_*` с `file:line` — кто потребляет вывод роли) ·
`est. tokens/runtime` (источник — `agent_runs`, помечено «оценка») · **`consults-LLM`** (consultation
vs LLM-capable transport/slot, §0.3 BRD) · **`axis`** (C control-path / P artifact-producer, §0-bis) ·
`classification` (D4) · `rationale` · `dependency` · `risk`.

Каждый `file:line` **обязан резолвиться** в реальный код (тест D5 точечно проверяет ключевые якоря).

**Машинно-читаемый якорь для тестов.** Карта несёт в `llm-call-sites.md` **канонический
markdown-блок** со стабильным заголовком таблицы (колонки `id | role | location | output_consumer |
consults_llm | axis | avoidable | classification`). Тест D5 парсит этот блок (split по `|`, без новых
зависимостей) и сверяет с кодом. Это держит документ человекочитаемым и одновременно
машинно-проверяемым (вместо хрупкого regex по прозе).

### D3 — Три ортогональных оси и нормативное определение «avoidable LLM control path» (BR-8/BR-9; FR-8; AC-10; NFR-7)

Карта/политика **явно** вводят три раздельных факта (их смешение — корень R3→R5-блокеров):

1. **Ось 1 — consultation ≠ transport/slot.** «LLM-консультация» = точка, где решение/артефакт
   конвейера **потребляет суждение LLM**. Транспорт (`_spawn`) — реализация, не определение. Слот
   агента (D1/D2 job-роли) делает site LLM-**capable**, но консультация гейтится потоком управления
   (перехват до `_spawn`) → capability ≠ consultation.
2. **Ось 2 — control-path (C) ≠ artifact-producer (P).** Определяется **кодом-потребителем**:
   - **(C) control-path** — LLM эмитит machine-verdict, на котором **ветвится `check_*`-гейт**
     (PASS→дальше / FAIL→откат). Суждение LLM входит в control flow.
   - **(P) artifact-producer** — LLM производит артефакт, а продвижение решает **детерминированный
     гейт**, судящий артефакт **независимо** (наличие файлов / CI). Суждение LLM в control flow не
     входит.
3. **Ось 3 — деривируемость вердикта.** Вердикт C-консультации либо есть **детерминированная функция
   tool-сигналов** (exit-code `pytest`/smoke/`staging_check.py`/деплоя), которые оркестратор **уже
   вычисляет сам**, либо требует **настоящего суждения**, не сводимого к exit-коду.

**Нормативное определение (фиксируется в `llm-usage-policy.md`):**

> Call-site — **avoidable LLM control path** ⟺ выполнены **оба** условия:
> **(i)** это **C**-консультация (её LLM-вердикт потребляется потоком управления — `check_*` ветвится
> на нём); **и** **(ii)** вердикт **деривируем** из tool-сигналов, которые оркестратор уже вычисляет
> → суждение LLM не добавляет информации → консультацию можно снять без потери смысла.

Это **двухбитный проверяемый предикат над `src/qg/checks.py`**, а не «удобство на глаз».

### D4 — Таксономия и каноническая классификация (= «фиксация карты») (BR-2; FR-2; AC-2)

Четыре взаимоисключающих класса; класс **выводится** из осей D3 (а не постулируется):

- `keep-LLM` — нужно настоящее суждение (обязательно **назвать** конкретное суждение).
- `replace-deterministic-now` — безопасная детерминированная замена сейчас.
- `replace-later/risky` — замена возможна позже / с предпосылками.
- `needs-hybrid-fallback` — детерминированное ядро + LLM-фолбэк только на суждение.

**Правило вывода:** `P → keep-LLM`; `C + не-деривируемый вердикт → keep-LLM`; `C + деривируемый
вердикт → replace-* / needs-hybrid-fallback (= avoidable)`.

**Каноническая таблица (source of truth; карта и тесты зеркалят её байт-в-смысл):**

| id | Роль | Транспорт/слот | Потребитель вывода (`src/qg/checks.py`) | Ось | Avoidable LLM control path? | Класс | Названное суждение (для keep) |
|----|------|----------------|------------------------------------------|-----|------------------------------|-------|-------------------------------|
| S0 | `_spawn` | транспорт | — | — | — (транспорт) | — | — |
| A1 | analyst | да (через S0) | `check_analysis_complete:33` (наличие файлов) | **P** | нет | `keep-LLM` | анализ требований, BRD/ТЗ — суждение |
| A2 | architect | да (через S0) | `check_architecture_done:62` (наличие 06-adr/07) | **P** | нет | `keep-LLM` | архитектурное решение/ADR — суждение |
| A3 | developer | да (через S0) | `check_ci_green:82` + `check_branch_mergeable:657` (CI/merge) | **P** | нет | `keep-LLM` | написание кода — суждение |
| A4 | reviewer | да (через S0) | `check_reviewer_verdict:336` (`verdict:`) | **C** | **нет** (вердикт НЕ деривируем) | `keep-LLM` | «приемлемость кода/решения» — не сводится к exit-коду |
| A5 | tester | да (через S0) | `check_tests_passed:182`→`_parse_tests_verdict:226` (`result:`) | **C** | **ДА** | `needs-hybrid-fallback` | (ядро детерминировано; LLM — триаж падений / маппинг TC↔критерии) |
| A6 | deployer | да (через S0) | `check_staging_status:599`→`_parse_staging_status:538` (`staging_status:`); `check_deploy_status:473`→`_parse_deploy_status:413` (`deploy_status:`) | **C** | **ДА** | `replace-deterministic-now` | (вердикт = `staging_check.py`/exit-code; прод уже детерминирован Phase A/B/C ORCH-036) |
| D1 | deploy-finalizer | слот, перехват до `_spawn` (`launcher.py:389`) | — | — | — (уже детерминирован) | `already-deterministic` (эталон) | — |
| D2 | post-deploy-monitor | слот, перехват до `_spawn` (`launcher.py:394`) | — | — | — (уже детерминирован) | `already-deterministic` (эталон) | — |

**Итог (поимённо, проверяется тестами D5):** `avoidable LLM control paths = {tester, deployer}`;
control-path-но-keep = `{reviewer}`; не-control-path (P) = `{analyst, architect, developer}`;
already-deterministic-эталон = `{deploy-finalizer, post-deploy-monitor}`.

> **Уточнение по deployer (точность карты).** Роль `deployer` охватывает два ребра. На `deploy-staging`
> (`staging_status:`) её вердикт — чистый маппинг exit-кода `staging_check.py` → `replace-deterministic
> -now`. На `deploy` (`deploy_status:`) для self-hosting `orchestrator` вердикт **уже** производит
> детерминированный finalizer (Phase C, ORCH-036), LLM в критическом self-restart-пути нет; для прочих
> репо deployer-агент делает синхронный ssh-деплой. Поэтому «чисто деривируемый» срез deployer'а —
> прежде всего **staging-status** (см. D6).

### D5 — Дизайн структурных анти-дрейф тестов (BR-6; FR-6; AC-6)

Новый offline-файл `tests/test_llm_call_site_inventory.py` (без сети/LLM/subprocess-к-модели; маркер
`# ORCH-118` в шапке — TRACEABILITY). Дискриминатор всех проверок — **«консультирует LLM», а не
«спавнит subprocess»**.

- **(a) Единственный транспорт.** В `src/**` ровно одна точка сборки/запуска Claude CLI — матчинг по
  **конъюнкции** признаков LLM-транспорта (`CLAUDE_BIN` **и** `--system-prompt` **и** `Popen`/`bash -c`
  в одном месте), и это `launcher._spawn`. ⚠️ Конъюнкция обязательна: bare-`CLAUDE_BIN` дал бы
  false-positive на `preflight.py` (existence-check) и `config.py` (литерал пути) — они **не**
  консультируют (см. Контекст). Allowlist единственного транспорта = `_spawn`.
- **(f) Отсутствие иного LLM-транспорта.** В `src/**`+`watchdog/**` нет импорта `anthropic`/`openai`/
  LLM-SDK, нет прямого HTTP-эндпоинта Anthropic/Claude (`api.anthropic.com`, `/v1/messages`), нет
  второго model-invoking subprocess-сборщика. *(a)+(f) вместе = транспорт-агностичный двусторонний
  инвариант.*
- **(b) Нет консультации в детерминированных путях.** Перечисленные модули D-списка и обработчики
  D1/D2 не содержат LLM-транспорта (ни `_spawn`, ни (f)).
- **(c) Промпты ↔ файлы.** Карта перечисляет **ровно** те 6 промптов, что физически лежат в
  `.openclaw/agents/` (двусторонняя сверка с `glob`).
- **(d) Тотальность.** Каждый перечисленный в карте call-site классифицирован **ровно один раз**.
- **(e) Capability ≠ consultation.** `launch_job` перехватывает `deploy-finalizer`/`post-deploy-monitor`
  **до** `_spawn` (assert по `launcher.py` — наличие ранних return-веток до точки spawn).
- **(g) Control-path-разметка верна (TC-13).** Из машинного блока карты (D2) извлекается `role→axis`;
  тест сверяет: P-роли потребляются `check_analysis_complete`/`check_architecture_done`/`check_ci_green`,
  C-роли — `check_reviewer_verdict`/`_parse_tests_verdict`/`_parse_staging_status`/`_parse_deploy_status`
  (наличие этих `def` в `src/qg/checks.py` как ground-truth). Дрейф разметки → красный.
- **(h) Avoidable-набор зафиксирован (TC-14).** Множество avoidable из карты = ровно `{tester,
  deployer}`; `reviewer` = control-path-keep; `analyst`/`architect`/`developer` = не control path.

> ❌ **Не вводить** тест, прибивающий карту к конкретным follow-up Plane-ID → ✅ только инварианты,
> резолвящиеся в код/файлы репозитория (R3/NFR-6). Тесты — обычный `pytest`, **не** Quality Gate и
> **не** стадия (FR-6 §QG).

### D6 — Рекомендованный первый срез roadmap'а (BR-4; FR-4; AC-4)

**Первый срез = deployer (staging-status).** Обоснование (самый низкорисковый «чисто деривируемый»
control path):

1. Вердикт — **чистый маппинг** exit-кода `scripts/staging_check.py` → `staging_status:` (уже есть
   leaf `src/staging_verdict.py` с `compute_staging_verdict`, ORCH-061) — деривируемость максимальна.
2. **Прод уже детерминирован** (Phase A/B/C, ORCH-036) → срез не трогает критический self-restart-путь
   → минимальная поверхность риска.
3. Опирается на **существующий прецедент** D1/D2 (`launch_job`-перехват до `_spawn`) — архитектурный
   риск замены снижен (BRD §6).
4. `replace-deterministic-now`, без потребности в hybrid-fallback (в отличие от tester).

**Порядок roadmap'а:** (1) **deployer-замена** (staging-маппинг; prod уже детерминирован) →
(2) **tester-гибрид** (детерминированное ядро `pytest`+smoke + LLM-фолбэк на триаж падений / маппинг
TC↔критерии — `needs-hybrid-fallback`). Для каждого кандидата roadmap несёт: зависимости, **оценку**
экономии токенов/времени из `agent_runs` (помечено «оценка до фактического замера», NFR-5), риск
безопасности, потребность в hybrid-fallback, ожидание kill-switch/обратимости. Кандидаты названы
**по роли**; конкретный Plane-ID **не** фиксируется (NFR-6) — заводится при создании задачи.

### D7 — Скоуп-гард и инварианты (BR-7; FR-7; NFR-1/NFR-3; AC-7/AC-9)

- **Docs + tests only.** Диф не меняет `STAGE_TRANSITIONS` / реестр и имена `QG_CHECKS`/`check_*` /
  machine-verdict-ключи (`verdict:`/`result:`/`staging_status:`/`deploy_status:`/`security_status:`/
  `coverage_status:`) / схему БД. В `src/**` нет нового детерминированного раннера tester/deployer.
- **Анти-фабрикация.** Ни один артефакт не фиксирует конкретный follow-up Plane-ID; все ссылки
  резолвятся в репозиторий. Тест не пинит карту к follow-up ID.
- **Self-hosting.** Только чтение кода + запись docs/tests — не деплоит, не рестартит прод, не трогает
  `main`/force-push, без процессов/сети. kill-switch не нужен (нет рантайм-поведения), как
  ORCH-077/079/101/102/103/011.
- **Наблюдаемость в `GET /queue`/`GET /metrics`** — **вне скоупа** (TRZ §4); карта/политика —
  документы, не рантайм.

### Применимость 07/08
- **07-infra-requirements — N/A:** топология не меняется (нет нового сервиса/контейнера/порта/маунта).
- **08-data-requirements — N/A:** схема БД не меняется; `agent_runs` читается только для оценок (NFR-5).

## Альтернативы

- **Один объединённый документ (карта+roadmap+политика)** — отвергнуто: разные жизненные циклы и
  тестируемость (D1); снижает стабильность нормативной политики и проверяемость снимка-карты.
- **Размещение карты только в `docs/work-items/ORCH-118/`** — отвергнуто: документ сквозной и durable,
  читается будущими follow-up'ами; work-item-папка — неверная альтитуда (теряется при навигации по
  архитектуре).
- **Тест по сырому regex прозы карты** — отвергнуто как хрупкое: дрейф формулировок ломал бы тест без
  смыслового дрейфа. Выбран машинный markdown-блок (D2/D5g).
- **Тест-матчинг по bare-`CLAUDE_BIN`** — отвергнуто: false-positive на `preflight.py`/`config.py`
  (capability/литерал, не консультация). Выбрана конъюнкция признаков транспорта (D5a).
- **Фиксация follow-up Plane-ID (`ORCH-115`/`ORCH-116`)** — отвергнуто нормативно (NFR-6, корень
  отклонённой ревизии R2): этих work item нет; кандидаты — по роли.
- **Первый срез = tester** — отвергнуто: tester требует hybrid-fallback (триаж падений), поверхность
  риска и объём больше; deployer-staging — чище деривируем и лучше обеспечен прецедентом (D6).
- **Включить наблюдаемость карты в `GET /queue`** — отвергнуто как scope-creep (TRZ §4): карта —
  документ, а не рантайм-состояние.

## Последствия

- **+** Доказательная, код-привязанная карта разводит транспорт/слот ≠ консультация ≠ control-path и
  даёт **проверяемый** предикат «avoidable» → закрывает блокеры R3→R5; follow-up'ы выполняются
  предсказуемо.
- **+** Нормативная политика делает «LLM только там, где нужно суждение» инвариантом любой будущей
  правки control-path'а; защищает автономность (NFR-2).
- **+** Структурные тесты держат карту синхронной с кодом (включая control-path-ось) — анти-дрейф.
- **−** Карта — **снимок**: при эволюции `src/qg/checks.py` (смена потребителя / новая роль) тесты
  D5g/h станут красными — требуется обновлять карту/политику в том же PR. *Митигейшн:* это
  **запланированное** свойство (норматив сопровождения), а не дефект; тест указывает точку дрейфа.
- **−** Машинный блок карты вводит лёгкую форматную дисциплину (стабильный заголовок таблицы).
  *Митигейшн:* формат человекочитаемый, документирован в D2; парсер — stdlib split.
- **Откат:** удаление/правка трёх `docs/architecture/llm-*.md` + тест-файла + секции README. Рантайм
  не затронут (риска нет).

## Ссылки
- BRD: `docs/work-items/ORCH-118/01-brd.md` (§0 / §0-bis / BR-1…BR-9 / NFR-1…NFR-7)
- TRZ: `docs/work-items/ORCH-118/02-trz.md` (FR-1…FR-8 / §2 таблица модулей)
- Acceptance: `docs/work-items/ORCH-118/03-acceptance-criteria.md` (AC-1…AC-10)
- Tech-risks: `docs/work-items/ORCH-118/10-tech-risks.md`
- Сквозной ADR: `docs/architecture/adr/adr-0047-llm-usage-policy-and-call-site-map.md`
- Сверено по коду: `src/agents/launcher.py` (`_spawn:472`, CLI `610-614`, `launch_job:377`,
  перехват `389/394`, «Not an LLM spawn» `407/428`), `src/qg/checks.py`
  (`33/62/82/336/182/226/599/538/473/413/657`), `.openclaw/agents/*.md` (6 промптов),
  `src/{staging_verdict,self_deploy,frontmatter,...}.py` (детерминированные leaf'ы)
- Прецедент детерминированной замены агента: ORCH-036 (self-deploy Phase A/B/C), D1/D2 `launch_job`
- Прецедент docs+tests-only задач: ORCH-077/079/101/102/103/011
</content>
</invoke>
