Files
orchestrator/docs/work-items/ORCH-088/02-trz.md

211 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.
# 02 — ТЗ (TRZ): ORCH-088 — Serial gate (Этап 1: пакетный автономный режим, serial e2e)
Work Item: **ORCH-088** · Repo: **orchestrator** · Стадия: analysis
> Документ описывает **что** должно измениться и **где** (модули/контракты/артефакты). **Как**
> (конкретная схема реализации, выбор «таблица vs sentinel», точки врезки) — решает архитектор в
> `06-adr/`. ТЗ фиксирует требования и границы, не предлагает архитектурное решение.
> ⚠️ Скоп — только FR-1…FR-5 (serial e2e). Merge-очередь / pre-merge rebase / фазы A/B/C / ORCH-83 —
> вне скопа.
---
## 1. Сводка изменения
Ввести **per-repo serial gate**: новая задача репо не входит в стадию `analysis` (не режет ветку, не
запускает analyst-агент), пока в том же репо есть незавершённая задача (`stage != 'done'`). Открытие
gate — по достижении предшественником `stage = 'done'` (после прод-деплоя). Дополнительно — **per-repo
freeze** при деградации/rollback прода (post-deploy), снимаемый вручную. Всё — аддитивно, под
kill-switch, с областью репо, never-raise, restart-safe. Машина стадий и реестр QG **не меняются**.
---
## 2. Задействованные модули `src/`
| Модуль | Роль в задаче | Характер изменения |
|--------|---------------|--------------------|
| `src/db.py` | `claim_next_job` (горячий claim), схема `tasks`/`jobs`, helper'ы выборки активной задачи репо; (возможно) аддитивная таблица/колонка для freeze | gate-условие в claim + новые read-only helper'ы + аддитивная миграция (идемпотентная, `_ensure_column`/`CREATE TABLE IF NOT EXISTS`) |
| `src/queue_worker.py` | вызывает `claim_next_job` в `_drain_once` | без изменения контракта; gate работает внутри claim |
| `src/webhooks/plane.py` | `start_pipeline` / `handle_status_start` / `_create_gitea_branch` | **отсрочка создания ветки** до момента, когда репо свободен (ключевое для AC-6); постановка задачи в очередь ожидания вместо немедленного среза ветки |
| `src/git_worktree.py` | `ensure_worktree` — срез ветки от `origin/main` | гарантия: для новой задачи база = свежий `origin/main` после `git fetch` (см. §6) |
| `src/agents/launcher.py` | `_spawn` — ленивое создание worktree на claim | согласование с отсрочкой среза ветки (не материализовать stale-ветку) |
| `src/stage_engine.py` | `run_post_deploy_monitor` / блок `next_stage == "done"` | при вердикте деградации/rollback — выставить per-repo freeze (FR-5) |
| `src/post_deploy.py` | `decide_action` / реакция | сигнал для freeze (`ALERT_ONLY` self / `ROLLBACK*` non-self) → выставление freeze |
| `src/config.py` | флаги фичи | новые: `serial_gate_enabled`, `serial_gate_repos` (CSV), при необходимости — флаги freeze |
| `src/main.py` | `GET /queue` | новый read-only блок наблюдаемости `serial_gate` |
| `src/notifications.py` / `src/plane_sync.py` | алерты freeze | переиспользовать `send_telegram` / `set_issue_blocked` / `notify_*` (never-raise) |
> Чистую логику gate/freeze желательно вынести в **leaf-модуль** (например `src/serial_gate.py`,
> never-raise, по образцу `src/task_deps.py` / `src/post_deploy.py`) — окончательно решает архитектор.
---
## 3. Функциональные изменения (требования к поведению)
### 3.1. FR-1 — Serial gate на входе в анализ
- **Условие закрытия gate (per-repo):** для репо `R` gate **закрыт**, если существует задача `A` репо
`R` со `stage != 'done'` (любая стадия `created…deploy`), **отличная** от рассматриваемой новой
задачи `B`.
- **Что блокируется при закрытом gate:** запуск analyst-агента новой задачи `B` **и** создание её
ветки (Gitea-ветка + worktree). Branch у `B` не должен быть срезан, пока gate закрыт (иначе stale-base,
AC-6).
- **Где гейтить:** в горячем пути выбора работы — `db.claim_next_job` (по образцу `task_deps` NOT EXISTS
gate), читая ТОЛЬКО локальную БД (NFR-2). Дополнительно — на входе `start_pipeline`, чтобы **не резать
ветку** до открытия gate (см. §3.3).
- **Применимость:** gate работает только для analyst-job новой задачи (вход в анализ). Job'ы уже
активной задачи (architect/developer/…/deployer) проходят свободно — иначе единственная активная
задача не сможет двигаться по конвейеру.
### 3.2. FR-2 — Очередь e2e
- Накиданные задачи репо встают в очередь; обрабатывается строго одна end-to-end. Реализуется
естественно: gate держит остальных, активная идёт по стадиям до `done`, затем gate открывается и
выбирается следующая (FIFO по существующему порядку очереди `jobs.id`).
### 3.3. FR-1/AC-6 — Отсрочка среза ветки (анти-stale-base)
- **Проблема (проверено):** ветка создаётся в Gitea в `start_pipeline._create_gitea_branch` от `main`
в момент перевода issue в «To Analyse» (T0) — **до** того, как предшественник влит. `ensure_worktree`
затем **присоединяет уже существующую** Gitea-ветку (а не режет свежую от `origin/main`), т.е. свежий
`git fetch` не спасает — база остаётся stale.
- **Требование:** создание ветки (Gitea-ветка и/или worktree) для новой задачи должно происходить
**после** того, как gate открылся (предшественник `done`), чтобы базой был `origin/main`, уже
содержащий код предшественника. Конкретный механизм отсрочки (отложить `_create_gitea_branch`;
материализовать ветку лениво при claim'е analyst-job из свежего `origin/main`; и т.п.) — выбирает
архитектор. Инвариант результата: **ветка `B` имеет в предках merge-commit/код всех ранее
завершённых задач репо** (проверяемо `git merge-base --is-ancestor`).
- Если архитектура решит резать ветку при claim'е analyst-job (а не в `start_pipeline`), это
автоматически даёт AC-6 (claim происходит только при открытом gate).
### 3.4. FR-3 — Per-repo
- Все выборки gate фильтруются по `tasks.repo``jobs.repo`). Состояние gate/freeze репо `R` не
влияет на claim/старт задач другого репо. Cross-repo параллелизм сохранён.
### 3.5. FR-4 — Restart-safe
- «Активная задача репо» вычисляется запросом к БД (`tasks` по `repo` + `stage != 'done'`), не из
in-memory. Freeze хранится в БД (аддитивная таблица/колонка). После рестарта поведение идентично.
### 3.6. FR-5 — Rollback-freeze
- При вердикте post-deploy `DEGRADED` (для self — реакция `ALERT_ONLY`; для non-self с
`post_deploy_auto_rollback``ROLLBACK`) для репо выставляется **durable freeze** (в БД).
- При активном freeze репо gate **закрыт безусловно**, независимо от наличия задач `stage<done`
(важно: деградировавшая задача к этому моменту уже `stage='done'` — BR-7 — поэтому обычный gate её
не удержит; нужен отдельный сигнал).
- Снятие freeze — **ручное** (оператор). Способ снятия (эндпоинт/админ-команда/ручная правка БД/
Plane-жест) определяет архитектор; требование — снятие должно быть простым, явным и наблюдаемым.
- Алерт: Telegram (`send_telegram`/`notify_*`) + Plane `Blocked` для деградировавшей задачи (как
ORCH-021), плюс явное сообщение «пакет заморожен, следующая задача не стартует до ручного снятия».
---
## 4. Изменения API
### 4.1. Новые публичные endpoint'ы
- **Нет обязательных новых endpoint'ов.** (Снятие freeze может быть реализовано как админ-эндпоинт —
на усмотрение архитектора; если вводится, описать в ADR и обновить таблицу API в README.)
### 4.2. Изменяемые endpoint'ы
- `GET /queue`**аддитивно** добавляется блок `serial_gate` (read-only снимок), по образцу блоков
`task_deps` / `reconcile` / `post_deploy`:
- `enabled` (флаг), `repos` (область),
- per-repo: `active_task` (`{work_item_id, stage}` или `null`), `waiting` (список ожидающих
задач/job'ов репо), `frozen` (bool) + причина/таймстамп freeze.
- never-raise: при ошибке — минимальный словарь с флагами и пустыми данными.
- Контракт `GET /queue`**расширяется аддитивно**, существующие ключи не меняются.
### 4.3. Webhook-обработчики
- `start_pipeline` / `handle_status_start` (`webhooks/plane.py`): добавляется ветвление «репо занят/
заморожен → отложить старт/срез ветки, поставить в очередь ожидания» вместо немедленного
`_create_gitea_branch` + enqueue. Внешний контракт вебхука Plane не меняется.
---
## 5. Изменения схемы БД
> Только **аддитивные, идемпотентные** миграции (общая прод-БД, enduro не трогать). Без изменения
> существующих таблиц-контрактов.
- **Freeze-состояние (FR-5):** требуется durable per-repo признак заморозки. Варианты (выбор —
архитектор): новая таблица `repo_freeze(repo TEXT, frozen_at TEXT, reason TEXT, work_item_id TEXT,
cleared_at TEXT)` **или** аддитивная колонка в существующей таблице. Требования к выбранному варианту:
идемпотентная миграция (`CREATE TABLE IF NOT EXISTS` / `_ensure_column`), restart-safe, per-repo.
- **Активная задача репо:** **новых колонок НЕ требуется** — вычисляется из существующих
`tasks(repo, stage)`.
- **Очередь ожидания:** переиспользовать существующую `jobs` (status='queued' + gate в claim) — новой
таблицы очереди **не вводить** (FR-2 решается gate'ом, не отдельной структурой).
- `STAGE_TRANSITIONS`, `QG_CHECKS`, `tasks`-контракт, `job_deps`, `agent_runs`**без изменений**.
---
## 6. Требования к срезу ветки (`git_worktree` / launcher)
- Для новой задачи, чья ветка создаётся после открытия gate: перед срезом — `git fetch origin`
(уже есть в `ensure_worktree`), база — `origin/main` HEAD.
- Гарантировать, что ветка НЕ присоединяется к stale Gitea-ветке, созданной раньше времени: либо не
создавать Gitea-ветку преждевременно (отсрочка §3.3), либо при материализации worktree база
безусловно = свежий `origin/main` (включающий предшественника).
- Никогда не push/force-push в `main`. Существующие merge-lease / auto_rebase (ORCH-026/043) не
трогаются.
---
## 7. Требования к новым QG checks
- **Новых QG-проверок не вводить.** Gate — это условие планировщика (claim / старт), а **не**
Quality Gate стадии. Реестр `QG_CHECKS` и `check_*` не меняются (как `task_deps` ORCH-026 —
gate в claim, не новый QG).
## 8. Конфигурация (`src/config.py`)
По образцу `task_deps_enabled` / `merge_gate_*` / `post_deploy_*`:
- `serial_gate_enabled: bool = True` (env `ORCH_SERIAL_GATE_ENABLED`) — kill-switch; `False` → claim и
старт ведут себя строго как сейчас (нулевая регрессия, NFR-4).
- `serial_gate_repos: str = ""` (env `ORCH_SERIAL_GATE_REPOS`, CSV) — область; пусто → применять как
по умолчанию (см. ниже).
- Helper `serial_gate_applies(repo) -> bool` (leaf-модуль, never-raise) по образцу `post_deploy_applies`:
`enabled` + (если CSV непуст — членство репо; иначе — область по умолчанию).
- **Область по умолчанию (решение для ADR):** serial gate осмыслен для ВСЕХ репо (FR-3 — и orchestrator,
и enduro выигрывают от serial e2e), в отличие от self-hosting-only гейтов (ORCH-35/43/58). Рекомендация:
пустой CSV → применять ко всем зарегистрированным репо. Архитектор фиксирует и обосновывает в ADR.
- При необходимости — отдельные флаги для freeze (FR-5), например `serial_gate_freeze_enabled`.
---
## 9. Наблюдаемость и алерты
- `GET /queue` блок `serial_gate` (см. §4.2).
- Лог: каждое решение «gate закрыт, задача отложена» и «freeze выставлен/снят» → `logger.info/warning`.
- Telegram: freeze (выставление) → алерт (`send_telegram`/`notify_*`); карточка задачи (ORCH-042/087)
может отражать «⏳ ждёт завершения <work_item_id>» (по образцу строки `task_deps` «⏳ ждёт ORCH-NNN»),
never-raise.
---
## 10. Артефакты pipeline (создать/обновить в ТОМ ЖЕ PR)
Документация — golden source (CLAUDE.md §2). По итогам разработки обновить:
- `docs/work-items/ORCH-088/06-adr/ADR-001-serial-gate.md` — решение (механизм отсрочки ветки, freeze-
хранилище, область по умолчанию, точки врезки).
- `docs/architecture/README.md` — новый раздел «Serial gate (ORCH-088)» + строка статуса доработок;
обновить описание `GET /queue` (блок `serial_gate`) и раздел «База данных», если добавлена таблица.
- `CLAUDE.md` — краткий абзац о serial-режиме (если уместно в паспорте).
- `CHANGELOG.md` — запись `feat:`.
- При новой таблице freeze — `docs/work-items/ORCH-088/08-data-requirements.md`.
- При новом админ-эндпоинте снятия freeze — обновить таблицу API в README.
---
## 11. Инварианты (не нарушать)
- `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, `check_*`, exit-коды deploy-хука, merge-gate (ORCH-043),
merge-verify (ORCH-071/073), image-freshness (ORCH-058), post-deploy контракт (ORCH-021),
`max_concurrency`**без изменений**.
- never-raise на единицу работы; claim fail-**open** на ошибке БД (NFR-1); freeze fail-**closed**.
- Offline в горячем claim (NFR-2): без сетевых вызовов Plane/Gitea.
- Не рестартить/не ронять прод-контейнер (CLAUDE.md self-hosting).
- Миграции аддитивны и идемпотентны; enduro при выключенном/неприменимом флаге не затрагивается.
---
## 12. Открытые вопросы для архитектора (не блокируют анализ)
- OQ-1: Механизм отсрочки среза ветки — отложить `_create_gitea_branch` в `start_pipeline` ИЛИ
перенести материализацию ветки на claim analyst-job? (Влияет на AC-6 и на то, где живёт «ожидающая»
задача — в Plane-статусе vs как `queued` job без ветки.)
- OQ-2: Хранилище freeze — отдельная таблица `repo_freeze` vs колонка.
- OQ-3: Способ ручного снятия freeze (эндпоинт / Plane-жест / админ-команда).
- OQ-4: Поведение при задаче в Blocked/Needs-Input, держащей gate закрытым (Этап 1 — держит; нужен ли
отдельный «вывод из учёта активных» — вероятно нет, фиксируем как осознанное).
- OQ-5: Область по умолчанию (все репо vs только self-hosting) — рекомендация §8.