# ТЗ — ORCH-068: устранить livelock reconciler F-2 + спам unblock done-задачи

> Документ описывает ТРЕБОВАНИЯ к изменению поведения (что и где), а не выбор реализации. Конкретный механизм (state.group vs явный allowlist терминалов; TTL vs flush-on-unknown) — решение архитектора в ADR.

## 1. Затронутые модули `src/`
| Модуль | Роль в баге | Требуемое изменение |
|--------|-------------|---------------------|
| `src/reconciler.py` | F-2 `_reconcile_plane_project` / `_reconcile_plane_issue` / `_note_unblock` | Исключить терминальные статусы из actionable-выборки; `_note_unblock` только при подтверждённом state change; дедуп. |
| `src/plane_sync.py` | `get_project_states`, `list_issues_by_state`, `_STATES_CACHE` | Дать способ различать терминальные/review статусы (группа состояния); устранить вечно-устаревший кэш (TTL/flush). |
| `src/config.py` | флаги | (если нужны) новые kill-switch/настройки TTL — с дефолтами, не меняющими прод-инварианты. |
| `src/main.py` (`/queue`) | наблюдаемость | (опц.) отразить дедуп/skip-терминалов в снимке `reconcile`. |

**НЕ трогать:** `src/stages.py` (`STAGE_TRANSITIONS`), `src/qg/checks.py` (`QG_CHECKS`), схему БД, контракты `handle_status_start` / `handle_verdict`, F-1 (`reconcile_gate_once`), F-3.

## 2. Требования к F-2 (src/reconciler.py)

### TR-1. Исключить терминальные статусы из actionable-выборки
`_reconcile_plane_project` НЕ должен подавать задачи в терминальном Plane-статусе (`Done` и прочие completed-группы, `Cancelled`) ни в `list_issues_by_state`, ни в последующее сравнение веток.
- Требование проектно-независимое: работает на enduro И orchestrator, независимо от того, «схлопывает» ли проект статусы по UUID.
- Различение `Done`/`Cancelled` (completed) от `approved`/`rejected` (review) НЕ должно опираться только на голый UUID, если проект их алиасит. Допустимый ориентир — группа состояния Plane (`state.group`: `completed`/`started`/`unstarted`/`backlog`/`cancelled`) либо явный набор логических ключей терминалов. Выбор — за архитектором.

### TR-2. `_note_unblock` — только при реальном state change
`_note_unblock` (лог + Telegram + инкремент `unblocked_total`) ВЫЗЫВАЕТСЯ ТОЛЬКО когда диспетчеризованный обработчик фактически изменил состояние задачи (advance / replayed transition, реально сдвинувший стадию). No-op dispatch (задача уже в целевом состоянии) → нотификация НЕ отправляется.
- Сейчас `_dispatch` (`asyncio.run(coro_fn(...))`) отбрасывает результат, а `_note_unblock` зовётся безусловно. Требуется механизм подтверждения изменения (напр. сравнение стадии задачи до/после dispatch через `get_task_by_plane_id`, либо проброс сигнала из обработчика). Конкретику выбирает архитектор; контракт обработчиков `handle_*` менять НЕ обязательно (предпочтительно сравнение состояния до/после на стороне reconciler).
- Восстановить соответствие docstring `_note_unblock`: «Fires only on an actual state change … never per idle tick».

### TR-3. Дедуп / идемпотентность нотификаций
Reconciler НЕ должен слать повторное уведомление о той же задаче, если её состояние не менялось с прошлого тика. TR-1+TR-2 закрывают основной кейс (done более не входит в выборку и не нотифицируется); TR-3 — дополнительная страховка (best-effort), чтобы любой будущий no-op путь не дал повторного спама.

## 3. Требования к статус-кэшу (src/plane_sync.py) — secondary

### TR-4. Устаревший `_STATES_CACHE` обновляется без рестарта
После появления нового Plane-статуса процесс не должен бесконечно держать устаревший набор. Допустимые подходы (выбор архитектора, можно комбинировать):
- TTL на запись кэша (напр. `ORCH_PLANE_STATES_TTL_S`, дефолт разумный, 0/неуст. = прежнее поведение для совместимости);
- flush-on-unknown: при детекте неизвестного статуса в вебхуке/реконсилере — вызвать существующий `reload_project_states(pid)` и перезапросить;
- админ-эндпоинт/сигнал для ручного flush без рестарта.
`reload_project_states()` уже существует — переиспользовать как примитив сброса, новую логику сброса не дублировать.

## 4. Изменения API
- Новых обязательных endpoint'ов нет.
- Опционально (TR-4, на усмотрение архитектора): admin-эндпоинт сброса кэша статусов (напр. `POST /admin/plane-states/reload`) — если выбран этот вариант flush. Должен быть защищён/идемпотентен; документировать в README таблице API.
- Снимок `GET /queue` (блок `reconcile`) — без ломающих изменений; допустимо добавить поля наблюдаемости (skip-терминалов / dedup-счётчик).

## 5. Изменения схемы БД
**Нет.** Дедуп TR-3 реализуется in-memory (best-effort, как существующие счётчики `unblocked_total`/`last_unblocked`, AC-11 ORCH-053 допускает их сброс при рестарте) либо через сравнение живого состояния Plane/БД. Миграции не требуются.

## 6. Требования к новым QG checks
Нет. Реестр `QG_CHECKS` не меняется.

## 7. Инварианты (обязаны сохраниться)
- INV-1. Source of truth F-2 — Plane (НЕ меняем).
- INV-2. never-raise на единицу работы (per-issue / per-project / per-tick) сохранён.
- INV-3. Kill-switch `ORCH_RECONCILE_ENABLED` (+ `ORCH_RECONCILE_PLANE_ENABLED` гасит только F-2) — работают.
- INV-4. F-1 / F-3 поведение не изменено.
- INV-5. 0 jobs / 0 токенов для синхронизированных задач (как сейчас) сохранено.
- INV-6. Легитимная разблокировка реально-потерянного approved/in_progress webhook продолжает работать (нет регресса F-2).
- INV-7. Self-hosting: тик reconciler НИКОГДА не рестартит/не роняет прод-контейнер.

## 8. Артефакты pipeline, которые надо обновить в ТОМ ЖЕ PR
- `CLAUDE.md` — если меняется наблюдаемое поведение reconciler/кэша (раздел про reconciler/правила).
- `docs/architecture/README.md` — секция «Reconciler … (ORCH-053)»: уточнить исключение терминалов + дедуп; при TR-4 — секция «Plane Sync».
- `docs/architecture/adr/adr-0007-reconciler.md` (или новый per-WI ADR `docs/work-items/ORCH-068/06-adr/ADR-001-…`) — зафиксировать решение по терминалам/группе состояния и по кэшу.
- `CHANGELOG.md` — запись о фиксе (`fix:`).
- `.env.example` / `.env.staging` — если введён новый флаг (TTL/kill-switch).

## 9. Замечания по приёмке/тестированию
- Регресс-тест ОБЯЗАН покрывать: задача в `Done` (синхронизирована) → тик F-2 = 0 нотификаций, на enduro И orchestrator проектах (terminal не зависит от алиасинга).
- Тест НЕ должен делать реальных сетевых вызовов — мокать `list_issues_by_state` / `get_project_states` / `send_telegram` / `_dispatch` (handle_*).
