# require-plan.sh: корень, ветка, code-paths, .bounded и планы — из репозитория правимого файла (спека hook-guards). H="$ROOT/hooks/require-plan.sh" S="$TMP/rp"; mkdir -p "$S/out" 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/a"; touch "$S/a/.bounded" # A: есть .bounded mk "$S/b" # B: ни плана, ни .bounded mk "$S/f"; git -C "$S/f" worktree add -q "$S/wt" -b feat/6-x; touch "$S/wt/.bounded" mk "$S/c"; mkdir -p "$S/c/.claude"; printf 'hooks/ # код\n\nskills/\n' > "$S/c/.claude/code-paths" mk "$S/p2" feat/6-x; mkdir -p "$S/p2/docs/superpowers/plans" echo "Ветка: feat/6-x" > "$S/p2/docs/superpowers/plans/2026-10-16-a.md"; echo "другое" > "$S/p2/docs/superpowers/plans/2026-10-11-b.md" mk "$S/p0" feat/7-y; mkdir -p "$S/p0/docs/superpowers/plans" echo "Ветка: feat/6-x" > "$S/p0/docs/superpowers/plans/a.md"; echo "другое" > "$S/p0/docs/superpowers/plans/b.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" 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 assert_eq "code-paths: hooks/ без плана" "$(rc "$S/a" "$S/c/hooks/x.sh")" 2 assert_eq "code-paths: src/ не защищён" "$(rc "$S/c" "$S/c/src/a.py")" 0 assert_eq "два плана, один с веткой" "$(rc "$S/p2" "$S/p2/src/a.py")" 0 assert_eq "два плана без ветки" "$(rc "$S/p0" "$S/p0/src/a.py")" 2 assert_eq "один план — пропуск" "$(rc "$S/p1" "$S/p1/src/a.py")" 0 assert_eq "detached HEAD при планах — пропуск" "$(rc "$S/pd" "$S/pd/src/a.py")" 0 assert_eq "не JSON — пропуск" "$(printf 'not json' | bash "$H" 2>/dev/null; echo $?)" 0 assert_eq "без file_path — пропуск" "$(printf '{"cwd":"%s","tool_input":{}}' "$S/b" | bash "$H" 2>/dev/null; echo $?)" 0 m=$(j "$S/a" "$S/b/src/a.py" | bash "$H" 2>&1 >/dev/null) assert_grep "сообщение: префикс и путь" '^BLOCKED \[require-plan\]: правка src/a.py' <(printf '%s' "$m") assert_grep "сообщение: .bounded репозитория файла" "$(basename "$S")/b/\.bounded" <(printf '%s' "$m") assert_grep "settings.hooks.json регистрирует require-plan" 'require-plan\.sh' "$ROOT/hooks/settings.hooks.json"