371 lines
23 KiB
Markdown
371 lines
23 KiB
Markdown
# 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)*
|