Files
wiki/tasks/multi-agent/DEV_TASK_ORCHESTRATOR_FIXES.md
2026-06-02 20:00:01 +03:00

371 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/<repo>/<task_file>`, НЕ в 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/<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-пути:
```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` в контейнере не показывает `<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_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: <ID>
verdict: APPROVED # либо REQUEST_CHANGES
version: <N>
---
```
(аналогично 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/<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`:
```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)*