Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/hooks/plan_completion_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
PROJECT_DIR = os.environ.get("CLAUDE_PROJECT_DIR", "")
PLAN_BASE = os.path.join(PROJECT_DIR, "docs", "plan") if PROJECT_DIR else ""

REQUIRED_PLAN_FILES = ["plan.md", "context.md", "checklist.md"]
REQUIRED_PLAN_FILES = ["plan.md", "checklist.md"]

# gh pr create 또는 git push 감지
PR_CREATE_RE = re.compile(r"\bgh\s+pr\s+create\b")
Expand Down
52 changes: 48 additions & 4 deletions .claude/hooks/test_plan_completion_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,18 @@ def run_hook(tool_name, tool_input, plan_base=None):
return result.returncode, result.stderr


def create_plan_dir(base, name, complete=True):
"""테스트용 plan 디렉토리 생성."""
def create_plan_dir(base, name, complete=True, extra_files=None):
"""테스트용 plan 디렉토리 생성. 정책상 plan.md + checklist.md 2종이 필수."""
plan_dir = os.path.join(base, "docs", "plan", name)
os.makedirs(plan_dir, exist_ok=True)
check = "[x]" if complete else "[ ]"
with open(os.path.join(plan_dir, "plan.md"), "w") as f:
f.write(f"# Test\n- {check} step 1\n")
with open(os.path.join(plan_dir, "context.md"), "w") as f:
f.write("# Context\n")
with open(os.path.join(plan_dir, "checklist.md"), "w") as f:
f.write("# Checklist\n")
for extra in extra_files or []:
with open(os.path.join(plan_dir, extra), "w") as f:
f.write(f"# {extra}\n")


def test_non_bash_allowed():
Expand Down Expand Up @@ -97,6 +98,46 @@ def test_mixed_plans_blocked():
assert "wip-task" in stderr


def test_two_doc_incomplete_plan_blocked():
"""정책 정합성 회귀: plan.md + checklist.md 2종만 있는 미완료 plan은 차단되어야 함.

이전 가드는 context.md까지 3종을 요구해, 정책에 맞춰 2종으로 작성된 미완료 plan이
거짓통과(exit=0) 되는 결함이 있었다. (#44)
"""
with tempfile.TemporaryDirectory() as tmp:
create_plan_dir(tmp, "two-doc-wip", complete=False)
code, stderr = run_hook(
"Bash", {"command": 'gh pr create --title "test"'}, plan_base=tmp
)
assert code == 2, f"Should block 2-doc incomplete plan, got {code}"
assert "two-doc-wip" in stderr


def test_two_doc_with_extra_files_still_blocked():
"""가드는 필수 2종만 보고, 추가 파일이 있어도 미완료면 차단되어야 함."""
with tempfile.TemporaryDirectory() as tmp:
create_plan_dir(
tmp, "extras-wip", complete=False, extra_files=["context.md", "notes.md"]
)
code, _ = run_hook(
"Bash", {"command": "git push origin main"}, plan_base=tmp
)
assert code == 2, f"Should block regardless of extra files, got {code}"


def test_plan_without_checklist_skipped():
"""필수 2종이 모두 갖춰지지 않은 디렉토리는 가드 대상이 아님."""
with tempfile.TemporaryDirectory() as tmp:
plan_dir = os.path.join(tmp, "docs", "plan", "no-checklist")
os.makedirs(plan_dir, exist_ok=True)
with open(os.path.join(plan_dir, "plan.md"), "w") as f:
f.write("# Test\n- [ ] step 1\n")
code, _ = run_hook(
"Bash", {"command": 'gh pr create --title "test"'}, plan_base=tmp
)
assert code == 0, f"Incomplete-but-not-required dir should be skipped, got {code}"


if __name__ == "__main__":
tests = [
test_non_bash_allowed,
Expand All @@ -106,6 +147,9 @@ def test_mixed_plans_blocked():
test_pr_create_allowed_with_complete_plan,
test_pr_create_allowed_with_no_plans,
test_mixed_plans_blocked,
test_two_doc_incomplete_plan_blocked,
test_two_doc_with_extra_files_still_blocked,
test_plan_without_checklist_skipped,
]
failed = 0
for test in tests:
Expand Down
4 changes: 1 addition & 3 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
{
"permissions": {
"allow": [
"Bash(rm -rf *)"
]
"allow": []
},
"hooks": {
"PreToolUse": [
Expand Down
13 changes: 13 additions & 0 deletions docs/plan/#44-plan-guard-policy-fix/checklist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# plan_completion_guard 정책 정합성 회복 및 위험 권한 제거 검증 체크리스트

## 필수 항목
- [x] `plan_completion_guard.py`가 `plan.md` + `checklist.md`만 필수로 요구
- [x] 정책 문서(`docs/work-planning-rules.md`)와 가드의 필수 문서 목록이 일치
- [x] 회귀 테스트 추가: `context.md` 없이 미완료 plan이 차단되는지 확인하는 케이스 존재
- [x] `test_plan_completion_guard.py` 모든 테스트 통과 (10/10)
- [x] `test_work_plan_enforcer.py` 회귀 없음 (34/34)
- [x] `.claude/settings.json`에서 와일드카드 삭제 allow 제거됨 (allow: [])
- [x] `block_dangerous.py`가 여전히 보조 방어선으로 동작 (PreToolUse Bash 매처 유지)

## 선택 항목 (해당 시)
- [x] 가드 메시지가 사용자에게 명확한 안내 제공 (기존 메시지 유지)
17 changes: 17 additions & 0 deletions docs/plan/#44-plan-guard-policy-fix/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# plan_completion_guard 정책 정합성 회복 및 위험 권한 제거 계획

> Issue: #44

## 단계

- [x] 1단계: 회귀 재현 — 2종 문서(plan.md + checklist.md)만 있는 미완료 plan에서 가드가 거짓통과(exit=0)하는 것을 임시 디렉토리로 재현
- [x] 2단계: `plan_completion_guard.py`의 `REQUIRED_PLAN_FILES`에서 `context.md` 제거
- [x] 3단계: `test_plan_completion_guard.py`의 `create_plan_dir` 헬퍼를 2종 구조로 갱신 (extra_files 인자로 부수 파일 추가 지원)
- [x] 4단계: 회귀 방지 테스트 3건 추가 — 2종만 있는 미완료 차단, extra 파일 있어도 차단, 필수 2종 미충족 디렉토리는 가드 대상 외
- [x] 5단계: `.claude/settings.json`의 와일드카드 삭제 allow 항목 제거 (allow 빈 배열로)
- [x] 6단계: 가드 테스트 실행 — `test_plan_completion_guard.py` 10/10, `test_work_plan_enforcer.py` 34/34 통과
- [x] 7단계: 검증 체크리스트 확인 후 커밋/PR 생성

## 비고

- `test_block_dangerous.py`(pytest 기반) 12건 실패는 본 변경 이전부터 존재한 결함(soft-warn 케이스가 hard-block으로 동작). 별도 이슈로 분리 예정.
Loading