8.6 KiB
ТЗ: 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, не слать нотификации), если выполнено ЛЮБОЕ из условий:
-
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'.
-
Явный человеческий/терминальный 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} → не доигрываются (зафиксировать регресс-тестом).
analysiscarve-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. Точки изменения кода (конкретно)
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) returnsrc/reconciler.py: импортMAX_DEVELOPER_RETRIES(и retry-count хелпера) изstage_engine(или новый read-helper вdb.py).- Хелпер проверки Plane-статуса (
_is_blocked_or_needs_input) — never-raise.