feat(worktree): git worktree per task to isolate shared /repos (ORCH-2 / S-4)

- add src/git_worktree.py: ensure/remove/get_worktree_path
- config: worktrees_dir=/repos/_wt
- launcher: agent runs in per-branch worktree; task-file + commit/push in worktree; no shared checkout
- qg/checks: read artifacts + run make test from worktree (branch arg, backward-compatible)
- webhooks/plane: pass branch into QG dispatch; review fallback from worktree
- webhooks/gitea: keep read-only branch --contains in main clone (documented)
- tests: test_git_worktree.py (isolation) + update test_launcher write-task-file
- docs: ARCHITECTURE worktree section + BUGFIXES_2026-06-02_ORCH2

Preserves B-1/B-2/S-1/S-5 fixes (paths now point at worktree).
This commit is contained in:
Dev Agent
2026-06-02 21:12:06 +03:00
parent 66a37612fd
commit 1ebe8afc23
10 changed files with 474 additions and 89 deletions

View File

@@ -39,7 +39,7 @@ STAGE_TRANSITIONS = {
|-------|---------------|
| check_analysis_approved | Filesystem: 4 файла + :approved: comment в Plane |
| check_architecture_done | Filesystem: ADR dir или infra-requirements.md |
| check_tests_local | Оркестратор сам гоняет `make test` в `/repos/<repo>` (judge по exit-code). Заменил check_ci_green: Gitea CI не сконфигурирован. |
| check_tests_local | Оркестратор сам гоняет `make test` в **worktree задачи** `/repos/_wt/<repo>/<branch>` (judge по exit-code). Заменил check_ci_green: Gitea CI не сконфигурирован. Worktree-изоляция → безопасно при параллельных задачах (ORCH-2 / S-4). |
| 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 развития |
@@ -188,7 +188,7 @@ services:
Каждый агент — Claude CLI с:
- **System prompt**: `.openclaw/agents/{role}.md` (в репозитории)
- **Task file**: `.task-{suffix}.md` — генерируется orchestrator **прямой записью в смонтированный volume `/repos/<repo>/`** (B-1, без docker). В `.gitignore` репозитория проекта (рантайм-артефакт, не коммитится).
- **Task file**: `.task-{suffix}.md` — генерируется orchestrator **прямой записью в worktree задачи** `/repos/_wt/<repo>/<branch>/` (B-1, без docker; ORCH-2 — в изолированную рабочую копию, не в shared `/repos/<repo>`). В `.gitignore` репозитория проекта (рантайм-артефакт, не коммитится).
- **Tools**: Read, Write, Edit, Bash
- **Output**: `--print` mode (весь вывод в stdout после завершения)
@@ -201,13 +201,39 @@ services:
| tester | test-report.md, e2e results | 10-25 мин |
| deployer | merge PR + SSH deploy-hook + smoke | 5-10 мин |
## Изоляция через git worktree (ORCH-2 / S-4)
Каждая задача (= одна git-ветка) работает в **изолированной git worktree**, а не в общем
`/repos/<repo>`. Это убирает гонки `git checkout`, когда две задачи активны одновременно.
```
/repos/<repo> ← основной clone (fetch / управление worktree, read-only запросы)
/repos/_wt/<repo>/<safe-branch> ← worktree конкретной задачи (рабочая копия агента)
```
Модуль `src/git_worktree.py`:
- `get_worktree_path(repo, branch)` — путь worktree (не создаёт).
- `ensure_worktree(repo, branch)` — создаёт (или переиспользует) worktree на нужной ветке;
для новой ветки создаёт её от `origin/main`. Возвращает путь.
- `remove_worktree(repo, branch)` — опциональная очистка при `done`.
Где используется worktree:
- **launcher**: агент запускается с `cd <worktree>` (без `git checkout` в cmd); task-файл
пишется в worktree; commit/push в `_monitor_agent` идут в worktree.
- **qg/checks**: чтение артефактов агента (`check_analysis_complete`, `check_architecture_done`,
`check_tests_passed`, `check_reviewer_verdict`) и `check_tests_local` (`make test`) — из worktree.
Артефакт-функции принимают опциональный `branch`; без него падают на shared `/repos/<repo>`
(обратная совместимость).
- **webhooks/gitea**: `git branch -r --contains <sha>` оставлен в основном clone — это
**read-only** запрос (нет checkout/мутации), гонок не создаёт.
> Один branch может быть checked out только в одной worktree одновременно —
> это и есть нужное свойство: одна задача = одна ветка = одна worktree.
## Известные ограничения
- **Shared `/repos` checkout (гонки при параллельных задачах).** Все агенты и
`check_tests_local` делают `git checkout` в одном `/repos/<repo>`. При двух
одновременно активных задачах checkout одной перетрёт рабочую копию другой.
Пока приемлемо (задачи идут последовательно). **Исправление — git worktree per task/branch
(запланировано отдельной задачей S-4).**
- ~~Shared `/repos` checkout (гонки при параллельных задачах).~~ **РЕШЕНО (ORCH-2 / S-4):**
git worktree per task/branch — см. раздел «Изоляция через git worktree» ниже.
- **In-process daemon-потоки.** Агенты запускаются в daemon-потоках uvicorn. При
рестарте uvicorn запущенные агенты осиротевают → ловит orphan-recovery (M-1).
Целевая архитектура — очередь задач (F-2b, отдельно).

View File

@@ -0,0 +1,81 @@
# ORCH-2 / S-4 — git worktree per task (изоляция shared /repos)
**Дата:** 2026-06-02
**Ветка:** `feature/ORCH-2-worktree`
**Источник:** `AUDIT_2026-06-02.md` (SERIOUS S-4), `DEV_TASK_ORCH2_WORKTREE.md`
**Исполнитель:** Dev (Opus 4.8 Tokenator)
## Проблема (S-4)
Все git-операции (`launcher.launch` cmd, `_monitor_agent` commit/push, `check_tests_local`)
делали `git checkout <branch>` в одном общем `/repos/<repo>`. При двух активных задачах
checkout одной перетирал рабочую копию другой → гонки (на ET-009 это дало «два коллектора»
и путаницу веток).
## Решение
**git worktree per branch.** Каждая задача (ветка) работает в изолированной рабочей копии:
```
/repos/<repo> ← основной clone (fetch / worktree mgmt / read-only)
/repos/_wt/<repo>/<safe-branch> ← worktree задачи (рабочая копия агента)
```
## Изменения
| Файл | Что |
|------|-----|
| `src/config.py` | + `worktrees_dir: str = "/repos/_wt"` |
| `src/git_worktree.py` (новый) | `_safe`, `get_worktree_path`, `ensure_worktree`, `remove_worktree` |
| `src/agents/launcher.py` | `launch()`: ветка резолвится заранее → `ensure_worktree`; cmd = `cd <worktree>` без `git checkout`; `_write_task_file(repo, branch, ...)` пишет в worktree; `_monitor_agent` commit/push в worktree (checkout убран); чтение `01-questions.md`/`10-conflict.md` из worktree; QG-диспетчер прокидывает `branch` |
| `src/qg/checks.py` | `_repo_path(repo, branch)` helper (worktree если есть, иначе shared); артефакт-чеки получили опциональный `branch`; `check_tests_local``ensure_worktree` + `make test` в worktree (TODO про S-4 удалён) |
| `src/webhooks/plane.py` | QG-диспетчер прокидывает `branch`; review-файл fallback читается из worktree |
| `src/webhooks/gitea.py` | `git branch -r --contains <sha>` — подтверждено read-only, оставлено в main clone (+ комментарий) |
| `tests/test_git_worktree.py` (новый) | покрытие `_safe`/`get_worktree_path`/`ensure_worktree`/`remove_worktree` + изоляция двух веток (реальные локальные git-репо в tmp, без сети) |
| `tests/test_launcher.py` | `TestWriteTaskFile` обновлён под новую сигнатуру (запись в worktree) |
| `docs/ARCHITECTURE.md` | раздел «Изоляция через git worktree»; убран пункт про shared-checkout гонки |
## Совместимость с прежними фиксами
- **B-1** (запись task-файла без docker, прямой `open()`): сохранена — теперь путь = worktree.
- **B-2** (Popen stdout → файл, monitor `proc.wait()` без зомби): не тронут.
- **S-5** (`check_reviewer_verdict` — только YAML-frontmatter): не тронут, добавлен лишь worktree-путь.
- **S-1** (`check_tests_local` — свой `make test` вместо Gitea CI): сохранён, тесты теперь в worktree.
Обратная совместимость QG-диспетчеризации: артефакт-чеки принимают `branch` опционально
(default `None` → shared `/repos/<repo>`), поэтому существующие 2-арг вызовы/тесты не сломаны.
## Проверка
```bash
# Тесты (в контейнере через образ — хостовый .venv сломан):
IMG=$(docker inspect orchestrator --format '{{.Config.Image}}')
docker run --rm -v /home/slin/repos/orchestrator:/code -w /code --entrypoint python3 $IMG -m pytest tests/ -q
# → 37 passed, 9 failed (pre-existing test_webhooks 401/signature — НЕ относятся к ORCH-2,
# идентичны baseline на main).
# test_git_worktree.py изолированно → 9 passed.
```
### Тест изоляции (в работающем контейнере)
```bash
docker exec orchestrator python3 -c "
import sys; sys.path.insert(0,'/app')
from src.git_worktree import ensure_worktree
import subprocess
p1 = ensure_worktree('enduro-trails','feature/wt-test-A')
p2 = ensure_worktree('enduro-trails','feature/wt-test-B')
b1 = subprocess.run(['git','-C',p1,'branch','--show-current'],capture_output=True,text=True).stdout.strip()
b2 = subprocess.run(['git','-C',p2,'branch','--show-current'],capture_output=True,text=True).stdout.strip()
assert p1!=p2 and b1!=b2, 'NOT ISOLATED'
print('ISOLATION OK', p1, p2, b1, b2)
"
```
(Результат прогона на сервере — см. ниже / в отчёте Стрим.)
## Ограничения / заметки
- Очередь задач (ORCH-1 / F-2b) **не** входит в эту задачу.
- `remove_worktree` существует, но автоматический вызов при `done` не подключён (опционально, отдельным шагом).