clouvel 0.3.1__py3-none-any.whl → 0.6.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,75 @@
1
+ # Clouvel Tools Package
2
+ # 모듈별로 도구 구현을 분리
3
+ # Free 기능만 포함 (v0.8까지)
4
+
5
+ from .core import (
6
+ can_code,
7
+ scan_docs,
8
+ analyze_docs,
9
+ init_docs,
10
+ REQUIRED_DOCS,
11
+ )
12
+
13
+ from .docs import (
14
+ get_prd_template,
15
+ write_prd_section,
16
+ get_prd_guide,
17
+ get_verify_checklist,
18
+ get_setup_guide,
19
+ )
20
+
21
+ from .setup import (
22
+ init_clouvel,
23
+ setup_cli,
24
+ )
25
+
26
+ from .rules import (
27
+ init_rules,
28
+ get_rule,
29
+ add_rule,
30
+ )
31
+
32
+ from .verify import (
33
+ verify,
34
+ gate,
35
+ handoff,
36
+ )
37
+
38
+ from .planning import (
39
+ init_planning,
40
+ save_finding,
41
+ refresh_goals,
42
+ update_progress,
43
+ )
44
+
45
+ from .agents import (
46
+ spawn_explore,
47
+ spawn_librarian,
48
+ )
49
+
50
+ from .hooks import (
51
+ hook_design,
52
+ hook_verify,
53
+ )
54
+
55
+ # Pro 기능은 clouvel-pro 패키지로 분리됨
56
+ # pip install clouvel-pro
57
+
58
+ __all__ = [
59
+ # core
60
+ "can_code", "scan_docs", "analyze_docs", "init_docs", "REQUIRED_DOCS",
61
+ # docs
62
+ "get_prd_template", "write_prd_section", "get_prd_guide", "get_verify_checklist", "get_setup_guide",
63
+ # setup
64
+ "init_clouvel", "setup_cli",
65
+ # rules (v0.5)
66
+ "init_rules", "get_rule", "add_rule",
67
+ # verify (v0.5)
68
+ "verify", "gate", "handoff",
69
+ # planning (v0.6)
70
+ "init_planning", "save_finding", "refresh_goals", "update_progress",
71
+ # agents (v0.7)
72
+ "spawn_explore", "spawn_librarian",
73
+ # hooks (v0.8)
74
+ "hook_design", "hook_verify",
75
+ ]
@@ -0,0 +1,245 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Agent tools (v0.7): spawn_explore, spawn_librarian"""
3
+
4
+ from pathlib import Path
5
+ from datetime import datetime
6
+ from mcp.types import TextContent
7
+
8
+
9
+ async def spawn_explore(path: str, query: str, scope: str, save_findings: bool) -> list[TextContent]:
10
+ """탐색 전문 에이전트"""
11
+ project_path = Path(path)
12
+
13
+ if not project_path.exists():
14
+ return [TextContent(type="text", text=f"❌ 경로가 존재하지 않습니다: {path}")]
15
+
16
+ # 스코프별 탐색 전략
17
+ scope_strategies = {
18
+ "file": {
19
+ "description": "단일 파일 분석",
20
+ "actions": ["파일 내용 읽기", "함수/클래스 구조 파악", "의존성 확인"],
21
+ "depth": 1
22
+ },
23
+ "folder": {
24
+ "description": "폴더 내 파일들 분석",
25
+ "actions": ["폴더 구조 스캔", "관련 파일 식별", "패턴 매칭"],
26
+ "depth": 2
27
+ },
28
+ "project": {
29
+ "description": "프로젝트 전체 탐색",
30
+ "actions": ["디렉토리 구조 파악", "엔트리포인트 찾기", "모듈 관계 분석"],
31
+ "depth": 3
32
+ },
33
+ "deep": {
34
+ "description": "심층 분석 (병렬 조사)",
35
+ "actions": ["전체 스캔", "크로스 레퍼런스", "의존성 그래프", "데드코드 탐지"],
36
+ "depth": 5
37
+ }
38
+ }
39
+
40
+ strategy = scope_strategies.get(scope, scope_strategies["project"])
41
+
42
+ # 탐색 프롬프트 생성
43
+ explore_prompt = f"""# 탐색 에이전트 활성화
44
+
45
+ ## 목표
46
+ {query}
47
+
48
+ ## 탐색 전략: {strategy['description']}
49
+
50
+ ### 실행할 액션 (병렬 권장)
51
+ {chr(10).join(f"- [ ] {action}" for action in strategy['actions'])}
52
+
53
+ ### 2-Action Rule 적용
54
+ > view/browser 작업 2개 후 반드시 결과 저장
55
+
56
+ 탐색 중 발견한 내용은 즉시 기록하세요:
57
+ - 파일 위치: `파일경로:라인번호`
58
+ - 핵심 발견: 한 줄 요약
59
+ - 다음 액션: 추가 탐색 필요 여부
60
+
61
+ ### 탐색 범위
62
+ - 경로: `{path}`
63
+ - 깊이: {strategy['depth']} 레벨
64
+ - 스코프: {scope}
65
+
66
+ ---
67
+
68
+ ## 체크리스트
69
+
70
+ 1. [ ] 관련 파일 식별
71
+ 2. [ ] 핵심 코드 위치 파악
72
+ 3. [ ] 의존성/호출 관계 확인
73
+ 4. [ ] 결과 정리
74
+
75
+ ---
76
+
77
+ **탐색 시작!** 위 액션들을 병렬로 실행하세요.
78
+ """
79
+
80
+ # findings.md에 저장 (옵션)
81
+ if save_findings:
82
+ planning_dir = project_path / ".claude" / "planning"
83
+ findings_file = planning_dir / "findings.md"
84
+
85
+ if findings_file.exists():
86
+ timestamp = datetime.now().strftime('%Y-%m-%d %H:%M')
87
+ finding_entry = f"""
88
+ ---
89
+
90
+ ## [{timestamp}] 탐색: {query}
91
+
92
+ - **스코프**: {scope}
93
+ - **상태**: 진행중
94
+ - **결과**: *(탐색 후 업데이트)*
95
+
96
+ """
97
+ existing = findings_file.read_text(encoding='utf-8')
98
+ findings_file.write_text(existing + finding_entry, encoding='utf-8')
99
+
100
+ return [TextContent(type="text", text=explore_prompt)]
101
+
102
+
103
+ async def spawn_librarian(path: str, topic: str, research_type: str, depth: str) -> list[TextContent]:
104
+ """라이브러리언 에이전트"""
105
+ project_path = Path(path)
106
+
107
+ if not project_path.exists():
108
+ return [TextContent(type="text", text=f"❌ 경로가 존재하지 않습니다: {path}")]
109
+
110
+ # 조사 타입별 전략
111
+ type_strategies = {
112
+ "library": {
113
+ "focus": "라이브러리 사용법",
114
+ "sources": ["공식 문서", "GitHub README", "npm/PyPI 페이지"],
115
+ "questions": [
116
+ "설치 방법은?",
117
+ "기본 사용법은?",
118
+ "주요 API는?",
119
+ "버전별 차이는?"
120
+ ]
121
+ },
122
+ "api": {
123
+ "focus": "API 스펙 조사",
124
+ "sources": ["API 문서", "Swagger/OpenAPI", "예제 코드"],
125
+ "questions": [
126
+ "엔드포인트 목록은?",
127
+ "인증 방식은?",
128
+ "요청/응답 형식은?",
129
+ "에러 코드는?"
130
+ ]
131
+ },
132
+ "migration": {
133
+ "focus": "마이그레이션 가이드",
134
+ "sources": ["마이그레이션 문서", "CHANGELOG", "Breaking Changes"],
135
+ "questions": [
136
+ "주요 변경사항은?",
137
+ "호환성 이슈는?",
138
+ "마이그레이션 단계는?",
139
+ "롤백 방법은?"
140
+ ]
141
+ },
142
+ "best_practice": {
143
+ "focus": "베스트 프랙티스",
144
+ "sources": ["공식 가이드", "커뮤니티 블로그", "Stack Overflow"],
145
+ "questions": [
146
+ "권장 패턴은?",
147
+ "안티패턴은?",
148
+ "성능 최적화는?",
149
+ "보안 고려사항은?"
150
+ ]
151
+ }
152
+ }
153
+
154
+ # 깊이별 조사 수준
155
+ depth_levels = {
156
+ "quick": {"time": "5분", "detail": "핵심만", "sources_count": 1},
157
+ "standard": {"time": "15분", "detail": "기본 + 예제", "sources_count": 2},
158
+ "thorough": {"time": "30분+", "detail": "심층 분석", "sources_count": 3}
159
+ }
160
+
161
+ strategy = type_strategies.get(research_type, type_strategies["library"])
162
+ level = depth_levels.get(depth, depth_levels["standard"])
163
+
164
+ # 라이브러리언 프롬프트 생성
165
+ librarian_prompt = f"""# 라이브러리언 에이전트 활성화
166
+
167
+ ## 조사 주제
168
+ **{topic}**
169
+
170
+ ## 조사 전략: {strategy['focus']}
171
+
172
+ ### 참고할 소스 (우선순위 순)
173
+ {chr(10).join(f"{i+1}. {src}" for i, src in enumerate(strategy['sources'][:level['sources_count']]))}
174
+
175
+ ### 답해야 할 질문
176
+ {chr(10).join(f"- [ ] {q}" for q in strategy['questions'])}
177
+
178
+ ---
179
+
180
+ ## 조사 깊이: {depth.upper()}
181
+
182
+ | 항목 | 값 |
183
+ |------|-----|
184
+ | 예상 시간 | {level['time']} |
185
+ | 상세도 | {level['detail']} |
186
+ | 소스 수 | {level['sources_count']}개 |
187
+
188
+ ---
189
+
190
+ ## 2-Action Rule
191
+
192
+ > 외부 문서 2개 확인 후 반드시 findings.md에 기록
193
+
194
+ ### 기록 형식
195
+ ```markdown
196
+ ## [주제]
197
+
198
+ ### 질문
199
+ (찾고 있던 것)
200
+
201
+ ### 발견
202
+ (핵심 내용)
203
+
204
+ ### 소스
205
+ (링크/문서명)
206
+
207
+ ### 결론
208
+ (다음 액션)
209
+ ```
210
+
211
+ ---
212
+
213
+ ## 체크리스트
214
+
215
+ 1. [ ] 공식 문서 확인
216
+ 2. [ ] 예제 코드 수집
217
+ 3. [ ] 버전 호환성 확인
218
+ 4. [ ] 결과 정리 및 저장
219
+
220
+ ---
221
+
222
+ **조사 시작!** `save_finding` 도구로 결과를 저장하세요.
223
+ """
224
+
225
+ # findings.md에 조사 시작 기록
226
+ planning_dir = project_path / ".claude" / "planning"
227
+ findings_file = planning_dir / "findings.md"
228
+
229
+ if findings_file.exists():
230
+ timestamp = datetime.now().strftime('%Y-%m-%d %H:%M')
231
+ finding_entry = f"""
232
+ ---
233
+
234
+ ## [{timestamp}] 조사: {topic}
235
+
236
+ - **타입**: {research_type}
237
+ - **깊이**: {depth}
238
+ - **상태**: 진행중
239
+ - **결과**: *(조사 후 업데이트)*
240
+
241
+ """
242
+ existing = findings_file.read_text(encoding='utf-8')
243
+ findings_file.write_text(existing + finding_entry, encoding='utf-8')
244
+
245
+ return [TextContent(type="text", text=librarian_prompt)]
clouvel/tools/core.py ADDED
@@ -0,0 +1,315 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Core tools: can_code, scan_docs, analyze_docs, init_docs"""
3
+
4
+ import re
5
+ from pathlib import Path
6
+ from datetime import datetime
7
+ from mcp.types import TextContent
8
+
9
+ # 필수 문서 정의
10
+ REQUIRED_DOCS = [
11
+ {"type": "prd", "name": "PRD", "patterns": [r"prd", r"product.?requirement"], "priority": "critical"},
12
+ {"type": "architecture", "name": "아키텍처", "patterns": [r"architect", r"arch", r"module"], "priority": "warn"}, # B4: WARN으로 변경
13
+ {"type": "api_spec", "name": "API 스펙", "patterns": [r"api", r"swagger", r"openapi"], "priority": "warn"},
14
+ {"type": "db_schema", "name": "DB 스키마", "patterns": [r"schema", r"database", r"db"], "priority": "warn"},
15
+ {"type": "verification", "name": "검증 계획", "patterns": [r"verif", r"test.?plan"], "priority": "warn"},
16
+ ]
17
+
18
+ # PRD 필수 섹션 (B4: acceptance 없으면 BLOCK)
19
+ REQUIRED_PRD_SECTIONS = [
20
+ {"name": "acceptance", "patterns": [r"##\s*(acceptance|완료\s*기준|수락\s*조건|done\s*when)"], "priority": "critical"},
21
+ {"name": "scope", "patterns": [r"##\s*(scope|범위|목표)"], "priority": "warn"},
22
+ {"name": "non_goals", "patterns": [r"##\s*(non.?goals?|하지\s*않을|제외|out\s*of\s*scope)"], "priority": "warn"},
23
+ ]
24
+
25
+
26
+ def _find_prd_file(docs_path: Path) -> Path | None:
27
+ """PRD 파일 찾기"""
28
+ for f in docs_path.iterdir():
29
+ if f.is_file():
30
+ name_lower = f.name.lower()
31
+ if "prd" in name_lower or "product" in name_lower and "requirement" in name_lower:
32
+ return f
33
+ return None
34
+
35
+
36
+ def _check_prd_sections(prd_path: Path) -> tuple[list[str], list[str], list[str]]:
37
+ """PRD 파일 내용에서 필수 섹션 확인
38
+ Returns: (found_critical, missing_critical, missing_warn)
39
+ """
40
+ try:
41
+ content = prd_path.read_text(encoding='utf-8')
42
+ except Exception:
43
+ return [], ["acceptance"], []
44
+
45
+ found_critical = []
46
+ missing_critical = []
47
+ missing_warn = []
48
+
49
+ for section in REQUIRED_PRD_SECTIONS:
50
+ found = False
51
+ for pattern in section["patterns"]:
52
+ if re.search(pattern, content, re.IGNORECASE | re.MULTILINE):
53
+ found = True
54
+ break
55
+
56
+ if found:
57
+ if section["priority"] == "critical":
58
+ found_critical.append(section["name"])
59
+ else:
60
+ if section["priority"] == "critical":
61
+ missing_critical.append(section["name"])
62
+ else:
63
+ missing_warn.append(section["name"])
64
+
65
+ return found_critical, missing_critical, missing_warn
66
+
67
+
68
+ def _check_tests(project_path: Path) -> tuple[int, list[str]]:
69
+ """테스트 파일 확인
70
+ Returns: (test_count, test_files)
71
+ """
72
+ test_patterns = [r"test_.*\.py$", r".*_test\.py$", r".*\.test\.(ts|js)$", r".*\.spec\.(ts|js)$"]
73
+ test_files = []
74
+
75
+ # 프로젝트 루트와 하위 폴더에서 테스트 파일 검색
76
+ search_paths = [project_path]
77
+ for subdir in ["tests", "test", "src", "__tests__"]:
78
+ subpath = project_path / subdir
79
+ if subpath.exists():
80
+ search_paths.append(subpath)
81
+
82
+ for search_path in search_paths:
83
+ if not search_path.exists():
84
+ continue
85
+ try:
86
+ for f in search_path.rglob("*"):
87
+ try:
88
+ if f.is_file():
89
+ for pattern in test_patterns:
90
+ if re.match(pattern, f.name, re.IGNORECASE):
91
+ test_files.append(str(f.relative_to(project_path)))
92
+ break
93
+ except (OSError, PermissionError):
94
+ # 심볼릭 링크 깨짐, 접근 권한 없음 등 무시
95
+ continue
96
+ except (OSError, PermissionError):
97
+ continue
98
+
99
+ return len(test_files), test_files[:5] # 최대 5개만 반환
100
+
101
+
102
+ async def can_code(path: str) -> list[TextContent]:
103
+ """코딩 가능 여부 확인 - 핵심 기능 (B4: 품질 게이트 확장)"""
104
+ docs_path = Path(path)
105
+ project_path = docs_path.parent if docs_path.name == "docs" else docs_path
106
+
107
+ if not docs_path.exists():
108
+ return [TextContent(type="text", text=f"""
109
+ # ⛔ BLOCK: 코딩 금지
110
+
111
+ ## 이유
112
+ docs 폴더가 없습니다: `{path}`
113
+
114
+ ## 지금 해야 할 것
115
+ 1. `docs` 폴더를 생성하세요
116
+ 2. PRD(제품 요구사항 문서)를 먼저 작성하세요
117
+ 3. `get_prd_template` 도구로 템플릿을 생성할 수 있습니다
118
+
119
+ ## 왜?
120
+ PRD 없이 코딩하면:
121
+ - 요구사항 불명확 → 재작업
122
+ - 예외 케이스 누락 → 버그
123
+ - 팀원 간 인식 차이 → 충돌
124
+
125
+ **문서 먼저, 코딩은 나중에.**
126
+
127
+ 사용자에게 PRD 작성을 도와주겠다고 말하세요.
128
+ """)]
129
+
130
+ files = [f for f in docs_path.iterdir() if f.is_file()]
131
+ file_names = [f.name.lower() for f in files]
132
+
133
+ detected_critical = []
134
+ detected_warn = []
135
+ missing_critical = []
136
+ missing_warn = []
137
+
138
+ for req in REQUIRED_DOCS:
139
+ found = False
140
+ for filename in file_names:
141
+ for pattern in req["patterns"]:
142
+ if re.search(pattern, filename, re.IGNORECASE):
143
+ if req["priority"] == "critical":
144
+ detected_critical.append(req["name"])
145
+ else:
146
+ detected_warn.append(req["name"])
147
+ found = True
148
+ break
149
+ if found:
150
+ break
151
+ if not found:
152
+ if req["priority"] == "critical":
153
+ missing_critical.append(req["name"])
154
+ else:
155
+ missing_warn.append(req["name"])
156
+
157
+ # B4: PRD 내용 검사 (acceptance 섹션 필수)
158
+ prd_file = _find_prd_file(docs_path)
159
+ prd_sections_found = []
160
+ prd_sections_missing_critical = []
161
+ prd_sections_missing_warn = []
162
+
163
+ if prd_file:
164
+ prd_sections_found, prd_sections_missing_critical, prd_sections_missing_warn = _check_prd_sections(prd_file)
165
+
166
+ # B4: 테스트 파일 확인
167
+ test_count, test_files = _check_tests(project_path)
168
+
169
+ # BLOCK 조건: PRD 없음 OR acceptance 섹션 없음
170
+ if missing_critical or prd_sections_missing_critical:
171
+ all_missing_critical = missing_critical + [f"PRD의 {s} 섹션" for s in prd_sections_missing_critical]
172
+ detected_list = "\n".join(f"- {d}" for d in detected_critical + detected_warn) if (detected_critical or detected_warn) else "없음"
173
+
174
+ return [TextContent(type="text", text=f"""
175
+ # ⛔ BLOCK: 코딩 금지
176
+
177
+ ## 현재 상태
178
+ ✅ 있음:
179
+ {detected_list}
180
+
181
+ ❌ 없음 (필수 - BLOCK):
182
+ {chr(10).join(f'- {m}' for m in all_missing_critical)}
183
+
184
+ ## 지금 해야 할 것
185
+ 코드를 작성하지 마세요. 대신:
186
+
187
+ 1. 누락된 문서/섹션을 먼저 작성하세요
188
+ 2. **PRD에 acceptance(완료 기준) 섹션이 필수입니다**
189
+ 3. `get_prd_guide` 도구로 작성법을 확인하세요
190
+ 4. `get_prd_template` 도구로 템플릿을 생성하세요
191
+
192
+ ## 사용자에게 전달할 메시지
193
+ "코드를 작성하기 전에 먼저 문서를 준비해야 합니다.
194
+ 필수 항목이 없습니다: {', '.join(all_missing_critical)}
195
+ 제가 PRD 작성을 도와드릴까요?"
196
+
197
+ **절대 코드를 작성하지 마세요. 문서 작성을 도와주세요.**
198
+ """)]
199
+
200
+ # WARN 조건: 아키텍처 없음, 테스트 0개 등
201
+ warn_count = len(missing_warn) + len(prd_sections_missing_warn) + (1 if test_count == 0 else 0)
202
+
203
+ # 짧은 요약 형식
204
+ found_docs = ", ".join(detected_critical) if detected_critical else "없음"
205
+ warn_items = missing_warn + [f"PRD.{s}" for s in prd_sections_missing_warn]
206
+ if test_count == 0:
207
+ warn_items.append("테스트")
208
+ warn_summary = ", ".join(warn_items) if warn_items else "없음"
209
+
210
+ test_info = f" | 테스트 {test_count}개" if test_count > 0 else ""
211
+
212
+ if warn_count > 0:
213
+ return [TextContent(type="text", text=f"✅ PASS | ⚠️ WARN {warn_count}개 | 필수: {found_docs} ✓{test_info} | 권장 없음: {warn_summary}")]
214
+ else:
215
+ return [TextContent(type="text", text=f"✅ PASS | 필수: {found_docs} ✓{test_info} | 코딩 시작 가능")]
216
+
217
+
218
+ async def scan_docs(path: str) -> list[TextContent]:
219
+ """docs 폴더 스캔"""
220
+ docs_path = Path(path)
221
+
222
+ if not docs_path.exists():
223
+ return [TextContent(type="text", text=f"경로 없음: {path}")]
224
+
225
+ if not docs_path.is_dir():
226
+ return [TextContent(type="text", text=f"디렉토리 아님: {path}")]
227
+
228
+ files = []
229
+ for f in sorted(docs_path.iterdir()):
230
+ if f.is_file():
231
+ stat = f.stat()
232
+ files.append(f"{f.name} ({stat.st_size:,} bytes)")
233
+
234
+ result = f"📁 {path}\n총 {len(files)}개 파일\n\n"
235
+ result += "\n".join(files)
236
+
237
+ return [TextContent(type="text", text=result)]
238
+
239
+
240
+ async def analyze_docs(path: str) -> list[TextContent]:
241
+ """docs 폴더 분석"""
242
+ docs_path = Path(path)
243
+
244
+ if not docs_path.exists():
245
+ return [TextContent(type="text", text=f"경로 없음: {path}")]
246
+
247
+ files = [f.name.lower() for f in docs_path.iterdir() if f.is_file()]
248
+ detected = []
249
+ missing = []
250
+
251
+ for req in REQUIRED_DOCS:
252
+ found = False
253
+ for filename in files:
254
+ for pattern in req["patterns"]:
255
+ if re.search(pattern, filename, re.IGNORECASE):
256
+ detected.append(req["name"])
257
+ found = True
258
+ break
259
+ if found:
260
+ break
261
+ if not found:
262
+ missing.append(req["name"])
263
+
264
+ critical_total = len([r for r in REQUIRED_DOCS if r["priority"] == "critical"])
265
+ critical_found = len([r for r in REQUIRED_DOCS if r["priority"] == "critical" and r["name"] in detected])
266
+ coverage = critical_found / critical_total if critical_total > 0 else 1.0
267
+
268
+ result = f"## 분석 결과: {path}\n\n"
269
+ result += f"커버리지: {coverage:.0%}\n\n"
270
+
271
+ if detected:
272
+ result += "### 있음\n" + "\n".join(f"- {d}" for d in detected) + "\n\n"
273
+
274
+ if missing:
275
+ result += "### 없음 (작성 필요)\n" + "\n".join(f"- {m}" for m in missing) + "\n\n"
276
+
277
+ if not missing:
278
+ result += "✅ 필수 문서 다 있음. 바이브코딩 시작해도 됨.\n"
279
+ else:
280
+ result += f"⛔ {len(missing)}개 문서 먼저 작성하고 코딩 시작할 것.\n"
281
+
282
+ return [TextContent(type="text", text=result)]
283
+
284
+
285
+ async def init_docs(path: str, project_name: str) -> list[TextContent]:
286
+ """docs 폴더 초기화 + 템플릿 생성"""
287
+ project_path = Path(path)
288
+ docs_path = project_path / "docs"
289
+
290
+ docs_path.mkdir(parents=True, exist_ok=True)
291
+
292
+ templates = {
293
+ "PRD.md": f"# {project_name} PRD\n\n> 작성일: {datetime.now().strftime('%Y-%m-%d')}\n\n## 한 줄 요약\n\n[작성 필요]\n\n## Acceptance (완료 기준)\n\n- [ ] [완료 조건 1]\n- [ ] [완료 조건 2]\n- [ ] [완료 조건 3]\n",
294
+ "ARCHITECTURE.md": f"# {project_name} 아키텍처\n\n## 시스템 구조\n\n[작성 필요]\n",
295
+ "API.md": f"# {project_name} API 스펙\n\n## 엔드포인트\n\n[작성 필요]\n",
296
+ "DATABASE.md": f"# {project_name} DB 스키마\n\n## 테이블\n\n[작성 필요]\n",
297
+ "VERIFICATION.md": f"# {project_name} 검증 계획\n\n## 테스트 케이스\n\n[작성 필요]\n",
298
+ }
299
+
300
+ created = []
301
+ for filename, content in templates.items():
302
+ file_path = docs_path / filename
303
+ if not file_path.exists():
304
+ file_path.write_text(content, encoding='utf-8')
305
+ created.append(filename)
306
+
307
+ result = f"## docs 폴더 초기화 완료\n\n경로: `{docs_path}`\n\n"
308
+ if created:
309
+ result += "### 생성된 파일\n" + "\n".join(f"- {f}" for f in created) + "\n\n"
310
+ else:
311
+ result += "모든 파일이 이미 존재합니다.\n\n"
312
+
313
+ result += "### 다음 단계\n1. PRD.md부터 작성하세요\n2. `get_prd_guide` 도구로 작성법을 확인하세요\n"
314
+
315
+ return [TextContent(type="text", text=result)]