From de4f06765555183827f32ba30ab36a992f1bc2b4 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Tue, 16 Jun 2026 19:17:43 +0300 Subject: [PATCH] architect(ET): auto-commit from architect run_id=764 --- docs/architecture/README.md | 34 +- ...0051-serial-gate-pause-without-blocking.md | 111 +++++++ docs/architecture/internals.md | 8 + ...-001-serial-gate-pause-without-blocking.md | 300 ++++++++++++++++++ .../ORCH-124/08-data-requirements.md | 54 ++++ docs/work-items/ORCH-124/10-tech-risks.md | 41 +++ 6 files changed, 544 insertions(+), 4 deletions(-) create mode 100644 docs/architecture/adr/adr-0051-serial-gate-pause-without-blocking.md create mode 100644 docs/work-items/ORCH-124/06-adr/ADR-001-serial-gate-pause-without-blocking.md create mode 100644 docs/work-items/ORCH-124/08-data-requirements.md create mode 100644 docs/work-items/ORCH-124/10-tech-risks.md diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 4f41179..5e343f3 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -621,10 +621,34 @@ ORCH-027 вводит детерминированный (без LLM) **гейт `serial_gate_freeze_enabled`. Наблюдаемость — аддитивный блок `serial_gate` в `GET /queue` (per-repo `active_task` / `waiting` / `frozen`). Cross-repo параллелизм сохранён (FR-3); при выключенном флаге — нулевая регрессия (enduro не затронут). +- **Ось «пауза без блокировки» (ORCH-124 — [adr-0051](adr/adr-0051-serial-gate-pause-without-blocking.md)).** + Баг (инцидент ORCH-116/ORCH-123): serial-gate считал «активной» задачу **исключительно по машинной + стадии**, а Plane-статусы Backlog/Blocked/Needs-Input (слой B индикации, ORCH-066) **не меняют + `tasks.stage`** ⇒ приостановленный предшественник держал FIFO закрытым против срочного успешника, и у + оператора не было чистого механизма «пауза без блокировки», отдельного от cancel (терминал) и от + глобального выключения гейта. Решение — **явный per-task park-сигнал**: аддитивная колонка + `tasks.paused_at TEXT` (NULL = не на паузе; паттерн `cancelled_at`/`track`) + **новая ортогональная ось + планировщика «пауза»**, отделённая от оси «терминальность». serial-gate «активна» ⇔ + `stage NOT IN ('done','cancelled') AND paused_at IS NULL` (терм `AND t2.paused_at IS NULL` во всех 3 + точках под под-флагом). **Терминал `{done,cancelled}` в `serial_gate`/`task_deps`/`stages.py` — + байт-в-байт (adr-0026 не регрессирует)**: `task_deps`/`stages.py` колонку `paused_at` НЕ читают ⇒ + паузнутая объявленная зависимость и `repo_freeze` **по-прежнему блокируют** (пауза их не обходит — разные + оси). Намерение — явные эндпоинты `POST /serial-gate/pause|resume?work_item=` (по образцу + `unfreeze`), durable/offline/webhook-независимо (NFR-2). **Анти-stale-base (ORCH-088) не регрессирует:** + нормальная задача (`paused_at IS NULL`) держит гейт; при resume свежесть базы дают существующие механизмы + — отложенный срез (для паузнутой-в-`analysis`) и pre-merge `auto_rebase_onto_main` + merge-gate re-test + (ORCH-026/093/110) для материализованной ветки; новой rebase-машинерии нет. Наблюдаемость — ключ `paused` + + `reason` ожидания (`active-task`/`dependency`/`freeze`) в блоке `serial_gate` `GET /queue`. Под-флаг + `serial_gate_pause_enabled` (env `ORCH_SERIAL_GATE_PAUSE_ENABLED`, дефолт `True`; зеркало + `serial_gate_freeze_enabled`); `False` ⇒ pause-терм опущен, serial-gate байт-в-байт ORCH-088/090. Дефолт + безопасен (no-op, пока ничего не паузнуто — enduro не затронут). `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/ + схемы существующих таблиц — не тронуты. Детали — + `docs/work-items/ORCH-124/06-adr/ADR-001-serial-gate-pause-without-blocking.md`. -Подробнее: [adr-0017](adr/adr-0017-serial-gate.md), детально — -`docs/work-items/ORCH-088/06-adr/ADR-001-serial-gate.md`, -`docs/work-items/ORCH-088/08-data-requirements.md`. +Подробнее: [adr-0017](adr/adr-0017-serial-gate.md) + [adr-0051](adr/adr-0051-serial-gate-pause-without-blocking.md) +(пауза), детально — `docs/work-items/ORCH-088/06-adr/ADR-001-serial-gate.md`, +`docs/work-items/ORCH-088/08-data-requirements.md`, +`docs/work-items/ORCH-124/06-adr/ADR-001-serial-gate-pause-without-blocking.md`. ### Авто-режим по лейблам: autoApprove + autoDeploy (ORCH-089 — реализовано) Конвейер имеет два **человеческих** гейта, тормозящих пакетный автономный прогон (эпик @@ -1410,7 +1434,7 @@ Monitoring after Deploy → Done ## База данных (SQLite) - `events` — входящие вебхуки (дедуп) -- `tasks` — задачи и их стадии; колонки `cancelled_at`/`cancel_requested_at` (ORCH-090) — durable-метки STOP-отмены (вторая — отложенная отмена в критичном окне merge/deploy). Терминальная стадия `cancelled` (сток, параллельно `done`); натуральные ключи отменённой строки тумбстонятся суффиксом `#cancelled-` (`plane_id`/`work_item_id`/`plane_issue_id`) +- `tasks` — задачи и их стадии; колонки `cancelled_at`/`cancel_requested_at` (ORCH-090) — durable-метки STOP-отмены (вторая — отложенная отмена в критичном окне merge/deploy). Терминальная стадия `cancelled` (сток, параллельно `done`); натуральные ключи отменённой строки тумбстонятся суффиксом `#cancelled-` (`plane_id`/`work_item_id`/`plane_issue_id`). Колонка `paused_at` (ORCH-124, adr-0051) — durable per-task park-сигнал serial-gate (NULL = не на паузе): **ортогональная** оси «терминальность» ось «пауза» (`paused_at IS NOT NULL`), читается **только** serial-gate (`task_deps`/`stages.py` её не читают); паузнутый предшественник не держит FIFO, но не обходит `repo_freeze`/`task_deps` - `agent_runs` — запуски агентов (run_id, usage, cost) - `jobs` — очередь задач (ORCH-1); статусы `queued|running|done|failed|cancelled` (ORCH-090: `cancelled` — терминальный исход STOP, нигде не реквью'ится); колонка `pid` (ORCH-065) — pid агентского процесса для liveness-детекции зомби job-reaper'ом - `job_deps` — декларативные зависимости задач (ORCH-026, Уровень B): `(task_id, depends_on_task_id)`, аддитивная; источник истины планировщика для гейта «B ждёт A» @@ -1429,6 +1453,8 @@ Monitoring after Deploy → Done | GET | `/queue` | очередь: counts + max_concurrency + resilience + reconcile (ORCH-053) + reaper (ORCH-065) + post_deploy (ORCH-021) + task_deps (ORCH-026) + serial_gate (ORCH-088) + auto_labels (ORCH-089) + stop (ORCH-090) + lessons (ORCH-098) + transition_lease (ORCH-114) + последние jobs | | GET | `/metrics` | ORCH-099 (FND/F1a): read-only машинное «сырьё» для sidecar F1b — конверт `schema_version`/`generated_at`/`clk_tck` + разделы `stages`/`queue`/`agents` (liveness: pid/runtime/cpu_ticks)/`cost`. never-raise по разделам; kill-switch `ORCH_METRICS_ENABLED` (дефолт `True`). Контракт — см. раздел «Сырьё-эндпоинт `/metrics`» | | POST | `/serial-gate/unfreeze` | ORCH-088 (FR-5): ручное снятие per-repo rollback-freeze (query/body `repo=`) → `{ok, repo, cleared, frozen}`; идемпотентно. Альтернатива — `UPDATE repo_freeze SET cleared_at=datetime('now') WHERE repo=? AND cleared_at IS NULL` | +| POST | `/serial-gate/pause` | ORCH-124 (D7): поставить задачу на паузу для serial-gate (query/body `work_item=`) → `{ok, work_item, task_id, paused_at}`; идемпотентно. Паузнутый предшественник не держит FIFO против срочного успешника (пауза ≠ cancel, ≠ глобальный kill-switch); НЕ обходит `repo_freeze`/`task_deps` | +| POST | `/serial-gate/resume` | ORCH-124 (D7): снять паузу (query/body `work_item=`) → `{ok, work_item, task_id, paused_at: null}`; идемпотентно. Возобновлённая задача снова участвует в serial-gate; свежесть базы — существующие отложенный срез / merge-gate rebase+re-test | | POST | `/transition-lease/release` | ORCH-114 (FR-6, **опц.**): операторский ручной реклейм застрявшего владения переходом (query/body `work_item=`) → `{ok, task_id, released}`; идемпотентно (паттерн `/serial-gate/unfreeze`). При выключенном `transition_lease_enabled` → no-op | | GET | `/lessons` | ORCH-098 (FR-4): read-only выборка журнала уроков; query-фильтры `type`/`status`/`repo`/`work_item`/`limit` → `{enabled, lessons:[…]}` (всегда `200`, чтение не мутирует). При `lessons_enabled=False` → `{enabled:false, lessons:[]}` | | POST | `/lessons` | ORCH-098 (FR-5): ручная запись урока (JSON-тело, `lesson_type` обязателен, `source="manual"` не дедупится) → `{id}`; при выключенном флаге → `{enabled:false}` | diff --git a/docs/architecture/adr/adr-0051-serial-gate-pause-without-blocking.md b/docs/architecture/adr/adr-0051-serial-gate-pause-without-blocking.md new file mode 100644 index 0000000..0988e09 --- /dev/null +++ b/docs/architecture/adr/adr-0051-serial-gate-pause-without-blocking.md @@ -0,0 +1,111 @@ +--- +work_item: ORCH-124 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-16 +model_used: claude-opus-4-8 +--- + +# ADR-0051: Ось «пауза» serial-gate — park-сигнал без блокировки FIFO + +Сквозной (cross-cutting) ADR. Детальное решение задачи — +`docs/work-items/ORCH-124/06-adr/ADR-001-serial-gate-pause-without-blocking.md`. + +Статус: **Proposed** · Дата: 2026-06-16 · Источник: **ORCH-124** (bug → escalate full-cycle) + +## Контекст + +ORCH-088 (serial-gate, adr-0017) определяет «активную задачу репо» **исключительно по машинной стадии** +`tasks.stage NOT IN ('done','cancelled')` (после ORCH-090/adr-0026 — с учётом терминала `cancelled`). +Plane-статусы Backlog/Blocked/Needs-Input — **слой B (индикация), ORCH-066** — не меняют `tasks.stage` +(слой A); у таблицы `tasks` нет колонки статуса. ⇒ приостановленная оператором задача неотличима от +активно исполняемой и держит FIFO-гейт (`t2.id < jobs.task_id`) закрытым для более поздних analyst-job +того же репо. + +**Инцидент ORCH-116/ORCH-123:** ORCH-116 поставили на паузу, чтобы пропустить срочный фикс ORCH-123, но +serial-gate держал analyst-job ORCH-123 в `queued`. Единственные обходы (терминальный `cancel`, довод до +`done`, глобальное `serial_gate_enabled=false`) — грубые. + +Горячий путь `serial_gate.build_claim_clause` врезан в `claim_next_job` — **offline SQL** — и сетевого +чтения Plane-статуса (как делает reconciler ORCH-060) позволить не может. Нужен **DB-резолвимый** сигнал +паузы. + +## Решение + +### Инвариант: «пауза» — ОТДЕЛЬНАЯ ОСЬ планировщика, ортогональная «терминальности» + +Вводится **per-task park-сигнал** — аддитивная нуллабельная колонка **`tasks.paused_at TEXT`** +(NULL = не на паузе) — и **новая ось планировщика «пауза»**, независимая от оси «терминальность». + +| Ось | Предикат | Кто использует | Меняется ORCH-124? | +|-----|----------|----------------|--------------------| +| **Терминальность** (adr-0026) | `stage IN ('done','cancelled')` | `serial_gate` + `task_deps` + `stages.py` | **НЕТ — байт-в-байт** | +| **Пауза** (новая, ORCH-124) | `paused_at IS NOT NULL` | **только** FIFO «active» предикат `serial_gate` | да (аддитивно) | + +**serial-gate «активная задача» ⇔ `stage NOT IN ('done','cancelled') AND paused_at IS NULL`.** Это +**осознанная, задокументированная** дивергенция serial-gate от чисто-терминального предиката (требование +гармонизации adr-0026): пауза выводит предшественника из FIFO-учёта serial-gate, **не делая его +терминальным**. + +### Что НЕ меняется (анти-регресс adr-0026) + +- **`task_deps`** (adr-0015) и **`stages.py::STAGE_TRANSITIONS`** колонку `paused_at` **не читают** — + остаются чисто терминальными. Явно объявленная зависимость (`job_deps`) на **приостановленную** задачу + **по-прежнему блокирует** зависимый job. Пауза («пропустите меня в FIFO») и dependency («B нужен + результат A») — разные оси; пауза НЕ обходит dependency и НЕ обходит per-repo `repo_freeze`. +- `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict / схемы существующих таблиц — без + изменений. Пауза — не стадия и не Quality Gate, а признак планировщика очереди. + +### Точки, признающие ось «пауза» (исчерпывающе) + +1. `src/serial_gate.py::build_claim_clause` — терм `AND t2.paused_at IS NULL` внутри `active_clause` + (под под-флагом). **(маркер ORCH-124, рядом с ORCH-088/ORCH-090)** +2. `src/serial_gate.py::repo_has_active_task` / `_per_repo_snapshot` — тот же предикат + наблюдаемость + (ключ `paused`, `reason` ожидания). +3. `src/db.py` — колонка `tasks.paused_at` (`_ensure_column`) + хелперы `set_task_paused`/ + `clear_task_paused`/`is_task_paused`. +4. `src/main.py` — операторские эндпоинты `POST /serial-gate/pause|resume` (по образцу + `POST /serial-gate/unfreeze`). + +### Анти-stale-base при возобновлении (ORCH-088 не регрессирует) + +Пауза «демотирует» задачу в FIFO; свежесть базы при resume обеспечивают **существующие** механизмы — новой +rebase-машинерии нет: отложенный срез ветки (ORCH-088, для паузнутой-в-`analysis`) + безусловный pre-merge +`auto_rebase_onto_main` под merge-lease (ORCH-026/093) + merge-gate re-test (ORCH-110) для уже +материализованной ветки. Нормальная задача (`paused_at IS NULL`) по-прежнему держит гейт. + +### Флаги / совместимость + +- Независимый под-флаг `serial_gate_pause_enabled` (env `ORCH_SERIAL_GATE_PAUSE_ENABLED`, дефолт `True`) — + зеркало `serial_gate_freeze_enabled`. `False` ⇒ pause-терм опущен из SQL, эндпоинты no-op ⇒ serial-gate + байт-в-байт как ORCH-088/090. Область — переиспользует `serial_gate_repos` (новый `*_repos` не вводится). +- Дефолт `True` безопасен: пока ни одна задача не на паузе, `paused_at` везде `NULL` ⇒ истинный no-op + (enduro не затронут). +- never-raise: pause-терм в `build_claim_clause` сохраняет **fail-OPEN**; freeze — **fail-CLOSED**. +- Миграция — только аддитивная/идемпотентная (`_ensure_column`); общая прод-БД безопасна (NFR-3). + +## Последствия + +- **+** Чистая операторская «пауза без блокировки», отличная от cancel (терминал) и от kill-switch; + durable, offline, webhook-независимая; закрывает инцидент ORCH-116/ORCH-123. +- **+** Единый, явно описанный двухосевой предикат планировщика (терминальность ⊥ пауза) — устранён риск + будущего рассинхрона. +- **−** Появилась вторая ось «активности» serial-gate — будущие подсистемы планировщика обязаны помнить: + serial-gate «активна» = `не терминальна И не на паузе`, но **терминал** (`task_deps`/`stages.py`) ось + «пауза» НЕ включает. Митигейшн: этот ADR + маркер `ORCH-124` в изменённых местах + тесты. +- **Откат:** `ORCH_SERIAL_GATE_PAUSE_ENABLED=false` (serial-gate 1:1 как ORCH-088/090; колонка `paused_at` + инертна). + +## Эволюция маркеров + +Горячий SQL serial-gate несёт теперь 3 маркера (`ORCH-088` FIFO-гейт, `ORCH-090` терминал `cancelled`, +`ORCH-124` ось паузы) — правка любого из них сверяется с этим сводным ADR (анти-археология: 3+ маркеров → +одна ссылка сюда, `docs/_standards/TRACEABILITY.md`). + +## Ссылки +- Детальный ADR: `docs/work-items/ORCH-124/06-adr/ADR-001-serial-gate-pause-without-blocking.md` +- Данные: `docs/work-items/ORCH-124/08-data-requirements.md` +- Связанные: adr-0017 (serial-gate ORCH-088), adr-0026 (терминал `{done,cancelled}` ORCH-090), + adr-0015 (task-deps), adr-0027 (merge-актор rebase/retry ORCH-093), adr-0042 (merge-gate re-test ORCH-110) + diff --git a/docs/architecture/internals.md b/docs/architecture/internals.md index 0328e77..82c958a 100644 --- a/docs/architecture/internals.md +++ b/docs/architecture/internals.md @@ -70,6 +70,14 @@ STAGE_TRANSITIONS = { рёбер не меняются), а терминал STOP-отмены. Системный предикат «задача завершена» — `stage ∈ {done, cancelled}` (синхронно в `reconciler`/`serial_gate`/`task_deps`; adr-0026). +**Ось «пауза» ⊥ оси «терминальность» (ORCH-124, adr-0051):** serial-gate вводит **отдельную** ось +паузы `tasks.paused_at IS NOT NULL` (durable per-task park-сигнал) — **ортогональную** терминалу. Для +serial-gate «активная задача» ⇔ `stage NOT IN ('done','cancelled') AND paused_at IS NULL` (паузнутый +предшественник не держит FIFO). **Терминал `{done,cancelled}` НЕ расширяется паузой:** `task_deps` и +`stages.py` колонку `paused_at` НЕ читают (паузнутая объявленная зависимость по-прежнему блокирует +зависимый job; пауза не обходит `repo_freeze`). Пауза — признак планировщика очереди, не стадия и не +терминальное состояние. + ### 3. Quality Gates (`src/qg/checks.py`) | Check | Метод проверки | diff --git a/docs/work-items/ORCH-124/06-adr/ADR-001-serial-gate-pause-without-blocking.md b/docs/work-items/ORCH-124/06-adr/ADR-001-serial-gate-pause-without-blocking.md new file mode 100644 index 0000000..4b757b1 --- /dev/null +++ b/docs/work-items/ORCH-124/06-adr/ADR-001-serial-gate-pause-without-blocking.md @@ -0,0 +1,300 @@ +--- +work_item: ORCH-124 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-16 +model_used: claude-opus-4-8 +--- + +# ADR-001: Serial-gate «пауза без блокировки» — явный per-task park-сигнал (ORCH-124) + +Work Item: **ORCH-124** · Repo: **orchestrator** (self-hosting) · Стадия: architecture +Связь: BRD `01-brd.md`, ТЗ `02-trz.md`, AC `03-acceptance-criteria.md`, данные `08-data-requirements.md`, риски `10-tech-risks.md`. +Сквозная регистрация: `docs/architecture/adr/adr-0051-serial-gate-pause-without-blocking.md`. + +## Статус +Proposed + +--- + +## Контекст + +Баг (метка `Bug`, эскалирован в full-cycle — `escalate: full-cycle`, ADR-001 D5 ORCH-019): по сути +**архитектурный** дефект семантики serial-gate, требующий ADR (выбор механизма «паузы» + разрешение +конфликта с анти-stale-base ORCH-088). + +**Симптом (инцидент ORCH-116/ORCH-123, установленный факт).** Задачу-предшественника ORCH-116 +поставили на паузу (перевели в Plane Blocked/Backlog), чтобы пропустить вперёд срочный фикс ORCH-123. +`serial_gate` **по-прежнему считал ORCH-116 активной** и держал analyst-job ORCH-123 в `queued` — срочный +фикс не стартовал, пока ORCH-116 формально не `done`/`cancelled`. + +**Причина (верифицировано в коде).** `serial_gate.py` определяет «активную задачу репо» **исключительно +по машинной стадии** `tasks.stage NOT IN ('done','cancelled')` в трёх точках: +- `build_claim_clause()` — горячий SQL-фрагмент в `db.claim_next_job` (`src/serial_gate.py:274-278`): + `EXISTS (SELECT 1 FROM tasks t2 WHERE t2.repo = jobs.repo AND t2.id < jobs.task_id AND + t2.stage NOT IN ('done','cancelled'))`; +- `repo_has_active_task()` — Python-зеркало (`src/serial_gate.py:117-127`); +- `_per_repo_snapshot()` — выбор `active_task` для `GET /queue` (`src/serial_gate.py:340-344`). + +Plane-статусы Backlog/Blocked/Needs-Input — **слой B (индикация), ORCH-066** — **не меняют `tasks.stage` +(слой A)**. Сеттеры `set_issue_blocked`/`set_issue_needs_input` делают только `PATCH` Plane-статуса; у +таблицы `tasks` **нет колонки статуса** (`src/reconciler.py:322`: «`tasks` has no status column, so the +live Plane state is the source of truth»). ⇒ для serial-gate приостановленная задача неотличима от активно +исполняемой: её стадия вне `{done,cancelled}` ⇒ она «активна» ⇒ держит FIFO закрытым для всех более +поздних analyst-job того же репо. + +**Прецедент, который НЕ переиспользуем.** `reconciler` уже различает Blocked/Needs-Input +(`_is_blocked_or_needs_input`, ORCH-060 Guard 2) — но **сетевым** запросом Plane. `serial_gate.build_claim_clause` +врезан в `claim_next_job` — **offline hot-path** — и сетевого вызова позволить **не может** (NFR-2). Это и +есть центральное расхождение: сигнал паузы есть в Plane, но недоступен горячему SQL гейта. + +**Нужен** явный, durable, **DB-резолвимый** признак «пауза», который горячий SQL читает локально, при этом +**не регрессирует** анти-stale-base ORCH-088 (R-1) и не ломает гармонизированный терминал `{done,cancelled}` +(ORCH-090 / adr-0026, NFR-4). + +--- + +## Решение + +### Сводка +Вводится **явный per-task park-сигнал** — аддитивная нуллабельная колонка **`tasks.paused_at TEXT`** +(NULL = не на паузе; non-NULL = поставлена оператором на паузу) — и **новая ортогональная ось +планировщика «пауза»**, отделённая от оси «терминальность». «Активная задача» для serial-gate +переопределяется как **`не терминальна И не на паузе`** во всех трёх точках; терминал `{done,cancelled}` +в `serial_gate`/`task_deps`/`stages.py` остаётся **байт-в-байт**. Намерение паузы задаётся явными +операторскими эндпоинтами `POST /serial-gate/pause|resume` (по образцу `POST /serial-gate/unfreeze`). +Анти-stale-base при возобновлении обеспечивают **существующие** механизмы (отложенный срез ветки ORCH-088 ++ pre-merge `auto_rebase_onto_main` под merge-lease ORCH-026/093 + merge-gate re-test ORCH-110) — **новой +rebase-машинерии не вводится**. Аддитивно, под независимым под-флагом, never-raise, restart-safe. +`STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict / схемы существующих таблиц — +**не трогаются** (правка планировщика очереди, не Quality Gate). + +### D1 — Механизм: явный per-task pause-флаг (а не release-on-status / task_deps) (FR-3, BR-2) + +**Решение: явный durable DB-сигнал «park» на уровне задачи**, инициируемый оператором через API, а **не** +маппинг Plane-статуса и **не** `task_deps`. + +Обоснование выбора (см. «Альтернативы» для отклонённых): +- **Чистое намерение, отличное от cancel и от kill-switch** (BR-2): park ≠ терминал (`cancelled`), + ≠ глобальное выключение гейта (`serial_gate_enabled=False`). +- **DB-резолвимо и offline** (NFR-2): сигнал — колонка локальной БД, читается горячим SQL без сети. +- **Не перегружает Plane-статус** (ORCH-066/059): pause НЕ управляется сменой Plane-статуса. Оператор + может **дополнительно** перевести карточку в Blocked для индикации, но это косметика — гейт ею не + управляется. Это прямое следование решению ORCH-088 D4 (снятие freeze Plane-жестом отвергнуто как + анти-паттерн ORCH-059). +- **Durable/идемпотентно/restart-safe** (BR-2/R-3): колонка переживает рестарт; не зависит от доставки + webhook (потерянный webhook не рассинхронит сигнал). + +### D2 — Хранилище: аддитивная колонка `tasks.paused_at` (а не отдельная таблица) (NFR-3) + +**Решение: нуллабельная колонка `tasks.paused_at TEXT`** через `_ensure_column` — паттерн уже +существующих per-task durable-сигналов `tasks.cancelled_at` / `tasks.cancel_requested_at` / `tasks.track` +(`src/db.py:141-149`). NULL = не на паузе; ISO-таймстамп = на паузе (момент постановки, наблюдаемость). + +Почему **колонка**, а не таблица по образцу `repo_freeze`: +- Пауза — **per-task** сигнал (кардинальность 1:1 с задачей), в отличие от `repo_freeze` (per-**repo**, + append-only журнал истории заморозок). +- Горячий SQL `build_claim_clause` уже сканирует `tasks t2` — добавление `AND t2.paused_at IS NULL` + внутрь существующего `EXISTS`-подзапроса — **минимальная, offline, index-дружественная** правка без + лишнего JOIN/EXISTS. Таблица потребовала бы доп. подзапрос в горячем пути. +- Схемы существующих таблиц (`tasks`/`jobs`/`job_deps`/`repo_freeze`) не меняются деструктивно; миграция — + идемпотентный `_ensure_column` (no-op на уже мигрированной БД), безопасна на общей прод-БД (enduro не + затронут). Детали — `08-data-requirements.md`. + +### D3 — Пауза — ортогональная ось; терминал `{done,cancelled}` не трогается (NFR-4, FR-6 — критично) + +**Решение: «активность» для serial-gate = `не терминальна И не на паузе`; терминал остаётся +`{done,cancelled}` без изменений.** + +Это явная, задокументированная дивергенция, которую требует NFR-4. Две независимые оси: + +| Ось | Предикат | Где используется | Меняется ORCH-124? | +|-----|----------|------------------|--------------------| +| **Терминальность** | `stage IN ('done','cancelled')` | `serial_gate` + `task_deps` + `stages.py` (adr-0026) | **НЕТ — байт-в-байт** | +| **Пауза (новая)** | `paused_at IS NOT NULL` | **только** FIFO «active» предикат `serial_gate` | да (аддитивно) | + +Следствия (закрывает R-4 и FR-6): +- **serial-gate** «активная задача» = `stage NOT IN ('done','cancelled') AND paused_at IS NULL`. Пауза + выводит предшественника из FIFO-учёта serial-gate. +- **task_deps** НЕ трогается: остаётся чисто терминальным (`stage NOT IN ('done','cancelled')`). Явно + объявленная зависимость (`job_deps`) на **приостановленную** задачу **по-прежнему блокирует** зависимый + job — пауза НЕ обходит `task_deps` (FR-6/AC-5). Пауза («пропустите меня в FIFO») и dependency + («B реально нужен результат A») — разные оси. +- **stages.py** `STAGE_TRANSITIONS` не трогается: пауза — не стадия и не ребро (нет нового стока/перехода). + +### D4 — Три точки serial-gate правятся согласованно (FR-1, FR-2) + +Один предикат «активна» во всех трёх точках (анти-дрейф: одинаковый ответ на одинаковый вход), под +под-флагом паузы (D6): + +1. **`build_claim_clause()`** — в `active_clause` добавить терм `AND t2.paused_at IS NULL` (только когда + слой паузы включён; иначе фрагмент строится байт-в-байт как ORCH-088/090): + ```sql + EXISTS (SELECT 1 FROM tasks t2 + WHERE t2.repo = jobs.repo AND t2.id < jobs.task_id + AND t2.stage NOT IN ('done','cancelled') + {pause_term}) -- pause_term = " AND t2.paused_at IS NULL" | "" + ``` + Инварианты сохранены: гейт только для `jobs.agent='analyst'`; FIFO `t2.id < jobs.task_id` (R-7, + нет самоблокировки); job'ы активной задачи проходят. +2. **`repo_has_active_task()`** — добавить `AND paused_at IS NULL` (под тем же под-флагом). +3. **`_per_repo_snapshot()`** — выбор `active_task` исключает приостановленные (`AND paused_at IS NULL`), + и отдельно перечисляет приостановленные (D5). + +### D5 — Наблюдаемость: причина ожидания + список paused (FR-5, BR-4, AC-6) + +`_per_repo_snapshot` расширяется **аддитивно** (существующие ключи `active_task`/`waiting`/`frozen`/ +`frozen_reason`/`frozen_at` — байт-в-байт BC): +- `active_task` больше **не** показывает приостановленную задачу (D4.3). +- Новый ключ `paused: [{work_item_id, stage, paused_at}, …]` — приостановленные незавершённые задачи репо + (видимы, но не как `active_task`). +- Для каждого `waiting`-job добавляется `reason` — причина, по которой job НЕ claimable, с приоритетом: + **`freeze`** (активен `repo_freeze`) → **`dependency`** (незавершённая `task_deps` для task этого job) + → **`active-task`** (есть более ранняя **не-приостановленная** незавершённая задача) → **`null`** + (claimable). Категория `paused-predecessor` из ТЗ FR-5 — наблюдается через ключ `paused` (приостановленный + предшественник по дизайну **не** блокирует ⇒ не является причиной ожидания после фикса). + +### D6 — Условность: независимый под-флаг `serial_gate_pause_enabled` (FR-7, NFR-3) + +По образцу `serial_gate_freeze_enabled` (`src/config.py:1006`) — независимый тумблер для поэтапного раската +и обратимости: +- `serial_gate_pause_enabled: bool = True` (env `ORCH_SERIAL_GATE_PAUSE_ENABLED`). Дефолт `True` безопасен: + пока ни одна задача не на паузе (`paused_at` всегда NULL), предикат `AND t2.paused_at IS NULL` всегда + истинен ⇒ поведение **идентично** ORCH-088/090 ⇒ **истинный no-op** до явной операторской паузы (enduro + не затронут). Постановка на паузу возможна только через явный эндпоинт (D7). +- `False` ⇒ pause-терм **опускается** из SQL, эндпоинты pause/resume — no-op-предупреждение; serial-gate + ведёт себя **байт-в-байт** как ORCH-088/090 (осознанный rollback-режим — возврат к текущему багу, не + дефолт). +- Хелпер `serial_gate._pause_layer_enabled()` (never-raise, зеркало `_freeze_layer_enabled`). +- **Область** — переиспользует `serial_gate_repos` (пауза — уточнение того же гейта; новый `*_repos` + **не** вводится — принцип минимума конфигурации). Под-флаг паузы независим от `serial_gate_freeze_enabled`, + но подчинён kill-switch `serial_gate_enabled` (при выключенном гейте паузы нет смысла). + +### D7 — Операторские эндпоинты pause/resume (FR-3, BR-5, AC-3, AC-10) + +По образцу `POST /serial-gate/unfreeze` (`src/main.py:350-376`), never-raise, с Telegram-подтверждением: +- **`POST /serial-gate/pause?work_item=`** → `db.set_task_paused(task_id)` (`paused_at=datetime('now')`, + идемпотентно). Применимо к **нетерминальной** задаче (паузить `done`/`cancelled` — no-op + явный ответ). + Возвращает `{ok, work_item, task_id, paused_at}`. +- **`POST /serial-gate/resume?work_item=`** → `db.clear_task_paused(task_id)` (`paused_at=NULL`). + Возобновлённая задача снова участвует в serial-gate (AC-10): если ещё в `analysis` без ветки — + ре-входит в FIFO с отложенным срезом ветки; если уже материализована — держит гейт как активная, + её свежесть гарантирует merge-gate (D8). Возвращает `{ok, work_item, task_id, paused_at: null}`. +- DB-хелперы `db.set_task_paused`/`db.clear_task_paused`/`db.is_task_paused` (по образцу + `set_task_track`/`get_task_track`, `src/db.py:740-757`). never-raise. +- Освобождение гейта — **только** по этому явному durable намерению; эвристического само-распаузивания + нет (AC-3, R-2). + +### D8 — Анти-stale-base при возобновлении: переиспользуем существующие механизмы (FR-4, R-1 — критично) + +**Решение: пауза «демотирует» задачу в FIFO; свежесть базы при возобновлении гарантируют УЖЕ +существующие механизмы — новой rebase-машинерии НЕ вводится.** + +Два случая возобновления: +1. **Пауза, пока задача ещё в `analysis` с queued analyst-job и НЕматериализованной веткой** (отложенный + срез ORCH-088 D1): при resume срез ветки происходит на момент claim analyst-job + (`launcher._materialize_deferred_branch`) от **тогда-актуального** `origin/main` — который уже содержит + любого успешника, смерженного за время паузы. База **структурно свежая** ⇒ stale-base невозможна. +2. **Пауза после материализации ветки** (development/review/testing/deploy-staging): ветка уже срезана от + более раннего `main`. За время паузы успешник может смержиться ⇒ `main` уходит вперёд. При resume, когда + задача дойдёт до merge-gate (`deploy-staging → deploy`), **существующий безусловный pre-merge + `auto_rebase_onto_main` под merge-lease** (ORCH-026/088/093) перебазирует ветку на актуальный `main`, а + **merge-gate re-test** (ORCH-110) перепрогоняет сюиту на перебазированном HEAD. Свежесть обеспечивается + на merge, **не обходится**. + +Итог (разрешение конфликта R-1): пауза меняет **только порядок FIFO** (кто держит гейт), но **не** +контракт свежести на merge. Нормально исполняемая задача (`paused_at IS NULL`) по-прежнему держит гейт ⇒ +анти-stale-base для нормального случая (BR-3/AC-2) не регрессирует. Порядок merge при «B обгоняет +паузнутую A» = B, затем A (A ребейзится на B) — ровно намерение оператора. Проверяемо тестом по контракту +ADR (AC-4). + +### D9 — never-raise и сохранённые fail-directions (NFR-1, AC-9) + +- **`build_claim_clause`** остаётся **fail-OPEN**: pause-терм строится **внутри** существующего + `try/except`; любая ошибка (в т.ч. в pause-подвыражении) → `""` → claim без гейта (не заклинить очередь + всех проектов, AC-8). Направление не инвертируется. +- **Freeze** остаётся **fail-CLOSED** (`is_repo_frozen`, AC-9) — pause-логика его не касается. +- Pause-зеркало/снапшот/мутаторы never-raise → консервативная деградация (на ошибке чтения паузы в зеркале + — «не на паузе», т.е. задача считается активной = гейт скорее закрыт = анти-stale-base-safe). + +--- + +## Точки врезки (для разработчика) + +| Файл | Изменение | +|------|-----------| +| `src/db.py` | `_ensure_column(conn, "tasks", "paused_at", "TEXT")` (D2); хелперы `set_task_paused`/`clear_task_paused`/`is_task_paused` (D7) | +| `src/serial_gate.py` | `_pause_layer_enabled()` (D6); pause-терм в `build_claim_clause` (D4.1); `AND paused_at IS NULL` в `repo_has_active_task` (D4.2) и `_per_repo_snapshot` (D4.3); ключ `paused` + `reason` в снапшоте (D5). Маркер `ORCH-124` рядом с `ORCH-088`/`ORCH-090` | +| `src/config.py` | `serial_gate_pause_enabled: bool = True` (D6) | +| `src/main.py` | `POST /serial-gate/pause`, `POST /serial-gate/resume` (D7); блок `serial_gate` в `GET /queue` уже зовёт `snapshot()` (D5 — расширение прозрачно) | +| `tests/test_orch124_serial_gate_pause.py` | **новый** — AC-1 регресс инцидента (красный до фикса, зелёный после), AC-2…AC-10 | +| `docs/architecture/README.md`, `internals.md`, `CHANGELOG.md` | обновить раздел serial-gate + ось паузы (golden source) | + +`STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict ключи / `start_pipeline` / `launcher` +deferred-branch / merge-gate / схемы существующих таблиц — **не трогаются**. + +--- + +## Альтернативы (отклонены) + +- **Release-on-status (Plane Blocked/Backlog → DB-сигнал через webhook)** — отвергнуто: перегружает + Plane-статус управлением конвейером (нарушает слой A/B ORCH-066 и анти-паттерн ORCH-059, ровно как + ORCH-088 D4 отверг снятие freeze Plane-жестом); хрупко к потере webhook (R-3); намерение не доступно + offline hot-path (NFR-2). +- **Переиспользовать `task_deps`** — отвергнуто: `task_deps` моделирует «B ждёт A», не умеет выразить + «A на паузе, остальных пропустить» (обратное направление). Кроме того, пауза НЕ должна обходить + объявленную зависимость (FR-6) — это разные оси (D3). +- **Отдельная таблица `task_hold` (по образцу `repo_freeze`)** — отвергнуто: пауза per-task 1:1; колонка + минимальнее и не требует JOIN в горячем SQL (D2). `repo_freeze` — таблица, т.к. per-repo append-only журнал. +- **Реюз `repo_freeze` для паузы** — отвергнуто: freeze замораживает **весь репо** (блокирует всех + успешников) — противоположность «пропустить срочного успешника». +- **Расширить терминал `{done,cancelled,paused}`** — отвергнуто: пауза не терминальна; это сломало бы + `task_deps`/`stages.py` (NFR-4). Пауза — ортогональная ось, не терминальное состояние (D3). +- **Новая rebase-машинерия при resume** — отвергнуто как избыточное: существующие отложенный срез + + merge-gate rebase/re-test уже покрывают свежесть (D8). + +--- + +## Последствия + +### Плюсы +- **+** Закрывает инцидент ORCH-116/ORCH-123 (AC-1): срочный фикс стартует поверх паузнутого предшественника. +- **+** Чистое, явное, durable намерение паузы, отличное от cancel и kill-switch (BR-2); webhook-независимо + (R-3); offline hot-path (NFR-2). +- **+** Терминал `{done,cancelled}` и `task_deps`/`stages.py` — байт-в-байт (NFR-4); пауза НЕ обходит + freeze/dependency (FR-6). +- **+** Анти-stale-base (ORCH-088) не регрессирует — нормальная задача держит гейт; resume опирается на + существующие отложенный срез + merge-gate rebase/re-test (D8, AC-2/AC-4). +- **+** Переиспользует проверенные паттерны (`cancelled_at`-колонка, `unfreeze`-эндпоинт, leaf never-raise, + `/queue`-снапшот). `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/схемы существующих таблиц — без изменений. +- **+** Истинный no-op для enduro при дефолтном флаге (пауза не выставлена) и байт-в-байт откат при флаге off. + +### Минусы / ограничения +- **−** Пауза — операторское действие через API (не Plane-жест). Митигейшн: симметрично существующему + `unfreeze`; задокументировано в README + Telegram-подтверждение; оператор может дополнительно перевести + карточку в Blocked для индикации. +- **−** «Залипшая пауза» при невнимании оператора (resume забыт) → задача навсегда вне гейта. Митигейшн: + наблюдаемость (`paused` в `GET /queue`); resume идемпотентен; намерение durable, не теряется (R-2). +- **−** Горячий SQL serial-gate теперь несёт 3 маркера (`ORCH-088`/`ORCH-090`/`ORCH-124`) ⇒ сводный + сквозной ADR `adr-0051` (анти-археология, TRACEABILITY.md). + +### Откат +Полный откат — `ORCH_SERIAL_GATE_PAUSE_ENABLED=false` (serial-gate 1:1 как ORCH-088/090; pause-терм +опущен, эндпоинты no-op). Колонка `tasks.paused_at` инертна при выключенном под-флаге. Глубже — +`serial_gate_enabled=false` (весь гейт инертен, как до ORCH-088). + +--- + +## Ссылки +- BRD: `docs/work-items/ORCH-124/01-brd.md` · ТЗ: `02-trz.md` · Acceptance: `03-acceptance-criteria.md` +- Данные: `docs/work-items/ORCH-124/08-data-requirements.md` · Риски: `10-tech-risks.md` +- Сквозной ADR: `docs/architecture/adr/adr-0051-serial-gate-pause-without-blocking.md` +- Сверено по коду: `src/serial_gate.py` (117-127, 274-278, 340-344), `src/db.py` (claim_next_job + 1043-1110, `_ensure_column`/tasks-колонки 141-149, 740-757), `src/main.py` (350-376), `src/config.py` + (1004-1006), `src/reconciler.py:322` +- Базовые решения: adr-0017 (serial-gate ORCH-088), adr-0026 (терминал `{done,cancelled}` ORCH-090), + adr-0015 (task-deps ORCH-026), adr-0027 (merge-актор rebase/retry ORCH-093), adr-0042 (merge-gate + re-test ORCH-110) + + diff --git a/docs/work-items/ORCH-124/08-data-requirements.md b/docs/work-items/ORCH-124/08-data-requirements.md new file mode 100644 index 0000000..d6d9fee --- /dev/null +++ b/docs/work-items/ORCH-124/08-data-requirements.md @@ -0,0 +1,54 @@ +--- +work_item: ORCH-124 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-16 +model_used: claude-opus-4-8 +--- + +# 08 — Требования к данным: ORCH-124 — per-task park-сигнал serial-gate + +Work Item: **ORCH-124** · Repo: **orchestrator** · Стадия: architecture + +> When-applicable / информационный (гейтом не парсится). + +## Изменения схемы БД + +**Одна аддитивная нуллабельная колонка** на существующей таблице `tasks` (никаких новых таблиц): + +| Таблица | Колонка | Тип / дефолт | Семантика | +|---------|---------|--------------|-----------| +| `tasks` | `paused_at` | `TEXT` (по умолчанию отсутствует → `NULL`) | `NULL` = не на паузе; ISO-таймстамп (`datetime('now')`) = задача поставлена оператором на паузу (park) | + +Миграция — идемпотентный `_ensure_column(conn, "tasks", "paused_at", "TEXT")` в `init_db()`, ровно по +образцу `tasks.cancelled_at` / `tasks.cancel_requested_at` / `tasks.track` (`src/db.py:141-149`). На уже +мигрированной БД — no-op. + +**Индекс не требуется.** Горячий SQL `build_claim_clause` сканирует `tasks t2` уже сегодня (по `repo`/`id`); +терм `AND t2.paused_at IS NULL` — дополнительный фильтр в существующем `EXISTS`-подзапросе, не новый план +доступа. Кардинальность `tasks` per-repo мала; добавление индекса — преждевременная оптимизация (принцип +минимума). + +## Новые/изменённые сущности + +- **`tasks.paused_at`** — единственное durable хранилище намерения паузы. Запись — `db.set_task_paused` + (`paused_at=datetime('now')`); сброс — `db.clear_task_paused` (`paused_at=NULL`); чтение — + `db.is_task_paused` и SQL-предикат serial-gate. Все хелперы never-raise. +- **Инвариант оси:** `paused_at` — **ортогональная** ось «пауза», независимая от оси «терминальность» + (`stage IN ('done','cancelled')`). serial-gate «активна» = `stage NOT IN ('done','cancelled') AND + paused_at IS NULL`. `task_deps`/`stages.py` колонку `paused_at` **не читают** (терминал не трогается, + NFR-4). +- **Существующие таблицы** (`jobs` / `job_deps` / `repo_freeze` / `agent_runs`) — без изменений. + +## Совместимость данных / миграции + +- **Аддитивно и идемпотентно:** `_ensure_column` — no-op на уже-мигрированной БД; новая колонка + дефолтит в `NULL` для всех существующих строк ⇒ все текущие задачи считаются «не на паузе» ⇒ поведение + до ORCH-124 сохраняется до первой явной операторской паузы. +- **Restart-safe / durable:** значение в БД переживает рестарт процесса/контейнера (BR-2, R-3). +- **Общая прод-БД (self-hosting):** колонка добавляется на общей БД; при дефолтном `serial_gate_pause_enabled` + и отсутствии паузнутых задач — нулевая регрессия для enduro (`paused_at` везде `NULL`). +- **Откат:** колонка инертна при `ORCH_SERIAL_GATE_PAUSE_ENABLED=false` (pause-терм опускается из SQL). + Колонку можно оставить (безвредна); деструктивный drop не требуется и не рекомендуется на прод-БД. + diff --git a/docs/work-items/ORCH-124/10-tech-risks.md b/docs/work-items/ORCH-124/10-tech-risks.md new file mode 100644 index 0000000..19d83f6 --- /dev/null +++ b/docs/work-items/ORCH-124/10-tech-risks.md @@ -0,0 +1,41 @@ +--- +work_item: ORCH-124 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-16 +model_used: claude-opus-4-8 +--- + +# 10 — Технические риски: ORCH-124 — serial-gate «пауза без блокировки» + +Work Item: **ORCH-124** · Repo: **orchestrator** · Стадия: architecture + +> Информационный (гейтом не парсится). Перечисляет риски реализации и митигейшн; покрывает R-1…R-5 из BRD §8. + +## Реестр рисков + +| ID | Риск | Вер. | Влия. | Митигейшн | +|----|------|------|-------|-----------| +| TR-1 (= R-1, ключевой) | **Пауза vs анти-stale-base ORCH-088.** Успешник срезает ветку от `main` без кода паузнутого предшественника; при возобновлении предшественника возможна stale-база/затирание. | Сред. | Выс. | **D8:** новой rebase-машинерии нет — свежесть гарантируют существующие механизмы. Паузнутая-в-`analysis` задача при resume режет ветку отложенно (ORCH-088) от свежего `origin/main`. Материализованная — ребейзится на merge-gate (`auto_rebase_onto_main` под merge-lease ORCH-026/093) + re-test (ORCH-110). Нормальная задача (`paused_at IS NULL`) по-прежнему держит гейт (BR-3/AC-2). Тест AC-4. | +| TR-2 (= R-2) | **Неявное/случайное освобождение гейта** (баг в детекте намерения) ослабит сериализацию для всех. | Низ. | Выс. | Освобождение **только** по явной операторской паузе через эндпоинт (D7); никакого эвристического само-распаузивания (AC-3). Дефолтный флаг безопасен (no-op без явной паузы). Тест AC-3. | +| TR-3 (= R-3) | **Рассинхрон Plane-статус ↔ DB-сигнал паузы** (потерянный webhook оставит сигнал устаревшим). | Низ. | Сред. | Механизм НЕ опирается на webhook/Plane-статус (D1): сигнал — durable колонка `tasks.paused_at`, пишется прямым операторским вызовом, идемпотентен, переживает рестарт. Plane-статус — только косметическая индикация. | +| TR-4 (= R-4) | **Регрессия гармонизированного терминала `{done,cancelled}`** в `task_deps`/`stages.py`. | Низ. | Выс. | **D3:** пауза — отдельная ось; терминал `{done,cancelled}` в `serial_gate`/`task_deps`/`stages.py` байт-в-байт. `task_deps` колонку `paused_at` не читает (паузнутая зависимость по-прежнему блокирует, FR-6/AC-5). Структурный тест AC-8. | +| TR-5 (= R-5) | **Инверсия fail-direction** (ошибка в pause-ветке роняет claim или меняет fail-OPEN/fail-CLOSED). | Низ. | Выс. | **D9:** pause-терм внутри `try/except` `build_claim_clause` → fail-OPEN сохранён; freeze fail-CLOSED не тронут; все pause-функции never-raise. Тест AC-9 (инъекция ошибки → claim не падает). | +| TR-6 | **«Залипшая пауза»** — оператор забыл `resume`, задача навсегда вне FIFO-учёта. | Сред. | Низ. | Наблюдаемость: ключ `paused` + `reason` в `GET /queue` (D5); `resume` идемпотентен; durable сигнал не теряется. Операторская гигиена (как «вечный freeze» ORCH-088). | +| TR-7 | **Дрейф трёх точек** serial-gate (одна правится, другие нет → расхождение SQL-гейта и снапшота). | Низ. | Сред. | **D4:** один предикат «активна» во всех трёх точках, под одним под-флагом; анти-дрейф-тест (одинаковый ответ на одинаковый вход). | +| TR-8 | **Миграция колонки на общей прод-БД** (self-hosting) затронет enduro. | Низ. | Сред. | Идемпотентный `_ensure_column`, дефолт `NULL` (паттерн `cancelled_at`/`track`); при дефолтном флаге и отсутствии паузнутых задач — нулевая регрессия (08-data-requirements). | + +## Сводный вывод + +Доминирующий класс — **семантический конфликт паузы с анти-stale-base (TR-1)**, разрешённый +**переиспользованием существующих** механизмов свежести (D8), без новой машинерии. Остальные риски — +стандартные для leaf-расширения serial-gate (fail-direction, дрейф точек, миграция), покрыты паттернами +ORCH-088/090. Изменение **аддитивно, под независимым под-флагом, never-raise**, без правки +`STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/терминала/схем существующих таблиц. + +**Эскалация `arch:major-change` не требуется** (нет новой стадии/компонента/QG/смены БД — аддитивная +правка планировщика внутри существующего компонента serial-gate). Возврат в анализ не требуется (ТЗ +удовлетворяется без нарушения принципов архитектуры). Остаточный риск для прод-конвейера (self-hosting) — +**низкий**: дефолтное поведение — истинный no-op до явной операторской паузы; полный откат — один env-флаг. +