feat(serial-gate): per-repo serial gate + deferred branch cut + rollback-freeze (ORCH-088)

Этап 1 (serial e2e) пакетного автономного режима. Новая задача репо не входит
в analysis (analyst-job не выбирается, ветка не режется), пока в репо есть более
ранняя незавершённая задача (FIFO, t2.id < jobs.task_id) ИЛИ репо заморожен.

- src/serial_gate.py — новый leaf (never-raise): build_claim_clause (fail-OPEN),
  is_repo_frozen (fail-CLOSED), set/clear_repo_freeze, serial_gate_applies, snapshot.
- src/db.py — идемпотентная миграция repo_freeze + serial_gate-фрагмент в claim_next_job.
- src/webhooks/plane.py + src/agents/launcher.py — отложенный срез ветки: start_pipeline
  не создаёт Gitea-ветку/docs для применимого репо; релокация в _materialize_deferred_branch
  на момент claim analyst-job (база = свежий origin/main с кодом предшественника, AC-6).
- src/stage_engine.py — post-deploy DEGRADED → durable per-repo freeze + Telegram-алерт.
- src/main.py — блок serial_gate в GET /queue + POST /serial-gate/unfreeze.
- src/config.py — serial_gate_enabled / serial_gate_repos / serial_gate_freeze_enabled.

FIFO-уточнение реализации (FR-2): ADR-001 D1 фиксировал t2.id != jobs.task_id; при !=
пакет одновременно созданных свежих задач взаимно блокировался бы (дедлок). t2.id <
jobs.task_id допускает самую раннюю задачу и сериализует остальные, сохраняя AC-1/R-7.

STAGE_TRANSITIONS / QG_CHECKS / check_* — без изменений. Аддитивно, под kill-switch,
never-raise, restart-safe; при выключенном флаге — нулевая регрессия (enduro не затронут).

Тесты: TC-01..TC-22 (test_serial_gate*.py + test_queue_endpoint.py); полный прогон 1114 зелёных.
Docs: README (serial gate / /queue / API / БД), CLAUDE.md, CHANGELOG.md, .env.example.

Refs: ORCH-088
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 11:14:48 +03:00
committed by stream
parent 4597a8471d
commit ee4773f5b0
19 changed files with 1315 additions and 22 deletions

View File

@@ -433,6 +433,31 @@ class Settings(BaseSettings):
task_deps_enabled: bool = True
task_deps_source: str = "db"
# ORCH-088 (Этап 1, serial e2e): per-repo serial gate. A new task's analyst-job
# does NOT enter analysis (no branch cut, no analyst agent) while the same repo
# has another unfinished task (tasks.stage != 'done') OR the repo is frozen
# (repo_freeze). The gate lives in claim_next_job (offline-safe hot path, like
# the ORCH-026 dep-gate) + the branch cut is deferred from start_pipeline to the
# analyst-job claim (launcher) so the branch base is always a fresh origin/main
# that already contains the predecessor (anti-stale-base, AC-6). All additive,
# never-raise, restart-safe; STAGE_TRANSITIONS / QG_CHECKS unchanged. See
# docs/work-items/ORCH-088/06-adr/ADR-001-serial-gate.md.
# serial_gate_enabled -> kill-switch (env ORCH_SERIAL_GATE_ENABLED).
# False -> claim_next_job AND start_pipeline are 1:1
# as before ORCH-088 (clause omitted, branch cut in
# start_pipeline) — zero regression (AC-7).
# serial_gate_repos -> CSV scope (env ORCH_SERIAL_GATE_REPOS). Empty ->
# applies to ALL registered repos (D5); non-empty ->
# only the listed repos. Repo tokens are sanitised
# (^[A-Za-z0-9._-]+$) before being embedded in SQL.
# serial_gate_freeze_enabled-> independent tumbler for the FR-5 rollback-freeze
# layer (env ORCH_SERIAL_GATE_FREEZE_ENABLED). False
# -> freeze is neither set (post-deploy DEGRADED) nor
# consulted in the claim gate.
serial_gate_enabled: bool = True
serial_gate_repos: str = ""
serial_gate_freeze_enabled: bool = True
# ORCH-073 (ADR-001 Р-4): main-integrity regression guard. After the merge-verify
# under-gate confirms the deployed SHA is an ancestor of origin/main (FR-1), a
# secondary deterministic (no-LLM) guard checks that a declarative set of markers