From 0f82ebc1a7d6f312fe5e3c65b8b9803698251776 Mon Sep 17 00:00:00 2001 From: Slava Date: Mon, 8 Jun 2026 21:24:18 +0300 Subject: [PATCH 1/8] docs: init ORCH-074 business request --- docs/work-items/ORCH-074/00-business-request.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 docs/work-items/ORCH-074/00-business-request.md diff --git a/docs/work-items/ORCH-074/00-business-request.md b/docs/work-items/ORCH-074/00-business-request.md new file mode 100644 index 0000000..3eb9326 --- /dev/null +++ b/docs/work-items/ORCH-074/00-business-request.md @@ -0,0 +1,7 @@ +# Business Request: ORCH-52a: фикс модели/эффорта агентов (мёртвый frontmatter → routing+effort) + +Work Item ID: ORCH-074 + +## Description + +TBD From e89f7c7a11c465d4247ae96255370132755bbcac Mon Sep 17 00:00:00 2001 From: claude-bot Date: Mon, 8 Jun 2026 21:27:22 +0300 Subject: [PATCH 2/8] analyst(ET): auto-commit from analyst run_id=392 --- docs/work-items/ORCH-074/01-brd.md | 77 +++++++++++++++ docs/work-items/ORCH-074/02-trz.md | 99 +++++++++++++++++++ .../ORCH-074/03-acceptance-criteria.md | 78 +++++++++++++++ docs/work-items/ORCH-074/04-test-plan.yaml | 92 +++++++++++++++++ 4 files changed, 346 insertions(+) create mode 100644 docs/work-items/ORCH-074/01-brd.md create mode 100644 docs/work-items/ORCH-074/02-trz.md create mode 100644 docs/work-items/ORCH-074/03-acceptance-criteria.md create mode 100644 docs/work-items/ORCH-074/04-test-plan.yaml diff --git a/docs/work-items/ORCH-074/01-brd.md b/docs/work-items/ORCH-074/01-brd.md new file mode 100644 index 0000000..b8add45 --- /dev/null +++ b/docs/work-items/ORCH-074/01-brd.md @@ -0,0 +1,77 @@ +# BRD — ORCH-074: фикс модели агентов (мёртвый frontmatter → валидация имени) + +Work Item ID: ORCH-074 +Эпик: ORCH-052 (слой 3), под-задача ORCH-52a +Приоритет: **urgent** +Тип: доработка механизма выбора модели агентов (self-modifying). + +## 1. Контекст и проблема + +Каркас выбора модели агентов реализован в ORCH-041 и **работает корректно**: +`src/agents/launcher.py::resolve_agent_model(agent, project_id)` резолвит модель +по приоритету project-override → `ORCH_AGENT_MODEL_` → `agent_model_default` +→ CLI-дефолт. Все 6 агентов сейчас резолвятся в `claude-opus-4-8` (через +`agent_model_default`). + +Аудит кода (08.06) выявил два дефекта данных/валидации (НЕ дефект механизма): + +- **P1. Лживый/мёртвый `model:` во frontmatter `.openclaw/agents/*.md`.** + Все 6 промптов содержат `model:` в YAML-frontmatter: + `claude-sonnet-4-6` (analyst, developer, tester, deployer) и + `claude-opus-4-7` (architect, reviewer). launcher **НЕ читает** frontmatter + `model:` — это мёртвая декларация, которая лжёт о реально используемой модели + и нарушает принцип «документация = golden source». Мина: если кто-то «починит» + launcher читать frontmatter → все агенты молча упадут на устаревшие модели. + +- **P2. Нет валидации ИМЕНИ модели.** В отличие от effort (есть `VALID_EFFORTS`-гард, + невалидный effort логируется и дропается), имя модели не валидируется. Опечатка + в `agent_model_*` / project-override → `--model <мусор>` → CLI падает или тихо + деградирует. Нарушение принципа never-break. + +## 2. Решение Славы (08.06) — фиксированный скоп + +> G3 model-routing **НЕ включаем** — ВСЕ 6 агентов остаются на `claude-opus-4-8`. +> Скоп: **G1** (убрать лживый `model:` из frontmatter) + **G2** (валидация имени +> модели, never-break) + **опц. G4** (`fallback_model` — на усмотрение архитектора, +> НЕ routing). **Эффорт НЕ трогать.** AC-4 (routing) снят. + +## 3. Бизнес-цели + +| ID | Цель | Драйвер | +|----|------|---------| +| G1 | Устранить лживый frontmatter: убрать `model:` из всех 6 `.openclaw/agents/*.md`. config — единственный источник правды модели. | Наблюдаемость (frontmatter не лжёт) | +| G2 | Добавить валидацию имени модели: невалидное имя → лог + откат на default, никогда не передаётся в `--model`. | Надёжность (never-break) | +| G4 | (опц., решает архитектор) Задать `agent_fallback_model` для страховки доступности. | Надёжность (availability) | + +## 4. Не-цели (явно вне скоупа) + +- **G3 routing НЕ включаем.** Все 6 агентов остаются `claude-opus-4-8`. AC-4 снят. +- **Эффорт НЕ трогать** — уже корректно настроен (`thinking → high`, `tester/deployer → medium`). +- **Не менять resolve-механизм ORCH-041** — он корректен. Меняются только данные + (frontmatter, опц. config) + добавляется валидация. +- **Не трогать non-self поведение** — per-project override (`projects.py agent_models`) + для enduro-trails остаётся рабочим. + +## 5. Заинтересованные стороны + +- **Owner (Слава)** — зафиксировал скоп; деплой через штатный «Confirm Deploy». +- **Агенты оркестратора** — потребители resolve-механизма (self-hosting). +- **Проект enduro-trails** — НЕ должен пострадать (общий инстанс/БД/очередь). + +## 6. Риски и инварианты + +- **Self-hosting:** изменение применяется к БУДУЩИМ запускам агентов. НЕ ломать + текущий конвейер; не ронять прод-контейнер. Деплой только через «Confirm Deploy». +- **never-break:** невалидная модель/эффорт НЕ должны ронять запуск агента — + деградация на default/CLI-дефолт + лог. +- **frontmatter автогенерация:** убедиться, что инструмент (если автогенерит + frontmatter) не вернёт `model:` обратно. Frontmatter остаётся описательным + (`name`/`description`/`tools`). +- **enduro per-project override** не должен сломаться валидацией (валидные имена + проходят без изменения поведения). + +## 7. Бизнес-эффект + +- Frontmatter перестаёт лгать → меньше риск «починки», ломающей агентов. +- Опечатка в имени модели больше не роняет/деградирует запуск агента. +- (опц.) fallback повышает доступность при перегрузке основной модели. diff --git a/docs/work-items/ORCH-074/02-trz.md b/docs/work-items/ORCH-074/02-trz.md new file mode 100644 index 0000000..cf0152d --- /dev/null +++ b/docs/work-items/ORCH-074/02-trz.md @@ -0,0 +1,99 @@ +# ТЗ — ORCH-074: убрать мёртвый frontmatter `model:` + валидация имени модели + +Work Item ID: ORCH-074 +Базируется на: BRD `01-brd.md`. Скоп фиксирован решением Славы (08.06): +**G1 + G2 + опц. G4. G3 (routing) НЕ включаем. Эффорт НЕ трогать.** + +## 1. Задействованные модули `src/` и файлы + +| Файл | Изменение | +|------|-----------| +| `.openclaw/agents/analyst.md` | **G1:** удалить строку `model: claude-sonnet-4-6` из frontmatter | +| `.openclaw/agents/architect.md` | **G1:** удалить строку `model: claude-opus-4-7` | +| `.openclaw/agents/developer.md` | **G1:** удалить строку `model: claude-sonnet-4-6` | +| `.openclaw/agents/reviewer.md` | **G1:** удалить строку `model: claude-opus-4-7` | +| `.openclaw/agents/tester.md` | **G1:** удалить строку `model: claude-sonnet-4-6` | +| `.openclaw/agents/deployer.md` | **G1:** удалить строку `model: claude-sonnet-4-6` | +| `src/agents/launcher.py` | **G2:** добавить валидацию имени модели в `resolve_agent_model` (или helper), по образцу `VALID_EFFORTS`-гарда в `resolve_agent_effort` | +| `src/config.py` | **G4 (опц.):** задать `agent_fallback_model` (если архитектор решит). При G2 — возможно добавить константу/настройку валидного формата модели | +| `docs/architecture/README.md` | **AC-6:** таблица «модель/эффорт по ролям» актуализирована; нет упоминаний sonnet/opus-4-7 как «модели агента» | +| `.env.example` | **AC-3/AC-6:** добавить блок `ORCH_AGENT_MODEL_*` / `ORCH_AGENT_EFFORT_*` / `ORCH_AGENT_FALLBACK_MODEL` (сейчас в `.env.example` их НЕТ) | +| `CLAUDE.md` | **AC-6:** при необходимости — отметить, что модель агента берётся ТОЛЬКО из config (frontmatter описательный) | +| `CHANGELOG.md` | запись о доработке | +| `tests/test_resolve_agent_model.py` | **AC-2:** добавить кейсы валидации мусорного имени | + +## 2. G1 — убрать мёртвый frontmatter `model:` + +Удалить **только** строку `model: …` из YAML-frontmatter каждого из 6 файлов +`.openclaw/agents/*.md`. Остальные ключи (`name`, `description`, `tools`/`model`-comment) +не трогать. frontmatter остаётся валидным YAML и описательным. + +Проверка (AC-1): +``` +grep -L "^model:" .openclaw/agents/*.md # должны вернуться ВСЕ 6 файлов +``` +(`grep -L` печатает файлы БЕЗ совпадения — все 6 не должны содержать `^model:`.) + +## 3. G2 — валидация имени модели (never-break) + +Требование (НЕ предписывает архитектуру — выбор предиката за архитектором): + +- Резолвенное имя модели валидируется ПЕРЕД возвратом из `resolve_agent_model` + (либо в общем helper). Невалидное имя → `logger.warning(...)` + откат на + следующий валидный уровень (в пределе — `agent_model_default`, а если и он + невалиден → `""`, т.е. без флага `--model`, CLI-дефолт). **Никогда** не вернуть + мусор, который попадёт в `--model`. +- Поведение — точная аналогия `resolve_agent_effort` (`VALID_EFFORTS`): валидный → + как есть; невалидный → лог + дроп. +- Предикат валидности (на усмотрение архитектора, рекомендация аналитика): + формат-чек `claude-*` (forward-compatible — новые версии моделей не требуют + правки allowlist) ЛИБО явный `VALID_MODELS` allowlist (строже, но требует + поддержки при выходе новых моделей). **Выбор и обоснование — в ADR.** +- Инвариант обратной совместимости: ВСЕ ныне используемые валидные имена + (`claude-opus-4-8`, а также enduro per-project override) проходят валидацию + без изменения поведения. Невалидным считается только мусор (опечатка, + `gpt-4`, пустая строка после strip и т.п.). +- Контракт уровней резолва ORCH-041 сохраняется: валидация добавляется поверх, + механизм приоритетов не меняется. + +## 4. G4 — fallback_model (опционально, решает архитектор) + +- `src/config.py::agent_fallback_model` сейчас `""` (флаг не прокидывается). +- Если архитектор решит включить — задать каноничное имя модели; launcher уже + прокидывает его в `--fallback-model` (строки 374-375 launcher.py). Имя fallback + ТОЖЕ должно проходить валидацию G2 (или быть гарантированно валидным). +- Если архитектор решит НЕ включать — оставить `""`, AC-5 помечается N/A в ADR. + +## 5. Изменения API / схемы БД + +- **API (HTTP):** нет. +- **Схема БД:** нет миграций. +- **CLI-команда агента:** формируется в `launcher._spawn` (строки 384-392). + Меняется только КАЧЕСТВО значения `--model` (валидное/дроп), сама структура + команды не меняется. + +## 6. Требования к QG checks + +- Новых QG-чеков НЕ требуется. Валидация — это runtime-гард в launcher, не + отдельный quality-gate. + +## 7. Артефакты pipeline + +Должны быть созданы/обновлены в ЭТОМ PR (golden source = код + доки): +- `docs/architecture/README.md` — таблица «модель/эффорт по ролям». +- `.env.example` — блок переменных моделей/эффорта/fallback. +- `CHANGELOG.md` — запись. +- `06-adr/ADR-NNN-*.md` — решение по предикату валидации (G2) и по G4 (fallback вкл/выкл). +- ADR архитектора фиксирует: выбран вариант G1 «убрать» (не «читать frontmatter»). + +## 8. Эффорт — НЕ ТРОГАТЬ + +`agent_effort_*` корректны (`thinking → high`, `tester/deployer → medium`). +Менять только при явном отдельном обосновании (вне скоупа этой задачи). + +## 9. Грабли + +- Имена моделей — каноничные строки Claude CLI; сверить с тем, что реально + принимает CLI на проде (`ORCH_CLAUDE_BIN`). НЕ хардкодить версию вне `config.py`. +- Если frontmatter автогенерится инструментом — убедиться, что `model:` не вернётся. +- Self-hosting: НЕ ронять прод-контейнер; деплой через «Confirm Deploy». diff --git a/docs/work-items/ORCH-074/03-acceptance-criteria.md b/docs/work-items/ORCH-074/03-acceptance-criteria.md new file mode 100644 index 0000000..139d744 --- /dev/null +++ b/docs/work-items/ORCH-074/03-acceptance-criteria.md @@ -0,0 +1,78 @@ +# Критерии приёмки — ORCH-074 + +Work Item ID: ORCH-074 +Скоп (Слава 08.06): G1 + G2 + опц. G4. **G3 routing снят — AC-4 не применяется.** + +Каждый критерий: чёткое условие PASS/FAIL. + +--- + +## AC-1 — frontmatter `model:` убран из всех 6 промптов (G1) + +- **PASS:** ни один файл `.openclaw/agents/*.md` не содержит строки `^model:` в + frontmatter. Команда `grep -L "^model:" .openclaw/agents/*.md` возвращает все 6 + файлов (analyst, architect, developer, reviewer, tester, deployer). +- **FAIL:** хотя бы в одном файле осталась строка `model:`. +- Доп. инвариант: frontmatter остаётся валидным YAML; ключи `name`/`description`/`tools` + сохранены. + +## AC-2 — валидация имени модели, never-break (G2) + +- **PASS:** при невалидном `agent_model_*` / project-override (мусорное имя) + `resolve_agent_model` возвращает откат на default (или `""`), пишет + `logger.warning`, и мусор **никогда** не попадает в `--model`. Покрыто + unit-тестом с мусорным именем (см. `04-test-plan.yaml`, TC-03..TC-05). +- **FAIL:** мусорное имя проходит насквозь в `--model`, или валидация роняет + запуск агента (исключение вместо graceful-деградации). + +## AC-3 — resolve_agent_model осмыслен для всех 6 агентов + +- **PASS:** для каждого из 6 агентов `resolve_agent_model(agent)` (без + project_id) возвращает `claude-opus-4-8` (routing G3 выключен → intelligence- + модель для всех). Значение документировано в README (таблица env) и `.env.example`. +- **FAIL:** хотя бы один агент резолвится в пустую/невалидную/устаревшую модель, + либо документация не отражает фактическую модель. + +## AC-4 — routing (G3) — **СНЯТ (N/A)** + +- Routing НЕ включается в этой задаче. Критерий не применяется. ADR фиксирует + отказ от G3 как осознанное решение Славы (08.06). + +## AC-5 — fallback_model (G4, опционально) + +- **PASS (если G4 включён):** `agent_fallback_model` задан каноничным именем, + проходит валидацию G2, прокидывается в `--fallback-model` (launcher 374-375), + задокументирован. +- **PASS (если G4 НЕ включён):** `agent_fallback_model = ""`, ADR явно фиксирует + отказ; AC-5 помечен N/A. +- **FAIL:** fallback задан невалидным именем, ИЛИ включён без документации/ADR. + +## AC-6 — синхронизация документации + +- **PASS:** `docs/architecture/README.md`, `CLAUDE.md`, `.env.example` + синхронизированы — таблица «модель по ролям» актуальна (все = `claude-opus-4-8`); + НЕТ упоминаний `claude-sonnet-4-6` / `claude-opus-4-7` как «модели агента» + (если они не используются). `.env.example` содержит блок + `ORCH_AGENT_MODEL_*` / `ORCH_AGENT_EFFORT_*` / `ORCH_AGENT_FALLBACK_MODEL`. +- **FAIL:** документация противоречит config, или остались мёртвые упоминания + sonnet/opus-4-7 как модели агента. + +## AC-7 — pytest зелёный + never-break + +- **PASS:** `pytest tests/ -q` зелёный. Невалидная модель/эффорт НЕ роняет запуск + агента (graceful-деградация подтверждена тестами). +- **FAIL:** падают тесты, или невалидный вход роняет запуск. + +## AC-8 — enduro per-project override не сломан + +- **PASS:** валидный per-project override (`projects.py agent_models`) для не-self + проекта (enduro) резолвится и проходит валидацию без изменения поведения + (покрыто существующими тестами `test_resolve_agent_model.py`). +- **FAIL:** валидация ломает корректный per-project override. + +## AC-9 — ADR зафиксирован + +- **PASS:** ADR в `06-adr/` фиксирует: (а) выбран вариант G1 «убрать frontmatter» + (не «читать»); (б) предикат валидации G2 (формат-чек vs allowlist) с обоснованием; + (в) решение по G4 (вкл/выкл) и по отказу от G3. +- **FAIL:** ADR отсутствует или не покрывает эти решения. diff --git a/docs/work-items/ORCH-074/04-test-plan.yaml b/docs/work-items/ORCH-074/04-test-plan.yaml new file mode 100644 index 0000000..d1effdb --- /dev/null +++ b/docs/work-items/ORCH-074/04-test-plan.yaml @@ -0,0 +1,92 @@ +work_item: ORCH-074 +# Скоп (Слава 08.06): G1 + G2 + опц. G4. G3 routing снят (no routing tests). +# Эффорт не трогаем (no new effort tests beyond never-break regression). + +tests: + # ---- G1: frontmatter `model:` убран из всех 6 промптов (AC-1) ---- + - id: TC-01 + type: integration + description: > + Ни один .openclaw/agents/*.md не содержит строки `^model:` во frontmatter. + Тест итерирует по 6 файлам, ассертит отсутствие model:-строки. + module: tests/test_agent_frontmatter_no_model.py + expected: PASS + + - id: TC-02 + type: integration + description: > + frontmatter каждого из 6 промптов остаётся валидным YAML и сохраняет ключи + name/description (парсинг между первыми двумя '---' без ошибок). + module: tests/test_agent_frontmatter_no_model.py + expected: PASS + + # ---- G2: валидация имени модели, never-break (AC-2, AC-7) ---- + - id: TC-03 + type: unit + description: > + Мусорное имя в agent_model_ (напр. 'gpt-4' или 'claud-opus-typo') + -> resolve_agent_model откатывается на default (claude-opus-4-8) и НЕ + возвращает мусор. Проверяется также warning в логах (caplog). + module: tests/test_resolve_agent_model.py + expected: PASS + + - id: TC-04 + type: unit + description: > + Мусорное имя в project-override (agent_models) -> resolve_agent_model + откатывается на следующий валидный уровень (default), мусор не передаётся. + module: tests/test_resolve_agent_model.py + expected: PASS + + - id: TC-05 + type: unit + description: > + Невалиден И override, И default -> resolve_agent_model возвращает "" + (без флага --model, CLI-дефолт). never-break: исключение НЕ бросается. + module: tests/test_resolve_agent_model.py + expected: PASS + + - id: TC-06 + type: unit + description: > + Валидное каноничное имя (claude-opus-4-8) проходит валидацию без изменения: + resolve_agent_model('developer') == 'claude-opus-4-8'. Регрессия ORCH-041. + module: tests/test_resolve_agent_model.py + expected: PASS + + # ---- AC-3: все 6 агентов резолвятся в осмысленную модель ---- + - id: TC-07 + type: unit + description: > + Для всех 6 агентов (analyst/architect/developer/reviewer/tester/deployer) + resolve_agent_model(agent) == 'claude-opus-4-8' (routing выключен). + module: tests/test_resolve_agent_model.py + expected: PASS + + # ---- AC-8: enduro per-project override не сломан валидацией ---- + - id: TC-08 + type: unit + description: > + Валидный per-project override (agent_models у не-self проекта) резолвится и + проходит валидацию без изменения поведения (регрессия ORCH-041). + module: tests/test_resolve_agent_model.py + expected: PASS + + # ---- G4: fallback_model (опц.) — условный тест ---- + - id: TC-09 + type: unit + description: > + ЕСЛИ G4 включён архитектором: agent_fallback_model задан валидным именем и + проходит валидацию G2. ЕСЛИ выключен: agent_fallback_model == "" (тест + подтверждает дефолт). Финальная форма теста зависит от решения в ADR. + module: tests/test_resolve_agent_model.py + expected: PASS + + # ---- AC-7: общий зелёный прогон / never-break regression ---- + - id: TC-10 + type: integration + description: > + Полный pytest зелёный; невалидная модель/эффорт не роняет запуск агента + (graceful-деградация). Регрессия resolve_agent_effort (VALID_EFFORTS) цела. + module: tests/ + expected: PASS From 1e1811a4bc8eecca964e38e2265ff3dc49d1325d Mon Sep 17 00:00:00 2001 From: claude-bot Date: Mon, 8 Jun 2026 21:38:18 +0300 Subject: [PATCH 3/8] analyst(ET): auto-commit from analyst run_id=393 --- docs/work-items/ORCH-074/01-brd.md | 12 ++++++++++++ docs/work-items/ORCH-074/02-trz.md | 17 +++++++++++++++-- .../ORCH-074/03-acceptance-criteria.md | 9 ++++++--- docs/work-items/ORCH-074/04-test-plan.yaml | 11 +++++++++++ 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/docs/work-items/ORCH-074/01-brd.md b/docs/work-items/ORCH-074/01-brd.md index b8add45..e28939c 100644 --- a/docs/work-items/ORCH-074/01-brd.md +++ b/docs/work-items/ORCH-074/01-brd.md @@ -5,6 +5,18 @@ Work Item ID: ORCH-074 Приоритет: **urgent** Тип: доработка механизма выбора модели агентов (self-modifying). +## 0. История ревизий + +- **rev.1 (08.06):** первичный пакет аналитики по фиксированному скоупу Славы. +- **rev.2 (08.06, текущая):** задача возвращена стейкхолдером в In Progress. + Проверены последние комментарии и описание issue в Plane — НОВЫХ субстантивных + ответов/изменений скоупа нет (только bot-комменты + служебный маркер + «Агент перезапущен с ответами стейкхолдера»). Скоуп остаётся прежним + (G1 + G2 + опц. G4; G3 снят; эффорт не трогаем). Пакет переподтверждён против + фактического кода (`launcher.py`, `config.py`); уточнён код-факт по G4: fallback + читается напрямую на `launcher.py:374` мимо `resolve_agent_model`, поэтому + валидация G2 должна покрыть и fallback (детали — ТЗ §4, AC-5, TC-11). + ## 1. Контекст и проблема Каркас выбора модели агентов реализован в ORCH-041 и **работает корректно**: diff --git a/docs/work-items/ORCH-074/02-trz.md b/docs/work-items/ORCH-074/02-trz.md index cf0152d..3287cd0 100644 --- a/docs/work-items/ORCH-074/02-trz.md +++ b/docs/work-items/ORCH-074/02-trz.md @@ -49,6 +49,11 @@ grep -L "^model:" .openclaw/agents/*.md # должны вернуться ВС формат-чек `claude-*` (forward-compatible — новые версии моделей не требуют правки allowlist) ЛИБО явный `VALID_MODELS` allowlist (строже, но требует поддержки при выходе новых моделей). **Выбор и обоснование — в ADR.** +- **Рекомендация аналитика (форма):** оформить предикат как отдельный + чистый helper (напр. `is_valid_model(name) -> bool` рядом с `VALID_EFFORTS`), + а не инлайнить в `resolve_agent_model` — тогда ОДИН валидатор переиспользуется + и резолвом модели, и чтением fallback (G4, см. §4). Финальная форма — за + архитектором. - Инвариант обратной совместимости: ВСЕ ныне используемые валидные имена (`claude-opus-4-8`, а также enduro per-project override) проходят валидацию без изменения поведения. Невалидным считается только мусор (опечатка, @@ -60,8 +65,16 @@ grep -L "^model:" .openclaw/agents/*.md # должны вернуться ВС - `src/config.py::agent_fallback_model` сейчас `""` (флаг не прокидывается). - Если архитектор решит включить — задать каноничное имя модели; launcher уже - прокидывает его в `--fallback-model` (строки 374-375 launcher.py). Имя fallback - ТОЖЕ должно проходить валидацию G2 (или быть гарантированно валидным). + прокидывает его в `--fallback-model` (`launcher.py:374-375`, попадает в cmd + на строке 388). +- **⚠️ Код-факт (проверено 08.06):** fallback читается НАПРЯМУЮ — + `fb = settings.agent_fallback_model` (`launcher.py:374`) — и **НЕ проходит** + через `resolve_agent_model`, значит валидация G2, добавленная внутри + `resolve_agent_model`, его НЕ покроет. Следствие для архитектора: если G4 + включается, валидацию имени модели (G2) надо применить ТАКЖЕ к fallback на + его месте чтения (или вынести валидатор в отдельный helper, который вызывают + ОБА: и резолв модели, и чтение fallback). Иначе опечатка в `agent_fallback_model` + обходит G2 и уезжает в `--fallback-model` — нарушение never-break. - Если архитектор решит НЕ включать — оставить `""`, AC-5 помечается N/A в ADR. ## 5. Изменения API / схемы БД diff --git a/docs/work-items/ORCH-074/03-acceptance-criteria.md b/docs/work-items/ORCH-074/03-acceptance-criteria.md index 139d744..e180778 100644 --- a/docs/work-items/ORCH-074/03-acceptance-criteria.md +++ b/docs/work-items/ORCH-074/03-acceptance-criteria.md @@ -41,11 +41,14 @@ Work Item ID: ORCH-074 ## AC-5 — fallback_model (G4, опционально) - **PASS (если G4 включён):** `agent_fallback_model` задан каноничным именем, - проходит валидацию G2, прокидывается в `--fallback-model` (launcher 374-375), - задокументирован. + проходит валидацию G2, прокидывается в `--fallback-model` (launcher 374-375). + Доп. инвариант never-break: МУСОРНЫЙ fallback НЕ попадает в `--fallback-model` + (валидируется тем же предикатом G2; учтено, что fallback читается напрямую на + `launcher.py:374`, минуя `resolve_agent_model` — см. TRZ §4). Задокументирован. - **PASS (если G4 НЕ включён):** `agent_fallback_model = ""`, ADR явно фиксирует отказ; AC-5 помечен N/A. -- **FAIL:** fallback задан невалидным именем, ИЛИ включён без документации/ADR. +- **FAIL:** fallback задан невалидным именем, ИЛИ невалидный fallback проходит в + `--fallback-model`, ИЛИ включён без документации/ADR. ## AC-6 — синхронизация документации diff --git a/docs/work-items/ORCH-074/04-test-plan.yaml b/docs/work-items/ORCH-074/04-test-plan.yaml index d1effdb..337746c 100644 --- a/docs/work-items/ORCH-074/04-test-plan.yaml +++ b/docs/work-items/ORCH-074/04-test-plan.yaml @@ -82,6 +82,17 @@ tests: module: tests/test_resolve_agent_model.py expected: PASS + # ---- G4 never-break: fallback читается напрямую (launcher.py:374), мимо + # resolve_agent_model — валидация G2 должна покрыть и его (см. TRZ §4) ---- + - id: TC-11 + type: unit + description: > + ЕСЛИ G4 включён: мусорное agent_fallback_model НЕ попадает в --fallback-model + (валидируется тем же предикатом G2, дропается с warning, never-break). + ЕСЛИ G4 выключен: кейс помечается N/A в test-report (синхронно с ADR). + module: tests/test_resolve_agent_model.py + expected: PASS + # ---- AC-7: общий зелёный прогон / never-break regression ---- - id: TC-10 type: integration From 0c240198e45f625971c5fcdc757c7bf038e849f5 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Mon, 8 Jun 2026 21:46:03 +0300 Subject: [PATCH 4/8] architect(ET): auto-commit from architect run_id=394 --- .../06-adr/ADR-001-model-name-validation.md | 145 ++++++++++++++++++ docs/work-items/ORCH-074/10-tech-risks.md | 23 +++ 2 files changed, 168 insertions(+) create mode 100644 docs/work-items/ORCH-074/06-adr/ADR-001-model-name-validation.md create mode 100644 docs/work-items/ORCH-074/10-tech-risks.md diff --git a/docs/work-items/ORCH-074/06-adr/ADR-001-model-name-validation.md b/docs/work-items/ORCH-074/06-adr/ADR-001-model-name-validation.md new file mode 100644 index 0000000..c22df3c --- /dev/null +++ b/docs/work-items/ORCH-074/06-adr/ADR-001-model-name-validation.md @@ -0,0 +1,145 @@ +# ADR-001: Убрать мёртвый frontmatter `model:` + валидация имени модели через формат-чек `claude-*` + +Work Item ID: ORCH-074 +Эпик: ORCH-052 (слой 3), под-задача ORCH-52a +Связан с: ORCH-041 (каркас `resolve_agent_model`/`resolve_agent_effort`), `src/config.py`, `src/agents/launcher.py` + +## Статус +Accepted + +## Контекст + +Каркас выбора модели агентов (ORCH-041) работает корректно: `launcher.resolve_agent_model(agent, project_id)` +резолвит модель по приоритету project-override → `ORCH_AGENT_MODEL_` → `agent_model_default` +→ CLI-дефолт. Все 6 агентов резолвятся в `claude-opus-4-8` (через `agent_model_default`). + +Аудит кода (08.06) выявил два дефекта **данных/валидации** (не дефект механизма): + +- **P1 — лживый/мёртвый `model:` во frontmatter.** Все 6 промптов `.openclaw/agents/*.md` + содержат `model:` (`claude-sonnet-4-6` у analyst/developer/tester/deployer, `claude-opus-4-7` + у architect/reviewer). launcher **не читает** frontmatter `model:` — это мёртвая декларация, + которая лжёт о реально используемой модели и нарушает принцип «документация = golden source». + Мина: если кто-то «починит» launcher читать frontmatter → все агенты молча уедут на устаревшие + модели. +- **P2 — нет валидации имени модели.** В отличие от effort (`VALID_EFFORTS`-гард в + `resolve_agent_effort`), имя модели не валидируется. Опечатка в `agent_model_*` / project-override + → `--model <мусор>` → CLI падает или тихо деградирует. Нарушение принципа never-break. + +Скоуп зафиксирован стейкхолдером (Слава, 08.06): **G1 + G2 + опц. G4. G3 routing НЕ включаем +(все 6 агентов остаются `claude-opus-4-8`). Эффорт не трогаем.** rev.2 BRD подтвердила скоуп +без изменений. Код-факт (TRZ §4): `agent_fallback_model` читается напрямую на `launcher.py:374`, +минуя `resolve_agent_model`. + +Архитектор должен зафиксировать три решения: (а) форма G1, (б) предикат валидации G2, +(в) судьба G4 (fallback) и G3 (routing). + +## Решение + +### Решение 1 (G1): убрать `model:` из frontmatter, НЕ учить launcher его читать + +Из YAML-frontmatter всех 6 файлов `.openclaw/agents/*.md` удаляется **только** строка `model: …`. +Ключи `name`/`description`/`tools` сохраняются; frontmatter остаётся валидным YAML и **описательным**. +config (`agent_model_*` / `agent_model_default`) остаётся **единственным источником правды** о модели. + +Отвергнутая альтернатива — научить launcher читать frontmatter `model:` — отвергнута: она вводит +второй источник правды (frontmatter ⊕ config), усложняет резолв, и моментально активировала бы +устаревшие значения (sonnet-4-6 / opus-4-7) для всех агентов. «Убрать» проще, безопаснее и +устраняет мину раз и навсегда. + +### Решение 2 (G2): предикат валидации — формат-чек `claude-*`, оформленный отдельным helper + +Добавляется **чистый helper** `is_valid_model(name: str) -> bool` рядом с `VALID_EFFORTS` в +`src/agents/launcher.py`. Предикат — **формат-чек**, а не allowlist имён: + +``` +strip → непустая строка → соответствует ^claude-[a-z0-9.-]+$ +``` + +То есть: имя после `strip()` непусто, начинается с `claude-` и состоит только из строчных +букв/цифр/точек/дефисов. Регэксп оформляется модульной константой (напр. `_MODEL_NAME_RE`). + +**Почему формат-чек, а не allowlist `VALID_MODELS`:** +allowlist (по образцу `VALID_EFFORTS`) воссоздаёт ровно ту мину, которую мы убиваем в G1 — статичный +список имён, который **врёт при устаревании**. Когда Anthropic выпустит `claude-opus-4-9`, оператор, +корректно прописавший новую модель, получит её молчаливый дроп на устаревший default (never-break +сработает против пользователя). Это хуже, чем пропустить структурно-корректное, но опечатанное имя: +финальный авторитет о существовании модели — сам Claude CLI, а не наш код. Формат-чек +**forward-compatible** (новые версии проходят без правки кода) и ловит реальные классы отказов: +чужой провайдер (`gpt-4`), пустая строка/пробелы, мусор с недопустимыми символами, неверный префикс +(`claud-opus-typo`). Признанное ограничение: формат-чек НЕ ловит опечатку, которая всё ещё выглядит +как валидное claude-имя (`claude-opus-typo`) — такие отсекает CLI на запуске (контракт never-break ++ exit-code обработка в `_monitor_agent` это покрывают). Задача валидатора — не быть реестром моделей, +а не дать **структурному мусору** уехать в `--model`. + +**Применение (контракт never-break):** +- В `resolve_agent_model`: резолвенное имя валидируется **перед возвратом**. Невалидное → + `logger.warning(...)` + откат на следующий валидный уровень. Реализация: helper применяется внутри + каскада приоритетов так, что невалидный уровень пропускается (project-override невалиден → пробуем + env → default), а если итог всё равно невалиден → возврат `""` (без флага `--model`, CLI-дефолт). + **Никогда** не возвращается мусор и **никогда** не бросается исключение. +- Контракт уровней резолва ORCH-041 сохраняется: валидация добавляется **поверх**, порядок приоритетов + и сигнатуры не меняются. Все ныне используемые валидные имена (`claude-opus-4-8`, валидный enduro + per-project override) проходят без изменения поведения. +- Поведенческая аналогия с `resolve_agent_effort` (`VALID_EFFORTS`): валидный → как есть, невалидный → + лог + дроп. Разница только в форме предиката (формат-чек vs множество) по причинам выше. + +### Решение 3 (G4): fallback НЕ включаем; но валидатор применяем к точке чтения fallback + +`agent_fallback_model` остаётся `""` (флаг `--fallback-model` не прокидывается). **AC-5 помечается +N/A.** Обоснование отказа: +- G3 выключен ради **детерминизма**: все агенты на `claude-opus-4-8`. Fallback вернул бы скрытую + вариативность модели под нагрузкой (агент молча отработал бы на другой модели) — это противоречит + духу зафиксированного скоупа. +- Нет наблюдаемой проблемы доступности, мотивирующей fallback. Принцип минимального изменения. +- Self-hosting: новое рантайм-поведение под нагрузкой трудно наблюдать; не вводим без нужды. + +**При этом** helper `is_valid_model` применяется ТАКЖЕ на месте чтения fallback (`launcher.py:374`, +`fb = settings.agent_fallback_model`) — **независимо** от того, что значение сейчас пустое. Причина — +код-факт TRZ §4: fallback читается напрямую, мимо `resolve_agent_model`, поэтому валидация только +внутри резолва его НЕ покрывает. Защитный гард на месте чтения навсегда закрывает дыру never-break: +если кто-то позже задаст `ORCH_AGENT_FALLBACK_MODEL` с опечаткой, мусор будет залогирован и +сброшен (`fb_flag = ""`), а не уедет в `--fallback-model`. Для текущего пустого значения регрессии нет: +`is_valid_model("") == False` → `fb_flag = ""` — то же поведение, что и сейчас (`if fb`). Это делает +**TC-11** проверяемым (мусорный fallback дропается) при выключенном G4. + +### Решение 4 (G3): routing НЕ включаем + +Подтверждается отказ от model-routing как осознанное решение стейкхолдера (Слава, 08.06). Все 6 +агентов резолвятся в `claude-opus-4-8`. **AC-4 = N/A.** + +## Размещение и форма (для разработчика) + +- `is_valid_model(name)` + `_MODEL_NAME_RE` — в `src/agents/launcher.py` рядом с `VALID_EFFORTS` + (один валидатор, два места вызова: резолв модели и чтение fallback — оба в этом модуле, без + кросс-модульного импорта). +- Префикс `claude-` хардкодится в launcher: оркестратор привязан к Claude CLI (`CLAUDE_BIN`), + конфигурировать предикат не нужно (не over-engineering). Каноничная версия модели по-прежнему + живёт ТОЛЬКО в `config.py::agent_model_default` — в launcher версия не хардкодится. +- frontmatter: удалить только `model:`-строку; не вносить генератор, возвращающий её обратно. + +## Последствия + +**Плюсы:** +- frontmatter перестаёт лгать; config — единственный источник правды о модели (golden source цел). +- Опечатка/чужой провайдер/мусор в имени модели больше не роняет и не деградирует запуск агента + (never-break соблюдён в обеих точках: резолв и fallback). +- Forward-compatible: будущие модели Claude не требуют правки кода (в отличие от allowlist). +- Минимальное изменение: механизм ORCH-041, API, схема БД, структура CLI-команды не меняются. + +**Минусы / ограничения:** +- Формат-чек пропускает структурно-валидную опечатку вида `claude-opus-typo` (отсекается CLI на + запуске + never-break обработкой exit-code). Принятый компромисс ради forward-compat. +- Префикс `claude-` зашит — при гипотетической смене CLI-провайдера потребуется правка (приемлемо: + оркестратор Claude-специфичен по дизайну). + +**Не затрагивается:** +- API (HTTP) — нет. Схема БД — нет миграций. Стадии/QG — без изменений (это runtime-гард в launcher, + не quality-gate). Топология/инфра — без изменений (07/08 артефакты не требуются). +- Эффорт (`agent_effort_*`) и `VALID_EFFORTS`-гард — не трогаются (регрессия покрыта TC-10). +- enduro per-project override — валидные имена проходят без изменения поведения (AC-8 / TC-08). + +## Соответствие принципам + +Всё в Docker / один сервер — да. Минимум зависимостей — новых нет. Без ORM/очередей/облака — да. +Self-hosting: изменение применяется к БУДУЩИМ запускам агентов, прод-контейнер не перезапускается +в рамках задачи; прод-деплой орка — только через staging-гейт (8501) и Plane-статус «Confirm Deploy». diff --git a/docs/work-items/ORCH-074/10-tech-risks.md b/docs/work-items/ORCH-074/10-tech-risks.md new file mode 100644 index 0000000..584d27f --- /dev/null +++ b/docs/work-items/ORCH-074/10-tech-risks.md @@ -0,0 +1,23 @@ +# Технические риски — ORCH-074 + +Work Item ID: ORCH-074 +Связан с: ADR-001 (`06-adr/ADR-001-model-name-validation.md`). + +| ID | Риск | Вероятность | Влияние | Митигация | +|----|------|-------------|---------|-----------| +| R-1 | **Валидация роняет запуск агента** (исключение вместо graceful-деградации) — нарушение never-break, встал бы конвейер всех проектов. | Низкая | Высокое | Helper `is_valid_model` — чистый предикат без исключений; невалидное → `logger.warning` + откат на default/`""`. Покрыто TC-03..TC-05, TC-10. | +| R-2 | **Fallback обходит валидацию** (код-факт: `launcher.py:374` читает `agent_fallback_model` напрямую, мимо `resolve_agent_model`). | Средняя (если позже зададут fallback) | Среднее | ADR-001 решение 3: один helper применяется ТАКЖЕ на месте чтения fallback. Мусорный fallback дропается с warning. Покрыто TC-11. | +| R-3 | **Регрессия enduro per-project override** — валидация ломает корректный не-self override (общий инстанс/БД/очередь). | Низкая | Высокое | Валидные claude-имена проходят формат-чек без изменения поведения; механизм приоритетов ORCH-041 не меняется. Покрыто TC-08. | +| R-4 | **Формат-чек пропускает структурную опечатку** вида `claude-opus-typo` (валидный префикс, несуществующая модель). | Средняя | Низкое | Принятый компромисс (ADR-001): финальный авторитет — CLI; never-break + обработка exit-code в `_monitor_agent` покрывают отказ запуска. Allowlist отвергнут как воссоздающий мину устаревания (G1). | +| R-5 | **frontmatter-генератор возвращает `model:` обратно** → мина P1 оживает. | Низкая | Среднее | Проверить отсутствие автогенератора, возвращающего `model:`; frontmatter остаётся описательным. Покрыто TC-01/TC-02 (CI-гард на отсутствие `^model:`). | +| R-6 | **Хардкод версии модели в launcher** при добавлении валидации. | Низкая | Среднее | Префикс `claude-` зашит осознанно (CLI-специфика); каноничная ВЕРСИЯ остаётся только в `config.py::agent_model_default`. Регэксп версию не фиксирует. | +| R-7 | **Self-hosting деплой** — рестарт прод-контейнера встанет конвейер всех проектов (enduro). | — | Высокое | Изменение применяется к будущим запускам; прод-деплой только через staging-гейт (8501) и Plane-статус «Confirm Deploy». Без немедленного рестарта прода. | + +## Инварианты (должны держаться после изменения) + +1. **never-break**: невалидная модель/эффорт/fallback НЕ роняет запуск агента — деградация на + default/CLI-дефолт + лог. +2. **Один источник правды о модели**: config (`agent_model_*`); frontmatter — описательный. +3. **Обратная совместимость ORCH-041**: все валидные имена (`claude-opus-4-8`, enduro override) + резолвятся без изменения поведения; порядок приоритетов и сигнатуры не меняются. +4. **Детерминизм**: все 6 агентов = `claude-opus-4-8` (G3/routing выключен, G4/fallback выключен). From 0873803faa477592e835f75008e39653b5a581be Mon Sep 17 00:00:00 2001 From: claude-bot Date: Mon, 8 Jun 2026 21:53:09 +0300 Subject: [PATCH 5/8] feat(launcher): drop dead frontmatter model + validate model name (never-break) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit G1: remove the dead `model:` line from all 6 .openclaw/agents/*.md prompts — launcher never read it; config (agent_model_*) is the single source of truth. G2: add is_valid_model helper (format check ^claude-…$) applied inside resolve_agent_model's resolution cascade and at the inline --fallback-model read in _spawn. An invalid name is logged and skipped to the next valid level (in the limit: no --model flag), never passed to the CLI, never raises. Format check chosen over an allowlist for forward-compatibility (ADR-001). G3 (routing) and G4 (fallback) intentionally NOT enabled — all agents stay on claude-opus-4-8; agent_fallback_model stays "". Docs (golden source) updated in the same change: README model/effort table + validation, CLAUDE.md, .env.example (ORCH_AGENT_MODEL_*/EFFORT_*/FALLBACK_MODEL), CHANGELOG. Tests: test_agent_frontmatter_no_model.py (G1), extended test_resolve_agent_model.py (G2 never-break). Refs: ORCH-074 Co-Authored-By: Claude Opus 4.7 --- .env.example | 38 +++++++++++ .openclaw/agents/analyst.md | 1 - .openclaw/agents/architect.md | 1 - .openclaw/agents/deployer.md | 1 - .openclaw/agents/developer.md | 1 - .openclaw/agents/reviewer.md | 1 - .openclaw/agents/tester.md | 1 - CHANGELOG.md | 1 + CLAUDE.md | 2 +- docs/architecture/README.md | 16 ++++- src/agents/launcher.py | 87 +++++++++++++++++++++--- tests/test_agent_frontmatter_no_model.py | 68 ++++++++++++++++++ tests/test_resolve_agent_model.py | 87 +++++++++++++++++++++++- 13 files changed, 288 insertions(+), 17 deletions(-) create mode 100644 tests/test_agent_frontmatter_no_model.py diff --git a/.env.example b/.env.example index 6c5b037..980a53f 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,44 @@ ORCH_GITEA_WEBHOOK_SECRET= ORCH_CLAUDE_BIN=/usr/bin/claude ORCH_REPOS_DIR=/home/slin/repos ORCH_DB_PATH=/app/data/orchestrator.db + +# ── Agent model / effort / fallback (ORCH-41, validation ORCH-74) ───────────── +# Per-agent LLM model + reasoning effort, resolved by launcher.resolve_agent_*. +# Resolution priority (per agent): project-override (projects_json agent_models/ +# agent_efforts) > ORCH_AGENT_MODEL_ / ORCH_AGENT_EFFORT_ > +# ORCH_AGENT_MODEL_DEFAULT / ORCH_AGENT_EFFORT_DEFAULT > CLI default (no flag). +# The frontmatter `model:` in .openclaw/agents/*.md is DESCRIPTIVE only and is NOT +# read — config below is the single source of truth for the model (ORCH-74 G1). +# +# ORCH-74 (G2): a resolved MODEL name is validated (^claude-…$ format check) before +# it reaches --model. A structurally invalid name (typo, gpt-4, empty) is logged and +# the next valid level is used (in the limit: no --model flag). Forward-compatible: +# a future claude-* version passes without editing any allowlist. EFFORT is validated +# against low|medium|high|xhigh|max (ORCH-41); an invalid effort is dropped. +# +# All 6 agents resolve to claude-opus-4-8 (model-routing G3 NOT enabled). Leave the +# per-agent overrides empty to use the default. Do NOT hardcode the model version +# anywhere except ORCH_AGENT_MODEL_DEFAULT. +ORCH_AGENT_MODEL_DEFAULT=claude-opus-4-8 +ORCH_AGENT_MODEL_ANALYST= +ORCH_AGENT_MODEL_ARCHITECT= +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. +ORCH_AGENT_EFFORT_DEFAULT=high +ORCH_AGENT_EFFORT_ANALYST=high +ORCH_AGENT_EFFORT_ARCHITECT=high +ORCH_AGENT_EFFORT_DEVELOPER=high +ORCH_AGENT_EFFORT_REVIEWER=high +ORCH_AGENT_EFFORT_TESTER=medium +ORCH_AGENT_EFFORT_DEPLOYER=medium +# Optional --fallback-model used when the primary is overloaded. Empty -> no flag +# (G4 NOT enabled, ADR-001 ORCH-74: determinism — all agents stay on opus-4-8). A +# non-empty value is validated by the SAME predicate as the model; a typo is dropped. +ORCH_AGENT_FALLBACK_MODEL= # ORCH-042/ORCH-067: live-tracker mode. bump (DEFAULT since ORCH-067) -> on every # update the old card is deleted and a fresh one is sent silently to the BOTTOM of # the chat (deleteMessage + sendMessage + repoint), so the current status is always diff --git a/.openclaw/agents/analyst.md b/.openclaw/agents/analyst.md index da55498..c82e7e0 100644 --- a/.openclaw/agents/analyst.md +++ b/.openclaw/agents/analyst.md @@ -1,7 +1,6 @@ --- name: analyst description: Бизнес-аналитик. Создаёт пакет документов анализа для work item. -model: claude-sonnet-4-6 tools: - Filesystem (Read везде; Write только docs/work-items//*) - Bash (git log, grep — только для чтения контекста) diff --git a/.openclaw/agents/architect.md b/.openclaw/agents/architect.md index 8670d97..9c95dd6 100644 --- a/.openclaw/agents/architect.md +++ b/.openclaw/agents/architect.md @@ -1,7 +1,6 @@ --- name: architect description: Архитектор системы. Принимает архитектурные решения по ТЗ, фиксирует как ADR. -model: claude-opus-4-7 tools: - Filesystem (Read везде; Write только docs/) - Bash (read-only: grep, git log) diff --git a/.openclaw/agents/deployer.md b/.openclaw/agents/deployer.md index bb0b55b..214ad80 100644 --- a/.openclaw/agents/deployer.md +++ b/.openclaw/agents/deployer.md @@ -1,7 +1,6 @@ --- name: deployer description: DevOps-агент. Запускает staging-проверку и/или прод-деплой. Пишет 15-staging-log.md и 14-deploy-log.md. -model: claude-sonnet-4-6 tools: - Filesystem (Read везде; Write только docs/work-items/*/14-deploy-log.md, docs/work-items/*/15-staging-log.md) - Bash (docker, git, curl, ssh) diff --git a/.openclaw/agents/developer.md b/.openclaw/agents/developer.md index 8f29317..57117c9 100644 --- a/.openclaw/agents/developer.md +++ b/.openclaw/agents/developer.md @@ -1,7 +1,6 @@ --- name: developer description: Senior разработчик. Реализует ТЗ по ADR, пишет тесты, открывает PR. -model: claude-sonnet-4-6 tools: - Filesystem (Read везде; Write — src/, tests/, docs/work-items/*/[07-10]*, CHANGELOG.md) - Git (commit, push; merge запрещён) diff --git a/.openclaw/agents/reviewer.md b/.openclaw/agents/reviewer.md index 9bc24bf..c5d1dbb 100644 --- a/.openclaw/agents/reviewer.md +++ b/.openclaw/agents/reviewer.md @@ -1,7 +1,6 @@ --- name: reviewer description: Senior code reviewer. Проверяет PR на соответствие ТЗ, ADR, качеству кода и обновлению документации. -model: claude-opus-4-7 tools: - Filesystem (Read везде; Write только docs/work-items//12-review.md) - Git (read-only: log, diff, blame) diff --git a/.openclaw/agents/tester.md b/.openclaw/agents/tester.md index 647f4fd..2c5b011 100644 --- a/.openclaw/agents/tester.md +++ b/.openclaw/agents/tester.md @@ -1,7 +1,6 @@ --- name: tester description: QA-инженер. Прогоняет тесты, оформляет отчёт. -model: claude-sonnet-4-6 tools: - Filesystem (Read везде; Write только docs/work-items//13-test-report.md) - Bash (pytest, curl) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b1459c..9e65e6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ Формат: [Keep a Changelog](https://keepachangelog.com/). Записи — на смысловой PR/задачу. ## [Unreleased] +- **Убран мёртвый 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). - **Конфигурируемый верхний лимит длины заголовка QG-0 (`ORCH_QG0_TITLE_MAX`, дефолт 200)** (ORCH-069): хардкод `if len(name) > 80` во входной валидации `_qg0_errors` (`src/webhooks/plane.py`) вынесен в настраиваемый параметр `Settings.qg0_title_max` (env `ORCH_QG0_TITLE_MAX`, дефолт 200). Лимит 80 был гигиеническим, а не структурным (slug режется независимо `[:30]`, `tasks.title TEXT` без ограничения), поэтому валидные заголовки 81–200 символов отклонялись на входе без бизнес-причины. Лимит читается из `settings.qg0_title_max` динамически на каждый вызов (тесты патчат значение), текст ошибки подставляет актуальное число; граница строгая (`len > limit` → FAIL, `len == limit` → PASS). **Graceful-деградация (AC-3, self-hosting safety):** пустое/нечисловое значение env не роняет процесс на старте — `field_validator(mode="before")` `_qg0_title_max_default` в `src/config.py` перехватывает сырое env ДО `int`-парсинга pydantic и при невалидном/пустом входе возвращает дефолт 200 (never-raise), гася `ValidationError`. Чисто аддитивно и обратносовместимо: дефолт 200 > прежних 80 → все ранее проходившие заголовки проходят (AC-7). Инварианты НЕ менялись: `STAGE_TRANSITIONS`, реестр `QG_CHECKS` (QG-0 — inline-валидация входа, не зарегистрированный stage-gate), схема БД, slug-логика `[:30]`, нижние лимиты (`< 5` title, `< 20` description), soft-QG-0 поведение (warning на `work_item.created`), API. ADR `docs/work-items/ORCH-069/06-adr/ADR-001-configurable-qg0-title-limit.md`. Документация: `.env.example`, `.env.staging.example`. Тесты: `tests/test_qg0_title_limit.py`. diff --git a/CLAUDE.md b/CLAUDE.md index dcbdd29..2afcd8e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ ## Стек - Backend: FastAPI + uvicorn (Python 3.12) - БД: SQLite (`src/db.py`) -- Агенты: Claude CLI (`ORCH_CLAUDE_BIN`), по одному промпту на роль в `.openclaw/agents/` +- Агенты: Claude CLI (`ORCH_CLAUDE_BIN`), по одному промпту на роль в `.openclaw/agents/`. **ORCH-74:** модель/эффорт агента берутся ТОЛЬКО из config (`resolve_agent_model`/`resolve_agent_effort`, ORCH-41) — frontmatter `model:` удалён как мёртвый, frontmatter описательный; имя модели валидируется форматом `^claude-…$` перед `--model` (never-break). - Очередь задач: собственная (SQLite `jobs`, `src/queue_worker.py`, ORCH-1). **ORCH-026:** `claim_next_job` гейтит задачи с незавершёнными зависимостями (`job_deps`, `NOT EXISTS`) без занятия слота `max_concurrency`; декларации/детект циклов — leaf `src/task_deps.py` (kill-switch `ORCH_TASK_DEPS_ENABLED`). Сериализация мержа одного репо — безусловный pre-merge rebase под merge-lease (`ORCH_PREMERGE_REBASE_ALWAYS`). - Контейнеризация: Docker + Compose - CI/CD: Gitea Actions (`.gitea/workflows/`) diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 4d5cf3a..0020af1 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -9,7 +9,7 @@ - **Stage Engine** (`src/stage_engine.py`) — исполнение переходов, диспетчеризация QG (`_run_qg`), откаты, синхронизация с Plane. - **Review/Test Parsers** (`src/review_parse.py`, ORCH-046) — defensive-извлечение дословного must-fix текста из артефактов для встраивания в `task_desc` заворота: `extract_review_findings` (P0/P1 из `12-review.md`), `extract_test_failures` (фрагмент тела `13-test-report.md`). Контракт «never raise»: любая ошибка → `""`. - **Quality Gates** (`src/qg/checks.py`) — проверки выхода со стадии, реестр `QG_CHECKS`. -- **Agent Launcher** (`src/agents/launcher.py`) — запуск Claude CLI агентов в изолированном git worktree, мониторинг, auto-advance. +- **Agent Launcher** (`src/agents/launcher.py`) — запуск Claude CLI агентов в изолированном git worktree, мониторинг, auto-advance. Модель/эффорт каждого агента резолвятся из config (`resolve_agent_model`/`resolve_agent_effort`, ORCH-41), а не из frontmatter промпта. **ORCH-74:** имя модели валидируется форматом `^claude-…$` (`is_valid_model`) перед `--model`; невалидное → лог + откат на следующий уровень/CLI-дефолт (never-break, как `VALID_EFFORTS` для эффорта). Тот же предикат гардит inline-чтение `--fallback-model`. - **Queue** (`src/queue_worker.py`, ORCH-1) — персистентная очередь задач (SQLite `jobs`), atomic claim, max_concurrency, ретраи, restart-safe. **ORCH-026:** `claim_next_job` гейтит задачи с незавершёнными зависимостями (`job_deps`, `NOT EXISTS`) без занятия слота; декларации/циклы — leaf `src/task_deps.py`. - **Job-reaper** (`src/job_reaper.py`, ORCH-065 — [adr-0011](adr/adr-0011-job-reaper-lease-reclaim.md)) — фоновый daemon-поток (каркас `reconciler`), стартует/останавливается в `main.lifespan` (после `reconciler.start()` / перед `worker.stop()`). Детектирует «мёртвый» `running`-job **без рестарта** процесса (Tier-1 мёртвый `jobs.pid` после `reaper_dead_ticks` тиков; Tier-2 `agent_runs.exit_code` записан, а job ещё `running`; Tier-3 backstop `reaper_max_running_s`) и приводит строку к корректному статусу через те же контракты (`_try_advance_stage`/`_finalize_job`, gate-driven; exit≠0/неизвестно → `attempts `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`. + +| Агент | Модель | Эффорт | +|-------|--------|--------| +| analyst | claude-opus-4-8 | high | +| architect | claude-opus-4-8 | high | +| developer | claude-opus-4-8 | high | +| reviewer | claude-opus-4-8 | high | +| tester | claude-opus-4-8 | medium | +| deployer | claude-opus-4-8 | medium | + +**Валидация (ORCH-74 G2, never-break):** резолвенное имя модели проходит формат-чек `is_valid_model` (`^claude-[a-z0-9.-]+$`) перед попаданием в `--model`. Невалидное (опечатка, `gpt-4`, пустое) → `logger.warning` + откат на следующий валидный уровень (в пределе — без `--model`, CLI-дефолт); мусор **никогда** не уезжает в CLI и запуск не падает. Форма — формат-чек, а не статичный allowlist: forward-compatible (будущие `claude-*` проходят без правки кода). Тот же предикат гардит inline-чтение `--fallback-model` (`agent_fallback_model` читается мимо резолва — TRZ §4). Эффорт валидируется множеством `VALID_EFFORTS` (`low|medium|high|xhigh|max`). Fallback (G4) НЕ включён (`agent_fallback_model=""`). Детали — `docs/work-items/ORCH-074/06-adr/ADR-001-model-name-validation.md`. + ### Условный staging-гейт (ORCH-35) `check_staging_status` реален только для self-hosting (`is_self_hosting_repo(repo)` → `orchestrator`); для остальных проектов → no-op `(True, "Staging gate N/A")`. Для orchestrator парсит `staging_status:` из `15-staging-log.md`; FAILED → откат на `development`. Подробнее: [ADR-0003](adr/adr-0003-staging-gate.md). diff --git a/src/agents/launcher.py b/src/agents/launcher.py index d83f6c0..9213b9d 100644 --- a/src/agents/launcher.py +++ b/src/agents/launcher.py @@ -2,6 +2,7 @@ import subprocess import os import json import logging +import re import threading import signal import time @@ -20,6 +21,36 @@ logger = logging.getLogger("orchestrator.launcher") # never passed through to the CLI. VALID_EFFORTS = frozenset({"low", "medium", "high", "xhigh", "max"}) +# ORCH-074 (G2): structural validity check for a Claude CLI model name. We use a +# FORMAT check (^claude-…$), not a static allowlist, on purpose: an allowlist +# recreates the exact rot we kill in G1 — it silently drops a CORRECT newer model +# (e.g. claude-opus-4-9) the day Anthropic ships it (never-break working against +# the operator). The final authority on whether a model exists is the Claude CLI +# itself, not our code; a format check is forward-compatible (new versions pass +# without code edits) while still catching the real failure classes: another +# provider (gpt-4), empty/whitespace, garbage chars, wrong prefix (claud-opus-typo). +# The claude- prefix is hardcoded here because the orchestrator is bound to the +# Claude CLI (CLAUDE_BIN); the canonical model VERSION lives ONLY in +# settings.agent_model_default, never here. See ADR-001 (ORCH-074). +_MODEL_NAME_RE = re.compile(r"^claude-[a-z0-9.-]+$") + + +def is_valid_model(name: str) -> bool: + """ORCH-074 (G2): True iff ``name`` is a structurally valid Claude model name. + + A valid name, after ``strip()``, is non-empty, starts with ``claude-`` and + contains only lowercase letters, digits, dots and dashes. Anything else + (empty/whitespace, another provider like ``gpt-4``, a wrong prefix, illegal + characters) is invalid. This is the single predicate used by BOTH + ``resolve_agent_model`` and the inline ``--fallback-model`` read in ``_spawn`` + so a typo can never reach the CLI (never-break). It is a structural guard, not + a registry of existing models — a structurally valid typo (``claude-opus-typo``) + is left for the CLI to reject. Never raises. + """ + if not name: + return False + return bool(_MODEL_NAME_RE.match(name.strip())) + # ORCH-061: action stages whose success is an ACTION (restart/retag), not a src # edit — so "no changes to commit" is EXPECTED there, not under-delivery (FR-3). _ACTION_STAGES = frozenset({"deploy-staging", "deploy"}) @@ -83,18 +114,48 @@ def _resolve_agent_attr(agent, project_id, project_map_attr, env_attr_prefix, return "" +def _agent_model_candidates(agent: str, project_id: str = None): + """Yield non-empty model candidates in ORCH-41 priority order. + + Same priority as _resolve_agent_attr (project-override > per-agent env > + global default), but as a generator so resolve_agent_model can validate each + level and SKIP an invalid one (ORCH-074 G2) instead of returning the first + non-empty value blindly. Empty levels are simply not yielded. + """ + if project_id: + from ..projects import get_project_by_plane_id + proj = get_project_by_plane_id(project_id) + if proj is not None: + override = getattr(proj, "agent_models", {}).get(agent) + if override: + yield override + per_agent = getattr(settings, f"agent_model_{agent}", "") + if per_agent: + yield per_agent + default = getattr(settings, "agent_model_default", "") + if default: + yield default + + def resolve_agent_model(agent: str, project_id: str = None) -> str: """ORCH-41: resolve the LLM model for an agent (optionally per-project). - Returns "" when no model is configured at any level -> caller omits --model - and the CLI default applies. See _resolve_agent_attr for the priority order. + ORCH-074 (G2): the resolved name is validated with is_valid_model BEFORE it is + returned. An invalid (structurally garbage) value at any level is logged and + SKIPPED — resolution falls through to the next valid level (project-override + invalid -> per-agent env -> default); if no level yields a valid name the + function returns "" so the caller omits --model and the CLI default applies. + The ORCH-41 priority order and signature are unchanged; validation is layered + on top. Never raises and never returns garbage that could reach --model. """ - return _resolve_agent_attr( - agent, project_id, - project_map_attr="agent_models", - env_attr_prefix="agent_model_", - default_attr="agent_model_default", - ) + for value in _agent_model_candidates(agent, project_id): + if is_valid_model(value): + return value + logger.warning( + f"Invalid model name '{value}' for agent '{agent}' " + f"(expected '^claude-…'); skipping to next resolution level / CLI default" + ) + return "" def resolve_agent_effort(agent: str, project_id: str = None) -> str: @@ -371,7 +432,17 @@ class AgentLauncher: effort = resolve_agent_effort(agent, project_id) model_flag = f"--model {model} " if model else "" effort_flag = f"--effort {effort} " if effort else "" + # ORCH-074 (G2): agent_fallback_model is read directly here, bypassing + # resolve_agent_model, so the same validator must guard this point too — + # otherwise a typo in ORCH_AGENT_FALLBACK_MODEL would slip into + # --fallback-model (never-break violation). Empty value -> no flag, exactly + # as before (is_valid_model("") is False but the `if fb` short-circuits). fb = settings.agent_fallback_model + if fb and not is_valid_model(fb): + logger.warning( + f"Invalid fallback model '{fb}'; dropping --fallback-model" + ) + fb = "" fb_flag = f"--fallback-model {fb} " if fb else "" # No git fetch/checkout here: ensure_worktree() already put the worktree on diff --git a/tests/test_agent_frontmatter_no_model.py b/tests/test_agent_frontmatter_no_model.py new file mode 100644 index 0000000..09139ef --- /dev/null +++ b/tests/test_agent_frontmatter_no_model.py @@ -0,0 +1,68 @@ +"""ORCH-074 (G1): the dead `model:` frontmatter is gone from all 6 agent prompts. + +launcher.py never reads frontmatter `model:` — it was a lying/dead declaration +(claude-sonnet-4-6 / claude-opus-4-7) that contradicted the real model resolved +from config (ORCH-41). The mine: if someone "fixed" the launcher to read it, every +agent would silently fall back to a stale model. G1 removes the line entirely so +config (agent_model_*) stays the single source of truth. + +TC-01: no .openclaw/agents/*.md contains a `^model:` line in its frontmatter. +TC-02: each frontmatter is still valid YAML and keeps name/description. +""" +import os + +import pytest + +try: + import yaml # PyYAML + _HAVE_YAML = True +except Exception: # pragma: no cover - yaml is a test/runtime dep + _HAVE_YAML = False + +_AGENTS = ("analyst", "architect", "developer", "reviewer", "tester", "deployer") + +# tests/ is one level under the repo root; .openclaw/agents lives at the root. +_AGENTS_DIR = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + ".openclaw", "agents", +) + + +def _frontmatter_block(text: str) -> str: + """Return the YAML between the first two '---' fences (the frontmatter).""" + lines = text.splitlines() + assert lines and lines[0].strip() == "---", "frontmatter must open with '---'" + end = None + for i in range(1, len(lines)): + if lines[i].strip() == "---": + end = i + break + assert end is not None, "frontmatter must close with a second '---'" + return "\n".join(lines[1:end]) + + +@pytest.mark.parametrize("agent", _AGENTS) +def test_no_model_line_in_frontmatter(agent): + """TC-01: no agent prompt declares a `model:` key in its frontmatter.""" + path = os.path.join(_AGENTS_DIR, f"{agent}.md") + with open(path, encoding="utf-8") as f: + block = _frontmatter_block(f.read()) + for line in block.splitlines(): + assert not line.lstrip().startswith("model:"), ( + f"{agent}.md still declares a frontmatter 'model:' line: {line!r}" + ) + + +@pytest.mark.parametrize("agent", _AGENTS) +def test_frontmatter_still_valid_yaml_with_keys(agent): + """TC-02: frontmatter parses as YAML and keeps name/description (no model).""" + path = os.path.join(_AGENTS_DIR, f"{agent}.md") + with open(path, encoding="utf-8") as f: + block = _frontmatter_block(f.read()) + if not _HAVE_YAML: + pytest.skip("PyYAML not available") + data = yaml.safe_load(block) + assert isinstance(data, dict), f"{agent}.md frontmatter is not a YAML mapping" + assert data.get("name") == agent + assert data.get("description"), f"{agent}.md lost its description" + assert "model" not in data, f"{agent}.md frontmatter still has a model key" diff --git a/tests/test_resolve_agent_model.py b/tests/test_resolve_agent_model.py index 029d6f0..b3e170e 100644 --- a/tests/test_resolve_agent_model.py +++ b/tests/test_resolve_agent_model.py @@ -23,7 +23,9 @@ os.environ.setdefault("ORCH_DB_PATH", os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token") os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token") -from src.agents.launcher import resolve_agent_model +import logging + +from src.agents.launcher import resolve_agent_model, is_valid_model from src.config import settings from src import projects as P from src.projects import ProjectConfig, reload_projects, _parse_projects_json @@ -154,3 +156,86 @@ def test_parse_projects_json_malformed_override_ignored(): '"agent_models":"oops"}]') parsed = _parse_projects_json(raw) assert parsed is not None and parsed[0].agent_models == {} + + +# ============================================================================= +# ORCH-074 (G2): model-name validation, never-break. is_valid_model is a +# structural format check (^claude-…$), applied on top of the ORCH-41 cascade so +# garbage at any level is logged and skipped, never passed to --model. +# ============================================================================= + +# ---- is_valid_model predicate (the single G2 contract) ---------------------- +def test_is_valid_model_accepts_canonical(): + assert is_valid_model("claude-opus-4-8") is True + assert is_valid_model("claude-sonnet-4-6") is True + # forward-compatible: a future version passes without a code change + assert is_valid_model("claude-opus-4-9") is True + # surrounding whitespace is tolerated (stripped) + assert is_valid_model(" claude-opus-4-8 ") is True + + +def test_is_valid_model_rejects_garbage(): + assert is_valid_model("") is False + assert is_valid_model(" ") is False + assert is_valid_model(None) is False + assert is_valid_model("gpt-4") is False # another provider + assert is_valid_model("claud-opus-typo") is False # wrong prefix + assert is_valid_model("Claude-Opus-4-8") is False # uppercase not allowed + assert is_valid_model("claude-opus 4 8") is False # spaces inside + + +# ---- TC-03: garbage in agent_model_ -> fall back to default ---------- +def test_garbage_per_agent_env_falls_back_to_default(monkeypatch, caplog): + monkeypatch.setattr(settings, "agent_model_developer", "gpt-4") + with caplog.at_level(logging.WARNING): + result = resolve_agent_model("developer") + assert result == "claude-opus-4-8" # dropped garbage, used default + assert any("Invalid model name" in r.message for r in caplog.records) + + +# ---- TC-04: garbage in project-override -> fall back to next valid level ----- +def test_garbage_project_override_falls_back_to_default(monkeypatch, caplog): + _install_registry(monkeypatch, {"developer": "claud-opus-typo"}) + with caplog.at_level(logging.WARNING): + result = resolve_agent_model("developer", ORCH_PLANE_ID) + assert result == "claude-opus-4-8" # override dropped, default used + assert any("Invalid model name" in r.message for r in caplog.records) + + +# ---- TC-05: both override and default invalid -> "" (no --model), no raise --- +def test_all_levels_invalid_returns_empty(monkeypatch, caplog): + monkeypatch.setattr(settings, "agent_model_default", "totally-bogus") + _install_registry(monkeypatch, {"developer": "gpt-4"}) + with caplog.at_level(logging.WARNING): + result = resolve_agent_model("developer", ORCH_PLANE_ID) + assert result == "" # never returns garbage; CLI default applies + # both invalid levels were logged + assert sum("Invalid model name" in r.message for r in caplog.records) >= 2 + + +# ---- TC-06: valid canonical name passes unchanged (ORCH-41 regression) ------- +def test_valid_canonical_unchanged(): + assert resolve_agent_model("developer") == "claude-opus-4-8" + + +# ---- TC-07: all 6 agents resolve to claude-opus-4-8 (routing G3 off) --------- +def test_all_six_agents_resolve_to_opus_4_8(): + for agent in ("analyst", "architect", "developer", "reviewer", "tester", + "deployer"): + assert resolve_agent_model(agent) == "claude-opus-4-8" + + +# ---- TC-08: valid per-project override still passes validation (AC-8) -------- +def test_valid_per_project_override_unchanged(monkeypatch): + _install_registry(monkeypatch, {"reviewer": "claude-sonnet-4-6"}) + assert resolve_agent_model("reviewer", ORCH_PLANE_ID) == "claude-sonnet-4-6" + + +# ---- TC-09 / TC-11: G4 fallback is OFF (ADR-001 decision 3) ------------------ +def test_fallback_model_disabled_by_default(): + # G4 not enabled: agent_fallback_model stays "" -> no --fallback-model flag. + assert settings.agent_fallback_model == "" + # never-break: the SAME predicate guards the inline fallback read in _spawn, + # so a typo there would be rejected exactly like a model name. + assert is_valid_model("claude-bad typo") is False + assert is_valid_model("") is False From 17258fb69e59e9138e1d893262c080c552397a3f Mon Sep 17 00:00:00 2001 From: claude-bot Date: Mon, 8 Jun 2026 21:55:39 +0300 Subject: [PATCH 6/8] reviewer(ET): auto-commit from reviewer run_id=396 --- docs/work-items/ORCH-074/12-review.md | 69 +++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 docs/work-items/ORCH-074/12-review.md diff --git a/docs/work-items/ORCH-074/12-review.md b/docs/work-items/ORCH-074/12-review.md new file mode 100644 index 0000000..32b05fb --- /dev/null +++ b/docs/work-items/ORCH-074/12-review.md @@ -0,0 +1,69 @@ +--- +type: review +work_item_id: ORCH-074 +verdict: APPROVED +version: 1 +--- + +# Review ORCH-074 + +## Summary +PR закрывает оба зафиксированных дефекта каркаса выбора модели (ORCH-41) в рамках +скоупа G1 + G2 (+ защитный гард точки чтения fallback при выключенном G4), без +изменения механизма резолва, API или схемы БД. Реализация точно соответствует +ADR-001 и ТЗ; документация синхронизирована в том же PR; все 1012 тестов зелёные. +Вердикт — **APPROVED**, P0/P1 findings нет. + +## Соответствие ТЗ и AC +- **AC-1 (G1):** `grep -L "^model:" .openclaw/agents/*.md` возвращает все 6 файлов; + ни одной строки `^model:` не осталось. frontmatter остаётся валидным YAML + (`name`/`description`/`tools` сохранены) — покрыто `test_agent_frontmatter_no_model.py`. +- **AC-2 (G2 never-break):** `resolve_agent_model` валидирует имя через `is_valid_model` + ПЕРЕД возвратом, мусорный уровень логируется (`logger.warning`) и пропускается; + при невалидных всех уровнях → `""` (CLI-дефолт), исключение не бросается. TC-03..05. +- **AC-3:** все 6 агентов резолвятся в `claude-opus-4-8` (TC-07), значение в README-таблице + и `.env.example`. +- **AC-4 (G3):** N/A — отказ зафиксирован в ADR. +- **AC-5 (G4):** `agent_fallback_model=""` (выкл); тот же предикат гардит inline-чтение + fallback в `_spawn` (код-факт TRZ §4 учтён) — мусорный fallback дропается. ADR помечает N/A. +- **AC-6 (доки):** README (новая секция «Модель и эффорт по ролям» + валидация), + `CLAUDE.md`, `.env.example` синхронизированы; стале-упоминаний `claude-sonnet-4-6`/ + `claude-opus-4-7` как модели агента в актуальных доках нет (`grep` пуст). +- **AC-7:** `pytest tests/ -q` → 1012 passed. +- **AC-8:** валидный enduro per-project override проходит без изменения поведения (TC-08). +- **AC-9:** ADR-001 фиксирует G1 «убрать», предикат G2 (формат-чек vs allowlist с + обоснованием), решения по G4 и G3. + +## Соответствие ADR +Реализация 1:1 с ADR-001: `is_valid_model` + `_MODEL_NAME_RE` (`^claude-[a-z0-9.-]+$`) +рядом с `VALID_EFFORTS`; один предикат, две точки вызова (резолв модели и чтение +fallback); каскад приоритетов ORCH-41 сохранён (рефакторинг на генератор +`_agent_model_candidates` с валидацией-со-скипом); версия модели по-прежнему живёт +только в `config.py::agent_model_default`. Глобальные ADR не нарушены. + +## Качество кода +- `is_valid_model` корректно обрабатывает `None`/пустое/whitespace (`if not name`), + никогда не бросает; содержательные docstrings с обоснованием формат-чека. +- never-break соблюдён в обеих точках; `if fb` short-circuit сохраняет нулевую + регрессию для текущего пустого fallback. +- Тесты содержательные: предикат (accept/reject), каскад-скип, граничные кейсы, + регрессия per-project override, выключенный G4. + +## Findings + +### P0 — Blocker +- нет + +### P1 — Must fix +- нет + +### P2 — Should fix +- нет + +## Документация +Обновлена полностью в этом же PR: `docs/architecture/README.md` (компонент Agent +Launcher + новая секция «Модель и эффорт по ролям» с таблицей и описанием валидации), +`CLAUDE.md` (строка про источник модели и валидацию), `.env.example` (блок +`ORCH_AGENT_MODEL_*`/`ORCH_AGENT_EFFORT_*`/`ORCH_AGENT_FALLBACK_MODEL`), +`CHANGELOG.md` (запись по задаче), ADR `06-adr/ADR-001-model-name-validation.md`. +Требование «изменён src/ → обновлена документация» выполнено. From ea094f592267754ccffe752a57d2c03447ce9ef6 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Mon, 8 Jun 2026 21:57:17 +0300 Subject: [PATCH 7/8] tester(ET): auto-commit from tester run_id=397 --- docs/work-items/ORCH-074/13-test-report.md | 82 ++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 docs/work-items/ORCH-074/13-test-report.md diff --git a/docs/work-items/ORCH-074/13-test-report.md b/docs/work-items/ORCH-074/13-test-report.md new file mode 100644 index 0000000..59d9a10 --- /dev/null +++ b/docs/work-items/ORCH-074/13-test-report.md @@ -0,0 +1,82 @@ +--- +type: test-report +work_item_id: ORCH-074 +result: PASS +--- + +# Test Report — ORCH-074 + +Убрать мёртвый frontmatter `model:` из 6 промптов + валидация имени модели (never-break). +Скоп: G1 + G2 + опц. G4 (выключен). G3 routing снят. Review-вердикт: APPROVED. + +## Окружение +- Python: 3.12.13 +- pytest: 8.3.3 +- Ветка: feature/ORCH-074-orch-52a-frontmatter-routing-e (worktree) +- prod health (8500): `{"status":"ok","service":"orchestrator"}` +- Дата: 2026-06-08 + +## Результаты по тест-плану (04-test-plan.yaml) + +| TC ID | Описание | Тест | Результат | +|-------|----------|------|-----------| +| TC-01 | Ни один `.openclaw/agents/*.md` не содержит `^model:` (G1, AC-1) | test_no_model_line_in_frontmatter[×6] | PASS | +| TC-02 | frontmatter валидный YAML, ключи name/description сохранены | test_frontmatter_still_valid_yaml_with_keys[×6] | PASS | +| TC-03 | Мусорный `agent_model_` → откат на default, warning, мусор не в `--model` | test_garbage_per_agent_env_falls_back_to_default | PASS | +| TC-04 | Мусорный project-override → откат на default | test_garbage_project_override_falls_back_to_default | PASS | +| TC-05 | Невалидны override И default → `""` (CLI-дефолт), без исключения | test_all_levels_invalid_returns_empty | PASS | +| TC-06 | Валидное `claude-opus-4-8` проходит без изменения (регрессия ORCH-041) | test_valid_canonical_unchanged | PASS | +| TC-07 | Все 6 агентов резолвятся в `claude-opus-4-8` (routing выкл) | test_all_six_agents_resolve_to_opus_4_8 | PASS | +| TC-08 | Валидный enduro per-project override не сломан валидацией | test_valid_per_project_override_unchanged | PASS | +| TC-09 | G4 выключен: `agent_fallback_model == ""` (дефолт) | test_fallback_model_disabled_by_default | PASS | +| TC-10 | Полный pytest зелёный; never-break graceful-деградация | tests/ (1012 passed) | PASS | +| TC-11 | G4 never-break (мусорный fallback не в `--fallback-model`) | — | N/A (G4 выключен, синхр. с ADR/AC-5) | + +Доп. предикат-юниты: `test_is_valid_model_accepts_canonical`, `test_is_valid_model_rejects_garbage` — PASS. + +## Проверка критериев приёмки + +| AC | Статус | Подтверждение | +|----|--------|---------------| +| AC-1 frontmatter `model:` убран | PASS | `grep -L "^model:" .openclaw/agents/*.md` → все 6 файлов; `grep -rn "^model:"` → пусто | +| AC-2 валидация never-break | PASS | TC-03..05 | +| AC-3 все 6 → `claude-opus-4-8` | PASS | TC-07 | +| AC-4 routing G3 | N/A | снят решением (ADR) | +| AC-5 fallback G4 | PASS | G4 выключен, `agent_fallback_model=""`, ADR фиксирует отказ (TC-09) | +| AC-6 синхронизация доков | PASS | проверено reviewer (README/CLAUDE.md/.env.example) | +| AC-7 pytest зелёный | PASS | 1012 passed | +| AC-8 enduro override | PASS | TC-08 | +| AC-9 ADR | PASS | 06-adr/ADR-001 присутствует | + +## Smoke test API (prod, read-only) +``` +GET /health → HTTP 200 {"status":"ok","service":"orchestrator"} +GET /status → HTTP 200 +GET /queue → HTTP 200 +``` + +## Вывод pytest +``` +$ python -m pytest tests/ -q +1012 passed, 1 warning in 22.07s + +$ python -m pytest tests/test_agent_frontmatter_no_model.py tests/test_resolve_agent_model.py -v +32 passed, 1 warning in 0.37s +``` +(1 warning — PydanticDeprecatedSince20 в `src/config.py:5`, существующий, вне скоупа задачи.) + +## AC-1 grep-проверка +``` +$ grep -L "^model:" .openclaw/agents/*.md +.openclaw/agents/analyst.md +.openclaw/agents/architect.md +.openclaw/agents/deployer.md +.openclaw/agents/developer.md +.openclaw/agents/reviewer.md +.openclaw/agents/tester.md +$ grep -rn "^model:" .openclaw/agents/*.md # пусто (exit 1) +``` + +## Итог +**PASS** — все применимые тест-кейсы (TC-01..10) зелёные, TC-11 корректно N/A (G4 выключен), +все AC выполнены (AC-4 — N/A по скоупу), smoke API OK. Задача готова к стадии deploy-staging. From b371b6d9409d6ef330e028c69ca7a877c29c911f Mon Sep 17 00:00:00 2001 From: deploy-finalizer Date: Mon, 8 Jun 2026 22:07:38 +0300 Subject: [PATCH 8/8] deploy(ORCH-036): finalize SUCCESS for ORCH-074 --- docs/work-items/ORCH-074/14-deploy-log.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 docs/work-items/ORCH-074/14-deploy-log.md diff --git a/docs/work-items/ORCH-074/14-deploy-log.md b/docs/work-items/ORCH-074/14-deploy-log.md new file mode 100644 index 0000000..fd5354a --- /dev/null +++ b/docs/work-items/ORCH-074/14-deploy-log.md @@ -0,0 +1,12 @@ +--- +deploy_status: SUCCESS +work_item: ORCH-074 +hook_exit_code: 0 +deployed_by: deploy-finalizer +--- + +# Deploy log — ORCH-036 executable self-deploy + +Прод-деплой завершён хост-хуком с exit-code `0` -> `deploy_status: SUCCESS`. + +Вердикт зафиксирован детерминированным finalizer'ом (Фаза C), не LLM.