23 KiB
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-бага убивают автономность:
- Нет
dockerбинарника в контейнере orchestrator → запись.task-*.mdчерезdocker runпадает молча → агент читает старый таск-файл. 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/<repo>/<task_file>, НЕ в host-путь.
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/<repo>
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-пути:
if task_content:
self._write_task_file(repo, config["task_file"], task_content)
- 2.3 Проверка записи:
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 напрямую:
# 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 по «отсутствию вывода»). Оставить:
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 в контейнере не показывает <defunct> 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_agentgit-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:
---
type: review
work_item_id: <ID>
verdict: APPROVED # либо REQUEST_CHANGES
version: <N>
---
(аналогично tester'у, у которого уже verdict: PASS)
- 5.2 Переписать
check_reviewer_verdict— читать ТОЛЬКО frontmatter:
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:
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— оркестратор сам запускает тесты проекта:
def check_tests_local(repo, branch):
"""Run project test suite locally in /repos/<repo>, 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:
"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:
developmentQG теперьcheck_tests_local(неcheck_ci_green);reviewQG =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 Пересобрать и поднять оркестратор:
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
/reposcheckout — 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)