@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
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# AGENTS.md — Project Context for AI Agents
|
|
2
|
+
|
|
3
|
+
> 이 파일은 모든 AI 에이전트(Claude, Cursor, Copilot, Windsurf 등)의 공통 진입점입니다.
|
|
4
|
+
> CLAUDE.md는 이 파일의 심볼릭 링크입니다.
|
|
5
|
+
> Generated by walwal-harness ({{DATE}})
|
|
6
|
+
|
|
7
|
+
## Project
|
|
8
|
+
|
|
9
|
+
- **Name**: (Planner가 설정)
|
|
10
|
+
- **Description**: (Planner가 설정)
|
|
11
|
+
- **Phase**: INIT
|
|
12
|
+
- **Harness**: `.harness/HARNESS.md` 참조
|
|
13
|
+
|
|
14
|
+
## Tech Stack
|
|
15
|
+
|
|
16
|
+
- Backend: (scan-project.sh가 감지)
|
|
17
|
+
- Frontend: (scan-project.sh가 감지)
|
|
18
|
+
- Database: (scan-project.sh가 감지)
|
|
19
|
+
|
|
20
|
+
## IA-MAP (Information Architecture)
|
|
21
|
+
|
|
22
|
+
> Planner가 프로젝트 시작 시 확정합니다.
|
|
23
|
+
> `[?]` 태그는 Planner가 분류해야 하는 미확인 경로입니다.
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
/
|
|
27
|
+
├── AGENTS.md # [META] 프로젝트 컨텍스트 → Planner
|
|
28
|
+
├── CLAUDE.md # [META] → AGENTS.md 심볼릭 링크
|
|
29
|
+
└── .harness/ # [HARNESS] 하네스 시스템 → Planner
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Rules (모든 에이전트 공통)
|
|
33
|
+
|
|
34
|
+
### 읽기/쓰기 권한
|
|
35
|
+
|
|
36
|
+
| 파일 | 읽기 | 쓰기 |
|
|
37
|
+
|------|------|------|
|
|
38
|
+
| AGENTS.md | 전체 | Planner만 |
|
|
39
|
+
| .harness/actions/api-contract.json | 전체 | Planner만 |
|
|
40
|
+
| .harness/actions/feature-list.json | 전체 | passes: Generator, 나머지: Planner |
|
|
41
|
+
| .harness/progress.txt | 전체 | 전체 (세션 종료 시 업데이트 의무) |
|
|
42
|
+
| `[BE]` 소유 경로 | 전체 | Generator-Backend만 |
|
|
43
|
+
| `[FE]` 소유 경로 | 전체 | Generator-Frontend만 |
|
|
44
|
+
| .harness/archive/ | 전체 | 쓰기 금지 (불변) |
|
|
45
|
+
| .harness/gotchas/ | 전체 | Dispatcher만 |
|
|
46
|
+
|
|
47
|
+
### 금지 사항
|
|
48
|
+
|
|
49
|
+
- AGENTS.md를 Planner 외 에이전트가 수정
|
|
50
|
+
- 서비스 간 직접 DB 접근
|
|
51
|
+
- 테스트 삭제/약화
|
|
52
|
+
- archive/ 내 파일 수정
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# 6-Agent Production Harness — 사용 가이드
|
|
2
|
+
|
|
3
|
+
> Anthropic 블로그 "Harness Design for Long-Running Application Development" 기반
|
|
4
|
+
> Solo: 20min/$9 (broken) → Harness: 6hr/$200 (fully functional)
|
|
5
|
+
> Stack: NestJS MSA + React/Next.js + Playwright MCP
|
|
6
|
+
|
|
7
|
+
## 디렉토리 구조
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
.harness/
|
|
11
|
+
├── HARNESS.md # 이 파일
|
|
12
|
+
├── config.json # 하네스 설정
|
|
13
|
+
├── progress.txt # 세션 간 상태 전달 로그
|
|
14
|
+
├── prompts/ # 에이전트 프롬프트 (6개)
|
|
15
|
+
│ ├── dispatcher.md # Dispatcher — 파이프라인 선택
|
|
16
|
+
│ ├── planner.md # Planner — 제품 사양 + MSA 설계
|
|
17
|
+
│ ├── generator-backend.md # Generator-BE — NestJS MSA 구현
|
|
18
|
+
│ ├── generator-frontend.md # Generator-FE — React/Next.js 구현
|
|
19
|
+
│ ├── evaluator-functional.md # Evaluator-기능 — Playwright E2E
|
|
20
|
+
│ └── evaluator-visual.md # Evaluator-비주얼 — 디자인/접근성
|
|
21
|
+
├── actions/ # 현재 활성 문서
|
|
22
|
+
│ ├── pipeline.json # Dispatcher 결정 (어떤 파이프라인인지)
|
|
23
|
+
│ ├── plan.md # 제품 사양
|
|
24
|
+
│ ├── feature-list.json # 기능 추적 (layer + service 필드)
|
|
25
|
+
│ ├── api-contract.json # API 계약서
|
|
26
|
+
│ ├── sprint-contract.md # 현재 스프린트 계약
|
|
27
|
+
│ ├── evaluation-functional.md
|
|
28
|
+
│ └── evaluation-visual.md
|
|
29
|
+
└── archive/ # 완료 스프린트 보관 (불변)
|
|
30
|
+
└── sprint-NNN/
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 실행 흐름
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
사용자: 프로젝트 요청 (자유 형식)
|
|
37
|
+
│
|
|
38
|
+
▼
|
|
39
|
+
┌──────────────────┐
|
|
40
|
+
│ 0. DISPATCHER │ 요청 분석 → pipeline.json 생성
|
|
41
|
+
│ (파이프라인 선택) │ 사용자 확인
|
|
42
|
+
└────────┬─────────┘
|
|
43
|
+
│
|
|
44
|
+
┌────┴────┬──────────┐
|
|
45
|
+
▼ ▼ ▼
|
|
46
|
+
FULLSTACK FE-ONLY BE-ONLY
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### FULLSTACK — 신규 PRD 기반 풀스택
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
Planner → Gen-BE → Gen-FE → Eval-Func → Eval-Visual → Archive
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### FE-ONLY — 기존 API + 프론트엔드 연동
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
Planner(light) → Gen-FE → Eval-Func → Eval-Visual → Archive
|
|
59
|
+
│
|
|
60
|
+
└─ OpenAPI spec → api-contract.json 변환
|
|
61
|
+
Gen-BE SKIP (외부 서버 사용)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### BE-ONLY — 기존 서버 + 백엔드 기능 추가
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
Planner → Gen-BE → Eval-Func(API-only) → Archive
|
|
68
|
+
│
|
|
69
|
+
└─ 기존 코드 분석 후 확장 설계
|
|
70
|
+
Gen-FE SKIP, Eval-Visual SKIP
|
|
71
|
+
Eval-Func: Playwright 대신 curl/httpie API 테스트
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 공통 — 실패 시 루프
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
Eval-Func FAIL → failure_location에 따라 Gen-BE 또는 Gen-FE 재작업 (max 3회)
|
|
78
|
+
Eval-Visual FAIL → Gen-FE 재작업 (max 3회)
|
|
79
|
+
3회 초과 → 사용자 개입 요청
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 수동 프롬프트 실행 방법
|
|
83
|
+
|
|
84
|
+
### Step 0: Dispatcher (항상 최초 실행)
|
|
85
|
+
```
|
|
86
|
+
".harness/prompts/dispatcher.md를 읽고 Dispatcher로 동작하세요.
|
|
87
|
+
[사용자의 자유 형식 요청]"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Step 1: Planner
|
|
91
|
+
```
|
|
92
|
+
".harness/prompts/planner.md를 읽고 Planner 에이전트로 동작하세요.
|
|
93
|
+
pipeline.json을 참조하여 [light/full] 모드로 실행합니다."
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Step 2: Generator-Backend (FULLSTACK, BE-ONLY)
|
|
97
|
+
```
|
|
98
|
+
".harness/prompts/generator-backend.md를 읽고 Generator-Backend으로 동작하세요.
|
|
99
|
+
Sprint [N]을 시작합니다."
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Step 3: Generator-Frontend (FULLSTACK, FE-ONLY)
|
|
103
|
+
```
|
|
104
|
+
".harness/prompts/generator-frontend.md를 읽고 Generator-Frontend으로 동작하세요.
|
|
105
|
+
Sprint [N]의 프론트엔드를 구현합니다."
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Step 4: Evaluator-Functional
|
|
109
|
+
```
|
|
110
|
+
".harness/prompts/evaluator-functional.md를 읽고 Evaluator-Functional로 동작하세요.
|
|
111
|
+
Sprint [N]을 평가합니다. Playwright MCP를 사용하세요."
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Step 5: Evaluator-Visual
|
|
115
|
+
```
|
|
116
|
+
".harness/prompts/evaluator-visual.md를 읽고 Evaluator-Visual로 동작하세요.
|
|
117
|
+
Sprint [N]의 비주얼을 평가합니다."
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Archive (Eval-Visual PASS 후)
|
|
121
|
+
```
|
|
122
|
+
"Sprint [N] 문서를 아카이브 하세요.
|
|
123
|
+
.harness/actions/의 스프린트 문서를 .harness/archive/sprint-NNN/으로 이동합니다."
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## 핵심 원칙
|
|
127
|
+
|
|
128
|
+
1. **Backend First** — API가 안정된 후 Frontend 연동 (없는 API 호출 방지)
|
|
129
|
+
2. **api-contract.json이 진실의 원천** — FE↔Gateway↔Services 간 유일한 계약
|
|
130
|
+
3. **한 세션에 1 스프린트** — 컨텍스트 소진 방지
|
|
131
|
+
4. **feature-list.json의 passes만 수정** — 기능 정의는 Planner만 변경
|
|
132
|
+
5. **테스트 삭제/약화 금지** — 테스트는 계약이다
|
|
133
|
+
6. **Evaluator는 회의적** — 자기 설득 금지, 기준 미달 = FAIL
|
|
134
|
+
7. **아카이브 불변** — 완료 문서 수정 금지
|
|
135
|
+
8. **MSA 경계 존수** — 서비스 간 직접 DB 접근 금지, 반드시 메시지 패턴
|
|
136
|
+
|
|
137
|
+
## Tech Stack
|
|
138
|
+
|
|
139
|
+
| 영역 | 기술 |
|
|
140
|
+
|------|------|
|
|
141
|
+
| Backend Framework | NestJS (TypeScript) |
|
|
142
|
+
| Architecture | MSA (Microservice Architecture) |
|
|
143
|
+
| Monorepo | NestJS 내장 monorepo (nest-cli.json) |
|
|
144
|
+
| Runner | 통합 러너 (`npm run dev` = concurrently) |
|
|
145
|
+
| Transport | TCP (dev) / RabbitMQ·NATS (prod) |
|
|
146
|
+
| Frontend | React 또는 Next.js (TypeScript) |
|
|
147
|
+
| Styling | Tailwind CSS |
|
|
148
|
+
| State | TanStack Query + Zustand |
|
|
149
|
+
| E2E Testing | Playwright MCP |
|
|
150
|
+
| Unit Testing | Jest (backend) + Vitest (frontend) |
|
|
151
|
+
| Database | PostgreSQL (dev: SQLite 가능) |
|
|
152
|
+
|
|
153
|
+
## MCP 도구
|
|
154
|
+
|
|
155
|
+
Playwright MCP (`@playwright/mcp`) — headless + vision 모드:
|
|
156
|
+
- `browser_navigate`, `browser_click`, `browser_fill`
|
|
157
|
+
- `browser_take_screenshot`, `browser_snapshot`
|
|
158
|
+
- `browser_console_messages`, `browser_network_requests`
|
|
159
|
+
- `browser_resize`, `browser_press_key`, `browser_wait`
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
{
|
|
2
|
+
"harness": {
|
|
3
|
+
"name": "6-Agent Production Harness",
|
|
4
|
+
"version": "3.0.0",
|
|
5
|
+
"description": "Dispatcher + NestJS MSA + React/Next.js + Playwright 기반 실무 하네스",
|
|
6
|
+
"source": "https://www.anthropic.com/engineering/harness-design-long-running-apps"
|
|
7
|
+
},
|
|
8
|
+
"agents": {
|
|
9
|
+
"dispatcher": {
|
|
10
|
+
"role": "사용자 요청 분석 → 파이프라인 결정 (FE-ONLY / BE-ONLY / FULLSTACK)",
|
|
11
|
+
"prompt_file": "prompts/dispatcher.md",
|
|
12
|
+
"outputs": ["actions/pipeline.json"],
|
|
13
|
+
"triggers_next": "planner",
|
|
14
|
+
"always_first": true
|
|
15
|
+
},
|
|
16
|
+
"planner": {
|
|
17
|
+
"role": "제품 사양 + API 계약서 + 서비스 분할 설계",
|
|
18
|
+
"prompt_file": "prompts/planner.md",
|
|
19
|
+
"outputs": [
|
|
20
|
+
"actions/plan.md",
|
|
21
|
+
"actions/feature-list.json",
|
|
22
|
+
"actions/api-contract.json"
|
|
23
|
+
],
|
|
24
|
+
"triggers_next": ["generator-backend", "generator-frontend"]
|
|
25
|
+
},
|
|
26
|
+
"generator-backend": {
|
|
27
|
+
"role": "NestJS MSA 서비스 구현, API 엔드포인트, DB 스키마",
|
|
28
|
+
"prompt_file": "prompts/generator-backend.md",
|
|
29
|
+
"inputs": ["actions/plan.md", "actions/feature-list.json", "actions/api-contract.json", "actions/sprint-contract.md"],
|
|
30
|
+
"outputs": ["code:backend", "actions/sprint-contract.md"],
|
|
31
|
+
"triggers_next": "generator-frontend",
|
|
32
|
+
"order": 1
|
|
33
|
+
},
|
|
34
|
+
"generator-frontend": {
|
|
35
|
+
"role": "React/Next.js UI 컴포넌트, 상태관리, API 연동",
|
|
36
|
+
"prompt_file": "prompts/generator-frontend.md",
|
|
37
|
+
"inputs": ["actions/plan.md", "actions/feature-list.json", "actions/api-contract.json", "actions/sprint-contract.md"],
|
|
38
|
+
"outputs": ["code:frontend", "actions/sprint-contract.md"],
|
|
39
|
+
"triggers_next": "evaluator-functional",
|
|
40
|
+
"order": 2
|
|
41
|
+
},
|
|
42
|
+
"evaluator-functional": {
|
|
43
|
+
"role": "Playwright로 E2E 기능 검증, API 응답/DB 상태 확인",
|
|
44
|
+
"prompt_file": "prompts/evaluator-functional.md",
|
|
45
|
+
"tools": ["playwright:browser_*"],
|
|
46
|
+
"inputs": ["actions/sprint-contract.md"],
|
|
47
|
+
"outputs": ["actions/evaluation-functional.md"],
|
|
48
|
+
"on_fail": "generator-backend OR generator-frontend (결함 위치에 따라)",
|
|
49
|
+
"on_pass": "evaluator-visual"
|
|
50
|
+
},
|
|
51
|
+
"evaluator-visual": {
|
|
52
|
+
"role": "디자인 일관성, 반응형, 접근성, AI슬롭 감지",
|
|
53
|
+
"prompt_file": "prompts/evaluator-visual.md",
|
|
54
|
+
"tools": ["playwright:browser_take_screenshot", "playwright:browser_resize", "playwright:browser_snapshot"],
|
|
55
|
+
"inputs": ["actions/sprint-contract.md", "actions/evaluation-functional.md"],
|
|
56
|
+
"outputs": ["actions/evaluation-visual.md"],
|
|
57
|
+
"on_fail": "generator-frontend",
|
|
58
|
+
"on_pass": "archive_and_next_sprint"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"flow": {
|
|
62
|
+
"sequence": ["dispatcher", "planner", "generator-backend", "generator-frontend", "evaluator-functional", "evaluator-visual"],
|
|
63
|
+
"pipeline_selection": {
|
|
64
|
+
"comment": "Dispatcher가 pipeline.json으로 활성 에이전트를 결정. 이하 sequence에서 스킵 대상 제외.",
|
|
65
|
+
"pipelines": {
|
|
66
|
+
"FULLSTACK": ["planner", "generator-backend", "generator-frontend", "evaluator-functional", "evaluator-visual"],
|
|
67
|
+
"FE-ONLY": ["planner:light", "generator-frontend", "evaluator-functional", "evaluator-visual"],
|
|
68
|
+
"BE-ONLY": ["planner", "generator-backend", "evaluator-functional:api-only"]
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"sprint_execution": {
|
|
72
|
+
"comment": "Backend 먼저 → API 안정 후 Frontend 연동 → 기능 QA → 비주얼 QA (FULLSTACK 기준)",
|
|
73
|
+
"backend_first": true,
|
|
74
|
+
"reason": "프론트가 존재하지 않는 API를 호출하는 실패 방지"
|
|
75
|
+
},
|
|
76
|
+
"retry_on_fail": true,
|
|
77
|
+
"max_retries_per_sprint": 3,
|
|
78
|
+
"archive_on_sprint_complete": true
|
|
79
|
+
},
|
|
80
|
+
"paths": {
|
|
81
|
+
"actions": ".harness/actions",
|
|
82
|
+
"archive": ".harness/archive",
|
|
83
|
+
"prompts": ".harness/prompts",
|
|
84
|
+
"gotchas": ".harness/gotchas",
|
|
85
|
+
"progress": ".harness/progress.txt"
|
|
86
|
+
},
|
|
87
|
+
"tech_stack": {
|
|
88
|
+
"backend": {
|
|
89
|
+
"framework": "NestJS (TypeScript)",
|
|
90
|
+
"architecture": "MSA (Microservice Architecture)",
|
|
91
|
+
"monorepo": "Nx 또는 Turborepo",
|
|
92
|
+
"runner": "통합 러너 (단일 명령으로 전체 서비스 기동/종료)",
|
|
93
|
+
"database": "PostgreSQL (dev: SQLite fallback 가능)",
|
|
94
|
+
"api_gateway": "NestJS API Gateway 패턴",
|
|
95
|
+
"transport": "TCP (dev) / RabbitMQ 또는 NATS (prod)"
|
|
96
|
+
},
|
|
97
|
+
"frontend": {
|
|
98
|
+
"framework": "React 또는 Next.js (TypeScript)",
|
|
99
|
+
"state": "Zustand 또는 TanStack Query",
|
|
100
|
+
"styling": "Tailwind CSS"
|
|
101
|
+
},
|
|
102
|
+
"testing": {
|
|
103
|
+
"e2e": "Playwright MCP (browser_* tools)",
|
|
104
|
+
"backend_unit": "Jest (NestJS 내장)",
|
|
105
|
+
"frontend_unit": "Vitest"
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Harness Progress Log
|
|
2
|
+
# 세션 간 상태 전달을 위한 진행 기록 파일
|
|
3
|
+
# ─────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
## [{{DATE}}] Harness Initialized
|
|
6
|
+
- walwal-harness 설치 완료
|
|
7
|
+
- 6-Agent 아키텍처 (Dispatcher → Planner → Gen-BE → Gen-FE → Eval-Func → Eval-Visual)
|
|
8
|
+
- Gotcha 관리 시스템 활성화
|
|
9
|
+
|
|
10
|
+
## Current State
|
|
11
|
+
- Phase: INIT (Dispatcher 실행 대기)
|
|
12
|
+
- Active Sprint: none
|
|
13
|
+
- Features Completed: 0 / 0
|
|
14
|
+
- Blocking Issues: none
|
|
15
|
+
- Next Action: "하네스 엔지니어링 시작"으로 Dispatcher 실행
|
package/bin/init.js
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* walwal-harness initializer
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx walwal-harness # Interactive init
|
|
8
|
+
* npx walwal-harness --auto # Auto init (postinstall)
|
|
9
|
+
* npx walwal-harness --force # Force reinit
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { execSync } = require('child_process');
|
|
15
|
+
|
|
16
|
+
const PROJECT_ROOT = process.cwd();
|
|
17
|
+
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
18
|
+
const HARNESS_DIR = path.join(PROJECT_ROOT, '.harness');
|
|
19
|
+
const CLAUDE_SKILLS_DIR = path.join(PROJECT_ROOT, '.claude', 'skills');
|
|
20
|
+
|
|
21
|
+
const isAuto = process.argv.includes('--auto');
|
|
22
|
+
const isForce = process.argv.includes('--force');
|
|
23
|
+
|
|
24
|
+
// ─────────────────────────────────────────
|
|
25
|
+
// Utility
|
|
26
|
+
// ─────────────────────────────────────────
|
|
27
|
+
function ensureDir(dir) {
|
|
28
|
+
if (!fs.existsSync(dir)) {
|
|
29
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function copyFile(src, dest) {
|
|
34
|
+
ensureDir(path.dirname(dest));
|
|
35
|
+
fs.copyFileSync(src, dest);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function copyDir(src, dest) {
|
|
39
|
+
ensureDir(dest);
|
|
40
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
const srcPath = path.join(src, entry.name);
|
|
43
|
+
const destPath = path.join(dest, entry.name);
|
|
44
|
+
if (entry.isDirectory()) {
|
|
45
|
+
copyDir(srcPath, destPath);
|
|
46
|
+
} else {
|
|
47
|
+
copyFile(srcPath, destPath);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function fileExists(p) {
|
|
53
|
+
return fs.existsSync(p);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function log(msg) {
|
|
57
|
+
console.log(`[walwal-harness] ${msg}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─────────────────────────────────────────
|
|
61
|
+
// 1. .harness/ scaffolding
|
|
62
|
+
// ─────────────────────────────────────────
|
|
63
|
+
function scaffoldHarness() {
|
|
64
|
+
log('Scaffolding .harness/ directory...');
|
|
65
|
+
|
|
66
|
+
// Core directories
|
|
67
|
+
ensureDir(path.join(HARNESS_DIR, 'actions'));
|
|
68
|
+
ensureDir(path.join(HARNESS_DIR, 'archive'));
|
|
69
|
+
ensureDir(path.join(HARNESS_DIR, 'gotchas'));
|
|
70
|
+
|
|
71
|
+
// Copy gotchas initial files
|
|
72
|
+
const gotchasSrc = path.join(PKG_ROOT, 'gotchas');
|
|
73
|
+
if (fs.existsSync(gotchasSrc)) {
|
|
74
|
+
const files = fs.readdirSync(gotchasSrc);
|
|
75
|
+
for (const file of files) {
|
|
76
|
+
const dest = path.join(HARNESS_DIR, 'gotchas', file);
|
|
77
|
+
if (!fileExists(dest) || isForce) {
|
|
78
|
+
copyFile(path.join(gotchasSrc, file), dest);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Copy templates as initial files
|
|
84
|
+
const templateMap = {
|
|
85
|
+
'progress.txt.template': path.join(HARNESS_DIR, 'progress.txt'),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const templatesDir = path.join(PKG_ROOT, 'assets', 'templates');
|
|
89
|
+
if (fs.existsSync(templatesDir)) {
|
|
90
|
+
for (const [template, dest] of Object.entries(templateMap)) {
|
|
91
|
+
const src = path.join(templatesDir, template);
|
|
92
|
+
if (fs.existsSync(src) && (!fileExists(dest) || isForce)) {
|
|
93
|
+
let content = fs.readFileSync(src, 'utf8');
|
|
94
|
+
content = content.replace(/\{\{DATE\}\}/g, new Date().toISOString().split('T')[0]);
|
|
95
|
+
fs.writeFileSync(dest, content);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Copy config.json
|
|
101
|
+
const configSrc = path.join(PKG_ROOT, 'assets', 'templates', 'config.json');
|
|
102
|
+
const configDest = path.join(HARNESS_DIR, 'config.json');
|
|
103
|
+
if (fs.existsSync(configSrc) && (!fileExists(configDest) || isForce)) {
|
|
104
|
+
copyFile(configSrc, configDest);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Copy HARNESS.md
|
|
108
|
+
const harnessMdSrc = path.join(PKG_ROOT, 'assets', 'templates', 'HARNESS.md');
|
|
109
|
+
const harnessMdDest = path.join(HARNESS_DIR, 'HARNESS.md');
|
|
110
|
+
if (fs.existsSync(harnessMdSrc) && (!fileExists(harnessMdDest) || isForce)) {
|
|
111
|
+
copyFile(harnessMdSrc, harnessMdDest);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
log('.harness/ scaffolding complete');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─────────────────────────────────────────
|
|
118
|
+
// 2. Skills → .claude/skills/
|
|
119
|
+
// ─────────────────────────────────────────
|
|
120
|
+
function installSkills() {
|
|
121
|
+
log('Installing skills to .claude/skills/...');
|
|
122
|
+
|
|
123
|
+
const skillsSrc = path.join(PKG_ROOT, 'skills');
|
|
124
|
+
if (!fs.existsSync(skillsSrc)) {
|
|
125
|
+
log('WARNING: skills/ directory not found in package');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const skills = fs.readdirSync(skillsSrc, { withFileTypes: true })
|
|
130
|
+
.filter(d => d.isDirectory())
|
|
131
|
+
.map(d => d.name);
|
|
132
|
+
|
|
133
|
+
for (const skill of skills) {
|
|
134
|
+
const src = path.join(skillsSrc, skill);
|
|
135
|
+
const dest = path.join(CLAUDE_SKILLS_DIR, `harness-${skill}`);
|
|
136
|
+
|
|
137
|
+
if (!fileExists(dest) || isForce) {
|
|
138
|
+
copyDir(src, dest);
|
|
139
|
+
log(` Installed: harness-${skill}`);
|
|
140
|
+
} else {
|
|
141
|
+
log(` Skipped (exists): harness-${skill}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
log('Skills installation complete');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ─────────────────────────────────────────
|
|
149
|
+
// 3. Scripts
|
|
150
|
+
// ─────────────────────────────────────────
|
|
151
|
+
function installScripts() {
|
|
152
|
+
log('Installing scripts...');
|
|
153
|
+
|
|
154
|
+
const scriptsSrc = path.join(PKG_ROOT, 'scripts');
|
|
155
|
+
const scriptsDest = path.join(PROJECT_ROOT, 'scripts');
|
|
156
|
+
|
|
157
|
+
if (fs.existsSync(scriptsSrc)) {
|
|
158
|
+
ensureDir(scriptsDest);
|
|
159
|
+
const files = fs.readdirSync(scriptsSrc);
|
|
160
|
+
for (const file of files) {
|
|
161
|
+
const dest = path.join(scriptsDest, file);
|
|
162
|
+
if (!fileExists(dest) || isForce) {
|
|
163
|
+
copyFile(path.join(scriptsSrc, file), dest);
|
|
164
|
+
try { fs.chmodSync(dest, '755'); } catch (e) {}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
log('Scripts installation complete');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ─────────────────────────────────────────
|
|
173
|
+
// 4. AGENTS.md + CLAUDE.md
|
|
174
|
+
// ─────────────────────────────────────────
|
|
175
|
+
function setupAgentsMd() {
|
|
176
|
+
const agentsMd = path.join(PROJECT_ROOT, 'AGENTS.md');
|
|
177
|
+
const claudeMd = path.join(PROJECT_ROOT, 'CLAUDE.md');
|
|
178
|
+
|
|
179
|
+
// Run scan if no AGENTS.md
|
|
180
|
+
if (!fileExists(agentsMd) || isForce) {
|
|
181
|
+
log('Running project scan...');
|
|
182
|
+
try {
|
|
183
|
+
execSync(`bash "${path.join(PKG_ROOT, 'scripts', 'scan-project.sh')}" "${PROJECT_ROOT}"`, {
|
|
184
|
+
stdio: 'inherit'
|
|
185
|
+
});
|
|
186
|
+
execSync(`bash "${path.join(PKG_ROOT, 'scripts', 'init-agents-md.sh')}" "${PROJECT_ROOT}"`, {
|
|
187
|
+
stdio: 'inherit'
|
|
188
|
+
});
|
|
189
|
+
} catch (e) {
|
|
190
|
+
log('WARNING: Auto-scan failed. Run manually: bash scripts/scan-project.sh .');
|
|
191
|
+
// Create minimal AGENTS.md
|
|
192
|
+
const templateSrc = path.join(PKG_ROOT, 'assets', 'templates', 'AGENTS.md.template');
|
|
193
|
+
if (fs.existsSync(templateSrc)) {
|
|
194
|
+
let content = fs.readFileSync(templateSrc, 'utf8');
|
|
195
|
+
content = content.replace(/\{\{DATE\}\}/g, new Date().toISOString().split('T')[0]);
|
|
196
|
+
fs.writeFileSync(agentsMd, content);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Ensure CLAUDE.md symlink
|
|
202
|
+
if (fileExists(agentsMd)) {
|
|
203
|
+
try {
|
|
204
|
+
const stat = fs.lstatSync(claudeMd);
|
|
205
|
+
if (!stat.isSymbolicLink()) {
|
|
206
|
+
// Backup existing CLAUDE.md
|
|
207
|
+
const backupDir = path.join(HARNESS_DIR, 'archive', 'pre-harness-backup');
|
|
208
|
+
ensureDir(backupDir);
|
|
209
|
+
fs.copyFileSync(claudeMd, path.join(backupDir, `CLAUDE.md.${Date.now()}.bak`));
|
|
210
|
+
fs.unlinkSync(claudeMd);
|
|
211
|
+
fs.symlinkSync('AGENTS.md', claudeMd);
|
|
212
|
+
log('CLAUDE.md backed up and replaced with symlink → AGENTS.md');
|
|
213
|
+
}
|
|
214
|
+
} catch (e) {
|
|
215
|
+
// CLAUDE.md doesn't exist
|
|
216
|
+
try {
|
|
217
|
+
fs.symlinkSync('AGENTS.md', claudeMd);
|
|
218
|
+
log('Created symlink: CLAUDE.md → AGENTS.md');
|
|
219
|
+
} catch (e2) {}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ─────────────────────────────────────────
|
|
225
|
+
// 5. Playwright MCP check
|
|
226
|
+
// ─────────────────────────────────────────
|
|
227
|
+
function checkPlaywrightMcp() {
|
|
228
|
+
const mcpJson = path.join(require('os').homedir(), '.mcp.json');
|
|
229
|
+
|
|
230
|
+
if (fileExists(mcpJson)) {
|
|
231
|
+
try {
|
|
232
|
+
const config = JSON.parse(fs.readFileSync(mcpJson, 'utf8'));
|
|
233
|
+
if (config.mcpServers && config.mcpServers.playwright) {
|
|
234
|
+
log('Playwright MCP: already configured');
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
} catch (e) {}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
log('');
|
|
241
|
+
log('NOTE: Playwright MCP is not configured.');
|
|
242
|
+
log('Evaluator agents require Playwright MCP for browser testing.');
|
|
243
|
+
log('Add to ~/.mcp.json:');
|
|
244
|
+
log('');
|
|
245
|
+
log(' {');
|
|
246
|
+
log(' "mcpServers": {');
|
|
247
|
+
log(' "playwright": {');
|
|
248
|
+
log(' "command": "npx",');
|
|
249
|
+
log(' "args": ["-y", "@playwright/mcp@latest", "--headless", "--caps", "vision"]');
|
|
250
|
+
log(' }');
|
|
251
|
+
log(' }');
|
|
252
|
+
log(' }');
|
|
253
|
+
log('');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ─────────────────────────────────────────
|
|
257
|
+
// Main
|
|
258
|
+
// ─────────────────────────────────────────
|
|
259
|
+
function main() {
|
|
260
|
+
console.log('');
|
|
261
|
+
console.log('╔══════════════════════════════════════╗');
|
|
262
|
+
console.log('║ walwal-harness v1.0.0 ║');
|
|
263
|
+
console.log('║ AI Agent Harness Engineering ║');
|
|
264
|
+
console.log('╚══════════════════════════════════════╝');
|
|
265
|
+
console.log('');
|
|
266
|
+
|
|
267
|
+
scaffoldHarness();
|
|
268
|
+
installSkills();
|
|
269
|
+
installScripts();
|
|
270
|
+
setupAgentsMd();
|
|
271
|
+
checkPlaywrightMcp();
|
|
272
|
+
|
|
273
|
+
console.log('');
|
|
274
|
+
log('═══ Initialization Complete ═══');
|
|
275
|
+
log('');
|
|
276
|
+
log('Start with: "하네스 엔지니어링 시작"');
|
|
277
|
+
log('Or invoke: /harness-dispatcher');
|
|
278
|
+
console.log('');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
main();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Gotchas — Agent Mistake Registry
|
|
2
|
+
|
|
3
|
+
> 이 디렉토리는 각 에이전트가 반복하는 실수를 누적 기록합니다.
|
|
4
|
+
> Dispatcher가 사용자의 실수 지적을 감지하면 해당 에이전트의 gotchas 파일에 추가합니다.
|
|
5
|
+
> 각 에이전트는 세션 시작 시 자신의 gotchas 파일을 읽고 같은 실수를 반복하지 않습니다.
|
|
6
|
+
|
|
7
|
+
## 파일 구조
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
.harness/gotchas/
|
|
11
|
+
├── README.md # 이 파일
|
|
12
|
+
├── planner.md # Planner의 반복 실수
|
|
13
|
+
├── generator-backend.md # Generator-Backend의 반복 실수
|
|
14
|
+
├── generator-frontend.md # Generator-Frontend의 반복 실수
|
|
15
|
+
├── evaluator-functional.md # Evaluator-Functional의 반복 실수
|
|
16
|
+
└── evaluator-visual.md # Evaluator-Visual의 반복 실수
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 항목 형식
|
|
20
|
+
|
|
21
|
+
```markdown
|
|
22
|
+
### [G-NNN] 간결한 제목
|
|
23
|
+
- **Date**: YYYY-MM-DD
|
|
24
|
+
- **Trigger**: 사용자가 한 말 (원문 요약)
|
|
25
|
+
- **Wrong**: 에이전트가 했던 잘못된 행동
|
|
26
|
+
- **Right**: 올바른 행동
|
|
27
|
+
- **Why**: 왜 잘못인지 근거
|
|
28
|
+
- **Scope**: 이 규칙이 적용되는 조건/범위
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 관리 규칙
|
|
32
|
+
|
|
33
|
+
- Dispatcher만 gotchas 파일에 쓰기 가능
|
|
34
|
+
- 각 에이전트는 자신의 gotchas 파일을 읽기 전용으로 참조
|
|
35
|
+
- 중복 항목 금지 — 같은 실수는 기존 항목에 횟수 증가
|
|
36
|
+
- 해결된 항목은 삭제하지 않고 `[RESOLVED]` 태그 추가
|
|
37
|
+
- 항목이 20개 초과 시 가장 오래된 RESOLVED 항목부터 정리
|