# BRD — ORCH-053: Sweeper потерянных webhook (реконсиляция застрявших стадий)

Work Item ID: ORCH-053
Стадия: analysis → (architecture)
Тип: надёжность конвейера (проектирование + реализация). Self-hosting (ORCH).

## 1. Проблема (бизнес-контекст)

Продвижение задач между стадиями конвейера завязано **исключительно** на входящие
webhook-события:
- **Plane** (`work_item.updated` → статус In Progress / Approved / Rejected) — единственный
  триггер старта задачи, advance и rollback (`src/webhooks/plane.py`).
- **Gitea** (CI-status `success`/`failure`, push, PR reviewed/merged) — триггер
  development→review, architecture→development, review→testing, deploy→done
  (`src/webhooks/gitea.py`).

Если входящее событие **потеряно** (502 на падающем/ребилдящемся инстансе, Plane/Gitea
не повторяют доставку, сетевой сбой, sha→branch не разрезолвился, вебхук был временно
выключен) — статус в источнике истины (Plane / зелёный CI) уже изменился, **а задача в
оркестраторе не сдвинулась**. Задача висит молча, без какого-либо механизма восстановления.

**Живой инцидент (ORCH-044, 06.06):** dev-агент отработал (exit 0, CI позеленел), но
Gitea webhook о CI-success не продвинул задачу (не дошёл / не сматчился sha→branch).
Задача висела бы на `development` молча навсегда — спасли только ручным дёрганьем гейта
`check_ci_green`. Это **системная дыра**, а не разовый сбой; сейчас её ловит ручной
heartbeat-watchdog Стрима (костыль).

### Что уже есть и почему недостаточно
| Механизм | Что покрывает | Почему не закрывает дыру |
|----------|---------------|--------------------------|
| `requeue_running_jobs()` (startup) | зависшие **jobs** при рестарте | про jobs, не про застрявший **stage-переход** |
| orphan-recovery (`main.py`) | `agent_runs` без `finished_at` | job-уровень, не stage |
| ORCH-5 events de-dup (`delivery_id`) | защита от **дублей** webhook | обратной защиты от **потери** нет |
| ORCH-045 `ci_poll` в `check_ci_green` | поллит CI 12×10с | только **если гейт уже вызван** webhook'ом; не пришёл webhook → гейт не вызывается |

Общий принцип всех существующих механизмов — restart-safe resilience на уровне jobs.
**Нет ни одного механизма, реконсилирующего рассинхрон «источник истины ≠ стадия задачи».**

## 2. Цель

Задача **не должна застревать молча** из-за потерянного входящего события. Ввести
фоновый периодический **sweeper / reconciler**, который сам находит «зависшие» задачи
и доигрывает пропущенный переход — через **те же штатные гейты и обработчики**, что и
webhook (никакой параллельной логики продвижения). Убрать необходимость в ручном
heartbeat-watchdog.

## 3. Заинтересованные стороны
- **Owner / Стрим (Слава)** — перестаёт ловить зависания вручную.
- **Все проекты на инстансе** (enduro-trails + orchestrator) — конвейер не встаёт молча.
- **Self-hosting (ORCH)** — особенно при ребилде прода (ORCH-51): вебхуки, прилетевшие
  на падающий инстанс, подбираются реконсиляцией после старта.

## 4. Объём (Scope)

В объёме — **две взаимодополняющие ветки реконсиляции** (обе обязательны):

### F-1. Gate-side sweeper (реконсиляция застрявшей стадии по локальной БД)
Периодический проход по таблице `tasks`: найти задачи, у которых
(а) `stage != done`, (б) нет активных job'ов в очереди, (в) с момента `updated_at`
прошло больше **per-stage порога** → пере-проверить QG текущей стадии и, если passed —
продвинуть **штатным путём** (`stage_engine.advance_stage(..., finished_agent=None)`,
тот же путь, что использует webhook). Закрывает потерю Gitea CI/PR-вебхуков (ORCH-044).

