architect(ET): auto-commit from architect run_id=519

This commit is contained in:
2026-06-09 23:11:40 +03:00
committed by orchestrator-deployer
parent f36528705e
commit 8959e0e3f4
4 changed files with 455 additions and 0 deletions

View File

@@ -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-образ свеж и провалидирован». Этой гарантии нет:

View File

@@ -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`.
</content>

View File

@@ -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 = <lookup по work_item_id>`; `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-<id>`), **без изменения схемы**. Один
локальный 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-терминал).
</content>
</invoke>

View File

@@ -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-<id>`) не коллизируют с живым рядом.
---
## Сводка по инвариантам (не нарушены)
| Инвариант | Статус |
|-----------|--------|
| `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) |
</content>