---
work_item: ORCH-100
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-10
model_used: claude-opus-4-8
---

# 02 — ТЗ (TRZ): ORCH-100 — FND/F1b: sidecar-watchdog (мозг мониторинга, отдельный контейнер)

Work Item: **ORCH-100** · Repo: **orchestrator** · Стадия: analysis

> ТЗ описывает **конкретные изменения к реализации**, выведенные из BRD (`01-brd.md`) и фактического
> кода. Архитектурное обоснование/решения (выбор стека Python/Go, формат хранения порогов, владелец
> диск-алерта, точная топология сети sidecar, бюджет памяти/`mem_limit`) — **зона архитектора**
> (`06-adr/`). ТЗ фиксирует ТРЕБОВАНИЯ и ограничения, не способ реализации.

## 1. Сводка изменения

Добавить **отдельный sidecar-контейнер** `orchestrator-watchdog`, код которого лежит в новой папке
`watchdog/` репозитория орка, а рантайм — изолированный контейнер (свой `watchdog/Dockerfile` + сервис
в `docker-compose.yml`). Sidecar периодически (тик): (1) тянет `GET /metrics` орка; (2) меряет хост
(диск/inode/память/CPU); (3) читает статусы контейнеров через read-only `docker.sock`; (4) пингует
Plane/Gitea/Anthropic. По набору **конфигурируемых порогов** через **чистую решающую функцию**
(образец `disk_watchdog.decide`) принимает решение `alert | realert | recovery | none` с дедупом/
throttle, и шлёт алерт в **собственный** Telegram-канал (свой токен/chat, независимо от кода орка).
Особый сигнал: `/metrics` не отвечает → алерт «орк не отвечает». Всё — never-raise, под kill-switch,
строго read-only к наблюдаемому (self-hosting-безопасно).