### F-2. Plane-side reconciler (реконсиляция потерянного Plane status-webhook)
Периодический опрос Plane API по проектам реестра (`projects.py`): issues в статусах,
требующих действия (In Progress / Approved / Rejected). Сверить с локальной `tasks` и
доиграть **через существующие обработчики `webhooks/plane.py`**:
- **In Progress + нет задачи в БД** → создать+запустить (`handle_status_start`/`start_pipeline`);
- **Approved + стадия не сдвинута** → advance (`handle_verdict(approved=True)`);
- **Rejected + не откатана** → rollback (`handle_verdict(approved=False)`).

### F-3. Усиление sha→branch резолва в Gitea-вебхуке
В `handle_ci_status` добавить надёжный fallback (поиск task по БД), чтобы исходный
webhook реже терялся из-за неразрезолвленного branch. Sweeper работает от задачи
(repo+branch известны из БД) и обходит эту хрупкость по определению.

### F-4. Наблюдаемость
Лог (и опц. Telegram) каждый раз, когда sweeper **разблокировал** застрявшую задачу —
чтобы видеть частоту срабатывания дыры (метрика потерянных webhook). Опц. вывод
счётчика в `/queue` или `/reconcile`. Не спамить, когда всё синхронно.

### Вне объёма
- Буфер недоставленных webhook (это ORCH-51; sweeper — резервная сетка к нему).
- Изменение состава стадий/гейтов (`STAGE_TRANSITIONS`, `QG_CHECKS`).
- Изменение логики самих гейтов и обработчиков (только переиспользование).
- Новый исполняемый деплой (ORCH-36).

## 5. Ключевые требования (бизнес-уровень)

1. **Источник истины — гейт/Plane, а не событие.** Sweeper дёргает ровно те же функции
   продвижения, что и webhook. Параллельной логики продвижения быть не должно.
2. **Идемпотентность (критично).** Задержавшийся или дублированный webhook + sweeper
   НЕ создают двойную задачу / двойной запуск / двойной advance. Тот же guard, что у
   webhook: нет активного job + стадия совпадает + atomic claim как в `queue_worker`.
3. **Безопасность активной работы.** Sweeper НЕ трогает задачи с активными
   (`queued`/`running`) job'ами — они легитимно в работе, не потеряны.
4. **Per-stage grace.** Разные стадии имеют разное нормальное время (analysis ~8–15 мин
   vs deploy). Порог застревания настраивается, чтобы не дёргать гейт у задачи, где агент
   законно работает.
5. **Restart-safe.** Sweeper — фоновый поток, стартует с приложением, переживает рестарт
   (как `queue_worker`). Без потери состояния.
6. **Self-hosting safety.** Sweeper не должен ронять/рестартить прод-контейнер; kill-switch
   в конфиге для поэтапного раската и аварийного отключения.
7. **Без шума.** Когда всё синхронно — никаких действий и нотификаций.
8. **Документация = golden source.** README/architecture, ADR, CHANGELOG обновляются в
   том же PR.

## 6. Эффект
- Потерянный webhook больше не = молча застрявшая задача.
- Ручной heartbeat-watchdog Стрима больше не нужен для ловли зависаний (AC-5 в эпике).
- Резервная сетка к ORCH-51 при ребилде прода.

## 7. Связи
- **Дополняет ORCH-51** (потеря webhook при рестарте — буфер; sweeper — реконсиляция).
- **Дополняет ORCH-36** (если deploy-webhook потеряется — sweeper добьёт deploy→done).
- **ORCH-1b** — та же философия resilience: транзиентный сбой не убивает задачу.
- Эпик: звено **ORCH-54** (автономное внедрение). Параллельна ORCH-36 (разные файлы),
  но `max_concurrency=1` → встанет в очередь.

## 8. Риски (кратко; подробно — 10-tech-risks архитектора)
- **Гонка sweeper ↔ живой webhook** → двойной запуск. Митигируется atomic claim +
  active-job guard + grace-период (не конкурировать с задержавшимся webhook).
- **Spam нотификаций** при персистентно красном гейте на каждом тике. Митигируется:
  действие/нотификация только на изменении состояния (advance), не на каждый тик.
- **Нагрузка на Plane API** при опросе каждые N сек. Митигируется интервалом + фильтром
  по статусам + per-project.
- **Self-hosting:** sweeper правит инструмент, обслуживающий и другие проекты. Kill-switch
  обязателен.
