# 01 — BRD: ORCH-082 (ORCH-81)

**Конвейер не создаёт PR для ветки → деплой стопорится на merge-verify (HOLD)**

- Work Item: **ORCH-082** (Plane-заголовок «ORCH-81»)
- Repo: `orchestrator` (self-hosting)
- Тип: **Багфикс / надёжность конвейера**
- Приоритет: **HIGH** — блокирует автономный деплой
- Зона: создание PR (reviewer/developer/deployer пути), `src/merge_gate.py`, `src/stage_engine.py` (`_handle_merge_verify`), `src/agents/launcher.py` (`_ensure_pr`)

---

## 1. Контекст и проблема

При деплое **ORCH-074** (08.06, статус «Confirm Deploy») детерминированный finalizer
(`run_deploy_finalizer` → под-гейт `_handle_merge_verify`) вызвал
`merge_gate.merge_pr(repo, branch)` и получил **`ok=False` («no open PR»)**: в Gitea для
ветки `feature/ORCH-074-…` **не существовало открытого PR** с `head.ref==branch` и
`base.ref=="main"`.

Защита **ORCH-073** (fail-closed по «SHA-в-main») отработала **корректно**: задача удержана
на стадии `deploy` (НЕ `done`), Plane → Blocked, Telegram-alert, ложно-зелёного `done` не
произошло. Это **правильное** поведение для случая «merge реально невозможен».

**Дефект не в защите, а в инварианте до неё:** автономный конвейер **не гарантировал**, что к
моменту merge у ветки существует открытый PR. PR на сегодня создаётся ровно в одном месте —
`launcher._ensure_pr`, вызываемом **только** на пути `agent == "developer"` и **только** когда
в этом конкретном run был непустой git-diff, успешный commit и успешный push (см. root-cause
ниже). Любой сценарий, где developer-run не произвёл свежий коммит, оставляет ветку **без PR**,
и задача неминуемо застревает на merge-verify.

### Workaround, применённый вручную (НЕ фикс)
PR #79 создан вручную через Gitea API (`mergeable=True`) → штатно перезапущен
`run_deploy_finalizer` → `merge_pr` честно влил код в `main` → задача `done`. Это разовое ручное
вмешательство, **не** устранение причины.

### Почему это системный пробел, а не разовый сбой
Так как создание PR **не гарантировано конвейером**, любая следующая задача с тем же стечением
обстоятельств (developer-run без нового коммита; тихо упавший вызов создания PR; ветка
восстановлена/пересоздана вручную) застрянет на merge-verify тем же образом. Автономность
деплоя (цель ORCH-54) этим заблокирована.

---

## 2. Root cause (предварительный аудит кода — подтвердить логами G1)

PR создаётся **исключительно** функцией `AgentLauncher._ensure_pr` (`src/agents/launcher.py`),
которая вызывается из `_monitor_agent` по цепочке условий:

```
exit_code == 0
  → есть worktree-изменения (git status --porcelain непусто)
    → git commit succeeded
      → git push succeeded
        → agent == "developer"   ←── ТОЛЬКО здесь вызывается self._ensure_pr(...)
```

Отсюда минимум три структурных способа остаться без PR:

- **R-A (условное создание).** Если developer-run завершился без изменений (`git status`
  пустой) — ветка уже была закоммичена/запушена в прошлый run, бойнс REQUEST_CHANGES без новых
  правок, повторный прогон, или ручное восстановление ветки — `_ensure_pr` **не вызывается
  вовсе**. PR не появится никогда. (Соответствует гипотезе ТЗ №2.)
- **R-B (тихий сбой создания).** `_ensure_pr` ловит любое исключение
  (`except Exception → logger.error → return None`): транзиентная ошибка Gitea на шаге
  `POST …/pulls` теряется без ретрая и без эскалации. Конвейер «думает», что developer
  отработал, и едет дальше. (Гипотеза ТЗ №1 — silent fail.)
- **R-C (разъехавшееся состояние ветки/PR).** ORCH-074 — первая задача после серии ручных
  восстановлений `main` 08.06. PR мог быть закрыт/пересоздан, либо у ветки остался только
  авто-docs-PR (`base != main`), который `merge_pr`/`pr_already_merged` корректно НЕ считают
  кодовым PR. (Гипотеза ТЗ №4.)

Идемпотентность (гипотеза №3): сам `_ensure_pr` идемпотентен на чтении (сначала `GET …open&head`,
создаёт только если пусто), но он не запускается вне «свежий developer-коммит», поэтому
идемпотентность не достигает merge-стадии — никакой флаг «PR создан» в БД не хранится.

**Вывод:** гарантия «к моменту merge у ветки есть открытый код-PR» в конвейере **отсутствует**.

---

## 3. Бизнес-цели

| ID | Цель |
|----|------|
| **G1** | Установить и задокументировать точную причину отсутствия PR на ORCH-074 (код-аудит + логи run_id 396/398). |
| **G2** | Гарантировать инвариант: к моменту merge-verify у ветки **есть** открытый код-PR; если его нет — finalizer/deployer создаёт его сам, **идемпотентно**, ПЕРЕД `merge_pr`, вместо HOLD на ручное вмешательство. |
| **G3** | Явно логировать факт PR: **PR-created / PR-existed / PR-create-failed** (наблюдаемость). |

## 4. Не-цели (явные границы)

- НЕ ослаблять защиту ORCH-073: fail-closed по «SHA-в-main» остаётся. Реальная невозможность
  merge → по-прежнему HOLD + alert.
- НЕ авто-мержить без PR (PR — обязательный артефакт ревью/слияния).
- НЕ создавать PR в неподходящий момент — только на ребре `deploy → done`, ПОСЛЕ прохождения
  всех гейтов (security/merge-gate/staging/image-freshness уже пройдены).
- НЕ менять `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, схему БД, контракты `check_deploy_status`,
  exit-коды хука.

## 5. Заинтересованные стороны
- **Owner** (homenet542) — автономность деплоя орка.
- Все проекты на инстансе (enduro-trails) — общий прод/очередь: ложный HOLD self-задачи не
  должен требовать ручного вмешательства, а реальный дефект merge — обязан удерживаться.

## 6. Бизнес-риски и допущения
- **Грабли (из ORCH-073):** у ветки может быть несколько PR (код-PR + авто docs-PR). Создание/
  выбор PR обязан фильтровать `head.ref==branch` И `base.ref=="main"`, иначе слияние/верификация
  схватят не тот PR.
- **Допущение:** merge-verify исполняется ПОСЛЕ всех гейтов, поэтому создание PR именно здесь не
  обходит ревью и безопасно по времени.
- **Контракт надёжности:** весь новый путь — **never-raise**; ошибка создания PR (Gitea
  недоступна) → честный HOLD + alert, а не исключение в `advance_stage`.

## 7. Definition of Done (бизнес-уровень)
1. Root cause задокументирован (`06-adr/` архитектором, ссылка из ADR на этот BRD).
2. После фикса задача с веткой без PR не зависает: конвейер создаёт PR идемпотентно и доводит до
   `done` (при честном merge).
3. Защита ORCH-073 цела (регресс-тест на «код не в main» → HOLD).
4. Логи различают created/existed/failed.
5. `pytest` зелёный; never-raise соблюдён.
