Merge pull request 'Worktree в .claude/worktrees/ через EnterWorktree, вход без подтверждения (#25, 0.5.1)' (#27) from feat/25-worktrees-in-repo into main

This commit is contained in:
aep 2026-10-23 23:22:48 +03:00
commit 957ce1e007
15 changed files with 50 additions and 8 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.superpowers/ .superpowers/
.bounded .bounded
.claude/worktrees/
.DS_Store .DS_Store

View File

@ -1,5 +1,11 @@
# Changelog # Changelog
## 0.5.1 — 2026-10-24
- Worktree живут в `<repo>/.claude/worktrees/<slug>`, а не в соседнем каталоге: их создаёт `EnterWorktree` с именем, ветка `worktree-<slug>` переименовывается в `feat/<id>-<slug>` (#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 ## 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` и недостающие правила. - **`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`. - `adopt-project.sh` и `new-project.sh` печатают строку `push --delete` для установки с `--no-permissions`. WARN проверки в `doctor.sh` подсказывает `bash install.sh`.

View File

@ -43,7 +43,8 @@
- Сделано: B1+B2 и отмена исключения openspec-only для guard-main — хуки по цели записи и корпус хуков (issue #6, PR #12, 0.4.0), спека — `openspec/specs/hook-guards/`. Отложенные находки — issue #13. - Сделано: 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. - Сделано: 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). - Сделано: allow-правила цикла фичи ставит `install.sh` (issue #24, 0.5.0; пересмотр решения владельца 2).
- Дальше: worktree в `.claude/worktrees/` (issue #25), затем B12. - Сделано: worktree — `EnterWorktree` в `<repo>/.claude/worktrees/<slug>`, вход без подтверждения (issue #25, 0.5.1). Для awp-ca вопрос открыт в части 2 issue #2.
- Дальше: B12.
## OpenSpec и superpowers ## OpenSpec и superpowers
- Вход в любую задачу — superpowers:brainstorming. Skills openspec-propose и openspec-explore сами по себе не запускать. - Вход в любую задачу — superpowers:brainstorming. Skills openspec-propose и openspec-explore сами по себе не запускать.

View File

@ -45,7 +45,7 @@ fi
# .gitignore: только строки обвязки # .gitignore: только строки обвязки
touch .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 grep -qxF "$line" .gitignore || echo "$line" >> .gitignore
done done
mkdir -p docs/superpowers/plans && touch docs/superpowers/plans/.gitkeep mkdir -p docs/superpowers/plans && touch docs/superpowers/plans/.gitkeep

View File

@ -19,7 +19,7 @@
| 1 | Формулируете цель одной фразой; агент заводит issue | вы + агент | issue #N | | 1 | Формулируете цель одной фразой; агент заводит issue | вы + агент | issue #N |
| 2 | `brainstorming`: классификация spike / bounded / architectural, вопросы по одному, подходы, дизайн по частям | агент + вы | — | | 2 | `brainstorming`: классификация spike / bounded / architectural, вопросы по одному, подходы, дизайн по частям | агент + вы | — |
| 3 | Спека в `openspec/changes/<id>/` (proposal, specs/*, design) + раздел «Decisions made without discussion»; **ваше утверждение** | агент, вы | change | | 3 | Спека в `openspec/changes/<id>/` (proposal, specs/*, design) + раздел «Decisions made without discussion»; **ваше утверждение** | агент, вы | change |
| 4 | `using-git-worktrees`: ветка `feat/N-slug` | агент | worktree | | 4 | `using-git-worktrees` → `EnterWorktree` с именем: `<repo>/.claude/worktrees/<slug>`, ветка `worktree-<slug>` → `feat/N-slug` | агент | worktree |
| 5 | `writing-plans`: план с Global Constraints, Review Focus, полным кодом задач; Task 1 — тестовая инфраструктура; **ваше утверждение** | агент, вы | `docs/superpowers/plans/*.md` | | 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 | | 6 | SDD: контроллер (sonnet) → implementer на задачу (haiku/sonnet) → reviewer (sonnet) → fix loop до 5 раундов → ledger | агент автономно | коммиты, ledger |
| 7 | Финальное ревью всей ветки (opus), одна волна фиксов | агент | — | | 7 | Финальное ревью всей ветки (opus), одна волна фиксов | агент | — |
@ -29,9 +29,9 @@
Шаги 6–8 идут без остановок. Агент останавливается только на необратимой операции, вопросе безопасности, побочном эффекте вне worktree или плане, который невозможно исполнить. Шаги 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/<slug>` и `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 вне `<repo>/.claude/worktrees/` спрашивает подтверждение при любых правилах, перенос worktree — #25. **Подтверждения.** В обычном режиме Claude Code спрашивает подтверждение на всё, что не покрыто allow-правилами. В auto mode решает классификатор: он пропускает или запрещает, но вас не спрашивает. Allow-правила проверяются раньше него. Вход в worktree вне `<repo>/.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 и без плана. **Bounded-задача** (флаг, фикс) идёт короче: дизайн в чате, файл `.bounded` в корне worktree (пропускает хук require-plan), TDD, коммит, PR. Без OpenSpec change и без плана.

View File

@ -32,6 +32,11 @@ guard-main SHALL блокировать вызов только тогда, ко
- **WHEN** Edit файла внутри репозитория, текущая ветка которого main, при любом cwd - **WHEN** Edit файла внутри репозитория, текущая ветка которого main, при любом cwd
- **THEN** guard-main завершается с кодом 2 - **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 #### Scenario: Запись через симлинк, указывающий в main
- **WHEN** Write в симлинк вне репозитория, цель которого — ещё не созданный файл в репозитории на main - **WHEN** Write в симлинк вне репозитория, цель которого — ещё не созданный файл в репозитории на main
- **THEN** guard-main завершается с кодом 2 - **THEN** guard-main завершается с кодом 2
@ -282,6 +287,10 @@ require-plan SHALL определять корень, ветку, `.claude/code-
- **WHEN** cwd — основной checkout, Edit файла `src/a.py` в worktree, в корне которого есть `.bounded` - **WHEN** cwd — основной checkout, Edit файла `src/a.py` в worktree, в корне которого есть `.bounded`
- **THEN** require-plan завершается с кодом 0 - **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: Имя файла со скобками — литерал #### Scenario: Имя файла со скобками — литерал
- **WHEN** Edit файла `src/[slug].ts` в репозитории без плана и без `.bounded` - **WHEN** Edit файла `src/[slug].ts` в репозитории без плана и без `.bounded`
- **THEN** require-plan завершается с кодом 2 - **THEN** require-plan завершается с кодом 2

View File

@ -4,6 +4,7 @@ main.js
data.json data.json
.superpowers/ .superpowers/
.bounded .bounded
.claude/worktrees/
tests/fixtures/*.m4a tests/fixtures/*.m4a
tests/fixtures/*.webm tests/fixtures/*.webm
.DS_Store .DS_Store

View File

@ -16,7 +16,7 @@
- Architectural-задача: brainstorming пишет спеку не в `docs/superpowers/specs/`, а в `openspec/changes/<id>/`. В поле **Spec** плана указывается этот путь. Перед architectural brainstorming прочитать релевантные файлы в `openspec/specs/` и ARCHITECTURE.md, если есть. - Architectural-задача: brainstorming пишет спеку не в `docs/superpowers/specs/`, а в `openspec/changes/<id>/`. В поле **Spec** плана указывается этот путь. Перед architectural brainstorming прочитать релевантные файлы в `openspec/specs/` и ARCHITECTURE.md, если есть.
### Исполнение ### Исполнение
- Работа только в worktree. Ветка называется по issue: `feat/<issue-id>-<slug>`. Пути worktree в планах и брифах — абсолютные. - Работа только в worktree. Worktree создаёт `EnterWorktree` с именем `<slug>`: `<repo>/.claude/worktrees/<slug>` на временной ветке `worktree-<slug>` от `origin/main`; после issue — `git branch -m feat/<issue-id>-<slug>`. Не `git worktree add` в соседний каталог: вход в worktree вне `.claude/worktrees/` каждый раз спрашивает подтверждение (Claude Code ≥ 2.1.206). Пути worktree в планах и брифах — абсолютные.
- Пока работает фоновый SDD-контроллер, в главной сессии не делать `cd` в основной checkout: cwd оболочки общий, и команды implementer-ов с относительными путями пойдут в main — guard-main их заблокирует, и они остановятся. Команды с абсолютными путями worktree и `git -C <worktree>` от cwd не зависят. - Пока работает фоновый SDD-контроллер, в главной сессии не делать `cd` в основной checkout: cwd оболочки общий, и команды implementer-ов с относительными путями пойдут в main — guard-main их заблокирует, и они остановятся. Команды с абсолютными путями worktree и `git -C <worktree>` от cwd не зависят.
- Режим по умолчанию: subagent-driven-development. Контроллер SDD запускается фоновым сабагентом **на модели sonnet** (не на модели сессии); сессия только ждёт его отчёт. Для планов до 5 задач — executing-plans (Native). Режим не спрашивать, он задан здесь. - Режим по умолчанию: subagent-driven-development. Контроллер SDD запускается фоновым сабагентом **на модели sonnet** (не на модели сессии); сессия только ждёт его отчёт. Для планов до 5 задач — executing-plans (Native). Режим не спрашивать, он задан здесь.
- Модели исполнителей по SDD Model Selection: задача с готовым кодом в плане — haiku, интеграция — sonnet, ревью всей ветки — opus. Модель в dispatch указывать всегда. - Модели исполнителей по 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 — только по явной команде человека. - finishing-a-development-branch создаёт PR в Gitea (`tea pulls create`) со ссылкой на issue. Комментарии к PR — через receiving-code-review: читать из Gitea, править в той же ветке, отвечать в PR. Merge — только по явной команде человека.
- После merge по команде «закрывай» выполняется весь чеклист без остановок: - После merge по команде «закрывай» выполняется весь чеклист без остановок:
1. `openspec archive <id>` — всегда отдельным PR (ветка `chore/archive-<id>` из main, `tea pulls create`, merge по команде человека). Прямых коммитов в main нет, исключений нет (решение владельца 11.10.2026). 1. `openspec archive <id>` — всегда отдельным PR (`EnterWorktree` с именем `archive-<id>`, ветка `chore/archive-<id>`, `tea pulls create`, merge по команде человека). Прямых коммитов в main нет, исключений нет (решение владельца 11.10.2026).
2. Issue закрывается (`tea issues close <id>`, комментарий со ссылкой на PR) только если все её пункты выполнены. Если PR закрывает часть пунктов — issue остаётся открытой, в неё пишется комментарий: какие маркеры закрыты, каким PR и коммитом. 2. Issue закрывается (`tea issues close <id>`, комментарий со ссылкой на PR) только если все её пункты выполнены. Если PR закрывает часть пунктов — issue остаётся открытой, в неё пишется комментарий: какие маркеры закрыты, каким PR и коммитом.
3. Удалить SDD workspace (`.superpowers/sdd/<plan>/`). 3. Удалить SDD workspace (`.superpowers/sdd/<plan>/`).
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`; есть незакоммиченные изменения — не трогать, перечислить их в итоговом сообщении. 5. Обновить основной checkout: дерево чистое — `git pull --ff-only`; есть незакоммиченные изменения — не трогать, перечислить их в итоговом сообщении.
6. Все находки со статусом `deferred` собрать в одну issue `Follow-ups from #<issue-id>`: маркер, одна строка, откуда взялось. Нет отложенного — issue не создавать. 6. Все находки со статусом `deferred` собрать в одну issue `Follow-ups from #<issue-id>`: маркер, одна строка, откуда взялось. Нет отложенного — issue не создавать.
7. Файл auto-memory задачи, если был, свернуть в строку `registry-closed.md` (issue, PR, follow-up, дата); уроки из него перенести в `notes/` направления. 7. Файл auto-memory задачи, если был, свернуть в строку `registry-closed.md` (issue, PR, follow-up, дата); уроки из него перенести в `notes/` направления.

View File

@ -43,6 +43,7 @@ description: Диагностика обвязки Claude Code (claude-harness)
- «code-paths: умолчание» при коде вне `src/` → создать `.claude/code-paths`. - «code-paths: умолчание» при коде вне `src/` → создать `.claude/code-paths`.
- «SDD workspace: …» при отсутствии активной фичи → хвост незакрытой фичи; проверить чеклист закрытия. - «SDD workspace: …» при отсутствии активной фичи → хвост незакрытой фичи; проверить чеклист закрытия.
- `WARN .bounded не в .gitignore` → добавить строку, иначе уедет в main. - `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 нет 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. - `WARN tea не отвечает` → `tea logins list`; токен нужен со scopes read:user, write:repository, write:issue.
- «workspace (не git)» → режим нескольких репозиториев. `WARN … битая ссылка` → репозиторий направления переименован или файл удалён: `bash ~/.claude/harness/link-roles.sh <workspace>`. `WARN нет <dir>/.claude/roles/<r>.md` → создать проектный файл роли из `~/.claude/harness/project-template/role-repo/ROLE.md`. - «workspace (не git)» → режим нескольких репозиториев. `WARN … битая ссылка` → репозиторий направления переименован или файл удалён: `bash ~/.claude/harness/link-roles.sh <workspace>`. `WARN нет <dir>/.claude/roles/<r>.md` → создать проектный файл роли из `~/.claude/harness/project-template/role-repo/ROLE.md`.

View File

@ -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/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' ' ')" [ -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 .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" allow_delete "$root"
[ -d "$root/.git/hooks" ] && [ -x "$root/.git/hooks/pre-commit" ] && ok "git pre-commit есть" || info "git pre-commit нет" [ -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 if command -v tea >/dev/null 2>&1 && git -C "$root" remote get-url origin >/dev/null 2>&1; then

View File

@ -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 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) 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-проект: нет 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" 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) 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") assert_grep "git-проект: правило в .claude/settings.json — ok" 'ok +allow git push origin --delete \(.*/p/\.claude/settings\.json\)' <(printf '%s' "$out")

View File

@ -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/f"; git -C "$S/f" worktree add -q "$S/wt" -b feat/6-x # основной checkout на main + worktree на ветке
mk "$S/ms" master # на master mk "$S/ms" master # на master
mk "$S/d"; git -C "$S/d" checkout -q --detach # detached HEAD 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]}}))' "$@"; } 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 $?; } rc(){ j "$@" | bash "$G" 2>/dev/null; echo $?; }
err(){ j "$@" | bash "$G" 2>&1 >/dev/null; } 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 "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 на master" "$(rc "$S/ws" Write "$S/ms/a.md")" 2
assert_eq "Write при detached HEAD" "$(rc "$S/ws" Write "$S/d/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 # файловые цели Bash
assert_eq "echo > в main" "$(rc "$S/m" Bash 'echo x > notes.md')" 2 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 assert_eq "make 2>build.log в main" "$(rc "$S/m" Bash 'make 2>build.log')" 2

View File

@ -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/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" 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 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]}}))' "$@"; } 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 $?; } 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 "файл в 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 "файл в 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 "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 "имя файла с [скобками] в 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/b/docs/a.md")" 0
assert_eq "вне репозитория — пропуск" "$(rc "$S/b" "$S/out/src/a.py")" 0 assert_eq "вне репозитория — пропуск" "$(rc "$S/b" "$S/out/src/a.py")" 0

View File

@ -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 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: чеклист закрытия перенесён" 'openspec archive' "$ROOT/roles/dev.md"
assert_grep "dev: архив всегда отдельным PR" 'всегда отдельным PR' "$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 — строку 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: п. 4 — worktree remove раньше branch -D" 'git worktree remove`.*`git branch -D' "$ROOT/roles/dev.md"
assert_grep "dev: не cd при фоновом контроллере" 'cd' "$ROOT/roles/dev.md" assert_grep "dev: не cd при фоновом контроллере" 'cd' "$ROOT/roles/dev.md"

View File

@ -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_eq "new-project (старый режим): git" "$([ -d "$P/.git" ] && echo git || echo no)" "git"
assert_file "new-project (старый режим): ARCHITECTURE.md" "$P/ARCHITECTURE.md" 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 (старый режим): печатает 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=$? out=$(bash "$ROOT/adopt-project.sh" "$TMP/notgit" 2>&1); rc=$?
assert_eq "adopt (старый режим): не git → exit 1" "$rc" "1" 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 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 (старый режим): печатает allow-правило (B3)" '"Bash\(git push origin --delete:\*\)"' <(printf '%s' "$out")
assert_grep "adopt: строка — для установки с --no-permissions (#24)" 'no-permissions' <(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_eq "adopt (старый режим): settings.json не создаёт" "$([ -e "$A/.claude/settings.json" ] && echo есть || echo нет)" "нет"
assert_grep "adopt: .claude/worktrees/ в .gitignore (#25)" '^\.claude/worktrees/$' "$A/.gitignore"