auto-sync: 2026-06-02 21:10:01
This commit is contained in:
276
tasks/orchestrator/DEV_TASK_ORCH2_WORKTREE.md
Normal file
276
tasks/orchestrator/DEV_TASK_ORCH2_WORKTREE.md
Normal file
@@ -0,0 +1,276 @@
|
||||
# DEV TASK: ORCH-2 [S-4] git worktree per task — изоляция shared /repos
|
||||
|
||||
**Статус:** Ready for dev
|
||||
**Проект:** orchestrator
|
||||
**Plane:** ORCH-2 (sequence #2, project id `8da6aa25-a60e-44d6-a1e2-d8ae59aa7d6a`)
|
||||
**Источник:** `tasks/orchestrator/AUDIT_2026-06-02.md` (SERIOUS S-4)
|
||||
**Исполнитель:** Dev-агент (model: tokenator/claude-opus-4-8)
|
||||
|
||||
---
|
||||
|
||||
## Цель
|
||||
|
||||
Каждая задача (по своей ветке) работает в **изолированной git worktree**, а не в общем
|
||||
`/repos/<repo>`. Убрать гонки `git checkout`, когда две задачи активны одновременно.
|
||||
Заодно это делает `check_tests_local` (S-1) безопасным при конкуренции.
|
||||
|
||||
## Проблема (корень)
|
||||
|
||||
Сейчас ВСЕ операции делают `git checkout <branch>` в одном `/repos/<repo>`:
|
||||
- `launcher.launch()` — строка ~110: `cd {local_repo_path} && git checkout {agent_branch}` (cmd агента)
|
||||
- `launcher._monitor_agent()` — строки ~238-244: checkout перед commit/push
|
||||
- `qg/checks.py check_tests_local()` — строки ~247: checkout перед прогоном тестов (есть TODO про S-4)
|
||||
- `webhooks/gitea.py` — строка ~152: git-операция в shared repo
|
||||
|
||||
При двух активных задачах checkout одной перетирает рабочую копию другой → хаос
|
||||
(на ET-009 это дало «два коллектора» и путаницу веток).
|
||||
|
||||
## Решение (архитектура)
|
||||
|
||||
**git worktree per branch.** Для ветки `<branch>` создаём изолированную рабочую копию:
|
||||
```
|
||||
/repos/<repo> ← bare-ish основной clone (остаётся, для fetch/worktree management)
|
||||
/repos/_wt/<repo>/<safe-branch> ← worktree конкретной задачи (рабочая копия агента)
|
||||
```
|
||||
Агент работает в своём worktree. Все git-операции задачи (checkout, commit, push, test) идут
|
||||
в worktree-пути, не в shared `/repos/<repo>`.
|
||||
|
||||
**Ключевой принцип:** ввести единую функцию получения рабочего пути задачи —
|
||||
`get_worktree_path(repo, branch)` — и заменить ВСЕ места, где сейчас
|
||||
`os.path.join(settings.repos_dir, repo)` используется как **рабочая копия для checkout/commit**,
|
||||
на путь worktree. (Места, где это просто чтение `docs/` для QG — тоже должны читать из worktree,
|
||||
т.к. артефакты агента лежат там.)
|
||||
|
||||
---
|
||||
|
||||
## Инфраструктура
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Сервер | `slin@82.22.50.71` (SSH key) |
|
||||
| Репо орка | `/home/slin/repos/orchestrator/` (remote: `admin/orchestrator`) |
|
||||
| Контейнер | `orchestrator` (port 8500) |
|
||||
| Volume | `/home/slin/repos` → `/repos` (RW) |
|
||||
| `settings.repos_dir` | `/repos` |
|
||||
| Health | `curl -s http://localhost:8500/health` |
|
||||
| Тесты | в контейнере: `docker run --rm -v /home/slin/repos/orchestrator:/code -w /code --entrypoint python3 $(docker inspect orchestrator --format '{{.Config.Image}}') -m pytest tests/ -q` |
|
||||
|
||||
⚠️ Хостовый `.venv` сломан (symlinks py3.10) — тесты гонять через образ (команда выше).
|
||||
|
||||
---
|
||||
|
||||
## Файловая карта
|
||||
|
||||
| Действие | Файл | Что |
|
||||
|----------|------|-----|
|
||||
| Создать | `src/git_worktree.py` | модуль управления worktree: ensure/remove/path |
|
||||
| Изменить | `src/agents/launcher.py` | `launch` cmd + `_monitor_agent` git-операции → worktree |
|
||||
| Изменить | `src/qg/checks.py` | `check_tests_local` + чтение артефактов → worktree |
|
||||
| Изменить | `src/webhooks/gitea.py` | git-операция (~152) → worktree |
|
||||
| Изменить | `src/config.py` | добавить `worktrees_dir` (напр. `/repos/_wt`) |
|
||||
| Создать | `tests/test_git_worktree.py` | покрытие модуля |
|
||||
| Изменить | `docs/ARCHITECTURE.md`, `BUGFIXES_*` | задокументировать |
|
||||
|
||||
---
|
||||
|
||||
## Задачи
|
||||
|
||||
### Task 1: контекст + новый модуль worktree
|
||||
|
||||
- [ ] **1.1** Прочитай `AUDIT_2026-06-02.md` (S-4) и текущий `launcher.py`, `qg/checks.py`. Пойми все checkout-точки.
|
||||
- [ ] **1.2** `git status` orchestrator — рабочее дерево должно быть чистым. Работай в ветке `feature/ORCH-2-worktree`.
|
||||
- [ ] **1.3** Добавить в `config.py`:
|
||||
```python
|
||||
worktrees_dir: str = "/repos/_wt"
|
||||
```
|
||||
- [ ] **1.4** Создать `src/git_worktree.py`:
|
||||
```python
|
||||
"""Git worktree management — isolated working copy per task/branch (ORCH-2 / S-4)."""
|
||||
import os, re, subprocess, logging
|
||||
from .config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def _safe(branch: str) -> str:
|
||||
"""Filesystem-safe branch name for path."""
|
||||
return re.sub(r"[^A-Za-z0-9._-]", "_", branch)
|
||||
|
||||
def get_worktree_path(repo: str, branch: str) -> str:
|
||||
"""Path of the worktree for (repo, branch). Does NOT create it."""
|
||||
return os.path.join(settings.worktrees_dir, repo, _safe(branch))
|
||||
|
||||
def ensure_worktree(repo: str, branch: str) -> str:
|
||||
"""Create (or reuse) an isolated worktree for branch. Returns its path.
|
||||
|
||||
Main clone stays at /repos/<repo>. Worktree lives at /repos/_wt/<repo>/<safe-branch>.
|
||||
"""
|
||||
main_repo = os.path.join(settings.repos_dir, repo)
|
||||
wt = get_worktree_path(repo, branch)
|
||||
if not os.path.isdir(main_repo):
|
||||
raise FileNotFoundError(f"Main repo not found: {main_repo}")
|
||||
# fetch latest in main clone
|
||||
subprocess.run(["git", "-C", main_repo, "fetch", "origin"],
|
||||
capture_output=True, timeout=60)
|
||||
if os.path.isdir(os.path.join(wt, ".git")) or os.path.isfile(os.path.join(wt, ".git")):
|
||||
# worktree exists — just update it
|
||||
subprocess.run(["git", "-C", wt, "fetch", "origin"], capture_output=True, timeout=60)
|
||||
subprocess.run(["git", "-C", wt, "checkout", branch], capture_output=True, timeout=30)
|
||||
subprocess.run(["git", "-C", wt, "reset", "--hard", f"origin/{branch}"],
|
||||
capture_output=True, timeout=30) # if remote branch exists
|
||||
return wt
|
||||
os.makedirs(os.path.dirname(wt), exist_ok=True)
|
||||
# try existing remote branch; else create new from origin/main
|
||||
r = subprocess.run(["git", "-C", main_repo, "worktree", "add", wt, branch],
|
||||
capture_output=True, text=True, timeout=60)
|
||||
if r.returncode != 0:
|
||||
# branch doesn't exist yet — create it from origin/main
|
||||
subprocess.run(["git", "-C", main_repo, "worktree", "add", "-b", branch, wt, "origin/main"],
|
||||
capture_output=True, text=True, timeout=60)
|
||||
logger.info(f"Worktree ready: {wt} (branch {branch})")
|
||||
return wt
|
||||
|
||||
def remove_worktree(repo: str, branch: str):
|
||||
"""Remove worktree after task done (optional cleanup)."""
|
||||
main_repo = os.path.join(settings.repos_dir, repo)
|
||||
wt = get_worktree_path(repo, branch)
|
||||
subprocess.run(["git", "-C", main_repo, "worktree", "remove", "--force", wt],
|
||||
capture_output=True, timeout=30)
|
||||
logger.info(f"Worktree removed: {wt}")
|
||||
```
|
||||
⚠️ **Грабля:** `git worktree` требует, чтобы основной `/repos/<repo>` был валидным git-репо (он есть). Одна ветка не может быть checked out в двух worktree одновременно — это нам и нужно (одна задача = одна ветка = один worktree).
|
||||
|
||||
**Критерий:** модуль создаёт изолированный worktree, повторный вызов переиспользует.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: launcher — агент работает в worktree
|
||||
|
||||
**Файл:** `src/agents/launcher.py`
|
||||
|
||||
- [ ] **2.1** В `launch()`: вместо `local_repo_path = os.path.join(settings.repos_dir, repo)` и последующего checkout в cmd — получить worktree:
|
||||
```python
|
||||
from ..git_worktree import ensure_worktree, get_worktree_path
|
||||
...
|
||||
agent_branch = ... # как сейчас (из tasks.branch, иначе нужно знать ветку)
|
||||
work_path = ensure_worktree(repo, agent_branch)
|
||||
```
|
||||
- [ ] **2.2** В cmd агента (строка ~110) убрать `git checkout` (worktree уже на нужной ветке), `cd` указывать на `work_path`:
|
||||
```python
|
||||
f'cd {work_path} && ' # без git fetch/checkout — ensure_worktree уже сделал
|
||||
```
|
||||
- [ ] **2.3** `_write_task_file` (B-1 fix) должен писать в **worktree**, не в shared repo:
|
||||
```python
|
||||
# теперь путь = get_worktree_path(repo, branch), а не repos_dir/repo
|
||||
```
|
||||
Передавай в `_write_task_file(repo, branch, task_file, content)` и пиши в `get_worktree_path(repo, branch)`.
|
||||
- [ ] **2.4** В `_monitor_agent` все git-операции (fetch/checkout/add/commit/push, строки ~235-271) — выполнять в worktree-пути (`work_path = get_worktree_path(repo, branch)`), не в `repos_dir/repo`. Checkout больше не нужен (worktree уже на ветке) — оставить только add/commit/push.
|
||||
|
||||
**Критерий:** агент пишет таск-файл и коммитит в свой worktree; shared `/repos/<repo>` не трогается checkout'ом.
|
||||
|
||||
---
|
||||
|
||||
### Task 3: QG checks — читать/тестировать из worktree
|
||||
|
||||
**Файл:** `src/qg/checks.py`
|
||||
|
||||
- [ ] **3.1** Функции, читающие артефакты агента (`check_analyst_docs`, `check_architect_docs`, `check_reviewer_verdict` и т.п.) сейчас берут `os.path.join(settings.repos_dir, repo)`. Поскольку артефакты теперь в worktree — заменить на `get_worktree_path(repo, branch)`.
|
||||
⚠️ Многие QG-функции принимают `(repo, work_item_id)` без `branch`. Нужно прокинуть `branch` туда, где читаются файлы. Проверь сигнатуры в `stages.py`/`QG_CHECKS` и в вызовах `_try_advance_stage` — добавь `branch` где нужно. **Не ломай обратную совместимость диспетчеризации QG** (см. как сейчас вызываются 2-арг и 3-арг checks).
|
||||
- [ ] **3.2** `check_tests_local(repo, branch)` (строки ~240-251): убрать собственный checkout, использовать `ensure_worktree(repo, branch)` и гонять `make test` в worktree-пути. Удалить TODO-комментарий про S-4 (он решён этой задачей).
|
||||
|
||||
**Критерий:** QG читает артефакты и гоняет тесты из worktree; нет shared-checkout.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: webhooks/gitea git-операция
|
||||
|
||||
**Файл:** `src/webhooks/gitea.py` (~строка 152)
|
||||
|
||||
- [ ] **4.1** Git-операция в shared repo → определить ветку и работать в worktree (или, если это просто `git branch -r --contains <sha>` для определения ветки — это read-only в main clone, оставить как есть; **проверь что именно там делается** и трогай только если это мутирующий checkout).
|
||||
|
||||
**Критерий:** нет мутирующих checkout в shared repo из webhook.
|
||||
|
||||
---
|
||||
|
||||
### Task 5: тесты + документация
|
||||
|
||||
- [ ] **5.1** `tests/test_git_worktree.py`: покрыть `_safe`, `get_worktree_path`, и `ensure_worktree`/`remove_worktree` (с временным git-репо в tmp, без сети — создать локальный репо, ветку, проверить что worktree появляется в отдельной директории и checkout ветки корректен). Моки для `fetch origin` где нет remote.
|
||||
- [ ] **5.2** Прогнать ВСЕ тесты (команда из «Инфраструктура») — зелёные. `test_webhooks.py` 9 pre-existing падений (401/signature) — не твои, не трогай, но и не сломай остальное.
|
||||
- [ ] **5.3** Обновить `docs/ARCHITECTURE.md`: раздел про worktree-изоляцию; убрать из «Известные ограничения» пункт про shared-checkout гонки (решён).
|
||||
- [ ] **5.4** Создать `docs/BUGFIXES_2026-06-03.md` (или дату прогона): что сделано по ORCH-2, как проверено.
|
||||
|
||||
**Критерий:** тесты зелёные, доки отражают worktree-модель.
|
||||
|
||||
---
|
||||
|
||||
### Task 6: деплой + проверка изоляции
|
||||
|
||||
- [ ] **6.1** Коммиты (Conventional Commits), push в ветку `feature/ORCH-2-worktree`, PR в `orchestrator`.
|
||||
- [ ] **6.2** Пересобрать/поднять: `cd /home/slin/repos/orchestrator && docker compose up -d --build && sleep 5 && curl -s http://localhost:8500/health`.
|
||||
- [ ] **6.3** **Тест изоляции (главный критерий):**
|
||||
```bash
|
||||
# создать worktree для двух разных веток и убедиться что они независимы
|
||||
docker exec orchestrator python3 -c "
|
||||
import sys; sys.path.insert(0,'/app')
|
||||
from src.git_worktree import ensure_worktree, get_worktree_path
|
||||
import subprocess
|
||||
p1 = ensure_worktree('enduro-trails','feature/wt-test-A')
|
||||
p2 = ensure_worktree('enduro-trails','feature/wt-test-B')
|
||||
print('A:', p1)
|
||||
print('B:', p2)
|
||||
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()
|
||||
print('branch A:', b1, '| branch B:', b2)
|
||||
assert p1 != p2 and b1 != b2, 'NOT ISOLATED'
|
||||
print('ISOLATION OK')
|
||||
"
|
||||
# cleanup тестовых worktree после
|
||||
docker exec orchestrator python3 -c "
|
||||
import sys; sys.path.insert(0,'/app')
|
||||
from src.git_worktree import remove_worktree
|
||||
remove_worktree('enduro-trails','feature/wt-test-A')
|
||||
remove_worktree('enduro-trails','feature/wt-test-B')
|
||||
print('cleaned')
|
||||
"
|
||||
```
|
||||
- [ ] **6.4** Отчитаться Стрим: что прошло, изоляция подтверждена.
|
||||
|
||||
**Критерий:** два worktree на разные ветки независимы; shared `/repos/<repo>` не мутируется.
|
||||
|
||||
---
|
||||
|
||||
## Acceptance (что Стрим проверит)
|
||||
|
||||
| # | Проверка | Ожидаемо |
|
||||
|---|----------|----------|
|
||||
| 1 | модуль worktree создаёт изолированные пути | A≠B, branch A≠B |
|
||||
| 2 | агент пишет/коммитит в worktree | не в shared repo |
|
||||
| 3 | check_tests_local в worktree | без shared checkout |
|
||||
| 4 | тесты орка | all pass (кроме 9 pre-existing webhook) |
|
||||
| 5 | health | ok |
|
||||
| 6 | shared /repos/<repo> не мутируется | `git -C /repos/enduro-trails branch --show-current` стабилен |
|
||||
| 7 | доки обновлены | worktree описан |
|
||||
|
||||
---
|
||||
|
||||
## Ограничения
|
||||
|
||||
- 🚫 **НЕ трогай:** nginx, openclaw.json, .env, deploy-хук `enduro-deploy-hook.sh`.
|
||||
- ⚠️ **Прокинь `branch` в QG-функции аккуратно** — там смешаны 2-арг и 3-арг checks, не сломай диспетчеризацию.
|
||||
- ⚠️ **Не ломай B-1/B-2/S-5/S-1 фиксы** из BUGFIXES_2026-06-02 — они должны продолжать работать, просто пути меняются на worktree.
|
||||
- ⚠️ Первый `git worktree add` для НЕсуществующей ветки — создавать от `origin/main`.
|
||||
- ⚠️ Cleanup worktree (`remove_worktree`) — вызывать опционально при переходе задачи в `done` (можно отдельным шагом, не обязательно в этой задаче, но модуль должен уметь).
|
||||
- 🚫 Очередь задач (ORCH-1 / F-2b) — НЕ в этой задаче. Только worktree-изоляция.
|
||||
|
||||
## Деплой-чеклист
|
||||
- [ ] `git_worktree.py` создан + тесты
|
||||
- [ ] launcher/checks/gitea переведены на worktree
|
||||
- [ ] B-1/B-2/S-1/S-5 продолжают работать
|
||||
- [ ] все тесты зелёные (кроме pre-existing webhook)
|
||||
- [ ] орк пересобран, health ok
|
||||
- [ ] тест изоляции пройден
|
||||
- [ ] доки + BUGFIXES обновлены
|
||||
- [ ] отчёт Стрим
|
||||
|
||||
---
|
||||
|
||||
*Создано: 2026-06-02 | Автор ТЗ: Стрим | Исполнитель: Dev (Opus 4.8 Tokenator)*
|
||||
Reference in New Issue
Block a user