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

# ADR-001: Merge-gate re-test — толерантность к инфра-таймауту, tree-kill спавненных процессов и контракт необходимости re-test

Work Item: **ORCH-110** — BUG: merge-gate local re-test timeout causes false rollback after green CI
Стадия: **architecture**
Сквозная регистрация: **`docs/architecture/adr/adr-0042-merge-gate-retest-infra-tolerance-and-tree-kill.md`**
(решение кросс-каттинговое: затрагивает merge-gate ORCH-043, coverage-gate ORCH-027 и
сквозной инвариант времени reaper ORCH-065/109).

## Статус
Proposed

## Контекст

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

**Инцидент (ORCH-109 / PR #129, факты сверены по коду):** tester `PASS` (полный регресс
`1899 passed` за `516.70s`), CI Gitea зелёный, PR `mergeable=true`, ветка не-behind — но merge-gate
re-test **упал по таймауту** (`re-test timeout after 600s`). На хосте — осиротевшие pytest-процессы
(`tests/test_install_lite_script.py`), жившие > 2 суток и грузившие CPU. Цепочка отказа: осиротевшие
процессы → CPU-голодание → сюит (516.7s, запас до 600s ≈ 16%) превысил бюджет →
`check_branch_mergeable` вернул `(False, "re-test timeout after 600s")` → `_handle_merge_gate`
маршрутизировал в `_handle_merge_gate_rollback` (откат `deploy-staging → development` +
developer-retry) → каждый из 3 retry падал по тому же CPU-голоданию → alert «Merge-gate still failing
after 3 developer retries» → задача застряла, потребовалось ручное вмешательство.

**Корни (подтверждены по коду):**
1. **Утечка осиротевших процессов.** `merge_gate.retest_branch` (`src/merge_gate.py:202`) и
   `coverage_gate.measure_coverage` (`src/coverage_gate.py:156`) запускают
   `subprocess.run([... pytest ...], timeout=...)` **без изоляции группы процессов**. При
   `TimeoutExpired` Python убивает только **прямого потомка** (`proc.kill()`); внуки pytest
   репарентируются на PID 1 (tini жнёт зомби, но не убивает живых сирот) и живут сутками. Это
   источник CPU-голодания.
2. **Нет толерантности к инфра-таймауту.** `_handle_merge_gate` (`src/stage_engine.py:967`)
   различает лишь `"merge-lock busy"` (→ defer) от всего остального (→ rollback). Re-test **таймаут**
   (инфра/ресурс) классифицируется идентично **красному** re-test (дефект кода) → откат на
   `development` + расход developer-retry, который разработчик не может «починить».
3. **Тонкий бюджет.** `merge_retest_timeout_s=600` практически равен фактическому времени сюита
   (516.7s); запас не растёт с сюитом.
4. **Контракт необходимости re-test.** При `premerge_rebase_always=True` (дефолт, ORCH-026 A-2)
   `check_branch_mergeable` (`src/qg/checks.py:705`) **всегда** ребейзит и пере-тестирует — даже на
   ветке, уже актуальной к `origin/main` (rebase — no-op). На таком HEAD локальный re-test
   пере-проверяет ровно тот коммит, что CI + tester + staging уже подтвердили, становясь избыточной
   единственной точкой ложного отказа. (Заметим: на пути `premerge_rebase_always=False` не-behind
   ветка re-test **уже пропускает** — `src/qg/checks.py:707-709`.)

Баг-карточка явно отложила выбор механизма архитектору: «Решение намеренно не описано; нужен
отдельный анализ вариантов и контрактов merge-gate» — основание эскалации `escalate: full-cycle`.

## Решение

### Сводка

Бьём по двум корням, одному контракту и одному бюджету — **аддитивно, под kill-switch, never-raise,
скоуп self-hosting**, сохраняя исходную защиту merge-gate от семантического конфликта:

- **D1 (root)** — единый leaf `src/proc_group.py` спавнит оркестратор-порождённые pytest-прогоны в
  **отдельной группе процессов** (`start_new_session`) и при таймауте/прерывании убивает **всё
  дерево** (`os.killpg`, каскад SIGTERM→grace→SIGKILL). Используют его `retest_branch` и
  `measure_coverage`. Контракты возврата сохранены — меняется лишь побочный эффект (нет сирот).
- **D2 (классификация)** — чистый предикат `merge_gate.classify_retest_failure(reason)` различает
  `timeout`/`red`/`lock-busy`/`other` без смены имени/семантики `check_branch_mergeable`.
