--- work_item: ORCH-113 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-113 — BUG: job-reaper не должен повторно запускать финализацию `deploy-staging`, пока жив исходный finalizer Work Item: **ORCH-113** · Repo: **orchestrator** · Стадия: analysis > ТЗ описывает **требования к изменению**, выведенные из BRD и фактического кода. Конкретный > механизм (heartbeat живости / sub-state `finalizing` / per-stage grace / ownership-lease на > edge-гейты), точные имена символов/колонок/флагов и порядок врезок — **архитектурное решение** > (06-adr), не зона аналитика. Модули-плейсхолдеры ниже выровнены под манифест `PIPELINE_DOCS.md`. ## 1. Сводка изменения Сделать повторную обработку reaper'ом завершившегося-но-ещё-`running` job на стадии `deploy-staging` **идемпотентной и владеющей состоянием**: пока исходный monitor/finalizer **жив** (или edge-гейты для пары `(job, stage)` уже исполняются), reaper **не должен** независимо запускать второй прогон edge- под-гейтов ребра `deploy-staging → deploy` (security / merge-gate / локальный re-test / coverage / image-freshness) и **не должен** на этом основании откатывать задачу. Корень — ошибочное допущение Tier-2 finalization-grace (`reaper_finalize_grace_s=300`), что финализация после штампа `finished_at` длится «секунды…десятки секунд»; для `deploy-staging` она длится **минуты** (полный re-test + coverage + rebuild), потому что `_try_advance_stage` (тяжёлые edge-гейты) выполняется **после** штампа `finished_at` и **до** `_finalize_job`. Фикс должен дать reaper'у способ отличить «живой, долго финализирующий монитор» от «мёртвого монитора» и обеспечить строгое владение исполнением edge-гейтов. ## 2. Задействованные модули / пути | Путь | Действие | |------|----------| | `src/job_reaper.py` | изменить — Tier-2-ветка `_reap_job` (строки ~197-209), `_reap_known_outcome` / `_reap_exit0` / `_gate_driven_advance`: ввести проверку живости/владения перед side-effectful re-drive `advance_stage` для `deploy-staging`; при живом finalizer'е — **defer**, не reap | | `src/agents/launcher.py` | изменить (вероятно) — `_monitor_agent`: место/порядок штампа `finished_at` (строка ~861) относительно `_try_advance_stage` (строка ~998) и/или эмиссия durable-сигнала «finalizer жив/финализирует» перед запуском тяжёлых edge-гейтов | | `src/db.py` | изменить (вероятно) — `get_running_jobs` (строки ~1337-1367, источник `finished_age_s`) и/или аддитивная колонка владения/живости (`_ensure_column`, паттерн `tasks.cancelled_at`); `reap_running_job` — без изменения контракта атомарного флипа | | `src/config.py` | изменить (вероятно) — kill-switch фикса и/или per-stage/finalize-aware grace; сохранить сквозной инвариант `reaper_max_running_s > Σ gate-work + grace` (NFR-6) | | `src/stage_engine.py` | **только чтение/ссылка** — `advance_stage` ребро `deploy-staging` (строки ~321-383): эталон того, какие edge-гейты дублируются. Изменение нежелательно; идемпотентность предпочтительно решать на стороне reaper/launcher | > Точный набор затронутых модулей и распределение логики (reaper-only vs launcher-сигнал vs db-колонка) > архитектор фиксирует в 06-adr. Аналитик фиксирует, что центр тяжести правки — `src/job_reaper.py`. ## 3. Функциональные требования ### FR-1 — Распознавание живости финализирующего монитора (BR-1, BR-3) Reaper в Tier-2 для стадии `deploy-staging` должен распознавать ситуацию «процесс агента завершён, но monitor/finalizer ещё **жив** и исполняет edge-гейты» и **не** трактовать её как мёртвый монитор по одному лишь `finished_age_s >= reaper_finalize_grace_s`. Сигнал живости должен переживать долгую (минуты) финализацию `deploy-staging`. **Инвариант:** живой finalizer **никогда** не reap'ается. ### FR-2 — Идемпотентность и строгое владение edge-гейтами (BR-2) Для пары `(job, stage)` на `deploy-staging` тяжёлый прогон edge-гейтов (security / merge-gate / локальный re-test / coverage / image-freshness) исполняется **не более одного раза одновременно**: актор, не владеющий состоянием, **не** запускает второй `advance_stage`/re-test/merge-gate. Текущий атомарный claim-before-act (`reap_running_job ... WHERE status='running'`) защищает только флип строки job — требование расширяет защиту на **side-effectful исполнение edge-гейтов**. ### FR-3 — Согласование grace/бюджета (BR-3, NFR-6) Длительность finalization-grace (или заменяющий её сигнал живости) должна покрывать фактический wall-clock финализации `deploy-staging` = Σ(security + merge re-test `merge_retest_timeout_s=900` + coverage + image rebuild). Любая правка grace/таймаутов сохраняет сквозной инвариант ORCH-065/109/110: `reaper_max_running_s (5400) > Σ(deploy-staging gate-work) + grace`. ### FR-4 — Сохранение добивания мёртвого finalizer'а (BR-4) Реально **мёртвый** monitor/finalizer на `deploy-staging` (краш посреди финализации, сигнал живости отсутствует/протух) по-прежнему добивается reaper'ом за ограниченное время по существующему контракту (retry в пределах бюджета, иначе `failed` + Telegram; Tier-3 backstop как крайний предохранитель). Reaper не становится no-op для `deploy-staging`. ### FR-5 — Отсутствие расхождения состояния (BR-5) Исключить одновременный ложный откат `deploy-staging → development` и успешное завершение `deploy → done` для одной задачи. После согласования у задачи — единственное консистентное терминальное/стадийное состояние; ложный developer-retry не инкрементируется. Под-гейт merge-verify (`deploy → done`, ORCH-071) остаётся единственным choke-point'ом в `done` — он **не** ослабляется. ## 4. Изменения API Новых публичных эндпоинтов **не требуется**. Допустима **аддитивная read-only** наблюдаемость в `GET /queue` (например, отметка «deploy-staging finalize in-progress / deferred-by-liveness» в блоке `reaper`) — без изменения существующих ключей ответа. ## 5. Изменения схемы БД Возможна **аддитивная** колонка владения/живости finalizer'а (например, durable-таймштамп «finalizing heartbeat» или owner-токен на job/agent_run), вводимая идемпотентно через `_ensure_column` (паттерн `tasks.cancelled_at` / `tasks.track`). Существующие таблицы/колонки/индексы и их семантика — **без изменений**. Точная необходимость и форма — за архитектором (06-adr / 08-data-requirements). ## 6. Требования к новым/изменённым QG checks **Нет.** `QG_CHECKS` и любой `check_*` (включая `check_staging_status`, `check_branch_mergeable`, `check_coverage_gate`, `check_security_gate`, `check_staging_image_fresh`, `check_deploy_status`) — не трогаются ни по составу, ни по семантике, ни по machine-verdict ключам. Багфикс — свойство страховочного демона/финализатора, **не** Quality Gate. ## 7. Совместимость / регресс - **Kill-switch:** фикс под выделенным флагом; `False` → строго прежнее поведение reaper (нулевая регрессия). - **Область:** поведение для **не-`deploy-staging`** стадий и путь добивания мёртвого монитора — сохранены; self-hosting-only/скоуп репо согласовать с существующими leaf-паттернами (если вводится per-repo разрез). - **Обратимость:** раскат обратим выключением флага (+ откатом значений grace/таймаутов к дефолтам). - **Never-raise / restart-safe / self-hosting (NFR-1/3/5):** любой сбой нового пути живости/владения деградирует безопасно (reaper-тик продолжается); новый durable-маркер восстановим после рестарта; фикс не рестартит прод и не пушит `main`. - **Сквозной инвариант (NFR-6):** `reaper_max_running_s > Σ gate-work + grace` сохранён. - **Анти-регресс:** структурный тест-гард (см. `04-test-plan.yaml`), полный `tests/` остаётся зелёным.