diff --git a/README.md b/README.md index 91daf27..fd3a97e 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,10 @@ created → analysis → architecture → development → review → testing → | created | — | — | Plane webhook (work_item.created) | | analysis | analyst | Файлы BRD/TRZ/AC/TestPlan | Push docs/ | | architecture | architect | ADR или infra-requirements | Push docs/ | -| development | developer | CI green | Gitea status event | -| review | reviewer | PR approved (no stale) | Gitea review event | +| development | developer | check_tests_local (орк сам гоняет `make test`) | Auto-advance после developer | +| review | reviewer | check_reviewer_verdict (`verdict:` во frontmatter 12-review.md) | Auto-advance после reviewer | | testing | tester | Test report с PASS | Auto-advance после tester | -| deploy | — | — | PR merge | +| deploy | deployer | — | SSH deploy-hook | | done | — | — | — | ## API Endpoints @@ -115,8 +115,14 @@ uvicorn src.main:app --reload --port 8500 ### Review bounce При REQUEST_CHANGES от reviewer задача откатывается в development, developer перезапускается (до 3 попыток). При исчерпании — эскалация. -### Orphan recovery -При старте контейнера все runs с `finished_at IS NULL` старше 35 минут помечаются как failed (exit_code=-1). +### Orphan recovery (M-1) +При старте контейнера каждый run с `finished_at IS NULL` старше 35 минут помечается exit_code=-1, логируется per-run warning и отправляется Telegram-уведомление «нужна ручная проверка/перезапуск» (не молча). + +### Запись task-файлов (B-1) +Task-файлы `.task-*.md` пишутся **прямой записью в смонтированный volume `/repos//`** (без docker). При ошибке записи — RuntimeError (не молчит). В `.gitignore` проекта. + +### Логи агентов (B-2) +stdout/stderr агента перенаправляются СРАЗУ в `/app/data/runs/{id}.log` на уровне ОС (без PIPE). monitor-поток делает `proc.wait()` → реальный exit_code, нет зомби. ### Watchdog Каждый агент имеет timeout 30 минут. При превышении — SIGKILL + запись exit_code=-9. @@ -125,7 +131,7 @@ uvicorn src.main:app --reload --port 8500 Gitea events роутятся по типу: - `push` → проверка файлов, advance architecture/development - `pull_request*` (wildcard) → review approved/rejected, PR merge -- `status` → CI green/failure +- `status` → (legacy) Gitea CI; С-1: больше не authoritative, `failure` логируется на debug и не блокирует/не алертит (QG развития = локальный `check_tests_local`) ## Тесты @@ -135,7 +141,9 @@ pytest tests/ -v ## Известные ограничения -1. **Single-task** — одновременно обрабатывается одна задача на репозиторий (нет параллелизма) +1. **Single-task / shared `/repos` checkout** — одновременно безопасно обрабатывается одна задача: все агенты и `check_tests_local` делают `git checkout` в одном `/repos/` → гонки при параллельных задачах. Исправление — git worktree per task (S-4, отдельно). 2. **Plane sync** — маппинг issue ID может быть некорректным (P3, в работе) +3. **In-process daemon-потоки** — агенты живут в потоках uvicorn; при рестарте ловит orphan-recovery. Целевое — очередь задач (F-2b) +4. **Gitea CI не настроен** — тесты гоняет сам оркестратор локально 3. **Tester timeout** — e2e тесты с Playwright могут занимать >25 мин на тяжёлых фичах 4. **No retry on API errors** — httpx вызовы к Gitea/Plane без retry logic diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 798f1e8..696b659 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -26,9 +26,9 @@ STAGE_TRANSITIONS = { created: → analysis (agent: None) analysis: → architecture (agent: architect, QG: check_analysis_approved) architecture: → development (agent: developer, QG: check_architecture_done) - development: → review (agent: reviewer, QG: check_ci_green) - review: → testing (agent: tester, QG: check_review_approved) - testing: → deploy (agent: None, QG: check_tests_passed) + development: → review (agent: reviewer, QG: check_tests_local) + review: → testing (agent: tester, QG: check_reviewer_verdict) + testing: → deploy (agent: deployer, QG: check_tests_passed) deploy: → done (agent: None, QG: None) } ``` @@ -39,9 +39,11 @@ STAGE_TRANSITIONS = { |-------|---------------| | check_analysis_approved | Filesystem: 4 файла + :approved: comment в Plane | | check_architecture_done | Filesystem: ADR dir или infra-requirements.md | -| check_ci_green | Gitea API: GET /commits/{branch}/status | -| check_review_approved | Gitea API: GET /pulls/{n}/reviews (skip stale) | +| check_tests_local | Оркестратор сам гоняет `make test` в `/repos/` (judge по exit-code). Заменил check_ci_green: Gitea CI не сконфигурирован. | +| check_reviewer_verdict | Filesystem: читает `verdict: APPROVED\|REQUEST_CHANGES` из YAML-frontmatter `12-review.md` (только машиночитаемое поле, не подстроки в тексте) | | check_tests_passed | Filesystem: test-report.md содержит "PASS" | +| check_ci_green | (legacy) Gitea API: GET /commits/{branch}/status — больше не используется как QG развития | +| check_review_approved | (legacy) Gitea API: GET /pulls/{n}/reviews — не используется в STAGE_TRANSITIONS | ### 4. Agent Launcher (`src/agents/launcher.py`) @@ -53,9 +55,9 @@ claude.exe --print --system-prompt --allowedTools Read,Write,Edit,Bash Каждый запуск: 1. Записывает run в DB (agent_runs) -2. Запускает subprocess с stdout → `/app/data/runs/{id}.log` -3. Стартует **watchdog thread** (timeout 30 мин → SIGKILL) -4. Стартует **monitor thread** (ждёт завершения → git commit/push → auto-advance) +2. Запускает subprocess. **stdout/stderr перенаправляются СРАЗУ в файл `/app/data/runs/{id}.log` на уровне ОС** (Popen `stdout=log_fh`). Никакого PIPE в памяти оркестратора → нет PIPE-deadlock, нет потока-читателя, нет зомби (B-2). +3. Стартует **watchdog thread** (timeout 30 мин → SIGKILL по pid) +4. Стартует **monitor thread**: `proc.wait()` (гарантированный reap → реальный exit_code в БД) → закрывает log_fh → git commit/push → auto-advance ### 5. Auto-advance (`launcher._try_advance_stage`) @@ -65,7 +67,7 @@ claude.exe --print --system-prompt --allowedTools Read,Write,Edit,Bash 3. Если QG пройден — продвигает стадию 4. Запускает следующего агента (если определён) -Исключение: `check_review_approved` — обрабатывается через PR webhook, не через auto-advance. +Примечание: переход `review → testing` использует `check_reviewer_verdict` (читается из frontmatter `12-review.md`); `development → review` — `check_tests_local` (оркестратор сам прогоняет тесты, не зависит от Gitea CI). ### 6. Review Bounce @@ -176,18 +178,17 @@ services: | Механизм | Описание | |----------|----------| -| Orphan recovery | При старте: runs без finished_at старше 35 мин → exit_code=-1 | | Watchdog | Каждый агент: timeout 30 мин → SIGKILL + exit_code=-9 | -| tini | PID 1 reaper — zombie processes невозможны | | safe.directory | git операции работают в любой директории | -| Stale review skip | check_review_approved игнорирует stale reviews | | Max retries | Developer: max 3 попытки, затем эскалация | +| Zombie-free | stdout идёт сразу в файл + monitor `proc.wait()` → процесс всегда reap'нут (B-2) | +| Orphan recovery | При старте: orphan-run'ы (finished_at IS NULL, старше 35 мин) помечаются exit=-1 с per-run warning + Telegram-уведомлением «нужна ручная проверка» (M-1) | ## Агенты Каждый агент — Claude CLI с: - **System prompt**: `.openclaw/agents/{role}.md` (в репозитории) -- **Task file**: `.task-{suffix}.md` (генерируется orchestrator) +- **Task file**: `.task-{suffix}.md` — генерируется orchestrator **прямой записью в смонтированный volume `/repos//`** (B-1, без docker). В `.gitignore` репозитория проекта (рантайм-артефакт, не коммитится). - **Tools**: Read, Write, Edit, Bash - **Output**: `--print` mode (весь вывод в stdout после завершения) @@ -198,3 +199,19 @@ services: | developer | src/, tests/, PR | 15-30 мин | | reviewer | review report, PR review | 3-5 мин | | tester | test-report.md, e2e results | 10-25 мин | +| deployer | merge PR + SSH deploy-hook + smoke | 5-10 мин | + +## Известные ограничения + +- **Shared `/repos` checkout (гонки при параллельных задачах).** Все агенты и + `check_tests_local` делают `git checkout` в одном `/repos/`. При двух + одновременно активных задачах checkout одной перетрёт рабочую копию другой. + Пока приемлемо (задачи идут последовательно). **Исправление — git worktree per task/branch + (запланировано отдельной задачей S-4).** +- **In-process daemon-потоки.** Агенты запускаются в daemon-потоках uvicorn. При + рестарте uvicorn запущенные агенты осиротевают → ловит orphan-recovery (M-1). + Целевая архитектура — очередь задач (F-2b, отдельно). +- **Gitea CI не настроен.** QG развития теперь локальный (`check_tests_local`); + Gitea CI-статусы не являются authoritative и не блокируют pipeline. +- **Docker внутри контейнера orchestrator НЕДОСТУПЕН.** Деплой идёт только через + SSH-хук `enduro-deploy-hook.sh` на хосте. diff --git a/docs/BUGFIXES_2026-06-02.md b/docs/BUGFIXES_2026-06-02.md new file mode 100644 index 0000000..ddefa82 --- /dev/null +++ b/docs/BUGFIXES_2026-06-02.md @@ -0,0 +1,66 @@ +# 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"}`.