diff --git a/.env.example b/.env.example index 980a53f..9fb8d2e 100644 --- a/.env.example +++ b/.env.example @@ -37,12 +37,15 @@ ORCH_AGENT_MODEL_DEVELOPER= ORCH_AGENT_MODEL_REVIEWER= ORCH_AGENT_MODEL_TESTER= ORCH_AGENT_MODEL_DEPLOYER= -# Effort split: thinking agents (analyst/architect/developer/reviewer) -> high; -# mechanical agents (tester/deployer) -> medium. +# Effort split (ORCH-081/ORCH-52h): thinking agents (analyst/architect/reviewer) +# -> high; developer -> xhigh (coding/agentic role, Opus 4.8 canon); mechanical +# agents (tester/deployer) -> medium. NB: an empty ORCH_AGENT_EFFORT_*= no longer +# zeroes the effort — the launcher falls back to a per-role floor (= the config.py +# class-default) so each role still runs at its canonical level (ORCH-081). ORCH_AGENT_EFFORT_DEFAULT=high ORCH_AGENT_EFFORT_ANALYST=high ORCH_AGENT_EFFORT_ARCHITECT=high -ORCH_AGENT_EFFORT_DEVELOPER=high +ORCH_AGENT_EFFORT_DEVELOPER=xhigh ORCH_AGENT_EFFORT_REVIEWER=high ORCH_AGENT_EFFORT_TESTER=medium ORCH_AGENT_EFFORT_DEPLOYER=medium diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e65e6c..377131e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ Формат: [Keep a Changelog](https://keepachangelog.com/). Записи — на смысловой PR/задачу. ## [Unreleased] +- **Устойчивость резолва `--effort` к пустому env + developer → `xhigh`** (ORCH-081/ORCH-52h): фикс конфигурационного бага, из-за которого в проде `resolve_agent_effort()` возвращал `''` для всех 6 агентов и `--effort` не передавался в Claude CLI (каждый агент бежал на встроенном CLI-дефолте вместо заявленного уровня — прямой удар по предсказуемости качества всего конвейера, включая enduro-trails из общего инстанса). **Корень:** pydantic Settings трактует ПРИСУТСТВУЮЩУЮ env-переменную, даже пустую (`ORCH_AGENT_EFFORT_*=` без значения), как явное `''` и перебивает class-default; в проде пусты И per-agent, И `agent_effort_default`, поэтому у цепочки резолва (`_resolve_agent_attr`: project-override → per-agent env → default → `''`) не остаётся непустого «пола» для отката. **Фикс (вариант c, ADR-001):** в `resolve_agent_effort` (`src/agents/launcher.py`) добавлен уровень 4 — непустой **per-role floor** ниже `default`: новый чистый helper `_agent_effort_floor(agent)` возвращает декларированный class-default поля `agent_effort_` через `type(settings).model_fields[...].default` (значение, которое пустой env перебить НЕ может). Floor срабатывает ТОЛЬКО когда уровни 1–3 пусты и применяется ДО валидации, поэтому: (а) при пустом прод-`.env` каждая роль получает СВОЙ канонический уровень (developer=`xhigh`, tester/deployer=`medium`, analyst/architect/reviewer=`high`), а не общий default; (б) явная опечатка (`turbo`/`ultra`) непуста → floor НЕ применяется → значение штатно дропается валидацией `VALID_EFFORTS` в `''` (never-break ORCH-41 не регрессирует, floor не маскирует мусор); (в) непустой явный env/project-override/`default` по-прежнему ПОБЕЖДАЕТ floor (приоритет резолва сохранён 1:1). Unknown-agent (имя вне 6 ролей) деградирует на class-default `agent_effort_default` (`high`) — безопасный непустой пол. **`config.py`:** `agent_effort_developer` `high → xhigh` (канон Opus 4.8: coding/agentic роль) — единственное изменение значений; floor подтягивает его автоматически (единый источник правды, ноль риска дрейфа floor-карты). Инварианты НЕ менялись: приоритеты/сигнатуры резолва ORCH-41, `_resolve_agent_attr` (общий с model-резолвом, не тронут), `resolve_agent_model` (ORCH-074), путь проброса `--effort` в `_spawn`, `VALID_EFFORTS`, API, схема БД (без миграций). ADR `docs/work-items/ORCH-081/06-adr/ADR-001-effort-resolution-floor.md`. Документация: `docs/architecture/README.md` (таблица «модель/эффорт по ролям»: developer `xhigh` + ремарка про floor), `.env.example` (`ORCH_AGENT_EFFORT_DEVELOPER=xhigh` + комментарий split/floor). Тесты: `tests/test_resolve_agent_effort.py` (TC-01..08: канон-дефолты, floor при пустом env per-role, floor-не-маскирует-typo, приоритет, `xhigh∈VALID_EFFORTS`, сборка флага `--effort xhigh`/`--effort medium`). - **Убран мёртвый frontmatter `model:` + валидация имени модели (never-break)** (ORCH-074): закрыты два дефекта данных/валидации каркаса выбора модели агентов (ORCH-41), без изменения механизма резолва, API или схемы БД. **G1 — мёртвый frontmatter:** из YAML-frontmatter всех 6 промптов `.openclaw/agents/*.md` удалена строка `model:` (`claude-sonnet-4-6` у analyst/developer/tester/deployer, `claude-opus-4-7` у architect/reviewer). launcher НЕ читал frontmatter `model:` — это была лживая/мёртвая декларация, противоречащая реально используемой модели (config) и принципу «документация = golden source»; мина: если бы кто-то «починил» launcher читать frontmatter, все агенты молча уехали бы на устаревшие модели. config (`agent_model_*`/`agent_model_default`) остаётся единственным источником правды; frontmatter описательный. **G2 — валидация имени модели:** добавлен чистый helper `is_valid_model(name)` + `_MODEL_NAME_RE` (`^claude-[a-z0-9.-]+$`) рядом с `VALID_EFFORTS` в `src/agents/launcher.py`. Резолвенное имя модели валидируется ПЕРЕД попаданием в `--model`: невалидное (опечатка, `gpt-4`, пустое, неверный префикс) → `logger.warning` + откат на следующий валидный уровень каскада ORCH-41 (project-override → env → default), в пределе → `""` (без флага `--model`, CLI-дефолт). Никогда не возвращается мусор и не бросается исключение (never-break, поведенческая аналогия `resolve_agent_effort`/`VALID_EFFORTS`). Выбран **формат-чек, а не allowlist `VALID_MODELS`**: allowlist воссоздаёт ровно ту мину, что убивается в G1 (статичный список врёт при устаревании — молча дропнул бы корректную будущую `claude-opus-4-9`); формат-чек forward-compatible (новые `claude-*` проходят без правки кода), финальный авторитет о существовании модели — сам Claude CLI. Тот же предикат применён к inline-чтению `--fallback-model` (`agent_fallback_model` читается напрямую в `_spawn`, мимо `resolve_agent_model` — TRZ §4), поэтому опечатка в `ORCH_AGENT_FALLBACK_MODEL` тоже дропается с warning; для текущего пустого значения регрессии нет. **G4 (fallback) НЕ включён** (`agent_fallback_model=""`, AC-5 N/A) — ради детерминизма (все агенты на `claude-opus-4-8`); **G3 (routing) НЕ включён** (AC-4 N/A) — осознанное решение стейкхолдера (Слава 08.06). Реализация `resolve_agent_model` рефакторнута на генератор кандидатов `_agent_model_candidates` (тот же приоритет ORCH-41) + валидация-со-скипом. Инварианты НЕ менялись: приоритеты/сигнатуры резолва ORCH-41, структура CLI-команды `_spawn`, `VALID_EFFORTS`-гард эффорта, `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, схема БД (без миграций); enduro per-project override валидные имена проходят без изменения поведения. ADR `docs/work-items/ORCH-074/06-adr/ADR-001-model-name-validation.md`. Документация: `docs/architecture/README.md` (таблица «модель/эффорт по ролям» + валидация), `CLAUDE.md`, `.env.example` (блок `ORCH_AGENT_MODEL_*`/`ORCH_AGENT_EFFORT_*`/`ORCH_AGENT_FALLBACK_MODEL`). Тесты: `tests/test_agent_frontmatter_no_model.py` (G1: TC-01/02), `tests/test_resolve_agent_model.py` (G2 never-break: TC-03..09, TC-11 + is_valid_model). - **Управление зависимостями задач (B ждёт A) + сериализация мержа одного репо** (ORCH-026): два уровня по ADR-001, оба условны (kill-switch + CSV-область, never-raise), без новой стадии и без изменения `STAGE_TRANSITIONS`/реестра `QG_CHECKS`. **Уровень A — сериализация merge/deploy внутри одного репо:** переиспользует существующий merge-lease ORCH-043/065 (никакого нового механизма); единственная новая логика — **безусловный pre-merge rebase**: в `check_branch_mergeable` (`src/qg/checks.py`) под удержанным лизом при флаге `premerge_rebase_always` (дефолт `True`) `auto_rebase_onto_main` вызывается **всегда** (а не только при `branch_is_behind_main`) — детерминированный структурный анти-фантом на ребре планировщика, дополняющий рубежи ORCH-073. На актуальной ветке это no-op (rebase не сдвигает HEAD, `push --force-with-lease` → «Everything up-to-date», CI не триггерится); kill-switch `premerge_rebase_always=False` → прежнее поведение ORCH-043 1:1. Окно сериализации «merge → main-updated» per-repo (для self `done` ⇔ SHA-in-main, ORCH-073): пока A не в `main`, B того же репо получает `merge-lock busy` → defer (не откат); кросс-репо параллелизм сохранён (лиз — per-repo файл). **Уровень B — декларативные зависимости задач:** аддитивная таблица `job_deps(task_id, depends_on_task_id)` (идемпотентный `CREATE TABLE/INDEX IF NOT EXISTS` в `init_db`, без миграции на живой БД); гейт планировщика в `claim_next_job` (`src/db.py`) — `NOT EXISTS (job_deps d JOIN tasks t … WHERE d.task_id=jobs.task_id AND t.stage!='done')` при `task_deps_enabled`: задача с незавершённой зависимостью **не выбирается** и слот `max_concurrency` не занимает; инертно при пустой `job_deps` → нулевая регрессия, kill-switch `task_deps_enabled=False` → запрос 1:1 как ORCH-1. Новый leaf-модуль `src/task_deps.py` (контракт never-raise): `is_task_ready` (fail-open → ready), DFS-детектор циклов (`detect_cycle`/`find_any_cycle`, итеративный WHITE/GREY/BLACK), `handle_cycle` (`set_issue_blocked` по каждой задаче цикла + один Telegram-alert с цепочкой «A → B → A»), `declare_dependency` (вставка + детект цикла), `ingest_plane_relations` (только для `task_deps_source=plane|hybrid`: резолв Plane `blocked-by` UUID → локальный task → запись в `job_deps`; источник истины горячего цикла остаётся БД, дефолт `db` НЕ ходит в сеть на claim), `snapshot` (read-only сводка). Видимость: строка «⏳ ждёт ORCH-NNN» в Telegram-карточке (`src/notifications.py`, never-raise, инвариант «одна карточка на задачу» сохранён); блок `task_deps` в `GET /queue` (`src/main.py`). Совместимость: `reconciler` F-1 пропускает dep-заблокированные задачи (`is_task_ready`, паттерн ORCH-060) + backstop-детект цикла; `job_reaper` сканирует только `running` → dep-блок остаётся `queued`. Зависимости — только intra-repo (v1). Новые настройки: `ORCH_PREMERGE_REBASE_ALWAYS` (true), `ORCH_TASK_DEPS_ENABLED` (true), `ORCH_TASK_DEPS_SOURCE` (db). Инварианты НЕ менялись: `STAGE_TRANSITIONS`, реестр `QG_CHECKS` (гейт зависимостей — врезка в `claim_next_job`, НЕ зарегистрированный QG), схема `tasks`/`jobs`/`agent_runs`, внешние HTTP-эндпоинты; non-self (enduro) — no-op при пустых `job_deps`/области. ADR `docs/work-items/ORCH-026/06-adr/ADR-001-merge-serialization-and-task-deps.md`, глобальный `docs/architecture/adr/adr-0015-task-deps-and-merge-serialization.md`. Документация: `docs/architecture/README.md`, `CLAUDE.md`, `.env.example`. Тесты: `tests/test_orch026_premerge_rebase.py`, `tests/test_orch026_merge_serialize.py`, `tests/test_orch026_conditionality.py`, `tests/test_orch026_task_deps.py`, `tests/test_orch026_dep_cycles.py`, `tests/test_orch026_dep_visibility.py`, `tests/test_orch026_migration.py`, `tests/test_orch026_queue_observability.py`, `tests/test_orch026_serialize_integration.py`, `tests/test_orch026_deps_integration.py`. - **CRIT: системный фикс эрозии `main` — SHA-в-main как единственный критерий merge-verify + регресс-гард + `.gitattributes`** (ORCH-073): устранён корень фантомного merge, из-за которого код задач ORCH-067 (`plane_issue_link`) и ORCH-069 (`qg0_title_max`) дошёл до `done`, но физически отсутствовал в `origin/main` (в `main` попадали только их авто docs-PR). **(FR-1)** `merge_gate.verify_merged_to_main` подтверждает merge **ТОЛЬКО** прямым фактом `git merge-base --is-ancestor origin/main` — OR-ветка `pr_already_merged` удалена (merged PR больше не подтверждает merge); пустой SHA / git-ошибка → `False` (fail-closed, never-raise). **(FR-2)** `pr_already_merged` понижен до idempotency-guard для `merge_pr` и засчитывает PR лишь при `merged & head.ref== & base.ref=="main"` (явный in-loop фильтр вместо ненадёжного query-параметра `head` — исключает авто docs-PR). **(FR-3)** `merge_pr` выбирает open code-PR строго по `head.ref==` И `base.ref=="main"`; merge только через Gitea PR-merge API, никогда push/force-push в `main`. **(FR-5)** новый детерминированный регресс-гард `merge_gate.check_main_regression` в `_handle_merge_verify` ПОСЛЕ подтверждённого SHA-в-main и ДО `done` проверяет, что `origin/main` содержит декларативный append-only набор маркеров ранее-merged задач (`MAIN_REGRESSION_MARKERS`, `git grep -c origin/main -- `); детерминированный `count==0` → alert «main regressed» + HOLD (`set_issue_blocked` + Telegram + Plane, задача НЕ `done`, БЕЗ авто-отката на `development`), git-ошибка самого грепа → fail-OPEN (не блокирует, SHA-в-main остаётся первичным гейтом). Kill-switch `ORCH_REGRESSION_GUARD_ENABLED` (дефолт `true`), область — `merge_verify_applies` (self-hosting / `merge_verify_repos`), non-self → no-op. **(FR-4)** корневой `.gitattributes` с `CHANGELOG.md merge=union` — правки `## [Unreleased]` авто-сливаются при `auto_rebase_onto_main` без конфликта (обе записи сохраняются), ветка не откатывается в `development` и не тащит устаревший код-сосед; `docs/**` под union НЕ ставится. `GET /queue::merge_verify_status` дополнен счётчиком `main_regressed_alerts_total` (read-only). Инварианты НЕ менялись: `STAGE_TRANSITIONS`, реестр `QG_CHECKS` (под-гейт — врезка в `advance_stage`), `check_deploy_status`/`_parse_deploy_status`, merge-gate, image-freshness, схема БД, внешние HTTP-эндпоинты; non-self (enduro) merge/verify/гард — no-op (INV-5); ручной `Confirm Deploy` сохранён. ADR `docs/work-items/ORCH-073/06-adr/ADR-001-merge-verify-sha-truth-and-regression-guard.md` (+ сквозной `adr-0014`). Документация: `docs/architecture/README.md`, `.env.example`. Тесты: `tests/test_orch073_*.py` (TC-01..18). diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 0020af1..7bb26b9 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -42,13 +42,13 @@ created → analysis → architecture → development → review → testing → **Канон гейтов:** машинные вердикты читаются ТОЛЬКО из YAML-frontmatter, никогда из прозы. Лог-файлы мержатся в `origin/main` отдельным PR; гейт читает из `origin/main`. ### Модель и эффорт по ролям (ORCH-41, валидация ORCH-74) -Модель и `--effort` каждого агента берутся из config (`src/config.py`), резолвятся `launcher.resolve_agent_model` / `resolve_agent_effort` по приоритету **project-override (`projects_json` `agent_models`/`agent_efforts`) > `ORCH_AGENT_MODEL_`/`ORCH_AGENT_EFFORT_` > `*_default` > CLI-дефолт (без флага)**. frontmatter `model:` в `.openclaw/agents/*.md` **удалён** (ORCH-74 G1) — он был мёртвой/лживой декларацией (launcher его не читает); config — единственный источник правды о модели. Model-routing (G3) НЕ включён — все 6 агентов на `claude-opus-4-8`. +Модель и `--effort` каждого агента берутся из config (`src/config.py`), резолвятся `launcher.resolve_agent_model` / `resolve_agent_effort` по приоритету **project-override (`projects_json` `agent_models`/`agent_efforts`) > `ORCH_AGENT_MODEL_`/`ORCH_AGENT_EFFORT_` > `*_default` > CLI-дефолт (без флага)**. **Эффорт (ORCH-081):** ниже `*_default` добавлен непустой **per-role floor** — class-default поля `agent_effort_` из `config.py` (его пустой env перебить не может). Floor — строго последний уровень (ниже default) и срабатывает ТОЛЬКО когда все уровни пусты, поэтому пустые прод-`ORCH_AGENT_EFFORT_*=` (которые pydantic трактует как явное `''` и обнуляют дефолт) больше не приводят к запуску без `--effort`: каждая роль получает свой канонический пол (developer=`xhigh`, tester/deployer=`medium`, прочие=`high`). Непустой явный конфиг по-прежнему побеждает floor; опечатка вне `VALID_EFFORTS` дропается валидацией ДО floor (never-break, не маскируется). См. `docs/work-items/ORCH-081/06-adr/ADR-001-effort-resolution-floor.md`. frontmatter `model:` в `.openclaw/agents/*.md` **удалён** (ORCH-74 G1) — он был мёртвой/лживой декларацией (launcher его не читает); config — единственный источник правды о модели. Model-routing (G3) НЕ включён — все 6 агентов на `claude-opus-4-8`. | Агент | Модель | Эффорт | |-------|--------|--------| | analyst | claude-opus-4-8 | high | | architect | claude-opus-4-8 | high | -| developer | claude-opus-4-8 | high | +| developer | claude-opus-4-8 | xhigh | | reviewer | claude-opus-4-8 | high | | tester | claude-opus-4-8 | medium | | deployer | claude-opus-4-8 | medium | diff --git a/docs/work-items/ORCH-081/00-business-request.md b/docs/work-items/ORCH-081/00-business-request.md new file mode 100644 index 0000000..855414d --- /dev/null +++ b/docs/work-items/ORCH-081/00-business-request.md @@ -0,0 +1,7 @@ +# Business Request: ORCH-52h: эффорт агентов резолвится в пустую строку в проде (env перебивает config) + +Work Item ID: ORCH-081 + +## Description + +TBD diff --git a/docs/work-items/ORCH-081/01-brd.md b/docs/work-items/ORCH-081/01-brd.md new file mode 100644 index 0000000..29ff204 --- /dev/null +++ b/docs/work-items/ORCH-081/01-brd.md @@ -0,0 +1,82 @@ +# 01 — BRD: ORCH-081 (ORCH-52h) + +**Work Item:** ORCH-081 +**Эпик:** ORCH-052 (продолжение ORCH-52a / ORCH-074) +**Тип:** Багфикс (конфигурация эффорта агентов) +**Приоритет:** HIGH +**Repo:** orchestrator (self-hosting) + +## 1. Контекст и проблема + +При проверке ORCH-074 (08.06) обнаружено: `resolve_agent_effort()` для **всех 6 агентов +в проде** возвращает пустую строку `''`, хотя в `src/config.py` заданы осмысленные +дефолты (`agent_effort_default="high"`, per-agent `high`/`medium`). Итог: флаг +`--effort` **не передаётся** в Claude CLI, и каждый агент бежит на встроенном +CLI-дефолте эффорта, а **не** на заявленном `high`/`medium`. + +### Корень (диагностика) +В проде env-переменные `ORCH_AGENT_EFFORT_DEFAULT` и +`ORCH_AGENT_EFFORT_{ANALYST,ARCHITECT,DEVELOPER,REVIEWER,TESTER,DEPLOYER}` выставлены в +**пустую строку** (`VAR=` без значения). Pydantic Settings трактует присутствующую +env-переменную (даже пустую) как явное значение и **перебивает** дефолт класса: +`agent_effort_* = ''`. В цепочке резолва (`launcher._resolve_agent_attr`): +- per-agent `''` → falsy → пропуск (уровень 2); +- default `''` → falsy → пропуск (уровень 3); +- → возврат `''` (уровень 4, «без флага»). + +Поскольку **и default тоже пуст**, привычный откат «per-agent пуст → взять default» +не спасает: откатываться не на что. Это ключевой нюанс — фикс обязан давать каждой +роли непустой «пол» (floor) даже когда И per-agent, И default env пусты. + +## 2. Бизнес-ценность / зачем важно + +Для Opus 4.8 (канон Anthropic) уровень reasoning-эффорта влияет на качество вывода +**сильнее**, чем у прежних моделей. Coding/agentic роли (особенно `developer`) должны +идти минимум на `high`, а `developer` — кандидат на `xhigh`. Сейчас фактически работает +неконтролируемый CLI-дефолт → прямой удар по стратегии надёжности и предсказуемости +качества всего конвейера (включая enduro-trails из общего инстанса). + +## 3. Решение (бизнес-уровень) + +Принят **вариант (c)** (решение Славы, 08.06): пустая строка эффорта трактуется как +«не задано» и откатывается на осмысленный per-role дефолт (а не на CLI-дефолт), +**устойчиво** к пустым env. Дополнительно — зафиксировать целевые дефолты в `config.py` +и `.env.example`. + +### Целевые значения эффорта (единственный апгрейд — `developer`) +| Агент | Эффорт | Обоснование | +|-------|--------|-------------| +| analyst | high | intelligence-роль | +| architect | high | intelligence-роль | +| **developer** | **xhigh** | coding/agentic, канон Opus 4.8 → апгрейд с `high` | +| reviewer | high | intelligence-роль | +| tester | medium | механическая роль | +| deployer | medium | механическая роль | + +`developer → xhigh` — единственное изменение относительно текущих config-дефолтов; +остальные значения подтверждают текущий замысел и фиксируются устойчиво. + +## 4. Грабли / ограничения (из бизнес-запроса) + +- **Хост-репо / env-правки НЕ переживают деплой**, если положены в git-managed файл + (урок 08.06 про docker-compose + TZ). Источник правды для реальных значений — + `.env` на хосте (gitignored), канон-шаблон — `.env.example`. Фикс обязан быть + **code-side robust**: даже если прод-`.env` снова окажется с пустыми + `ORCH_AGENT_EFFORT_*`, эффорт всё равно резолвится в целевые значения. +- **Self-hosting:** правка касается инструмента, который сейчас в проде обслуживает и + другие проекты. Прод-контейнер `orchestrator` не ронять в рамках задачи; деплой — + через штатный `deploy-staging` → `Confirm Deploy`. + +## 5. Не-цели + +- НЕ трогать model-резолв (`resolve_agent_model` — сделан в ORCH-074). +- НЕ включать G3 model-routing — все 6 агентов остаются на `claude-opus-4-8`. +- НЕ менять значения эффорта сверх согласованных (`high`/`medium`/`xhigh` для + developer). Иные значения — отдельное взвешенное решение. + +## 6. Затронутые стороны + +- Все агенты конвейера (analyst → deployer) во всех проектах общего инстанса. +- Операторы (правка прод-`.env`), документация (README таблица, `.env.example`). + + diff --git a/docs/work-items/ORCH-081/02-trz.md b/docs/work-items/ORCH-081/02-trz.md new file mode 100644 index 0000000..fa0a4cb --- /dev/null +++ b/docs/work-items/ORCH-081/02-trz.md @@ -0,0 +1,110 @@ +# 02 — ТЗ: ORCH-081 (ORCH-52h) + +**Work Item:** ORCH-081 · **Тип:** багфикс конфигурации · **Repo:** orchestrator + +Документ описывает ТРЕБУЕМОЕ ПОВЕДЕНИЕ и затронутые модули. Конкретный механизм +(field_validator vs изменение резолвера) — на усмотрение архитектора; ниже зафиксированы +инварианты, которым любая реализация обязана удовлетворять. + +## 1. Задействованные модули + +| Модуль | Роль в задаче | +|--------|----------------| +| `src/config.py` (`Settings`) | дефолты эффорта; устойчивость к пустому env (ядро фикса) | +| `src/agents/launcher.py` | `resolve_agent_effort` / `_resolve_agent_attr` (цепочка резолва), `VALID_EFFORTS`, сборка `--effort` в `_spawn` | +| `.env.example` | канон-шаблон значений эффорта по ролям | +| `docs/architecture/README.md` | таблица «Модель и эффорт по ролям» (строки ~47–54) | +| `CHANGELOG.md` | запись о фиксе | +| `tests/test_resolve_agent_effort.py` | расширить кейсами пустого env | + +## 2. Корень бага (точная механика) + +`launcher._resolve_agent_attr` (строки ~104–114): +``` +per_agent = getattr(settings, f"agent_effort_{agent}", "") # '' в проде -> falsy -> skip +default = getattr(settings, "agent_effort_default", "") # '' в проде -> falsy -> skip +return "" # уровень 4: без флага +``` +Pydantic: `ORCH_AGENT_EFFORT_*=` (пустая строка в env) перебивает дефолт класса → +поле `= ''`. Поскольку пустым оказывается **и** `agent_effort_default`, у резолва нет +непустого «пола» для отката → `''` → `--effort` не передаётся. + +## 3. Требования к фиксу (вариант c) + +### FR-1. Непустой floor на каждую роль при пустом env +При ЛЮБОЙ комбинации пустых `ORCH_AGENT_EFFORT_*` (включая `ORCH_AGENT_EFFORT_DEFAULT=`) +`resolve_agent_effort(agent)` обязан вернуть целевое непустое значение для каждой из 6 +ролей: + +| agent | результат | +|-------|-----------| +| analyst | `high` | +| architect | `high` | +| developer | `xhigh` | +| reviewer | `high` | +| tester | `medium` | +| deployer | `medium` | + +Замечание для реализации: floor должен быть **per-role**, а не единым на default — +иначе пустой `ORCH_AGENT_EFFORT_TESTER=` снапнется на `high` вместо `medium`. Т.е. +«пустая строка трактуется как не-задано» применяется так, чтобы каждая роль получала +СВОЙ канонический дефолт, а не общий. + +### FR-2. Приоритет резолва сохраняется +Порядок не меняется: project-override (`projects_json.agent_efforts`) > per-agent env > +default > floor. Непустой явный env/override по-прежнему ПОБЕЖДАЕТ floor (оператор может +осознанно задать, напр., `ORCH_AGENT_EFFORT_DEVELOPER=high`, и это применится). + +### FR-3. Валидация невалидного значения не регрессирует +Значение вне `VALID_EFFORTS` (`low|medium|high|xhigh|max`) по-прежнему логируется +(`logger.warning`) и **дропается** → `''` (без флага). Floor НЕ должен «спасать» явную +опечатку (`turbo`/`ultra`) — поведение ORCH-41 сохраняется (never-break, мусор не +уезжает в CLI). + +### FR-4. `developer → xhigh` зафиксирован явно +`config.py`: `agent_effort_developer` со значением `xhigh` (сейчас `high`). +`.env.example`: `ORCH_AGENT_EFFORT_DEVELOPER=xhigh` (сейчас `high`) + правка комментария +про split (developer теперь xhigh, не в группе «thinking → high»). + +### FR-5. `xhigh` принимается CLI-слоем +Подтвердить, что `xhigh` присутствует в `VALID_EFFORTS` +(`src/agents/launcher.py:22` — уже `frozenset({"low","medium","high","xhigh","max"})`, +**присутствует**; добавления не требуется, только верификация тестом). Эффорт реально +собирается в команду: `_spawn` строит `effort_flag = f"--effort {effort} "` при непустом +`effort` (строка ~434) — путь проброса не менять, только убедиться тестом сборки флага. + +## 4. Изменения API / схемы БД + +- **API endpoints:** нет. +- **Схема БД:** нет. +- **Конфиг (env-контракт):** значения `ORCH_AGENT_EFFORT_*` неизменны по ИМЕНАМ; + меняется лишь дефолт `developer` (high → xhigh) и устойчивость к пустым значениям. + Обратная совместимость: непустой явный env работает 1:1 как раньше. + +## 5. Требования к QG checks + +Новых QG checks не требуется. Гейты конвейера не затрагиваются. + +## 6. Артефакты pipeline (обновить в ТОМ ЖЕ PR) + +- `src/config.py` — дефолт developer + устойчивость к пустому env. +- `src/agents/launcher.py` — если фикс кладётся в резолвер (на усмотрение архитектора). +- `.env.example` — `ORCH_AGENT_EFFORT_DEVELOPER=xhigh` + правка комментария split. +- `docs/architecture/README.md` — таблица эффорта: developer `high` → `xhigh`; при + необходимости — ремарка про floor/устойчивость к пустому env. +- `CHANGELOG.md` — запись (`fix:`). +- `tests/test_resolve_agent_effort.py` — новые кейсы (см. 04-test-plan.yaml). + +## 7. Операционная часть (вне PR-кода, для деплой-лога) + +- Реальные значения — в прод-`.env` на хосте (gitignored). Рекомендуется привести + прод-`.env` к каноне `.env.example` (developer=xhigh, остальные непустые), НО фикс + обязан работать и без этого (FR-1). Не коммитить секреты/хост-env в git. +- Деплой — через `deploy-staging` (8501) → `Confirm Deploy`. Прод-контейнер не ронять + вне штатного хука. + +## 8. Definition of Done + +AC-1…AC-5 из `03-acceptance-criteria.md` выполнены; `pytest -q` зелёный; документация +(README + `.env.example` + CHANGELOG) синхронизирована в том же PR; never-break соблюдён. + diff --git a/docs/work-items/ORCH-081/03-acceptance-criteria.md b/docs/work-items/ORCH-081/03-acceptance-criteria.md new file mode 100644 index 0000000..efcba9f --- /dev/null +++ b/docs/work-items/ORCH-081/03-acceptance-criteria.md @@ -0,0 +1,60 @@ +# 03 — Критерии приёмки: ORCH-081 (ORCH-52h) + +Каждый критерий — чёткое условие PASS/FAIL. Пустой env моделируется в unit-тестах +(установка `agent_effort_* = ""`), проверка «в проде» — операционная (post-deploy). + +## AC-1 — осмысленный непустой эффорт для всех 6 агентов +**PASS:** `resolve_agent_effort(agent)` возвращает целевое непустое значение для каждой +роли при канонической конфигурации: + +| agent | ожидаемое | +|-------|-----------| +| analyst | `high` | +| architect | `high` | +| developer | `xhigh` | +| reviewer | `high` | +| tester | `medium` | +| deployer | `medium` | + +**FAIL:** любой агент возвращает `''` или значение, отличное от таблицы. + +## AC-2 — пустой env НЕ приводит к пустому эффорту (вариант c) +**PASS:** при `agent_effort_default = ""` И всех `agent_effort_ = ""` +(моделирование прод-env, где `ORCH_AGENT_EFFORT_*=` пусты) `resolve_agent_effort` для +каждой из 6 ролей возвращает значение по таблице AC-1 (floor per-role срабатывает: +developer=`xhigh`, tester/deployer=`medium`, остальные=`high`), а **не** `''`. +**FAIL:** хотя бы одна роль при полностью пустом env даёт `''`. + +## AC-3 — эффорт реально пробрасывается в запуск агента +**PASS:** в `launcher._spawn` (или эквивалентной сборке) при непустом резолвнутом +эффорте формируется `--effort ` во флагах команды; при пустом — флаг +отсутствует. Тест сборки флага подтверждает наличие `--effort xhigh ` для developer и +`--effort medium ` для tester. +**FAIL:** `--effort` отсутствует при непустом значении ИЛИ присутствует при пустом. + +## AC-4 — документация синхронизирована +**PASS:** `.env.example` содержит `ORCH_AGENT_EFFORT_DEVELOPER=xhigh` и корректный +комментарий про split; таблица «Модель и эффорт по ролям» в +`docs/architecture/README.md` показывает developer = `xhigh` (остальные без изменений); +`CHANGELOG.md` содержит запись о фиксе. +**FAIL:** любой из трёх артефактов рассинхронизирован с фактическими дефолтами config. + +## AC-5 — never-break, тесты зелёные +**PASS:** +- `pytest -q` целиком зелёный (включая существующие + `tests/test_resolve_agent_effort.py` и новые кейсы). +- Невалидное значение эффорта (`turbo`/`ultra`/`bogus`) по-прежнему логируется и + дропается в `''` (floor его НЕ маскирует) — регрессии валидации ORCH-41 нет. +- Непустой явный per-agent env / project-override по-прежнему побеждает floor + (приоритет резолва сохранён). +- `xhigh ∈ VALID_EFFORTS` (подтверждено тестом). + +**FAIL:** падение любого теста, регрессия валидации/приоритета, либо `xhigh` +отвергается как невалидный. + +## AC-6 (операционный, для деплой-стадии) — проверка в проде +**PASS:** после деплоя на проде `resolve_agent_effort` для 6 агентов даёт значения +AC-1 (проверяется в рантайме прод-инстанса / по логам запуска агента — наличие +`--effort` с верным уровнем). Фиксируется в `14-deploy-log.md`. +**FAIL:** в проде хотя бы один агент бежит без `--effort` или с неверным уровнем. + diff --git a/docs/work-items/ORCH-081/04-test-plan.yaml b/docs/work-items/ORCH-081/04-test-plan.yaml new file mode 100644 index 0000000..712b86a --- /dev/null +++ b/docs/work-items/ORCH-081/04-test-plan.yaml @@ -0,0 +1,86 @@ +work_item: ORCH-081 +description: > + Тест-план фикса ORCH-52h — устойчивость резолва эффорта к пустому env (вариант c) + + фиксация целевых дефолтов (developer -> xhigh). Расширяет существующий + tests/test_resolve_agent_effort.py. Пустой прод-env моделируется установкой + agent_effort_* = "" на settings (через monkeypatch), как уже делают текущие тесты. +tests: + - id: TC-01 + type: unit + description: > + Канонические дефолты: resolve_agent_effort для всех 6 ролей даёт + analyst/architect/reviewer=high, developer=xhigh, tester/deployer=medium. + module: tests/test_resolve_agent_effort.py + covers: [AC-1, FR-4] + expected: PASS + + - id: TC-02 + type: unit + description: > + Пустой env (вариант c): при agent_effort_default="" И всех + agent_effort_="" каждая из 6 ролей возвращает целевое значение по AC-1 + (НЕ ""). Ключевой кейс бага: developer -> xhigh, tester/deployer -> medium, + analyst/architect/reviewer -> high. + module: tests/test_resolve_agent_effort.py + covers: [AC-2] + expected: PASS + + - id: TC-03 + type: unit + description: > + Floor НЕ маскирует опечатку: невалидное значение (default/per-agent/override = + 'turbo'/'ultra'/'bogus') по-прежнему логируется и дропается в "" (валидация + ORCH-41 не регрессирует). Проверить, что floor не подменяет невалидный явный ввод + на дефолт. + module: tests/test_resolve_agent_effort.py + covers: [AC-5, FR-3] + expected: PASS + + - id: TC-04 + type: unit + description: > + Приоритет сохранён: непустой per-agent env побеждает floor/ default + (ORCH_AGENT_EFFORT_DEVELOPER=high -> "high", не "xhigh"); project-override + побеждает per-agent (agent_efforts={"developer":"xhigh"}). + module: tests/test_resolve_agent_effort.py + covers: [AC-5, FR-2] + expected: PASS + + - id: TC-05 + type: unit + description: > + xhigh валиден: xhigh ∈ VALID_EFFORTS и resolve_agent_effort с developer-дефолтом + xhigh не дропается. + module: tests/test_resolve_agent_effort.py + covers: [AC-5, FR-5] + expected: PASS + + - id: TC-06 + type: unit + description: > + Сборка флага: при resolve developer=xhigh во флагах присутствует "--effort xhigh ", + при tester=medium — "--effort medium "; при пустом эффорте "--effort" отсутствует + (mirror логики _spawn, как существующие test_flags_* кейсы). + module: tests/test_resolve_agent_effort.py + covers: [AC-3] + expected: PASS + + - id: TC-07 + type: integration + description: > + Документация синхронизирована: .env.example содержит + ORCH_AGENT_EFFORT_DEVELOPER=xhigh; README таблица эффорта показывает developer + xhigh. (Проверяется ревьюером/тестером по diff; опционально — текстовая ассерта.) + module: tests/test_resolve_agent_effort.py + covers: [AC-4] + expected: PASS + + - id: TC-08 + type: unit + description: > + Регрессия существующего набора: весь tests/test_resolve_agent_effort.py + + tests/test_resolve_agent_model.py остаются зелёными (never-break ORCH-41/074). + module: tests/test_resolve_agent_effort.py + covers: [AC-5] + expected: PASS + diff --git a/docs/work-items/ORCH-081/06-adr/ADR-001-effort-resolution-floor.md b/docs/work-items/ORCH-081/06-adr/ADR-001-effort-resolution-floor.md new file mode 100644 index 0000000..7a52b7b --- /dev/null +++ b/docs/work-items/ORCH-081/06-adr/ADR-001-effort-resolution-floor.md @@ -0,0 +1,129 @@ +# 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_=`), +И 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_) — непустой побеждает +3. global default (settings.agent_effort_default) — непустой побеждает +4. per-role FLOOR (class-default поля agent_effort_) — НОВОЕ, непустой пол + ↓ (только если все 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_` нет (имя не из 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`. diff --git a/docs/work-items/ORCH-081/10-tech-risks.md b/docs/work-items/ORCH-081/10-tech-risks.md new file mode 100644 index 0000000..19a40cd --- /dev/null +++ b/docs/work-items/ORCH-081/10-tech-risks.md @@ -0,0 +1,17 @@ +# 10 — Технические риски: ORCH-081 (ORCH-52h) + +| ID | Риск | Вероятн. | Влияние | Митигация | +|----|------|----------|---------|-----------| +| R-1 | **Floor маскирует опечатку.** Если floor применить ПОСЛЕ/ВМЕСТО валидации, мусорное `turbo` подменится на floor вместо дропа → регрессия never-break ORCH-41. | низк. | средн. | Floor строго ДО валидации и ТОЛЬКО при пустом резолве (значение `turbo` непустое → floor не трогается → дроп). Покрыть тестом FR-3 (опечатка → `''`). | +| R-2 | **Floor перебивает явный конфиг.** Ошибка порядка → floor встанет выше default/per-agent и `ORCH_AGENT_EFFORT_DEFAULT=max` перестанет применяться. | низк. | средн. | Floor — строго уровень 4 (ниже default). Тест FR-2: непустой default/per-agent/override побеждает floor. | +| R-3 | **Зависимость от pydantic-internal** `model_fields[...].default`. Будущий мажор pydantic может сменить API → floor отвалится. | низк. | низк. | Публичный стабильный атрибут pydantic v2. Тест AC-1/AC-2 поймает регрессию сразу (floor вернёт не то/пусто). Фиксируется версией pydantic в зависимостях. | +| R-4 | **Дрейф floor vs config** при выборе hardcoded-карты. | — | — | Снят архитектурно: floor = class-default поля, единый источник правды (см. ADR-001, вариант B отклонён). | +| R-5 | **Self-hosting:** правка резолва эффорта затрагивает запуск ВСЕХ агентов всех проектов общего инстанса; ошибка ломает конвейер enduro-trails тоже. | низк. | высок. | Обязательный `deploy-staging` (8501) перед прод-деплоем; прод-контейнер не ронять вне штатного хука; `Confirm Deploy`-гейт. Post-deploy проверка AC-6 по логам запуска агента. | +| R-6 | **Прод-`.env` снова с пустыми `ORCH_AGENT_EFFORT_*`** после деплоя (урок 08.06: git-managed env не переживает). | средн. | низк. | Именно это и закрывает фикс (FR-1, code-side robust): эффорт резолвится в floor независимо от состояния `.env`. Приведение `.env` к каноне — рекомендация, не зависимость. | +| R-7 | **`xhigh` не принимается CLI-слоем.** developer-апгрейд бессмыслен, если `xhigh ∉ VALID_EFFORTS`. | очень низк. | средн. | `xhigh` уже в `VALID_EFFORTS` (`launcher.py:22`); добавления не требуется — только верификация тестом (FR-5). | + +## Сводный вывод +Изменение локализовано в `resolve_agent_effort` + один дефолт `config.py`; не трогает +API, схему БД, QG-гейты, model-резолв и путь проброса `--effort`. Главный остаточный +риск — операционный (R-5, self-hosting), снимается штатным staging-гейтом. Контракт +ORCH-41/ORCH-074 сохранён, обратная совместимость полная. diff --git a/docs/work-items/ORCH-081/12-review.md b/docs/work-items/ORCH-081/12-review.md new file mode 100644 index 0000000..1daa279 --- /dev/null +++ b/docs/work-items/ORCH-081/12-review.md @@ -0,0 +1,57 @@ +--- +type: review +work_item_id: ORCH-081 +verdict: APPROVED +version: 1 +--- + +# Review ORCH-081 (ORCH-52h) — устойчивость резолва `--effort` к пустому env + developer→xhigh + +## Summary +Фикс конфигурационного бага: в проде `resolve_agent_effort()` возвращал `''` для всех 6 агентов (пустые `ORCH_AGENT_EFFORT_*=` перебивают class-default pydantic), `--effort` не доходил до Claude CLI. Решение — вариант C по ADR-001: непустой **per-role floor** уровня 4 в `resolve_agent_effort`, значение = декларированный class-default поля `agent_effort_` через `model_fields[...].default`. `developer` поднят `high→xhigh` в `config.py` (единый источник правды, floor подтягивается автоматически). + +Реализация полностью соответствует ТЗ и ADR; вся документация синхронизирована в том же бранче; `pytest -q` — **1031 passed**. + +## Соответствие ТЗ (FR-1…FR-5) +- **FR-1** per-role floor при пустом env → каждая роль получает свой канон (`_agent_effort_floor`, TC-02). ✓ +- **FR-2** приоритет резолва сохранён: явный env/override/default побеждают floor (TC-04: `test_explicit_env_beats_floor`, `test_default_beats_floor`, `test_project_override_beats_floor`). ✓ +- **FR-3** валидация не регрессирует: непустая опечатка (`turbo`) не доходит до floor → дропается в `''` (TC-03 `test_floor_does_not_mask_typo`). ✓ +- **FR-4** `agent_effort_developer = "xhigh"` в `config.py`; `ORCH_AGENT_EFFORT_DEVELOPER=xhigh` + правка комментария split в `.env.example`. ✓ +- **FR-5** `xhigh ∈ VALID_EFFORTS`; сборка флага `--effort xhigh `/`--effort medium ` подтверждена (TC-05/TC-06). ✓ + +## Соответствие ADR-001 +- Floor как **строго уровень 4** ниже default, в резолвере — ✓ (вариант C, не field_validator/не hardcoded map). +- Floor = **class-default поля** (`type(settings).model_fields[...].default`), который пустой env перебить не может — ✓. +- `_resolve_agent_attr` (общий с model-резолвом) **не тронут** — ✓. +- Floor применяется **ДО валидации и только при пустом резолве** — ✓. +- Unknown-agent деградирует на class-default `agent_effort_default` (`high`) — ✓ (`test_empty_env_unknown_agent_floor_is_default`). +- Никаких изменений API / схемы БД / QG / model-резолва / пути проброса в `_spawn` — ✓. + +## Качество кода и тестов +- Чистый leaf-helper, подробные docstrings, контракт never-raise соблюдён. +- Тесты содержательные, покрывают все AC/FR (канон-дефолты, floor per-role, не-маскирование опечатки, приоритет на 3 уровнях, `xhigh`-валидность, сборка флага + негативные кейсы). + +## Findings + +### P0 — Blocker +- (нет) + +### P1 — Must fix +- (нет) + +### P2 — Should fix +- (нет) + +### P3 — Nice-to-have +- `tests/test_resolve_agent_effort.py:218-219` — продублирована строка `assert "--fallback-model" not in flags` в `test_flags_absent_when_model_empty`. Безвредно, можно убрать при случае. + +## Документация +Изменён `src/` → документация обновлена в том же бранче (доку-гейт пройден): +- `docs/architecture/README.md` — таблица «Модель и эффорт по ролям»: developer = `xhigh`; добавлена ремарка про per-role floor / устойчивость к пустому env (AC-4). ✓ +- `.env.example` — `ORCH_AGENT_EFFORT_DEVELOPER=xhigh` + комментарий split/floor (AC-4). ✓ +- `CHANGELOG.md` — запись `fix:` с разбором корня/фикса. ✓ +- `docs/work-items/ORCH-081/06-adr/ADR-001-effort-resolution-floor.md` — присутствует (Accepted). ✓ + +## Примечание (вне scope ревью) +- AC-6 — операционная проверка в проде после деплоя, фиксируется в `14-deploy-log.md` на стадии deploy. К коду PR не относится. +- `git diff main...HEAD` показывает также код ORCH-074 (`is_valid_model`/`resolve_agent_model`) из-за устаревшего локального `main`; собственно изменения ORCH-081 — коммит `56bf303` (+ README обновлён в линии бранча). На ревью это не влияет: HEAD-состояние корректно по всем осям. diff --git a/docs/work-items/ORCH-081/13-test-report.md b/docs/work-items/ORCH-081/13-test-report.md new file mode 100644 index 0000000..07bbeae --- /dev/null +++ b/docs/work-items/ORCH-081/13-test-report.md @@ -0,0 +1,61 @@ +--- +type: test-report +work_item_id: ORCH-081 +result: PASS +--- + +# Test Report — ORCH-081 (ORCH-52h) + +Устойчивость резолва `--effort` к пустому env (вариант c) + фиксация целевых +дефолтов (developer → xhigh). + +## Окружение +- Python: 3.12.13 +- pytest: 8.3.3 +- Repo/branch: orchestrator @ `feature/ORCH-081-orch-52h-env-config` (worktree) +- prod `/health`: ok (8500) · staging `/health`: ok (8501) — не трогались +- Дата: 2026-06-08 + +## Результаты по тест-плану (04-test-plan.yaml) + +| TC ID | Описание | Покрытие | Результат | +|-------|----------|----------|-----------| +| TC-01 | Канонические дефолты: 6 ролей дают high/high/xhigh/high/medium/medium | AC-1, FR-4 | PASS | +| TC-02 | Пустой env (вариант c): per-role floor, developer→xhigh, tester/deployer→medium, остальные→high (НЕ "") | AC-2 | PASS | +| TC-03 | Floor НЕ маскирует опечатку: `turbo`/`ultra`/`bogus` логируется и дропается в "" | AC-5, FR-3 | PASS | +| TC-04 | Приоритет сохранён: непустой per-agent env / project-override побеждают floor/default | AC-5, FR-2 | PASS | +| TC-05 | `xhigh ∈ VALID_EFFORTS` и не дропается | AC-5, FR-5 | PASS | +| TC-06 | Сборка флага: `--effort xhigh ` (developer), `--effort medium ` (tester); пустой → флаг отсутствует | AC-3 | PASS | +| TC-07 | Документация синхронизирована: `.env.example` DEVELOPER=xhigh, README таблица developer=xhigh | AC-4 | PASS | +| TC-08 | Регрессия: весь набор test_resolve_agent_effort.py + полный регресс зелёные | AC-5 | PASS | + +### Сопоставление с критериями приёмки +- **AC-1** — `test_canonical_effort_all_roles[*]` (6 параметров) → PASS. +- **AC-2** — `test_empty_env_falls_back_to_per_role_floor[*]` (6 параметров) + `test_empty_env_unknown_agent_floor_is_default` → PASS. +- **AC-3** — `test_flags_present_when_configured`, `test_flags_effort_per_role`, `test_flags_absent_when_effort_empty` → PASS. +- **AC-4** — verified по diff: `src/config.py:108` `agent_effort_developer = "xhigh"`; `.env.example:48` `ORCH_AGENT_EFFORT_DEVELOPER=xhigh`; `docs/architecture/README.md` таблица developer=`xhigh`; `CHANGELOG.md` содержит запись `fix:` → PASS. +- **AC-5** — `test_floor_does_not_mask_typo`, `test_*_beats_floor`, `test_xhigh_is_valid`, `test_invalid_*_dropped` + полный регресс зелёный → PASS. +- **AC-6** — операционный, вне scope стадии testing: проверяется в рантайме прода на стадии `deploy`, фиксируется в `14-deploy-log.md`. + +## Smoke test API (prod 8500) +- `GET /health` → `{"status":"ok","service":"orchestrator"}` +- `GET /status` → HTTP 200 +- `GET /queue` → HTTP 200 + +## Вывод pytest + +Целевой файл задачи: +``` +tests/test_resolve_agent_effort.py ... 29 passed, 1 warning in 0.36s +``` + +Полный регресс: +``` +........................................................................ [ 97%] +....................... [100%] +1031 passed, 1 warning in 27.02s +``` +(единственный warning — PydanticDeprecatedSince20 в `src/config.py:5`, не относится к задаче, предсуществующий.) + +## Итог +**PASS** — все 8 TC пройдены, критерии AC-1…AC-5 выполнены (AC-6 операционный, для стадии deploy), полный регресс `1031 passed`, smoke API зелёный. Прод/staging-контейнеры не затрагивались. diff --git a/docs/work-items/ORCH-081/14-deploy-log.md b/docs/work-items/ORCH-081/14-deploy-log.md new file mode 100644 index 0000000..50929f1 --- /dev/null +++ b/docs/work-items/ORCH-081/14-deploy-log.md @@ -0,0 +1,12 @@ +--- +deploy_status: SUCCESS +work_item: ORCH-081 +hook_exit_code: 0 +deployed_by: deploy-finalizer +--- + +# Deploy log — ORCH-036 executable self-deploy + +Прод-деплой завершён хост-хуком с exit-code `0` -> `deploy_status: SUCCESS`. + +Вердикт зафиксирован детерминированным finalizer'ом (Фаза C), не LLM. diff --git a/src/agents/launcher.py b/src/agents/launcher.py index 9213b9d..6bc29c9 100644 --- a/src/agents/launcher.py +++ b/src/agents/launcher.py @@ -158,12 +158,50 @@ def resolve_agent_model(agent: str, project_id: str = None) -> str: return "" +def _agent_effort_floor(agent: str) -> str: + """ORCH-081 (ORCH-52h): per-role non-empty floor for --effort resolution. + + Returns the DECLARED class-default of the ``agent_effort_`` field on + Settings (e.g. developer -> ``xhigh``, tester/deployer -> ``medium``, the rest + -> ``high``). This is the value pydantic WOULD have used were it not clobbered + by a spurious empty env var (``ORCH_AGENT_EFFORT_=``): the class-default + is fixed in the class body and a present-but-empty env value cannot override it, + so it is a robust floor even when the host ``.env`` zeroes every effort var. + + config.py is the single source of truth: upgrading developer to ``xhigh`` there + automatically raises the floor here — no second map to keep in sync (ADR-001). + + Unknown agent (a name outside the 6 roles) has no ``agent_effort_`` + field; we degrade to the class-default of ``agent_effort_default`` (``high``), + a safe non-empty floor. Never raises. + """ + fields = type(settings).model_fields + for key in (f"agent_effort_{agent}", "agent_effort_default"): + field = fields.get(key) + if field is not None and field.default: + return field.default + return "" + + def resolve_agent_effort(agent: str, project_id: str = None) -> str: """ORCH-41: resolve the --effort level for an agent (optionally per-project). - Same priority as resolve_agent_model. The resolved value is validated against - VALID_EFFORTS; an invalid value is logged and dropped (returns "") so a typo - in env/projects_json can never pass a bad flag to the CLI. + Same priority as resolve_agent_model, with one extra level below the global + default (ORCH-081 / ADR-001): + 1. project-override (projects_json.agent_efforts[agent]) + 2. per-agent env (settings.agent_effort_) + 3. global default (settings.agent_effort_default) + 4. per-role FLOOR (class-default of agent_effort_) — NEW + + The floor only kicks in when levels 1-3 are all empty (the prod bug: a present + but empty ``ORCH_AGENT_EFFORT_*=`` clobbers every default to ''), guaranteeing + a non-empty target effort for the 6 known roles regardless of host .env state. + + The floor is applied BEFORE validation and ONLY to an empty resolve, so it + never masks a typo: an explicit invalid value (e.g. ``turbo``) is non-empty, + skips the floor, and is logged + dropped to "" exactly as in ORCH-41 (the + resolved value is validated against VALID_EFFORTS; an invalid value can never + pass a bad flag to the CLI). Never raises. """ value = _resolve_agent_attr( agent, project_id, @@ -171,6 +209,11 @@ def resolve_agent_effort(agent: str, project_id: str = None) -> str: env_attr_prefix="agent_effort_", default_attr="agent_effort_default", ) + if not value: + # Levels 1-3 all empty (typically a prod .env with empty ORCH_AGENT_EFFORT_*): + # fall through to the per-role floor (class-default). Applied before + # validation but only here, so a typo (non-empty) never reaches this branch. + value = _agent_effort_floor(agent) if value and value not in VALID_EFFORTS: logger.warning( f"Invalid effort '{value}' for agent '{agent}' " diff --git a/src/config.py b/src/config.py index dc93615..2fc010d 100644 --- a/src/config.py +++ b/src/config.py @@ -97,13 +97,15 @@ class Settings(BaseSettings): agent_model_deployer: str = "" # ORCH-41: per-agent effort / reasoning level: low|medium|high|xhigh|max. - # Empty -> agent_effort_default. Same resolution order as model. Default split: - # thinking agents (analyst/architect/developer/reviewer) -> high; mechanical - # agents (tester/deployer) -> medium. + # Empty -> agent_effort_default. Same resolution order as model. Default split + # (ORCH-081/ORCH-52h): thinking agents (analyst/architect/reviewer) -> high; + # developer -> xhigh (coding/agentic role, Opus 4.8 canon); mechanical agents + # (tester/deployer) -> medium. These class-defaults are ALSO the per-role floor + # used by resolve_agent_effort when the env is empty (single source of truth). agent_effort_default: str = "high" agent_effort_analyst: str = "high" agent_effort_architect: str = "high" - agent_effort_developer: str = "high" + agent_effort_developer: str = "xhigh" agent_effort_reviewer: str = "high" agent_effort_tester: str = "medium" agent_effort_deployer: str = "medium" diff --git a/tests/test_resolve_agent_effort.py b/tests/test_resolve_agent_effort.py index d2718d4..c48a7ba 100644 --- a/tests/test_resolve_agent_effort.py +++ b/tests/test_resolve_agent_effort.py @@ -26,13 +26,22 @@ from src.projects import ProjectConfig, reload_projects ORCH_PLANE_ID = "8da6aa25-a60e-44d6-a1e2-d8ae59aa7d6a" +# ORCH-081/ORCH-52h: canonical effort per role (developer upgraded high -> xhigh). +CANON_EFFORT = { + "analyst": "high", + "architect": "high", + "developer": "xhigh", + "reviewer": "high", + "tester": "medium", + "deployer": "medium", +} + + @pytest.fixture(autouse=True) def _clean_settings(monkeypatch): monkeypatch.setattr(settings, "agent_effort_default", "high") - for a in ("analyst", "architect", "developer", "reviewer"): - monkeypatch.setattr(settings, f"agent_effort_{a}", "high") - for a in ("tester", "deployer"): - monkeypatch.setattr(settings, f"agent_effort_{a}", "medium") + for a, e in CANON_EFFORT.items(): + monkeypatch.setattr(settings, f"agent_effort_{a}", e) monkeypatch.setattr(P.settings, "projects_json", "") reload_projects() yield @@ -50,19 +59,40 @@ def _install_registry(monkeypatch, agent_efforts): monkeypatch.setattr(P, "_BY_REPO", {p.repo: p for p in reg}) -# ---- default split ---------------------------------------------------------- +# ---- TC-01: canonical defaults (AC-1 / FR-4) -------------------------------- def test_default_split(): - assert resolve_agent_effort("developer") == "high" + assert resolve_agent_effort("developer") == "xhigh" assert resolve_agent_effort("architect") == "high" assert resolve_agent_effort("tester") == "medium" assert resolve_agent_effort("deployer") == "medium" -# ---- level 4: nothing -> "" ------------------------------------------------- -def test_no_config_returns_empty(monkeypatch): +@pytest.mark.parametrize("agent,expected", list(CANON_EFFORT.items())) +def test_canonical_effort_all_roles(agent, expected): + assert resolve_agent_effort(agent) == expected + + +# ---- TC-02: empty env -> per-role floor (variant c, AC-2) ------------------- +@pytest.mark.parametrize("agent,expected", list(CANON_EFFORT.items())) +def test_empty_env_falls_back_to_per_role_floor(monkeypatch, agent, expected): + """Models the prod bug: ORCH_AGENT_EFFORT_*= present-but-empty -> every level + resolves to '' on the instance; the per-role floor (config class-default) must + still yield the canonical level (NOT '').""" + monkeypatch.setattr(settings, "agent_effort_default", "") + for a in CANON_EFFORT: + monkeypatch.setattr(settings, f"agent_effort_{a}", "") + result = resolve_agent_effort(agent) + assert result == expected + assert result != "" + + +# ---- unknown agent floor degrades to default (high), never '' --------------- +def test_empty_env_unknown_agent_floor_is_default(monkeypatch): monkeypatch.setattr(settings, "agent_effort_default", "") monkeypatch.setattr(settings, "agent_effort_tester", "") - assert resolve_agent_effort("tester") == "" + # An agent with no agent_effort_ field falls back to the + # agent_effort_default class-default (high), a safe non-empty floor. + assert resolve_agent_effort("nonexistent_role") == "high" # ---- level 2: per-agent env beats default ----------------------------------- @@ -103,6 +133,45 @@ def test_all_valid_efforts_pass(monkeypatch): assert resolve_agent_effort("developer") == e +# ---- TC-03: floor does NOT mask a typo (FR-3 / AC-5) ------------------------ +def test_floor_does_not_mask_typo(monkeypatch): + """An explicit invalid value is non-empty, so the floor is NOT applied: the + value is validated and dropped to '' (never-break ORCH-41), even though the + developer floor (xhigh) exists.""" + monkeypatch.setattr(settings, "agent_effort_default", "") + monkeypatch.setattr(settings, "agent_effort_developer", "turbo") + assert resolve_agent_effort("developer") == "" + + +# ---- TC-04: priority preserved — explicit config beats floor (FR-2) --------- +def test_explicit_env_beats_floor(monkeypatch): + """Operator may deliberately downgrade developer to high; the explicit + non-empty env wins over the xhigh floor.""" + monkeypatch.setattr(settings, "agent_effort_developer", "high") + assert resolve_agent_effort("developer") == "high" + + +def test_default_beats_floor(monkeypatch): + """A non-empty global default wins over the per-role floor (floor is strictly + below default): default=max with empty per-agent -> max, not the xhigh floor.""" + monkeypatch.setattr(settings, "agent_effort_developer", "") + monkeypatch.setattr(settings, "agent_effort_default", "max") + assert resolve_agent_effort("developer") == "max" + + +def test_project_override_beats_floor(monkeypatch): + monkeypatch.setattr(settings, "agent_effort_developer", "") + _install_registry(monkeypatch, {"developer": "high"}) + assert resolve_agent_effort("developer", ORCH_PLANE_ID) == "high" + + +# ---- TC-05: xhigh is a valid effort (FR-5) ---------------------------------- +def test_xhigh_is_valid(): + assert "xhigh" in VALID_EFFORTS + # developer canonical xhigh resolves (is not dropped by validation) + assert resolve_agent_effort("developer") == "xhigh" + + # ---- flag assembly (mirror of launcher cmd construction) -------------------- def _build_flags(model, effort, fb): model_flag = f"--model {model} " if model else "" @@ -111,6 +180,7 @@ def _build_flags(model, effort, fb): return f"{model_flag}{effort_flag}{fb_flag}" +# ---- TC-06: flag assembly (AC-3) -------------------------------------------- def test_flags_present_when_configured(monkeypatch): monkeypatch.setattr(settings, "agent_fallback_model", "claude-sonnet-4-6") model = resolve_agent_model("developer") @@ -118,21 +188,32 @@ def test_flags_present_when_configured(monkeypatch): fb = settings.agent_fallback_model flags = _build_flags(model, effort, fb) assert "--model claude-opus-4-8 " in flags - assert "--effort high " in flags + assert "--effort xhigh " in flags assert "--fallback-model claude-sonnet-4-6 " in flags -def test_flags_absent_when_empty(monkeypatch): +def test_flags_effort_per_role(monkeypatch): + """developer -> --effort xhigh; tester -> --effort medium (mirrors _spawn).""" + assert "--effort xhigh " in _build_flags("", resolve_agent_effort("developer"), "") + assert "--effort medium " in _build_flags("", resolve_agent_effort("tester"), "") + + +def test_flags_absent_when_effort_empty(): + """When the resolved effort is empty, --effort is omitted entirely. Mirrors the + `f"--effort {effort} " if effort else ""` branch in _spawn (AC-3 negative case).""" + flags = _build_flags("", "", "") + assert flags == "" + assert "--effort" not in flags + + +def test_flags_absent_when_model_empty(monkeypatch): monkeypatch.setattr(settings, "agent_model_default", "") monkeypatch.setattr(settings, "agent_model_developer", "") - monkeypatch.setattr(settings, "agent_effort_default", "") - monkeypatch.setattr(settings, "agent_effort_developer", "") monkeypatch.setattr(settings, "agent_fallback_model", "") model = resolve_agent_model("developer") - effort = resolve_agent_effort("developer") fb = settings.agent_fallback_model - flags = _build_flags(model, effort, fb) + flags = _build_flags(model, "", fb) assert flags == "" assert "--model" not in flags - assert "--effort" not in flags + assert "--fallback-model" not in flags assert "--fallback-model" not in flags