# guard-main.sh: решение по цели записи (спека hook-guards) в песочнице из настоящих git-репозиториев. G="$ROOT/hooks/guard-main.sh" S="$TMP/sb"; mkdir -p "$S/ws" "$HOME/.claude/projects/p/memory" mk(){ git init -q "$1" && git -C "$1" symbolic-ref HEAD "refs/heads/${2:-main}" && git -C "$1" commit -q --allow-empty -m init; } mk "$S/m" # репозиторий на main 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; } # решение по цели, а не по cwd assert_eq "чтение с 2>/dev/null на main" "$(rc "$S/m" Bash 'grep -rn "npm i\|npm install" . 2>/dev/null | head')" 0 assert_eq "fetch 2>&1 | tail на main" "$(rc "$S/m" Bash 'git fetch origin --prune 2>&1 | tail -3')" 0 assert_eq "Write вне репозитория при cwd на main" "$(rc "$S/m" Write "$HOME/.claude/projects/p/memory/x.md")" 0 assert_eq "Edit в worktree при cwd на main" "$(rc "$S/f" Edit "$S/wt/a.md")" 0 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 assert_eq "sed -i по файлу main" "$(rc "$S/ws" Bash "sed -i 's/a/b/' $S/m/docs/HARNESS.md")" 2 assert_eq "sed -i '' по файлу памяти" "$(rc "$S/m" Bash "sed -i '' 's/a/b/' $HOME/.claude/projects/p/memory/x.md")" 0 assert_eq "docker … sh -c > /tmp" "$(rc "$S/m" Bash "docker compose exec -T db sh -c 'psql -c \"select 1\" > /tmp/x'")" 0 assert_eq "pip install на main" "$(rc "$S/m" Bash 'pip install requests')" 0 assert_eq "npm install на main" "$(rc "$S/m" Bash 'npm install')" 2 assert_eq "цель в переменной" "$(rc "$S/m" Bash 'echo x > "$OUT"')" 0 assert_eq "цель через \$HOME в main" "$(HOME="$S" rc "$S/ws" Bash 'echo x > $HOME/m/f')" 2 # каталог внутри команды assert_eq "cd в worktree и запись" "$(rc "$S/f" Bash "cd $S/wt && echo x > f")" 0 assert_eq "cd в main и touch" "$(rc "$S/wt" Bash "cd $S/m && touch f")" 2 assert_eq "mkdir -p .wt/ в не-git workspace" "$(rc "$S/ws" Bash 'mkdir -p .wt/6-x')" 0 assert_eq "bash -c с записью в main" "$(rc "$S/m" Bash "bash -c 'echo x > f'")" 2 # git assert_eq "add && commit в main" "$(rc "$S/m" Bash 'git add -A && git commit -m x')" 2 assert_eq "commit -C worktree при cwd на main" "$(rc "$S/f" Bash "git -C $S/wt commit -m x")" 0 assert_eq "worktree add -C из workspace" "$(rc "$S/ws" Bash "git -C $S/m worktree add ../.wt/x -b feat/x")" 0 assert_eq "pull --ff-only на main" "$(rc "$S/m" Bash 'git pull --ff-only')" 0 assert_eq "stash list на main" "$(rc "$S/m" Bash 'git stash list')" 0 assert_eq "-C в переменной — блок" "$(rc "$S/ws" Bash 'git -C "$REPO" commit -m x')" 2 assert_grep "-C в переменной — подсказка git -C" 'git -C <абсолютный путь>' <(err "$S/ws" Bash 'git -C "$REPO" commit -m x') assert_eq "switch -c && commit на main" "$(rc "$S/m" Bash 'git switch -q -c chore/archive-x && git add openspec && git commit -m a')" 0 assert_eq "switch -c && touch на main" "$(rc "$S/m" Bash 'git switch -q -c feat/16 && touch .bounded')" 0 assert_eq "switch в переменную && commit — блок" "$(rc "$S/m" Bash 'git switch "$B" && git commit -m x')" 2 assert_eq "switch в переменную && touch — пропуск" "$(rc "$S/m" Bash 'git switch "$B" && touch f')" 0 assert_eq "commit вне репозитория" "$(rc "$S/ws" Bash 'git commit -m x')" 0 # push assert_eq "push с main" "$(rc "$S/m" Bash 'git push')" 2 assert_eq "push --delete фичи с main" "$(rc "$S/m" Bash 'git push origin --delete feat/6-x')" 0 assert_eq "push фичи с main" "$(rc "$S/m" Bash 'git push origin feat/6-x')" 0 assert_eq "push HEAD:main из worktree" "$(rc "$S/wt" Bash 'git push origin HEAD:main')" 2 assert_eq "push из worktree" "$(rc "$S/wt" Bash 'git push -u origin HEAD')" 0 # без исключения для openspec (решение владельца 1) mkdir -p "$S/m/openspec"; echo a > "$S/m/openspec/x.md"; git -C "$S/m" add openspec assert_eq "commit только openspec/ в main" "$(rc "$S/m" Bash 'git commit -m archive')" 2 git -C "$S/m" reset -q assert_eq "openspec archive в main" "$(rc "$S/m" Bash 'openspec archive x')" 2 # граничные случаи (Review Focus) ln -s "$S/wt/CLAUDE.md" "$S/ws/CLAUDE.md"; ln -s "$S/m/CLAUDE.md" "$S/ws/MAIN.md" assert_eq "запись через симлинк в worktree" "$(rc "$S/ws" Write "$S/ws/CLAUDE.md")" 0 assert_eq "запись через симлинк в main" "$(rc "$S/ws" Write "$S/ws/MAIN.md")" 2 assert_eq "пустой cwd и относительный путь — пропуск" "$(rc "" Write "a.md")" 0 assert_eq "запись внутри .git/ — не рабочее дерево" "$(rc "$S/m" Bash 'echo x >> .git/info/exclude')" 0 assert_eq "путь с пробелом в main" "$(rc "$S/m" Bash 'touch "a b.md"')" 2 # финальное ревью: I-1, I-2, M-3, M-4, M-5 assert_eq "checkout main && merge && push из worktree (I-1)" "$(rc "$S/wt" Bash 'git checkout main && git pull && git merge --no-ff feat/6-x && git push')" 2 assert_eq "checkout master && commit (I-1)" "$(rc "$S/wt" Bash 'git checkout master && git commit -am x')" 2 assert_eq "switch - && merge (I-1)" "$(rc "$S/wt" Bash 'git switch - && git merge x')" 2 assert_eq "timeout 60 git push origin main (I-2)" "$(rc "$S/wt" Bash 'timeout 60 git push origin main')" 2 assert_eq "nice -n 5 git commit на main (I-2)" "$(rc "$S/m" Bash 'nice -n 5 git commit -m x')" 2 ln -s "$S/m" "$S/ws/mlink"; ln -s /etc/hosts "$S/m/LINK.md" assert_eq "rm симлинка в workspace на main-репозиторий (M-3)" "$(rc "$S/ws" Bash "rm $S/ws/mlink")" 0 assert_eq "ln -sfn поверх симлинка в workspace (M-3)" "$(rc "$S/ws" Bash "ln -sfn $S/m $S/ws/mlink")" 0 assert_eq "rm симлинка внутри main, указывающего наружу (M-3)" "$(rc "$S/m" Bash 'rm LINK.md')" 2 assert_grep "путь Edit с [скобками] не режется (M-4)" 'docs/\[x\]\.md' <(err "$S/ws" Write "$S/m/docs/[x].md") assert_eq "push текущей ветки через подстановку из worktree (M-5)" "$(rc "$S/wt" Bash 'git push -u origin "$(git branch --show-current)"')" 0 assert_eq "push --tags с main (M-5)" "$(rc "$S/m" Bash 'git push --tags')" 0 # сбой разбора и сообщение assert_eq "не JSON — пропуск" "$(printf 'not json' | bash "$G" 2>/dev/null; echo $?)" 0 assert_eq "незакрытая кавычка — пропуск" "$(rc "$S/m" Bash 'echo "x > f')" 0 mkdir -p "$TMP/nopy" assert_eq "нет python3 — пропуск" "$(env PATH="$TMP/nopy" /bin/bash "$G" <<< "$(j "$S/m" Write "$S/m/a.md")" 2>/dev/null; echo $?)" 0 m=$(err "$S/ws" Write "$S/m/docs/HARNESS.md") assert_grep "сообщение: префикс" '^BLOCKED \[guard-main\]:' <(printf '%s' "$m") assert_grep "сообщение: путь отн. корня" 'docs/HARNESS.md' <(printf '%s' "$m") assert_grep "сообщение: корень и worktree" "$(basename "$S")/m на main.*worktree" <(printf '%s' "$m") assert_grep "settings.hooks.json регистрирует guard-main" 'guard-main\.sh' "$ROOT/hooks/settings.hooks.json"