--- 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 --- # 02 — ТЗ (TRZ): ORCH-110 — merge-gate local re-test timeout: устранение ложного отката + утечки процессов Work Item: **ORCH-110** · Repo: **orchestrator** · Стадия: analysis > ТЗ описывает **конкретные требования к реализации**, выведенные из BRD и фактического кода. > Архитектурное обоснование, выбор вариантов и **контракт merge-gate** — задача архитектора (06-adr, > основание `escalate: full-cycle`). Здесь — поведение/контракты/инварианты и привязка к модулям, > НЕ выбор механизма. ## 1. Сводка изменения Устранить ложный откат `deploy-staging → development`, возникающий когда локальный re-test merge-gate падает по **таймауту** (инфра/ресурс), при зелёном tester `PASS` и зелёном CI. Изменение бьёт по двум корням и одному контракту: (1) **утечка осиротевших pytest-процессов** из оркестратор-спавненных прогонов re-test/coverage (источник CPU-голодания) → гарантировать tree-kill дерева подпроцесса при таймауте/kill; (2) **классификация инфра-таймаута** как транзиента (повтор/defer/инфра-alert), а не код-фейла (откат + расход developer-retry); (3) **контракт необходимости** локального re-test относительно зелёного CI и состояния `branch vs origin/main`. Сопутствующе — согласование **бюджета** re-test с реальным временем сюита. Всё — аддитивно, под kill-switch, never-raise, скоуп self-hosting, с сохранением исходной защиты merge-gate от семантического конфликта (красный re-test по-прежнему откатывает). ## 2. Задействованные модули / пути | Путь | Действие | |------|----------| | `src/merge_gate.py` | изменить — `retest_branch`: жизненный цикл подпроцесса (tree-kill при таймауте/kill); классификация исхода «timeout» как транзиента (контракт возврата) | | `src/coverage_gate.py` | изменить — `measure_coverage`: тот же tree-kill при таймауте (сиблинг-источник утечки, BR-3) | | `src/qg/checks.py` | изменить — `check_branch_mergeable`: различать «timeout/infra» от «red re-test» в возвращаемом контракте (без смены имени/семантики зарегистрированного `check_*`) | | `src/stage_engine.py` | изменить — `_handle_merge_gate` / маршрутизация исхода: инфра-таймаут → defer/повтор/инфра-alert (по образцу `_handle_merge_gate_defer`), НЕ `_handle_merge_gate_rollback`; красный re-test → прежний rollback | | `src/config.py` | изменить — флаг(и) толерантности к инфра-таймауту + (опц.) согласование `merge_retest_timeout_s`; уважить сквозные инварианты `merge_lock_timeout_s` / `reaper_max_running_s` / `coverage_run_timeout_s` | | `docs/architecture/README.md`, `CLAUDE.md`, `CHANGELOG.md` | обновить — описание поведения merge-gate re-test (golden source наравне с кодом) | | `tests/test_*` | создать — покрытие по `04-test-plan.yaml` | > Точный набор новых символов/флагов и механизм tree-kill (process-group `start_new_session`+killpg, > либо иной) — решение архитектора. ТЗ фиксирует **что** должно выполняться, не **как**. ## 3. Функциональные требования ### FR-1 — Толерантность к инфра-таймауту re-test (нет ложного отката) [BR-1, BR-2] Когда merge-gate локальный re-test завершается специфически по **таймауту** (а не детерминированно красным результатом), исход ДОЛЖЕН классифицироваться как транзиент/инфра, не код-фейл. Путь восстановления НЕ ДОЛЖЕН быть тем же `_handle_merge_gate_rollback` (откат на `development` + инкремент developer-retry), который при зелёных CI/tester ведёт к «Manual intervention needed». Допустимая реакция (выбор — архитектор): ограниченный повтор re-test и/или defer (по образцу существующего `_handle_merge_gate_defer` для `merge-lock busy`) и/или отдельный инфра-alert. Прецеденты толерантности к инфра: ORCH-061 (staging infra tolerance), ORCH-093 (transient vs terminal классификация merge-POST). ### FR-2 — Tree-kill оркестратор-спавненных тест-процессов [BR-3] `merge_gate.retest_branch` и `coverage_gate.measure_coverage` ДОЛЖНЫ гарантировать, что при таймауте (а также при любом kill/прерывании прогона) завершается **всё дерево** подпроцесса pytest, включая внуков, а не только прямой потомок. После таймаута ни один оркестратор-спавненный pytest-процесс не должен оставаться живым и грузить CPU. Контракт возврата `retest_branch` (`(False, "re-test timeout after s")`) сохраняется; меняется лишь побочный эффект — отсутствие утечки. Существующий каскад launcher `SIGTERM→grace→SIGKILL` (`stop_process`) — образец на уровне агентов; для этих subprocess-прогонов требуется эквивалентная гарантия на уровне группы процессов. ### FR-3 — Согласованность бюджета re-test [BR-4, NFR-6] Бюджет `merge_retest_timeout_s` ДОЛЖЕН иметь достаточный запас над фактическим временем полного сюита (наблюдаемо: 600s бюджет vs 516.70s факт ≈ 16%). Бюджет остаётся конфигурируемым; при его изменении ДОЛЖНЫ соблюдаться сквозные инварианты: `reaper_max_running_s > max(agent_timeout, бюджеты) + grace` (ORCH-065/109) и согласование с `merge_lock_timeout_s` (TTL merge-lease держится на время re-test). Малформный/непозитивный конфиг → безопасный дефолт + WARNING (never-break). ### FR-4 — Контракт необходимости локального re-test [BR-5, BR-6] Merge-gate ДОЛЖЕН различать риск-кейсы и применять re-валидацию пропорционально реальному риску слияния: - ветка **реально отстала** от уехавшего `origin/main` и ребейзнута → семантический риск → re-test оправдан (текущая цель ORCH-043 сохраняется); - ветка **уже актуальна** / rebase — no-op, и CI по этому самому HEAD зелёный → локальный полный re-test пере-проверяет ровно подтверждённый CI коммит и не должен быть единственной точкой ложного отказа. Конкретный контракт (например: пропуск re-test при «не-behind + зелёный CI по HEAD», сокращённый scope, доверие SHA, подтверждённому CI, и т. п.) — **выбор архитектора в ADR** (ядро запрошенного баг-карточкой «анализа контрактов merge-gate»). Инвариант **BR-6**: детерминированно **красный** re-test (реальный сбой теста) обязан и далее откатывать на `development` — послабление применяется ТОЛЬКО к таймауту/инфра. ### FR-5 — Сохранение инвариантов и kill-switch [NFR-1, NFR-2, NFR-3, NFR-4] Изменение аддитивно: `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / семантика `check_*` / machine-verdict ключи / схема БД — без изменений; merge-gate остаётся под-гейтом-врезкой, не новой стадией/QG. Под kill-switch: выключенный флаг → байт-в-байт прежнее поведение (таймаут → откат). Скоуп self-hosting (`orchestrator`); enduro — no-op. never-raise; INV-4 (никогда push/force-push `main`; merge только через Gitea PR API) и запрет рестарта прод-контейнера — соблюдены. ### FR-6 — Наблюдаемость и ограниченность [BR-7, NFR-5] Состояние «инфра-таймаут» ДОЛЖНО логироваться, уведомляться в Telegram (кликабельный номер задачи) и быть видимым read-only (например, расширение блока `merge`/`merge_verify` в `GET /queue`), отличимо от код-фейл-отката. Любой повтор/defer строго ограничен (число попыток + суммарное время); исчерпание → **инфра-alert** (не «developer must fix»). Координация с ORCH-111 (`proc_blocking`) — без дубля: ORCH-110 предотвращает/толерирует, ORCH-111 наблюдает. ## 4. Изменения API Новых обязательных эндпоинтов **не требуется**. Допустимо (when-applicable, на усмотрение архитектора) **read-only** расширение существующего снимка `GET /queue` (блок merge-gate) полями наблюдаемости инфра-таймаута/повторов. Никаких новых управляющих эндпоинтов. ## 5. Изменения схемы БД **Нет.** Счётчики повторов/defer — по образцу существующих (`_merge_defer_count` / `_developer_retry_count` поверх `jobs`/`agent_runs`) либо in-memory/sentinel; новые таблицы/колонки не вводятся (NFR-1). ## 6. Требования к новым/изменённым QG checks **Нет нового зарегистрированного QG.** `check_branch_mergeable` остаётся в реестре `QG_CHECKS` с тем же именем и семантикой PASS/FAIL; меняется лишь **различение причины FAIL** (timeout/infra vs red) в возвращаемом reason и **маршрутизация исхода** во врезке `_handle_merge_gate` (`advance_stage`). `STAGE_TRANSITIONS` и состав `QG_CHECKS` — байт-в-байт. ## 7. Совместимость / регресс - **Обратная совместимость:** kill-switch off → поведение байт-в-байт как до ORCH-110 (таймаут → rollback на `development`), включая текст alert'ов. - **Область раската:** self-hosting `orchestrator` (как ORCH-035/043/058/071); прочие репо — no-op, путь LLM-`deployer`/прежний merge не затронут. - **Обратимость:** чисто аддитивная логика под флагом; откат = выключить флаг. - **Self-hosting:** без рестарта прод-контейнера; merge только через Gitea PR API; никаких операций с `main` (INV-4). - **Анти-регресс целей merge-gate:** красный re-test → прежний rollback (BR-6); защита от семантического конфликта/фантомного merge (ORCH-043/071/073) — не ослаблена. - **Трассировка маркеров (ORCH-078):** правки в `merge_gate.py`/`coverage_gate.py`/`qg/checks.py` затрагивают блоки с маркерами ORCH-043/071/073/093/027/065/109 — перед изменением сверить их `06-adr` и не сломать зафиксированные инварианты (lease, never-raise, fail-open/closed, бюджеты).