architect(ET): auto-commit from architect run_id=289
All checks were successful
CI / test (push) Successful in 16s
All checks were successful
CI / test (push) Successful in 16s
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
- **Quality Gates** (`src/qg/checks.py`) — проверки выхода со стадии, реестр `QG_CHECKS`.
|
||||
- **Agent Launcher** (`src/agents/launcher.py`) — запуск Claude CLI агентов в изолированном git worktree, мониторинг, auto-advance.
|
||||
- **Queue** (`src/queue_worker.py`, ORCH-1) — персистентная очередь задач (SQLite `jobs`), atomic claim, max_concurrency, ретраи, restart-safe.
|
||||
- **Reconciler** (`src/reconciler.py`, ORCH-053 — реализовано, [adr-0007](adr/adr-0007-reconciler.md)) — фоновый daemon-поток (паттерн `queue_worker`), стартует/останавливается в `main.lifespan` (после `worker.start()` / перед `worker.stop()`). Реконсилирует рассинхрон «источник истины ≠ стадия задачи» при потерянном webhook. F-1 gate-side (продвигает застрявшую стадию по локальной БД через штатный `advance_stage(..., finished_agent=None)`), F-2 plane-side (опрос Plane API → `handle_*` из `plane.py`), F-3 (БД-fallback `sha→branch` в `handle_ci_status`). Источник истины — гейт/Plane, не событие; идемпотентность (active-job guard + atomic-claim + grace); kill-switch `ORCH_RECONCILE_ENABLED`. `analysis` F-1 не трогает (человеческий гейт). Наблюдаемость — блок `reconcile` в `GET /queue`.
|
||||
- **Reconciler** (`src/reconciler.py`, ORCH-053 — реализовано, [adr-0007](adr/adr-0007-reconciler.md)) — фоновый daemon-поток (паттерн `queue_worker`), стартует/останавливается в `main.lifespan` (после `worker.start()` / перед `worker.stop()`). Реконсилирует рассинхрон «источник истины ≠ стадия задачи» при потерянном webhook. F-1 gate-side (продвигает застрявшую стадию по локальной БД через штатный `advance_stage(..., finished_agent=None)`), F-2 plane-side (опрос Plane API → `handle_*` из `plane.py`), F-3 (БД-fallback `sha→branch` в `handle_ci_status`). Источник истины — гейт/Plane, не событие; идемпотентность (active-job guard + atomic-claim + grace); kill-switch `ORCH_RECONCILE_ENABLED`. `analysis` F-1 не трогает (человеческий гейт). F-1 также пропускает escalated (retry≥лимита) и Blocked/Needs-Input задачи (ORCH-060). Наблюдаемость — блок `reconcile` в `GET /queue`.
|
||||
- **Project Registry** (`src/projects.py`, ORCH-6) — Plane project id → repo + prefix; фильтрация вебхуков по проекту.
|
||||
- **Plane Sync** (`src/plane_sync.py`) — синхронизация статусов/комментариев в Plane.
|
||||
|
||||
@@ -118,6 +118,13 @@ helper `validated_revision` питает и штамп A, и `EXPECTED_REVISION`
|
||||
`age(updated_at) ≥ grace_for_stage(stage)` — read-only пред-оценка канонического QG;
|
||||
зелёный → `stage_engine.advance_stage(..., finished_agent=None)`; красный →
|
||||
тишина (спам нотификаций структурно невозможен). `analysis` не реконсилируется.
|
||||
**Skip escalated / Blocked / Needs-Input (ORCH-060):** ДО оценки гейта F-1
|
||||
пропускает (молча, без advance/нотификаций) задачи, которые ждут человека —
|
||||
(1) исчерпавшие лимит developer-ретраев (`developer_retry_count(task_id) >=
|
||||
MAX_DEVELOPER_RETRIES`, детерминированно, без сети — закрывает bounce-петлю
|
||||
ET-013) и (2) в явном Plane-статусе **Blocked** / **Needs Input** (Вариант A —
|
||||
запрос Plane API, без миграции БД; never-raise → консервативный skip). Гард
|
||||
retry-count проверяется первым (дёшево, локальный SQL).
|
||||
- **F-2 plane-side:** опрос Plane API per-project → `handle_status_start` /
|
||||
`handle_verdict` из `webhooks/plane.py` (логика не дублируется).
|
||||
- **F-3:** усиление `sha→branch` в `handle_ci_status` (БД-fallback по единственной
|
||||
@@ -194,4 +201,4 @@ never-raise на единицу работы; тишина при синхрон
|
||||
Схема БД, потоки данных, resilience-слой, детали Dockerfile — [internals.md](internals.md).
|
||||
|
||||
---
|
||||
*Актуально на 2026-06-07. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. Статусы доработок: ORCH-036 (исполняемый самодеплой `deploy`, adr-0007) — реализовано; ORCH-043 (merge-gate, adr-0006) — design, ветка feature/ORCH-043; ORCH-053 (reconciler, adr-0007, src/reconciler.py) — реализовано; ORCH-058 (провенанс staging-образа: check_staging_image_fresh + staging_check свежего образа + хук-guard, adr-0008) — реализовано в ветке feature/ORCH-058 (обновлять также при изменении src/image_freshness.py, scripts/orchestrator-deploy-hook.sh, Dockerfile).*
|
||||
*Актуально на 2026-06-07. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. Статусы доработок: ORCH-036 (исполняемый самодеплой `deploy`, adr-0007) — реализовано; ORCH-043 (merge-gate, adr-0006) — design, ветка feature/ORCH-043; ORCH-053 (reconciler, adr-0007, src/reconciler.py) — реализовано; ORCH-060 (F-1 skip escalated/Blocked/Needs-Input, `docs/work-items/ORCH-060/06-adr/ADR-001`) — architecture, ветка feature/ORCH-060; ORCH-058 (провенанс staging-образа: check_staging_image_fresh + staging_check свежего образа + хук-guard, adr-0008) — реализовано в ветке feature/ORCH-058 (обновлять также при изменении src/image_freshness.py, scripts/orchestrator-deploy-hook.sh, Dockerfile).*
|
||||
|
||||
@@ -61,6 +61,14 @@ grace + `max_concurrency=1`); never-raise на единицу работы; ти
|
||||
(`reconcile_plane_enabled` гасит только F-2); reconciler не рестартит/не роняет
|
||||
прод-контейнер. БД-схема и реестры (`STAGE_TRANSITIONS`/`QG_CHECKS`) не меняются.
|
||||
|
||||
## Уточнения
|
||||
- **ORCH-060** (`docs/work-items/ORCH-060/06-adr/ADR-001-reconciler-skip-escalated.md`):
|
||||
F-1 (`_reconcile_gate_task`) приобретает два пред-гарда ДО оценки гейта —
|
||||
пропускает escalated (`developer_retry_count ≥ MAX_DEVELOPER_RETRIES`,
|
||||
детерминированно) и Blocked/Needs-Input (Вариант A, Plane API, без миграции)
|
||||
задачи. Инварианты adr-0007 сохранены (схема/реестры не меняются, never-raise,
|
||||
тишина при пропуске).
|
||||
|
||||
## Связи
|
||||
adr-0002 (очередь / `available_at`, single-process-singleton), adr-0003 (условный
|
||||
гейт — образец условности/флагов раската), adr-0006 (merge-gate как под-гейт ребра
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
# ADR-001: Reconciler (F-1) пропускает escalated / Blocked / Needs-Input задачи
|
||||
|
||||
- **Статус:** Accepted
|
||||
- **Дата:** 2026-06-07
|
||||
- **Задача:** ORCH-060
|
||||
- **Стадия:** architecture
|
||||
- **Связано:** adr-0007 (reconciler, ORCH-053) — уточняет контракт F-1;
|
||||
ORCH-046 (retry-счётчик), ORCH-047 (BLOCKED-вердикт)
|
||||
|
||||
## Контекст
|
||||
|
||||
ORCH-053 ввёл F-1 (`Reconciler._reconcile_gate_task`): для каждой не-терминальной
|
||||
задачи без активного job и старше grace делается read-only пред-оценка
|
||||
канонического QG; зелёный → `advance_if_gate_passed` →
|
||||
`advance_stage(..., finished_agent=None)`.
|
||||
|
||||
**Дефект (инцидент ET-013, 06–07.06.2026).** Задача, исчерпавшая лимит
|
||||
developer-ретраев (`_developer_retry_count(task_id) >= MAX_DEVELOPER_RETRIES = 3`),
|
||||
**escalated** в обработчиках `gitea.py` (`:280` CI-failure, `:371` review
|
||||
REQUEST_CHANGES) выполняет ТОЛЬКО `notify_error(...)`:
|
||||
|
||||
- стадия НЕ меняется (остаётся `development`);
|
||||
- терминального маркера в БД нет (нет столбца статуса в `tasks`);
|
||||
- активного job нет.
|
||||
|
||||
Для F-1 такая задача **неотличима** от «застрявшей из-за потерянного webhook».
|
||||
Если CI зелёный (типовой кейс: dev починил CI, но reviewer слал REQUEST_CHANGES
|
||||
до лимита), каждые `reconcile_interval_s` (120с) F-1 видит зелёный `check_ci_green`
|
||||
и разблокирует `development → review` → reviewer снова REQUEST_CHANGES → откат →
|
||||
снова эскалация (стадия не меняется) → следующий тик снова разблокирует.
|
||||
**Бесконечный цикл:** ET-013 разблокирована 10 раз за ночь, лишние запуски агентов
|
||||
(токены/деньги), спам в Telegram, паразитная нагрузка общего self-hosting-инстанса.
|
||||
|
||||
Симметричный риск: задачу, которую человек явно перевёл в Plane-статус **Blocked**
|
||||
/ **Needs Input** (ручной гейт), sweeper не должен авторазблокировать до
|
||||
вмешательства человека.
|
||||
|
||||
## Решение
|
||||
|
||||
В `_reconcile_gate_task` ПОСЛЕ существующих гардов (`stage=='analysis'` carve-out,
|
||||
`get_qg_for_stage is None`, `has_active_job_for_task`, grace) и ДО
|
||||
`advance_if_gate_passed` добавляются два пред-гарда. Любой срабатывает → ранний
|
||||
`return`: задача пропущена, гейт НЕ оценивается, `unblocked_total` не растёт,
|
||||
нотификаций нет.
|
||||
|
||||
### Гард 1 — escalated по ретраям (детерминированный, без сети) — **обязателен**
|
||||
|
||||
```python
|
||||
# ORCH-060: escalated tasks (max developer retries reached) are terminal —
|
||||
# they wait for a human, not the sweeper. Deterministic, no network.
|
||||
if developer_retry_count(task_id) >= MAX_DEVELOPER_RETRIES:
|
||||
return
|
||||
```
|
||||
|
||||
- Источник истины по retry — `agent_runs` (как у `_developer_retry_count`):
|
||||
`SELECT COUNT(*) FROM agent_runs WHERE task_id=? AND agent='developer'`.
|
||||
- `MAX_DEVELOPER_RETRIES` импортируется из `stage_engine` — **не хардкодить `3`**
|
||||
(AC-11).
|
||||
- Граница `>=` (на лимите — skip, на `лимит−1` — advance; AC-4).
|
||||
|
||||
**Промоут хелпера.** `stage_engine._developer_retry_count` повышается до публичного
|
||||
`developer_retry_count` (приватное имя сохраняется как алиас для существующих
|
||||
внутренних call-sites). Reconciler импортирует
|
||||
`MAX_DEVELOPER_RETRIES, developer_retry_count` из `stage_engine`. SQL **не
|
||||
дублируется** в `db.py` — единый источник истины по подсчёту ретраев.
|
||||
|
||||
### Гард 2 — явный человеческий Plane-статус (Blocked / Needs Input) — **Вариант A**
|
||||
|
||||
```python
|
||||
# ORCH-060: respect an explicit human gate (Blocked / Needs Input).
|
||||
if self._is_blocked_or_needs_input(task):
|
||||
return
|
||||
```
|
||||
|
||||
Механика — **Вариант A (запрос Plane API, без миграции схемы):**
|
||||
|
||||
1. Новый never-raise хелпер `plane_sync.fetch_issue_state(issue_id, project_id)
|
||||
-> str | None` — GET issue-detail (тот же endpoint/headers, что
|
||||
`fetch_issue_sequence_id` / `fetch_issue_fields`), возвращает uuid текущего
|
||||
`state`; любая ошибка/отсутствие поля → `None`.
|
||||
2. `Reconciler._is_blocked_or_needs_input(task)`:
|
||||
- `repo → ProjectConfig` через `projects.get_project_by_repo(task['repo'])`;
|
||||
- `pid = proj.plane_project_id`; `states = get_project_states(pid)` (кэш per-project);
|
||||
- `cur = fetch_issue_state(task['plane_id' | 'plane_issue_id'], pid)`;
|
||||
- вернуть `cur in {states['blocked'], states['needs_input']}`.
|
||||
- **Never-raise → консервативный фоллбэк:** любая ошибка/`None`/нерезолвленный
|
||||
проект → трактуем как «возможно заблокировано» → возвращаем `True` (skip).
|
||||
Не-разблокировать безопаснее, чем разблокировать (AC-10).
|
||||
|
||||
**Порядок гардов:** Гард 1 (локальный SQL, дёшево) — ПЕРВЫМ; Гард 2 (сеть) —
|
||||
вторым. Для зафиксированного инцидента (ET-013 = escalated) Гард 1 закрывает кейс
|
||||
**без единого сетевого вызова**.
|
||||
|
||||
### Что НЕ меняется (инварианты ORCH-053)
|
||||
|
||||
- Схема БД — **без миграции** (Вариант A). `STAGE_TRANSITIONS` / `QG_CHECKS` —
|
||||
без изменений. Гард — ВНЕ гейта: решает, ЗАПУСКАТЬ ли пред-оценку, а не меняет
|
||||
вердикт.
|
||||
- Never-raise на единицу работы (`reconcile_gate_once` per-task `try/except`
|
||||
сохраняется; новая логика не бросает наружу).
|
||||
- `analysis` carve-out, kill-switch'и (`reconcile_enabled`,
|
||||
`reconcile_plane_enabled`) — как прежде.
|
||||
- F-2 по существу не меняется: Blocked/Needs Input не входят в
|
||||
`{in_progress, approved, rejected}` → не доигрываются (фиксируется
|
||||
регресс-тестом AC-9).
|
||||
|
||||
### Опционально (вне scope AC, рекомендации)
|
||||
|
||||
- Под-флаг `reconcile_skip_blocked_enabled` (default `true`) для независимого
|
||||
отключения только Гарда 2 (сетевого), по аналогии с `reconcile_plane_enabled`.
|
||||
Гард 1 (локальный, безопасный) — всегда активен.
|
||||
- Best-effort счётчик `skipped_escalated` в снимке `GET /queue` (наблюдаемость).
|
||||
|
||||
## Альтернативы
|
||||
|
||||
- **Вариант B — локальный терминальный маркер в БД** (`tasks.blocked` /
|
||||
`tasks.reconcile_skip`, идемпотентный ALTER, выставляется в `set_issue_blocked`
|
||||
/ `set_issue_needs_input` и точках эскалации `gitea.py`). **Отклонён как
|
||||
primary:**
|
||||
- нарушает инвариант ORCH-053 «схема reconciler не меняется» (миграция на живой
|
||||
прод-БД = self-hosting-риск);
|
||||
- затрагивает больше точек записи (4+: две эскалации gitea + два set_issue_*) —
|
||||
выше риск рассинхрона маркера и факта;
|
||||
- для зафиксированного инцидента **не нужен**: Гард 1 (retry-count) закрывает
|
||||
ET-013 детерминированно и без сети.
|
||||
Вариант B остаётся задокументированным будущим упрочнением, если Plane-coupling
|
||||
Гарда 2 окажется болезненным (см. Последствия).
|
||||
- **Подавление в самом `advance_stage` / новый терминальный вердикт гейта** —
|
||||
отклонён: меняет общий критический путь; ORCH-053 уже постановил «не вызывать
|
||||
advance на красном», тот же принцип «не вызывать advance на escalated».
|
||||
- **Гард только по retry (без Гарда 2)** — недостаточно: не покрывает ручной
|
||||
Blocked при retry<лимита; AC-5/AC-6 требуют пропуск.
|
||||
|
||||
## Последствия
|
||||
|
||||
- **Плюсы:** ET-013-петля устранена детерминированно; 0 фантомных разблокировок,
|
||||
0 лишних запусков агентов, 0 спама по escalated-задачам; ручной Blocked/Needs
|
||||
Input уважается; без миграции БД и без изменения реестров → минимальный
|
||||
self-hosting-риск; единый источник истины по retry (промоут хелпера).
|
||||
- **Минусы / плата:**
|
||||
- Гард 2 вводит **per-candidate сетевой вызов** Plane на тике. Митигировано:
|
||||
кандидатов после grace+no-active-job немного; `get_project_states` кэшируется;
|
||||
Гард 1 отсекает escalated до сети.
|
||||
- **Plane-coupling F-1:** при недоступности Plane Гард 2 фоллбэкает в skip →
|
||||
F-1 во время Plane-outage не доигрывает кандидатов с retry<лимита (консерва-
|
||||
тивно «не навреди»). Приемлемо: outage редок/транзиентен; escalated-кейс
|
||||
(Гард 1) от Plane не зависит и продолжает работать; альтернатива
|
||||
(proceed-on-error) рискует вернуть bounce при реальном Blocked. Под-флаг
|
||||
`reconcile_skip_blocked_enabled` даёт ручной обход на время инцидента.
|
||||
- **Self-hosting:** изменение — чистая логика sweeper'а; прод-контейнер не
|
||||
рестартится/не роняется; деплой через staging (8501) по канону.
|
||||
|
||||
## Связи
|
||||
|
||||
- **adr-0007 (reconciler, ORCH-053)** — данный ADR уточняет контракт F-1
|
||||
(`_reconcile_gate_task` приобретает два пред-гарда; инварианты сохранены).
|
||||
- **adr-0003 (условный staging-гейт)** — образец never-raise + флага раската
|
||||
(Гард 2 / `reconcile_skip_blocked_enabled`).
|
||||
- **adr-0001 (реестр проектов)** — `get_project_by_repo` → `plane_project_id`
|
||||
для резолва per-project статусов (Вариант A).
|
||||
- ORCH-046 (retry-счётчик `agent_runs`), ORCH-047 (BLOCKED-вердикт).
|
||||
20
docs/work-items/ORCH-060/10-tech-risks.md
Normal file
20
docs/work-items/ORCH-060/10-tech-risks.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Технические риски: ORCH-060
|
||||
|
||||
Work Item ID: ORCH-060
|
||||
Стадия: architecture
|
||||
|
||||
| # | Риск | Вероятность | Влияние | Митигация |
|
||||
|---|------|-------------|---------|-----------|
|
||||
| R-1 | **Plane-coupling F-1.** Гард 2 (Вариант A) делает сетевой вызов на тике; при недоступности Plane все кандидаты с retry<лимита фоллбэкают в skip → F-1 временно не доигрывает. | Низкая (outage редок) | Среднее | Консервативный фоллбэк («не навреди»); escalated-кейс закрыт Гардом 1 без сети; под-флаг `reconcile_skip_blocked_enabled` для ручного обхода; `get_project_states` кэшируется. |
|
||||
| R-2 | **Стоимость поллинга.** Per-candidate GET issue-detail каждые 120с при большом числе stuck-задач. | Низкая | Низкое | Кандидатов после grace+no-active-job мало; Гард 1 (локальный SQL) отсекает escalated до сети; вызов только для переживших Гард 1. |
|
||||
| R-3 | **Промоут хелпера ломает call-sites.** `_developer_retry_count → developer_retry_count`. | Низкая | Среднее | Сохранить приватный алиас `_developer_retry_count = developer_retry_count`; grep всех вызовов перед мержем; покрыто существующими тестами stage_engine. |
|
||||
| R-4 | **Неверный фоллбэк-знак Гарда 2.** Если ошибку трактовать как «не заблокировано» → возврат ET-013-bounce при реальном Blocked. | Средняя (ошибка реализации) | Высокое | ADR явно фиксирует: ошибка/None/нерезолвленный проект → `True` (skip); AC-10 проверяет never-raise+skip. |
|
||||
| R-5 | **Резолв plane-issue-id из task.** В `tasks` два поля (`plane_id` / `plane_issue_id`); неверный выбор → пустой запрос. | Низкая | Низкое | Использовать тот же приоритет, что `get_task_by_plane_id` (оба поля); пустой id → фоллбэк skip. |
|
||||
| R-6 | **Регресс happy-path.** Слишком широкий гард пропустит честно-застрявшие задачи (retry<лимита, не Blocked). | Низкая | Высокое | AC-3/AC-4 (граница ровно на лимите); регресс существующих тестов AC-13. |
|
||||
| R-7 | **Self-hosting деплой.** Изменение работающего в проде sweeper'а. | Низкая | Высокое | Чистая логика, без миграции/рестарт-контрактов; обязательный прогон через staging (8501) перед прод-деплоем; kill-switch `reconcile_enabled`. |
|
||||
|
||||
## Вывод
|
||||
Все риски — низкие/средние по вероятности и митигируемы в рамках выбранной
|
||||
архитектуры (Вариант A, без миграции). Критичен корректный знак never-raise
|
||||
фоллбэка Гарда 2 (R-4) — выделен в AC-10. Схема БД и реестры не меняются →
|
||||
self-hosting-риск минимален.
|
||||
Reference in New Issue
Block a user