Files
wiki/tasks/orchestrator/DEV_TASK_ORCH2_WORKTREE.md
2026-06-02 21:10:03 +03:00

17 KiB
Raw Permalink Blame History

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:
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.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 Тест изоляции (главный критерий):
# создать 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)