- 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).
82 lines
5.2 KiB
Markdown
82 lines
5.2 KiB
Markdown
# 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` не подключён (опционально, отдельным шагом).
|