# BRD — ORCH-065: zombie jobs + залипший merge-lease

Work Item ID: ORCH-065
Тип: BUG (P0)
Репозиторий: orchestrator (self-hosting)
Эпик: блокер ORCH-54 (полностью автономный self-deploy)

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

Оркестратор — единый инстанс с **общей БД и общей очередью** (`jobs`,
`max_concurrency=1` для self-hosting), обслуживающий несколько проектов. Финальная
автономность self-deploy упирается в два связанных класса отказов, оба сводящиеся
к «процесс умер/завершился, а состояние осталось захваченным навсегда»:

### Проблема A — zombie jobs (строка `jobs` навсегда `running`)
Агент (deployer/developer/reviewer) завершается **или умирает** (краш, OOM,
рестарт контейнера в ходе self-deploy, гибель monitor-потока), но строка в таблице
`jobs` остаётся в статусе `running`. Финализация статуса job выполняется **только**
в `_monitor_agent` → `_finalize_job` внутри того же процесса; если этот поток/процесс
не доживает до финализации — job «зомбирован».

- Единственная имеющаяся защита — `requeue_running_jobs()` в `main.lifespan`,
  срабатывающая **исключительно на старте процесса**. Зомби, возникший **без**
  рестарта (умер дочерний процесс/monitor-поток, а сервис жив), не реанимируется
  никогда.
- При `max_concurrency=1` одна зомби-строка `running` блокирует claim всех
  последующих job (`count_running_jobs() >= max_concurrency` → claim не происходит)
  → **встаёт конвейер всех проектов**.

### Проблема B — залипший merge-lease
Merge-gate (ORCH-043) берёт файловый lease `<repos_dir>/.merge-lease-<repo>.json`
ПЕРЕД rebase+re-test и держит его до фактического merge PR в `main`. Если процесс
умирает на финальном merge **с зажатым lease**:

- Реклейм lease реализован **лениво и только по возрасту** (`age >=
  merge_lock_timeout_s`) и **только в момент `acquire_merge_lease` другой задачей**.
  Проактивного освобождения (на старте / периодически) нет; **liveness держателя по
  pid не проверяется** (хотя `pid` в lease пишется).
- Пострадавшая задача сама re-drive не получает: merge не финализируется → задача
  висит, lease мешает чужим merge до истечения TTL.

### Проблема C — неидемпотентная финализация merge
Если rebase+re-test прошли зелёно (ветка догнана и проверена), но процесс умер до
завершения слияния PR — повторного «докатывания» merge нет. Задача застревает в
полу-выполненном состоянии, хотя вся дорогая работа (rebase+re-test) уже сделана.

## 2. Бизнес-последствия
- **Это ПОСЛЕДНЯЯ ручная точка автономного деплоя.** Без фикса ни одна self-hosting
  задача не доезжает до прода без оператора (cancel zombie + ручной merge PR +
  ручной `--deploy`).
- Прямой блокер эпика ORCH-54.
- Доказанные инциденты (07.06): ORCH-58/60/61/21 — каждый раз после успешного
  deployer-прохода job оставался `running`; jobs **236/239/242/254** — зомби,
  прод-merge/deploy доводились вручную.
- Групповой риск: зомби в общей очереди при concurrency=1 останавливает конвейер
  enduro-trails и всех прочих проектов.

## 3. Цель
Сделать так, чтобы **смерть процесса/потока на любой стадии (включая self-restart
во время deploy) НЕ оставляла навсегда захваченных ресурсов** — ни строки `jobs` в
`running`, ни merge-lease. Конвейер должен самовосстанавливаться без оператора, при
этом сохраняя все инварианты self-hosting (не ронять прод-контейнер, не трогать
`main`, fail-closed на реальных ошибках).

## 4. Объём (Scope)

### В объёме
1. **Job-reaper** — фоновый watchdog (паттерн `reconciler`/`queue_worker`),
   детектирующий «мёртвый» `running`-job и приводящий его строку в корректный
   терминальный/повторный статус (`done`/`failed`/`queued`) детерминированно,
   без LLM. Restart-safe и работающий **без** рестарта процесса.
2. **Проактивный реклейм stale merge-lease** — освобождение lease, чей держатель
   мёртв (pid не жив) ИЛИ возраст превысил TTL — на старте и периодически (reaper/
   reconciler), а не только лениво при чужом `acquire`.
3. **Идемпотентная финализация merge** — если rebase+re-test зелёные, но merge не
   состоялся, операция повторяется/докатывается без потери уже сделанной работы.

### Вне объёма
- Переход на внешний брокер очередей / смену схемы блокировок merge на БД-lock.
- Полный авто-approve деплоя (ORCH-54) — отдельная задача; здесь только снятие
  технического блокера.
- Изменение конвейера стадий (`STAGE_TRANSITIONS`) и реестра гейтов как контрактов.

## 5. Заинтересованные стороны
- Owner оркестратора (self-hosting автономность).
- Все проекты на общем инстансе (enduro-trails и пр.) — страдают от блокировки
  общей очереди.

## 6. Допущения и ограничения
- `max_concurrency=1` для self-hosting сохраняется.
- Self-hosting safety (CLAUDE.md): нельзя ронять/рестартить прод-контейнер в рамках
  задачи; нельзя пушить/форс-пушить `main`; реклейм lease не должен прерывать
  легитимно работающий merge.
- Никаких ложных реанимаций: живой, но долгий job не должен помечаться зомби
  (нужен порог/грейс «N тиков» + проверка реальной смерти, а не просто долготы).
- Контракт **never-raise** для всей новой фоновой логики (как у reconciler/merge_gate).
- Kill-switch на каждый новый механизм (как `reconcile_enabled` / `merge_gate_enabled`).

## 7. Критерий успеха (бизнес-уровень)
После фикса воспроизводимый сценарий «успешный deployer-проход + смерть процесса/
self-restart» НЕ оставляет зомби-job и зажатого lease: задача либо корректно
доезжает до `done` сама, либо откатывается по штатному контракту — **без участия
оператора**. Регресс-тест на jobs-зомби и stale-lease зелёный.
