From cf602b481077a659d155d0d33b8323b7e265fbe3 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Mon, 15 Jun 2026 09:40:46 +0300 Subject: [PATCH] architect(ET): auto-commit from architect run_id=687 --- docs/architecture/README.md | 36 +- ...te-retest-infra-tolerance-and-tree-kill.md | 84 +++++ ...te-retest-infra-tolerance-and-tree-kill.md | 352 ++++++++++++++++++ .../ORCH-110/07-infra-requirements.md | 68 ++++ docs/work-items/ORCH-110/10-tech-risks.md | 40 ++ 5 files changed, 579 insertions(+), 1 deletion(-) create mode 100644 docs/architecture/adr/adr-0042-merge-gate-retest-infra-tolerance-and-tree-kill.md create mode 100644 docs/work-items/ORCH-110/06-adr/ADR-001-merge-gate-retest-infra-tolerance-and-tree-kill.md create mode 100644 docs/work-items/ORCH-110/07-infra-requirements.md create mode 100644 docs/work-items/ORCH-110/10-tech-risks.md diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 922d12a..da81ed3 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -447,13 +447,47 @@ Self-hosting зацикливался на `deploy-staging`: `scripts/staging_ch Назначение: ветка валидируется относительно того `main`, из которого создана; параллельная задача могла уйти вперёд → семантический конфликт слияния (зелёная ветка ломает обновлённый `main`). Merge-gate гарантирует проверку против **актуального** `origin/main` перед слиянием: - **Догон:** ветка отстаёт (⇔ `origin/main` не предок HEAD) → `rebase origin/main` в worktree + `push --force-with-lease` (ТОЛЬКО ветка задачи; `main` — никогда). Текстовый конфликт → `rebase --abort` → откат на `development`. - **Безусловный pre-merge rebase (ORCH-026, A-2):** при `premerge_rebase_always` (дефолт `True`, скоуп `merge_gate_repos`) short-circuit `branch_is_behind_main` пропускается — `auto_rebase_onto_main` вызывается **всегда** под лизом. На актуальной ветке это no-op (`rebase` не меняет HEAD, `push --force-with-lease` → «Everything up-to-date», CI не триггерится); на отстающей — реальный догон. Детерминированный структурный анти-фантом на уровне планировщика (дополняет рубежи ORCH-073, не заменяет). Kill-switch `premerge_rebase_always=False` → прежнее поведение (ребейз только при behind). -- **Re-test:** `python -m pytest` (`merge_retest_target`, дефолт `tests/`) в worktree догнанной ветки, тайм-аут `merge_retest_timeout_s`. Красный/тайм-аут → откат на `development`. +- **Re-test:** `python -m pytest` (`merge_retest_target`, дефолт `tests/`) в worktree догнанной ветки, тайм-аут `merge_retest_timeout_s`. **Красный** → откат на `development`. **ORCH-110 (design):** (1) re-test исполняется лишь когда rebase реально сдвинул HEAD (`main` уехал); доказанный no-op rebase (ветка уже актуальна, HEAD уже прошёл CI+tester+staging) **пропускает** локальный re-test; (2) **таймаут** re-test — инфра-транзиент (ограниченный повтор + отдельный инфра-alert), а НЕ код-фейл (раньше шёл в откат на `development` + расход developer-retry); (3) спавненный pytest бежит в отдельной группе процессов и при таймауте убивается **деревом** (`os.killpg`) — нет осиротевших процессов (см. ниже). - **Сериализация (merge-lock):** файловый **merge-lease** на репо (`/.merge-lease-.json`), живёт от гейта до фактического merge. Acquire **неблокирующий** (anti-deadlock при `max_concurrency=1`): busy → **defer** (повторная постановка deployer'а на `deploy-staging` с задержкой через `available_at`), а не откат. Release — на PR-merged вебхуке / `deploy→done` / откате / по возрасту (crash-реклейм). Restart-safe; без изменения схемы БД. **ORCH-026 (A-1):** это окно = «merge → main-updated» (для self `done` ⇔ SHA-in-main, ORCH-073) — пока A не в `main`, B того же репо получает `merge-lock busy` → defer. Окно сериализации per-repo НЕ переписывается; кросс-репо параллелизм сохранён (лиз — per-repo файл). - **Условность (как ORCH-35):** реален для `orchestrator`; прочие репо — no-op. Флаги `merge_gate_enabled` / `merge_gate_repos` — поэтапный раскат. Контракт **never-raise**. Подробнее: [adr-0006](adr/adr-0006-merge-gate.md), детально — `docs/work-items/ORCH-043/06-adr/ADR-001-merge-gate.md`. Безусловный pre-merge rebase + связь с зависимостями задач — [adr-0015](adr/adr-0015-task-deps-and-merge-serialization.md) (ORCH-026). +#### Merge-gate re-test: толерантность к инфра-таймауту + tree-kill + контракт re-test (ORCH-110 — design) +Инцидент ORCH-109/PR#129: при зелёном tester `PASS` (1899 passed / 516.7s), зелёном CI и актуальной +ветке локальный re-test merge-gate упал по **таймауту** (600s) из-за CPU-голодания от **осиротевших** +pytest-процессов (жили > 2 суток). Таймаут классифицировался как код-фейл → ложный откат +`deploy-staging → development` + 3 сожжённых developer-retry → manual-gate. ORCH-110 бьёт по двум +корням, одному контракту и бюджету — **аддитивно, под kill-switch, never-raise, скоуп self-hosting**; +исходная защита merge-gate от семантического конфликта сохранена (красный re-test по-прежнему +откатывает). `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схема БД — **не тронуты**. +- **Tree-kill (root, D1):** новый leaf `src/proc_group.py::run_in_process_group` спавнит + оркестратор-порождённые pytest-прогоны (`merge_gate.retest_branch` + `coverage_gate.measure_coverage`) + в отдельной группе процессов (`start_new_session`) и при таймауте/прерывании убивает **всё дерево** + (`os.killpg`, каскад SIGTERM→grace→SIGKILL, зеркало `launcher.stop_process`). Контракты возврата 1:1 + (меняется лишь побочный эффект — нет сирот); fallback на прежний `subprocess.run` при kill-switch off + / не-POSIX. Kill-switch `subprocess_tree_kill_enabled`. +- **Классификация + маршрутизация (D2/D3):** чистый `merge_gate.classify_retest_failure(reason) → + timeout|red|lock-busy|other`; инфра-таймаут → `_handle_merge_gate_infra_retry` (ограниченный + повтор/defer по образцу `_handle_merge_gate_defer`, **без** отката на `development`, **без** расхода + developer-retry; исчерпание → отдельный **инфра-alert**, не «developer must fix»); красный re-test → + прежний `_handle_merge_gate_rollback`. Kill-switch `merge_retest_infra_tolerance_enabled`, бюджеты + `merge_retest_infra_max_retries` (2) / `merge_retest_infra_retry_delay_s` (120s). +- **Контракт re-test (D4):** локальный re-test исполняется ⇔ rebase реально сдвинул HEAD; доказанный + no-op rebase пропускает re-test (как уже делает путь `premerge_rebase_always=False` для не-behind + ветки), offline, без сетевого CI-запроса; fail-safe — на неопределённости re-test бежит. Kill-switch + `merge_retest_skip_when_current_enabled`. +- **Бюджет (D5):** `merge_retest_timeout_s` 600 → 900 (запас 74% над 516.7s) + валидация (непозитив → + дефолт + WARNING); сквозной инвариант `reaper_max_running_s (5400) > Σ(deploy-staging gate-work + ≈4460)+grace` проверен — `reaper_max_running_s` **не меняется**. +- **Наблюдаемость (D6):** счётчики + блок `merge_gate` в `GET /queue`; координация с ORCH-111 + (`proc_blocking`, adr-0041) без дубля: ORCH-110 предотвращает/толерирует у источника, ORCH-111 + наблюдает. + +Подробнее: [adr-0042](adr/adr-0042-merge-gate-retest-infra-tolerance-and-tree-kill.md), детально — +`docs/work-items/ORCH-110/06-adr/ADR-001-merge-gate-retest-infra-tolerance-and-tree-kill.md`. + ### Coverage-гейт: защита от деградации покрытия тестами (ORCH-027 — design) Существующие тестовые гейты (`check_ci_green`, `check_tests_passed`, merge-gate re-test) судят только по факту прохождения тестов, не по **полноте** — фича «300 строк, 0 тестов» проходит diff --git a/docs/architecture/adr/adr-0042-merge-gate-retest-infra-tolerance-and-tree-kill.md b/docs/architecture/adr/adr-0042-merge-gate-retest-infra-tolerance-and-tree-kill.md new file mode 100644 index 0000000..a5f1830 --- /dev/null +++ b/docs/architecture/adr/adr-0042-merge-gate-retest-infra-tolerance-and-tree-kill.md @@ -0,0 +1,84 @@ +--- +work_item: ORCH-110 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-15 +model_used: claude-opus-4-8 +--- + +# adr-0042: Merge-gate re-test — толерантность к инфра-таймауту + tree-kill спавненных процессов + контракт re-test + +- **Статус:** proposed +- **Дата:** 2026-06-15 +- **Задача:** ORCH-110 (bug → escalate full-cycle) +- **Детальный ADR:** `docs/work-items/ORCH-110/06-adr/ADR-001-merge-gate-retest-infra-tolerance-and-tree-kill.md` +- **Парные/смежные ADR:** `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` — комплементарный наблюдатель) + +## Контекст + +Merge-gate (ORCH-043) на ребре `deploy-staging → deploy` локально пере-прогоняет тест-сюит +(`retest_branch`) для защиты от семантического конфликта слияния. Инцидент ORCH-109/PR#129: при +зелёном tester `PASS` (1899 passed / 516.7s), зелёном CI и актуальной ветке re-test упал по +**таймауту** (600s) из-за CPU-голодания от **осиротевших** pytest-процессов, переживших > 2 суток. +Таймаут классифицировался как код-фейл → откат `deploy-staging → development` + 3 сожжённых +developer-retry → manual-gate. Корни: (1) `subprocess.run(timeout=)` убивает только прямого потомка — +внуки pytest репарентируются на PID 1 и живут (в `merge_gate.retest_branch` и +`coverage_gate.measure_coverage`); (2) нет толерантности к инфра-таймауту; (3) тонкий бюджет (≈16%); +(4) избыточный re-test на уже актуальной ветке (`premerge_rebase_always=True` форсит rebase+retest +даже на no-op rebase). + +Решение кросс-каттинговое: затрагивает merge-gate, coverage-gate и сквозной инвариант времени +reaper/lease — поэтому регистрируется глобально. + +## Решение (сводка) + +Аддитивно, под kill-switch, never-raise, скоуп self-hosting; исходная защита merge-gate от +семантического конфликта сохранена (красный re-test по-прежнему откатывает). + +- **D1 — tree-kill.** Новый leaf `src/proc_group.py::run_in_process_group` спавнит + оркестратор-порождённые pytest-прогоны в отдельной группе процессов (`start_new_session`) и при + таймауте убивает **всё дерево** (`os.killpg`, каскад SIGTERM→grace→SIGKILL, зеркало + `launcher.stop_process`). Используют `retest_branch` и `measure_coverage`; контракты возврата 1:1, + меняется лишь побочный эффект (нет сирот). Fallback на прежний `subprocess.run` при kill-switch off + / не-POSIX. Kill-switch `subprocess_tree_kill_enabled`. +- **D2 — классификация.** Чистый `merge_gate.classify_retest_failure(reason) → timeout|red|lock-busy| + other`; `check_branch_mergeable` не меняет имя/семантику/PASS-FAIL (реестр `QG_CHECKS` цел). +- **D3 — маршрутизация.** Инфра-таймаут → `_handle_merge_gate_infra_retry` (ограниченный повтор/defer + по образцу `_handle_merge_gate_defer`, **без** отката на `development`, **без** расхода + developer-retry); исчерпание → отдельный **инфра-alert** (не «developer must fix»). Красный re-test + → прежний `_handle_merge_gate_rollback`. Kill-switch `merge_retest_infra_tolerance_enabled`, + бюджеты `merge_retest_infra_max_retries`/`merge_retest_infra_retry_delay_s`. +- **D4 — контракт re-test.** Локальный re-test исполняется ⇔ rebase реально сдвинул HEAD (`main` + уехал); доказанный no-op rebase пропускает re-test (как уже делает путь + `premerge_rebase_always=False` для не-behind ветки), offline, без сетевого CI-запроса. Fail-safe: на + любой неопределённости re-test бежит. Kill-switch `merge_retest_skip_when_current_enabled`. +- **D5 — бюджет.** `merge_retest_timeout_s` 600 → 900 (запас 74%) + валидация (непозитив → дефолт + + WARNING). Сквозной инвариант `reaper_max_running_s (5400) > Σ(deploy-staging gate-work ≈4460)+grace` + проверен — `reaper_max_running_s` **не меняется**. +- **D6 — наблюдаемость.** Счётчики `merge_gate` + блок `merge_gate` в `GET /queue`; координация с + ORCH-111 без дубля (ORCH-110 предотвращает/толерирует у источника, ORCH-111 наблюдает). + +## Инварианты (неприкосновенны) + +- `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / семантика `check_*` / machine-verdict ключи / схема БД — + **байт-в-байт** (под-гейт — врезка в `advance_stage`, не новая стадия/QG; новых таблиц/колонок нет). +- INV-4: никогда push/force-push `main`, merge только через Gitea PR API; прод-контейнер не + рестартится; detached-деплой не трогается. +- never-raise во всех новых функциях/врезках; исключение не уходит в `advance_stage`/монитор. +- Kill-switch + нулевая регрессия: каждый флаг off → байт-в-байт до-ORCH-110; enduro (non-self) — no-op. + +## Последствия + +- **+** Устранён ложный откат/manual-gate при инфра-таймауте; устранена утечка CPU от сирот; + re-test не избыточен на актуальной ветке. +- **−** До ~34 мин на инфра-ретраи перед alert (вместо мгновенного ложного отката); +5 конфиг-ключей. +- **Откат:** вернуть 4 kill-switch и `merge_retest_timeout_s=600`. + +## Ссылки +- Детально: `docs/work-items/ORCH-110/06-adr/ADR-001-merge-gate-retest-infra-tolerance-and-tree-kill.md` +- Код: `src/merge_gate.py`, `src/coverage_gate.py`, `src/qg/checks.py`, `src/stage_engine.py`, + `src/config.py`, `src/agents/launcher.py`, `src/job_reaper.py`, новый `src/proc_group.py` + diff --git a/docs/work-items/ORCH-110/06-adr/ADR-001-merge-gate-retest-infra-tolerance-and-tree-kill.md b/docs/work-items/ORCH-110/06-adr/ADR-001-merge-gate-retest-infra-tolerance-and-tree-kill.md new file mode 100644 index 0000000..32ac860 --- /dev/null +++ b/docs/work-items/ORCH-110/06-adr/ADR-001-merge-gate-retest-infra-tolerance-and-tree-kill.md @@ -0,0 +1,352 @@ +--- +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=, + tree_kill=settings.subprocess_tree_kill_enabled, grace_s=settings.agent_kill_grace_seconds)`; + маппинг исхода 1:1 прежнему: `timed_out → (False, "re-test timeout after s")`, + `returncode==0 → (True, "re-test green")`, иначе `(False, "re-test failed: ...")`. **Контракт + возврата сохранён** (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 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` — комплементарный наблюдатель) + + diff --git a/docs/work-items/ORCH-110/07-infra-requirements.md b/docs/work-items/ORCH-110/07-infra-requirements.md new file mode 100644 index 0000000..738b7be --- /dev/null +++ b/docs/work-items/ORCH-110/07-infra-requirements.md @@ -0,0 +1,68 @@ +--- +work_item: ORCH-110 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-15 +model_used: claude-opus-4-8 +--- + +# 07 — Инфра-требования: ORCH-110 — merge-gate re-test infra-tolerance + tree-kill + +Work Item: **ORCH-110** · Repo: **orchestrator** · Стадия: architecture + +> When-applicable. Топология/контейнеры/порты/CI **не меняются**; файл фиксирует новые env-ключи, +> сквозной инвариант времени и аудит self-hosting безопасности. + +## I-1. Топология / окружения +**N/A.** Новых контейнеров/портов/томов/сетей нет. Изменения — только в коде приложения +(`src/proc_group.py` новый leaf; правки `merge_gate`/`coverage_gate`/`qg.checks`/`stage_engine`/ +`config`) и в значениях конфигурации. Прод `orchestrator` (8500) / staging (8501) — без изменений +топологии. + +**Требование среды:** оркестратор должен исполняться под POSIX (Linux-контейнер, как сейчас) — D1 +использует `os.setsid`/`os.killpg`/`os.getpgid`. На не-POSIX helper деградирует на прежний +`subprocess.run` (never-break), но боевая среда — Linux. + +## I-2. Переменные окружения / секреты +Секретов нет. Новые ключи (`src/config.py` + `.env.example`); **дефолт каждого = желаемому +прод-поведению** (ORCH-101 канон: пустой `.env` воспроизводит целевое поведение): + +| Ключ (config) | Env | Дефолт | Назначение | +|---------------|-----|--------|------------| +| `subprocess_tree_kill_enabled` | `ORCH_SUBPROCESS_TREE_KILL_ENABLED` | `True` | D1 kill-switch; off → прежний `subprocess.run(timeout=)` | +| `merge_retest_infra_tolerance_enabled` | `ORCH_MERGE_RETEST_INFRA_TOLERANCE_ENABLED` | `True` | D3 kill-switch; off → таймаут = прежний rollback | +| `merge_retest_infra_max_retries` | `ORCH_MERGE_RETEST_INFRA_MAX_RETRIES` | `2` | D3 бюджет повторов инфра-таймаута | +| `merge_retest_infra_retry_delay_s` | `ORCH_MERGE_RETEST_INFRA_RETRY_DELAY_S` | `120` | D3 задержка перед повтором staging-deployer | +| `merge_retest_skip_when_current_enabled` | `ORCH_MERGE_RETEST_SKIP_WHEN_CURRENT_ENABLED` | `True` | D4 kill-switch; off → re-test после rebase всегда | +| `merge_retest_timeout_s` (изменение значения) | `ORCH_MERGE_RETEST_TIMEOUT_S` | `600 → 900` | D5 бюджет re-test (запас 74% над 516.7s) | + +Реюз существующего `agent_kill_grace_seconds` (грейс tree-kill каскада) — новый ключ не вводится. + +## I-3. Деплой / рестарт +- **Self-hosting инвариант соблюдён:** изменение НЕ рестартит прод-контейнер, НЕ пушит/force-push + `main` (INV-4), НЕ трогает detached-деплой. Выкат — штатным конвейером через **обязательный + staging-гейт (8501)**. +- Дефолты вступают в силу при следующей сборке образа; ручных env-шагов на хосте не требуется + (дефолты = целевое поведение). Откат — выставить 4 kill-switch в `False` и + `ORCH_MERGE_RETEST_TIMEOUT_S=600`. + +## I-4. CI/CD +**Без изменений** `.gitea/workflows/`. Новые pytest-тесты (`tests/test_orch110_*.py` по +`04-test-plan.yaml`) исполняются существующим шагом `pytest tests/ -q`. Новых зависимостей нет +(`proc_group` — stdlib-only; `pytest-cov` уже в образе по ORCH-027). + +## I-5. Сквозной инвариант времени (NFR-6 — операционно критично) +Re-test/coverage исполняются в монитор-потоке staging-deployer-джоба, поэтому их суммарная работа +считается против `reaper_max_running_s`. Проверенный worst-case deploy-staging-джоба: + +``` +deployer-агент 1800 + security ~120 + rebase 120 + re-test 900 + coverage 900 + image ~600 + grace 20 +≈ 4460 s < reaper_max_running_s = 5400 (запас ~940 s) ✓ +``` + +`reaper_max_running_s` **не меняется** (D5). При ручном повышении `ORCH_MERGE_RETEST_TIMEOUT_S` env'ом +оператор ОБЯЗАН сохранить неравенство `reaper_max_running_s > Σ(gate-work) + grace`, иначе reaper +(ORCH-065) может реапнуть легитимный мид-гейт джоб; при необходимости поднять +`ORCH_REAPER_MAX_RUNNING_S` в локстеп (паттерн ORCH-109). + diff --git a/docs/work-items/ORCH-110/10-tech-risks.md b/docs/work-items/ORCH-110/10-tech-risks.md new file mode 100644 index 0000000..40dff2a --- /dev/null +++ b/docs/work-items/ORCH-110/10-tech-risks.md @@ -0,0 +1,40 @@ +--- +work_item: ORCH-110 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-15 +model_used: claude-opus-4-8 +--- + +# 10 — Технические риски: ORCH-110 — merge-gate re-test infra-tolerance + tree-kill + +Work Item: **ORCH-110** · Repo: **orchestrator** · Стадия: architecture + +> Информационный (гейтом не парсится). Риски реализации и их митигейшн. + +## Реестр рисков + +| ID | Риск | Вер. | Влия. | Митигейшн | +|----|------|------|-------|-----------| +| TR-1 | **Над-толерантность маскирует реально зависший тест ветки** как «инфра» (R-1 BRD): бесконечный/долгий тест ветки → таймаут → толерируется → задача не падает на дефекте | Низ. | Выс. | Строгая ограниченность D3 (`merge_retest_infra_max_retries=2`, суммарное время ≤ ~34 мин) → исчерпание = **инфра-alert** (не молчание); красно-откат-путь сохранён (BR-6); D4 не пропускает re-test когда HEAD реально сдвинут. Остаточно: «зависший тест ветки» эскалируется как инфра, а не код — приемлемо (оператор увидит alert и разберёт; тест-зависание чинит developer вручную) | +| TR-2 | **`os.killpg`/`start_new_session` платформенная зависимость** (не-POSIX / урезанный контейнер) | Низ. | Сред. | Гард `hasattr(os, "killpg")` + kill-switch `subprocess_tree_kill_enabled` → fallback на прежний `subprocess.run` (never-break); бой — Linux | +| TR-3 | **Tree-kill убьёт не те процессы** (неверный pgid → SIGKILL чужой группе) | Низ. | Выс. | `pgid == proc.pid` гарантирован `start_new_session` для **нашего** потомка; killpg только по `os.getpgid(proc.pid)` нашего Popen; `ProcessLookupError` толерируется; никогда не киляем pgid 0/own | +| TR-4 | **Рассинхрон сквозных таймаутов** при бампе бюджета (R-3 BRD): re-test 900 + coverage 900 + агент → > reaper_max_running_s | Низ. | Сред. | Проверенная таблица worst-case (≈4460 < 5400, 07-infra I-5); `reaper_max_running_s` не меняется; валидация непозитивного бюджета → дефолт+WARNING; операторская заметка про ручной бамп | +| TR-5 | **Магическая строка классификации** (`"timeout" in reason`) хрупка к смене текста reason | Низ. | Сред. | Единый предикат `classify_retest_failure` (одна тестируемая точка, TC-03) вместо россыпи; reason — стабильный контракт ORCH-043 (как `"merge-lock busy"`) | +| TR-6 | **D4 пропустит re-test и пропустит семантический конфликт** | Оч.низ. | Сред. | Пропуск ТОЛЬКО при доказанном no-op rebase (нет «уехавшего main» → нет нового класса конфликта); на неопределённости (`pre`/`post` пуст / git-ошибка) re-test бежит (fail-safe); HEAD уже прошёл CI+tester+staging транзитивно | +| TR-7 | **Дубль/конфликт с ORCH-111** (`proc_blocking`) | Оч.низ. | Низ. | Чёткое разделение слоёв (D6): ORCH-110 предотвращает/толерирует у источника, ORCH-111 наблюдает; ORCH-110 не вводит детектор процессов | +| TR-8 | **Регресс при выключенном флаге** (нарушен байт-в-байт fallback) | Низ. | Выс. | TC-07 проверяет kill-switch off = прежнее поведение и enduro no-op; каждый из 4 флагов независим и off → старый путь | +| TR-9 | **Гонка leдза на инфра-ретрае** (повторный acquire, держание дольше TTL) | Низ. | Сред. | Существующее свойство ORCH-043 (лиз держится дольше TTL; реклейм holder-aware + pid-aware, ORCH-065 не трогает живого держателя); таймаут уже освободил лиз перед ретраем — переacquire штатный | + +## Сводный вывод +Доминирующий класс — **над-толерантность vs анти-регресс защиты merge-gate** (TR-1/TR-6): закрыт +строгой ограниченностью + сохранением красно-откат-пути + пропуском re-test только на доказанном +no-op. Остальные риски — стандартного класса (платформенная зависимость, сквозные таймауты, +kill-switch регресс), митигированы существующими паттернами кодовой базы (never-raise, fail-safe, +дефолт=бой, валидация). Изменение **аддитивно** и **полностью обратимо** флагами; новой стадии/QG/ +схемы БД нет. **Эскалация `arch:major-change` не требуется**; возврат в анализ не требуется. Остаточный +риск для прод-конвейера (self-hosting) — **низкий**: critical-path не трогает `main`/прод-рестарт/ +detached-деплой, а наихудший новый сценарий (инфра-alert после ретраев) строго лучше текущего (ложный +manual-gate). +