---
work_item: ORCH-093
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-09
model_used: claude-opus-4-8
---

# adr-0027: Merge-актор — ретрай транзиентных ошибок Gitea + гард «ветка уже в `main`»

Сквозной (cross-cutting) ADR. **Амендмент** к [adr-0013](adr-0013-merge-verify-gate.md) (merge-verify
под-гейт), [adr-0014](adr-0014-merge-verify-sha-source-of-truth.md) (SHA-в-main как источник истины)
и [adr-0016](adr-0016-ensure-open-pr-before-merge-verify.md) (гарантированный код-PR). Детальное
решение задачи — `docs/work-items/ORCH-093/06-adr/ADR-001-merge-transient-retry-and-already-in-main-guard.md`.

> Регистрируется как сквозной, т.к. правит блок merge-актора с **3+ маркерами** (`ORCH-071`,
> `ORCH-073`, `ORCH-082`) — анти-археология маркеров (`docs/_standards/TRACEABILITY.md`): сводный
> ADR агрегирует эволюцию вместо перечисления work item в коде.

## Статус
Proposed

## Контекст

Детерминированный merge-актор merge-verify под-гейта (`deploy → done`, self-hosting) состоит из
`ensure_open_pr` → `merge_pr` → `verify_merged_to_main` (`src/merge_gate.py`). Инцидент **ORCH-063
(09.06)** вскрыл два дефекта, оба сверены по коду прода:

1. `merge_pr` — **one-shot**: `POST /pulls/{index}/merge`, любой не-`200/201` → мгновенный `False`.
   Транзиентная икота Gitea (`405 "Please try again later"` при пересчёте `mergeable` сразу после
   пуша; `5xx`; таймаут) → ложный HOLD защиты ORCH-071/073 → ручной домерж.
2. `ensure_open_pr` — после ручного мержа код-PR `closed`, открытый не найден → создаёт **новый
   пустой PR** на ветке, уже целиком в `main`.

Защита ORCH-071/073 («deploy succeeded but not merged») корректна и сохраняется; задача снижает
лишь **ложные** срабатывания на транзиентах и устраняет мусорные PR. Это блокер автономного прогона
(эпик ORCH-088).

## Решение

Аддитивно, без правки `STAGE_TRANSITIONS` / `QG_CHECKS` / схемы БД; INV-4 (мерж только через Gitea
PR-merge API; никогда `push`/`force-push` в `main`) и never-raise сохранены.

- **Ретрай-loop вокруг `POST …/merge`** (только мутирующий вызов) до `merge_retry_max_attempts`
  (дефолт 3) с экспоненциальным backoff и потолком (`base 2`, `max 5`; суммарно ≤10 с). Классификатор
  **транзиент** (`405`/`408`/`5xx`/таймаут/сетевое; `409`/`422` при `mergeable==True`; `mergeable==None`
  → транзиент-по-дефолту в рамках бюджета) vs **терминал** (`403`/`404`; `409`/`422` при
  `mergeable==False`) — по коду ответа **и** полю `mergeable` (`GET /pulls/{index}`). Терминал →
  быстрый честный `False` (защита ORCH-071/073 — как прежде). Образец — `check_ci_green`
  (`attempt i/N`) + transient-breaker агентов.
- **Гард already-in-main в `ensure_open_pr`**: перед созданием PR — `git merge-base --is-ancestor
  <branch> origin/main` (rc==0 → ветка целиком в `main`) → новый исход `"already-in-main"`, PR не
  создаётся; git-ошибка/ambiguous → **fail-OPEN** на текущий create-путь (гард не должен превратить
  икоту git в ложный no-op мержа). `_handle_merge_verify` трактует `"already-in-main"` как «мержить
  нечего» → пропуск `merge_pr` → авторитетный SHA-в-main (`verify_merged_to_main`, ADR-0014) доводит
  до `done` без мусорного PR.
- **Конфиг**: `merge_retry_enabled` (kill-switch; `False` → one-shot, нулевая регрессия),
  `merge_retry_max_attempts`, `merge_retry_backoff_base_s`, `merge_retry_backoff_max_s`
  (env `ORCH_MERGE_RETRY_*`). Гард already-in-main — без отдельного флага (накрыт существующим
  `merge_verify_autocreate_pr_enabled`).

Объём раската — реально только self-hosting (`merge_verify_applies`); на прочих репо мерж делает
LLM-deployer → изменение нейтрально.

## Последствия

- **+** Транзиент Gitea переживается автоматически → нет ложного HOLD / ручного домержа в автономном
  конвейере; нет мусорных пустых PR; повтор финализатора идемпотентен.
- **+** Реальный конфликт → быстрый честный HOLD; защита ORCH-071/073 и SHA-в-main (ADR-0014) —
  авторитетны и неизменны.
- **−** Дефолт `mergeable==None → transient` может добавить ≤10 с до HOLD на реальном конфликте
  (бюджет жёстко ограничен); один лишний `GET /pulls/{index}` в редком ambiguous-кейсе.
- **Откат:** `ORCH_MERGE_RETRY_ENABLED=false` → one-shot; `ORCH_MERGE_VERIFY_AUTOCREATE_PR_ENABLED=false`
  → отключает врезку `ensure_open_pr` с гардом. Полный откат — revert PR.

## Ссылки
- Детальный ADR: `docs/work-items/ORCH-093/06-adr/ADR-001-merge-transient-retry-and-already-in-main-guard.md`
- Лехатая: [adr-0006](adr-0006-merge-gate.md), [adr-0013](adr-0013-merge-verify-gate.md),
  [adr-0014](adr-0014-merge-verify-sha-source-of-truth.md),
  [adr-0016](adr-0016-ensure-open-pr-before-merge-verify.md)
- Код: `src/merge_gate.py`, `src/stage_engine.py::_handle_merge_verify`, `src/config.py`
