---
work_item: ORCH-110
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-15
model_used: claude-opus-4-8
escalate: full-cycle
---

# 01 — BRD (бизнес-требования): ORCH-110 — BUG: merge-gate local re-test timeout causes false rollback after green CI

Work Item: **ORCH-110** · Repo: **orchestrator** · Стадия: analysis

> **Багфикс-трек → эскалация в полный цикл (`escalate: full-cycle`).** Задача помечена `Bug`, но
> сама баг-карточка требует «отдельный анализ вариантов и контрактов merge-gate» (см. «Ограничение»
> ниже) — это решение с несколькими проектными альтернативами и нетривиальными инвариантами
> self-hosting, которому нужен ADR. По правилу ORCH-019 (ADR-001 D5) выпускается **полный**
> analysis-пакет, а трек эскалируется (`POST /bug-fast-track/escalate?work_item=ORCH-110`) → задача
> проходит стадию `architecture`. Прецедент — родственная задача ORCH-111 («bug → escalate
> full-cycle»).

---

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

Оркестратор — self-hosting инструмент: его прод-контейнер обслуживает конвейер всех проектов и
дорабатывает сам себя. На ребре `deploy-staging → deploy` исполняется детерминированный под-гейт
**merge-gate** (`check_branch_mergeable`, ORCH-043): он догоняет ветку до текущего `origin/main`
(`auto_rebase_onto_main`) и затем **локально пере-прогоняет весь тест-сюит** (`retest_branch` →
`python -m pytest tests/ -q`) в worktree, чтобы поймать **семантический** конфликт слияния (ветка
зелёная по своей базе, но ломает уехавший `main`).

