# 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/`. Убрать гонки `git checkout`, когда две задачи активны одновременно. Заодно это делает `check_tests_local` (S-1) безопасным при конкуренции. ## Проблема (корень) Сейчас ВСЕ операции делают `git checkout ` в одном `/repos/`: - `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.** Для ветки `` создаём изолированную рабочую копию: ``` /repos/ ← bare-ish основной clone (остаётся, для fetch/worktree management) /repos/_wt// ← worktree конкретной задачи (рабочая копия агента) ``` Агент работает в своём worktree. Все git-операции задачи (checkout, commit, push, test) идут в worktree-пути, не в shared `/repos/`. **Ключевой принцип:** ввести единую функцию получения рабочего пути задачи — `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/. Worktree lives at /repos/_wt//. """ 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/` был валидным 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/` не трогается 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 ` для определения ветки — это 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/` не мутируется. --- ## 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/ не мутируется | `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)*