diff --git a/.gitignore b/.gitignore index 4b7e182..6abc367 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .superpowers/ .bounded +.claude/worktrees/ .DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index be0b921..470a978 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.5.1 — 2026-10-24 +- Worktree живут в `/.claude/worktrees/`, а не в соседнем каталоге: их создаёт `EnterWorktree` с именем, ветка `worktree-` переименовывается в `feat/-` (#25). Вход в worktree вне `.claude/worktrees/` спрашивает подтверждение при любых allow-правилах (Claude Code ≥ 2.1.206). Описано в `roles/dev.md` и `HARNESS.md`. +- `.claude/worktrees/` — в `.gitignore` проектов: `adopt-project.sh`, `project-template/gitignore`, этот репозиторий. `doctor.sh`: `WARN .claude/worktrees/ не в .gitignore`. +- Спека `hook-guards`: сценарии «worktree внутри репозитория» для guard-main и require-plan — корень и ветка берутся из worktree. Тесты закрепляют уже существующее поведение, код хуков не менялся. +- Многорепозиторный workspace (awp-ca, `.wt/<задача>/`) не затронут — вопрос в части 2 #2. + ## 0.5.0 — 2026-10-23 - **`install.sh` ставит allow-правила цикла фичи** (#24; пересмотр решения владельца 2). Список — `global/permissions.json`: `git worktree`, `git branch -m/-D`, `git push -u origin feat/…`/`chore/…`, `git push origin --delete`, `tea issues`, `tea pulls create/list/edit`, `tea comments`. Merge и push в main в список не входят. Правила только дописываются в `permissions.allow` файла `~/.claude/settings.json`, чужие не трогаются; копия списка — `~/.claude/harness/permissions.json`. `--no-permissions` пропускает шаг. `--check` и `doctor.sh` показывают `permissions обвязки: N/M` и недостающие правила. - `adopt-project.sh` и `new-project.sh` печатают строку `push --delete` для установки с `--no-permissions`. WARN проверки в `doctor.sh` подсказывает `bash install.sh`. diff --git a/CLAUDE.md b/CLAUDE.md index fd9e41b..d832615 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,7 +43,8 @@ - Сделано: B1+B2 и отмена исключения openspec-only для guard-main — хуки по цели записи и корпус хуков (issue #6, PR #12, 0.4.0), спека — `openspec/specs/hook-guards/`. Отложенные находки — issue #13. - Сделано: B3 — allow-правило `git push origin --delete` печатают adopt/new-project, проверяет doctor (issue #22, 0.4.2). B4 закрыт в #6 (решение владельца 1). B10 — workspace-хук awp-ca, в части 2 issue #2. - Сделано: allow-правила цикла фичи ставит `install.sh` (issue #24, 0.5.0; пересмотр решения владельца 2). -- Дальше: worktree в `.claude/worktrees/` (issue #25), затем B12. +- Сделано: worktree — `EnterWorktree` в `/.claude/worktrees/`, вход без подтверждения (issue #25, 0.5.1). Для awp-ca вопрос открыт в части 2 issue #2. +- Дальше: B12. ## OpenSpec и superpowers - Вход в любую задачу — superpowers:brainstorming. Skills openspec-propose и openspec-explore сами по себе не запускать. diff --git a/adopt-project.sh b/adopt-project.sh index 5c76215..ea88fb0 100755 --- a/adopt-project.sh +++ b/adopt-project.sh @@ -45,7 +45,7 @@ fi # .gitignore: только строки обвязки touch .gitignore -for line in '.superpowers/' '.bounded' '.DS_Store'; do +for line in '.superpowers/' '.bounded' '.claude/worktrees/' '.DS_Store'; do grep -qxF "$line" .gitignore || echo "$line" >> .gitignore done mkdir -p docs/superpowers/plans && touch docs/superpowers/plans/.gitkeep diff --git a/docs/HARNESS.md b/docs/HARNESS.md index 9226f57..d4281ca 100644 --- a/docs/HARNESS.md +++ b/docs/HARNESS.md @@ -19,7 +19,7 @@ | 1 | Формулируете цель одной фразой; агент заводит issue | вы + агент | issue #N | | 2 | `brainstorming`: классификация spike / bounded / architectural, вопросы по одному, подходы, дизайн по частям | агент + вы | — | | 3 | Спека в `openspec/changes//` (proposal, specs/*, design) + раздел «Decisions made without discussion»; **ваше утверждение** | агент, вы | change | -| 4 | `using-git-worktrees`: ветка `feat/N-slug` | агент | worktree | +| 4 | `using-git-worktrees` → `EnterWorktree` с именем: `/.claude/worktrees/`, ветка `worktree-` → `feat/N-slug` | агент | worktree | | 5 | `writing-plans`: план с Global Constraints, Review Focus, полным кодом задач; Task 1 — тестовая инфраструктура; **ваше утверждение** | агент, вы | `docs/superpowers/plans/*.md` | | 6 | SDD: контроллер (sonnet) → implementer на задачу (haiku/sonnet) → reviewer (sonnet) → fix loop до 5 раундов → ledger | агент автономно | коммиты, ledger | | 7 | Финальное ревью всей ветки (opus), одна волна фиксов | агент | — | @@ -29,9 +29,9 @@ Шаги 6–8 идут без остановок. Агент останавливается только на необратимой операции, вопросе безопасности, побочном эффекте вне worktree или плане, который невозможно исполнить. -**Удаление ветки фичи (п. 4 закрытия).** `git push origin --delete <ветка>` из worktree фичи, затем из основного checkout `git worktree remove <путь>` и `git branch -D <ветка>`. guard-main с 0.4.0 эту команду не блокирует, но её может отклонить классификатор auto mode. Поэтому нужно allow-правило `"Bash(git push origin --delete:*)"`. Его вместе с остальными правилами цикла фичи (`global/permissions.json`) ставит `bash install.sh` в `~/.claude/settings.json`. Запускаете его вы: агент себе права не выдаёт. Если обвязка стоит с `--no-permissions`, строку добавляете в `.claude/settings.json` проекта сами: её печатают `adopt-project.sh` и `new-project.sh`, а `doctor.sh` показывает, найдена ли она. +**Удаление ветки фичи (п. 4 закрытия).** `git push origin --delete <ветка>` из worktree фичи, затем `ExitWorktree` (keep) и из основного checkout `git worktree remove .claude/worktrees/` и `git branch -D <ветка>`. guard-main с 0.4.0 эту команду не блокирует, но её может отклонить классификатор auto mode. Поэтому нужно allow-правило `"Bash(git push origin --delete:*)"`. Его вместе с остальными правилами цикла фичи (`global/permissions.json`) ставит `bash install.sh` в `~/.claude/settings.json`. Запускаете его вы: агент себе права не выдаёт. Если обвязка стоит с `--no-permissions`, строку добавляете в `.claude/settings.json` проекта сами: её печатают `adopt-project.sh` и `new-project.sh`, а `doctor.sh` показывает, найдена ли она. -**Подтверждения.** В обычном режиме Claude Code спрашивает подтверждение на всё, что не покрыто allow-правилами. В auto mode решает классификатор: он пропускает или запрещает, но вас не спрашивает. Allow-правила проверяются раньше него. Вход в worktree вне `/.claude/worktrees/` спрашивает подтверждение при любых правилах, перенос worktree — #25. +**Подтверждения.** В обычном режиме Claude Code спрашивает подтверждение на всё, что не покрыто allow-правилами. В auto mode решает классификатор: он пропускает или запрещает, но вас не спрашивает. Allow-правила проверяются раньше него. Вход в worktree вне `/.claude/worktrees/` спрашивает подтверждение при любых правилах (Claude Code ≥ 2.1.206). Поэтому worktree живут внутри репозитория: `EnterWorktree` с именем создаёт и открывает их без вопроса. `.claude/worktrees/` стоит в `.gitignore`, иначе worktree попадут в `git status` и в индекс codegraph основного checkout. Хуки берут корень и ветку записи из самого worktree, не из основного checkout. **Bounded-задача** (флаг, фикс) идёт короче: дизайн в чате, файл `.bounded` в корне worktree (пропускает хук require-plan), TDD, коммит, PR. Без OpenSpec change и без плана. diff --git a/openspec/specs/hook-guards/spec.md b/openspec/specs/hook-guards/spec.md index 8e3251b..c4f0718 100644 --- a/openspec/specs/hook-guards/spec.md +++ b/openspec/specs/hook-guards/spec.md @@ -32,6 +32,11 @@ guard-main SHALL блокировать вызов только тогда, ко - **WHEN** Edit файла внутри репозитория, текущая ветка которого main, при любом cwd - **THEN** guard-main завершается с кодом 2 +#### Scenario: Worktree внутри репозитория +- **WHEN** основной checkout на main, worktree на ветке `feat/25-w` лежит в его каталоге `.claude/worktrees/w`, Write файла внутри этого worktree или `echo x > a.md` с cwd в нём +- **THEN** guard-main завершается с кодом 0: корень и ветка цели берутся из worktree, а не из объемлющего checkout +- **AND** запись в файл основного checkout вне `.claude/worktrees/` из того же worktree завершается с кодом 2 + #### Scenario: Запись через симлинк, указывающий в main - **WHEN** Write в симлинк вне репозитория, цель которого — ещё не созданный файл в репозитории на main - **THEN** guard-main завершается с кодом 2 @@ -282,6 +287,10 @@ require-plan SHALL определять корень, ветку, `.claude/code- - **WHEN** cwd — основной checkout, Edit файла `src/a.py` в worktree, в корне которого есть `.bounded` - **THEN** require-plan завершается с кодом 0 +#### Scenario: .bounded основного checkout не действует во вложенном worktree +- **WHEN** в корне основного checkout есть `.bounded`, Edit файла `src/a.py` в worktree `.claude/worktrees/w` этого checkout без своего `.bounded` и без плана +- **THEN** require-plan завершается с кодом 2 + #### Scenario: Имя файла со скобками — литерал - **WHEN** Edit файла `src/[slug].ts` в репозитории без плана и без `.bounded` - **THEN** require-plan завершается с кодом 2 diff --git a/project-template/gitignore b/project-template/gitignore index ca0fe32..0945961 100644 --- a/project-template/gitignore +++ b/project-template/gitignore @@ -4,6 +4,7 @@ main.js data.json .superpowers/ .bounded +.claude/worktrees/ tests/fixtures/*.m4a tests/fixtures/*.webm .DS_Store diff --git a/roles/dev.md b/roles/dev.md index 6f10f44..b231851 100644 --- a/roles/dev.md +++ b/roles/dev.md @@ -16,7 +16,7 @@ - Architectural-задача: brainstorming пишет спеку не в `docs/superpowers/specs/`, а в `openspec/changes//`. В поле **Spec** плана указывается этот путь. Перед architectural brainstorming прочитать релевантные файлы в `openspec/specs/` и ARCHITECTURE.md, если есть. ### Исполнение -- Работа только в worktree. Ветка называется по issue: `feat/-`. Пути worktree в планах и брифах — абсолютные. +- Работа только в worktree. Worktree создаёт `EnterWorktree` с именем ``: `/.claude/worktrees/` на временной ветке `worktree-` от `origin/main`; после issue — `git branch -m feat/-`. Не `git worktree add` в соседний каталог: вход в worktree вне `.claude/worktrees/` каждый раз спрашивает подтверждение (Claude Code ≥ 2.1.206). Пути worktree в планах и брифах — абсолютные. - Пока работает фоновый SDD-контроллер, в главной сессии не делать `cd` в основной checkout: cwd оболочки общий, и команды implementer-ов с относительными путями пойдут в main — guard-main их заблокирует, и они остановятся. Команды с абсолютными путями worktree и `git -C ` от cwd не зависят. - Режим по умолчанию: subagent-driven-development. Контроллер SDD запускается фоновым сабагентом **на модели sonnet** (не на модели сессии); сессия только ждёт его отчёт. Для планов до 5 задач — executing-plans (Native). Режим не спрашивать, он задан здесь. - Модели исполнителей по SDD Model Selection: задача с готовым кодом в плане — haiku, интеграция — sonnet, ревью всей ветки — opus. Модель в dispatch указывать всегда. @@ -26,10 +26,10 @@ ### Завершение - finishing-a-development-branch создаёт PR в Gitea (`tea pulls create`) со ссылкой на issue. Комментарии к PR — через receiving-code-review: читать из Gitea, править в той же ветке, отвечать в PR. Merge — только по явной команде человека. - После merge по команде «закрывай» выполняется весь чеклист без остановок: - 1. `openspec archive ` — всегда отдельным PR (ветка `chore/archive-` из main, `tea pulls create`, merge по команде человека). Прямых коммитов в main нет, исключений нет (решение владельца 11.10.2026). + 1. `openspec archive ` — всегда отдельным PR (`EnterWorktree` с именем `archive-`, ветка `chore/archive-`, `tea pulls create`, merge по команде человека). Прямых коммитов в main нет, исключений нет (решение владельца 11.10.2026). 2. Issue закрывается (`tea issues close `, комментарий со ссылкой на PR) только если все её пункты выполнены. Если PR закрывает часть пунктов — issue остаётся открытой, в неё пишется комментарий: какие маркеры закрыты, каким PR и коммитом. 3. Удалить SDD workspace (`.superpowers/sdd//`). - 4. Удалить ветку фичи на сервере и локально: `git push origin --delete <ветка>` выполняется из worktree на ветке фичи (не из main-checkout), разрешён allow-правилом `Bash(git push origin --delete:*)` (его ставит `install.sh` владельца; при `--no-permissions` — строка в `.claude/settings.json` проекта, её печатают `adopt-project.sh` и `new-project.sh`, есть ли она — показывает `doctor.sh`; агент права себе не добавляет); затем из основного checkout `git worktree remove`, потом `git branch -D`. Остальные worktree не трогать. Обходы через API Gitea запрещены. + 4. Удалить ветку фичи на сервере и локально: `git push origin --delete <ветка>` выполняется из worktree на ветке фичи (не из main-checkout), разрешён allow-правилом `Bash(git push origin --delete:*)` (его ставит `install.sh` владельца; при `--no-permissions` — строка в `.claude/settings.json` проекта, её печатают `adopt-project.sh` и `new-project.sh`, есть ли она — показывает `doctor.sh`; агент права себе не добавляет); затем `ExitWorktree` (keep) и из основного checkout `git worktree remove`, потом `git branch -D`. Остальные worktree не трогать. Обходы через API Gitea запрещены. 5. Обновить основной checkout: дерево чистое — `git pull --ff-only`; есть незакоммиченные изменения — не трогать, перечислить их в итоговом сообщении. 6. Все находки со статусом `deferred` собрать в одну issue `Follow-ups from #`: маркер, одна строка, откуда взялось. Нет отложенного — issue не создавать. 7. Файл auto-memory задачи, если был, свернуть в строку `registry-closed.md` (issue, PR, follow-up, дата); уроки из него перенести в `notes/` направления. diff --git a/skills/harness-doctor/SKILL.md b/skills/harness-doctor/SKILL.md index 92130b6..b5d0f52 100644 --- a/skills/harness-doctor/SKILL.md +++ b/skills/harness-doctor/SKILL.md @@ -43,6 +43,7 @@ description: Диагностика обвязки Claude Code (claude-harness) - «code-paths: умолчание» при коде вне `src/` → создать `.claude/code-paths`. - «SDD workspace: …» при отсутствии активной фичи → хвост незакрытой фичи; проверить чеклист закрытия. - `WARN .bounded не в .gitignore` → добавить строку, иначе уедет в main. +- `WARN .claude/worktrees/ не в .gitignore` → добавить строку на ветке (как `.bounded`), иначе worktree из `EnterWorktree` попадут в `git status` и в индекс codegraph основного checkout. - `WARN нет allow-правила для git push origin --delete` → находка владельцу (чеклист закрытия п. 4). Правило ставит `bash install.sh`. При `--no-permissions` готовую строку из WARN владелец сам добавляет в `.claude/settings.json` проекта. Агент `settings.json` не правит. `ok allow …` называет файл, где правило найдено: проектный `settings.json`, `settings.local.json` или глобальный. - `WARN tea не отвечает` → `tea logins list`; токен нужен со scopes read:user, write:repository, write:issue. - «workspace (не git)» → режим нескольких репозиториев. `WARN … битая ссылка` → репозиторий направления переименован или файл удалён: `bash ~/.claude/harness/link-roles.sh `. `WARN нет /.claude/roles/.md` → создать проектный файл роли из `~/.claude/harness/project-template/role-repo/ROLE.md`. diff --git a/skills/harness-doctor/scripts/doctor.sh b/skills/harness-doctor/scripts/doctor.sh index d19c0c3..a9d8da5 100755 --- a/skills/harness-doctor/scripts/doctor.sh +++ b/skills/harness-doctor/scripts/doctor.sh @@ -141,6 +141,7 @@ if root=$(git -C "$DIR" rev-parse --show-toplevel 2>/dev/null); then [ -f "$root/.claude/code-paths" ] && info "code-paths: $(grep -v '^#' "$root/.claude/code-paths" | tr '\n' ' ')" || info "code-paths: умолчание (любой src/)" [ -f "$root/.claude/readonly-paths" ] && info "readonly-paths: $(grep -v '^#' "$root/.claude/readonly-paths" | tr '\n' ' ')" git -C "$root" check-ignore -q .bounded 2>/dev/null || warn ".bounded не в .gitignore" + git -C "$root" check-ignore -q .claude/worktrees/ 2>/dev/null || warn ".claude/worktrees/ не в .gitignore — worktree (EnterWorktree) попадут в git status и в индекс codegraph основного checkout" allow_delete "$root" [ -d "$root/.git/hooks" ] && [ -x "$root/.git/hooks/pre-commit" ] && ok "git pre-commit есть" || info "git pre-commit нет" if command -v tea >/dev/null 2>&1 && git -C "$root" remote get-url origin >/dev/null 2>&1; then diff --git a/tests/test-doctor.sh b/tests/test-doctor.sh index d1b75ff..8131900 100644 --- a/tests/test-doctor.sh +++ b/tests/test-doctor.sh @@ -36,6 +36,11 @@ assert_grep "workspace: нет allow push --delete — WARN с готовой с P="$TMP/p"; git init -q -b main "$P" && git -C "$P" commit -q --allow-empty -m init out=$(CLAUDE_CONFIG_DIR="$CC" HOME="$TMP/home" bash "$D" "$P" 2>&1) assert_grep "git-проект: нет allow push --delete — WARN" 'WARN +нет allow-правила для git push origin --delete' <(printf '%s' "$out") +assert_grep "git-проект: .claude/worktrees/ не в .gitignore — WARN (#25)" 'WARN +\.claude/worktrees/ не в \.gitignore' <(printf '%s' "$out") +printf '.bounded\n.claude/worktrees/\n' > "$P/.gitignore" +out=$(CLAUDE_CONFIG_DIR="$CC" HOME="$TMP/home" bash "$D" "$P" 2>&1) +assert_eq "git-проект: .claude/worktrees/ в .gitignore — без WARN" "$(printf '%s' "$out" | grep -c 'worktrees/ не в')" "0" +rm -f "$P/.gitignore" mkdir -p "$P/.claude"; printf '{"permissions":{"allow":["Bash(git push origin --delete:*)"]}}' > "$P/.claude/settings.json" out=$(CLAUDE_CONFIG_DIR="$CC" HOME="$TMP/home" bash "$D" "$P" 2>&1) assert_grep "git-проект: правило в .claude/settings.json — ok" 'ok +allow git push origin --delete \(.*/p/\.claude/settings\.json\)' <(printf '%s' "$out") diff --git a/tests/test-hook-guard-main.sh b/tests/test-hook-guard-main.sh index 363e237..476aaa7 100644 --- a/tests/test-hook-guard-main.sh +++ b/tests/test-hook-guard-main.sh @@ -6,6 +6,7 @@ mk "$S/m" # репозит mk "$S/f"; git -C "$S/f" worktree add -q "$S/wt" -b feat/6-x # основной checkout на main + worktree на ветке mk "$S/ms" master # на master mk "$S/d"; git -C "$S/d" checkout -q --detach # detached HEAD +mk "$S/n"; git -C "$S/n" worktree add -q "$S/n/.claude/worktrees/w" -b feat/25-w # вложенный worktree (#25) j(){ python3 -c 'import json,sys; k="command" if sys.argv[2]=="Bash" else "file_path"; print(json.dumps({"cwd":sys.argv[1],"tool_name":sys.argv[2],"tool_input":{k:sys.argv[3]}}))' "$@"; } rc(){ j "$@" | bash "$G" 2>/dev/null; echo $?; } err(){ j "$@" | bash "$G" 2>&1 >/dev/null; } @@ -17,6 +18,14 @@ assert_eq "Edit в worktree при cwd на main" "$(rc "$S/f" Edit "$S/wt/a.md" assert_eq "Edit в репозитории на main при cwd в worktree" "$(rc "$S/wt" Edit "$S/m/a.md")" 2 assert_eq "Write на master" "$(rc "$S/ws" Write "$S/ms/a.md")" 2 assert_eq "Write при detached HEAD" "$(rc "$S/ws" Write "$S/d/a.md")" 2 +# worktree внутри репозитория: .claude/worktrees/<имя> (#25) — корень и ветка берутся из самого worktree +W="$S/n/.claude/worktrees/w" +assert_eq "вложенный worktree: Write на ветке при cwd на main" "$(rc "$S/n" Write "$W/a.md")" 0 +assert_eq "вложенный worktree: echo > при cwd в нём" "$(rc "$W" Bash 'echo x > a.md')" 0 +assert_eq "вложенный worktree: git commit в нём" "$(rc "$W" Bash 'git add -A && git commit -m x')" 0 +assert_eq "вложенный worktree: запись в основной checkout на main" "$(rc "$W" Bash "echo x > $S/n/a.md")" 2 +assert_eq "вложенный worktree: Write в основной checkout рядом" "$(rc "$W" Write "$S/n/.claude/x.md")" 2 +assert_eq "git worktree add .claude/worktrees/… на main" "$(rc "$S/n" Bash 'git worktree add .claude/worktrees/y -b feat/y origin/main')" 0 # файловые цели Bash assert_eq "echo > в main" "$(rc "$S/m" Bash 'echo x > notes.md')" 2 assert_eq "make 2>build.log в main" "$(rc "$S/m" Bash 'make 2>build.log')" 2 diff --git a/tests/test-hook-require-plan.sh b/tests/test-hook-require-plan.sh index 624db5f..09bc303 100644 --- a/tests/test-hook-require-plan.sh +++ b/tests/test-hook-require-plan.sh @@ -13,11 +13,15 @@ echo "Ветка: feat/6-x" > "$S/p0/docs/superpowers/plans/a.md"; echo "дру mk "$S/p1" feat/7-y; mkdir -p "$S/p1/docs/superpowers/plans"; echo "x" > "$S/p1/docs/superpowers/plans/a.md" mk "$S/pd"; mkdir -p "$S/pd/docs/superpowers/plans"; echo a > "$S/pd/docs/superpowers/plans/a.md"; echo b > "$S/pd/docs/superpowers/plans/b.md" git -C "$S/pd" checkout -q --detach +mk "$S/n"; touch "$S/n/.bounded"; git -C "$S/n" worktree add -q "$S/n/.claude/worktrees/w" -b feat/25-w # вложенный worktree (#25) +mk "$S/n2"; git -C "$S/n2" worktree add -q "$S/n2/.claude/worktrees/w" -b feat/25-w; touch "$S/n2/.claude/worktrees/w/.bounded" j(){ python3 -c 'import json,sys; print(json.dumps({"cwd":sys.argv[1],"tool_name":"Edit","tool_input":{"file_path":sys.argv[2]}}))' "$@"; } rc(){ j "$@" | bash "$H" 2>/dev/null; echo $?; } assert_eq "файл в B без плана при cwd в A с .bounded" "$(rc "$S/a" "$S/b/src/a.py")" 2 assert_eq "файл в worktree с .bounded при cwd на main" "$(rc "$S/f" "$S/wt/src/a.py")" 0 assert_eq "src на глубине без плана" "$(rc "$S/b" "$S/b/api/src/a.py")" 2 +assert_eq "вложенный worktree: .bounded основного checkout не действует (#25)" "$(rc "$S/n" "$S/n/.claude/worktrees/w/src/a.py")" 2 +assert_eq "вложенный worktree: свой .bounded — пропуск (#25)" "$(rc "$S/n2" "$S/n2/.claude/worktrees/w/src/a.py")" 0 assert_eq "имя файла с [скобками] в src без плана (M-4)" "$(rc "$S/b" "$S/b/src/[slug].ts")" 2 assert_eq "не код — пропуск" "$(rc "$S/b" "$S/b/docs/a.md")" 0 assert_eq "вне репозитория — пропуск" "$(rc "$S/b" "$S/out/src/a.py")" 0 diff --git a/tests/test-roles.sh b/tests/test-roles.sh index f14afa1..ea09b3b 100644 --- a/tests/test-roles.sh +++ b/tests/test-roles.sh @@ -10,6 +10,8 @@ check_role_file(){ # $1 = путь, $2 = ожидаемое имя роли for r in dev admin analyst qa; do check_role_file "$ROOT/roles/$r.md" "$r"; done assert_grep "dev: чеклист закрытия перенесён" 'openspec archive' "$ROOT/roles/dev.md" assert_grep "dev: архив всегда отдельным PR" 'всегда отдельным PR' "$ROOT/roles/dev.md" +assert_grep "dev: worktree — EnterWorktree в .claude/worktrees/ (#25)" 'EnterWorktree.*\.claude/worktrees/' "$ROOT/roles/dev.md" +assert_grep "repo .gitignore: .claude/worktrees/ (#25)" '^\.claude/worktrees/$' "$ROOT/.gitignore" assert_grep "dev: п. 4 — строку allow печатают adopt/new-project, наличие показывает doctor (B3)" 'new-project\.sh.*doctor\.sh' "$ROOT/roles/dev.md" assert_grep "dev: п. 4 — worktree remove раньше branch -D" 'git worktree remove`.*`git branch -D' "$ROOT/roles/dev.md" assert_grep "dev: не cd при фоновом контроллере" 'cd' "$ROOT/roles/dev.md" diff --git a/tests/test-workspace.sh b/tests/test-workspace.sh index 868371f..f7cc3a7 100644 --- a/tests/test-workspace.sh +++ b/tests/test-workspace.sh @@ -43,6 +43,7 @@ assert_eq "new-project (старый режим): exit 0" "$rc" "0" assert_eq "new-project (старый режим): git" "$([ -d "$P/.git" ] && echo git || echo no)" "git" assert_file "new-project (старый режим): ARCHITECTURE.md" "$P/ARCHITECTURE.md" assert_grep "new-project (старый режим): печатает allow-правило (B3)" '"Bash\(git push origin --delete:\*\)"' <(printf '%s' "$out") +assert_grep "new-project: .claude/worktrees/ в .gitignore (#25)" '^\.claude/worktrees/$' "$P/.gitignore" out=$(bash "$ROOT/adopt-project.sh" "$TMP/notgit" 2>&1); rc=$? assert_eq "adopt (старый режим): не git → exit 1" "$rc" "1" A="$TMP/adopt"; git init -q -b main "$A" && git -C "$A" commit -q --allow-empty -m init @@ -51,3 +52,4 @@ assert_eq "adopt (старый режим): git-репозиторий → exit assert_grep "adopt (старый режим): печатает allow-правило (B3)" '"Bash\(git push origin --delete:\*\)"' <(printf '%s' "$out") assert_grep "adopt: строка — для установки с --no-permissions (#24)" 'no-permissions' <(printf '%s' "$out") assert_eq "adopt (старый режим): settings.json не создаёт" "$([ -e "$A/.claude/settings.json" ] && echo есть || echo нет)" "нет" +assert_grep "adopt: .claude/worktrees/ в .gitignore (#25)" '^\.claude/worktrees/$' "$A/.gitignore"