--- work_item: ORCH-099 stage: analysis author_agent: analyst status: ready-for-review created_at: 2026-06-10 model_used: claude-opus-4-8 --- # 01 — BRD (бизнес-требования): ORCH-099 — FND/F1a: лёгкий `/metrics` в орке (отдать сырьё) Work Item: **ORCH-099** · Repo: **orchestrator** · Стадия: analysis ## 1. Бизнес-контекст и проблема Задача — фундаментный кирпич **F1a** домена 0 «Фундамент» эпика автономного саморазвития (`docs/epics/self-evolution.md`). Архитектурная рамка наблюдаемости **зафиксирована заказчиком (Слава, 09.06)** и для аналитика — установленный факт, не предмет переизобретения: - **C-1/C-1б:** наблюдатель ОТДЕЛЁН от наблюдаемого. Мониторинг живёт в **отдельном sidecar-контейнере** (`watchdog/`, рантайм — свой Dockerfile + сервис в compose), а НЕ внутри орка. Если орк упал/завис/съел память — sidecar жив и репортит это. - **C-2/C-3:** без внешнего плеча, тонкий стек (хост впритык: RAM 171Mi free, диск 92% — НЕ Grafana/Prometheus). - **Разделение ответственности:** орк отдаёт **только сырьё** (лёгкий read-only `/metrics` — свои внутренние данные, которые знает только он сам), БЕЗ логики мониторинга/порогов/алертов/хранения. Мозг (пороги, алерты, свой Telegram-канал, история) — это **F1b (sidecar)**, отдельная задача. **Боль, которую закрывает задача.** Сегодня у орка нет машинного «сырья» о самом себе в одной точке. `/health` отдаёт лишь `{"status":"ok"}`, `/status` — список активных задач, `/queue` — богатый, но «человеческий» снимок очереди, перемешанный с конфигом демонов. Ни один из них не даёт sidecar'у структурированный, стабильный КОНТРАКТ для детекта: застрявшая стадия, зависший агент (liveness по pid/CPU), деградация очереди (breaker open, рост failed), всплеск стоимости токенов. Без этого источника весь домен наблюдаемости (F1b и далее) слеп и не может стартовать. **Self-hosting контекст.** Орк дорабатывает сам себя; прод-контейнер общий для всех проектов. `/metrics` обязан быть **строго read-only** и **never-raise** — он не должен ни при каких входных данных уронить или притормозить прод, обслуживающий enduro-trails. ## 2. Объём (scope) ### В объёме - Новый **read-only** HTTP-эндпоинт (`GET /metrics`), отдающий JSON-снимок сырья о самом орке. - Четыре раздела сырья: **активные стадии задач**, **очередь jobs**, **agent-liveness**, **стоимость/токены** (`agent_runs`). - Новый leaf-модуль `src/metrics.py` — сборка снимка из БД (чистый, never-raise, без побочных эффектов), по образцу `snapshot()`-функций (`serial_gate`/`task_deps`/`cancel`). - Документирование формата `/metrics` как **контракта для sidecar (F1b)** в `docs/architecture/README.md` + запись в `CHANGELOG.md`. - Pytest-покрытие: структура ответа, never-raise, read-only-инвариант. ### Вне объёма - ❌ Любая логика мониторинга: пороги, алерты, Telegram, оценка «застрял/завис», хранение истории — это **F1b (sidecar)**. - ❌ Сам sidecar-контейнер (`watchdog/`, Dockerfile, compose-сервис) — отдельная задача F1b. - ❌ Хостовые/контейнерные/внешние метрики (диск/RAM/CPU хоста, docker.sock, пинг Plane/Gitea/Anthropic) — их собирает sidecar, не орк. - ❌ Изменение `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / схемы БД / любых machine-verdict ключей. - ❌ Дашборд/UI (упомянут в F1 эпика как отдельный последующий шаг). - ❌ Прометей-совместимый text-формат — отдаём JSON (контракт под конкретный sidecar; OpenMetrics не требование заказчика). ## 3. Заинтересованные стороны - **Заказчик:** Слава (рамки наблюдаемости F1, эпик саморазвития). - **Прямой потребитель контракта:** будущий sidecar **F1b** (`watchdog/`) — читает `/metrics` по HTTP. Задача F1b **заблокирована** этой (ORCH-099 — источник контракта). - **Затрагивается:** прод-инстанс орка (общий с enduro-trails) — поэтому жёсткое требование read-only/never-raise. - **Принимает результат:** reviewer/tester конвейера + Слава как владелец рамок. ## 4. Бизнес-требования (BR) - **BR-1 — Эндпоинт сырья.** Орк предоставляет HTTP `GET /metrics`, отдающий JSON с четырьмя разделами: (a) активные стадии задач, (b) очередь jobs, (c) agent-liveness, (d) стоимость/токены. Состав полей каждого раздела — см. TRZ §3 (FR-1…FR-4). - **BR-2 — Стадии задач.** По каждой незавершённой задаче отдаётся `work_item`, текущая `stage` и «как давно в стадии» (секунды) — сырьё для детекта застреваний sidecar'ом. - **BR-3 — Очередь jobs.** Отдаются счётчики по статусам (`queued`/`running`/`failed`/…), глубина очереди, информация о ретраях и состояние circuit-breaker'а — сырьё для детекта деградации. - **BR-4 — Agent-liveness.** По каждому running-job отдаётся `agent`, `run_id`, `pid`, `runtime_s` и сырьё для alive-детекта (CPU-тики pid либо данные, по которым sidecar посчитает CPU-дельту). sidecar — арбитр «жив/завис»; орк лишь поставляет факты. - **BR-5 — Стоимость/токены.** Отдаётся текущая (по running-job) и агрегированная стоимость/токены из `agent_runs` (`cost_usd`, `input/output/cache_*` токены) — сырьё для cost-наблюдаемости (D3). - **BR-6 — Аддитивность.** Существующие `/health`, `/status`, `/queue` остаются байт-в-байт прежними по контракту; `/metrics` добавляется рядом, ничего не ломая. - **BR-7 — Документированный контракт.** Формат `/metrics` зафиксирован в `docs/architecture/README.md` как стабильный контракт для sidecar (F1b) + `CHANGELOG.md`. ## 5. Нефункциональные требования (NFR) - **NFR-1 — Read-only.** Эндпоинт НИЧЕГО не мутирует: не пишет в БД, не запускает/останавливает процессы, не рестартит, не дёргает внешние API. Только SELECT'ы + чтение in-memory-снимков демонов. - **NFR-2 — Never-raise (по полям).** Любая ошибка при сборе отдельного поля/раздела → это поле получает `null` (или раздел — безопасный дефолт), но эндпоинт **возвращает 200 и валидный JSON**, никогда не 500. Эталон — `serial_gate.snapshot()` с fallback в `except`. - **NFR-3 — Лёгкость.** Только быстрые запросы к локальной SQLite + чтение уже посчитанных in-memory снапшотов; без тяжёлых вычислений, без сетевых вызовов, без сканирования файлов/git. Цель — единичные мс на типовом объёме (десятки задач/jobs). - **NFR-4 — Self-hosting-безопасность.** Эндпоинт физически не способен повлиять на прод-конвейер (следствие NFR-1) — безопасен на общем инстансе с enduro-trails. - **NFR-5 — Совместимость БД/гейтов.** `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict ключи / схема БД — НЕ трогаются. Задача читает существующие таблицы (`tasks`/`jobs`/`agent_runs`) и существующие in-memory снапшоты. - **NFR-6 — Стабильность контракта.** Формат — аддитивный и версионируемый (поле `schema_version`), чтобы будущие расширения не ломали уже написанный sidecar. ## 6. Допущения и ограничения - **Данные уже есть в БД.** Все нужные поля присутствуют: `tasks(stage, work_item_id, updated_at, created_at)`, `jobs(status, attempts, max_attempts, transient_attempts, available_at, pid, run_id)`, `agent_runs(agent, started_at, finished_at, model, effort, cost_usd, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens)`. **Новые колонки/таблицы не нужны.** - **Breaker-состояние — in-memory** (`queue_worker.worker.status()` / `CircuitBreaker.snapshot()`); читается без БД. - **CPU-тики pid** читаются из `/proc//stat` (Linux прод-контейнер). Допущение: контейнер Linux; при отсутствии/гонке (процесс уже умер) — поле `null` (NFR-2), НЕ ошибка. Это согласуется с рамкой C-1: «орк лёг → endpoint недоступен = сам сигнал тревоги» — детект делает sidecar. - **Арбитраж liveness — на стороне sidecar.** Орк не решает «завис/жив»; он лишь отдаёт `pid`, `runtime_s` и (по возможности) CPU-тики; sidecar считает дельту между опросами. - **Формат — JSON**, не OpenMetrics/Prometheus (рамка C-3: тонкий кастомный sidecar, не Prometheus). ## 7. Критерии успеха `GET /metrics` отдаёт лёгкий, read-only, never-raise JSON с четырьмя разделами сырья; `/health`/`/status`/`/queue` не сломаны; формат задокументирован как контракт sidecar; pytest зелёный. Детальные PASS/FAIL — `03-acceptance-criteria.md`. ## 8. Риски - Гонка чтения `/proc//stat` (процесс умер между выборкой job и чтением proc) → закрывается NFR-2 (`null`, не ошибка). - Расхождение контракта `/metrics` и ожиданий sidecar (F1b) → закрывается BR-7 (контракт в одном репо, документирован) + `schema_version` (NFR-6). - Соблазн «протащить» в `/metrics` логику алертинга → закрывается scope-границей (вне объёма) и NFR-1. Детальная оценка технических рисков — `10-tech-risks.md` (заполняет архитектор).