# ТЗ: 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.