Merge pull request 'feat(launcher): ORCH-074 drop dead frontmatter model + validate model name (never-break)' (#79) from feature/ORCH-074-orch-52a-frontmatter-routing-e into main
Some checks failed
CI / test (push) Has been cancelled
Some checks failed
CI / test (push) Has been cancelled
This commit was merged in pull request #79.
This commit is contained in:
38
.env.example
38
.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_<AGENT> / ORCH_AGENT_EFFORT_<AGENT> >
|
||||
# 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
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
name: analyst
|
||||
description: Бизнес-аналитик. Создаёт пакет документов анализа для work item.
|
||||
model: claude-sonnet-4-6
|
||||
tools:
|
||||
- Filesystem (Read везде; Write только docs/work-items/<plane-id>/*)
|
||||
- Bash (git log, grep — только для чтения контекста)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
name: architect
|
||||
description: Архитектор системы. Принимает архитектурные решения по ТЗ, фиксирует как ADR.
|
||||
model: claude-opus-4-7
|
||||
tools:
|
||||
- Filesystem (Read везде; Write только docs/)
|
||||
- Bash (read-only: grep, git log)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 запрещён)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
name: reviewer
|
||||
description: Senior code reviewer. Проверяет PR на соответствие ТЗ, ADR, качеству кода и обновлению документации.
|
||||
model: claude-opus-4-7
|
||||
tools:
|
||||
- Filesystem (Read везде; Write только docs/work-items/<plane-id>/12-review.md)
|
||||
- Git (read-only: log, diff, blame)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
name: tester
|
||||
description: QA-инженер. Прогоняет тесты, оформляет отчёт.
|
||||
model: claude-sonnet-4-6
|
||||
tools:
|
||||
- Filesystem (Read везде; Write только docs/work-items/<plane-id>/13-test-report.md)
|
||||
- Bash (pytest, curl)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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/`)
|
||||
|
||||
@@ -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<max`→`queued`, иначе `failed`+Telegram). Атомарный reap-claim (guard `status='running'`) совместим со стартовым `requeue_running_jobs`. Тот же поток периодически делает проактивный реклейм stale/dead merge-lease (см. ниже). never-raise; kill-switch `ORCH_REAPER_ENABLED`; снимок в `GET /queue` (блок `reaper`).
|
||||
- **Reconciler** (`src/reconciler.py`, ORCH-053 — реализовано, [adr-0007](adr/adr-0007-reconciler.md)) — фоновый daemon-поток (паттерн `queue_worker`), стартует/останавливается в `main.lifespan` (после `worker.start()` / перед `worker.stop()`). Реконсилирует рассинхрон «источник истины ≠ стадия задачи» при потерянном webhook. F-1 gate-side (продвигает застрявшую стадию по локальной БД через штатный `advance_stage(..., finished_agent=None)`), F-2 plane-side (опрос Plane API → `handle_*` из `plane.py`), F-3 (БД-fallback `sha→branch` в `handle_ci_status`). Источник истины — гейт/Plane, не событие; идемпотентность (active-job guard + atomic-claim + grace); kill-switch `ORCH_RECONCILE_ENABLED`. `analysis` F-1 не трогает (человеческий гейт). F-1 также пропускает escalated (retry≥лимита) и Blocked/Needs-Input задачи (ORCH-060). Наблюдаемость — блок `reconcile` в `GET /queue`.
|
||||
@@ -41,6 +41,20 @@ 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_<AGENT>`/`ORCH_AGENT_EFFORT_<AGENT>` > `*_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).
|
||||
|
||||
|
||||
7
docs/work-items/ORCH-074/00-business-request.md
Normal file
7
docs/work-items/ORCH-074/00-business-request.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Business Request: ORCH-52a: фикс модели/эффорта агентов (мёртвый frontmatter → routing+effort)
|
||||
|
||||
Work Item ID: ORCH-074
|
||||
|
||||
## Description
|
||||
|
||||
TBD
|
||||
89
docs/work-items/ORCH-074/01-brd.md
Normal file
89
docs/work-items/ORCH-074/01-brd.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# BRD — ORCH-074: фикс модели агентов (мёртвый frontmatter → валидация имени)
|
||||
|
||||
Work Item ID: ORCH-074
|
||||
Эпик: ORCH-052 (слой 3), под-задача ORCH-52a
|
||||
Приоритет: **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 и **работает корректно**:
|
||||
`src/agents/launcher.py::resolve_agent_model(agent, project_id)` резолвит модель
|
||||
по приоритету project-override → `ORCH_AGENT_MODEL_<AGENT>` → `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 повышает доступность при перегрузке основной модели.
|
||||
112
docs/work-items/ORCH-074/02-trz.md
Normal file
112
docs/work-items/ORCH-074/02-trz.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# ТЗ — 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.**
|
||||
- **Рекомендация аналитика (форма):** оформить предикат как отдельный
|
||||
чистый helper (напр. `is_valid_model(name) -> bool` рядом с `VALID_EFFORTS`),
|
||||
а не инлайнить в `resolve_agent_model` — тогда ОДИН валидатор переиспользуется
|
||||
и резолвом модели, и чтением fallback (G4, см. §4). Финальная форма — за
|
||||
архитектором.
|
||||
- Инвариант обратной совместимости: ВСЕ ныне используемые валидные имена
|
||||
(`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` (`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 / схемы БД
|
||||
|
||||
- **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».
|
||||
81
docs/work-items/ORCH-074/03-acceptance-criteria.md
Normal file
81
docs/work-items/ORCH-074/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# Критерии приёмки — 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).
|
||||
Доп. инвариант 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 задан невалидным именем, ИЛИ невалидный fallback проходит в
|
||||
`--fallback-model`, ИЛИ включён без документации/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 отсутствует или не покрывает эти решения.
|
||||
103
docs/work-items/ORCH-074/04-test-plan.yaml
Normal file
103
docs/work-items/ORCH-074/04-test-plan.yaml
Normal file
@@ -0,0 +1,103 @@
|
||||
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_<agent> (напр. '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
|
||||
|
||||
# ---- 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
|
||||
description: >
|
||||
Полный pytest зелёный; невалидная модель/эффорт не роняет запуск агента
|
||||
(graceful-деградация). Регрессия resolve_agent_effort (VALID_EFFORTS) цела.
|
||||
module: tests/
|
||||
expected: PASS
|
||||
145
docs/work-items/ORCH-074/06-adr/ADR-001-model-name-validation.md
Normal file
145
docs/work-items/ORCH-074/06-adr/ADR-001-model-name-validation.md
Normal file
@@ -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>` → `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».
|
||||
23
docs/work-items/ORCH-074/10-tech-risks.md
Normal file
23
docs/work-items/ORCH-074/10-tech-risks.md
Normal file
@@ -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 выключен).
|
||||
69
docs/work-items/ORCH-074/12-review.md
Normal file
69
docs/work-items/ORCH-074/12-review.md
Normal file
@@ -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/ → обновлена документация» выполнено.
|
||||
82
docs/work-items/ORCH-074/13-test-report.md
Normal file
82
docs/work-items/ORCH-074/13-test-report.md
Normal file
@@ -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_<agent>` → откат на 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.
|
||||
12
docs/work-items/ORCH-074/14-deploy-log.md
Normal file
12
docs/work-items/ORCH-074/14-deploy-log.md
Normal file
@@ -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.
|
||||
@@ -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
|
||||
|
||||
68
tests/test_agent_frontmatter_no_model.py
Normal file
68
tests/test_agent_frontmatter_no_model.py
Normal file
@@ -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"
|
||||
@@ -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_<agent> -> 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
|
||||
|
||||
Reference in New Issue
Block a user