Merge pull request 'B3: allow-правило git push origin --delete — строка в adopt/new-project, проверка в doctor (#22, 0.4.2)' (#23) from feat/22-allow-push-delete into main

This commit is contained in:
aep 2026-10-23 22:42:06 +03:00
commit 89e647f230
11 changed files with 86 additions and 7 deletions

View File

@ -1,5 +1,9 @@
# Changelog
## 0.4.2 — 2026-10-23
- Удаление ветки фичи на сервере (чеклист закрытия, п. 4; B3, RT-3, решение владельца 2): `doctor.sh` в разделе «Проект» ищет allow-правило для `git push origin --delete` в `.claude/settings.json`, `settings.local.json` проекта и в глобальном `settings.json` — `ok` с файлом или `WARN` с готовой строкой `"Bash(git push origin --delete:*)"`. Строку печатают `adopt-project.sh` и `new-project.sh` во всех режимах (раньше — только `adopt --workspace`); файл настроек они не пишут, правило добавляет владелец (#22).
- `HARNESS.md`: шаг 10 — чеклист из 8 пунктов в `roles/dev.md` и рецепт п. 4. `roles/dev.md`, п. 4: `git worktree remove` раньше `git branch -D` (ветку, выведенную в worktree, git не удаляет).
## 0.4.1 — 2026-10-23
- harness-doctor и README: codegraph ставится `npm i -g @colbymchenry/codegraph` (формулы в brew нет) и подключается к Claude Code через `claude mcp add-json -s user`, не через `codegraph install` — тот дописывает секцию в `~/.claude/CLAUDE.md`, которую сотрёт `install.sh` (#19, O-8).

View File

@ -41,7 +41,8 @@
Бэклог обвязки — `docs/retro/2026-10-11.md`, раздел 4 (B1–B12), решения владельца — раздел 5. Находки ретро маркируются `RT-n`.
- Сделано: B5+B6 часть 1 — слой ролей (PR #4, 0.3.0). Часть 2 (awp-ca) — issue #2, отложенные находки — issue #5.
- Сделано: B1+B2 и отмена исключения openspec-only для guard-main — хуки по цели записи и корпус хуков (issue #6, PR #12, 0.4.0), спека — `openspec/specs/hook-guards/`. Отложенные находки — issue #13.
- Дальше: B3/B4/B10, B12.
- Сделано: 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.
- Дальше: B12.
## OpenSpec и superpowers
- Вход в любую задачу — superpowers:brainstorming. Skills openspec-propose и openspec-explore сами по себе не запускать.

View File

@ -73,6 +73,8 @@ fi
cat <<MSG
Проект $NAME подключён к обвязке. Дальше:
cd $(pwd) && claude
«Опиши проект в CLAUDE.md по коду: разделы «Что это», «Стек», «Архитектура», «Команды». Bounded-задача.»
1. Разреши агенту удалять ветки на сервере (чеклист закрытия, п. 4) — строка в .claude/settings.json → permissions.allow:
"Bash(git push origin --delete:*)"
2. cd $(pwd) && claude
«Опиши проект в CLAUDE.md по коду: разделы «Что это», «Стек», «Архитектура», «Команды». Bounded-задача.»
MSG

View File

@ -25,10 +25,12 @@
| 7 | Финальное ревью всей ветки (opus), одна волна фиксов | агент | — |
| 8 | `finishing-a-development-branch`: PR в Gitea с «Rulings I made» | агент | PR |
| 9 | Ревью PR в Gitea; комментарии → `receiving-code-review` → правки в ветке; **вы делаете merge** | вы ↔ агент | merge |
| 10 | «Закрывай»: чеклист из 7 пунктов (см. CLAUDE.md) | агент | archive, closed issue, follow-up issue, чистый main |
| 10 | «Закрывай»: чеклист из 8 пунктов (`~/.claude/roles/dev.md`, «Завершение») | агент | archive отдельным PR, closed issue, удалённая ветка, follow-up issue, чистый main |
Шаги 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:*)"` в `.claude/settings.json` → `permissions.allow`. Строку добавляете вы (решение владельца 2); `adopt-project.sh` и `new-project.sh` её печатают, `doctor.sh` показывает, найдена ли она.
**Bounded-задача** (флаг, фикс) идёт короче: дизайн в чате, файл `.bounded` в корне worktree (пропускает хук require-plan), TDD, коммит, PR. Без OpenSpec change и без плана.
## Subagent-driven development

View File

@ -32,7 +32,9 @@ if [ "${1:-}" = "--workspace" ]; then
Workspace $NAME готов. Дальше:
1. Заполни {{...}} в CLAUDE.md и в .claude/roles/dev.md; в репозиториях направлений — .claude/roles/<r>.md.
2. Заведи репозитории направлений в Gitea и добавь remote (tea repos create …; git remote add origin …).
3. cd $(pwd) && claude -n "dev …"
3. Разреши агенту удалять ветки на сервере (чеклист закрытия, п. 4) — строка в .claude/settings.json → permissions.allow:
"Bash(git push origin --delete:*)"
4. cd $(pwd) && claude -n "dev …"
MSG
exit 0
fi
@ -68,6 +70,8 @@ cat <<MSG
Проект $NAME готов. Дальше:
1. Заполните {{...}} в CLAUDE.md и ARCHITECTURE.md.
2. Положите первые фикстуры в tests/fixtures/.
3. cd $DIR && claude
3. Разрешите агенту удалять ветки на сервере (чеклист закрытия, п. 4) — строка в .claude/settings.json → permissions.allow:
"Bash(git push origin --delete:*)"
4. cd $DIR && claude
«Хочу сделать ... Описание и ограничения в CLAUDE.md. Давай начнём.»
MSG

View File

@ -29,7 +29,7 @@
1. `openspec archive <id>` — всегда отдельным PR (ветка `chore/archive-<id>` из main, `tea pulls create`, merge по команде человека). Прямых коммитов в main нет, исключений нет (решение владельца 11.10.2026).
2. Issue закрывается (`tea issues close <id>`, комментарий со ссылкой на PR) только если все её пункты выполнены. Если PR закрывает часть пунктов — issue остаётся открытой, в неё пишется комментарий: какие маркеры закрыты, каким PR и коммитом.
3. Удалить SDD workspace (`.superpowers/sdd/<plan>/`).
4. Удалить ветку фичи на сервере и локально: `git push origin --delete <ветка>` выполняется из worktree на ветке фичи (не из main-checkout), разрешён allow-правилом проекта (`Bash(git push origin --delete:*)` в `.claude/settings.json`; строку печатает `adopt-project.sh --workspace`, для обычного репозитория добавить вручную); затем `git branch -D`, `git worktree remove`. Остальные worktree не трогать. Обходы через API Gitea запрещены.
4. Удалить ветку фичи на сервере и локально: `git push origin --delete <ветка>` выполняется из worktree на ветке фичи (не из main-checkout), разрешён allow-правилом проекта (`Bash(git push origin --delete:*)` в `.claude/settings.json`; строку добавляет владелец, её печатают `adopt-project.sh` и `new-project.sh`, есть ли она — показывает `doctor.sh`); затем из основного checkout `git worktree remove`, потом `git branch -D`. Остальные worktree не трогать. Обходы через API Gitea запрещены.
5. Обновить основной checkout: дерево чистое — `git pull --ff-only`; есть незакоммиченные изменения — не трогать, перечислить их в итоговом сообщении.
6. Все находки со статусом `deferred` собрать в одну issue `Follow-ups from #<issue-id>`: маркер, одна строка, откуда взялось. Нет отложенного — issue не создавать.
7. Файл auto-memory задачи, если был, свернуть в строку `registry-closed.md` (issue, PR, follow-up, дата); уроки из него перенести в `notes/` направления.

View File

@ -42,6 +42,7 @@ description: Диагностика обвязки Claude Code (claude-harness)
- «code-paths: умолчание» при коде вне `src/` → создать `.claude/code-paths`.
- «SDD workspace: …» при отсутствии активной фичи → хвост незакрытой фичи; проверить чеклист закрытия.
- `WARN .bounded не в .gitignore` → добавить строку, иначе уедет в main.
- `WARN нет allow-правила для git push origin --delete` → находка владельцу: готовую строку из WARN он сам добавляет в `.claude/settings.json` проекта (решение владельца 2, чеклист закрытия п. 4); агент `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 <workspace>`. `WARN нет <dir>/.claude/roles/<r>.md` → создать проектный файл роли из `~/.claude/harness/project-template/role-repo/ROLE.md`.
- `WARN индекс auto-memory > 3 КБ` → индекс снова стал журналом состояния; свернуть закрытое в `registry-closed.md`, уроки — в `notes/` направления (политика памяти в глобальном CLAUDE.md).

View File

@ -5,6 +5,37 @@ set -uo pipefail
CC="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
DIR="${1:-.}"
ok(){ printf ' ok %s\n' "$*"; }; miss(){ printf ' MISS %s\n' "$*"; }; warn(){ printf ' WARN %s\n' "$*"; }; info(){ printf ' - %s\n' "$*"; }
# allow-правило для п. 4 закрытия (решение владельца 2): строку добавляет владелец, doctor показывает, где она есть
allow_delete(){ # $1 — корень проекта или workspace
local r line
r=$(python3 - "$1/.claude/settings.json" "$1/.claude/settings.local.json" "$CC/settings.json" <<'PY' 2>/dev/null
import json, sys
cmd = "git push origin --delete " # правило должно покрывать любую ветку: feat/…, chore/archive-…
for f in sys.argv[1:]:
try:
d = json.load(open(f, encoding="utf-8"))
except FileNotFoundError:
continue
except Exception:
print("bad " + f)
continue
allow = (d.get("permissions") or {}).get("allow") if isinstance(d, dict) else None
for a in allow if isinstance(allow, list) else []:
if not isinstance(a, str):
continue
p = a[5:-1] if a.startswith("Bash(") and a.endswith(")") else ("" if a == "Bash" else None)
if p is None:
continue
p = p[:-2] if p.endswith(":*") else p[:-1] if p.endswith("*") else p if a == "Bash" else None
if p is not None and cmd.startswith(p):
print("ok " + f)
sys.exit()
PY
)
while IFS= read -r line; do case "$line" in "bad "*) info "${line#bad } — не JSON, allow-правила не прочитаны" ;; esac; done <<< "$r"
case "$r" in *"ok "*) ok "allow git push origin --delete (${r##*ok })" ;;
*) warn "нет allow-правила для git push origin --delete (закрытие, п. 4) — строка в .claude/settings.json → permissions.allow: \"Bash(git push origin --delete:*)\"" ;; esac
}
echo "## harness-doctor $(date +%F' '%T)"
echo "## Машина"
@ -91,6 +122,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"
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
info "remote: $(git -C "$root" remote get-url origin)"
@ -114,5 +146,6 @@ elif [ -f "$DIR/CLAUDE.md" ] && grep -Eq '^\|[[:space:]]*Каталог[[:space:
# slug каталога памяти Claude Code: все не-алфавитно-цифровые символы пути → «-» (например, /Users/a.b/x → -Users-a-b-x)
slug=$(printf '%s' "$ws" | sed 's#[^A-Za-z0-9]#-#g'); mem="$CC/projects/$slug/memory/MEMORY.md"
if [ -f "$mem" ]; then n=$(wc -c < "$mem" | tr -d ' '); [ "$n" -le 3072 ] && ok "индекс auto-memory $n Б (≤ 3 КБ)" || warn "индекс auto-memory $n Б > 3 КБ ($mem)"; else info "индекс auto-memory: нет ($mem)"; fi
allow_delete "$ws"
[ -x "$CC/harness/link-roles.sh" ] && info "обновить ссылки: bash $CC/harness/link-roles.sh $ws"
else info "не git-репозиторий и не workspace (нет таблицы репозиториев в CLAUDE.md)"; fi

View File

@ -30,6 +30,29 @@ assert_grep "проект: режим workspace" 'workspace' <(printf '%s' "$out
assert_grep "проект: целая ссылка" 'ok +roles/admin.md' <(printf '%s' "$out")
assert_grep "проект: битая ссылка" 'WARN +agents/none.md' <(printf '%s' "$out")
assert_grep "проект: индекс памяти > 3 КБ" 'WARN +индекс auto-memory' <(printf '%s' "$out")
assert_grep "workspace: нет allow push --delete — WARN с готовой строкой (B3)" 'WARN +нет allow-правила для git push origin --delete .*"Bash\(git push origin --delete:\*\)"' <(printf '%s' "$out")
# allow-правило push --delete (B3, решение владельца 2): git-проект, проектные и глобальные настройки
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")
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")
printf '{"permissions":{"allow":["Bash(git push origin --delete feat/:*)"]}}' > "$P/.claude/settings.json"
out=$(CLAUDE_CONFIG_DIR="$CC" HOME="$TMP/home" bash "$D" "$P" 2>&1)
assert_grep "git-проект: правило только для feat/ не покрывает — WARN" 'WARN +нет allow-правила для git push origin --delete' <(printf '%s' "$out")
printf '{"permissions":{"allow":["Bash(git push:*)"]}}' > "$P/.claude/settings.local.json"
out=$(CLAUDE_CONFIG_DIR="$CC" HOME="$TMP/home" bash "$D" "$P" 2>&1)
assert_grep "git-проект: широкое правило в settings.local.json — ok" 'ok +allow git push origin --delete \(.*settings\.local\.json\)' <(printf '%s' "$out")
rm -f "$P/.claude/settings.local.json"; printf '{"permissions":' > "$P/.claude/settings.json"
out=$(CLAUDE_CONFIG_DIR="$CC" HOME="$TMP/home" bash "$D" "$P" 2>&1); rc=$?
assert_eq "git-проект: битый settings.json — doctor не падает" "$rc" "0"
assert_grep "git-проект: битый settings.json — info" '^ - +.*settings\.json — не JSON' <(printf '%s' "$out")
rm -f "$P/.claude/settings.json"; cp "$CC/settings.json" "$TMP/settings.bak"
printf '{"permissions":{"allow":["Bash(git push origin --delete:*)"]},"hooks":{}}' > "$CC/settings.json"
out=$(CLAUDE_CONFIG_DIR="$CC" HOME="$TMP/home" bash "$D" "$W" 2>&1)
assert_grep "workspace: правило в глобальном settings.json — ok" 'ok +allow git push origin --delete \(.*/cc/settings\.json\)' <(printf '%s' "$out")
cp "$TMP/settings.bak" "$CC/settings.json"
rm -f "$CC/roles/qa.md"
out=$(CLAUDE_CONFIG_DIR="$CC" HOME="$TMP/home" bash "$D" "$W" 2>&1)
assert_grep "машина: MISS roles/qa.md" 'MISS +roles/qa.md' <(printf '%s' "$out")

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
assert_grep "dev: чеклист закрытия перенесён" 'openspec archive' "$ROOT/roles/dev.md"
assert_grep "dev: архив всегда отдельным PR" 'всегда отдельным PR' "$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: не cd при фоновом контроллере" 'cd' "$ROOT/roles/dev.md"
assert_grep "dev: bounded обновляет спеку при смене API" 'спека .*обновляется в том же PR' "$ROOT/roles/dev.md"
assert_grep "dev: маркировка R-n" 'R-n' "$ROOT/roles/dev.md"

View File

@ -25,6 +25,7 @@ assert_eq "admin: коммит есть" "$(git -C "$W/admin" log --oneline | wc
assert_symlink "ws .claude/roles/admin.md слинкован" "$W/.claude/roles/admin.md"
assert_symlink "ws .claude/roles/qa.md слинкован" "$W/.claude/roles/qa.md"
assert_eq "workspace сам не git" "$([ -d "$W/.git" ] && echo git || echo no)" "no"
assert_grep "new-project --workspace: печатает allow-правило (B3)" '"Bash\(git push origin --delete:\*\)"' <(printf '%s' "$out")
# adopt-project.sh --workspace на существующем каталоге без CLAUDE.md
W2="$TMP/ws2"; mkdir -p "$W2/qa/.claude/roles"; echo '# Роль: qa — x' > "$W2/qa/.claude/roles/qa.md"
out=$(bash "$ROOT/adopt-project.sh" --workspace "$W2" 2>&1); rc=$?
@ -41,5 +42,11 @@ out=$(bash "$ROOT/new-project.sh" "$P" 2>&1); rc=$?
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")
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
out=$(bash "$ROOT/adopt-project.sh" "$A" 2>&1); rc=$?
assert_eq "adopt (старый режим): git-репозиторий → exit 0" "$rc" "0"
assert_grep "adopt (старый режим): печатает allow-правило (B3)" '"Bash\(git push origin --delete:\*\)"' <(printf '%s' "$out")
assert_eq "adopt (старый режим): settings.json не создаёт" "$([ -e "$A/.claude/settings.json" ] && echo есть || echo нет)" "нет"