From df98b317309c30e0a326d2bf615b8fa5b680707b Mon Sep 17 00:00:00 2001 From: claude-bot Date: Tue, 9 Jun 2026 23:11:40 +0300 Subject: [PATCH] architect(ET): auto-commit from architect run_id=519 --- docs/architecture/README.md | 33 +++ ...rminal-window-aware-deploy-status-guard.md | 97 ++++++++ ...rminal-window-aware-deploy-status-guard.md | 234 ++++++++++++++++++ docs/work-items/ORCH-094/10-tech-risks.md | 91 +++++++ 4 files changed, 455 insertions(+) create mode 100644 docs/architecture/adr/adr-0028-terminal-window-aware-deploy-status-guard.md create mode 100644 docs/work-items/ORCH-094/06-adr/ADR-001-terminal-window-aware-deploy-status-guard.md create mode 100644 docs/work-items/ORCH-094/10-tech-risks.md diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 9d953f8..76b9dac 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -585,6 +585,39 @@ sentinel-state, `write_post_deploy_log`. Подробнее: [adr-0010](adr/adr-0010-post-deploy-monitor.md), детально — `docs/work-items/ORCH-021/06-adr/ADR-001-post-deploy-monitor.md`. +### Terminal-window-aware гард deploy-статусов: done-задача держит Done (ORCH-094 — design) +Терминальная (`done`) задача в Plane **не держала `Done`**: непрерывный флапп +`Awaiting Deploy ⟷ Monitoring after Deploy` (верифицировано на **ORCH-061**, task 47, done с 07.06 — +273 активности, само не затихает). Причина: три code-писателя deploy-фазовых статусов +(`stage_engine.py:404/1218/1316`) делегируют в тонкие сеттеры `plane_sync`, которые **БД-стадию не +читают** ⇒ терминал-слепы; любой повторный/стейл вызов под бот-токеном орка перезаписывает `Done` +обратно. Тонкость: `update_task_stage("done")` (стр. 369) пишет стадию **раньше** легитимного +`set_issue_monitoring` (стр. 404) ⇒ пост-деплой-окно ORCH-021 by-design индицируется поверх уже-`done` +задачи; наивный гард «stage==done → Done» затёр бы легитимный `Monitoring` (регресс). + +Решение — **единый terminal-window-aware гард на входе трёх deploy-фазовых сеттеров** (новый leaf +`src/deploy_status_guard.py`, never-raise, config-gated; образец `serial_gate`/`labels`/`cancel`). +- **Инвариант:** deploy-фазовый статус легитимен ⇔ задача **нетерминальна** ИЛИ (`done` И активно + пост-деплой-окно). `decide(work_item_id, target) → ALLOW | CONVERGE_DONE | SUPPRESS`: off / чужой + issue / не-self репо / нетерминал → ALLOW; `cancelled` → SUPPRESS; `done`+`monitoring`+`window_active` + → ALLOW; `done` иначе → CONVERGE_DONE (`set_issue_done`, идемпотентно); исключение → ALLOW+warning. +- **Окно** — новый `post_deploy.window_active(repo,wi)` = `has_marker(ARMED) and not has_marker(DONE)` + (restart-safe). **Перенос арм-блока перед terminal-sync** в `advance_stage` блок `next_stage=="done"` + ⇒ на стр. 404 `ARMED` уже есть ⇒ легитимный первый `Monitoring` проходит; re-drive после закрытия + окна сходится к `Done`. +- **Харднинг монитора:** страж `has_marker(...DONE)` (ранний return) + тик no-op при `cancelled` + мид-окно; тики привязаны к активному job'у (нет job → нет тика, нет статус-PATCH). +- **Наблюдаемость:** каждый вердикт логируется (`work_item`/`caller`/`target`/`db_stage`/ + `window_active`/вердикт); подавление — явно. +- **Инварианты:** `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict ключи/схема БД — НЕ тронуты; + `main`/force-push/прод-контейнер/detached-деплой — НЕ тронуты; рабочий self-deploy-цикл 1:1; не-self + репо инертны. Kill-switch `ORCH_DEPLOY_STATUS_GUARD_ENABLED` (→ 1:1), область + `ORCH_DEPLOY_STATUS_GUARD_REPOS` (пусто → self-hosting). Ограничение: внешняя Plane-automation (если + таков актор) закрывается буфером сходимости, а не code-фиксом — локализация актора в задаче (BR-7). + +Подробнее: [adr-0028](adr/adr-0028-terminal-window-aware-deploy-status-guard.md), детально — +`docs/work-items/ORCH-094/06-adr/ADR-001-terminal-window-aware-deploy-status-guard.md`. + ### Свежесть артефакта BUILD-ONCE: провенанс staging-образа (ORCH-058 — реализовано) BUILD-ONCE retag (ORCH-36) промоутит `SOURCE_IMAGE=orchestrator-orchestrator-staging` в прод **без rebuild**, полагаясь на «staging-образ свеж и провалидирован». Этой гарантии нет: diff --git a/docs/architecture/adr/adr-0028-terminal-window-aware-deploy-status-guard.md b/docs/architecture/adr/adr-0028-terminal-window-aware-deploy-status-guard.md new file mode 100644 index 0000000..b46d0a2 --- /dev/null +++ b/docs/architecture/adr/adr-0028-terminal-window-aware-deploy-status-guard.md @@ -0,0 +1,97 @@ +--- +work_item: ORCH-094 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-09 +model_used: claude-opus-4-8 +--- + +# adr-0028: Terminal-window-aware гард выставления deploy-фазовых статусов Plane + +Сквозной (cross-cutting) ADR. **Амендмент** к [adr-0010](adr-0010-post-deploy-monitor.md) +(post-deploy monitor, ORCH-021) и Plane-статусной модели (ORCH-066): вводит инвариант +«deploy-фазовые Plane-статусы — terminal-window-aware» поверх общих сеттеров `plane_sync` и +переупорядочивает блок `next_stage == "done"` в `advance_stage`. Детальное решение задачи — +`docs/work-items/ORCH-094/06-adr/ADR-001-terminal-window-aware-deploy-status-guard.md`. + +> Регистрируется как сквозной, т.к. правит **общие** сеттеры `set_issue_awaiting_deploy`/ +> `set_issue_deploying`/`set_issue_monitoring` (используются системно) и трогает маркированный блок с +> `ORCH-021`/`ORCH-066` (`docs/_standards/TRACEABILITY.md`). + +## Статус +Proposed + +## Контекст + +Терминальная (`done`) задача в Plane **не держит `Done`**: непрерывный флапп +`Awaiting Deploy ⟷ Monitoring after Deploy` (верифицировано живьём на **ORCH-061**, task 47, done с +07.06 — 273 активности, само не затихает). Установлено по коду/логам/БД прода: + +- Три code-писателя deploy-фазовых статусов (`src/stage_engine.py:404/1218/1316`) делегируют в тонкие + сеттеры `src/plane_sync.py`, которые **БД-стадию не читают** ⇒ терминал-слепы: любой повторный вызов + перезаписывает `Done` обратно на промежуточный статус. +- **Ordering:** `update_task_stage("done")` (`stage_engine.py:369`) пишет `tasks.stage='done'` + **раньше** легитимного `set_issue_monitoring` (стр. 404) ⇒ пост-деплой-окно ORCH-021 — by-design + индикация поверх уже-`done` задачи. Наивный гард «stage==done → Done» ⇒ регресс легитимного окна. +- Актор всех 273 переходов — бот-токен орка (`daf4d3f4-…`), не привязан к активной task/job; в БД нет + активного post-deploy-monitor для task 47 (окно 15 мин закрыто). Реконсилятор F-1 пропускает + `done`/`cancelled`, F-2 опрашивает только `[to_analyse, approved, rejected]` ⇒ механизма привести + застрявшую на deploy-статусе done-задачу к `Done` нет. + +## Решение + +**Единый terminal-window-aware гард на низком чокпоинте** — на входе трёх deploy-фазовых сеттеров +`plane_sync`. Чистую логику держит **новый leaf-модуль `src/deploy_status_guard.py`** (never-raise, +config-gated; образец `serial_gate.py`/`labels.py`/`cancel.py`); сеттеры исполняют вердикт. + +- **Инвариант легитимности:** deploy-фазовый статус легитимен ⇔ задача **нетерминальна** ИЛИ + (`done` **И** активно пост-деплой-окно). Иначе — идемпотентное схождение к `Done`. + `decide(work_item_id, target) -> ALLOW | CONVERGE_DONE | SUPPRESS`: + kill-switch off / чужой issue / не-self репо / нетерминал → **ALLOW**; `cancelled` → **SUPPRESS**; + `done` + `target==monitoring` + `window_active` → **ALLOW**; `done` иначе → **CONVERGE_DONE** + (`set_issue_done`, идемпотентно); любое исключение → **ALLOW** + warning (never-raise). +- **Новый helper** `post_deploy.window_active(repo, wi)` = `has_marker(ARMED) and not + has_marker(DONE)` (restart-safe). +- **Перенос арм-блока** (`post_deploy.arm_monitor`) **перед** terminal-sync в блоке + `next_stage == "done"`: на стр. 404 `ARMED` уже записан ⇒ `window_active==True` ⇒ легитимный первый + `Monitoring` проходит; re-drive после закрытия окна сходится к `Done`. +- **Харднинг монитора:** идемпотентный страж `has_marker(...DONE)` (ранний return без PATCH/реэнкью) + + тик no-op при `cancelled` мид-окно; тики привязаны к активному job'у (нет job → нет тика). +- **Наблюдаемость:** каждый вердикт логируется (`work_item`/`caller`/`target`/`db_stage`/ + `window_active`/вердикт); подавление/схождение — явно. +- **Флаги** (`config.py`): `deploy_status_guard_enabled=True` + (`ORCH_DEPLOY_STATUS_GUARD_ENABLED`, kill-switch → 1:1) + `deploy_status_guard_repos=""` + (`ORCH_DEPLOY_STATUS_GUARD_REPOS`, пусто → self-hosting only) с локальным `applies(repo)`. + +## Альтернативы + +- **Гард в caller'ах `stage_engine`** — отвергнуто: не ловит неизвестный/стейл путь под бот-токеном, + размазывает инвариант. +- **Наивный «stage==done → Done» без предиката окна** — отвергнуто: регресс легитимного `Monitoring`. +- **Bypass-флаг на доверенном вызове 404** — отвергнуто в пользу переноса арм-блока (один предикат). +- **Активная сходимость в реконсиляторе F-2** — отвергнуто как основной механизм (лишний polling, + правка маркированного F-2); гард на сеттере гасит непрерывный флапп. + +## Последствия + +- Терминальная задача стабильно держит `Done`; маятник гаснет за один цикл независимо от актора. +- Легитимный пост-деплой `Monitoring` и рабочий self-deploy-цикл — 1:1 (предикат окна + перенос арм). +- `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict ключи / схема БД — **не тронуты**. +- `main`/force-push/прод-контейнер/detached-деплой — не тронуты; не-self репо инертны. +- Ограничение: если актор флаппа — внешняя Plane-automation (вне кода орка), гард — буфер на стороне + орка; локализация (FR-1) и итог документируются (BR-7). +- **Откат:** `ORCH_DEPLOY_STATUS_GUARD_ENABLED=false` → поведение 1:1; полный — revert ветки. + +## Связи + +- [adr-0010](adr-0010-post-deploy-monitor.md) (ORCH-021 — пост-деплой-окно, sentinel `armed`/`done`, + арм-блок) — амендмент: окно становится предикатом легитимности `Monitoring`. +- ORCH-066 (Plane-статусная модель — слой B индикации; `deploy→done` self ⇒ `Monitoring`) — инвариант + сохранён. +- [adr-0026](adr-0026-stop-cancel-task.md) (ORCH-090 — терминал `cancelled`) — гард не штампует + deploy-статус поверх `cancelled`. +- ORCH-068/086 (терминал-скип реконсилятора) — этот ADR распространяет идею терминал-aware на + выставление deploy-статусов. +- Детально: `docs/work-items/ORCH-094/06-adr/ADR-001-terminal-window-aware-deploy-status-guard.md`. + diff --git a/docs/work-items/ORCH-094/06-adr/ADR-001-terminal-window-aware-deploy-status-guard.md b/docs/work-items/ORCH-094/06-adr/ADR-001-terminal-window-aware-deploy-status-guard.md new file mode 100644 index 0000000..2c3d4c5 --- /dev/null +++ b/docs/work-items/ORCH-094/06-adr/ADR-001-terminal-window-aware-deploy-status-guard.md @@ -0,0 +1,234 @@ +--- +work_item: ORCH-094 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-09 +model_used: claude-opus-4-8 +--- + +# ADR-001: Terminal-window-aware гард выставления deploy-фазовых статусов Plane + +Work Item: **ORCH-094** — терминальная (done) задача флаппит deploy-статусы в Plane +(`Awaiting Deploy ⟷ Monitoring after Deploy`), не держит `Done`. +Стадия: **architecture** +Сквозная регистрация: **`docs/architecture/adr/adr-0028-terminal-window-aware-deploy-status-guard.md`** +(кросс-каттинг: правит общие сеттеры `plane_sync` + переупорядочивает маркированный блок +`next_stage == "done"` ORCH-021/066). + +## Статус +Proposed + +## Контекст + +Сверено по коду ветки `feature/ORCH-094-…`: + +- **Три code-писателя deploy-фазовых статусов** — все в `src/stage_engine.py`, все вызывают + тонкие сеттеры `src/plane_sync.py`, которые делегируют в общий `_set_issue_state_direct` + (PATCH issue.state; never-raise; **БД-стадию не читает**): + - `set_issue_awaiting_deploy` (Phase A, `stage_engine.py:1218`), + - `set_issue_deploying` (Phase B, `stage_engine.py:1316`), + - `set_issue_monitoring` (terminal-sync `deploy → done` для self-hosting, `stage_engine.py:404`). + - `set_issue_done` (`plane_sync.py:913`) — **терминальная цель**, отдельно. +- **Критический факт ordering'а:** в `advance_stage` строка **369** `update_task_stage(task_id, "done")` + пишет `tasks.stage='done'` **РАНЬШЕ**, чем строка **404** `set_issue_monitoring(...)`. То есть в + момент **легитимного** первого выставления `Monitoring after Deploy` задача в БД **уже `done`**. + Пост-деплой-окно ORCH-021 — это by-design индикация поверх уже-терминальной (`done`) задачи + («ответственность ЗА `done`»). ⇒ **наивный гард «stage==done → редирект на Done» подавил бы + легитимный `Monitoring` → регресс AC-4.** +- **Арм пост-деплой-монитора** (`stage_engine.py:431` → `post_deploy.arm_monitor`) выполняется + **ПОСЛЕ** строки 404. Sentinel `ARMED` пишется в `arm_monitor`; окно закрывается sentinel'ом + `DONE` (`post_deploy.mark_done`); идемпотентный страж `has_marker(...DONE)` в + `run_post_deploy_monitor` (~1729). +- **Симптом (верифицирован живьём на ORCH-061, task 47, done с 07.06):** Plane не держит `Done` — + непрерывный флапп `Awaiting ⟷ Monitoring` парами каждые ~сек, 273 активности, само не затихает. + В БД **нет активного post-deploy-monitor** для task 47 (окно 15 мин давно закрыто); реконсилятор + F-1 пропускает `done`/`cancelled`, F-2 опрашивает только `[to_analyse, approved, rejected]` — + механизма «привести застрявшую на deploy-статусе done-задачу обратно к Done» нет. Актор всех 273 + переходов — бот-токен орка (`daf4d3f4-…`), т.е. PATCH-и шлёт **что-то под токеном орка**, не + привязанное к активной task/job. Точный актор подлежит инструментальной локализации (FR-1, + developer); фикс должен быть **буфером, гасящим маятник на стороне орка независимо от актора**. + +**Почему «как есть» не годится:** сеттеры deploy-статусов терминал-слепы — любой повторный вызов +(стейл-job, двойной webhook, неизвестный внутренний путь под бот-токеном) перезаписывает `Done` +обратно на промежуточный deploy-статус, и наоборот, бесконечно. Нет ни идемпотентного схождения к +`Done` для терминальной задачи, ни наблюдаемости «кто/почему» ставит статус. + +## Решение + +### Сводка + +Вводим **единый terminal-window-aware гард на самом низком чокпоинте** — на входе трёх +deploy-фазовых сеттеров `plane_sync`. Решение принимает **новый leaf-модуль +`src/deploy_status_guard.py`** (чистая, never-raise, config-gated логика; по образцу +`serial_gate.py`/`labels.py`/`cancel.py`), сеттеры лишь исполняют вердикт. Ключевой инвариант: +**deploy-фазовый статус легитимен ⇔ задача нетерминальна ИЛИ (`done` И активно пост-деплой-окно)**; +иначе — идемпотентное схождение к `Done`. Чтобы легитимный первый `Monitoring` на строке 404 +проходил, **арм-блок переносится перед terminal-sync-блоком** (предикат «окно активно» становится +истинным до выставления `Monitoring`). Всё под kill-switch, аддитивно, в зоне self-hosting; реестры +конвейера не тронуты. + +### D1 — Где гард: единый чокпоинт в deploy-фазовых сеттерах `plane_sync` + +Гард ставится на входе **`set_issue_awaiting_deploy` / `set_issue_deploying` / `set_issue_monitoring`** +(а НЕ в caller'ах `stage_engine`). Это перехватывает **любой** путь к этим статусам — известные +(stage_engine), будущие и **неизвестный актор под бот-токеном** (если он проходит через код орка) — +одной точкой. `set_issue_done` **не гардится** (это цель схождения). Привязка: **FR-2, BR-1, BR-2**. + +> Альтернатива «гард в caller'ах stage_engine» отвергнута: не ловит неизвестный/стейл путь, который +> и есть подозреваемый источник 061-флаппа; размазывает инвариант по трём местам. См. «Альтернативы». + +### D2 — Предикат легитимности: терминал **И окно**, не только стадия + +Вердикт `deploy_status_guard.decide(work_item_id, target_status) -> ALLOW | CONVERGE_DONE | SUPPRESS`: + +1. `not settings.deploy_status_guard_enabled` → **ALLOW** (kill-switch off ⇒ поведение 1:1). +2. `task = `; `task is None` → **ALLOW** (чужой/не наш issue — не вмешиваемся). +3. `not deploy_status_guard.applies(task.repo)` → **ALLOW** (не-self репо ⇒ нулевая регрессия; для них + `Monitoring`/`Awaiting`/`Deploying` и так не выставляются — terminal-sync идёт сразу в `Done`). +4. `stage = task.stage`; `stage NOT IN ('done','cancelled')` → **ALLOW** (нетерминальная задача — + легитимный рабочий deploy-цикл; **AC-4**). +5. `stage == 'cancelled'` → **SUPPRESS** (не штампуем deploy-статус поверх терминала `cancelled`; + cancel-flow ORCH-090 уже привёл Plane к своему терминалу — гард лишь не затирает его). +6. `stage == 'done'`: + - `target == 'monitoring'` **И** `post_deploy.window_active(repo, work_item_id)` → **ALLOW** + (легитимное пост-деплой-окно — `Monitoring` корректен; **AC-4**); + - иначе → **CONVERGE_DONE** (для `done` `Awaiting`/`Deploying` всегда спуриозны — Phase A/B + случаются строго **до** `deploy → done`; и `Monitoring` при закрытом/неарм'ленном окне — + спуриозен, как 061). +7. **Любое исключение / невозможность определить стадию** → **ALLOW** + `logger.warning` + (never-raise, fail-safe к прежнему поведению; **NFR-1**). БД-чтение локальное (SQLite) и надёжное — + в штатном случае стадия читается, маятник не возникает. + +Сеттер исполняет вердикт: `ALLOW` → штатный PATCH; `CONVERGE_DONE` → `set_issue_done(work_item_id)` +(идемпотентно — уже-`Done` ⇒ no-op PATCH-эквивалент); `SUPPRESS` → ничего не патчим. Привязка: +**FR-2, BR-1, BR-2, AC-1, AC-2, AC-4**. + +**Новый helper** `post_deploy.window_active(repo, wi) -> bool` = `has_marker(ARMED) and not +has_marker(DONE)` (never-raise; restart-safe — sentinel'ы на диске переживают рестарт; **NFR-4**). + +### D3 — Перенос арм-блока перед terminal-sync (чтобы D2 пропускал легитимный первый `Monitoring`) + +В `advance_stage`, внутри ветки `next_stage == "done"`, **арм-блок** (`post_deploy.arm_monitor`, +сейчас стр. 431) перемещается **выше** terminal-sync-блока (`set_issue_monitoring`, стр. 404). После +переноса в момент строки 404: `ARMED` уже записан, `DONE` отсутствует ⇒ `window_active==True` ⇒ +вердикт **ALLOW** ⇒ легитимный `Monitoring` проходит как раньше. Re-drive `deploy → done` **после** +закрытия окна (`DONE` присутствует) ⇒ `window_active==False` ⇒ **CONVERGE_DONE** (не воскрешает +`Monitoring`). + +Перенос безопасен: `arm_monitor` лишь пишет sentinel + ставит отложенный job — не зависит ни от +Plane-статуса, ни от merge-lease (release остаётся после terminal-sync). Инварианты ORCH-021 +(идемпотентный арм по `ARMED`) и ORCH-066 (`deploy → done` для self ⇒ `Monitoring`, не `Done`) +сохранены. Привязка: **AC-4, BR-5**; маркеры `ORCH-021`/`ORCH-066` (прочитаны: `06-adr/ADR-001`, +`adr-0010`). + +> Альтернатива «bypass-флаг `force=True` на доверенном вызове 404 вместо переноса» отвергнута: плодит +> два определения «легитимности» и доверенный обход; перенос оставляет **один** предикат «окно активно». + +### D4 — Харднинг пост-деплой-монитора: нет «зомби»-тиков/PATCH после закрытия окна + +`run_post_deploy_monitor` (`stage_engine.py` ~1698): сохранить существующий идемпотентный страж +`has_marker(...DONE)` (~1729; первым — ранний `return` без PATCH/реэнкью). Аддитивно: тик +**no-op без PATCH и без перепостановки**, если задача стала терминальной аномально (`stage == +'cancelled'` мид-окно → закрыть окно `mark_done`, без статус-PATCH). Перепостановка тика остаётся +строго при `HEALTHY and ticks < budget` — тики **привязаны к активному job'у** (тик и есть job; нет +job → нет тика). После закрытия окна (`DONE`) или исчерпания бюджета — **0 последующих** статус-PATCH; +любой стейл-вызов `set_issue_monitoring` теперь добивается гардом D2 (`window_active==False` → +CONVERGE_DONE). `arm_monitor` уже идемпотентен по `ARMED` (повторный арм done-задачи → no-op). Привязка: +**FR-3, BR-3, BR-4, AC-3, NFR-4**. + +### D5 — Наблюдаемость «кто/почему» (FR-4) + +Каждый вердикт гарда логируется структурно одной записью: `work_item`, `caller` (короткая причина — +аддитивный BC-kwarg `reason: str | None = None` у трёх сеттеров; call-site передаёт напр. +`"advance:deploy->done"`/`"phase_a"`/`"phase_b"`/`"monitor-tick"`), `target_status`, `db_stage`, +`window_active`, итоговый вердикт (`ALLOW`/`CONVERGE_DONE`/`SUPPRESS`). Подавление/схождение +(`CONVERGE_DONE`/`SUPPRESS`) логируется **явно** («что подавили и почему»). Достаточно, чтобы по +логу однозначно атрибутировать будущий флапп. Привязка: **FR-4, BR-6, AC-5**. + +### D6 — Обратимость, скоуп, флаги (FR-5) + +`src/config.py` (по образцу ORCH-088/090): +- `deploy_status_guard_enabled: bool = True` — env `ORCH_DEPLOY_STATUS_GUARD_ENABLED` (kill-switch; + `False` → сеттеры терминал-слепы, поведение **1:1** прежнее). +- `deploy_status_guard_repos: str = ""` — env `ORCH_DEPLOY_STATUS_GUARD_REPOS` (CSV; **пусто → + self-hosting only**). `applies(repo)` (локальный, без сети) — единственная точка скоупа. + +Дефолт `enabled=True` + `repos=""` ⇒ активен только для self-hosting (`orchestrator`), где deploy-фазовые +статусы вообще выставляются; не-self репо (enduro-trails) гард не трогает (D2 шаг 3). Привязка: **NFR-3, +BR-5, FR-5, AC-4, AC-5**. + +### D7 — Что НЕ трогаем (инварианты) + +`STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict ключи +(`deploy_status:`/`staging_status:`/`security_status:`) — **байт-в-байт**. Схема БД — **без миграции** +(гард читает существующую `tasks.stage`; окно — существующие sentinel'ы `post_deploy.py`; привязка к +job — существующая таблица `jobs`). `main`/force-push/прод-контейнер/detached-деплой — **не трогаются**. +Рабочий критический путь self-deploy (Phase A→B→C, merge-gate, freeze-на-DEGRADED ORCH-088) — +сохранён 1:1. Реконсилятор F-1/F-2 — **без изменений** (гард на сеттере субсумирует «sync → Done»: +любой путь, дёрнувший deploy-сеттер для done-задачи, сходится к `Done`). Привязка: **NFR-2, NFR-5, AC-5**. + +### D8 — Лукап задачи по `work_item_id` (реализационная заметка для developer) + +Сеттеры принимают `work_item_id` (напр. `"ORCH-061"`). В `src/db.py` существующий +`get_task_by_plane_id` матчит `plane_id`/`plane_issue_id` (UUID-ы), **не** человекочитаемый +`work_item_id`. Developer добавляет минимальный **read-only** аксессор +`get_task_by_work_item_id(work_item_id)` (`SELECT * FROM tasks WHERE work_item_id = ?`; живой ряд +матчит точно — тумбстоны ORCH-090 имеют суффикс `#cancelled-`), **без изменения схемы**. Один +локальный SELECT отдаёт и `repo`, и `stage` для D2. + +## Альтернативы + +- **Гард в caller'ах `stage_engine` (а не в сеттерах)** — отвергнуто: не ловит неизвестный/стейл + актор под бот-токеном (вероятный источник 061-флаппа), размазывает инвариант по трём врезкам, + слабее как буфер BR-2 «сходимость из любого пути». +- **Наивный гард «stage==done → редирект на Done» (без предиката окна)** — отвергнуто: подавляет + легитимный пост-деплой `Monitoring` (он by-design поверх уже-`done` задачи, стр. 369 < 404) ⇒ + прямой регресс **AC-4**. +- **Bypass-флаг `force=True` на доверенном вызове 404** (вместо переноса арм-блока) — отвергнуто: + два определения легитимности + доверенный обход; перенос даёт один предикат «окно активно». +- **Активная сходимость в реконсиляторе (F-2 опрашивает Awaiting/Monitoring → set_issue_done)** — + отвергнуто как **основной** механизм (лишний Plane-polling, правка маркированного F-2). Гард на + сеттере уже гасит непрерывный флапп (каждый вызов актора сходится к `Done` за один цикл). Возможен + как **необязательный** follow-up для разовой зачистки quiescent-застрявшего статуса (вне scope — + такой кейс чинится разовым ручным sync; наблюдаемый дефект — непрерывный флапп, который буфер + покрывает). +- **Колонка-маркер в `tasks` для состояния окна** — отвергнуто: миграция на проде; sentinel'ы + `post_deploy.py` уже restart-safe (как ORCH-021/036). + +## Последствия + +- **+** Терминальная (`done`) задача стабильно держит `Done`: любой deploy-сеттер для неё сходится к + `Done` идемпотентно, маятник гаснет за один цикл независимо от актора (буфер BR-1/BR-2, AC-1/AC-2). +- **+** Легитимный пост-деплой `Monitoring` сохранён точно (предикат «окно активно» + перенос + арм-блока); рабочий deploy-цикл 1:1 (AC-4). +- **+** Наблюдаемость: лог однозначно атрибутирует «кто/почему» при будущем флаппе (AC-5). +- **+** Единый низкий чокпоинт ловит и неизвестный внутренний путь под бот-токеном. +- **−** Один локальный SELECT (`tasks`) на каждый deploy-фазовый PATCH-вызов self-репо. Митигейшн: + читается тот же ряд, что даёт `repo` для `applies`; SQLite-чтение пренебрежимо против сетевого PATCH; + для не-self/выключенного флага — ранний ALLOW без лукапа окна. +- **−** Если фактический актор флаппа — **внешняя** Plane-automation под другим токеном (вне кода + орка), code-фикс не закроет G1 полностью. Митигейшн: гард — буфер на стороне орка; локализация + актора (FR-1) и итог документируются (BR-7) — этот ADR фиксирует гипотезу «под бот-токеном орка». +- **−** Перенос арм-блока меняет порядок внутри маркированного блока ORCH-021/066. Митигейшн: + инварианты обоих ADR проверены сохранёнными (D3); анти-регресс — TC-11 (рабочий цикл) + структурные + тесты. +- **Откат:** `ORCH_DEPLOY_STATUS_GUARD_ENABLED=false` → сеттеры терминал-слепы, поведение 1:1 + прежнее (D2 шаг 1). Полный откат — revert ветки (перенос арм-блока + leaf + config + сеттер-врезки). + +## Ссылки + +- BRD: `docs/work-items/ORCH-094/01-brd.md` +- TRZ: `docs/work-items/ORCH-094/02-trz.md` +- Acceptance: `docs/work-items/ORCH-094/03-acceptance-criteria.md` +- Tech-risks: `docs/work-items/ORCH-094/10-tech-risks.md` +- Сквозной ADR: `docs/architecture/adr/adr-0028-terminal-window-aware-deploy-status-guard.md` +- Сверено по коду: `src/stage_engine.py` (369/404/431/1218/1316/~1698-1729), + `src/plane_sync.py` (913/954/964/974, `_set_issue_state_direct`), `src/post_deploy.py` + (`arm_monitor`/`has_marker`/`ARMED`/`DONE`/`state_dir`), `src/reconciler.py` (F-1/F-2), + `src/config.py` (флаги ORCH-088/021/036), `src/db.py` (`get_task_by_plane_id`). +- Маркеры (прочитаны, не сломаны): ORCH-021 (`adr-0010` / `06-adr/ADR-001`), ORCH-066 + (`06-adr/ADR-001-plane-status-model`), ORCH-086/068 (терминал-скип), ORCH-088 (freeze), + ORCH-090 (cancelled-терминал). + + diff --git a/docs/work-items/ORCH-094/10-tech-risks.md b/docs/work-items/ORCH-094/10-tech-risks.md new file mode 100644 index 0000000..6e85755 --- /dev/null +++ b/docs/work-items/ORCH-094/10-tech-risks.md @@ -0,0 +1,91 @@ +--- +work_item: ORCH-094 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-09 +model_used: claude-opus-4-8 +--- + +# 10 — Технические риски: ORCH-094 — terminal-window-aware гард deploy-статусов + +Work Item: **ORCH-094** · Repo: **orchestrator** · Стадия: architecture + +Формат: каждый риск — **вероятность × влияние**, причина, **митигейшн**, привязка к AC/ADR-решению. + +--- + +## R-1 — Гард подавляет ЛЕГИТИМНЫЙ `Monitoring` у реально деплоящейся задачи (регресс AC-4) +- **Вероятность:** средняя (без точного предиката — высокая) · **Влияние:** высокое. +- **Причина:** `update_task_stage("done")` (стр. 369) выполняется **раньше** `set_issue_monitoring` + (стр. 404) ⇒ в момент легитимного `Monitoring` задача в БД уже `done`. Наивный гард + «stage==done → Done» затёр бы легитимную индикацию. +- **Митигейшн:** предикат **«терминал И НЕ активное окно»** (D2 шаг 6) + **перенос арм-блока перед + terminal-sync** (D3): `window_active==True` на стр. 404 ⇒ ALLOW. Анти-регресс — **TC-11** + (рабочий цикл `Awaiting→Deploying→Monitoring→Done` без подавления) + **TC-03** (stage=deploy + проходит). + +## R-2 — Фактический актор флаппа — внешняя Plane-automation (вне кода орка) +- **Вероятность:** низкая · **Влияние:** среднее (G1 закрыт не полностью). +- **Причина:** все 273 перехода — под бот-токеном орка; гипотеза H-внешнее не исключена до + инструментальной локализации (FR-1). +- **Митигейшн:** гард — **буфер на стороне орка** (BR-2): если PATCH идёт через код орка — гасится; + developer локализует актора (FR-1) и фиксирует в ADR/CHANGELOG (BR-7). Если актор реально внешний — + это документируется как known-limitation, гард остаётся защитой от внутренних путей. + +## R-3 — Перенос арм-блока ломает инвариант ORCH-021/066 +- **Вероятность:** низкая · **Влияние:** высокое (self-hosting прод). +- **Причина:** правка порядка внутри маркированного блока `next_stage == "done"`. +- **Митигейшн:** `arm_monitor` не зависит от Plane-статуса/merge-lease (пишет sentinel + ставит + отложенный job); merge-lease release остаётся после terminal-sync; идемпотентность арма по `ARMED` + и инвариант ORCH-066 (`deploy→done` self ⇒ `Monitoring`) сохранены (D3). Прочитаны `adr-0010` + + `06-adr/ADR-001-plane-status-model`. Тесты TC-06/TC-08 + TC-11. + +## R-4 — `never-raise`-деградация маскирует флапп (fail-safe = ALLOW) +- **Вероятность:** низкая · **Влияние:** низкое. +- **Причина:** при ошибке лукапа стадии / сетевой ошибке гард делает ALLOW (прежнее поведение), что + в теории не гасит маятник. +- **Митигейшн:** БД-чтение — локальный SQLite (надёжно; ошибка редка); в штатном случае стадия + читается ⇒ сходимость работает. Деградация **логируется** `warning` (D5) ⇒ видно в диагностике. + NFR-1 приоритезирует «не падать/не блокировать конвейер всех проектов» над агрессивным подавлением. + Тест TC-05. + +## R-5 — «Зомби»-тик пост-деплой-монитора после рестарта/стейл-job шлёт статус-PATCH +- **Вероятность:** низкая · **Влияние:** среднее. +- **Причина:** стейл-job `post-deploy-monitor` в очереди после закрытия окна/рестарта мог бы дёрнуть + `set_issue_monitoring`. +- **Митигейшн:** идемпотентный страж `has_marker(...DONE)` (ранний return без PATCH/реэнкью, ~1729) + + тик no-op при `cancelled` мид-окно (D4) + **гард D2** (`window_active==False` ⇒ CONVERGE_DONE). + restart-safe (sentinel'ы на диске). Тесты TC-06/TC-07. + +## R-6 — Стоимость лукапа `tasks` на каждый deploy-PATCH +- **Вероятность:** низкая · **Влияние:** пренебрежимое. +- **Причина:** новый SELECT на каждый вызов deploy-сеттера self-репо. +- **Митигейшн:** тот же ряд даёт `repo` для `applies`; SQLite-чтение ничтожно против сетевого PATCH; + не-self/выключенный флаг → ранний ALLOW. Без кэша (корректность > микро-оптимизация). + +## R-7 — Регресс не-self репозиториев (enduro-trails) +- **Вероятность:** очень низкая · **Влияние:** среднее. +- **Причина:** общий инстанс/БД; правка общих сеттеров `plane_sync`. +- **Митигейшн:** `applies(repo)` (D2 шаг 3, `deploy_status_guard_repos=""` → self-hosting only); + для не-self deploy-фазовые статусы и так не выставляются (terminal-sync сразу `Done`). Тест TC-12. + +## R-8 — Лукап по `work_item_id` не матчит (нет аксессора) +- **Вероятность:** низкая · **Влияние:** низкое (деградирует в ALLOW). +- **Причина:** `get_task_by_plane_id` матчит UUID-ключи, не человекочитаемый `work_item_id`. +- **Митигейшн:** developer добавляет read-only `get_task_by_work_item_id` (D8, без миграции); при + промахе — ALLOW (never-raise). Тумбстоны ORCH-090 (`#cancelled-`) не коллизируют с живым рядом. + +--- + +## Сводка по инвариантам (не нарушены) + +| Инвариант | Статус | +|-----------|--------| +| `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict ключи | не тронуты (D7) | +| Схема БД | без миграции (read-only аксессор) (D7/D8) | +| `main` / force-push / прод-контейнер / detached-деплой | не тронуты (D7, NFR-2) | +| Рабочий self-deploy (Phase A→B→C, merge-gate, freeze ORCH-088) | 1:1 (D7, AC-4) | +| Реконсилятор F-1/F-2 | без изменений (гард субсумирует sync→Done) (D7) | +| Обратимость (kill-switch → 1:1) | `ORCH_DEPLOY_STATUS_GUARD_ENABLED` (D6) | +