# ТЗ: Reconciler пропускает escalated / max-retries / blocked-needs-input задачи

Work Item ID: ORCH-060
Стадия: analysis → architecture (архитектор фиксирует механику в ADR)

## 1. Задействованные модули `src/`

| Модуль | Роль в задаче |
|--------|---------------|
| `src/reconciler.py` | **Основное изменение.** F-1: `Reconciler._reconcile_gate_task` — добавить пред-проверки (escalated / blocked / needs-input) ДО `advance_if_gate_passed`. |
| `src/stage_engine.py` | Источник `MAX_DEVELOPER_RETRIES` (=3) и `_developer_retry_count(task_id)`. Кандидат на промоут приватного хелпера в переиспользуемый (решает архитектор). |
| `src/db.py` | Чтение состояния задачи (`get_active_tasks_for_reconcile` уже отдаёт строки `tasks`); возможный новый read-helper для retry-count, если решено не импортировать приватный из stage_engine. |
| `src/plane_sync.py` | Маппинг Plane-статусов (`PLANE_STATES`, `get_project_states`): `blocked`, `needs_input`. Источник для проверки «человеческого» статуса, если архитектор выберет проверку через Plane API. |
| `src/webhooks/gitea.py` | НЕ меняется (только справочно: точки эскалации `:280`, `:371`). |

## 2. Требуемое поведение (контракт F-1)

`Reconciler._reconcile_gate_task(task)` ДО вызова `advance_if_gate_passed(...)`
обязан вернуться (пропустить задачу, ничего не делая, не инкрементируя
`unblocked_total`, не слать нотификации), если выполнено ЛЮБОЕ из условий:

1. **Escalated по ретраям (обязательно, детерминированно, без сети):**
   `developer_retry_count(task_id) >= MAX_DEVELOPER_RETRIES`.
   - `MAX_DEVELOPER_RETRIES` импортируется из `stage_engine` (НЕ хардкодить число).
   - Источник счётчика — тот же запрос, что в `_developer_retry_count`:
     `SELECT COUNT(*) FROM agent_runs WHERE task_id=? AND agent='developer'`.

2. **Явный человеческий/терминальный Plane-статус:** issue в состоянии
   **Blocked** или **Needs Input**.

Порядок: проверки добавляются в `_reconcile_gate_task` ПОСЛЕ существующих гардов
(`stage=='analysis'` carve-out, `get_qg_for_stage is None`, `has_active_job_for_task`,
grace) и ДО `advance_if_gate_passed`. Условие (1) — дешёвое (локальный SQL) —
проверять раньше условия (2), если (2) требует сети.

## 3. Механика проверки blocked/needs-input (выбор — за архитектором, ADR)

В таблице `tasks` НЕТ столбца статуса (`stage` всегда `development` у escalated).
Архитектор выбирает и обосновывает один из вариантов; требования к каждому:

- **Вариант A — проверка через Plane API (без миграции, предпочтительно по
  инварианту ORCH-053 «схема не меняется»):** для кандидата F-1 запросить текущее
  состояние issue (per-project `get_project_states` → сверка с `blocked`/`needs_input`).
  Допустимо, т.к. F-1 уже делает сетевой вызов в гейте (`check_ci_green`), а
  кандидатов после grace+no-active-job немного. Обязателен never-raise: ошибка
  запроса → консервативно НЕ трогать задачу (skip), либо явно обоснованный фоллбэк.
- **Вариант B — локальный терминальный маркер в БД:** идемпотентная миграция
  (`tasks.blocked`/`tasks.reconcile_skip`), выставляется в точках `set_issue_blocked`/
  `set_issue_needs_input` и в точках эскалации `gitea.py`. Требует обоснования
  нарушения инварианта «схема reconciler не меняется» и затрагивает больше точек.

> Рекомендация аналитика: условие (1) полностью закрывает зафиксированный инцидент
> (ET-013 = escalated = max retries) детерминированно и без сети — оно
> обязательно к реализации. Условие (2) — защита от автоперекрытия ручного гейта;
> минимально-инвазивный путь — Вариант A. Архитектор вправе ограничить (2)
> Вариантом A либо обосновать B.

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

Нет. Эндпоинты не добавляются и не меняются. Снимок `GET /queue` (блок `reconcile`)
по содержимому не меняется; опционально архитектор может добавить best-effort
счётчик `skipped_escalated` (необязательно, вне scope AC).

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

По умолчанию — **нет** (Вариант A). При выборе Варианта B — идемпотентная
ALTER-миграция через `_ensure_column` (как остальные в `db.init_db`),
restart-safe, безопасная на живой прод-БД; обязательна явная мотивация в ADR.

## 6. Требования к QG checks

Нет новых QG. Реестр `QG_CHECKS` и `STAGE_TRANSITIONS` не меняются. Гард —
ВНЕ гейта: он решает, ЗАПУСКАТЬ ли пред-оценку гейта вообще, а не меняет вердикт
гейта.

## 7. Инварианты, которые нельзя нарушить

- **Never-raise** на единицу работы (per-task `try/except` в `reconcile_gate_once`
  сохраняется; новая логика не должна бросать наружу).
- **Тишина при пропуске:** пропущенная задача не инкрементирует `unblocked_total`,
  не пишет лог `разблокирована`, не шлёт Telegram.
- **Регресс F-1 happy-path:** задача с retry < лимита и не-Blocked/Needs-Input при
  зелёном гейте по-прежнему доигрывается (`advance_stage` вызывается).
- **F-2** по существу не меняется: Blocked/Needs Input не входят в
  {in_progress, approved, rejected} → не доигрываются (зафиксировать регресс-тестом).
- `analysis` carve-out F-1 сохраняется.
- Kill-switch'и (`reconcile_enabled`, `reconcile_plane_enabled`) работают как прежде.

## 8. Артефакты pipeline, которые должны быть созданы/обновлены

- `docs/work-items/ORCH-060/06-adr/ADR-001-*.md` — решение по механике (2) (A vs B).
- `docs/architecture/README.md` — дополнить описание F-1 («skip escalated /
  blocked / needs-input»).
- `CHANGELOG.md` — запись `fix(reconciler): ...`.
- Тесты — `tests/test_reconciler.py` (расширение).
- Обновить footer `docs/architecture/README.md` (статус ORCH-060).

## 9. Точки изменения кода (конкретно)

1. `src/reconciler.py`, `_reconcile_gate_task`: после grace-проверки и до
   `advance_if_gate_passed` вставить:
   ```python
   # ORCH-060: escalated tasks (max developer retries reached) are terminal —
   # they wait for a human, not the sweeper. Skip deterministically (no network).
   if developer_retry_count(task_id) >= MAX_DEVELOPER_RETRIES:
       return
   # ORCH-060: respect an explicit human gate (Blocked / Needs Input).
   if self._is_blocked_or_needs_input(task):  # mechanism per ADR (Variant A/B)
       return
   ```
2. `src/reconciler.py`: импорт `MAX_DEVELOPER_RETRIES` (и retry-count хелпера) из
   `stage_engine` (или новый read-helper в `db.py`).
3. Хелпер проверки Plane-статуса (`_is_blocked_or_needs_input`) — never-raise.
