---
work_item: ORCH-124
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-16
model_used: claude-opus-4-8
escalate: full-cycle
---

# 01 — BRD / Bug-report: ORCH-124 — serial gate treats Backlog/Blocked/Needs-Input paused tasks as active and blocks urgent successors

Work Item: **ORCH-124** · Repo: **orchestrator** · Стадия: analysis · Трек: **Bug → эскалация в full-cycle**

> ⚠️ **`escalate: full-cycle` (ADR-001 D5 ORCH-019).** Метка задачи — `Bug`, но по сути это
> **архитектурный** дефект: требуется **определить семантику wait/terminal состояний serial-gate**
> и выбрать механизм «пауза без блокировки» (release-on-status / явный per-task hold-флаг /
> переиспользование `task_deps`). Любой вариант пересекается с **корневым инвариантом ORCH-088
> (анти-stale-base)** и с гармонизированным терминальным предикатом ORCH-090 (`adr-0026`,
> `serial_gate` + `task_deps` + `stages.py`). Это не «однострочная» правка — нужен ADR с явным
> разрешением конфликта свойств (см. §8 и `10-tech-risks.md` архитектора). Поэтому выпускается
> **полный** analysis-пакет (а не облегчённый bug-пакет). Оператор снимает багфикс-трек:
> `POST /bug-fast-track/escalate?work_item=ORCH-124` → задача пойдёт через стадию `architecture`
> (architect выпустит ADR для семантики паузы serial-gate).

---

## 1. Бизнес-контекст и проблема

### Симптом (наблюдаемое — установленный факт инцидента)
Во время инцидента **ORCH-116/ORCH-123**: задачу **ORCH-116** намеренно поставили на паузу
(перевели в Plane-статус Blocked/Backlog), чтобы вперёд пропустить срочный фикс **ORCH-123**.
Однако `serial_gate` **по-прежнему считал ORCH-116 активной задачей** (`active_task`) и держал
analyst-job ORCH-123 в очереди (`queued`) — срочный фикс не мог стартовать, пока ORCH-116
формально не `done`/`cancelled`.

### Причина симптома (установленный факт — верифицировано по коду)
`serial_gate` определяет «активную задачу репо» **исключительно по машинной стадии**
`tasks.stage NOT IN ('done','cancelled')` — в трёх местах `src/serial_gate.py`:
- `build_claim_clause()` — горячий SQL-фрагмент в `db.claim_next_job`:
  `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-зеркало для наблюдаемости;
- `_per_repo_snapshot()` — выбор `active_task` для блока `serial_gate` в `GET /queue`.

При этом **Plane-статусы Backlog / Blocked / Needs Input — это слой B (индикация), ORCH-066**, и они
**не меняют `tasks.stage` (слой A)**. Сеттеры `set_issue_blocked` / `set_issue_needs_input`
(`src/plane_sync.py`) делают только `PATCH` Plane-статуса; машинная стадия задачи остаётся прежней
(`analysis` / `development` / `deploy-staging` …). Подтверждение из кода: у таблицы `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 того же репо.

### Почему это важно (бизнес-боль)
- **Срочный фикс не запускается**, пока более ранняя задача поставлена на паузу. Единственные
  существующие способы «разблокировать» — терминально `cancel`/довести до `done`, либо целиком
  выключить serial-gate (`ORCH_SERIAL_GATE_ENABLED=false`) для всех репо. Все три — грубые.
- У оператора **нет чистого механизма «пауза без блокировки»** с явным намерением — отдельного от
  отмены (терминал) и от глобального выключения гейта.
- На пакетном автономном прогоне (эпик ORCH-088) это превращает любую «отложенную» задачу в стоп-кран
  очереди репо.

### Прецедент в коде (контекст для архитектора, не решение)
Reconciler уже **умеет** уважать wait-состояния: ORCH-060 Guard 2 (`reconciler._is_blocked_or_needs_input`,
Variant A) **сетевым** запросом Plane-статуса пропускает Blocked/Needs-Input (и активные
ORCH-066-ожидания) и не «оживляет» их. Но reconciler — фоновый тик и **может** позволить себе сетевой
вызов; `serial_gate.build_claim_clause` врезан в `claim_next_job` (**offline hot-path**) и сетевого
вызова позволить **не может** (NFR-2 ниже). Это центральное расхождение, которое и порождает баг:
сигнал паузы есть в Plane, но не доступен горячему SQL гейта.

## 2. Объём (scope)

