# ADR-001: Per-role floor для резолва `--effort`, устойчивый к пустому env

**Work Item:** ORCH-081 (ORCH-52h) · **Эпик:** ORCH-052 (после ORCH-074)
**Связанные:** ORCH-41 (резолв model/effort), ORCH-074 (валидация модели, `is_valid_model`)

## Статус
Accepted

## Контекст

В проде `resolve_agent_effort()` возвращает `''` для всех 6 агентов, хотя в
`src/config.py` заданы осмысленные дефолты (`high`/`medium`). Итог: флаг `--effort`
не передаётся в Claude CLI, каждый агент бежит на встроенном CLI-дефолте, а не на
заявленном уровне. Для Opus 4.8 reasoning-эффорт сильнее влияет на качество, чем у
прежних моделей, → прямой удар по предсказуемости качества всего конвейера (включая
enduro-trails из общего инстанса).

### Корень (точная механика)
Pydantic Settings трактует **присутствующую** env-переменную — даже пустую
(`ORCH_AGENT_EFFORT_DEVELOPER=` без значения) — как явное значение и **перебивает**
дефолт класса: поле `= ''`. В проде пусты И per-agent (`ORCH_AGENT_EFFORT_<ROLE>=`),
И default (`ORCH_AGENT_EFFORT_DEFAULT=`). Цепочка резолва (`_resolve_agent_attr`):

```
project-override (agent_efforts) → пусто
per-agent env ('') → falsy → skip
default ('') → falsy → skip
→ '' (уровень 4: без флага)
```

Привычный откат «per-agent пуст → взять default» не спасает: откатываться не на что —
default тоже пуст. Нужен непустой **per-role** «пол» (floor) ниже default.

### Дополнительное ограничение (урок 08.06)
Хост-правки env, положенные в git-managed файл, **не переживают деплой**. Источник
правды реальных значений — `.env` на хосте (gitignored). Значит, фикс обязан быть
**code-side robust**: даже если прод-`.env` снова окажется с пустыми
`ORCH_AGENT_EFFORT_*`, эффорт всё равно резолвится в целевые значения.

## Рассмотренные варианты

### Вариант A — `field_validator` в `config.py` (coerce пустой → дефолт на уровне поля)
Валидатор каждого `agent_effort_*` конвертирует пустую строку в канонический дефолт
поля.
**Отклонён:** ломает приоритет FR-2. Если per-agent поле всегда непустое, оно ВСЕГДА
бьёт `default` (уровень 3 становится мёртвым для роли с пустым env). Сценарий: оператор
ставит `ORCH_AGENT_EFFORT_DEFAULT=max`, per-agent оставляет пустыми — намерение «все
роли на max», но coercion на уровне поля даст каждой роли её per-role дефолт, а не
`max`. Floor обязан стоять **строго ниже** default, а это видно только в резолвере,
где доступна вся цепочка приоритетов.

### Вариант B — explicit hardcoded map `{analyst: high, …}` в `launcher.py`
Отдельная константа-карта per-role floor.
**Отклонён как первичный:** вводит **второй источник правды** рядом с дефолтами
`config.py`. Баг, который мы чиним, — это и есть дрейф/рассинхрон конфигурации;
заводить новую поверхность дрейфа концептуально неверно (карту и config надо вручную
держать в синхроне).

### Вариант C — floor в резолвере, значение = class-default поля (ПРИНЯТО)
Floor применяется как **последний** уровень в `resolve_agent_effort`, ниже `default`,
а его значение берётся из **декларированного class-default** соответствующего поля
`Settings` (через `model_fields`), который пустой env НЕ может перебить.

## Решение

Фикс кладётся в `resolve_agent_effort` (`src/agents/launcher.py`), `_resolve_agent_attr`
остаётся общим с model-резолвом и **не трогается** (floor — effort-специфичен).

