114 lines
8.6 KiB
Markdown
114 lines
8.6 KiB
Markdown
# ТЗ: 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.
|