- **D3 (маршрутизация)** — инфра-таймаут → новый `_handle_merge_gate_infra_retry` (ограниченный
  повтор/defer по образцу `_handle_merge_gate_defer`, **без** отката на `development` и **без**
  расхода developer-retry); красный re-test → прежний `_handle_merge_gate_rollback`.
- **D4 (контракт re-test)** — re-test исполняется **тогда и только тогда**, когда rebase реально
  сдвинул HEAD (`main` уехал); no-op rebase (ветка уже актуальна) **пропускает** локальный re-test,
  ровно как уже делает путь `premerge_rebase_always=False` для не-behind ветки.
- **D5 (бюджет)** — `merge_retest_timeout_s` 600 → **900** (запас 74% над 516.7s) с валидацией и
  проверкой сквозного инварианта reaper/lease — **без** изменения `reaper_max_running_s`.
- **D6 (наблюдаемость)** — счётчики в `merge_gate`, блок `merge_gate` в `GET /queue`, отдельный
  инфра-alert, отличимый от код-фейла; координация с ORCH-111 без дубля.

### D1 — Process-group изоляция + tree-kill спавненных pytest [FR-2 / BR-3 / AC-4]

Новый **leaf-модуль** `src/proc_group.py` (stdlib-only, never-raise, импортирует только `os`/`signal`/
`subprocess`/`time`/`logging` — НЕ другие `src/*`, по образцу чистоты `serial_gate`/`staging_verdict`):

```python
def run_in_process_group(
    cmd: list[str], *, cwd: str, timeout: float, env: dict | None = None,
    grace_s: float = 5.0, tree_kill: bool = True,
) -> ProcResult:   # ProcResult(returncode:int|None, stdout:str, stderr:str, timed_out:bool)
```

Механика (POSIX):
1. `proc = subprocess.Popen(cmd, cwd=cwd, env=env, stdout=PIPE, stderr=PIPE, text=True,
   start_new_session=True)` — `start_new_session=True` делает потомка лидером новой сессии/группы
   (`setsid`), поэтому все его потомки (xdist-воркеры, подпроцессы тестов) разделяют один `pgid ==
   proc.pid`.
2. `proc.communicate(timeout=timeout)` — ждём в бюджете.
3. На `subprocess.TimeoutExpired` — **tree-kill группы** каскадом, зеркало `launcher.stop_process`,
   но по **группе**: `os.killpg(os.getpgid(pid), SIGTERM)` → poll `grace_s` → если жива
   `os.killpg(..., SIGKILL)`; затем обязательный `proc.communicate()`/`proc.wait()` (reap), чтобы не
   оставить зомби. `ProcessLookupError` толерируется на каждом шаге.
4. Возвращает `timed_out=True` при таймауте; иначе `returncode`/stdout/stderr.

**Fallback (never-break):** при `tree_kill=False` ИЛИ платформе без `os.killpg`/`start_new_session`
(`not hasattr(os, "killpg")`) — деградирует на прежний `subprocess.run(cmd, ..., timeout=timeout)`
(байт-в-байт прежнее поведение, прод-Linux на это не попадает).

**Интеграция:**
- `retest_branch` зовёт `run_in_process_group([... pytest target -q], cwd=wt, timeout=<resolved>,
  tree_kill=settings.subprocess_tree_kill_enabled, grace_s=settings.agent_kill_grace_seconds)`;
  маппинг исхода 1:1 прежнему: `timed_out → (False, "re-test timeout after <T>s")`,
  `returncode==0 → (True, "re-test green")`, иначе `(False, "re-test failed: ...<tail>")`. **Контракт
  возврата сохранён** (FR-2) — меняется лишь отсутствие утечки.
- `measure_coverage` зовёт тот же helper для `pytest --cov`; исход маппится как сейчас (таймаут/ошибка
  → `None`, иначе чтение `--cov-report=json`). Грантия tree-kill — сиблинг-источник утечки закрыт
  (BR-3).

Грейс берётся из существующего `agent_kill_grace_seconds` (новый ключ не вводим — минимизация
конфига); для subprocess-pytest грейс короткий и без необходимости «флашить артефакты».

Kill-switch `subprocess_tree_kill_enabled` (дефолт `True`). Это **глобальная** гигиена процессов (не
гейт-решение), поэтому без `*_repos`-скоупа; на практике оба call-site исполняются лишь для
merge_gate/coverage-репо (self-hosting).

