---
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`.
