architect(ET): auto-commit from architect run_id=764
All checks were successful
CI / test (push) Successful in 1m12s

This commit is contained in:
2026-06-16 19:17:43 +03:00
parent fef5ba15d5
commit de4f067655
6 changed files with 544 additions and 4 deletions

View File

@@ -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=<id>` (по образцу
`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-<id>` (`plane_id`/`work_item_id`/`plane_issue_id`)
- `tasks` — задачи и их стадии; колонки `cancelled_at`/`cancel_requested_at` (ORCH-090) — durable-метки STOP-отмены (вторая — отложенная отмена в критичном окне merge/deploy). Терминальная стадия `cancelled` (сток, параллельно `done`); натуральные ключи отменённой строки тумбстонятся суффиксом `#cancelled-<id>` (`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=<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=<id>`) → `{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=<id>`) → `{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=<id>`) → `{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}` |

View File

@@ -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)
</content>

View File

@@ -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 | Метод проверки |