### D2 — Классификация исхода re-test [FR-1 / AC-2 / TC-03]

Чистый предикат в `src/merge_gate.py` (never-raise), единая точка «магической строки» вместо россыпи
`"timeout" in reason`:

```python
def classify_retest_failure(reason: str) -> str:
    # "timeout"  — re-test упёрся в бюджет (инфра/ресурс)
    # "red"      — детерминированно красный re-test (дефект кода)
    # "lock-busy"— merge-lock busy (контеншн леза)
    # "other"    — rebase conflict / setup error / прочее
```

`check_branch_mergeable` **не меняет** имя/семантику/PASS-FAIL контракт (NFR-1): он уже возвращает
различимый reason (`"re-test timeout after <T>s"` vs `"re-test failed after rebase: ..."` vs
`"merge-lock busy"` vs `"rebase conflict: ..."`). Меняется только **различение причины FAIL** на
слое маршрутизации.

**Скоуп классификации — строго re-test таймаут.** `auto_rebase_onto_main` имеет собственный
`"rebase timeout"` (git завис) — это **другой** инфра-таймаут, но без успешного rebase ветку нельзя
догнать до `main` → merge невозможен по существу. Он остаётся на прежнем rollback-пути (вне объёма
ORCH-110; консервативно). Документируем границу явно.

### D3 — Маршрутизация инфра-таймаута: ограниченный повтор + инфра-alert [FR-1 / FR-6 / NFR-5 / AC-1 / AC-9]

В `_handle_merge_gate` (`src/stage_engine.py`) после ветки `reason == "merge-lock busy"`:

```python
if (settings.merge_retest_infra_tolerance_enabled
        and merge_gate.classify_retest_failure(reason) == "timeout"):
    _handle_merge_gate_infra_retry(task_id, current_stage, repo, work_item_id, branch, reason, result)
    return True
_handle_merge_gate_rollback(...)   # red re-test / conflict — БЕЗ изменений (BR-6/AC-3)
```

Новый `_handle_merge_gate_infra_retry` — зеркало `_handle_merge_gate_defer` (НЕ rollback):
- Перекладывает staging-deployer обратно в очередь с задержкой `merge_retest_infra_retry_delay_s`
  (`enqueue_job("deployer", ..., available_at_delay_s=...)`); задача **остаётся на `deploy-staging`**.
  **Нет** `update_task_stage("development")`, **нет** инкремента developer-retry, **нет**
  `notify_qg_failure`-код-фейл-семантики.
- Счётчик повторов — restart-safe, по маркеру в `task_content` (`_merge_infra_retry_count`, зеркало
  `_merge_defer_count`: `... LIKE '%merge-gate infra-timeout retry%'`). Бюджет
  `merge_retest_infra_max_retries` (дефолт 2).
- **Лиз уже освобождён** `check_branch_mergeable` при таймауте (`src/qg/checks.py:721`) — на повторе
  гейт переacquire'ит штатно (как defer-путь). Согласовано с инвариантом леза.
- **Исчерпание (anti-loop, NFR-5):** `set_issue_blocked` + **отдельный инфра-alert** (Telegram с
  кликабельным `link_for` + Plane-коммент), формулировка **явно инфраструктурная**, отличная от
  «developer must fix»: *«Merge-gate re-test infra-timeout сохраняется после N повторов — ресурсная
  проблема (CPU/осиротевшие процессы), НЕ дефект кода. Нужно ручное вмешательство /
  проверьте хост.»* Задача **не** уходит в `development`.

**Ограниченность по времени (NFR-5/AC-9).** Каждый повтор — **отдельный** staging-deployer job со
своим agent-timeout и reaper-backstop; ни один прогон не превышает `reaper_max_running_s` сам по себе.
Суммарная стоимость худшего случая: `N × (delay + re-test ≤ timeout)` =
`2 × (120 + 900) ≈ 34 мин` до инфра-alert — конечно и наблюдаемо. После первого таймаута D1 уже убил
сирот, поэтому следующий re-test, как правило, проходит — повтор есть механизм восстановления, а не
маскировки.

**Скоуп.** Отдельный `*_repos` не нужен: путь достижим только когда merge-gate **реален**
(`_merge_gate_applies` → self-hosting/`merge_gate_repos`); для прочих репо гейт N/A → PASS → ветка
недостижима. Kill-switch off → таймаут идёт прежним rollback-путём (NFR-2, байт-в-байт).

