# ТЗ — ORCH-021: Post-deploy мониторинг прода + авто-rollback

Work Item: ORCH-021
Стадия: analysis → (architecture)

> Документ описывает ТРЕБОВАНИЯ к изменениям и НАЗЫВАЕТ задействованные модули.
> Выбор механизма (стадия vs watchdog vs reserved-agent) и точная реализация —
> зона архитектора (см. BRD §7). Здесь фиксируется, ЧТО должно измениться и КАКИЕ
> контракты НЕЛЬЗЯ ломать.

## 1. Контекст в коде (как есть сейчас)

- Конвейер заканчивается в `src/stages.py`: `deploy → done`, gate `check_deploy_status`.
  Терминальный переход `deploy → done` исполняется в `src/stage_engine.py::advance_stage`
  (блок «Terminal sync», `set_issue_done`, release merge-lease). После этого ничего
  не наблюдает за продом.
- `scripts/orchestrator-deploy-hook.sh` уже умеет:
  - `health_check(max_attempts, sleep, label)` — опрос `http://localhost:$TARGET_PORT/health`
    с проверкой `"status":"ok"`;
  - `do_rollback()` — retag `PREV_IMAGE_FILE` → `TARGET_IMAGE` + рестарт + пост-rollback
    health-check; коды возврата 0 (ок) / 1 (нет prev-образа) / 2 (rollback тоже упал);
  - режим `--rollback` (ручной откат);
  - при обычном деплое сохраняет `PREV_IMG` в `PREV_IMAGE_FILE`
    (`.deploy-prev-image-prod` для прода, см. `settings.deploy_prod_prev_image_file`).
- Self-deploy прода идёт через detached host-процесс: `src/self_deploy.py`
  (`build_deploy_command`, `initiate_deploy`, sentinel-маркеры под
  `.deploy-state-<repo>/<wi>/`, `read_result`, `map_exit_code_to_status`).
- Фоновый daemon-паттерн: `src/reconciler.py` (`threading.Thread(daemon=True)` +
  `threading.Event`, старт/стоп в `src/main.py::lifespan` после `worker.start()` /
  перед `worker.stop()`, `status()` в `GET /queue`).
- Reserved-agent (детерминированный no-LLM job) паттерн: `deploy-finalizer` —
  перехват в `src/agents/launcher.py::launch_job` ДО `_spawn`, исполнение
  `stage_engine.run_deploy_finalizer`, отложенная постановка через
  `enqueue_job(..., available_at_delay_s=...)`.
- Условность self-hosting: `src/qg/checks.py::is_self_hosting_repo`,
  `src/self_deploy.py::self_deploy_applies` (флаг + CSV-репо; пусто → только `orchestrator`).
- Наблюдаемые эндпоинты прода (`src/main.py`): `GET /health`, `GET /status`, `GET /queue`.
- API БД: `src/db.py::enqueue_job` (с `available_at_delay_s`), `get_db`,
  `update_task_stage`, `get_active_tasks_for_reconcile`.

## 2. Требуемые изменения

### 2.1. Новый leaf-модуль чистой логики наблюдения — `src/post_deploy.py` (новый)
Контракт **never-raise** (по образцу `self_deploy.py` / `staging_verdict.py`).
Чистые, юнит-тестируемые функции:
- **Опрос сигналов:** функция, опрашивающая `/health` и ключевые эндпоинты
  (`/status`, `/queue`) прод-инстанса (base-url из config), возвращающая структуру
  с результатами (код ответа, ok-флаг, доля 5xx). Сеть/таймаут → консервативный
  результат, не исключение.
- **Классификация деградации** (чистая, без сети): на вход — серия результатов
  опросов; на выход — вердикт `HEALTHY | DEGRADED` по порогам (BR-3):
  `≥ post_deploy_fail_threshold` последовательных провалов health ИЛИ доля 5xx
  выше `post_deploy_5xx_threshold` на окне. Эта функция — основной предмет
  юнит-тестов (детерминированная, как `compute_staging_verdict` в ORCH-061).
- **Решение о реакции** (чистая): по `(repo, вердикт, политика)` → одно из
  `NONE | ROLLBACK | ALERT_ONLY`, с учётом self-hosting (BR-5).
