From 8cb3158c554435f57ea00121a5db7a7b72303f49 Mon Sep 17 00:00:00 2001 From: Stream Date: Tue, 2 Jun 2026 21:10:03 +0300 Subject: [PATCH] auto-sync: 2026-06-02 21:10:01 --- tasks/orchestrator/DEV_TASK_ORCH2_WORKTREE.md | 276 ++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 tasks/orchestrator/DEV_TASK_ORCH2_WORKTREE.md diff --git a/tasks/orchestrator/DEV_TASK_ORCH2_WORKTREE.md b/tasks/orchestrator/DEV_TASK_ORCH2_WORKTREE.md new file mode 100644 index 0000000..754404c --- /dev/null +++ b/tasks/orchestrator/DEV_TASK_ORCH2_WORKTREE.md @@ -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/`. Убрать гонки `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)*