Files
orchestrator/docs/work-items/ORCH-124/01-brd.md
claude-bot fef5ba15d5
All checks were successful
CI / test (push) Successful in 1m9s
analyst(ET): auto-commit from analyst run_id=763
2026-06-16 17:56:23 +03:00

186 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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.