### В объёме
- Определить **семантику wait/terminal** для serial-gate: какие состояния задачи-предшественника
  НЕ должны держать FIFO-гейт закрытым для более поздней задачи.
- Дать оператору **явный, durable, DB-резолвимый** механизм «пауза без блокировки» (или формально
  переиспользовать существующий: freeze / task_deps), с чётким намерением, отличным от cancel.
- Поправить определение «активной задачи» во **всех трёх** точках `serial_gate.py`, чтобы
  приостановленная задача не считалась `active_task`.
- Корректная **причина ожидания** в блоке `serial_gate` снапшота `GET /queue`
  (active task / paused-predecessor / freeze / dependency).
- Тесты: предшественник Blocked/Backlog/Needs-Input + срочный успешник; регресс-тест инцидента.

### Вне объёма
- Изменения `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict ключей — **не трогаем**
  (маршрутизация очереди — свойство планировщика, не Quality Gate).
- Введение нового **машинного** статуса в `STAGE_TRANSITIONS` (это не новая стадия конвейера).
- Изменение поведения reconciler ORCH-060 (его networked-skip уже корректен; гармонизация — на
  усмотрение архитектора, но переписывать его не требуется).
- Автоматическое управление паузой по данным вне явного намерения оператора (никакого эвристического
  «само-распаузивания»).
- Конкретный **выбор механизма** (release-on-status vs per-task hold-флаг vs task_deps) — это решение
  **архитектора** (ADR), а не аналитика.

## 3. Заинтересованные стороны
- **Оператор/владелец конвейера (Слава)** — заказчик: нуждается в чистой паузе, чтобы пропускать
  срочные фиксы без отмены и без выключения гейта.
- **Self-hosting orchestrator** — затрагивается напрямую (serial-gate активен для всех репо).
- **enduro-trails** — затрагивается косвенно (общая БД/очередь); регрессия недопустима при
  выключенном/нейтральном поведении.
- **Архитектор** — принимает решение о механизме и семантике (ADR), разрешает конфликт §8.
- Принимает результат — reviewer + tester по критериям `03-acceptance-criteria.md`.

## 4. Бизнес-требования (BR)
- **BR-1** — Перевод задачи-предшественника в состояние паузы/ожидания (Backlog / Blocked /
  Needs Input) **больше не должен случайно блокировать** более позднюю срочную задачу того же репо в
  serial-gate. Проверяемо: analyst-job успешника становится claimable.
- **BR-2** — У оператора есть **чистый механизм «пауза без блокировки»** с явным намерением,
  **отличный** от `cancel` (терминал) и от глобального выключения гейта. Намерение — durable
  (переживает рестарт процесса/контейнера).
- **BR-3** — Пауза снимает гейт **только по явному намерению**. **Нормально исполняемая** задача
  (реально идёт работа) **по-прежнему держит** гейт — анти-stale-base гарантия ORCH-088 не
  регрессирует (см. §8 — конфликт свойств, разрешает архитектор).
- **BR-4** — Снапшот `serial_gate` в `GET /queue` показывает **корректную причину** ожидания
  успешника: активная задача / приостановленный предшественник / freeze / dependency.
- **BR-5** — При **возобновлении** (распаузе) задачи serial-ordering корректно восстанавливается:
  возобновлённая задача снова участвует в гейте (либо держит его, либо явно ре-входит в FIFO с
  обязательством rebase) — нет «вечного обхода» и нет потерянного намерения.
- **BR-6** — Существующие гарантии serial-gate сохранены: FIFO по более ранним незавершённым
  задачам, durable per-repo `freeze` (`repo_freeze`), cross-repo параллелизм, явные `task_deps` —
  по-прежнему блокируют, где должны.

## 5. Нефункциональные требования (NFR)
- **NFR-1 (never-raise / fail-safe)** — Контракт leaf `serial_gate` сохранён: каждая публичная
  функция деградирует консервативно. Сохранить два направления отказа ORCH-088: hot-claim build →
  **fail-OPEN** (`""`-фрагмент, не заклинить очередь всех проектов, AC-8 ORCH-088); freeze-решение →
  **fail-CLOSED** (прод-безопасность, AC-9 ORCH-088). Новая логика паузы не должна инвертировать эти
  направления.
- **NFR-2 (чистота hot-path)** — Гейт-в-claim остаётся **offline SQL-предикатом**; **никаких сетевых
  вызовов** (в т.ч. Plane API) в `claim_next_job`. Сигнал «пауза» обязан быть **DB-резолвимым**
  (durable колонка/таблица), а не считываться из Plane на горячем пути (в отличие от reconciler).
- **NFR-3 (обратная совместимость / kill-switch / область)** — Изменение аддитивно и обратимо;
  выключатель (существующий `serial_gate_enabled` либо новый под-флаг) → байт-в-байт прежнее поведение
  до ORCH-124. `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict ключи / **схемы
  существующих таблиц** — без изменений (новая колонка/таблица — только аддитивно, паттерн
  `_ensure_column` / `CREATE TABLE IF NOT EXISTS`). enduro не затронут при нейтральном поведении.
