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

19 KiB
Raw Blame History

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.repojobs.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_rollbackROLLBACK) для репо выставляется 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.