@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.
Files changed (35) hide show
  1. package/assets/templates/AGENTS.md.template +52 -0
  2. package/assets/templates/HARNESS.md +159 -0
  3. package/assets/templates/config.json +108 -0
  4. package/assets/templates/progress.txt.template +15 -0
  5. package/bin/init.js +281 -0
  6. package/gotchas/README.md +37 -0
  7. package/gotchas/evaluator-functional.md +5 -0
  8. package/gotchas/evaluator-visual.md +5 -0
  9. package/gotchas/generator-backend.md +5 -0
  10. package/gotchas/generator-frontend.md +5 -0
  11. package/gotchas/planner.md +5 -0
  12. package/package.json +35 -0
  13. package/scripts/init-agents-md.sh +327 -0
  14. package/scripts/scan-project.sh +269 -0
  15. package/skills/dispatcher/SKILL.md +53 -0
  16. package/skills/dispatcher/references/gotcha-flow.md +52 -0
  17. package/skills/dispatcher/references/initialization.md +53 -0
  18. package/skills/dispatcher/references/pipeline-definitions.md +76 -0
  19. package/skills/evaluator-functional/SKILL.md +58 -0
  20. package/skills/evaluator-functional/references/ia-compliance.md +37 -0
  21. package/skills/evaluator-functional/references/playwright-tools.md +43 -0
  22. package/skills/evaluator-functional/references/scoring-rubric.md +52 -0
  23. package/skills/evaluator-visual/SKILL.md +41 -0
  24. package/skills/evaluator-visual/references/responsive-checklist.md +44 -0
  25. package/skills/evaluator-visual/references/scoring-rubric.md +59 -0
  26. package/skills/generator-backend/SKILL.md +43 -0
  27. package/skills/generator-backend/references/nestjs-msa-patterns.md +69 -0
  28. package/skills/generator-backend/references/sprint-contract-be.md +32 -0
  29. package/skills/generator-frontend/SKILL.md +49 -0
  30. package/skills/generator-frontend/references/anti-slop-rules.md +21 -0
  31. package/skills/generator-frontend/references/component-patterns.md +52 -0
  32. package/skills/planner/SKILL.md +48 -0
  33. package/skills/planner/references/api-contract-schema.md +34 -0
  34. package/skills/planner/references/ia-map-guide.md +31 -0
  35. package/skills/planner/references/plan-template.md +51 -0
@@ -0,0 +1,5 @@
1
+ # Gotchas — Planner
2
+
3
+ > Dispatcher가 관리. Planner는 세션 시작 시 이 파일을 읽고 같은 실수를 반복하지 않습니다.
4
+
5
+ <!-- 항목이 추가되면 아래에 기록됩니다 -->
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)"