# DEV TASK: Устранение багов оркестратора (по аудиту 2026-06-02) **Статус:** Ready for dev **Проект:** multi-agent (orchestrator) **Источник:** `tasks/multi-agent/AUDIT_2026-06-02.md` **Исполнитель:** Dev-агент (model: tokenator/claude-opus-4-8) --- ## Цель Вернуть автономность мультиагентного pipeline: после фиксов задача уровня ET-009 должна пройти analyst→architect→developer→reviewer→tester→deploy **без ручного запуска агентов**. ## Архитектура (корень проблем) Два BLOCKER-бага убивают автономность: 1. **Нет `docker` бинарника** в контейнере orchestrator → запись `.task-*.md` через `docker run` падает молча → агент читает старый таск-файл. 2. **`Popen` + PIPE + daemon-поток** → claude-процессы становятся зомби, `exit_code` теряется (в БД `exit=None`). Оба чинятся без docker: писать таск-файл напрямую в смонтированный volume `/repos`, а stdout агента перенаправлять сразу в файл на уровне ОС. ## Стек / Зависимости - Python 3.12, FastAPI, uvicorn - SQLite (`/app/data/orchestrator.db`) - Claude CLI (`/opt/claude-code/bin/claude.exe`) - Docker Compose (хост) --- ## Инфраструктура | Параметр | Значение | |----------|----------| | Сервер | `slin@82.22.50.71` (SSH key, без пароля) | | Sudo пароль (если нужен) | `motoZ@yaz2010` (env MVA154_SUDO_PASS) | | Репо оркестратора | `/home/slin/repos/orchestrator/` | | Репо проекта | `/home/slin/repos/enduro-trails/` | | Контейнер | `orchestrator` (network_mode: host, port 8500) | | Volume repos | `/home/slin/repos` → `/repos` (RW) внутри контейнера | | Деплой орка | `cd /home/slin/repos/orchestrator && docker compose up -d --build` | | Health | `curl -s http://localhost:8500/health` → `{"status":"ok"}` | | Тесты орка | `cd /home/slin/repos/orchestrator && .venv/bin/python -m pytest tests/ -v` | ⚠️ **ВАЖНО:** В репо orchestrator есть НЕЗАКОММИЧЕННЫЕ правки (`git status`: M launcher.py, config.py, notifications.py, plane_sync.py). **СНАЧАЛА** прочитай их (`git diff`), пойми что там, **не потеряй** при своих изменениях. Если правки осмысленные — закоммить их отдельным коммитом ПЕРЕД своими фиксами. --- ## Файловая карта | Действие | Файл | Ответственность | |----------|------|-----------------| | Изменить | `src/agents/launcher.py` | `_write_task_file` без docker; Popen stdout→файл; убрать PIPE-поток | | Изменить | `Dockerfile` | (опц.) если оставляешь docker-вариант — не нужно; основной путь без docker | | Изменить | `src/qg/checks.py` | `check_reviewer_verdict` читает `verdict:` из frontmatter | | Изменить | `src/stages.py` + `src/qg/checks.py` | QG тестов гоняет сам оркестратор (make test), не Gitea CI | | Изменить | `/repos/enduro-trails/.gitignore` | добавить `.task*.md` | | Изменить | `enduro-trails/.openclaw/agents/reviewer.md` | требовать `verdict:` во frontmatter | | Создать | `tests/test_launcher.py` | покрытие критичных функций launcher | | Изменить | `src/main.py` | orphan-recovery: реально проверять/убивать pid | --- ## Задачи ### Task 1: Прочитать контекст и закоммитить существующие правки **Шаги:** - [ ] **1.1** Прочитай `tasks/multi-agent/AUDIT_2026-06-02.md` целиком (он на твоём управляющем хосте у Стрим; если недоступен — Стрим передаст содержимое). Ключевые баги: B-1, B-2, B-3, S-1, S-5, M-1. - [ ] **1.2** На сервере: `cd /home/slin/repos/orchestrator && git diff` — изучи незакоммиченные правки в launcher.py, config.py, notifications.py, plane_sync.py. - [ ] **1.3** Если правки осмысленные и рабочие — закоммить: `git add -A && git commit -m "chore: save WIP changes before audit fixes"`. Если мусор — обсуди со Стрим, не удаляй молча. **Критерий готовности:** рабочее дерево orchestrator чистое, история сохранена. --- ### Task 2: B-1 — запись `.task-*.md` без docker **Файл:** `src/agents/launcher.py` метод `_write_task_file` **Проблема:** сейчас пишет через `docker run --rm -i python:3.12-slim bash -c "cat > {full_path}"`. Бинарника docker в контейнере НЕТ → падает молча. **Шаги:** - [ ] **2.1** Переписать `_write_task_file` на прямую запись в смонтированный volume. Репо смонтировано как `/repos` (это `settings.repos_dir`). Писать надо в `/repos//`, НЕ в host-путь. ```python def _write_task_file(self, repo: str, task_file: str, content: str): """Write task file directly to the mounted repo volume (/repos).""" container_repo_path = os.path.join(settings.repos_dir, repo) # /repos/ full_path = os.path.join(container_repo_path, task_file) try: with open(full_path, "w", encoding="utf-8") as f: f.write(content) logger.info(f"Task file written: {full_path} ({len(content)} bytes)") except OSError as e: logger.error(f"Failed to write task file {full_path}: {e}") raise RuntimeError(f"Failed to write task file: {e}") ``` - [ ] **2.2** Обнови вызов в `launch()`: сейчас `self._write_task_file(host_repo_path, config["task_file"], task_content)`. Поменяй на передачу `repo` (имя), а не host-пути: ```python if task_content: self._write_task_file(repo, config["task_file"], task_content) ``` - [ ] **2.3** Проверка записи: ```bash docker exec orchestrator python3 -c " import sys; sys.path.insert(0,'/app') 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 exec orchestrator rm /repos/enduro-trails/.task-test-write.md ``` **Критерий готовности:** таск-файл пишется без docker, при ошибке записи бросается исключение (не молчит). --- ### Task 3: B-2 — Popen stdout прямо в файл, убрать PIPE-поток **Файл:** `src/agents/launcher.py` методы `launch`, `_monitor_agent` **Проблема:** `Popen(stdout=PIPE)` + поток-читатель с select → PIPE-deadlock + зомби при рестарте. `exit_code` теряется. **Шаги:** - [ ] **3.1** В `launch()`: открывать лог-файл и передавать его дескриптор Popen напрямую: ```python # output_path уже формируется выше log_fh = open(output_path, "w") proc = subprocess.Popen( ["bash", "-c", cmd], stdout=log_fh, stderr=subprocess.STDOUT, env={...}, # как было ) # log_fh закроется в _monitor_agent после wait ``` - [ ] **3.2** Упростить `_monitor_agent`: убрать весь блок чтения PIPE (select/readline/startup-timeout по «отсутствию вывода»). Оставить: ```python def _monitor_agent(self, proc, run_id, agent, repo, branch, output_path=None, log_fh=None): import time as _time _start_ts = _time.time() exit_code = proc.wait() # синхронно ждём, без PIPE if log_fh: try: log_fh.close() except Exception: pass _duration_s = int(_time.time() - _start_ts) logger.info(f"Agent run_id={run_id} ({agent}) finished exit={exit_code}") # ... дальше как было: UPDATE agent_runs, notify, git commit/push, advance ``` - [ ] **3.3** Startup-timeout (агент молчит) больше НЕ нужен — watchdog по общему `AGENT_TIMEOUT` остаётся как есть (он по pid). Убедись, что watchdog корректно работает и не конфликтует. - [ ] **3.4** Прокинуть `log_fh` в поток `_monitor_agent` через `args`. **Критерий готовности:** после прогона агента в `agent_runs.exit_code` записывается реальный код (0 при успехе), лог-файл непустой, зомби не остаются (`ps` в контейнере не показывает `` claude после завершения). --- ### Task 4: B-3 — `.task-*.md` в gitignore + не коммитить **Шаги:** - [ ] **4.1** В `/repos/enduro-trails/.gitignore` добавить строку: ``` .task*.md ``` - [ ] **4.2** Убрать уже закоммиченные таск-файлы из индекса (если есть): на ветке main проверь `git ls-files | grep '.task'`. Если они трекаются — `git rm --cached .task-*.md` и закоммить «chore: stop tracking runtime task files». - [ ] **4.3** В `launcher._monitor_agent` git-commit логике: убедись, что `git add docs/` и `git add src/ tests/` НЕ цепляют `.task-*.md` (они и не должны после gitignore). **Критерий готовности:** `.task-*.md` не попадают в коммиты; `git status` чист после записи таск-файла. --- ### Task 5: S-5 — машиночитаемый verdict ревьюера **Файлы:** `src/qg/checks.py` (`check_reviewer_verdict`), `enduro-trails/.openclaw/agents/reviewer.md` **Проблема:** парсинг ищет подстроки `APPROVED`/`REQUEST_CHANGES` во всём тексте → ложные срабатывания на таблицах с этими словами. **Шаги:** - [ ] **5.1** В `reviewer.md` добавить требование: отчёт `12-review.md` ОБЯЗАН начинаться с YAML-frontmatter с полем `verdict`: ```markdown --- type: review work_item_id: verdict: APPROVED # либо REQUEST_CHANGES version: --- ``` (аналогично tester'у, у которого уже `verdict: PASS`) - [ ] **5.2** Переписать `check_reviewer_verdict` — читать ТОЛЬКО frontmatter: ```python def check_reviewer_verdict(repo, work_item_id): import yaml repo_path = os.path.join(settings.repos_dir, repo) review_path = os.path.join(repo_path, f"docs/work-items/{work_item_id}/12-review.md") if not os.path.isfile(review_path): return False, "Review report not found (12-review.md)" try: with open(review_path) as f: content = f.read() verdict = None if content.startswith("---"): parts = content.split("---", 2) if len(parts) >= 3: fm = yaml.safe_load(parts[1]) or {} verdict = str(fm.get("verdict", "")).upper() if verdict == "APPROVED": return True, "Reviewer verdict: APPROVED" if verdict == "REQUEST_CHANGES": return False, "Reviewer verdict: REQUEST_CHANGES" # Fallback (старый формат без frontmatter) — оставить старый парсинг как запасной return False, f"No machine-readable verdict in frontmatter (got: {verdict!r})" except Exception as e: return False, f"Error reading review: {e}" ``` - [ ] **5.3** Проверка на реальном файле ET-009: ```bash docker exec orchestrator python3 -c " import sys; sys.path.insert(0,'/app') from src.qg.checks import check_reviewer_verdict print(check_reviewer_verdict('enduro-trails','ET-009')) " ``` (если у ET-009 12-review.md нет frontmatter с verdict — это ок, fallback вернёт False; главное чтобы НЕ падало) **Критерий готовности:** verdict читается из frontmatter, таблицы с APPROVED/REQUEST_CHANGES в тексте больше не влияют. --- ### Task 6: S-1 — QG тестов гоняет сам оркестратор (не Gitea CI) **Файлы:** `src/qg/checks.py`, `src/stages.py` **Проблема:** `development → review` QG = `check_ci_green` дёргает Gitea status. CI в Gitea НЕ настроен → всегда false → автопереход не происходит, ложные алерты. **Шаги:** - [ ] **6.1** Добавить новый QG `check_tests_local` — оркестратор сам запускает тесты проекта: ```python def check_tests_local(repo, branch): """Run project test suite locally in /repos/, judge by exit code.""" import subprocess repo_path = os.path.join(settings.repos_dir, repo) try: # checkout нужной ветки subprocess.run(["git","-C",repo_path,"fetch","origin"], capture_output=True, timeout=30) subprocess.run(["git","-C",repo_path,"checkout",branch], capture_output=True, timeout=30) # запуск тестов (enduro-trails: make test) r = subprocess.run(["make","test"], cwd=repo_path, capture_output=True, text=True, timeout=600) if r.returncode == 0: return True, "Local tests passed" tail = (r.stdout + r.stderr)[-500:] return False, f"Local tests failed: ...{tail}" except subprocess.TimeoutExpired: return False, "Local tests timed out" except Exception as e: return False, f"Local test run error: {e}" ``` ⚠️ **Грабля:** локальный прогон тестов в shared `/repos` делает checkout — на этом этапе других активных задач быть не должно (см. S-4, отдельная задача worktree — НЕ в этом ТЗ). Пока что приемлемо. - [ ] **6.2** В `stages.py` поменять QG перехода `development`: ```python "development": {"next": "review", "agent": "reviewer", "qg": "check_tests_local"}, ``` - [ ] **6.3** Зарегистрировать в `QG_CHECKS`: `"check_tests_local": check_tests_local`. - [ ] **6.4** В `launcher._try_advance_stage` и `webhooks/plane._try_advance_stage` добавить ветку аргументов: `check_tests_local` принимает `(repo, branch)` как `check_ci_green`. - [ ] **6.5** Убрать/смягчить ложные «CI failed» алерты в `webhooks/gitea.handle_ci_status` — если CI не сконфигурирован, не слать notify_error. (Можно: реагировать на `status` только если task в development И state явно `failure` с непустым контекстом; иначе debug-лог.) **Критерий готовности:** переход development→review происходит после успешного локального прогона тестов, без зависимости от Gitea CI; ложных «CI failed» нет. --- ### Task 7: M-1 — нормальный orphan-recovery **Файл:** `src/main.py` lifespan **Проблема:** сейчас просто `UPDATE ... exit_code=-1 WHERE finished_at IS NULL AND started_at < now-35min` — маскирует зомби, не убивает, не уведомляет. **Шаги:** - [ ] **7.1** При старте — для каждого orphan-run: пометить exit=-1 (как сейчас) И поставить связанную задачу в стейт, требующий внимания (лог + Telegram-уведомление, что задача X прервана и нужен ручной перезапуск/проверка). НЕ пытайся автоперезапускать (можно зациклить). - [ ] **7.2** Лог явно: `logger.warning(f"Orphan run {id} (task {tid}, agent {agent}) recovered — manual check needed")`. **Критерий готовности:** orphan'ы не молча списываются — есть уведомление. --- ### Task 8: тесты + документация **Шаги:** - [ ] **8.1** Создать `tests/test_launcher.py`: покрыть `_write_task_file` (пишет в правильный путь, бросает при ошибке) и парсинг verdict (`check_reviewer_verdict` с frontmatter APPROVED/REQUEST_CHANGES/без verdict). Использовать tmp-пути/моки, без реального запуска claude. - [ ] **8.2** Прогнать всё: `cd /home/slin/repos/orchestrator && .venv/bin/python -m pytest tests/ -v` — всё зелёное. - [ ] **8.3** Обновить `docs/ARCHITECTURE.md` и `README.md`: - Убрать упоминание, что `.task` пишется через docker. - Исправить таблицу QG: `development` QG теперь `check_tests_local` (не `check_ci_green`); `review` QG = `check_reviewer_verdict` (в README сейчас ошибочно `check_review_approved`). - Добавить раздел «Известные ограничения» (shared /repos checkout — гонки при параллельных задачах; worktree запланирован отдельно). - [ ] **8.4** Создать `docs/BUGFIXES_2026-06-02.md` — что починено (B-1, B-2, B-3, S-1, S-5, M-1), как проверено. **Критерий готовности:** тесты зелёные, доки отражают реальность. --- ### Task 9: Деплой и проверка автономности **Шаги:** - [ ] **9.1** Закоммитить всё (Conventional Commits, отдельные коммиты по задачам), запушить в main orchestrator. - [ ] **9.2** Пересобрать и поднять оркестратор: ```bash cd /home/slin/repos/orchestrator && docker compose up -d --build sleep 5 && curl -s http://localhost:8500/health ``` - [ ] **9.3** **Тест автономности (главный критерий):** запустить тестовую задачу через нормальный путь (НЕ ручной base64). Способ согласуй со Стрим — вариант: создать Plane work item или дёрнуть `launcher.launch("analyst", ...)` штатно и проверить, что: - таск-файл записался свежий (B-1) - агент отработал, `exit_code=0` в БД (B-2) - зомби не осталось - автопереход на следующую стадию сработал - [ ] **9.4** Отчитаться Стрим: что прошло автономно, что нет. **Критерий готовности:** хотя бы один полный stage-переход прошёл БЕЗ ручного запуска агента. --- ## Проверка (Acceptance) — что Стрим проверит | # | Проверка | Команда | Ожидаемо | |---|----------|---------|----------| | 1 | Запись таск-файла без docker | Task 2.3 | файл создан, без docker | | 2 | exit_code пишется | прогон агента → `SELECT exit_code FROM agent_runs ORDER BY id DESC LIMIT 1` | не NULL | | 3 | Нет зомби | `docker exec orchestrator ps aux \| grep defunct` | пусто после завершения | | 4 | verdict из frontmatter | Task 5.3 | читается корректно | | 5 | tests-QG локально | переход dev→review | по make test | | 6 | `.task` не в git | `git status` после записи | clean | | 7 | тесты орка зелёные | `pytest tests/ -v` | all pass | | 8 | health | `curl :8500/health` | `{"status":"ok"}` | | 9 | доки обновлены | чтение ARCHITECTURE/README | соответствуют коду | --- ## Ограничения и контекст - ⚠️ **НЕ потеряй незакоммиченные правки** в orchestrator (Task 1) — сначала разберись с ними. - ⚠️ **Запись только в `/repos`** (смонтированный volume), НЕ в host-пути — внутри контейнера host-путь невалиден. - ⚠️ **Docker внутри контейнера orchestrator НЕДОСТУПЕН** — не закладывайся на docker-команды в коде орка. Деплой проекта идёт через SSH-хук `/home/slin/bin/enduro-deploy-hook.sh` (он рабочий, не трогай). - ⚠️ **shared `/repos` checkout** — git worktree НЕ в этом ТЗ (отдельная задача S-4). Просто не ломай текущее поведение. - ⚠️ **Deploy-хук существует и корректен** — `enduro-deploy-hook.sh` делает `git pull + docker compose up -d app [+ gps-collector]`. НЕ переписывай. - 🚫 **НЕ трогай** nginx, `openclaw.json`, секреты в `.env`. - 🚫 **НЕ меняй** Plane states mapping без необходимости. - 🚫 **НЕ удаляй** мёртвый код `_auto_merge_pr` молча — оставь, отметь TODO (отдельная чистка). ## Что НЕ входит в это ТЗ (отдельные задачи на потом) - S-2/S-3 (rollback деплоера в shared-репо) — отдельно - S-4 (git worktree per task) — отдельно, крупное - M-3 (единый stage-engine, дубль `_try_advance_stage`) — желательно, но осторожно; если успеешь и уверен — можно, иначе отдельно - F-2b (очередь задач вместо daemon-потоков) — крупный рефактор, отдельно - M-7 (идемпотентность webhook) — отдельно --- ## Деплой-чеклист - [ ] Существующие WIP-правки разобраны и закоммичены - [ ] B-1, B-2, B-3, S-5, S-1, M-1 реализованы - [ ] Тесты launcher созданы и зелёные - [ ] Доки обновлены (ARCHITECTURE, README, BUGFIXES_2026-06-02) - [ ] orchestrator пересобран и поднят, health ok - [ ] Тест автономности пройден (хотя бы 1 stage без ручника) - [ ] Нет ошибок в логах (`docker logs orchestrator --tail 50`) - [ ] Отчёт Стрим: что автономно, что нет --- *Создано: 2026-06-02 | Автор ТЗ: Стрим | Исполнитель: Dev-агент (Opus 4.8 Tokenator)*