@walwal-harness/cli 1.0.0
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.
- package/assets/templates/AGENTS.md.template +52 -0
- package/assets/templates/HARNESS.md +159 -0
- package/assets/templates/config.json +108 -0
- package/assets/templates/progress.txt.template +15 -0
- package/bin/init.js +281 -0
- package/gotchas/README.md +37 -0
- package/gotchas/evaluator-functional.md +5 -0
- package/gotchas/evaluator-visual.md +5 -0
- package/gotchas/generator-backend.md +5 -0
- package/gotchas/generator-frontend.md +5 -0
- package/gotchas/planner.md +5 -0
- package/package.json +35 -0
- package/scripts/init-agents-md.sh +327 -0
- package/scripts/scan-project.sh +269 -0
- package/skills/dispatcher/SKILL.md +53 -0
- package/skills/dispatcher/references/gotcha-flow.md +52 -0
- package/skills/dispatcher/references/initialization.md +53 -0
- package/skills/dispatcher/references/pipeline-definitions.md +76 -0
- package/skills/evaluator-functional/SKILL.md +58 -0
- package/skills/evaluator-functional/references/ia-compliance.md +37 -0
- package/skills/evaluator-functional/references/playwright-tools.md +43 -0
- package/skills/evaluator-functional/references/scoring-rubric.md +52 -0
- package/skills/evaluator-visual/SKILL.md +41 -0
- package/skills/evaluator-visual/references/responsive-checklist.md +44 -0
- package/skills/evaluator-visual/references/scoring-rubric.md +59 -0
- package/skills/generator-backend/SKILL.md +43 -0
- package/skills/generator-backend/references/nestjs-msa-patterns.md +69 -0
- package/skills/generator-backend/references/sprint-contract-be.md +32 -0
- package/skills/generator-frontend/SKILL.md +49 -0
- package/skills/generator-frontend/references/anti-slop-rules.md +21 -0
- package/skills/generator-frontend/references/component-patterns.md +52 -0
- package/skills/planner/SKILL.md +48 -0
- package/skills/planner/references/api-contract-schema.md +34 -0
- package/skills/planner/references/ia-map-guide.md +31 -0
- package/skills/planner/references/plan-template.md +51 -0
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@walwal-harness/cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Production harness for AI agent engineering — Planner, Generator(BE/FE), Evaluator(Func/Visual) with Gotcha management",
|
|
5
|
+
"bin": {
|
|
6
|
+
"walwal-harness": "bin/init.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"postinstall": "node bin/init.js --auto",
|
|
10
|
+
"init": "node bin/init.js",
|
|
11
|
+
"scan": "bash scripts/scan-project.sh .",
|
|
12
|
+
"reset": "node bin/init.js --force"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"harness-engineering",
|
|
16
|
+
"ai-agent",
|
|
17
|
+
"claude-code",
|
|
18
|
+
"skill",
|
|
19
|
+
"planner",
|
|
20
|
+
"generator",
|
|
21
|
+
"evaluator",
|
|
22
|
+
"nestjs",
|
|
23
|
+
"react",
|
|
24
|
+
"playwright"
|
|
25
|
+
],
|
|
26
|
+
"author": "",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"files": [
|
|
29
|
+
"bin/",
|
|
30
|
+
"skills/",
|
|
31
|
+
"scripts/",
|
|
32
|
+
"assets/",
|
|
33
|
+
"gotchas/"
|
|
34
|
+
]
|
|
35
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# init-agents-md.sh — scan-result.json을 기반으로 AGENTS.md를 생성/리빌드
|
|
3
|
+
# 기존 CLAUDE.md의 사용자 규칙을 보존하면서 하네스 포맷으로 변환
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
PROJECT_ROOT="${1:-.}"
|
|
7
|
+
SCAN_RESULT="${PROJECT_ROOT}/.harness/actions/scan-result.json"
|
|
8
|
+
AGENTS_FILE="${PROJECT_ROOT}/AGENTS.md"
|
|
9
|
+
CLAUDE_FILE="${PROJECT_ROOT}/CLAUDE.md"
|
|
10
|
+
BACKUP_DIR="${PROJECT_ROOT}/.harness/archive/pre-harness-backup"
|
|
11
|
+
|
|
12
|
+
if [ ! -f "$SCAN_RESULT" ]; then
|
|
13
|
+
echo "ERROR: scan-result.json not found. Run scan-project.sh first."
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
# ─────────────────────────────────────────
|
|
18
|
+
# 1. Python으로 scan-result.json 파싱
|
|
19
|
+
# ─────────────────────────────────────────
|
|
20
|
+
eval "$(python3 << 'PYEOF'
|
|
21
|
+
import json, sys, os
|
|
22
|
+
|
|
23
|
+
with open(os.environ.get("SCAN_RESULT", ".harness/actions/scan-result.json")) as f:
|
|
24
|
+
scan = json.load(f)
|
|
25
|
+
|
|
26
|
+
docs = scan["existing_docs"]
|
|
27
|
+
tech = scan["tech_stack"]
|
|
28
|
+
rec = scan["recommendation"]
|
|
29
|
+
dirs = scan["structure"]["directories"]
|
|
30
|
+
configs = scan["structure"]["config_files"]
|
|
31
|
+
|
|
32
|
+
print(f'ACTION="{rec["agents_md_action"]}"')
|
|
33
|
+
print(f'PROJECT_TYPE="{rec["project_type"]}"')
|
|
34
|
+
print(f'TECH_BE="{tech["backend"]}"')
|
|
35
|
+
print(f'TECH_FE="{tech["frontend"]}"')
|
|
36
|
+
print(f'TECH_DB="{tech["database"]}"')
|
|
37
|
+
print(f'TECH_MONO="{tech["monorepo"]}"')
|
|
38
|
+
print(f'OPENAPI="{scan["api"]["openapi_spec"]}"')
|
|
39
|
+
print(f'HAS_EXISTING_CLAUDE="{docs["claude_md"]}"')
|
|
40
|
+
print(f'EXISTING_CONTENT={json.dumps(docs["existing_claude_md_content"])}')
|
|
41
|
+
PYEOF
|
|
42
|
+
)"
|
|
43
|
+
|
|
44
|
+
echo "=== AGENTS.md Initialization ==="
|
|
45
|
+
echo "Action: ${ACTION}"
|
|
46
|
+
echo "Project Type: ${PROJECT_TYPE}"
|
|
47
|
+
echo ""
|
|
48
|
+
|
|
49
|
+
# ─────────────────────────────────────────
|
|
50
|
+
# 2. 기존 문서 백업
|
|
51
|
+
# ─────────────────────────────────────────
|
|
52
|
+
if [ "$ACTION" = "migrate-claude-to-agents" ] || [ "$ACTION" = "rebuild-with-harness" ]; then
|
|
53
|
+
mkdir -p "$BACKUP_DIR"
|
|
54
|
+
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
|
55
|
+
|
|
56
|
+
if [ -f "$AGENTS_FILE" ] && [ ! -L "$AGENTS_FILE" ]; then
|
|
57
|
+
cp "$AGENTS_FILE" "${BACKUP_DIR}/AGENTS.md.${TIMESTAMP}.bak"
|
|
58
|
+
echo "Backed up: AGENTS.md → ${BACKUP_DIR}/AGENTS.md.${TIMESTAMP}.bak"
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
if [ -f "$CLAUDE_FILE" ] && [ ! -L "$CLAUDE_FILE" ]; then
|
|
62
|
+
cp "$CLAUDE_FILE" "${BACKUP_DIR}/CLAUDE.md.${TIMESTAMP}.bak"
|
|
63
|
+
echo "Backed up: CLAUDE.md → ${BACKUP_DIR}/CLAUDE.md.${TIMESTAMP}.bak"
|
|
64
|
+
fi
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# ─────────────────────────────────────────
|
|
68
|
+
# 3. IA-MAP 자동 생성
|
|
69
|
+
# ─────────────────────────────────────────
|
|
70
|
+
generate_ia_map() {
|
|
71
|
+
python3 << 'PYEOF'
|
|
72
|
+
import json, os
|
|
73
|
+
|
|
74
|
+
with open(os.environ.get("SCAN_RESULT", ".harness/actions/scan-result.json")) as f:
|
|
75
|
+
scan = json.load(f)
|
|
76
|
+
|
|
77
|
+
dirs = scan["structure"]["directories"]
|
|
78
|
+
tech = scan["tech_stack"]
|
|
79
|
+
|
|
80
|
+
# 경로별 태그/설명 추론 규칙
|
|
81
|
+
tag_rules = {
|
|
82
|
+
# Backend patterns
|
|
83
|
+
"apps/gateway": ("[BE]", "API Gateway", "Generator-Backend"),
|
|
84
|
+
"apps/service": ("[BE]", "Microservice", "Generator-Backend"),
|
|
85
|
+
"src/controllers": ("[BE]", "API Controllers", "Generator-Backend"),
|
|
86
|
+
"src/services": ("[BE]", "Business Logic", "Generator-Backend"),
|
|
87
|
+
"src/models": ("[BE]", "Data Models", "Generator-Backend"),
|
|
88
|
+
"src/entities": ("[BE]", "DB Entities", "Generator-Backend"),
|
|
89
|
+
"src/modules": ("[BE]", "NestJS Modules", "Generator-Backend"),
|
|
90
|
+
"src/guards": ("[BE]", "Auth Guards", "Generator-Backend"),
|
|
91
|
+
"src/pipes": ("[BE]", "Validation Pipes", "Generator-Backend"),
|
|
92
|
+
"src/interceptors": ("[BE]", "Interceptors", "Generator-Backend"),
|
|
93
|
+
"src/filters": ("[BE]", "Exception Filters", "Generator-Backend"),
|
|
94
|
+
"routers": ("[BE]", "API Routers", "Generator-Backend"),
|
|
95
|
+
"routes": ("[BE]", "API Routes", "Generator-Backend"),
|
|
96
|
+
"middleware": ("[BE]", "Middleware", "Generator-Backend"),
|
|
97
|
+
"migrations": ("[BE]", "DB Migrations", "Generator-Backend"),
|
|
98
|
+
|
|
99
|
+
# Frontend patterns
|
|
100
|
+
"apps/web": ("[FE]", "Frontend App", "Generator-Frontend"),
|
|
101
|
+
"src/components": ("[FE]", "UI Components", "Generator-Frontend"),
|
|
102
|
+
"src/pages": ("[FE]", "Page Routes", "Generator-Frontend"),
|
|
103
|
+
"src/app": ("[FE]", "App Router", "Generator-Frontend"),
|
|
104
|
+
"src/hooks": ("[FE]", "Custom Hooks", "Generator-Frontend"),
|
|
105
|
+
"src/stores": ("[FE]", "State Management", "Generator-Frontend"),
|
|
106
|
+
"src/styles": ("[FE]", "Stylesheets", "Generator-Frontend"),
|
|
107
|
+
"src/api": ("[FE]", "API Client Layer", "Generator-Frontend"),
|
|
108
|
+
"src/lib": ("[FE]", "Utility Library", "Generator-Frontend"),
|
|
109
|
+
"public": ("[FE]", "Static Assets", "Generator-Frontend"),
|
|
110
|
+
|
|
111
|
+
# Shared / Libs
|
|
112
|
+
"libs/shared": ("[BE]", "Shared Libraries", "Generator-Backend"),
|
|
113
|
+
"libs/database": ("[BE]", "Database Module", "Generator-Backend"),
|
|
114
|
+
"libs/common": ("[BE]", "Common Utilities", "Generator-Backend"),
|
|
115
|
+
|
|
116
|
+
# Infra
|
|
117
|
+
"docker": ("[INFRA]", "Docker Configuration", "Planner"),
|
|
118
|
+
".github": ("[INFRA]", "CI/CD Workflows", "Planner"),
|
|
119
|
+
"deploy": ("[INFRA]", "Deployment Scripts", "Planner"),
|
|
120
|
+
"terraform": ("[INFRA]", "Infrastructure as Code", "Planner"),
|
|
121
|
+
"k8s": ("[INFRA]", "Kubernetes Manifests", "Planner"),
|
|
122
|
+
|
|
123
|
+
# Harness
|
|
124
|
+
".harness": ("[HARNESS]", "Harness System", "Planner"),
|
|
125
|
+
|
|
126
|
+
# Tests
|
|
127
|
+
"test": ("[TEST]", "Test Suite", "Evaluator"),
|
|
128
|
+
"tests": ("[TEST]", "Test Suite", "Evaluator"),
|
|
129
|
+
"e2e": ("[TEST]", "E2E Tests", "Evaluator"),
|
|
130
|
+
"__tests__": ("[TEST]", "Unit Tests", "Evaluator"),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# 디렉토리를 분석
|
|
134
|
+
ia_lines = []
|
|
135
|
+
for d in sorted(dirs):
|
|
136
|
+
if d == "." or not d:
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
matched = False
|
|
140
|
+
for pattern, (tag, desc, owner) in tag_rules.items():
|
|
141
|
+
if pattern in d:
|
|
142
|
+
depth = d.count("/")
|
|
143
|
+
indent = "│ " * depth
|
|
144
|
+
name = d.split("/")[-1] + "/"
|
|
145
|
+
ia_lines.append(f"{indent}├── {name:<25} # {tag} {desc:<30} → {owner}")
|
|
146
|
+
matched = True
|
|
147
|
+
break
|
|
148
|
+
|
|
149
|
+
if not matched:
|
|
150
|
+
depth = d.count("/")
|
|
151
|
+
if depth <= 1: # 1depth만 미분류 표시
|
|
152
|
+
indent = "│ " * depth
|
|
153
|
+
name = d.split("/")[-1] + "/"
|
|
154
|
+
ia_lines.append(f"{indent}├── {name:<25} # [?] (Planner가 분류 필요) → TBD")
|
|
155
|
+
|
|
156
|
+
for line in ia_lines:
|
|
157
|
+
print(line)
|
|
158
|
+
PYEOF
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
IA_MAP=$(generate_ia_map)
|
|
162
|
+
|
|
163
|
+
# ─────────────────────────────────────────
|
|
164
|
+
# 4. 기존 사용자 규칙 추출 (CLAUDE.md에서)
|
|
165
|
+
# ─────────────────────────────────────────
|
|
166
|
+
extract_user_rules() {
|
|
167
|
+
if [ "$HAS_EXISTING_CLAUDE" = "exists" ]; then
|
|
168
|
+
python3 << 'PYEOF'
|
|
169
|
+
import json, os
|
|
170
|
+
|
|
171
|
+
with open(os.environ.get("SCAN_RESULT", ".harness/actions/scan-result.json")) as f:
|
|
172
|
+
scan = json.load(f)
|
|
173
|
+
|
|
174
|
+
content = scan["existing_docs"]["existing_claude_md_content"]
|
|
175
|
+
|
|
176
|
+
if not content or content.strip() == "":
|
|
177
|
+
print("(기존 규칙 없음)")
|
|
178
|
+
else:
|
|
179
|
+
# 사용자 정의 규칙 영역만 추출 (harness 관련 제외)
|
|
180
|
+
lines = content.split("\n")
|
|
181
|
+
user_section = []
|
|
182
|
+
skip = False
|
|
183
|
+
for line in lines:
|
|
184
|
+
# harness가 생성한 섹션은 제외
|
|
185
|
+
if any(kw in line.lower() for kw in ["ia-map", "ia map", "harness", "## tech stack"]):
|
|
186
|
+
skip = True
|
|
187
|
+
elif line.startswith("## ") and skip:
|
|
188
|
+
skip = False
|
|
189
|
+
|
|
190
|
+
if not skip:
|
|
191
|
+
user_section.append(line)
|
|
192
|
+
|
|
193
|
+
result = "\n".join(user_section).strip()
|
|
194
|
+
if result:
|
|
195
|
+
print(result)
|
|
196
|
+
else:
|
|
197
|
+
print("(기존 규칙 없음)")
|
|
198
|
+
PYEOF
|
|
199
|
+
else
|
|
200
|
+
echo "(기존 규칙 없음)"
|
|
201
|
+
fi
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
USER_RULES=$(extract_user_rules)
|
|
205
|
+
|
|
206
|
+
# ─────────────────────────────────────────
|
|
207
|
+
# 5. AGENTS.md 생성
|
|
208
|
+
# ─────────────────────────────────────────
|
|
209
|
+
cat > "$AGENTS_FILE" << AGENTSEOF
|
|
210
|
+
# AGENTS.md — Project Context for AI Agents
|
|
211
|
+
|
|
212
|
+
> 이 파일은 모든 AI 에이전트(Claude, Cursor, Copilot, Windsurf 등)의 공통 진입점입니다.
|
|
213
|
+
> CLAUDE.md는 이 파일의 심볼릭 링크입니다.
|
|
214
|
+
> Generated by walwal-harness ($(date +%Y-%m-%d))
|
|
215
|
+
|
|
216
|
+
## Project
|
|
217
|
+
|
|
218
|
+
- **Name**: (Planner가 설정)
|
|
219
|
+
- **Description**: (Planner가 설정)
|
|
220
|
+
- **Phase**: INIT
|
|
221
|
+
- **Type**: ${PROJECT_TYPE}
|
|
222
|
+
- **Harness**: \`.harness/HARNESS.md\` 참조
|
|
223
|
+
|
|
224
|
+
## Tech Stack (Auto-detected)
|
|
225
|
+
|
|
226
|
+
- Backend: ${TECH_BE}
|
|
227
|
+
- Frontend: ${TECH_FE}
|
|
228
|
+
- Database: ${TECH_DB}
|
|
229
|
+
- Monorepo: ${TECH_MONO}
|
|
230
|
+
- OpenAPI: ${OPENAPI}
|
|
231
|
+
|
|
232
|
+
## IA-MAP (Information Architecture)
|
|
233
|
+
|
|
234
|
+
> 자동 스캔으로 생성됨. Planner가 검토 후 확정해야 합니다.
|
|
235
|
+
> \`[?]\` 태그는 Planner가 분류해야 하는 미확인 경로입니다.
|
|
236
|
+
|
|
237
|
+
\`\`\`
|
|
238
|
+
/
|
|
239
|
+
${IA_MAP}
|
|
240
|
+
├── AGENTS.md # [META] 프로젝트 컨텍스트 → Planner
|
|
241
|
+
├── CLAUDE.md # [META] → AGENTS.md 심볼릭 링크
|
|
242
|
+
└── .harness/ # [HARNESS] 하네스 시스템 → Planner
|
|
243
|
+
\`\`\`
|
|
244
|
+
|
|
245
|
+
### IA-MAP 범례
|
|
246
|
+
|
|
247
|
+
| 태그 | 의미 | 소유 에이전트 |
|
|
248
|
+
|------|------|--------------|
|
|
249
|
+
| \`[BE]\` | Backend 영역 | Generator-Backend |
|
|
250
|
+
| \`[FE]\` | Frontend 영역 | Generator-Frontend |
|
|
251
|
+
| \`[HARNESS]\` | 하네스 시스템 | Planner / Evaluator |
|
|
252
|
+
| \`[META]\` | 프로젝트 메타 문서 | Planner |
|
|
253
|
+
| \`[INFRA]\` | 인프라/배포 설정 | Planner |
|
|
254
|
+
| \`[TEST]\` | 테스트 코드 | Evaluator / Generator |
|
|
255
|
+
| \`[?]\` | 미분류 (Planner 확인 필요) | TBD |
|
|
256
|
+
|
|
257
|
+
## Preserved Rules (기존 프로젝트에서 이관)
|
|
258
|
+
|
|
259
|
+
${USER_RULES}
|
|
260
|
+
|
|
261
|
+
## Rules (모든 에이전트 공통)
|
|
262
|
+
|
|
263
|
+
### 읽기/쓰기 권한
|
|
264
|
+
|
|
265
|
+
| 파일 | 읽기 | 쓰기 |
|
|
266
|
+
|------|------|------|
|
|
267
|
+
| AGENTS.md | 전체 | Planner만 |
|
|
268
|
+
| .harness/actions/api-contract.json | 전체 | Planner만 |
|
|
269
|
+
| .harness/actions/feature-list.json | 전체 | passes: Generator, 나머지: Planner |
|
|
270
|
+
| .harness/progress.txt | 전체 | 전체 (세션 종료 시 업데이트 의무) |
|
|
271
|
+
| \`[BE]\` 소유 경로 | 전체 | Generator-Backend만 |
|
|
272
|
+
| \`[FE]\` 소유 경로 | 전체 | Generator-Frontend만 |
|
|
273
|
+
| .harness/archive/ | 전체 | 쓰기 금지 (불변) |
|
|
274
|
+
|
|
275
|
+
### 변경 요청 프로토콜
|
|
276
|
+
|
|
277
|
+
AGENTS.md 또는 api-contract.json 변경이 필요할 때:
|
|
278
|
+
1. sprint-contract.md에 \`## Change Request\` 섹션 추가
|
|
279
|
+
2. Planner가 다음 스프린트 전환 시 반영 여부 결정
|
|
280
|
+
|
|
281
|
+
### 금지 사항
|
|
282
|
+
|
|
283
|
+
- AGENTS.md를 Planner 외 에이전트가 수정
|
|
284
|
+
- 서비스 간 직접 DB 접근
|
|
285
|
+
- 테스트 삭제/약화
|
|
286
|
+
- archive/ 내 파일 수정
|
|
287
|
+
- 프로젝트를 조기 "완료" 선언
|
|
288
|
+
|
|
289
|
+
## Harness Quick Reference
|
|
290
|
+
|
|
291
|
+
| 명령 | 설명 |
|
|
292
|
+
|------|------|
|
|
293
|
+
| \`bash scripts/scan-project.sh\` | 프로젝트 구조 재스캔 |
|
|
294
|
+
| \`.harness/progress.txt\` | 현재 진행 상태 |
|
|
295
|
+
| \`.harness/actions/\` | 활성 스프린트 문서 |
|
|
296
|
+
| \`.harness/HARNESS.md\` | 하네스 상세 가이드 |
|
|
297
|
+
AGENTSEOF
|
|
298
|
+
|
|
299
|
+
echo ""
|
|
300
|
+
echo "=== AGENTS.md Generated ==="
|
|
301
|
+
echo "Path: ${AGENTS_FILE}"
|
|
302
|
+
|
|
303
|
+
# ─────────────────────────────────────────
|
|
304
|
+
# 6. CLAUDE.md 심볼릭 링크
|
|
305
|
+
# ─────────────────────────────────────────
|
|
306
|
+
if [ -f "$CLAUDE_FILE" ] && [ ! -L "$CLAUDE_FILE" ]; then
|
|
307
|
+
echo ""
|
|
308
|
+
echo "WARNING: CLAUDE.md가 일반 파일로 존재합니다."
|
|
309
|
+
echo "기존 내용은 AGENTS.md의 'Preserved Rules' 섹션에 이관되었습니다."
|
|
310
|
+
echo "백업: ${BACKUP_DIR}/"
|
|
311
|
+
rm "$CLAUDE_FILE"
|
|
312
|
+
fi
|
|
313
|
+
|
|
314
|
+
if [ ! -L "$CLAUDE_FILE" ]; then
|
|
315
|
+
ln -sf AGENTS.md "$CLAUDE_FILE"
|
|
316
|
+
echo "Created symlink: CLAUDE.md → AGENTS.md"
|
|
317
|
+
else
|
|
318
|
+
echo "Symlink already exists: CLAUDE.md → AGENTS.md"
|
|
319
|
+
fi
|
|
320
|
+
|
|
321
|
+
echo ""
|
|
322
|
+
echo "=== Initialization Complete ==="
|
|
323
|
+
echo ""
|
|
324
|
+
echo "Next steps:"
|
|
325
|
+
echo " 1. AGENTS.md의 [?] 태그를 확인하고 Planner에게 분류를 요청하세요"
|
|
326
|
+
echo " 2. 'Preserved Rules' 섹션에서 불필요한 규칙을 정리하세요"
|
|
327
|
+
echo " 3. '하네스 엔지니어링 시작'으로 Dispatcher를 실행하세요"
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# scan-project.sh — 기존 프로젝트 구조를 분석하여 하네스 초기화 정보를 생성
|
|
3
|
+
# 출력: .harness/actions/scan-result.json
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
PROJECT_ROOT="${1:-.}"
|
|
7
|
+
OUTPUT="${PROJECT_ROOT}/.harness/actions/scan-result.json"
|
|
8
|
+
|
|
9
|
+
mkdir -p "$(dirname "$OUTPUT")"
|
|
10
|
+
|
|
11
|
+
echo "=== Scanning project: ${PROJECT_ROOT} ==="
|
|
12
|
+
|
|
13
|
+
# ─────────────────────────────────────────
|
|
14
|
+
# 1. 기존 AGENTS.md / CLAUDE.md 감지
|
|
15
|
+
# ─────────────────────────────────────────
|
|
16
|
+
AGENTS_MD="none"
|
|
17
|
+
CLAUDE_MD="none"
|
|
18
|
+
CLAUDE_MD_IS_SYMLINK=false
|
|
19
|
+
EXISTING_RULES=""
|
|
20
|
+
|
|
21
|
+
if [ -f "${PROJECT_ROOT}/AGENTS.md" ]; then
|
|
22
|
+
AGENTS_MD="exists"
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
if [ -L "${PROJECT_ROOT}/CLAUDE.md" ]; then
|
|
26
|
+
CLAUDE_MD="symlink"
|
|
27
|
+
CLAUDE_MD_IS_SYMLINK=true
|
|
28
|
+
elif [ -f "${PROJECT_ROOT}/CLAUDE.md" ]; then
|
|
29
|
+
CLAUDE_MD="exists"
|
|
30
|
+
# 기존 CLAUDE.md 내용 보존용 추출
|
|
31
|
+
EXISTING_RULES=$(cat "${PROJECT_ROOT}/CLAUDE.md")
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# ─────────────────────────────────────────
|
|
35
|
+
# 2. Tech Stack 감지
|
|
36
|
+
# ─────────────────────────────────────────
|
|
37
|
+
TECH_BACKEND="unknown"
|
|
38
|
+
TECH_FRONTEND="unknown"
|
|
39
|
+
TECH_DB="unknown"
|
|
40
|
+
TECH_MONOREPO="none"
|
|
41
|
+
TECH_LANG="unknown"
|
|
42
|
+
|
|
43
|
+
# Backend
|
|
44
|
+
if [ -f "${PROJECT_ROOT}/nest-cli.json" ]; then
|
|
45
|
+
TECH_BACKEND="nestjs"
|
|
46
|
+
TECH_LANG="typescript"
|
|
47
|
+
elif [ -f "${PROJECT_ROOT}/requirements.txt" ] || [ -f "${PROJECT_ROOT}/pyproject.toml" ]; then
|
|
48
|
+
TECH_BACKEND="python"
|
|
49
|
+
TECH_LANG="python"
|
|
50
|
+
if grep -q "fastapi" "${PROJECT_ROOT}/requirements.txt" 2>/dev/null || grep -q "fastapi" "${PROJECT_ROOT}/pyproject.toml" 2>/dev/null; then
|
|
51
|
+
TECH_BACKEND="fastapi"
|
|
52
|
+
elif grep -q "django" "${PROJECT_ROOT}/requirements.txt" 2>/dev/null || grep -q "django" "${PROJECT_ROOT}/pyproject.toml" 2>/dev/null; then
|
|
53
|
+
TECH_BACKEND="django"
|
|
54
|
+
fi
|
|
55
|
+
elif [ -f "${PROJECT_ROOT}/go.mod" ]; then
|
|
56
|
+
TECH_BACKEND="go"
|
|
57
|
+
TECH_LANG="go"
|
|
58
|
+
elif [ -f "${PROJECT_ROOT}/pom.xml" ] || [ -f "${PROJECT_ROOT}/build.gradle" ]; then
|
|
59
|
+
TECH_BACKEND="java"
|
|
60
|
+
TECH_LANG="java"
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# Frontend
|
|
64
|
+
if [ -f "${PROJECT_ROOT}/next.config.js" ] || [ -f "${PROJECT_ROOT}/next.config.ts" ] || [ -f "${PROJECT_ROOT}/next.config.mjs" ]; then
|
|
65
|
+
TECH_FRONTEND="nextjs"
|
|
66
|
+
elif [ -f "${PROJECT_ROOT}/vite.config.ts" ] || [ -f "${PROJECT_ROOT}/vite.config.js" ]; then
|
|
67
|
+
TECH_FRONTEND="vite-react"
|
|
68
|
+
elif [ -f "${PROJECT_ROOT}/angular.json" ]; then
|
|
69
|
+
TECH_FRONTEND="angular"
|
|
70
|
+
elif [ -f "${PROJECT_ROOT}/nuxt.config.ts" ]; then
|
|
71
|
+
TECH_FRONTEND="nuxt"
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
# Monorepo 내 Frontend 감지
|
|
75
|
+
if [ -d "${PROJECT_ROOT}/apps/web" ]; then
|
|
76
|
+
if [ -f "${PROJECT_ROOT}/apps/web/next.config.js" ] || [ -f "${PROJECT_ROOT}/apps/web/next.config.ts" ]; then
|
|
77
|
+
TECH_FRONTEND="nextjs"
|
|
78
|
+
elif [ -f "${PROJECT_ROOT}/apps/web/vite.config.ts" ]; then
|
|
79
|
+
TECH_FRONTEND="vite-react"
|
|
80
|
+
fi
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
# Database
|
|
84
|
+
if grep -rq "typeorm\|prisma\|sequelize\|knex" "${PROJECT_ROOT}/package.json" 2>/dev/null; then
|
|
85
|
+
if grep -q "pg\|postgres" "${PROJECT_ROOT}/package.json" 2>/dev/null; then
|
|
86
|
+
TECH_DB="postgresql"
|
|
87
|
+
elif grep -q "mysql" "${PROJECT_ROOT}/package.json" 2>/dev/null; then
|
|
88
|
+
TECH_DB="mysql"
|
|
89
|
+
elif grep -q "sqlite" "${PROJECT_ROOT}/package.json" 2>/dev/null; then
|
|
90
|
+
TECH_DB="sqlite"
|
|
91
|
+
else
|
|
92
|
+
TECH_DB="orm-detected"
|
|
93
|
+
fi
|
|
94
|
+
elif grep -rq "mongoose\|mongodb" "${PROJECT_ROOT}/package.json" 2>/dev/null; then
|
|
95
|
+
TECH_DB="mongodb"
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
# Monorepo
|
|
99
|
+
if [ -f "${PROJECT_ROOT}/turbo.json" ]; then
|
|
100
|
+
TECH_MONOREPO="turborepo"
|
|
101
|
+
elif [ -f "${PROJECT_ROOT}/nx.json" ]; then
|
|
102
|
+
TECH_MONOREPO="nx"
|
|
103
|
+
elif [ -f "${PROJECT_ROOT}/lerna.json" ]; then
|
|
104
|
+
TECH_MONOREPO="lerna"
|
|
105
|
+
elif [ -f "${PROJECT_ROOT}/pnpm-workspace.yaml" ]; then
|
|
106
|
+
TECH_MONOREPO="pnpm-workspace"
|
|
107
|
+
elif [ -f "${PROJECT_ROOT}/nest-cli.json" ] && grep -q '"monorepo": true' "${PROJECT_ROOT}/nest-cli.json" 2>/dev/null; then
|
|
108
|
+
TECH_MONOREPO="nestjs-monorepo"
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# ─────────────────────────────────────────
|
|
112
|
+
# 3. 디렉토리 트리 스캔 (2 depth)
|
|
113
|
+
# ─────────────────────────────────────────
|
|
114
|
+
TREE=$(find "${PROJECT_ROOT}" -maxdepth 3 -type d \
|
|
115
|
+
! -path "*/node_modules/*" \
|
|
116
|
+
! -path "*/.git/*" \
|
|
117
|
+
! -path "*/.next/*" \
|
|
118
|
+
! -path "*/dist/*" \
|
|
119
|
+
! -path "*/__pycache__/*" \
|
|
120
|
+
! -path "*/.harness/archive/*" \
|
|
121
|
+
! -name "node_modules" \
|
|
122
|
+
! -name ".git" \
|
|
123
|
+
! -name ".next" \
|
|
124
|
+
! -name "dist" \
|
|
125
|
+
2>/dev/null | sort | sed "s|${PROJECT_ROOT}/||g" | sed '/^$/d')
|
|
126
|
+
|
|
127
|
+
# ─────────────────────────────────────────
|
|
128
|
+
# 4. 주요 설정 파일 목록
|
|
129
|
+
# ─────────────────────────────────────────
|
|
130
|
+
CONFIG_FILES=""
|
|
131
|
+
for f in package.json tsconfig.json nest-cli.json next.config.js next.config.ts \
|
|
132
|
+
vite.config.ts docker-compose.yml docker-compose.yaml Dockerfile \
|
|
133
|
+
.env .env.example turbo.json nx.json pyproject.toml requirements.txt \
|
|
134
|
+
go.mod pom.xml build.gradle Makefile; do
|
|
135
|
+
if [ -f "${PROJECT_ROOT}/${f}" ]; then
|
|
136
|
+
CONFIG_FILES="${CONFIG_FILES}\"${f}\","
|
|
137
|
+
fi
|
|
138
|
+
done
|
|
139
|
+
CONFIG_FILES="[${CONFIG_FILES%,}]"
|
|
140
|
+
|
|
141
|
+
# ─────────────────────────────────────────
|
|
142
|
+
# 5. OpenAPI / Swagger 감지
|
|
143
|
+
# ─────────────────────────────────────────
|
|
144
|
+
OPENAPI="none"
|
|
145
|
+
for f in openapi.json openapi.yaml swagger.json swagger.yaml api-spec.json api-spec.yaml; do
|
|
146
|
+
if [ -f "${PROJECT_ROOT}/${f}" ] || [ -f "${PROJECT_ROOT}/docs/${f}" ]; then
|
|
147
|
+
OPENAPI="${f}"
|
|
148
|
+
break
|
|
149
|
+
fi
|
|
150
|
+
done
|
|
151
|
+
|
|
152
|
+
# NestJS Swagger 모듈 감지
|
|
153
|
+
if grep -rq "@nestjs/swagger" "${PROJECT_ROOT}/package.json" 2>/dev/null; then
|
|
154
|
+
if [ "$OPENAPI" = "none" ]; then
|
|
155
|
+
OPENAPI="nestjs-swagger-module (runtime-generated)"
|
|
156
|
+
fi
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
# ─────────────────────────────────────────
|
|
160
|
+
# 6. Git 정보
|
|
161
|
+
# ─────────────────────────────────────────
|
|
162
|
+
GIT_INIT=false
|
|
163
|
+
GIT_COMMITS=0
|
|
164
|
+
GIT_BRANCH="none"
|
|
165
|
+
if [ -d "${PROJECT_ROOT}/.git" ]; then
|
|
166
|
+
GIT_INIT=true
|
|
167
|
+
GIT_COMMITS=$(git -C "${PROJECT_ROOT}" rev-list --count HEAD 2>/dev/null || echo 0)
|
|
168
|
+
GIT_BRANCH=$(git -C "${PROJECT_ROOT}" branch --show-current 2>/dev/null || echo "unknown")
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
# ─────────────────────────────────────────
|
|
172
|
+
# 7. 하네스 기존 상태
|
|
173
|
+
# ─────────────────────────────────────────
|
|
174
|
+
HARNESS_EXISTS=false
|
|
175
|
+
HARNESS_VERSION="none"
|
|
176
|
+
if [ -f "${PROJECT_ROOT}/.harness/config.json" ]; then
|
|
177
|
+
HARNESS_EXISTS=true
|
|
178
|
+
HARNESS_VERSION=$(grep '"version"' "${PROJECT_ROOT}/.harness/config.json" 2>/dev/null | head -1 | sed 's/.*: *"\(.*\)".*/\1/')
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
# ─────────────────────────────────────────
|
|
182
|
+
# 8. JSON 출력
|
|
183
|
+
# ─────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
# 디렉토리 트리를 JSON 배열로 변환
|
|
186
|
+
TREE_JSON=$(echo "$TREE" | awk 'BEGIN{printf "["} NR>1{printf ","} {printf "\"%s\"", $0} END{printf "]"}')
|
|
187
|
+
|
|
188
|
+
# 기존 CLAUDE.md 내용 (JSON escape)
|
|
189
|
+
EXISTING_RULES_JSON=$(echo "$EXISTING_RULES" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))" 2>/dev/null || echo '""')
|
|
190
|
+
|
|
191
|
+
cat > "$OUTPUT" << JSONEOF
|
|
192
|
+
{
|
|
193
|
+
"scanned_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
194
|
+
"project_root": "${PROJECT_ROOT}",
|
|
195
|
+
|
|
196
|
+
"existing_docs": {
|
|
197
|
+
"agents_md": "${AGENTS_MD}",
|
|
198
|
+
"claude_md": "${CLAUDE_MD}",
|
|
199
|
+
"claude_md_is_symlink": ${CLAUDE_MD_IS_SYMLINK},
|
|
200
|
+
"existing_claude_md_content": ${EXISTING_RULES_JSON}
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
"tech_stack": {
|
|
204
|
+
"backend": "${TECH_BACKEND}",
|
|
205
|
+
"frontend": "${TECH_FRONTEND}",
|
|
206
|
+
"database": "${TECH_DB}",
|
|
207
|
+
"monorepo": "${TECH_MONOREPO}",
|
|
208
|
+
"language": "${TECH_LANG}"
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
"structure": {
|
|
212
|
+
"directories": ${TREE_JSON},
|
|
213
|
+
"config_files": ${CONFIG_FILES}
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
"api": {
|
|
217
|
+
"openapi_spec": "${OPENAPI}"
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
"git": {
|
|
221
|
+
"initialized": ${GIT_INIT},
|
|
222
|
+
"commits": ${GIT_COMMITS},
|
|
223
|
+
"branch": "${GIT_BRANCH}"
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
"harness": {
|
|
227
|
+
"exists": ${HARNESS_EXISTS},
|
|
228
|
+
"version": "${HARNESS_VERSION}"
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
"recommendation": {
|
|
232
|
+
"project_type": "$(
|
|
233
|
+
if [ "$TECH_BACKEND" != "unknown" ] && [ "$TECH_FRONTEND" != "unknown" ]; then
|
|
234
|
+
echo "fullstack"
|
|
235
|
+
elif [ "$TECH_BACKEND" != "unknown" ]; then
|
|
236
|
+
echo "backend-only"
|
|
237
|
+
elif [ "$TECH_FRONTEND" != "unknown" ]; then
|
|
238
|
+
echo "frontend-only"
|
|
239
|
+
else
|
|
240
|
+
echo "empty"
|
|
241
|
+
fi
|
|
242
|
+
)",
|
|
243
|
+
"agents_md_action": "$(
|
|
244
|
+
if [ "$AGENTS_MD" = "none" ] && [ "$CLAUDE_MD" = "none" ]; then
|
|
245
|
+
echo "create-new"
|
|
246
|
+
elif [ "$AGENTS_MD" = "none" ] && [ "$CLAUDE_MD" = "exists" ]; then
|
|
247
|
+
echo "migrate-claude-to-agents"
|
|
248
|
+
elif [ "$AGENTS_MD" = "exists" ]; then
|
|
249
|
+
echo "rebuild-with-harness"
|
|
250
|
+
else
|
|
251
|
+
echo "create-new"
|
|
252
|
+
fi
|
|
253
|
+
)"
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
JSONEOF
|
|
257
|
+
|
|
258
|
+
echo ""
|
|
259
|
+
echo "=== Scan Complete ==="
|
|
260
|
+
echo "Output: ${OUTPUT}"
|
|
261
|
+
echo ""
|
|
262
|
+
echo "--- Summary ---"
|
|
263
|
+
echo "Tech Stack: ${TECH_BACKEND} / ${TECH_FRONTEND} / ${TECH_DB}"
|
|
264
|
+
echo "Monorepo: ${TECH_MONOREPO}"
|
|
265
|
+
echo "OpenAPI: ${OPENAPI}"
|
|
266
|
+
echo "Git: ${GIT_INIT} (${GIT_COMMITS} commits, branch: ${GIT_BRANCH})"
|
|
267
|
+
echo "Existing Docs: AGENTS.md=${AGENTS_MD}, CLAUDE.md=${CLAUDE_MD}"
|
|
268
|
+
echo "Harness: ${HARNESS_EXISTS} (v${HARNESS_VERSION})"
|
|
269
|
+
echo "Recommendation: $(cat "$OUTPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(f\"type={d['recommendation']['project_type']}, agents_md={d['recommendation']['agents_md_action']}\")" 2>/dev/null)"
|