# Bugfixes 2026-06-02 — устранение багов оркестратора

**Источник:** `tasks/multi-agent/AUDIT_2026-06-02.md`
**Цель:** вернуть автономность мультиагентного pipeline (ET-009: 0/6 этапов были автономны).
**Исполнитель:** Dev-агент (Opus 4.8 Tokenator).

---

## Что починено

### B-1 — запись `.task-*.md` без docker
**Было:** `launcher._write_task_file()` писал файл через `docker run --rm -i python:3.12-slim bash -c "cat > ..."`. Бинарника `docker` в контейнере НЕТ → запись падала молча → агент читал старый task-файл.
**Стало:** прямая запись в смонтированный volume `/repos/<repo>/<task_file>` обычным `open(..., "w")`. При ошибке записи — `RuntimeError` (не молчит).
**Файл:** `src/agents/launcher.py` (`_write_task_file`, вызов в `launch`).
**Проверка:**
```bash
docker exec orchestrator python3 -c "
import sys; sys.path.insert(0,'/repos/orchestrator')
from src.agents.launcher import launcher
launcher._write_task_file('enduro-trails', '.task-test-write.md', 'hello-from-fix')
print(open('/repos/enduro-trails/.task-test-write.md').read())"
# => hello-from-fix   (без docker)
```
✅ Verified: READBACK = `hello-from-fix`.

### B-2 — Popen stdout → файл, убран PIPE-поток (зомби, потеря exit_code)
**Было:** `Popen(stdout=PIPE)` + daemon-поток с `select`/`readline` + startup-timeout 120с. → PIPE-deadlock, зомби при рестарте, `exit_code=None` в БД (все прогоны ET-009).
**Стало:** `log_fh = open(output_path, "w")`; `Popen(stdout=log_fh, stderr=STDOUT)`. `_monitor_agent` упрощён до `proc.wait()` + `log_fh.close()`. PIPE-поток и startup-timeout удалены. Watchdog по pid (`AGENT_TIMEOUT`) сохранён.
**Файл:** `src/agents/launcher.py` (`launch`, `_monitor_agent`).
**Проверка:** после прогона `SELECT exit_code FROM agent_runs ORDER BY id DESC LIMIT 1` != NULL; `ps aux | grep defunct` — пусто.

### B-3 — `.task-*.md` в `.gitignore`, не коммитятся
**Было:** task-файлы трекались в git (`.task-arch.md`, `.task-dev.md`, `.task-review.md`, `.task.md`) и тащились между задачами.
**Стало:** в `enduro-trails/.gitignore` добавлено `.task*.md`; трекаемые файлы убраны из индекса (`git rm --cached`).
**Файл:** `enduro-trails/.gitignore` (+ untrack). Ветка `main` protected → изменения в **PR #19** (`chore/gitignore-task-files`).
**Проверка:** `git check-ignore .task.md .task-arch.md` → matched. `git add docs/ src/ tests/` (scoped) не цепляют task-файлы.