**Установленные факты инцидента (ORCH-109, PR #129):**
- tester завершился `result: PASS`; полный регресс — **`1899 passed` за `516.70s`**;
- CI Gitea по HEAD — зелёный (push + pull_request success); PR после rebase — open, `mergeable=true`;
- merge-gate локальный re-test упал по **таймауту**: `re-test timeout after 600s`
  (`merge_retest_timeout_s = 600`);
- на хосте обнаружены **старые зависшие pytest-процессы** `tests/test_install_lite_script.py`,
  жившие **> 2 суток** и грузившие CPU; прибиты вручную 2026-06-14.

**Цепочка отказа.** Зависшие осиротевшие pytest-процессы (CPU-голодание) → тот же сюит, что у tester
шёл 516.70s (запас до 600s ≈ 16%), под нагрузкой превысил 600s → `check_branch_mergeable` вернул
`(False, "re-test timeout after 600s")` → `_handle_merge_gate_rollback`: откат `deploy-staging →
development` + developer-retry. Каждый из 3 retry повторно падал по тому же CPU-голоданию → финальный
alert **«Merge-gate still failing after 3 developer retries (re-test timeout after 600s)»** → задача
застряла, потребовалось ручное вмешательство.

**Корень (подтверждён по коду):**
1. **Утечка осиротевших процессов.** `merge_gate.retest_branch` и `coverage_gate.measure_coverage`
   запускают `subprocess.run([... pytest ...], timeout=...)` **без изоляции группы процессов**
   (`start_new_session`/`preexec_fn`). При `TimeoutExpired` Python убивает только **прямого
   потомка**; внуки pytest репарентируются на PID 1 (tini жнёт зомби, но не убивает живых сирот) и
   живут сутками, грузя CPU. Это источник CPU-голодания (ровно симптом из фактов).
2. **Нет толерантности к инфра-таймауту.** Re-test **таймаут** (ресурсная/инфра-причина)
   классифицируется идентично **красному** re-test (реальный дефект кода): оба → откат на
   `development` + расход developer-retry. Разработчик не может «починить» CPU-голодание → retry
   сгорают вхолостую и упираются в alert «Manual intervention needed».
3. **Тонкий бюджет.** Бюджет re-test `600s` практически равен фактическому времени сюита
   (`516.70s`); запас не растёт вместе с сюитом (ср. ORCH-109, где по той же причине были подняты
   бюджеты агентов developer/reviewer).
4. **Контракт необходимости re-test.** На ветке, уже актуальной к `origin/main` (rebase — no-op), и
   с зелёным CI по этому же HEAD локальный полный re-test пере-проверяет ровно тот коммит, что CI
   уже подтвердил, — становясь избыточной единственной точкой ложного отказа.

## 2. Объём (scope)

### В объёме
- Поведение merge-gate при **таймауте** локального re-test: классификация и путь восстановления
  (толерантность к инфра-таймауту вместо ложного отката на `development`).
- **Жизненный цикл подпроцессов**, которые оркестратор запускает САМ для проверок: re-test merge-gate
  (`merge_gate.retest_branch`) и coverage-run (`coverage_gate.measure_coverage`) — гарантия отсутствия
  осиротевших процессов после таймаута/kill.
- **Согласованность бюджета** re-test с фактическим временем полного сюита (адекватный запас) с учётом
  сквозных инвариантов reaper/lease.
- **Контракт необходимости** локального re-test merge-gate (когда он реально нужен относительно
  зелёного CI и состояния `branch vs origin/main`) — анализ вариантов под решение архитектора.
- Наблюдаемость инфра-таймаута (отличить «инфра-таймаут, повтор/defer» от «дефект кода → developer»).

### Вне объёма
- **Алерт sidecar-watchdog на осиротевший тест-процесс** — это **ORCH-111** (`proc_blocking`,
  наблюдатель только сигналит, никогда не убивает, C-1). ORCH-110 — комплементарная сторона
  (предотвращение утечки + толерантность), дубля детекции не вводит.
- Ручное умерщвление уже зависших хост-процессов — операционная мера (выполнена 2026-06-14), не код.
- Любые правки `STAGE_TRANSITIONS` / реестра `QG_CHECKS` / `check_*`-семантики / machine-verdict
  ключей / схемы БД (инвариант NFR-1).
- Изменение конкретного теста `tests/test_install_lite_script.py` (его поведение — отдельный предмет;
  здесь важен класс «оркестратор-спавненный pytest не должен переживать свой бюджет»).
- Поведение не-self-hosting репозиториев (enduro-trails) — нулевая регрессия.
- Изменение хука прод-деплоя/рестарт прод-контейнера (self-hosting безопасность).

## 3. Заинтересованные стороны
- **Owner / оператор self-hosting** — страдает от ручного разбора застрявших задач и зависших
  процессов; заказчик исправления.
- **Конвейер всех проектов** — общий прод-контейнер: утечка CPU деградирует обслуживание enduro.
- **Пакетный автономный режим (эпик ORCH-088)** — ложные откаты и manual-gate'ы ломают цель
  «10–20 задач за ночь без вмешательства».
- **Принимает результат:** reviewer → tester → deployer штатного конвейера.

## 4. Бизнес-требования (BR)

- **BR-1 — Зелёный путь без ручного вмешательства.** При зелёном tester `PASS` и зелёном CI задача
  **не должна** требовать ручного вмешательства из-за инфраструктурного/локального re-test таймаута
  (прямое «Ожидаемое поведение» баг-карточки).
- **BR-2 — Инфра-таймаут ≠ дефект кода.** Таймаут локального re-test merge-gate (ресурсная/инфра
  причина) **не должен** трактоваться как код-фейл: путь восстановления **не** должен сжигать
  developer-retry и приводить к «Manual intervention after N developer retries», если CI и tester
  были зелёными. Реакция на таймаут — ограниченный повтор/defer и/или отдельный инфра-сигнал, не
  безусловный откат на `development`.
- **BR-3 — Нет осиротевших процессов.** Подпроцессы pytest, запущенные самим оркестратором для
  re-test и coverage-run, **должны** полностью завершаться (всё дерево, включая внуков) при
  таймауте/kill. Ни один оркестратор-спавненный pytest не должен переживать свой бюджет и продолжать
  грузить CPU.
- **BR-4 — Адекватный бюджет re-test.** Бюджет времени re-test **должен** иметь достаточный запас над
  фактическим временем полного сюита, чтобы здоровый сюит при штатной нагрузке не падал по таймауту;
  бюджет конфигурируем и со-эволюционирует с ростом сюита.
- **BR-5 — Контракт необходимости re-test.** Merge-gate **должен** различать «ветка реально отстала
  от уехавшего `origin/main` и была ребейзнута» (риск семантического конфликта → re-test оправдан) и
  «ветка уже актуальна / rebase — no-op, CI по этому HEAD зелёный» (re-test избыточен). Локальный
  re-test не должен быть избыточной единственной точкой ложного отказа на коммите, уже подтверждённом
  CI. Конкретный контракт (skip/scope/trust-CI-SHA) выбирает архитектор и фиксирует в ADR.
- **BR-6 — Сохранение защиты от семантического конфликта.** Толерантность к таймауту **не должна**
  ослаблять исходную цель merge-gate (ORCH-043): **детерминированно красный** re-test (реальный сбой
  теста, а не таймаут) по-прежнему обязан откатывать на `development`. Послабление применяется ТОЛЬКО
  к таймауту/инфра, никогда к красному результату.
- **BR-7 — Наблюдаемость.** Состояние «инфра-таймаут» должно быть видимым (лог + Telegram с
  кликабельным номером + read-only в `GET /queue`) и отличимым от код-фейл-отката; согласовано с
  сигналом ORCH-111 (без дубля).

## 5. Нефункциональные требования (NFR)

- **NFR-1 — Инварианты конвейера неприкосновенны.** `STAGE_TRANSITIONS` / реестр `QG_CHECKS` /
  семантика `check_*` / machine-verdict ключи (`verdict:`/`result:`/`deploy_status:`/
  `staging_status:`/`security_status:`/`coverage_status:`) / схема БД — **байт-в-байт** прежние.
  Исправление — аддитивное (врезка/leaf-логика), не новая стадия и не новый зарегистрированный QG.
- **NFR-2 — Kill-switch + нулевая регрессия.** Новое поведение под флагом; при выключенном флаге —
  поведение **байт-в-байт** как до ORCH-110 (таймаут → прежний откат). Скоуп — self-hosting
  (`orchestrator`); enduro не затронут.
- **NFR-3 — Self-hosting безопасность.** Исправление **никогда** не пушит/force-push в `main` (INV-4;
  merge только через Gitea PR-merge API), не рестартит прод-контейнер, не трогает detached-деплой.
- **NFR-4 — never-raise.** Любая ошибка в новом пути → безопасный дефолт + WARNING; исключение
  никогда не уходит в `advance_stage`/monitor-поток (контракт merge-gate сохранён).
- **NFR-5 — Ограниченность (anti-loop).** Любой повтор/defer таймаута строго ограничен по числу
  попыток и суммарному времени; исчерпание → **чёткий инфра-alert**, отличный от «developer must
  fix», а не бесконечный bounce и не молчаливое зависание.
- **NFR-6 — Сквозные инварианты времени.** Любое изменение бюджета re-test должно уважать
  существующие соотношения: `merge_lock_timeout_s` (TTL merge-lease), `reaper_max_running_s`
  (Tier-3 backstop reaper, ORCH-065/109), `coverage_run_timeout_s` — без рассинхрона.

## 6. Допущения и ограничения
- **Ограничение из баг-карточки (дословно):** «Решение намеренно не описано в этой баге; нужен
  отдельный анализ вариантов и контрактов merge-gate». → Аналитик фиксирует требования и
  тест-план; **варианты и контракт merge-gate** прорабатывает архитектор (06-adr) — основание
  эскалации в полный цикл.
- Допущение: tini (PID 1) жнёт зомби, но не убивает живых осиротевших процессов (подтверждено
  поведением инцидента) — отсюда требование tree-kill (BR-3).
- Допущение: таймаут merge-gate re-test в зелёном инциденте вызван внешним CPU-голоданием, а не
  реальным зависанием теста ветки; но решение обязано остаться **fail-safe** к случаю реального
  зависшего теста (см. Риск R-2 / BR-6).
- Среда верификации — staging-контур (8501), обязательная страховка перед прод-деплоем self.

## 7. Критерии успеха
Резюме: зелёный tester `PASS` + зелёный CI + актуальная ветка → задача доходит до `deploy` без
ложного отката на `development` и без manual-gate из-за инфра-таймаута; оркестратор-спавненные
pytest-процессы не переживают свой бюджет; реальный красный re-test по-прежнему откатывает на
`development`; инварианты конвейера и self-hosting не тронуты. Детальные PASS/FAIL —
`03-acceptance-criteria.md`.

## 8. Риски
- **R-1** — Над-толерантность маскирует реальный зависший тест (бесконечный/долгий) как «инфра» →
  смягчение: строгая ограниченность (NFR-5) + отдельный инфра-alert + сохранение красно-откат-пути
  (BR-6).
- **R-2** — Поднятие бюджета без правки tree-kill лишь отодвигает отказ (сюит растёт) → исправление
  должно бить корень (BR-3), бюджет (BR-4) — вторично.
- **R-3** — Рассинхрон сквозных таймаутов (reaper/lease) при изменении бюджета (NFR-6).
- **R-4** — Дубль/конфликт с сигналом ORCH-111 (`proc_blocking`) → координация: ORCH-110
  предотвращает/толерирует, ORCH-111 наблюдает; разные слои.
- Детальная оценка и митигации — `10-tech-risks.md` (заполняет архитектор).