- **Запись артефакта** результата наблюдения (см. §2.5), best-effort.
- Условность: хелпер `post_deploy_applies(repo)` (флаг + CSV-репо, пусто →
  только self-hosting), по образцу `self_deploy_applies` / `_merge_gate_applies`.

### 2.2. Оркестрация наблюдения (механизм — выбор архитектора)
Требования к механизму (независимо от выбора стадия/watchdog/reserved-agent):
- запускается ПОСЛЕ перехода `deploy → done` для применимого репозитория (BR-1);
- наблюдает окно `post_deploy_window_s` с интервалом `post_deploy_interval_s`;
- **restart-safe и идемпотентен** (BR-7): состояние наблюдения — в sentinel-файлах
  (по образцу `.deploy-state-<repo>/<wi>/`, напр. маркеры `monitor-started` /
  `monitor-done`) ИЛИ через отложенные `enqueue_job(available_at_delay_s=...)`;
  повторный старт не задваивает наблюдение и не теряет его при рестарте;
- по итогу вызывает «Решение о реакции» из `src/post_deploy.py` и исполняет реакцию (§2.3).

Кандидатные точки интеграции (на выбор архитектора, см. BRD §7):
- хук в `stage_engine.advance_stage` в блоке `next_stage == "done"` — арм наблюдения;
- reserved-agent `post-deploy-monitor` (расширение `launcher.launch_job` ДО `_spawn`,
  как `deploy-finalizer`), с само-перепостановкой через `available_at_delay_s`;
- отдельный daemon-поток `PostDeployWatcher` (как `Reconciler`), старт/стоп в `main.lifespan`.

### 2.3. Реакция на деградацию
- **Не-self репозитории / политика auto:** вызвать существующий хук в режиме отката
  (`scripts/orchestrator-deploy-hook.sh --rollback` с прод-параметрами окружения,
  как в `self_deploy.build_deploy_command`, но action=`--rollback`). Маппинг
  exit-code хука (0/1/2) в исход переиспользует логику `self_deploy.map_exit_code_to_status`
  по смыслу (0 → откат успешен; 1/2 → откат не выполнен/провалился → громкий алерт).
- **Self-hosting (`orchestrator`) по умолчанию (BR-5):** НЕ откатывать автоматически.
  Сформировать громкий алерт (Telegram + Plane-коммент) и запросить ручной approve
  отката (по образцу deploy Phase A — статус Plane / Telegram CTA). Откат самого
  прод-орка, если выполняется, — только через detached host-процесс (нельзя надёжно
  откатить контейнер, который при этом умирает; переиспользовать механику
  `self_deploy.initiate_deploy`).
- Команда отката для self НЕ должна ронять прод-контейнер в рамках обычного тика
  наблюдения (CLAUDE.md: не ронять/не рестартить прод-контейнер вне явного действия).

### 2.4. Конфигурация — `src/config.py` (расширение `Settings`)
Добавить (env-префикс `ORCH_`, дефолты безопасные):
- `post_deploy_monitor_enabled: bool = True` — глобальный kill-switch (BR-8).
- `post_deploy_repos: str = ""` — CSV применимых репо; пусто → только self-hosting
  (по образцу `self_deploy_repos` / `merge_gate_repos` / `image_freshness_repos`).
- `post_deploy_window_s: int = 900` — длина окна наблюдения (дефолт ~15 мин, BR-1).
- `post_deploy_interval_s: int = 30` — интервал между опросами.
- `post_deploy_fail_threshold: int = 3` — N последовательных провалов health → DEGRADED.
- `post_deploy_5xx_threshold: float = 0.5` — порог доли 5xx на окне → DEGRADED.
- `post_deploy_auto_rollback: bool = False` — глобально разрешён ли авто-откат;
  при `True` действует для не-self репо; для self всегда требует approve (BR-5).
- `post_deploy_base_url: str = "http://localhost:8500"` — base-url наблюдаемого прода.
- `post_deploy_target` параметры отката — переиспользовать существующие
  `deploy_prod_*` (service/port/image/prev_image_file), новых дублей не вводить.

