17 KiB
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/pushqg/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 statusorchestrator — рабочее дерево должно быть чистым. Работай в веткеfeature/ORCH-2-worktree. - 1.3 Добавить в
config.py:
worktrees_dir: str = "/repos/_wt"
- 1.4 Создать
src/git_worktree.py:
"""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:
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:
f'cd {work_path} && ' # без git fetch/checkout — ensure_worktree уже сделал
- 2.3
_write_task_file(B-1 fix) должен писать в worktree, не в shared repo:
# теперь путь = 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.py9 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 Тест изоляции (главный критерий):
# создать 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/ не мутируется | 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)