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:
@@ -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, отдельно).
|
||||
|
||||
81
docs/BUGFIXES_2026-06-02_ORCH2.md
Normal file
81
docs/BUGFIXES_2026-06-02_ORCH2.md
Normal 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` не подключён (опционально, отдельным шагом).
|
||||
Reference in New Issue
Block a user