- **NFR-4 (гармонизация предиката)** — Любой новый предикат «активна/терминальна/пауза» должен
  оставаться согласованным с гармонизированным терминальным множеством `{done,cancelled}` в
  `serial_gate` + `task_deps` + `stages.py` (ORCH-090 / adr-0026), либо архитектор явно описывает,
  почему serial-gate расходится (паузой), не ломая `task_deps`.
- **NFR-5 (self-hosting безопасность)** — Никакого рестарта/падения прод-контейнера, мутации `main`,
  force-push; только чтение/запись своих таблиц и принятие решения о claim. Hot-path не должен
  замедляться сетью или тяжёлым запросом.

## 6. Допущения и ограничения
- У таблицы `tasks` **сегодня нет колонки статуса**; Plane-статус (Backlog/Blocked/Needs Input) —
  слой B индикации, в БД не отражён. Значит «пауза» для горячего пути требует **нового durable
  DB-сигнала** (колонка `tasks` или отдельная таблица), либо переиспользования уже DB-резолвимого
  механизма (`repo_freeze` / `task_deps`).
- `repo_freeze` существует, но **freeze'ит весь репо** (блокирует всех успешников) — это
  противоположность «пропустить срочного успешника», поэтому как есть не годится для BR-1 (но годится
  как явный блок для BR-6).
- `task_deps` (`job_deps`) — явные декларации зависимостей, уже DB-резолвимы и консультируются в
  `claim_next_job` (`NOT EXISTS`); кандидат на «explicit intent», на усмотрение архитектора.
- Reconciler ORCH-060 различает Blocked/Needs-Input **сетевым** запросом Plane — прецедент семантики,
  но **не переиспользуем** на hot-path (NFR-2).
- Серый кейс: Needs Input во время `analysis` — нормальное короткое ожидание ответа; решение, считать
  ли его «паузой для гейта», за архитектором (важно не превратить штатное короткое ожидание в обход
  анти-stale-base).

## 7. Критерии успеха
Кратко (детальные PASS/FAIL — `03-acceptance-criteria.md`):
- Приостановленный предшественник (Backlog/Blocked/Needs-Input по явному намерению) не блокирует
  срочного успешника; нормально идущая задача — блокирует; freeze/dependency блокируют, где должны;
  `GET /queue` показывает корректную причину; всё под kill-switch; машинные инварианты байт-в-байт;
  регресс-тест инцидента красный до фикса и зелёный после.

## 8. Риски
Кратко (детально — `10-tech-risks.md`, заполняет архитектор):
- **R-1 (ключевой конфликт свойств) — пауза vs анти-stale-base (ORCH-088).** Если «пауза» освобождает
  гейт, успешник срежет ветку от `main`, ещё **не** содержащего код предшественника. Когда
  приостановленный предшественник позже возобновится и смержится — его база/ветка могут стать stale.
  ORCH-088 был построен ровно чтобы это предотвратить. Архитектор обязан разрешить конфликт явно
  (напр.: пауза «демотирует» задачу в FIFO и обязывает rebase при возобновлении; либо явный per-task
  «yield» с принятием rebase). **Аналитик фиксирует конфликт, не выбирает решение.**
- **R-2** — Случайное/неявное освобождение гейта (баг в детекте намерения) ослабит сериализацию для
  всех — требуется строго **явное** намерение оператора.
- **R-3** — Рассинхрон «Plane-статус ↔ DB-сигнал паузы»: если механизм опирается на webhook о смене
  статуса, потерянный webhook оставит задачу «активной» в БД (или наоборот). Нужен durable,
  идемпотентный, восстановимый сигнал.
- **R-4** — Регрессия гармонизированного предиката `{done,cancelled}` в `task_deps`/`stages.py`, если
  serial-gate изменит понимание «активности» неаккуратно.
- **R-5** — fail-direction: ошибка в новой ветке не должна инвертировать fail-OPEN (claim) /
  fail-CLOSED (freeze) контракты ORCH-088.
