Files
orchestrator/docs/work-items/ORCH-060/02-trz.md
claude-bot 365c67f45d
All checks were successful
CI / test (push) Successful in 17s
analyst(ET): auto-commit from analyst run_id=288
2026-06-07 11:28:57 +00:00

8.6 KiB
Raw Permalink Blame History

ТЗ: 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 вставить:
    # 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.