---
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/<pid>/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/<pid>/stat` (процесс умер между выборкой job и чтением proc) → закрывается
  NFR-2 (`null`, не ошибка).
- Расхождение контракта `/metrics` и ожиданий sidecar (F1b) → закрывается BR-7 (контракт в одном
  репо, документирован) + `schema_version` (NFR-6).
- Соблазн «протащить» в `/metrics` логику алертинга → закрывается scope-границей (вне объёма) и
  NFR-1.

Детальная оценка технических рисков — `10-tech-risks.md` (заполняет архитектор).
