Compare commits

..

1 Commits

Author SHA1 Message Date
post-deploy-monitor
e2c4881b21 docs(ORCH-021): post-deploy HEALTHY/NONE for ORCH-026
All checks were successful
CI / test (push) Successful in 25s
2026-06-08 20:03:03 +03:00
39 changed files with 54 additions and 1917 deletions

View File

@@ -12,47 +12,6 @@ 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 (ORCH-081/ORCH-52h): thinking agents (analyst/architect/reviewer)
# -> high; developer -> xhigh (coding/agentic role, Opus 4.8 canon); mechanical
# agents (tester/deployer) -> medium. NB: an empty ORCH_AGENT_EFFORT_*= no longer
# zeroes the effort — the launcher falls back to a per-role floor (= the config.py
# class-default) so each role still runs at its canonical level (ORCH-081).
ORCH_AGENT_EFFORT_DEFAULT=high
ORCH_AGENT_EFFORT_ANALYST=high
ORCH_AGENT_EFFORT_ARCHITECT=high
ORCH_AGENT_EFFORT_DEVELOPER=xhigh
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

View File

@@ -1,6 +1,7 @@
---
name: analyst
description: Бизнес-аналитик. Создаёт пакет документов анализа для work item.
model: claude-sonnet-4-6
tools:
- Filesystem (Read везде; Write только docs/work-items/<plane-id>/*)
- Bash (git log, grep — только для чтения контекста)

View File

@@ -1,6 +1,7 @@
---
name: architect
description: Архитектор системы. Принимает архитектурные решения по ТЗ, фиксирует как ADR.
model: claude-opus-4-7
tools:
- Filesystem (Read везде; Write только docs/)
- Bash (read-only: grep, git log)

View File

@@ -1,6 +1,7 @@
---
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)

View File

@@ -1,6 +1,7 @@
---
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 запрещён)

View File

@@ -1,6 +1,7 @@
---
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)

View File

@@ -1,6 +1,7 @@
---
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

View File

@@ -6,7 +6,7 @@
## Стек
- Backend: FastAPI + uvicorn (Python 3.12)
- БД: SQLite (`src/db.py`)
- Агенты: 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).
- Агенты: Claude CLI (`ORCH_CLAUDE_BIN`), по одному промпту на роль в `.openclaw/agents/`
- Очередь задач: собственная (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/`)

View File

@@ -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. Модель/эффорт каждого агента резолвятся из 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`.
- **Agent Launcher** (`src/agents/launcher.py`) — запуск Claude CLI агентов в изолированном git worktree, мониторинг, auto-advance.
- **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,20 +41,6 @@ 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-дефолт (без флага)**. **Эффорт (ORCH-081):** ниже `*_default` добавлен непустой **per-role floor** — class-default поля `agent_effort_<role>` из `config.py` (его пустой env перебить не может). Floor — строго последний уровень (ниже default) и срабатывает ТОЛЬКО когда все уровни пусты, поэтому пустые прод-`ORCH_AGENT_EFFORT_*=` (которые pydantic трактует как явное `''` и обнуляют дефолт) больше не приводят к запуску без `--effort`: каждая роль получает свой канонический пол (developer=`xhigh`, tester/deployer=`medium`, прочие=`high`). Непустой явный конфиг по-прежнему побеждает floor; опечатка вне `VALID_EFFORTS` дропается валидацией ДО floor (never-break, не маскируется). См. `docs/work-items/ORCH-081/06-adr/ADR-001-effort-resolution-floor.md`. frontmatter `model:` в `.openclaw/agents/*.md` **удалён** (ORCH-74 G1) — он был мёртвой/лживой декларацией (launcher его не читает); config — единственный источник правды о модели. Model-routing (G3) НЕ включён — все 6 агентов на `claude-opus-4-8`.
| Агент | Модель | Эффорт |
|-------|--------|--------|
| analyst | claude-opus-4-8 | high |
| architect | claude-opus-4-8 | high |
| developer | claude-opus-4-8 | xhigh |
| 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).

View File

@@ -0,0 +1,14 @@
---
post_deploy_status: HEALTHY
action_taken: NONE
work_item: ORCH-026
window_s: 900
checks_total: 30
checks_failed: 0
---
# Post-deploy log — ORCH-021 post-deploy monitor
Наблюдение прода завершено: `post_deploy_status: HEALTHY`, `action_taken: NONE`.
Окно наблюдения: 900s; опросов всего: 30, из них с провалом: 0.

View File

@@ -1,7 +0,0 @@
# Business Request: ORCH-52a: фикс модели/эффорта агентов (мёртвый frontmatter → routing+effort)
Work Item ID: ORCH-074
## Description
TBD

View File

@@ -1,89 +0,0 @@
# 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 повышает доступность при перегрузке основной модели.

View File

@@ -1,112 +0,0 @@
# ТЗ — 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».

View File

@@ -1,81 +0,0 @@
# Критерии приёмки — 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 отсутствует или не покрывает эти решения.

View File

@@ -1,103 +0,0 @@
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

View File

@@ -1,145 +0,0 @@
# 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».

View File

@@ -1,23 +0,0 @@
# Технические риски — 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 выключен).

View File

@@ -1,69 +0,0 @@
---
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/ → обновлена документация» выполнено.

View File

@@ -1,82 +0,0 @@
---
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.

View File

@@ -1,12 +0,0 @@
---
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.

View File

@@ -1,56 +0,0 @@
---
staging_status: SUCCESS
timestamp: 2026-06-08T18:57:59+00:00
base_url: http://localhost:8501
---
# Staging Gate Log
Staging test suite completed inside the `orchestrator-staging` container
(`docker exec` via Docker Engine API, ADR-001 / ORCH-048 canonical method —
preserves the running instance's process-env so the B6 registry-isolation check
reads `.env.staging` correctly).
- Command: `python3 /repos/orchestrator/scripts/staging_check.py --base-url http://localhost:8501 --mode stub`
- Exit code: **0**`staging_status: SUCCESS`
- Result: **8/10 checks PASS**, REAL failed: none.
## Infra waiver (ORCH-061)
```
INFRA-WAIVED: C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
VERDICT: SUCCESS (exit 0) — SUCCESS (infra-waived): ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue'] are known sandbox-infra checks; all real checks green
```
C9a/C9b are the two sandbox-infra-only checks (depend on SANDBOX bot accounts being
project members, not on the pipeline). Both were tolerated because every REAL check
is green; the script still exits 0 (fail-closed for any real failure). Trusting the
exit code per ORCH-061 — no re-judging of waived checks.
## Full output
```
[Block A] SMOKE
✓ PASS A1 GET /health → 200 status=ok [HTTP 200, body={'status': 'ok', 'service': 'orchestrator'}]
✓ PASS A2 GET /queue → 200 with counts/max_concurrency/resilience
✓ PASS A3 ORCH_STAGING=true (not prod) [ORCH_STAGING=true]
[Block B] ACCESS
✓ PASS B4 Plane: sandbox project accessible [HTTP 200, found 5 project(s), sandbox=YES]
✓ PASS B5 Gitea: orchestrator-sandbox accessible, push=true
✓ PASS B6 Registry: sandbox present, prod ET/ORCH absent [sandbox=YES, prod-ET=NO(good), prod-ORCH=NO(good)]
[Block C] E2E (mode=stub)
✓ PASS C7 Create issue in Plane SANDBOX [HTTP 201]
✓ PASS C8 Trigger pipeline via /webhook/plane [HTTP 200, resp={'status': 'accepted'}]
✗ FAIL C9a Branch appears in orchestrator-sandbox [branch=not found] (SANDBOX_INFRA, waived)
✗ FAIL C9b Analyst job enqueued in staging queue (SANDBOX_INFRA, waived)
[CLEANUP]
✓ PASS CLEANUP: deleted Plane issue (HTTP 204)
RESULT: 8/10 checks PASS
REAL failed : none
SANDBOX_INFRA failed: ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue']
tolerance: staging_infra_tolerance_enabled=True
```

View File

@@ -1,7 +0,0 @@
# Business Request: ORCH-52h: эффорт агентов резолвится в пустую строку в проде (env перебивает config)
Work Item ID: ORCH-081
## Description
TBD

View File

@@ -1,82 +0,0 @@
# 01 — BRD: ORCH-081 (ORCH-52h)
**Work Item:** ORCH-081
**Эпик:** ORCH-052 (продолжение ORCH-52a / ORCH-074)
**Тип:** Багфикс (конфигурация эффорта агентов)
**Приоритет:** HIGH
**Repo:** orchestrator (self-hosting)
## 1. Контекст и проблема
При проверке ORCH-074 (08.06) обнаружено: `resolve_agent_effort()` для **всех 6 агентов
в проде** возвращает пустую строку `''`, хотя в `src/config.py` заданы осмысленные
дефолты (`agent_effort_default="high"`, per-agent `high`/`medium`). Итог: флаг
`--effort` **не передаётся** в Claude CLI, и каждый агент бежит на встроенном
CLI-дефолте эффорта, а **не** на заявленном `high`/`medium`.
### Корень (диагностика)
В проде env-переменные `ORCH_AGENT_EFFORT_DEFAULT` и
`ORCH_AGENT_EFFORT_{ANALYST,ARCHITECT,DEVELOPER,REVIEWER,TESTER,DEPLOYER}` выставлены в
**пустую строку** (`VAR=` без значения). Pydantic Settings трактует присутствующую
env-переменную (даже пустую) как явное значение и **перебивает** дефолт класса:
`agent_effort_* = ''`. В цепочке резолва (`launcher._resolve_agent_attr`):
- per-agent `''` → falsy → пропуск (уровень 2);
- default `''` → falsy → пропуск (уровень 3);
- → возврат `''` (уровень 4, «без флага»).
Поскольку **и default тоже пуст**, привычный откат «per-agent пуст → взять default»
не спасает: откатываться не на что. Это ключевой нюанс — фикс обязан давать каждой
роли непустой «пол» (floor) даже когда И per-agent, И default env пусты.
## 2. Бизнес-ценность / зачем важно
Для Opus 4.8 (канон Anthropic) уровень reasoning-эффорта влияет на качество вывода
**сильнее**, чем у прежних моделей. Coding/agentic роли (особенно `developer`) должны
идти минимум на `high`, а `developer` — кандидат на `xhigh`. Сейчас фактически работает
неконтролируемый CLI-дефолт → прямой удар по стратегии надёжности и предсказуемости
качества всего конвейера (включая enduro-trails из общего инстанса).
## 3. Решение (бизнес-уровень)
Принят **вариант (c)** (решение Славы, 08.06): пустая строка эффорта трактуется как
«не задано» и откатывается на осмысленный per-role дефолт (а не на CLI-дефолт),
**устойчиво** к пустым env. Дополнительно — зафиксировать целевые дефолты в `config.py`
и `.env.example`.
### Целевые значения эффорта (единственный апгрейд — `developer`)
| Агент | Эффорт | Обоснование |
|-------|--------|-------------|
| analyst | high | intelligence-роль |
| architect | high | intelligence-роль |
| **developer** | **xhigh** | coding/agentic, канон Opus 4.8 → апгрейд с `high` |
| reviewer | high | intelligence-роль |
| tester | medium | механическая роль |
| deployer | medium | механическая роль |
`developer → xhigh` — единственное изменение относительно текущих config-дефолтов;
остальные значения подтверждают текущий замысел и фиксируются устойчиво.
## 4. Грабли / ограничения (из бизнес-запроса)
- **Хост-репо / env-правки НЕ переживают деплой**, если положены в git-managed файл
(урок 08.06 про docker-compose + TZ). Источник правды для реальных значений —
`.env` на хосте (gitignored), канон-шаблон — `.env.example`. Фикс обязан быть
**code-side robust**: даже если прод-`.env` снова окажется с пустыми
`ORCH_AGENT_EFFORT_*`, эффорт всё равно резолвится в целевые значения.
- **Self-hosting:** правка касается инструмента, который сейчас в проде обслуживает и
другие проекты. Прод-контейнер `orchestrator` не ронять в рамках задачи; деплой —
через штатный `deploy-staging``Confirm Deploy`.
## 5. Не-цели
- НЕ трогать model-резолв (`resolve_agent_model` — сделан в ORCH-074).
- НЕ включать G3 model-routing — все 6 агентов остаются на `claude-opus-4-8`.
- НЕ менять значения эффорта сверх согласованных (`high`/`medium`/`xhigh` для
developer). Иные значения — отдельное взвешенное решение.
## 6. Затронутые стороны
- Все агенты конвейера (analyst → deployer) во всех проектах общего инстанса.
- Операторы (правка прод-`.env`), документация (README таблица, `.env.example`).
</content>
</invoke>

View File

@@ -1,110 +0,0 @@
# 02 — ТЗ: ORCH-081 (ORCH-52h)
**Work Item:** ORCH-081 · **Тип:** багфикс конфигурации · **Repo:** orchestrator
Документ описывает ТРЕБУЕМОЕ ПОВЕДЕНИЕ и затронутые модули. Конкретный механизм
(field_validator vs изменение резолвера) — на усмотрение архитектора; ниже зафиксированы
инварианты, которым любая реализация обязана удовлетворять.
## 1. Задействованные модули
| Модуль | Роль в задаче |
|--------|----------------|
| `src/config.py` (`Settings`) | дефолты эффорта; устойчивость к пустому env (ядро фикса) |
| `src/agents/launcher.py` | `resolve_agent_effort` / `_resolve_agent_attr` (цепочка резолва), `VALID_EFFORTS`, сборка `--effort` в `_spawn` |
| `.env.example` | канон-шаблон значений эффорта по ролям |
| `docs/architecture/README.md` | таблица «Модель и эффорт по ролям» (строки ~4754) |
| `CHANGELOG.md` | запись о фиксе |
| `tests/test_resolve_agent_effort.py` | расширить кейсами пустого env |
## 2. Корень бага (точная механика)
`launcher._resolve_agent_attr` (строки ~104114):
```
per_agent = getattr(settings, f"agent_effort_{agent}", "") # '' в проде -> falsy -> skip
default = getattr(settings, "agent_effort_default", "") # '' в проде -> falsy -> skip
return "" # уровень 4: без флага
```
Pydantic: `ORCH_AGENT_EFFORT_*=` (пустая строка в env) перебивает дефолт класса →
поле `= ''`. Поскольку пустым оказывается **и** `agent_effort_default`, у резолва нет
непустого «пола» для отката → `''``--effort` не передаётся.
## 3. Требования к фиксу (вариант c)
### FR-1. Непустой floor на каждую роль при пустом env
При ЛЮБОЙ комбинации пустых `ORCH_AGENT_EFFORT_*` (включая `ORCH_AGENT_EFFORT_DEFAULT=`)
`resolve_agent_effort(agent)` обязан вернуть целевое непустое значение для каждой из 6
ролей:
| agent | результат |
|-------|-----------|
| analyst | `high` |
| architect | `high` |
| developer | `xhigh` |
| reviewer | `high` |
| tester | `medium` |
| deployer | `medium` |
Замечание для реализации: floor должен быть **per-role**, а не единым на default —
иначе пустой `ORCH_AGENT_EFFORT_TESTER=` снапнется на `high` вместо `medium`. Т.е.
«пустая строка трактуется как не-задано» применяется так, чтобы каждая роль получала
СВОЙ канонический дефолт, а не общий.
### FR-2. Приоритет резолва сохраняется
Порядок не меняется: project-override (`projects_json.agent_efforts`) > per-agent env >
default > floor. Непустой явный env/override по-прежнему ПОБЕЖДАЕТ floor (оператор может
осознанно задать, напр., `ORCH_AGENT_EFFORT_DEVELOPER=high`, и это применится).
### FR-3. Валидация невалидного значения не регрессирует
Значение вне `VALID_EFFORTS` (`low|medium|high|xhigh|max`) по-прежнему логируется
(`logger.warning`) и **дропается**`''` (без флага). Floor НЕ должен «спасать» явную
опечатку (`turbo`/`ultra`) — поведение ORCH-41 сохраняется (never-break, мусор не
уезжает в CLI).
### FR-4. `developer → xhigh` зафиксирован явно
`config.py`: `agent_effort_developer` со значением `xhigh` (сейчас `high`).
`.env.example`: `ORCH_AGENT_EFFORT_DEVELOPER=xhigh` (сейчас `high`) + правка комментария
про split (developer теперь xhigh, не в группе «thinking → high»).
### FR-5. `xhigh` принимается CLI-слоем
Подтвердить, что `xhigh` присутствует в `VALID_EFFORTS`
(`src/agents/launcher.py:22` — уже `frozenset({"low","medium","high","xhigh","max"})`,
**присутствует**; добавления не требуется, только верификация тестом). Эффорт реально
собирается в команду: `_spawn` строит `effort_flag = f"--effort {effort} "` при непустом
`effort` (строка ~434) — путь проброса не менять, только убедиться тестом сборки флага.
## 4. Изменения API / схемы БД
- **API endpoints:** нет.
- **Схема БД:** нет.
- **Конфиг (env-контракт):** значения `ORCH_AGENT_EFFORT_*` неизменны по ИМЕНАМ;
меняется лишь дефолт `developer` (high → xhigh) и устойчивость к пустым значениям.
Обратная совместимость: непустой явный env работает 1:1 как раньше.
## 5. Требования к QG checks
Новых QG checks не требуется. Гейты конвейера не затрагиваются.
## 6. Артефакты pipeline (обновить в ТОМ ЖЕ PR)
- `src/config.py` — дефолт developer + устойчивость к пустому env.
- `src/agents/launcher.py` — если фикс кладётся в резолвер (на усмотрение архитектора).
- `.env.example``ORCH_AGENT_EFFORT_DEVELOPER=xhigh` + правка комментария split.
- `docs/architecture/README.md` — таблица эффорта: developer `high``xhigh`; при
необходимости — ремарка про floor/устойчивость к пустому env.
- `CHANGELOG.md` — запись (`fix:`).
- `tests/test_resolve_agent_effort.py` — новые кейсы (см. 04-test-plan.yaml).
## 7. Операционная часть (вне PR-кода, для деплой-лога)
- Реальные значения — в прод-`.env` на хосте (gitignored). Рекомендуется привести
прод-`.env` к каноне `.env.example` (developer=xhigh, остальные непустые), НО фикс
обязан работать и без этого (FR-1). Не коммитить секреты/хост-env в git.
- Деплой — через `deploy-staging` (8501) → `Confirm Deploy`. Прод-контейнер не ронять
вне штатного хука.
## 8. Definition of Done
AC-1…AC-5 из `03-acceptance-criteria.md` выполнены; `pytest -q` зелёный; документация
(README + `.env.example` + CHANGELOG) синхронизирована в том же PR; never-break соблюдён.
</content>

View File

@@ -1,60 +0,0 @@
# 03 — Критерии приёмки: ORCH-081 (ORCH-52h)
Каждый критерий — чёткое условие PASS/FAIL. Пустой env моделируется в unit-тестах
(установка `agent_effort_* = ""`), проверка «в проде» — операционная (post-deploy).
## AC-1 — осмысленный непустой эффорт для всех 6 агентов
**PASS:** `resolve_agent_effort(agent)` возвращает целевое непустое значение для каждой
роли при канонической конфигурации:
| agent | ожидаемое |
|-------|-----------|
| analyst | `high` |
| architect | `high` |
| developer | `xhigh` |
| reviewer | `high` |
| tester | `medium` |
| deployer | `medium` |
**FAIL:** любой агент возвращает `''` или значение, отличное от таблицы.
## AC-2 — пустой env НЕ приводит к пустому эффорту (вариант c)
**PASS:** при `agent_effort_default = ""` И всех `agent_effort_<role> = ""`
(моделирование прод-env, где `ORCH_AGENT_EFFORT_*=` пусты) `resolve_agent_effort` для
каждой из 6 ролей возвращает значение по таблице AC-1 (floor per-role срабатывает:
developer=`xhigh`, tester/deployer=`medium`, остальные=`high`), а **не** `''`.
**FAIL:** хотя бы одна роль при полностью пустом env даёт `''`.
## AC-3 — эффорт реально пробрасывается в запуск агента
**PASS:** в `launcher._spawn` (или эквивалентной сборке) при непустом резолвнутом
эффорте формируется `--effort <value> ` во флагах команды; при пустом — флаг
отсутствует. Тест сборки флага подтверждает наличие `--effort xhigh ` для developer и
`--effort medium ` для tester.
**FAIL:** `--effort` отсутствует при непустом значении ИЛИ присутствует при пустом.
## AC-4 — документация синхронизирована
**PASS:** `.env.example` содержит `ORCH_AGENT_EFFORT_DEVELOPER=xhigh` и корректный
комментарий про split; таблица «Модель и эффорт по ролям» в
`docs/architecture/README.md` показывает developer = `xhigh` (остальные без изменений);
`CHANGELOG.md` содержит запись о фиксе.
**FAIL:** любой из трёх артефактов рассинхронизирован с фактическими дефолтами config.
## AC-5 — never-break, тесты зелёные
**PASS:**
- `pytest -q` целиком зелёный (включая существующие
`tests/test_resolve_agent_effort.py` и новые кейсы).
- Невалидное значение эффорта (`turbo`/`ultra`/`bogus`) по-прежнему логируется и
дропается в `''` (floor его НЕ маскирует) — регрессии валидации ORCH-41 нет.
- Непустой явный per-agent env / project-override по-прежнему побеждает floor
(приоритет резолва сохранён).
- `xhigh ∈ VALID_EFFORTS` (подтверждено тестом).
**FAIL:** падение любого теста, регрессия валидации/приоритета, либо `xhigh`
отвергается как невалидный.
## AC-6 (операционный, для деплой-стадии) — проверка в проде
**PASS:** после деплоя на проде `resolve_agent_effort` для 6 агентов даёт значения
AC-1 (проверяется в рантайме прод-инстанса / по логам запуска агента — наличие
`--effort` с верным уровнем). Фиксируется в `14-deploy-log.md`.
**FAIL:** в проде хотя бы один агент бежит без `--effort` или с неверным уровнем.
</content>

View File

@@ -1,86 +0,0 @@
work_item: ORCH-081
description: >
Тест-план фикса ORCH-52h — устойчивость резолва эффорта к пустому env (вариант c) +
фиксация целевых дефолтов (developer -> xhigh). Расширяет существующий
tests/test_resolve_agent_effort.py. Пустой прод-env моделируется установкой
agent_effort_* = "" на settings (через monkeypatch), как уже делают текущие тесты.
tests:
- id: TC-01
type: unit
description: >
Канонические дефолты: resolve_agent_effort для всех 6 ролей даёт
analyst/architect/reviewer=high, developer=xhigh, tester/deployer=medium.
module: tests/test_resolve_agent_effort.py
covers: [AC-1, FR-4]
expected: PASS
- id: TC-02
type: unit
description: >
Пустой env (вариант c): при agent_effort_default="" И всех
agent_effort_<role>="" каждая из 6 ролей возвращает целевое значение по AC-1
(НЕ ""). Ключевой кейс бага: developer -> xhigh, tester/deployer -> medium,
analyst/architect/reviewer -> high.
module: tests/test_resolve_agent_effort.py
covers: [AC-2]
expected: PASS
- id: TC-03
type: unit
description: >
Floor НЕ маскирует опечатку: невалидное значение (default/per-agent/override =
'turbo'/'ultra'/'bogus') по-прежнему логируется и дропается в "" (валидация
ORCH-41 не регрессирует). Проверить, что floor не подменяет невалидный явный ввод
на дефолт.
module: tests/test_resolve_agent_effort.py
covers: [AC-5, FR-3]
expected: PASS
- id: TC-04
type: unit
description: >
Приоритет сохранён: непустой per-agent env побеждает floor/ default
(ORCH_AGENT_EFFORT_DEVELOPER=high -> "high", не "xhigh"); project-override
побеждает per-agent (agent_efforts={"developer":"xhigh"}).
module: tests/test_resolve_agent_effort.py
covers: [AC-5, FR-2]
expected: PASS
- id: TC-05
type: unit
description: >
xhigh валиден: xhigh ∈ VALID_EFFORTS и resolve_agent_effort с developer-дефолтом
xhigh не дропается.
module: tests/test_resolve_agent_effort.py
covers: [AC-5, FR-5]
expected: PASS
- id: TC-06
type: unit
description: >
Сборка флага: при resolve developer=xhigh во флагах присутствует "--effort xhigh ",
при tester=medium — "--effort medium "; при пустом эффорте "--effort" отсутствует
(mirror логики _spawn, как существующие test_flags_* кейсы).
module: tests/test_resolve_agent_effort.py
covers: [AC-3]
expected: PASS
- id: TC-07
type: integration
description: >
Документация синхронизирована: .env.example содержит
ORCH_AGENT_EFFORT_DEVELOPER=xhigh; README таблица эффорта показывает developer
xhigh. (Проверяется ревьюером/тестером по diff; опционально — текстовая ассерта.)
module: tests/test_resolve_agent_effort.py
covers: [AC-4]
expected: PASS
- id: TC-08
type: unit
description: >
Регрессия существующего набора: весь tests/test_resolve_agent_effort.py +
tests/test_resolve_agent_model.py остаются зелёными (never-break ORCH-41/074).
module: tests/test_resolve_agent_effort.py
covers: [AC-5]
expected: PASS
</content>

View File

@@ -1,129 +0,0 @@
# ADR-001: Per-role floor для резолва `--effort`, устойчивый к пустому env
**Work Item:** ORCH-081 (ORCH-52h) · **Эпик:** ORCH-052 (после ORCH-074)
**Связанные:** ORCH-41 (резолв model/effort), ORCH-074 (валидация модели, `is_valid_model`)
## Статус
Accepted
## Контекст
В проде `resolve_agent_effort()` возвращает `''` для всех 6 агентов, хотя в
`src/config.py` заданы осмысленные дефолты (`high`/`medium`). Итог: флаг `--effort`
не передаётся в Claude CLI, каждый агент бежит на встроенном CLI-дефолте, а не на
заявленном уровне. Для Opus 4.8 reasoning-эффорт сильнее влияет на качество, чем у
прежних моделей, → прямой удар по предсказуемости качества всего конвейера (включая
enduro-trails из общего инстанса).
### Корень (точная механика)
Pydantic Settings трактует **присутствующую** env-переменную — даже пустую
(`ORCH_AGENT_EFFORT_DEVELOPER=` без значения) — как явное значение и **перебивает**
дефолт класса: поле `= ''`. В проде пусты И per-agent (`ORCH_AGENT_EFFORT_<ROLE>=`),
И default (`ORCH_AGENT_EFFORT_DEFAULT=`). Цепочка резолва (`_resolve_agent_attr`):
```
project-override (agent_efforts) → пусто
per-agent env ('') → falsy → skip
default ('') → falsy → skip
→ '' (уровень 4: без флага)
```
Привычный откат «per-agent пуст → взять default» не спасает: откатываться не на что —
default тоже пуст. Нужен непустой **per-role** «пол» (floor) ниже default.
### Дополнительное ограничение (урок 08.06)
Хост-правки env, положенные в git-managed файл, **не переживают деплой**. Источник
правды реальных значений — `.env` на хосте (gitignored). Значит, фикс обязан быть
**code-side robust**: даже если прод-`.env` снова окажется с пустыми
`ORCH_AGENT_EFFORT_*`, эффорт всё равно резолвится в целевые значения.
## Рассмотренные варианты
### Вариант A — `field_validator` в `config.py` (coerce пустой → дефолт на уровне поля)
Валидатор каждого `agent_effort_*` конвертирует пустую строку в канонический дефолт
поля.
**Отклонён:** ломает приоритет FR-2. Если per-agent поле всегда непустое, оно ВСЕГДА
бьёт `default` (уровень 3 становится мёртвым для роли с пустым env). Сценарий: оператор
ставит `ORCH_AGENT_EFFORT_DEFAULT=max`, per-agent оставляет пустыми — намерение «все
роли на max», но coercion на уровне поля даст каждой роли её per-role дефолт, а не
`max`. Floor обязан стоять **строго ниже** default, а это видно только в резолвере,
где доступна вся цепочка приоритетов.
### Вариант B — explicit hardcoded map `{analyst: high, …}` в `launcher.py`
Отдельная константа-карта per-role floor.
**Отклонён как первичный:** вводит **второй источник правды** рядом с дефолтами
`config.py`. Баг, который мы чиним, — это и есть дрейф/рассинхрон конфигурации;
заводить новую поверхность дрейфа концептуально неверно (карту и config надо вручную
держать в синхроне).
### Вариант C — floor в резолвере, значение = class-default поля (ПРИНЯТО)
Floor применяется как **последний** уровень в `resolve_agent_effort`, ниже `default`,
а его значение берётся из **декларированного class-default** соответствующего поля
`Settings` (через `model_fields`), который пустой env НЕ может перебить.
## Решение
Фикс кладётся в `resolve_agent_effort` (`src/agents/launcher.py`), `_resolve_agent_attr`
остаётся общим с model-резолвом и **не трогается** (floor — effort-специфичен).
### Цепочка резолва (новая, уровень 4 — floor)
```
1. project-override (projects_json.agent_efforts[agent]) — непустой побеждает
2. per-agent env (settings.agent_effort_<agent>) — непустой побеждает
3. global default (settings.agent_effort_default) — непустой побеждает
4. per-role FLOOR (class-default поля agent_effort_<agent>) — НОВОЕ, непустой пол
↓ (только если все 13 пусты)
5. валидация VALID_EFFORTS → невалидное дропается в '' (ORCH-41, never-break)
```
### Ключевые инварианты реализации
- **Floor = class-default поля, а не instance-значение.** `type(settings).model_fields[f"agent_effort_{agent}"].default` возвращает декларированный дефолт (`high`/`medium`/`xhigh`), который пустой env не клобберит. Это восстанавливает значение, которое pydantic дал бы, не будь спурьозного `VAR=`. **Единый источник правды — `config.py`**: developer-апгрейд на `xhigh` делается одной правкой поля, floor подтягивается автоматически.
- **Floor применяется ДО валидации и ТОЛЬКО при пустом резолве.** Порядок критичен для FR-3: явная опечатка (`turbo`) — непустая, поэтому floor НЕ применяется, и значение штатно дропается валидацией в `''`. Floor не маскирует мусор.
- **Floor — строго уровень 4 (ниже default).** Непустой явный env/override/`default` по-прежнему побеждает floor (FR-2). Floor срабатывает лишь когда сконфигурировать эффорт забыли/занулили на всех уровнях.
- **Unknown-agent fallback:** если поля `agent_effort_<agent>` нет (имя не из 6 ролей), floor деградирует на class-default `agent_effort_default` (`high`) — непустой безопасный пол, never-break.
### Сопутствующая правка config (FR-4)
`config.py`: `agent_effort_developer` `high → xhigh` (канон Opus 4.8: coding/agentic роль).
Это единственное изменение значений; остальные (`analyst/architect/reviewer=high`,
`tester/deployer=medium`) подтверждаются и фиксируются устойчиво. Поскольку floor =
class-default, апгрейд автоматически становится и новым floor для developer.
### Целевые значения (floor при полностью пустом env)
| agent | floor |
|-------|-------|
| analyst | high |
| architect | high |
| developer | **xhigh** |
| reviewer | high |
| tester | medium |
| deployer | medium |
## Последствия
**Плюсы**
- Code-side robust: пустой прод-`.env` больше не обнуляет эффорт; целевые уровни
гарантированы без зависимости от хост-правок, которые не переживают деплой.
- Единый источник правды (`config.py`); нулевой риск дрейфа floor-карты.
- Приоритет резолва и контракт ORCH-41 сохранены 1:1; непустой явный конфиг работает
как раньше (полная обратная совместимость).
- Валидация ORCH-41 не регрессирует — опечатки по-прежнему дропаются, never-break.
**Минусы / ограничения**
- Лёгкая зависимость от pydantic-v2 API (`model_fields[...].default`) — публичный
стабильный атрибут, но это связь с внутренним устройством Settings. Замокать в тестах
тривиально.
- «CLI-дефолт без флага» как исход для 6 штатных ролей становится недостижим — это
намеренно: для известных ролей всегда есть непустой пол. Unknown-agent сохраняет
безопасный непустой fallback.
**Не затрагивается**
- API endpoints — нет. Схема БД — нет. QG checks / гейты конвейера — нет.
Model-резолв (ORCH-074) — нет. Путь проброса `--effort` в `_spawn` (стр. ~434) — нет
(только верификация тестом, FR-3/FR-5).
## Деплой (self-hosting)
Правка касается инструмента, обслуживающего в проде и другие проекты. Прод-контейнер
`orchestrator` не ронять в рамках задачи; деплой — штатно `deploy-staging` (8501) →
`Confirm Deploy`. Рекомендуется привести прод-`.env` к каноне `.env.example`
(developer=xhigh, остальные непустые), НО фикс обязан работать и без этого (FR-1).
Проверка в проде (AC-6) фиксируется в `14-deploy-log.md`.

View File

@@ -1,17 +0,0 @@
# 10 — Технические риски: ORCH-081 (ORCH-52h)
| ID | Риск | Вероятн. | Влияние | Митигация |
|----|------|----------|---------|-----------|
| R-1 | **Floor маскирует опечатку.** Если floor применить ПОСЛЕ/ВМЕСТО валидации, мусорное `turbo` подменится на floor вместо дропа → регрессия never-break ORCH-41. | низк. | средн. | Floor строго ДО валидации и ТОЛЬКО при пустом резолве (значение `turbo` непустое → floor не трогается → дроп). Покрыть тестом FR-3 (опечатка → `''`). |
| R-2 | **Floor перебивает явный конфиг.** Ошибка порядка → floor встанет выше default/per-agent и `ORCH_AGENT_EFFORT_DEFAULT=max` перестанет применяться. | низк. | средн. | Floor — строго уровень 4 (ниже default). Тест FR-2: непустой default/per-agent/override побеждает floor. |
| R-3 | **Зависимость от pydantic-internal** `model_fields[...].default`. Будущий мажор pydantic может сменить API → floor отвалится. | низк. | низк. | Публичный стабильный атрибут pydantic v2. Тест AC-1/AC-2 поймает регрессию сразу (floor вернёт не то/пусто). Фиксируется версией pydantic в зависимостях. |
| R-4 | **Дрейф floor vs config** при выборе hardcoded-карты. | — | — | Снят архитектурно: floor = class-default поля, единый источник правды (см. ADR-001, вариант B отклонён). |
| R-5 | **Self-hosting:** правка резолва эффорта затрагивает запуск ВСЕХ агентов всех проектов общего инстанса; ошибка ломает конвейер enduro-trails тоже. | низк. | высок. | Обязательный `deploy-staging` (8501) перед прод-деплоем; прод-контейнер не ронять вне штатного хука; `Confirm Deploy`-гейт. Post-deploy проверка AC-6 по логам запуска агента. |
| R-6 | **Прод-`.env` снова с пустыми `ORCH_AGENT_EFFORT_*`** после деплоя (урок 08.06: git-managed env не переживает). | средн. | низк. | Именно это и закрывает фикс (FR-1, code-side robust): эффорт резолвится в floor независимо от состояния `.env`. Приведение `.env` к каноне — рекомендация, не зависимость. |
| R-7 | **`xhigh` не принимается CLI-слоем.** developer-апгрейд бессмыслен, если `xhigh ∉ VALID_EFFORTS`. | очень низк. | средн. | `xhigh` уже в `VALID_EFFORTS` (`launcher.py:22`); добавления не требуется — только верификация тестом (FR-5). |
## Сводный вывод
Изменение локализовано в `resolve_agent_effort` + один дефолт `config.py`; не трогает
API, схему БД, QG-гейты, model-резолв и путь проброса `--effort`. Главный остаточный
риск — операционный (R-5, self-hosting), снимается штатным staging-гейтом. Контракт
ORCH-41/ORCH-074 сохранён, обратная совместимость полная.

View File

@@ -1,57 +0,0 @@
---
type: review
work_item_id: ORCH-081
verdict: APPROVED
version: 1
---
# Review ORCH-081 (ORCH-52h) — устойчивость резолва `--effort` к пустому env + developer→xhigh
## Summary
Фикс конфигурационного бага: в проде `resolve_agent_effort()` возвращал `''` для всех 6 агентов (пустые `ORCH_AGENT_EFFORT_*=` перебивают class-default pydantic), `--effort` не доходил до Claude CLI. Решение — вариант C по ADR-001: непустой **per-role floor** уровня 4 в `resolve_agent_effort`, значение = декларированный class-default поля `agent_effort_<agent>` через `model_fields[...].default`. `developer` поднят `high→xhigh` в `config.py` (единый источник правды, floor подтягивается автоматически).
Реализация полностью соответствует ТЗ и ADR; вся документация синхронизирована в том же бранче; `pytest -q`**1031 passed**.
## Соответствие ТЗ (FR-1…FR-5)
- **FR-1** per-role floor при пустом env → каждая роль получает свой канон (`_agent_effort_floor`, TC-02). ✓
- **FR-2** приоритет резолва сохранён: явный env/override/default побеждают floor (TC-04: `test_explicit_env_beats_floor`, `test_default_beats_floor`, `test_project_override_beats_floor`). ✓
- **FR-3** валидация не регрессирует: непустая опечатка (`turbo`) не доходит до floor → дропается в `''` (TC-03 `test_floor_does_not_mask_typo`). ✓
- **FR-4** `agent_effort_developer = "xhigh"` в `config.py`; `ORCH_AGENT_EFFORT_DEVELOPER=xhigh` + правка комментария split в `.env.example`. ✓
- **FR-5** `xhigh ∈ VALID_EFFORTS`; сборка флага `--effort xhigh `/`--effort medium ` подтверждена (TC-05/TC-06). ✓
## Соответствие ADR-001
- Floor как **строго уровень 4** ниже default, в резолвере — ✓ (вариант C, не field_validator/не hardcoded map).
- Floor = **class-default поля** (`type(settings).model_fields[...].default`), который пустой env перебить не может — ✓.
- `_resolve_agent_attr` (общий с model-резолвом) **не тронут** — ✓.
- Floor применяется **ДО валидации и только при пустом резолве** — ✓.
- Unknown-agent деградирует на class-default `agent_effort_default` (`high`) — ✓ (`test_empty_env_unknown_agent_floor_is_default`).
- Никаких изменений API / схемы БД / QG / model-резолва / пути проброса в `_spawn` — ✓.
## Качество кода и тестов
- Чистый leaf-helper, подробные docstrings, контракт never-raise соблюдён.
- Тесты содержательные, покрывают все AC/FR (канон-дефолты, floor per-role, не-маскирование опечатки, приоритет на 3 уровнях, `xhigh`-валидность, сборка флага + негативные кейсы).
## Findings
### P0 — Blocker
- (нет)
### P1 — Must fix
- (нет)
### P2 — Should fix
- (нет)
### P3 — Nice-to-have
- `tests/test_resolve_agent_effort.py:218-219` — продублирована строка `assert "--fallback-model" not in flags` в `test_flags_absent_when_model_empty`. Безвредно, можно убрать при случае.
## Документация
Изменён `src/` → документация обновлена в том же бранче (доку-гейт пройден):
- `docs/architecture/README.md` — таблица «Модель и эффорт по ролям»: developer = `xhigh`; добавлена ремарка про per-role floor / устойчивость к пустому env (AC-4). ✓
- `.env.example``ORCH_AGENT_EFFORT_DEVELOPER=xhigh` + комментарий split/floor (AC-4). ✓
- `CHANGELOG.md` — запись `fix:` с разбором корня/фикса. ✓
- `docs/work-items/ORCH-081/06-adr/ADR-001-effort-resolution-floor.md` — присутствует (Accepted). ✓
## Примечание (вне scope ревью)
- AC-6 — операционная проверка в проде после деплоя, фиксируется в `14-deploy-log.md` на стадии deploy. К коду PR не относится.
- `git diff main...HEAD` показывает также код ORCH-074 (`is_valid_model`/`resolve_agent_model`) из-за устаревшего локального `main`; собственно изменения ORCH-081 — коммит `56bf303` (+ README обновлён в линии бранча). На ревью это не влияет: HEAD-состояние корректно по всем осям.

View File

@@ -1,61 +0,0 @@
---
type: test-report
work_item_id: ORCH-081
result: PASS
---
# Test Report — ORCH-081 (ORCH-52h)
Устойчивость резолва `--effort` к пустому env (вариант c) + фиксация целевых
дефолтов (developer → xhigh).
## Окружение
- Python: 3.12.13
- pytest: 8.3.3
- Repo/branch: orchestrator @ `feature/ORCH-081-orch-52h-env-config` (worktree)
- prod `/health`: ok (8500) · staging `/health`: ok (8501) — не трогались
- Дата: 2026-06-08
## Результаты по тест-плану (04-test-plan.yaml)
| TC ID | Описание | Покрытие | Результат |
|-------|----------|----------|-----------|
| TC-01 | Канонические дефолты: 6 ролей дают high/high/xhigh/high/medium/medium | AC-1, FR-4 | PASS |
| TC-02 | Пустой env (вариант c): per-role floor, developer→xhigh, tester/deployer→medium, остальные→high (НЕ "") | AC-2 | PASS |
| TC-03 | Floor НЕ маскирует опечатку: `turbo`/`ultra`/`bogus` логируется и дропается в "" | AC-5, FR-3 | PASS |
| TC-04 | Приоритет сохранён: непустой per-agent env / project-override побеждают floor/default | AC-5, FR-2 | PASS |
| TC-05 | `xhigh ∈ VALID_EFFORTS` и не дропается | AC-5, FR-5 | PASS |
| TC-06 | Сборка флага: `--effort xhigh ` (developer), `--effort medium ` (tester); пустой → флаг отсутствует | AC-3 | PASS |
| TC-07 | Документация синхронизирована: `.env.example` DEVELOPER=xhigh, README таблица developer=xhigh | AC-4 | PASS |
| TC-08 | Регрессия: весь набор test_resolve_agent_effort.py + полный регресс зелёные | AC-5 | PASS |
### Сопоставление с критериями приёмки
- **AC-1** — `test_canonical_effort_all_roles[*]` (6 параметров) → PASS.
- **AC-2** — `test_empty_env_falls_back_to_per_role_floor[*]` (6 параметров) + `test_empty_env_unknown_agent_floor_is_default` → PASS.
- **AC-3** — `test_flags_present_when_configured`, `test_flags_effort_per_role`, `test_flags_absent_when_effort_empty` → PASS.
- **AC-4** — verified по diff: `src/config.py:108` `agent_effort_developer = "xhigh"`; `.env.example:48` `ORCH_AGENT_EFFORT_DEVELOPER=xhigh`; `docs/architecture/README.md` таблица developer=`xhigh`; `CHANGELOG.md` содержит запись `fix:` → PASS.
- **AC-5** — `test_floor_does_not_mask_typo`, `test_*_beats_floor`, `test_xhigh_is_valid`, `test_invalid_*_dropped` + полный регресс зелёный → PASS.
- **AC-6** — операционный, вне scope стадии testing: проверяется в рантайме прода на стадии `deploy`, фиксируется в `14-deploy-log.md`.
## Smoke test API (prod 8500)
- `GET /health``{"status":"ok","service":"orchestrator"}`
- `GET /status` → HTTP 200
- `GET /queue` → HTTP 200
## Вывод pytest
Целевой файл задачи:
```
tests/test_resolve_agent_effort.py ... 29 passed, 1 warning in 0.36s
```
Полный регресс:
```
........................................................................ [ 97%]
....................... [100%]
1031 passed, 1 warning in 27.02s
```
(единственный warning — PydanticDeprecatedSince20 в `src/config.py:5`, не относится к задаче, предсуществующий.)
## Итог
**PASS** — все 8 TC пройдены, критерии AC-1…AC-5 выполнены (AC-6 операционный, для стадии deploy), полный регресс `1031 passed`, smoke API зелёный. Прод/staging-контейнеры не затрагивались.

View File

@@ -1,12 +0,0 @@
---
deploy_status: SUCCESS
work_item: ORCH-081
hook_exit_code: 0
deployed_by: deploy-finalizer
---
# Deploy log — ORCH-036 executable self-deploy
Прод-деплой завершён хост-хуком с exit-code `0` -> `deploy_status: SUCCESS`.
Вердикт зафиксирован детерминированным finalizer'ом (Фаза C), не LLM.

View File

@@ -1,27 +0,0 @@
---
staging_status: SUCCESS
timestamp: 2026-06-08T19:47:45+00:00
base_url: http://localhost:8501
---
# Staging Gate Log
Staging test suite completed. Exit code 0 → advance.
Canonical run inside the `orchestrator-staging` container (ORCH-048, ADR-001) via the
Docker Engine API over the unix socket (docker CLI unavailable in the agent container):
```
python3 /repos/orchestrator/scripts/staging_check.py --base-url http://localhost:8501 --mode stub
```
Result: **8/10 checks PASS**, all REAL checks green.
```
INFRA-WAIVED: C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
VERDICT: SUCCESS (exit 0) — SUCCESS (infra-waived): ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue'] are known sandbox-infra checks; all real checks green
```
The two waived checks (C9a/C9b) are the known sandbox-infra-only checks tolerated under
ORCH-061 (SANDBOX bot accounts are not members of the sandbox Plane project — not a pipeline
regression). All pipeline (REAL) checks A1A3, B4B6, C7C8 passed.

View File

@@ -1,49 +0,0 @@
---
staging_status: SUCCESS
timestamp: 2026-06-08T21:55:49Z
base_url: http://localhost:8501
---
# Staging Gate Log
Staging test suite completed against the live staging environment (`orchestrator-staging`, port 8501),
run inside the container per the canonical method (ORCH-048, ADR-001):
```
docker exec orchestrator-staging \
python3 /repos/orchestrator/scripts/staging_check.py \
--base-url http://localhost:8501 --mode stub
```
## Result: 8/10 checks PASS — exit code 0 (SUCCESS)
- REAL failed: none
- SANDBOX_INFRA failed (waived per ORCH-061): C9a, C9b
All REAL pipeline checks (Block A SMOKE, Block B ACCESS, C7/C8) passed. The only failures are the
two infra-only sandbox checks (C9a branch-in-sandbox / C9b analyst-job-enqueued), which depend on
SANDBOX bot accounts being members of the sandbox project — not on the pipeline. Tolerance is enabled
(`staging_infra_tolerance_enabled=True`), so these are waived and the script exits 0 (fail-closed for
any REAL failure remains intact).
```
INFRA-WAIVED: C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
VERDICT: SUCCESS (exit 0) — SUCCESS (infra-waived): ['C9a …', 'C9b …'] are known sandbox-infra checks; all real checks green
```
### Block-by-block summary
| Block | Check | Result |
|-------|-------|--------|
| A | A1 GET /health → 200 status=ok | ✓ PASS |
| A | A2 GET /queue → 200 with counts/max_concurrency/resilience | ✓ PASS |
| A | A3 ORCH_STAGING=true (not prod) | ✓ PASS |
| B | B4 Plane: sandbox project accessible | ✓ PASS |
| B | B5 Gitea: orchestrator-sandbox accessible, push=true | ✓ PASS |
| B | B6 Registry: sandbox present, prod ET/ORCH absent | ✓ PASS |
| C | C7 Create issue in Plane SANDBOX | ✓ PASS |
| C | C8 Trigger pipeline via /webhook/plane | ✓ PASS |
| C | C9a Branch appears in orchestrator-sandbox | ✗ FAIL (sandbox-infra, waived) |
| C | C9b Analyst job enqueued in staging queue | ✗ FAIL (sandbox-infra, waived) |
Cleanup completed: test Plane issue deleted (HTTP 204); no branch created to delete.

View File

@@ -2,7 +2,6 @@ import subprocess
import os
import json
import logging
import re
import threading
import signal
import time
@@ -21,36 +20,6 @@ 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"})
@@ -114,94 +83,26 @@ 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).
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.
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.
"""
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 _agent_effort_floor(agent: str) -> str:
"""ORCH-081 (ORCH-52h): per-role non-empty floor for --effort resolution.
Returns the DECLARED class-default of the ``agent_effort_<agent>`` field on
Settings (e.g. developer -> ``xhigh``, tester/deployer -> ``medium``, the rest
-> ``high``). This is the value pydantic WOULD have used were it not clobbered
by a spurious empty env var (``ORCH_AGENT_EFFORT_<ROLE>=``): the class-default
is fixed in the class body and a present-but-empty env value cannot override it,
so it is a robust floor even when the host ``.env`` zeroes every effort var.
config.py is the single source of truth: upgrading developer to ``xhigh`` there
automatically raises the floor here — no second map to keep in sync (ADR-001).
Unknown agent (a name outside the 6 roles) has no ``agent_effort_<agent>``
field; we degrade to the class-default of ``agent_effort_default`` (``high``),
a safe non-empty floor. Never raises.
"""
fields = type(settings).model_fields
for key in (f"agent_effort_{agent}", "agent_effort_default"):
field = fields.get(key)
if field is not None and field.default:
return field.default
return ""
return _resolve_agent_attr(
agent, project_id,
project_map_attr="agent_models",
env_attr_prefix="agent_model_",
default_attr="agent_model_default",
)
def resolve_agent_effort(agent: str, project_id: str = None) -> str:
"""ORCH-41: resolve the --effort level for an agent (optionally per-project).
Same priority as resolve_agent_model, with one extra level below the global
default (ORCH-081 / ADR-001):
1. project-override (projects_json.agent_efforts[agent])
2. per-agent env (settings.agent_effort_<agent>)
3. global default (settings.agent_effort_default)
4. per-role FLOOR (class-default of agent_effort_<agent>) — NEW
The floor only kicks in when levels 1-3 are all empty (the prod bug: a present
but empty ``ORCH_AGENT_EFFORT_*=`` clobbers every default to ''), guaranteeing
a non-empty target effort for the 6 known roles regardless of host .env state.
The floor is applied BEFORE validation and ONLY to an empty resolve, so it
never masks a typo: an explicit invalid value (e.g. ``turbo``) is non-empty,
skips the floor, and is logged + dropped to "" exactly as in ORCH-41 (the
resolved value is validated against VALID_EFFORTS; an invalid value can never
pass a bad flag to the CLI). Never raises.
Same priority as resolve_agent_model. The resolved value is validated against
VALID_EFFORTS; an invalid value is logged and dropped (returns "") so a typo
in env/projects_json can never pass a bad flag to the CLI.
"""
value = _resolve_agent_attr(
agent, project_id,
@@ -209,11 +110,6 @@ def resolve_agent_effort(agent: str, project_id: str = None) -> str:
env_attr_prefix="agent_effort_",
default_attr="agent_effort_default",
)
if not value:
# Levels 1-3 all empty (typically a prod .env with empty ORCH_AGENT_EFFORT_*):
# fall through to the per-role floor (class-default). Applied before
# validation but only here, so a typo (non-empty) never reaches this branch.
value = _agent_effort_floor(agent)
if value and value not in VALID_EFFORTS:
logger.warning(
f"Invalid effort '{value}' for agent '{agent}' "
@@ -475,17 +371,7 @@ 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

View File

@@ -97,15 +97,13 @@ class Settings(BaseSettings):
agent_model_deployer: str = ""
# ORCH-41: per-agent effort / reasoning level: low|medium|high|xhigh|max.
# Empty -> agent_effort_default. Same resolution order as model. Default split
# (ORCH-081/ORCH-52h): thinking agents (analyst/architect/reviewer) -> high;
# developer -> xhigh (coding/agentic role, Opus 4.8 canon); mechanical agents
# (tester/deployer) -> medium. These class-defaults are ALSO the per-role floor
# used by resolve_agent_effort when the env is empty (single source of truth).
# Empty -> agent_effort_default. Same resolution order as model. Default split:
# thinking agents (analyst/architect/developer/reviewer) -> high; mechanical
# agents (tester/deployer) -> medium.
agent_effort_default: str = "high"
agent_effort_analyst: str = "high"
agent_effort_architect: str = "high"
agent_effort_developer: str = "xhigh"
agent_effort_developer: str = "high"
agent_effort_reviewer: str = "high"
agent_effort_tester: str = "medium"
agent_effort_deployer: str = "medium"

View File

@@ -1,68 +0,0 @@
"""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"

View File

@@ -26,22 +26,13 @@ from src.projects import ProjectConfig, reload_projects
ORCH_PLANE_ID = "8da6aa25-a60e-44d6-a1e2-d8ae59aa7d6a"
# ORCH-081/ORCH-52h: canonical effort per role (developer upgraded high -> xhigh).
CANON_EFFORT = {
"analyst": "high",
"architect": "high",
"developer": "xhigh",
"reviewer": "high",
"tester": "medium",
"deployer": "medium",
}
@pytest.fixture(autouse=True)
def _clean_settings(monkeypatch):
monkeypatch.setattr(settings, "agent_effort_default", "high")
for a, e in CANON_EFFORT.items():
monkeypatch.setattr(settings, f"agent_effort_{a}", e)
for a in ("analyst", "architect", "developer", "reviewer"):
monkeypatch.setattr(settings, f"agent_effort_{a}", "high")
for a in ("tester", "deployer"):
monkeypatch.setattr(settings, f"agent_effort_{a}", "medium")
monkeypatch.setattr(P.settings, "projects_json", "")
reload_projects()
yield
@@ -59,40 +50,19 @@ def _install_registry(monkeypatch, agent_efforts):
monkeypatch.setattr(P, "_BY_REPO", {p.repo: p for p in reg})
# ---- TC-01: canonical defaults (AC-1 / FR-4) --------------------------------
# ---- default split ----------------------------------------------------------
def test_default_split():
assert resolve_agent_effort("developer") == "xhigh"
assert resolve_agent_effort("developer") == "high"
assert resolve_agent_effort("architect") == "high"
assert resolve_agent_effort("tester") == "medium"
assert resolve_agent_effort("deployer") == "medium"
@pytest.mark.parametrize("agent,expected", list(CANON_EFFORT.items()))
def test_canonical_effort_all_roles(agent, expected):
assert resolve_agent_effort(agent) == expected
# ---- TC-02: empty env -> per-role floor (variant c, AC-2) -------------------
@pytest.mark.parametrize("agent,expected", list(CANON_EFFORT.items()))
def test_empty_env_falls_back_to_per_role_floor(monkeypatch, agent, expected):
"""Models the prod bug: ORCH_AGENT_EFFORT_*= present-but-empty -> every level
resolves to '' on the instance; the per-role floor (config class-default) must
still yield the canonical level (NOT '')."""
monkeypatch.setattr(settings, "agent_effort_default", "")
for a in CANON_EFFORT:
monkeypatch.setattr(settings, f"agent_effort_{a}", "")
result = resolve_agent_effort(agent)
assert result == expected
assert result != ""
# ---- unknown agent floor degrades to default (high), never '' ---------------
def test_empty_env_unknown_agent_floor_is_default(monkeypatch):
# ---- level 4: nothing -> "" -------------------------------------------------
def test_no_config_returns_empty(monkeypatch):
monkeypatch.setattr(settings, "agent_effort_default", "")
monkeypatch.setattr(settings, "agent_effort_tester", "")
# An agent with no agent_effort_<name> field falls back to the
# agent_effort_default class-default (high), a safe non-empty floor.
assert resolve_agent_effort("nonexistent_role") == "high"
assert resolve_agent_effort("tester") == ""
# ---- level 2: per-agent env beats default -----------------------------------
@@ -133,45 +103,6 @@ def test_all_valid_efforts_pass(monkeypatch):
assert resolve_agent_effort("developer") == e
# ---- TC-03: floor does NOT mask a typo (FR-3 / AC-5) ------------------------
def test_floor_does_not_mask_typo(monkeypatch):
"""An explicit invalid value is non-empty, so the floor is NOT applied: the
value is validated and dropped to '' (never-break ORCH-41), even though the
developer floor (xhigh) exists."""
monkeypatch.setattr(settings, "agent_effort_default", "")
monkeypatch.setattr(settings, "agent_effort_developer", "turbo")
assert resolve_agent_effort("developer") == ""
# ---- TC-04: priority preserved — explicit config beats floor (FR-2) ---------
def test_explicit_env_beats_floor(monkeypatch):
"""Operator may deliberately downgrade developer to high; the explicit
non-empty env wins over the xhigh floor."""
monkeypatch.setattr(settings, "agent_effort_developer", "high")
assert resolve_agent_effort("developer") == "high"
def test_default_beats_floor(monkeypatch):
"""A non-empty global default wins over the per-role floor (floor is strictly
below default): default=max with empty per-agent -> max, not the xhigh floor."""
monkeypatch.setattr(settings, "agent_effort_developer", "")
monkeypatch.setattr(settings, "agent_effort_default", "max")
assert resolve_agent_effort("developer") == "max"
def test_project_override_beats_floor(monkeypatch):
monkeypatch.setattr(settings, "agent_effort_developer", "")
_install_registry(monkeypatch, {"developer": "high"})
assert resolve_agent_effort("developer", ORCH_PLANE_ID) == "high"
# ---- TC-05: xhigh is a valid effort (FR-5) ----------------------------------
def test_xhigh_is_valid():
assert "xhigh" in VALID_EFFORTS
# developer canonical xhigh resolves (is not dropped by validation)
assert resolve_agent_effort("developer") == "xhigh"
# ---- flag assembly (mirror of launcher cmd construction) --------------------
def _build_flags(model, effort, fb):
model_flag = f"--model {model} " if model else ""
@@ -180,7 +111,6 @@ def _build_flags(model, effort, fb):
return f"{model_flag}{effort_flag}{fb_flag}"
# ---- TC-06: flag assembly (AC-3) --------------------------------------------
def test_flags_present_when_configured(monkeypatch):
monkeypatch.setattr(settings, "agent_fallback_model", "claude-sonnet-4-6")
model = resolve_agent_model("developer")
@@ -188,32 +118,21 @@ def test_flags_present_when_configured(monkeypatch):
fb = settings.agent_fallback_model
flags = _build_flags(model, effort, fb)
assert "--model claude-opus-4-8 " in flags
assert "--effort xhigh " in flags
assert "--effort high " in flags
assert "--fallback-model claude-sonnet-4-6 " in flags
def test_flags_effort_per_role(monkeypatch):
"""developer -> --effort xhigh; tester -> --effort medium (mirrors _spawn)."""
assert "--effort xhigh " in _build_flags("", resolve_agent_effort("developer"), "")
assert "--effort medium " in _build_flags("", resolve_agent_effort("tester"), "")
def test_flags_absent_when_effort_empty():
"""When the resolved effort is empty, --effort is omitted entirely. Mirrors the
`f"--effort {effort} " if effort else ""` branch in _spawn (AC-3 negative case)."""
flags = _build_flags("", "", "")
assert flags == ""
assert "--effort" not in flags
def test_flags_absent_when_model_empty(monkeypatch):
def test_flags_absent_when_empty(monkeypatch):
monkeypatch.setattr(settings, "agent_model_default", "")
monkeypatch.setattr(settings, "agent_model_developer", "")
monkeypatch.setattr(settings, "agent_effort_default", "")
monkeypatch.setattr(settings, "agent_effort_developer", "")
monkeypatch.setattr(settings, "agent_fallback_model", "")
model = resolve_agent_model("developer")
effort = resolve_agent_effort("developer")
fb = settings.agent_fallback_model
flags = _build_flags(model, "", fb)
flags = _build_flags(model, effort, fb)
assert flags == ""
assert "--model" not in flags
assert "--fallback-model" not in flags
assert "--effort" not in flags
assert "--fallback-model" not in flags

View File

@@ -23,9 +23,7 @@ os.environ.setdefault("ORCH_DB_PATH",
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")
import logging
from src.agents.launcher import resolve_agent_model, is_valid_model
from src.agents.launcher import resolve_agent_model
from src.config import settings
from src import projects as P
from src.projects import ProjectConfig, reload_projects, _parse_projects_json
@@ -156,86 +154,3 @@ 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