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