# 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//` обычным `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 ` + `make test` в `/repos/`, 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)