186 lines
19 KiB
Markdown
186 lines
19 KiB
Markdown
---
|
||
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.
|