# adr-0007: Reconciler застрявших стадий (sweeper потерянных webhook)

- **Статус:** accepted (реализовано в `src/reconciler.py`)
- **Дата:** 2026-06-06
- **Задача:** ORCH-053
- **Детальный ADR:** `docs/work-items/ORCH-053/06-adr/ADR-001-stuck-task-reconciler.md`

## Контекст
Конвейер продвигается **только** входящими webhook (Plane status / Gitea CI/PR).
Потерянное событие (502 на ребилде, отсутствие ретраев у Plane/Gitea,
неразрезолвленный `sha→branch`) → источник истины изменился, а стадия задачи —
нет; задача застревает молча (инцидент ORCH-044). Существующий resilience
(`requeue_running_jobs`, orphan-recovery, events de-dup ORCH-5, `ci_poll`
ORCH-045) работает на уровне jobs/agent_runs и **не реконсилирует**
рассинхрон «источник истины ≠ стадия задачи».

## Решение
Фоновый daemon-поток `src/reconciler.py` (паттерн `queue_worker`, module-singleton,
`threading.Event`), стартует в `main.lifespan` после `worker.start()`, стоп в
`finally` перед `worker.stop()`. Две взаимодополняющие ветки на каждом тике
(`reconcile_interval_s`, дефолт 120с):

- **F-1 gate-side** (локальная БД): для каждой `task` где `stage∉{done}`, **нет**
  активного job, `age(updated_at) ≥ grace_for_stage(stage)` — read-only пред-оценка
  канонического QG стадии; если зелёный → продвижение **штатным**
  `stage_engine.advance_stage(..., finished_agent=None)` (тот же путь, что у Plane
  Approved-webhook). Красный → **тишина** (нет advance, нет нотификаций — спам
  структурно невозможен). `analysis` F-1 **не** реконсилирует (человеческий гейт →
  отдан F-2).
- **F-2 plane-side** (опрос Plane API per-project через `list_issues_by_state`):
  `In Progress`+нет задачи → `handle_status_start`; `Approved`+не сдвинута →
  `handle_verdict(approved=True)`; `Rejected`+не откатана →
  `handle_verdict(approved=False)`. Обработчики `webhooks/plane.py`
  **переиспользуются** (async → `asyncio.run` из sync-потока), логика не дублируется.
- **F-3:** усиление `sha→branch` в `handle_ci_status` (БД-fallback по
  `repo`+`stage='development'`, видимость на INFO) — defense-in-depth.

**Инварианты:** источник истины — гейт/Plane, не событие; продвижение только через
`advance_stage`; идемпотентность (active-job guard + atomic-claim на создании +
grace + `max_concurrency=1`); never-raise на единицу работы; тишина при
синхронности; restart-safe; kill-switch.

## Альтернативы
- **Флаг подавления нотификаций в `advance_stage`** — отклонён: меняет общий
  критический путь. Вместо этого «не вызывать advance_stage на красном гейте».
- **UNIQUE-индекс `tasks.plane_id`** для анти-дубля — отклонён как primary: риск
  падения миграции на проде; выбран process-wide `threading.Lock` (single-process
  топология). Индекс — задокументированное будущее упрочнение для multi-process.
- **Отдельная стадия/QG реконсиляции** — вне объёма; нарушает «источник истины —
  существующий гейт».
- **Реконсиляция analysis по локальным артефактам** — отклонена: автопродвижение
  неодобренного человеком BRD.

## Последствия
- Потерянный webhook ≠ молча застрявшая задача; ручной heartbeat-watchdog не нужен;
  резервная сетка к ORCH-51 (буфер недоставленных) и ORCH-36 (deploy).
- Плата: фоновый поток + опрос Plane API (митигируется интервалом/фильтром/
  per-project); двойная оценка гейта на зелёной задаче; анти-дубль опирается на
  single-process-допущение (как и очередь ORCH-1).
- Self-hosting: `reconcile_enabled` — обязательный kill-switch; поэтапный раскат
  (`reconcile_plane_enabled` гасит только F-2); reconciler не рестартит/не роняет
  прод-контейнер. БД-схема и реестры (`STAGE_TRANSITIONS`/`QG_CHECKS`) не меняются.

## Уточнения
- **ORCH-060** (`docs/work-items/ORCH-060/06-adr/ADR-001-reconciler-skip-escalated.md`):
  F-1 (`_reconcile_gate_task`) приобретает два пред-гарда ДО оценки гейта —
  пропускает escalated (`developer_retry_count ≥ MAX_DEVELOPER_RETRIES`,
  детерминированно) и Blocked/Needs-Input (Вариант A, Plane API, без миграции)
  задачи. Инварианты adr-0007 сохранены (схема/реестры не меняются, never-raise,
  тишина при пропуске).

- **ORCH-068** (`docs/work-items/ORCH-068/06-adr/ADR-001-reconciler-terminal-exclusion-and-cache-ttl.md`):
  фикс livelock F-2 (спам `_note_unblock` по синхронизированной done-задаче после
  ORCH-066). F-2 исключает терминалы по **группе состояния** (`completed`/`cancelled`,
  fallback — ключи `done`/`cancelled`) проектно-независимо; `_note_unblock` — только при
  подтверждённом state change (сравнение стадии до/после `_dispatch`) + in-memory дедуп;
  `_STATES_CACHE` получает TTL (`ORCH_PLANE_STATES_TTL_S`, дефолт 300с, `0`=lifetime).
  Инварианты adr-0007 сохранены (источник истины — Plane; реестры/схема/`handle_*`/F-1/F-3
  не меняются; never-raise; kill-switch'и).

## Связи
adr-0002 (очередь / `available_at`, single-process-singleton), adr-0003 (условный
гейт — образец условности/флагов раската), adr-0006 (merge-gate как под-гейт ребра
внутри `advance_stage`), adr-0001 (реестр проектов для F-2 per-project), ORCH-5
(events de-dup — защита от дублей; reconciler — обратная защита от потерь),
ORCH-045 (`ci_poll`).