**Орк-сторона (`src/**`) не меняется**: F1b — потребитель уже существующего `GET /metrics` (F1a,
ORCH-099). `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / схема БД орка — **не тронуты**.

## 2. Задействованные модули / пути

| Путь | Действие |
|------|----------|
| `watchdog/` | **создать** — корень кода sidecar (новая папка в репо орка) |
| `watchdog/Dockerfile` | **создать** — отдельный тонкий образ sidecar (стек — выбор архитектора) |
| `watchdog/<entrypoint>` | **создать** — демон/цикл сбора+решения+отправки (имя/структура — архитектор) |
| `watchdog/<collectors>` | **создать** — сбор: `/metrics` орка (HTTP), хост (диск/inode/память/CPU), контейнеры (`docker.sock` ro), пинг Plane/Gitea/Anthropic |
| `watchdog/<decision>` | **создать** — **чистая** решающая функция порога `(value, threshold, prev_state, now, cooldown) → alert\|realert\|recovery\|none` (образец `src/disk_watchdog.py::decide`) |
| `watchdog/<notify>` | **создать** — независимый Telegram-транспорт sidecar (свой токен/chat; НЕ импорт `src/notifications.py`) |
| `watchdog/<config>` | **создать** — чтение порогов/интервалов/токенов/kill-switch из env |
| `watchdog/tests/` (или `tests/watchdog/`) | **создать** — pytest на чистые функции (решение/парсинг/детект орк-down); размещение — архитектор |
| `docker-compose.yml` | **изменить** — добавить сервис `orchestrator-watchdog` (build `watchdog/`, restart-policy, read-only `docker.sock`, `mem_limit`, env, kill-switch) |
| `.env.example` | **изменить** — канон: токен/chat watchdog + пороги + интервалы + kill-switch (без секретов) |
| `CHANGELOG.md` | **изменить** — запись о F1b |
| `docs/work-items/ORCH-100/07-infra-requirements.md` | **создать (architect)** — разовое инфра-действие: добавить сервис в compose, создать bot/chat watchdog, первый запуск на хосте |

> **`src/**` НЕ редактируется.** Если в ходе разработки выяснится нехватка поля в `/metrics` — это
> отдельная задача-расширение F1a (ORCH-099), а не правка в рамках F1b (см. BRD §«Вне объёма»).

## 3. Функциональные требования

### FR-1 — Отдельный контейнер sidecar (BR-1, NFR-1)
Sidecar собирается из `watchdog/Dockerfile` в отдельный образ и поднимается сервисом
`orchestrator-watchdog` в `docker-compose.yml`: отдельный процесс/память/рестарт-политика, **НЕ**
внутри процесса орка. `restart: unless-stopped` (или эквивалент) — sidecar самовосстанавливается.

### FR-2 — Сбор сырья орка (BR-2, NFR-6)
На каждом тике `GET <orch-metrics-url>` (дефолт-достижимость `http://127.0.0.1:8500/metrics` при
host-network; URL конфигурируем). Тело — версионированный конверт F1a:
`{schema_version, generated_at, clk_tck, stages[], queue, agents[], cost, enabled}`. Парсинг
**толерантен**: неизвестные поля игнорируются, отсутствие опционального — не ошибка, рост
`schema_version` логируется (warning), не крэшит. Из конверта извлекаются сигналы для порогов:
agent-liveness (cpu_ticks/runtime → «завис»), застрявшая стадия, job-failed, длина очереди.

### FR-3 — Детект «орк не отвечает» (BR-7) — главный сигнал
Если `GET /metrics` завершается таймаутом / connection refused / 5xx / нечитаемым телом — это
**отдельный сигнал тревоги** `orchestrator_down`. Проходит через ту же машину порога/дедупа/recovery
(BR-9): один алерт «орк не отвечает», recovery при восстановлении. Единичный transient не должен
немедленно флаппить — порог/таймаут/ретрай подбираются так, чтобы алерт был осмысленным (детали —
архитектор/developer; требование: «не флаппить на одиночной сетевой икоте»).

### FR-4 — Сбор хоста (BR-3)
Измерять заполнение диска (% и, где доступно, inode), память, CPU по доступным контейнеру
хост-путям/интерфейсам (стдлиб-средствами выбранного стека; **без** тяжёлых агентов). Пути/пороги —
конфигурируемы. **Диск:** см. FR-9 (анти-дубль с ORCH-063).

### FR-5 — Сбор контейнеров (BR-4, NFR-4)
Через `docker.sock`, смонтированный **read-only**, читать состояния контейнеров платформы:
различать Up / healthy / restarting / exited / unhealthy. Минимум — статус `orchestrator` (и других
ключевых сервисов). **Только чтение** Docker API (list/inspect) — никаких start/stop/restart/exec.

### FR-6 — Пинг внешних зависимостей (BR-5)
Периодически проверять доступность Plane / Gitea / Anthropic лёгким запросом (health/ping, короткий
таймаут, never-raise). Недоступность → сигнал для порога. Эндпоинты/таймауты — конфигурируемы.

### FR-7 — Пороговый алертинг (BR-6, BR-9)
Каждый сигнал проходит через **чистую решающую функцию** (образец `disk_watchdog.decide`):
вход `(value/state, threshold, prev_state, now, cooldown)`, выход `alert | realert | recovery | none`.
Семантика:
- не-alerting & за порогом → **ALERT** (один на пересечение);
- alerting & за порогом & cooldown истёк → **REALERT**;
- alerting & за порогом & в cooldown → **NONE** (анти-спам);
- alerting & вернулось в норму → **RECOVERY**;
- не-alerting & в норме → **NONE**.
Состояние порога (alerting/last_alert_at) — per-signal, in-memory (best-effort; рестарт sidecar
сбрасывает → корректно повторно алертит ещё стоящую проблему, как `disk_watchdog`). Хранилище
состояния/порогов (in-memory vs файл/иное) — **решение архитектора**.

### FR-8 — Независимый Telegram-транспорт (BR-8, NFR-4)
Отправка через собственный код sidecar (свой `<notify>`), читающий **свои** `bot_token`/`chat_id`
из env. **Запрещено** импортировать/вызывать `src/notifications.py` или использовать токен/функции
орка (иначе падение орка утянет алерт-канал). `disable_web_page_preview`/`parse_mode` — по
усмотрению; сообщение содержит суть алерта (сигнал, значение, порог, хост/контейнер).

### FR-9 — Анти-дубль диск-алерта (BR-10)
Диск уже алертит `disk_watchdog` (ORCH-063, порог 85%, Telegram орка). F1b **не должен** слать
второй диск-алерт на то же событие. **Владельца диск-алерта выбирает архитектор** (варианты:
sidecar становится единственным владельцем и внутренний `disk_watchdog` остаётся как fallback на
случай down-канала орка; ИЛИ sidecar не дублирует диск, оставляя его за ORCH-063). ТЗ фиксирует
инвариант: **на одно событие переполнения диска — не более одного алерта**, решение и его обоснование —
в `06-adr/`.

### FR-10 — Управляемость (NFR-5)
Kill-switch (env): выключен → sidecar не стартует / инертен, нулевой эффект на орк и конвейер.
Пороги (диск, память, agent-завис N мин, длина очереди, и т.п.), интервал тика, таймауты, cooldown —
из env (`.env.example` — канон).

### FR-11 — never-raise (NFR-3)
Три уровня: per-source (битый источник деградирует один сигнал, прочие собираются), per-tick (внешний
try/except цикла), per-send (обёрнутая отправка). Демон не падает от ошибки сбора/сети/парсинга.

## 4. Изменения API

**Нет** изменений API орка. Sidecar — **клиент** существующего `GET /metrics` (F1a, ORCH-099). Орк
новых эндпоинтов не получает. Sidecar собственного входящего HTTP-API не обязан иметь (опциональный
liveness-эндпоинт самого sidecar — на усмотрение архитектора, вне обязательного объёма).

## 5. Изменения схемы БД

**Нет.** Sidecar **не пишет** в БД орка (NFR-4) и не имеет своей БД (тонкий стек, C-3). Состояние
порогов — in-memory best-effort (FR-7). Журнал уроков (F2, БД орка) — отдельная задача, не F1b.

## 6. Требования к новым/изменённым QG checks

**Нет.** F1b живёт **вне** процесса орка и **вне** конвейера Quality Gate. `QG_CHECKS` / `check_*` /
`STAGE_TRANSITIONS` — **не тронуты** (по образцу operational-демонов `disk_watchdog`/`reaper`/
`reconciler`, которые тоже не являются Quality Gate). Sidecar — операционный наблюдатель, не гейт.

## 7. Совместимость / регресс

- **Обратная совместимость:** изменения **аддитивны** — новая папка `watchdog/`, новый сервис в
  compose, новые ключи в `.env.example`. Существующий орк-контейнер и его поведение — без изменений.
- **Kill-switch:** выключенный sidecar = нулевой эффект (не стартует), полная обратимость (NFR-5).
- **Область раската:** только инфраструктура наблюдения; конвейер всех проектов не затронут
  (self-hosting-безопасно, NFR-4).
- **Регресс:** существующий `pytest tests/` остаётся зелёным; новые тесты sidecar добавляются
  изолированно (FR — чистые функции тестируемы без контейнера/таймера, образец
  `tests/` для `disk_watchdog.decide`).
- **Разовое инфра-предусловие** (не код): добавить сервис в compose + создать bot/chat watchdog +
  первый запуск на хосте (Слава/Стрим). Зафиксировать в `07-infra-requirements.md`. Отсутствие
  bot/chat watchdog = sidecar не шлёт (fail-safe, логирует), но не падает.