### D4 — Контракт необходимости локального re-test [FR-4 / BR-5 / BR-6 / AC-6]

**Выбранный вариант — пропуск re-test при no-op rebase** (ветка уже содержит свежий `origin/main`).
ORCH-043 защищает от семантического конфликта, который возникает **только** когда `main` уехал и
ветка была реально ребейзнута на новые коммиты. Если rebase HEAD **не сдвинул** (ветка уже актуальна),
«уехавшего main» нет → re-test не даёт сигнала сверх уже пройденных `check_ci_green` + tester
`check_tests_passed` + staging на **этом же** HEAD → он избыточен.

Реализация в `check_branch_mergeable` (детерминированно, **offline**, без сетевого CI-запроса):
1. `pre = _head_sha(repo, branch)` (rev-parse HEAD в worktree, never-raise → `""`).
2. `auto_rebase_onto_main(...)` (как сейчас; конфликт → release lease → FAIL без изменений).
3. `post = _head_sha(repo, branch)`.
4. Если `merge_retest_skip_when_current_enabled` И `pre` и `post` непусты И `pre == post` (rebase —
   доказанный no-op) → **пропустить re-test**, вернуть
   `(True, "branch up-to-date (re-test skipped: rebase no-op, HEAD CI-validated)")`; лиз HELD.
5. Иначе (HEAD сдвинут / SHA не определить / флаг off) → `retest_branch` как сейчас.

**Почему без отдельного сетевого CI-запроса (отличие от варианта C).** HEAD на момент merge-gate при
no-op rebase — тот же коммит, что уже прошёл `check_ci_green` (ребро development→review), tester и
staging в этом же конвейере. «CI green for this HEAD» гарантирован **транзитивно** пройденным
конвейером; повторный сетевой запрос статуса — лишняя зависимость и хрупкость.

**Fail-safe к контракту (BR-6/AC-3):** при невозможности доказать no-op (любой `pre`/`post` пуст,
git-ошибка) — re-test **выполняется** (не пропускается на неопределённости). Когда re-test
**исполняется** и красный → прежний rollback. Послабление применяется ТОЛЬКО к доказанному no-op, и
только к *пропуску*, не к красному вердикту. Это симметрично уже существующему пропуску re-test на
не-behind ветке при `premerge_rebase_always=False` — D4 лишь распространяет ту же оптимизацию на
no-op-rebase случай `premerge_rebase_always=True`, приводя оба режима к согласованному правилу:
«локальный re-test гоняется лишь когда ветка реально догоняла уехавший `main`».

Kill-switch `merge_retest_skip_when_current_enabled` (дефолт `True`); off → re-test после rebase
всегда (прежнее `premerge_rebase_always` поведение).

### D5 — Согласование бюджета re-test [FR-3 / BR-4 / AC-5 / NFR-6]

`merge_retest_timeout_s`: **600 → 900** (запас 74% над наблюдаемыми 516.7s; против прежних ≈16%).
Бюджет — теперь **третья** линия защиты (D1 убирает корень CPU-голодания, D3 толерирует редкий
остаточный таймаут, D4 резко сокращает частоту re-test), поэтому большой бамп не нужен; умеренные 900
дают runway под рост сюита, оставаясь в сквозном инварианте reaper **без** правки
`reaper_max_running_s`.

**Валидация (never-break):** `retest_branch` резолвит бюджет через новый
`_resolve_retest_timeout()` — `int(settings.merge_retest_timeout_s)` если `> 0`, иначе дефолт **900**
+ WARNING (зеркало `launcher._resolve_timeout`). Малформ/непозитив больше не уходит в `subprocess`.

**Проверка сквозного инварианта (NFR-6, AC-5).** Re-test/coverage исполняются в монитор-потоке
**staging-deployer**-джоба (агент deployer уже завершён; `advance_stage` post-agent), поэтому суммарная
работа джоба считается против `reaper_max_running_s`. Worst-case для deploy-staging-джоба:

| Слагаемое | Бюджет, s |
|-----------|-----------|
| deployer-агент (deploy-staging, `agent_timeout_seconds`) | 1800 |
| security-scan (gitleaks+pip-audit) | ~120 |
| rebase (`_REBASE_TIMEOUT`) | 120 |
| **re-test (`merge_retest_timeout_s`, новый)** | **900** |
| coverage (`coverage_run_timeout_s`) | 900 |
| image-freshness rebuild | ~600 |
| grace | 20 |
| **Σ** | **≈ 4460** |

