19 KiB
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): для репо
Rgate закрыт, если существует задачаAрепоRсоstage != 'done'(любая стадияcreated…deploy), отличная от рассматриваемой новой задачиB. - Что блокируется при закрытом gate: запуск analyst-агента новой задачи
Bи создание её ветки (Gitea-ветка + worktree). Branch уBне должен быть срезан, пока gate закрыт (иначе stale-base, AC-6). - Где гейтить: в горячем пути выбора работы —
db.claim_next_job(по образцуtask_depsNOT 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_*) + PlaneBlockedдля деградировавшей задачи (как 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/mainHEAD. - Гарантировать, что ветка НЕ присоединяется к 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_depsORCH-026 — gate в claim, не новый QG).
8. Конфигурация (src/config.py)
По образцу task_deps_enabled / merge_gate_* / post_deploy_*:
serial_gate_enabled: bool = True(envORCH_SERIAL_GATE_ENABLED) — kill-switch;False→ claim и старт ведут себя строго как сейчас (нулевая регрессия, NFR-4).serial_gate_repos: str = ""(envORCH_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 какqueuedjob без ветки.) - OQ-2: Хранилище freeze — отдельная таблица
repo_freezevs колонка. - OQ-3: Способ ручного снятия freeze (эндпоинт / Plane-жест / админ-команда).
- OQ-4: Поведение при задаче в Blocked/Needs-Input, держащей gate закрытым (Этап 1 — держит; нужен ли отдельный «вывод из учёта активных» — вероятно нет, фиксируем как осознанное).
- OQ-5: Область по умолчанию (все репо vs только self-hosting) — рекомендация §8.