### Цепочка резолва (новая, уровень 4 — floor)
```
1. project-override  (projects_json.agent_efforts[agent])   — непустой побеждает
2. per-agent env     (settings.agent_effort_<agent>)        — непустой побеждает
3. global default    (settings.agent_effort_default)        — непустой побеждает
4. per-role FLOOR    (class-default поля agent_effort_<agent>) — НОВОЕ, непустой пол
   ↓ (только если все 1–3 пусты)
5. валидация VALID_EFFORTS → невалидное дропается в '' (ORCH-41, never-break)
```

### Ключевые инварианты реализации
- **Floor = class-default поля, а не instance-значение.** `type(settings).model_fields[f"agent_effort_{agent}"].default` возвращает декларированный дефолт (`high`/`medium`/`xhigh`), который пустой env не клобберит. Это восстанавливает значение, которое pydantic дал бы, не будь спурьозного `VAR=`. **Единый источник правды — `config.py`**: developer-апгрейд на `xhigh` делается одной правкой поля, floor подтягивается автоматически.
- **Floor применяется ДО валидации и ТОЛЬКО при пустом резолве.** Порядок критичен для FR-3: явная опечатка (`turbo`) — непустая, поэтому floor НЕ применяется, и значение штатно дропается валидацией в `''`. Floor не маскирует мусор.
- **Floor — строго уровень 4 (ниже default).** Непустой явный env/override/`default` по-прежнему побеждает floor (FR-2). Floor срабатывает лишь когда сконфигурировать эффорт забыли/занулили на всех уровнях.
- **Unknown-agent fallback:** если поля `agent_effort_<agent>` нет (имя не из 6 ролей), floor деградирует на class-default `agent_effort_default` (`high`) — непустой безопасный пол, never-break.

### Сопутствующая правка config (FR-4)
`config.py`: `agent_effort_developer` `high → xhigh` (канон Opus 4.8: coding/agentic роль).
Это единственное изменение значений; остальные (`analyst/architect/reviewer=high`,
`tester/deployer=medium`) подтверждаются и фиксируются устойчиво. Поскольку floor =
class-default, апгрейд автоматически становится и новым floor для developer.

### Целевые значения (floor при полностью пустом env)
| agent | floor |
|-------|-------|
| analyst | high |
| architect | high |
| developer | **xhigh** |
| reviewer | high |
| tester | medium |
| deployer | medium |

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

**Плюсы**
- Code-side robust: пустой прод-`.env` больше не обнуляет эффорт; целевые уровни
  гарантированы без зависимости от хост-правок, которые не переживают деплой.
- Единый источник правды (`config.py`); нулевой риск дрейфа floor-карты.
- Приоритет резолва и контракт ORCH-41 сохранены 1:1; непустой явный конфиг работает
  как раньше (полная обратная совместимость).
- Валидация ORCH-41 не регрессирует — опечатки по-прежнему дропаются, never-break.

**Минусы / ограничения**
- Лёгкая зависимость от pydantic-v2 API (`model_fields[...].default`) — публичный
  стабильный атрибут, но это связь с внутренним устройством Settings. Замокать в тестах
  тривиально.
- «CLI-дефолт без флага» как исход для 6 штатных ролей становится недостижим — это
  намеренно: для известных ролей всегда есть непустой пол. Unknown-agent сохраняет
  безопасный непустой fallback.

**Не затрагивается**
- API endpoints — нет. Схема БД — нет. QG checks / гейты конвейера — нет.
  Model-резолв (ORCH-074) — нет. Путь проброса `--effort` в `_spawn` (стр. ~434) — нет
  (только верификация тестом, FR-3/FR-5).

## Деплой (self-hosting)
Правка касается инструмента, обслуживающего в проде и другие проекты. Прод-контейнер
`orchestrator` не ронять в рамках задачи; деплой — штатно `deploy-staging` (8501) →
`Confirm Deploy`. Рекомендуется привести прод-`.env` к каноне `.env.example`
(developer=xhigh, остальные непустые), НО фикс обязан работать и без этого (FR-1).
Проверка в проде (AC-6) фиксируется в `14-deploy-log.md`.