`reaper_max_running_s = 5400 > 4460` ✓ (запас ~940s). Инвариант ORCH-065/109
`reaper_max_running_s > max(agent_timeout)+grace` (5400 > 3600+20 для developer-джоба) сохранён.
`merge_lock_timeout_s = 300` (TTL леза) **меньше** держания леза на время re-test+coverage — но это
**существующее** свойство (ORCH-043: лиз держится от гейта до merge, дольше TTL; реклейм безопасен
holder-aware и pid-aware, ORCH-065) и **не** ухудшается ORCH-110 (re-test 900 vs прежние 600 — +300s
держания, всё ещё покрыто pid-liveness реклеймом, который не трогает живого держателя). Поэтому
`reaper_max_running_s` **не меняем**; D5 — только бамп `merge_retest_timeout_s` + валидация.

> Если оператор поднимет `merge_retest_timeout_s` env'ом выше — обязан соблюсти
> `reaper_max_running_s > Σ(deploy-staging gate-work) + grace` (таблица выше); зафиксировано в
> `07-infra-requirements.md`.

### D6 — Наблюдаемость и координация с ORCH-111 [FR-6 / BR-7 / AC-9 / R-4]

- **Счётчики** in-process в `merge_gate.py` (`_MERGE_GATE_COUNTERS`, образец `_MERGE_VERIFY_COUNTERS`):
  `retest_timeout_total`, `retest_infra_retry_total`, `retest_infra_exhausted_total`,
  `retest_skipped_current_total`, `last_infra_timeout_wi`. Снимок `merge_gate_status()` → read-only
  блок `merge_gate` в `GET /queue` (флаги/счётчики). Только информация, никогда не источник решения.
- **Логи** — раздельные строки для infra-retry vs code-fail rollback; INFO на пропуск re-test (D4).
- **Telegram/Plane** — инфра-alert на исчерпании (D3), формулировка инфраструктурная (не «developer
  must fix») с кликабельным номером.
- **Координация с ORCH-111 (`proc_blocking`, adr-0041) — без дубля (R-4):** ORCH-110
  **предотвращает/толерирует** (tree-kill у источника + транзиент-маршрут), ORCH-111 **наблюдает**
  (sidecar алертит на пережившие осиротевшие тест-процессы). ORCH-110 не вводит детектор процессов;
  он убирает сирот у источника. Разные слои — нулевое пересечение.

### Затронутые маркеры / трассировка (ORCH-078)

Правки касаются блоков с маркерами ORCH-043 (`retest_branch`, `check_branch_mergeable`, lease),
ORCH-026 (`premerge_rebase_always`), ORCH-027 (`measure_coverage`), ORCH-065/109 (инвариант
reaper/timeout). 3+ маркера в `merge_gate.py` → опираемся на **сводный сквозной**
`adr-0042` (этот пакет) + `adr-0006`/`adr-0040`. Инварианты ORCH-043/071/073/093 (лиз holder-aware,
INV-4 «никогда push/force-push main», never-raise, fail-open/closed классификации) **не нарушаются**:
ORCH-110 не трогает merge-актор/верификатор пути `deploy → done`.

**Директива developer'у (append-only regression-guard, ORCH-073):** дописать в
`merge_gate.MAIN_REGRESSION_MARKERS` строку `("ORCH-110", "classify_retest_failure",
"src/merge_gate.py")`, чтобы новый инвариант был защищён от фантомного отката.

## Альтернативы

- **Только поднять бюджет (без D1).** Отвергнуто (R-2): не бьёт корень — растущий сюит снова упрётся;
  сироты продолжат грузить CPU. Бюджет — вторичная мера.
- **Tree-kill через `subprocess.run` + ручной `Popen` в каждом модуле (без общего leaf).** Отвергнуто:
  дублирование хрупкой `killpg`-логики в двух местах → дрейф. Один тестируемый leaf
  (`proc_group.py`) — single source of truth.
- **Вариант C: trust-CI-SHA (сетевой запрос статуса CI HEAD → skip re-test).** Отвергнуто как контракт
  по умолчанию: вводит сетевую зависимость в детерминированный offline-гейт и ослабляет
  семантик-конфликт-гард (CI бежал на базе ветки, не на результате слияния). D4 (no-op-rebase skip)
  даёт ту же выгоду детерминированно и offline.