### S-5 — машиночитаемый verdict ревьюера
**Было:** `check_reviewer_verdict` искал подстроки `APPROVED`/`REQUEST_CHANGES` во всём тексте (5000 байт) → ложные срабатывания на таблицах.
**Стало:** читается ТОЛЬКО `verdict:` из YAML-frontmatter `12-review.md` (через `yaml.safe_load`). Нет verdict / нет frontmatter → not-approved. `reviewer.md` обновлён: требование frontmatter `verdict: APPROVED|REQUEST_CHANGES`.
**Файлы:** `src/qg/checks.py` (`check_reviewer_verdict`), `enduro-trails/.openclaw/agents/reviewer.md` (PR #19; рабочая копия применена сразу).
**Проверка:** ET-009 `12-review.md` (frontmatter `verdict: APPROVED`) → `(True, 'Reviewer verdict: APPROVED')`. Unit-тесты покрывают APPROVED/REQUEST_CHANGES/no-verdict/no-frontmatter/таблица-в-теле.

### S-1 — QG тестов гоняет сам оркестратор (не Gitea CI)
**Было:** `development → review` QG = `check_ci_green` (Gitea status). CI не настроен → всегда false → автопереход не происходил + ложные «CI failed» алерты.
**Стало:** новый QG `check_tests_local` — оркестратор делает `git fetch/checkout <branch>` + `make test` в `/repos/<repo>`, judge по exit-code. `stages.py`: `development` QG → `check_tests_local`. Dispatch добавлен в `launcher._try_advance_stage` и `webhooks/plane._try_advance_stage` (args `(repo, branch)`). `webhooks/gitea.handle_ci_status`: `failure` → debug-лог, без `notify_error`.
**Файлы:** `src/qg/checks.py`, `src/stages.py`, `src/agents/launcher.py`, `src/webhooks/plane.py`, `src/webhooks/gitea.py`.
**Грабля (известное ограничение):** `check_tests_local` делает checkout в shared `/repos` — небезопасно при параллельных задачах (S-4 worktree — отдельно).

### M-1 — нормальный orphan-recovery
**Было:** `UPDATE agent_runs SET exit_code=-1 WHERE finished_at IS NULL AND started_at < now-35min` — молча списывал зомби.
**Стало:** перечисляем каждый orphan-run, помечаем exit=-1, логируем per-run `warning` («manual check needed»), отправляем Telegram-уведомление. Не автоперезапускаем (риск зацикливания). Killing по pid невозможен — pid не персистится в БД (задокументировано).
**Файл:** `src/main.py` (lifespan).

---

## Что НЕ входило (отдельные задачи)
- S-2/S-3 (rollback деплоера в shared-репо), S-4 (git worktree per task), M-3 (единый stage-engine), F-2b (очередь задач), M-7 (идемпотентность webhook). `_auto_merge_pr` — мёртвый код оставлен (отдельная чистка).

## Тесты
- Новый файл `tests/test_launcher.py`: 10 тестов (`_write_task_file` пишет/raise/без docker; `check_reviewer_verdict` frontmatter cases).
- `tests/test_qg.py`: 16 passed. `tests/test_launcher.py`: 10 passed.
- ⚠️ Pre-existing: `tests/test_webhooks.py` имеет падения (401/signature + cross-file env pollution) — НЕ связаны с этими фиксами, существовали до правок. Запуск в изоляции part-passes; в общем прогоне больше падений из-за общего env/DB между тест-файлами. Гигиена test_webhooks — отдельная задача.

## Деплой
Оркестратор пересобран: `cd /home/slin/repos/orchestrator && docker compose up -d --build`. Health: `{"status":"ok"}`.

---

## Дополнительно найдено и починено в ходе теста автономности

### git safe.directory (launcher commit/push)
В ходе теста выяснилось: git внутри контейнера (root) над bind-mounted `/repos` падал с "dubious ownership" → авто-commit/push агента не проходил. Фикс: `git config --system --add safe.directory "*"` в Dockerfile. Теперь `_monitor_agent` commit+push работает автономно (проверено: `analyst(ET): auto-commit run_id=47` запушен в origin).

### init:true (PID-1 reaper) — добиваем B-2
Прямой child (bash) reap-ался корректно через `proc.wait()`, НО claude (node) порождает свои дочерние процессы; при выходе bash они реparent-ились на PID 1 (uvicorn), который их НЕ reap-ал → grandchild-зомби. Фикс: `init: true` в docker-compose.yml — Docker внедряет `docker-init`(tini) как PID 1. Проверено: после реального прогона агента `ZOMBIE_COUNT_AFTER=0`.

## Тест автономности (Task 9) — РЕЗУЛЬТАТ
Запуск через `launcher.launch("analyst", ...)` (НЕ base64). Подтверждено автономно:
- B-1: свежий `.task.md` записан без docker (which docker = NO_DOCKER_BINARY)
- B-2: `exit_code=0` в `agent_runs` (run 46/47/48)
- зомби: 0 после прогона (tini reaper)
- git: auto-commit + push в origin отработал
- M-1: при рестарте orphan-recovery залогировал per-run + Telegram (runs 42/43/44 ET-009)