### 2.5. Артефакт задачи — `16-post-deploy-log.md` (новый)
В `docs/work-items/<plane-id>/`. YAML-frontmatter (машиночитаемо, канон гейтов;
для будущей петли уроков BR-10):
```
---
post_deploy_status: HEALTHY | DEGRADED
action_taken: NONE | ROLLBACK_OK | ROLLBACK_FAILED | ALERT_ONLY
work_item: <plane-id>
window_s: <int>
checks_total: <int>
checks_failed: <int>
---
```
Тело — человекочитаемая сводка опросов. Записывается best-effort (по образцу
`self_deploy.write_deploy_log`); отсутствие файла не должно ничего ронять.
> Артефакт `16-post-deploy-log.md` добавить в перечень артефактов в `CLAUDE.md`
> и таблицу/описание в `docs/architecture/README.md` (golden-source, в том же PR).

### 2.6. Наблюдаемость — `GET /queue` (`src/main.py`) (BR-9)
Добавить блок `post_deploy` со снимком состояния (enabled, window, активные
наблюдения, последний исход) — по образцу блока `reconcile` (метод `status()`).

### 2.7. Изменения схемы БД
**Не требуются.** Состояние наблюдения — sentinel-файлы (restart-safe, без миграции,
по образцу ORCH-36) и/или отложенные jobs. Если архитектор выберет колонку в `tasks`
для отметки наблюдения — потребуется миграция; предпочтительно избежать (как ORCH-36/53/58).

### 2.8. Новые QG checks
**Не требуются.** Наблюдение происходит ПОСЛЕ `done` и не является gate'ом стадии;
реестр `QG_CHECKS` и `STAGE_TRANSITIONS` не меняются (если архитектор НЕ выберет
вариант «отдельная пост-deploy стадия» — тогда потребуется новая стадия+gate, что
надо явно отразить в ADR; по умолчанию предпочтителен вариант без изменения реестров).

## 3. Инварианты (НЕ ломать)
- `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, контракт `check_deploy_status` /
  `_parse_deploy_status`, момент вердикта `deploy_status`, БАГ-8 откат, terminal-sync
  `deploy → done`, merge-gate, exit-code-контракт хука (0/1/2) — без изменений.
- Контракт хука: дефолты STAGING-безопасны; прод-параметры приходят только через env.
- Условность как ORCH-35/36/43/58: реально для `orchestrator`/listed-repos, прочие — no-op.
- Never-raise: ошибка в наблюдении не роняет worker / lifespan / конвейер других проектов.
- Self-hosting: тик наблюдения НИКОГДА не рестартит прод-контейнер сам по себе (BR-5).

## 4. Задействованные модули (сводка)
| Модуль | Изменение |
|--------|-----------|
| `src/post_deploy.py` | **новый** — чистая логика опроса/классификации/решения/артефакта, never-raise |
| `src/config.py` | +параметры `post_deploy_*` (kill-switch, окно, пороги, политика) |
| `src/stage_engine.py` и/или `src/agents/launcher.py` и/или `src/main.py` | арм/исполнение наблюдения (точка — за архитектором) |
| `scripts/orchestrator-deploy-hook.sh` | переиспользуется (`--rollback`); правки — только если откат self требует отдельной ветки (за архитектором) |
| `src/main.py` | блок `post_deploy` в `GET /queue` (BR-9); возможный старт daemon в `lifespan` |
| `docs/work-items/<id>/16-post-deploy-log.md` | **новый** артефакт |
| `CLAUDE.md`, `docs/architecture/README.md`, `CHANGELOG.md` | обновить (golden-source, в том же PR) |
| ADR | `docs/work-items/ORCH-021/06-adr/ADR-001-*.md` (+ возможный сквозной `adr/adr-00NN`) |

## 5. Артефакты по pipeline, которые должны появиться/обновиться
- `16-post-deploy-log.md` (новый, машиночитаемый frontmatter).
- Обновлённые `CLAUDE.md` (перечень артефактов), `docs/architecture/README.md`
  (описание пост-деплой наблюдения), `CHANGELOG.md`.
- ADR work-item (`06-adr/`) с зафиксированным выбором механизма и порогов.