- **Толерировать ЛЮБОЙ FAIL merge-gate (включая красный) как транзиент.** Отвергнуто (BR-6/AC-3,
  R-1): сломает цель ORCH-043 — красный re-test обязан откатывать. Послабление строго к таймауту.
- **Новый зарегистрированный QG / новая стадия для инфра-ретрая.** Отвергнуто (NFR-1): под-гейт —
  врезка в `advance_stage`; `STAGE_TRANSITIONS`/`QG_CHECKS` неприкосновенны.
- **Init-process / убийца сирот на хосте.** Отвергнуто: реинтродукция привилегий/хост-доступа;
  предотвращение у источника (D1) проще и self-hosting-безопасно; наблюдение оставлено ORCH-111.

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

- **+** Зелёный путь (tester PASS + CI green + актуальная ветка) больше не ловит ложный откат/manual-gate
  из-за инфра-таймаута (BR-1/AC-1). Сироты не переживают бюджет (BR-3/AC-4). Re-test не гоняется
  избыточно на актуальной ветке (BR-5/AC-6), что само снижает шанс таймаута.
- **+** Красный re-test по-прежнему откатывает (BR-6/AC-3); инварианты конвейера/леза/INV-4
  неприкосновенны (NFR-1/NFR-3/AC-8).
- **−** Инфра-таймаут добавляет до `N×(delay+timeout)` (~34 мин) к худшему случаю перед инфра-alert
  (вместо мгновенного, но ложного, отката). Митигейшн: D1 устраняет первопричину → повтор обычно
  проходит первым; бюджет повторов мал (2).
- **−** +5 конфиг-ключей + бамп бюджета. Митигейшн: дефолты = желаемое прод-поведение (ORCH-101
  канон), каждый под kill-switch.
- **−** D4 пропускает re-test на no-op rebase: теоретический пропуск семантик-конфликта, не
  поймать который CI/tester/staging уже не смогли бы. Митигейшн: пропуск **только** при доказанном
  no-op (нет «уехавшего main» → нет нового класса конфликта); на любой неопределённости — re-test
  бежит (fail-safe).
- **Откат:** `subprocess_tree_kill_enabled=False` (→ прежний `subprocess.run`),
  `merge_retest_infra_tolerance_enabled=False` (→ таймаут=rollback),
  `merge_retest_skip_when_current_enabled=False` (→ всегда re-test после rebase),
  `merge_retest_timeout_s=600` (→ прежний бюджет). Каждый флаг независим; полный откат = вернуть 4
  значения → байт-в-байт до-ORCH-110.

## Ссылки
- BRD: `docs/work-items/ORCH-110/01-brd.md`
- TRZ: `docs/work-items/ORCH-110/02-trz.md`
- Acceptance: `docs/work-items/ORCH-110/03-acceptance-criteria.md`
- Test-plan: `docs/work-items/ORCH-110/04-test-plan.yaml`
- Инфра: `docs/work-items/ORCH-110/07-infra-requirements.md`
- Риски: `docs/work-items/ORCH-110/10-tech-risks.md`
- Сквозной ADR: `docs/architecture/adr/adr-0042-merge-gate-retest-infra-tolerance-and-tree-kill.md`
- Сверено по коду: `src/merge_gate.py` (`retest_branch`, `auto_rebase_onto_main`,
  `MAIN_REGRESSION_MARKERS`), `src/coverage_gate.py` (`measure_coverage`),
  `src/qg/checks.py` (`check_branch_mergeable`, `_merge_gate_applies`),
  `src/stage_engine.py` (`_handle_merge_gate`, `_handle_merge_gate_defer`,
  `_handle_merge_gate_rollback`, `_merge_defer_count`),
  `src/config.py` (`merge_retest_timeout_s`, `reaper_max_running_s`, `agent_kill_grace_seconds`),
  `src/agents/launcher.py` (`stop_process` — образец каскада),
  `src/job_reaper.py` (`reaper_max_running_s` backstop)
- Смежные: `adr-0006` (merge-gate ORCH-043), `adr-0040` (timeout-бюджеты ORCH-109),
  `adr-0029` (coverage-gate ORCH-027), `adr-0011` (reaper/lease ORCH-065),
  `adr-0041` (ORCH-111 `proc_blocking` — комплементарный наблюдатель)
</content>
</invoke>
