@walwal-harness/cli 4.0.0-alpha.13 → 4.0.0-alpha.15
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/README.md +226 -273
- package/bin/init.js +1 -0
- package/package.json +1 -1
- package/scripts/harness-dashboard-v4.sh +27 -115
- package/scripts/harness-prompts-v4.sh +106 -0
- package/scripts/harness-studio-v4.sh +61 -37
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# walwal-harness
|
|
1
|
+
# @walwal-harness/cli
|
|
2
2
|
|
|
3
3
|
**AI 에이전트를 위한 프로덕션 하네스 엔지니어링 프레임워크**
|
|
4
4
|
|
|
@@ -9,332 +9,333 @@
|
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
+
## 두 가지 모드
|
|
13
|
+
|
|
14
|
+
| 모드 | 설명 | 실행 방법 |
|
|
15
|
+
|------|------|----------|
|
|
16
|
+
| **Classic (v3)** | 순차 실행 — Planner → Generator → Evaluator 1세션씩 | `하네스 엔지니어링 시작` |
|
|
17
|
+
| **Agent Teams (v4)** | 병렬 실행 — 3 Team이 Feature 단위 Gen→Eval 루프 동시 실행 | `/harness-team` 또는 `npx walwal-harness v4` |
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
12
21
|
## 설치
|
|
13
22
|
|
|
23
|
+
### 첫 설치
|
|
24
|
+
|
|
14
25
|
```bash
|
|
26
|
+
cd your-project
|
|
15
27
|
npm install @walwal-harness/cli
|
|
16
28
|
```
|
|
17
29
|
|
|
18
|
-
`postinstall`이
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
5. `UserPromptSubmit` 훅 등록 (모든 프롬프트를 Dispatcher 경유 자동 라우팅)
|
|
25
|
-
6. `AGENTS.md` 생성 + `CLAUDE.md` 심볼릭 링크
|
|
26
|
-
7. 기존 프로젝트면 구조 스캔 → IA-MAP 자동 생성
|
|
30
|
+
`postinstall`이 자동으로:
|
|
31
|
+
1. `.harness/` 디렉토리 스캐폴딩
|
|
32
|
+
2. `.claude/skills/` 에 에이전트 스킬 설치 (8개)
|
|
33
|
+
3. `scripts/` 에 오케스트레이션 스크립트 설치
|
|
34
|
+
4. SessionStart / UserPromptSubmit 훅 등록
|
|
35
|
+
5. `AGENTS.md` + `CLAUDE.md` 심볼릭 링크 생성
|
|
27
36
|
|
|
28
37
|
> **중요:** 설치 후 Claude Code 세션을 **재시작**해야 skills가 인식됩니다.
|
|
29
|
-
> `/exit` → 디렉터리 재진입이 필요합니다.
|
|
30
38
|
|
|
31
|
-
###
|
|
39
|
+
### 기존 설치자 업데이트
|
|
32
40
|
|
|
33
41
|
```bash
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
npx walwal-harness --help # 도움말
|
|
37
|
-
```
|
|
42
|
+
# latest (v3 안정판)
|
|
43
|
+
npm update @walwal-harness/cli
|
|
38
44
|
|
|
39
|
-
|
|
45
|
+
# v4 alpha (Agent Teams)
|
|
46
|
+
npm install @walwal-harness/cli@next
|
|
40
47
|
|
|
48
|
+
# 스크립트 동기화 (코어 스크립트 자동 덮어쓰기)
|
|
49
|
+
npx walwal-harness
|
|
41
50
|
```
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
│ └── lib/
|
|
52
|
-
│ └── harness-render-progress.sh # 프로그레스 바 렌더러
|
|
53
|
-
├── .harness/
|
|
54
|
-
│ ├── config.json # 하네스 설정
|
|
55
|
-
│ ├── HARNESS.md # 하네스 상세 가이드
|
|
56
|
-
│ ├── progress.json # 세션 간 상태 (기계 판독)
|
|
57
|
-
│ ├── gotchas/ # 에이전트별 실수 기록
|
|
58
|
-
│ ├── actions/ # 활성 스프린트 문서
|
|
59
|
-
│ └── archive/ # 완료 스프린트 보관 (불변)
|
|
60
|
-
└── .claude/
|
|
61
|
-
├── settings.json # 훅 등록 (SessionStart + UserPromptSubmit)
|
|
62
|
-
└── skills/ # Claude Code 스킬 (9개)
|
|
63
|
-
├── harness-dispatcher/
|
|
64
|
-
├── harness-brainstorming/
|
|
65
|
-
├── harness-planner/
|
|
66
|
-
├── harness-generator-backend/
|
|
67
|
-
├── harness-generator-frontend/
|
|
68
|
-
├── harness-generator-frontend-flutter/
|
|
69
|
-
├── harness-evaluator-functional/
|
|
70
|
-
├── harness-evaluator-functional-flutter/
|
|
71
|
-
└── harness-evaluator-visual/
|
|
51
|
+
|
|
52
|
+
### CLI 명령어
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npx walwal-harness # 초기화 / 스크립트 업데이트
|
|
56
|
+
npx walwal-harness --force # 강제 재초기화
|
|
57
|
+
npx walwal-harness studio # Harness Studio v3 (tmux 5-pane)
|
|
58
|
+
npx walwal-harness v4 # Harness Studio v4 (Agent Teams, tmux 6-pane)
|
|
59
|
+
npx walwal-harness --help # 도움말
|
|
72
60
|
```
|
|
73
61
|
|
|
74
62
|
---
|
|
75
63
|
|
|
76
|
-
## Quick Start
|
|
64
|
+
## Quick Start — Classic Mode (v3)
|
|
77
65
|
|
|
78
66
|
### 1. 설치 + 재시작
|
|
79
67
|
|
|
80
68
|
```bash
|
|
81
69
|
cd your-project
|
|
82
70
|
npm install @walwal-harness/cli
|
|
83
|
-
# Claude Code
|
|
71
|
+
# Claude Code 재시작 (/exit → 재진입)
|
|
84
72
|
```
|
|
85
73
|
|
|
86
74
|
### 2. 하네스 시작
|
|
87
75
|
|
|
88
|
-
Claude Code에서 아무 요청이나 입력하면 **UserPromptSubmit 훅이 자동으로 Dispatcher를 호출**합니다. 또는 명시적으로:
|
|
89
|
-
|
|
90
76
|
```
|
|
91
|
-
|
|
77
|
+
❯ 하네스 엔지니어링 시작
|
|
92
78
|
```
|
|
93
79
|
|
|
94
|
-
|
|
80
|
+
또는 Claude Code에서 아무 요청 → UserPromptSubmit 훅이 자동으로 Dispatcher를 호출합니다.
|
|
81
|
+
|
|
82
|
+
### 3. 파이프라인 자동 선택
|
|
83
|
+
|
|
84
|
+
Dispatcher가 요청을 분석하여 파이프라인을 결정합니다:
|
|
95
85
|
|
|
96
86
|
```
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
│ └─ "브레인스토밍 필요합니까?" → Y: Brainstormer → Planner
|
|
101
|
-
│ → N: Planner 직행
|
|
102
|
-
├─ 실수 지적 → Gotcha 기록 → 해당 에이전트 재작업
|
|
103
|
-
├─ 특정 에이전트 명령 → 해당 에이전트 직접 라우팅
|
|
104
|
-
└─ 메타/인사 → 일반 응답
|
|
87
|
+
FULLSTACK : Planner → Gen-BE → Gen-FE → Eval-Func → Eval-Visual
|
|
88
|
+
FE-ONLY : Planner(light) → Gen-FE → Eval-Func → Eval-Visual
|
|
89
|
+
BE-ONLY : Planner → Gen-BE → Eval-Func(API-only)
|
|
105
90
|
```
|
|
106
91
|
|
|
107
|
-
### 4.
|
|
92
|
+
### 4. 순차 실행
|
|
108
93
|
|
|
109
|
-
각 에이전트가
|
|
94
|
+
각 에이전트가 독립 Claude Code 세션에서 실행됩니다:
|
|
110
95
|
|
|
111
96
|
```
|
|
112
|
-
|
|
97
|
+
❯ /harness-planner # Plan 작성
|
|
98
|
+
❯ /harness-generator-frontend # 코드 생성
|
|
99
|
+
❯ /harness-evaluator-functional # E2E 테스트
|
|
113
100
|
```
|
|
114
101
|
|
|
115
|
-
|
|
102
|
+
에이전트가 완료되면 **새 세션**을 시작하면 자동으로 다음 에이전트를 안내합니다.
|
|
116
103
|
|
|
117
|
-
|
|
118
|
-
═══ Sprint 1 / FULLSTACK ═══════════════════
|
|
104
|
+
### 5. Eval FAIL → 자동 재작업
|
|
119
105
|
|
|
120
|
-
|
|
106
|
+
Evaluator FAIL 시 → 실패 원인 + 피드백과 함께 Generator로 자동 리라우팅 (최대 10회).
|
|
121
107
|
|
|
122
|
-
|
|
123
|
-
```
|
|
108
|
+
---
|
|
124
109
|
|
|
125
|
-
|
|
110
|
+
## Quick Start — Agent Teams (v4)
|
|
126
111
|
|
|
127
|
-
|
|
112
|
+
> **v4는 alpha 단계입니다.** `npm install @walwal-harness/cli@next`로 설치하세요.
|
|
128
113
|
|
|
129
|
-
|
|
114
|
+
### 개요
|
|
115
|
+
|
|
116
|
+
3개 Agent Team이 **Feature 단위**로 Gen→Eval 루프를 **병렬 실행**합니다.
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
기존 (v3): Planner → Gen(F-001~028 전부) → Eval(전부)
|
|
120
|
+
v4: Planner → 3 Teams 병렬, 각 Team이 Feature 1개씩 Gen→Eval
|
|
121
|
+
```
|
|
130
122
|
|
|
131
|
-
|
|
123
|
+
### 1. 설치
|
|
132
124
|
|
|
133
|
-
|
|
125
|
+
```bash
|
|
126
|
+
npm install @walwal-harness/cli@next
|
|
127
|
+
npx walwal-harness # 스크립트 동기화
|
|
128
|
+
```
|
|
134
129
|
|
|
135
|
-
###
|
|
130
|
+
### 2. Planner 실행 (Classic으로)
|
|
131
|
+
|
|
132
|
+
v4에서도 Planner는 먼저 실행해야 합니다:
|
|
136
133
|
|
|
137
134
|
```
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
└──→ 재작업 (max 10회)
|
|
135
|
+
❯ 하네스 엔지니어링 시작
|
|
136
|
+
# Dispatcher → Planner → feature-list.json 생성
|
|
141
137
|
```
|
|
142
138
|
|
|
143
|
-
###
|
|
139
|
+
### 3. Agent Teams 실행
|
|
144
140
|
|
|
145
141
|
```
|
|
146
|
-
|
|
147
|
-
└─ OpenAPI → api-contract.json 자동 변환
|
|
142
|
+
❯ /harness-team
|
|
148
143
|
```
|
|
149
144
|
|
|
150
|
-
|
|
145
|
+
또는:
|
|
151
146
|
|
|
147
|
+
```bash
|
|
148
|
+
npx walwal-harness v4
|
|
152
149
|
```
|
|
153
|
-
|
|
150
|
+
|
|
151
|
+
### 4. tmux 레이아웃
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
┌──────────────┬──────────────┬──────────────┐
|
|
155
|
+
│ │ Progress │ Team 1 │
|
|
156
|
+
│ │ (Queue, │ Gen→Eval │
|
|
157
|
+
│ Main │ Teams, ├──────────────┤
|
|
158
|
+
│ (Claude) │ Features) │ Team 2 │
|
|
159
|
+
│ ├──────────────┤ Gen→Eval │
|
|
160
|
+
│ │ Prompts ├──────────────┤
|
|
161
|
+
│ │ (History) │ Team 3 │
|
|
162
|
+
│ │ │ Gen→Eval │
|
|
163
|
+
└──────────────┴──────────────┴──────────────┘
|
|
154
164
|
```
|
|
155
165
|
|
|
156
|
-
|
|
166
|
+
| 패널 | 역할 |
|
|
167
|
+
|------|------|
|
|
168
|
+
| **Main** | 사용자 대화형 Claude — 오케스트레이터 역할 |
|
|
169
|
+
| **Progress** | Feature Queue + Team 상태 + Feature 목록 (자동 갱신) |
|
|
170
|
+
| **Prompts** | 사용자 매뉴얼 프롬프트 + Team 활동 로그 (newest first) |
|
|
171
|
+
| **Team 1~3** | `claude -p --dangerously-skip-permissions` headless worker |
|
|
157
172
|
|
|
158
|
-
###
|
|
173
|
+
### 5. 작동 원리
|
|
159
174
|
|
|
160
|
-
|
|
175
|
+
```
|
|
176
|
+
Feature Queue (dependency-aware topological sort)
|
|
177
|
+
Ready: [F-002, F-003, F-007] ← depends_on 충족된 것만
|
|
178
|
+
Blocked: [F-004, F-005, ...] ← 선행 Feature 미완료
|
|
161
179
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
| **flutter** | mobile | `generator-frontend-flutter` | `evaluator-functional-flutter` (flutter analyze/test) | **SKIP** |
|
|
167
|
-
| **flutter** | desktop | `generator-frontend-flutter` | `evaluator-functional-flutter` (flutter analyze/test) | **SKIP** |
|
|
180
|
+
Team 1: dequeue F-002 → worktree 생성 → Gen → Gate → Eval
|
|
181
|
+
PASS → merge to main → worktree 정리 → unblock dependents → dequeue next
|
|
182
|
+
FAIL → retry (max 3) → 3회 실패 시 failed 처리
|
|
183
|
+
```
|
|
168
184
|
|
|
169
|
-
|
|
185
|
+
- 각 Team은 **git worktree**로 격리 실행 (파일 충돌 없음)
|
|
186
|
+
- Feature PASS 시 main으로 **auto-merge** + worktree 즉시 삭제
|
|
187
|
+
- Merge conflict 시 auto-rebase 시도
|
|
170
188
|
|
|
171
|
-
|
|
189
|
+
### 6. Main에서 할 수 있는 것
|
|
172
190
|
|
|
173
|
-
|
|
191
|
+
Main Claude는 **오케스트레이터**입니다:
|
|
192
|
+
- 대시보드 상태 확인 요청
|
|
193
|
+
- 실패한 Feature 분석/피드백
|
|
194
|
+
- 코드 리뷰, 아키텍처 판단
|
|
195
|
+
- Gotcha 등록 (실수 지적 → 이후 Team에 반영)
|
|
174
196
|
|
|
175
|
-
|
|
197
|
+
Main에서는 `/harness-generator-*`, `/harness-evaluator-*` 를 **직접 호출하지 않습니다** — Team이 처리합니다.
|
|
176
198
|
|
|
177
|
-
|
|
178
|
-
|----------|------|----------|
|
|
179
|
-
| **Dispatcher** | 파이프라인 선택 + Gotcha 관리 + 라우팅 | 항상 (auto-routing 훅) |
|
|
180
|
-
| **Brainstormer** | 러프한 요구사항을 대화형으로 구체화 | Dispatcher가 사용자에게 확인 후 |
|
|
181
|
-
| **Planner** | 제품 사양 + API 계약서 + 서비스 분할 | 파이프라인 시작 시 |
|
|
182
|
-
| **Generator-Backend** | NestJS MSA 구현 (Gateway + Microservices) | FULLSTACK, BE-ONLY |
|
|
183
|
-
| **Generator-Frontend** | React/Next.js UI + API 연동 | FULLSTACK, FE-ONLY (React) |
|
|
184
|
-
| **Generator-Frontend-Flutter** | Flutter (Riverpod + Retrofit + ARB i18n) | FULLSTACK, FE-ONLY (Flutter) |
|
|
185
|
-
| **Evaluator-Functional** | Playwright E2E 기능 검증 | FULLSTACK, FE-ONLY (React) |
|
|
186
|
-
| **Evaluator-Functional-Flutter** | flutter analyze/test + 정적 anti-pattern 검증 | FULLSTACK, FE-ONLY (Flutter) |
|
|
187
|
-
| **Evaluator-Visual** | 디자인 일관성, 반응형, 접근성, AI슬롭 감지 | FULLSTACK, FE-ONLY (React만) |
|
|
199
|
+
---
|
|
188
200
|
|
|
189
|
-
|
|
201
|
+
## 에이전트
|
|
190
202
|
|
|
191
|
-
|
|
192
|
-
|
|
203
|
+
| 에이전트 | 역할 | 모델 |
|
|
204
|
+
|----------|------|------|
|
|
205
|
+
| **Dispatcher** | 파이프라인 선택 + Gotcha 관리 | opus |
|
|
206
|
+
| **Brainstormer** | 러프한 요구사항 대화형 구체화 | opus |
|
|
207
|
+
| **Planner** | 제품 사양 + API 계약 + Feature 설계 | opus/ultraplan |
|
|
208
|
+
| **Generator-Backend** | NestJS MSA 구현 | sonnet |
|
|
209
|
+
| **Generator-Frontend** | React/Next.js UI 구현 | sonnet |
|
|
210
|
+
| **Evaluator-Functional** | Playwright E2E 기능 검증 | opus/ultrathink |
|
|
211
|
+
| **Evaluator-Visual** | 디자인, 반응형, 접근성 검증 | opus/ultrathink |
|
|
193
212
|
|
|
194
|
-
|
|
195
|
-
- HARD-GATE: 사용자 승인 없이 구현 단계로 넘어갈 수 없음
|
|
196
|
-
- 한 번에 한 질문, 객관식 우선
|
|
197
|
-
- 2-3개 접근법 제시 후 사용자가 선택
|
|
198
|
-
- Spec self-review + User Review Gate
|
|
213
|
+
### Brainstormer (조건부)
|
|
199
214
|
|
|
200
|
-
|
|
215
|
+
[obra/superpowers](https://github.com/obra/superpowers) (MIT License) 기반. 바이브코딩 수준의 러프한 요구사항을 대화형 Q&A로 구체화합니다.
|
|
201
216
|
|
|
202
217
|
---
|
|
203
218
|
|
|
204
219
|
## 핵심 메커니즘
|
|
205
220
|
|
|
206
|
-
### Auto-Routing
|
|
221
|
+
### Auto-Routing
|
|
207
222
|
|
|
208
|
-
|
|
223
|
+
모든 사용자 프롬프트가 Dispatcher를 자동 경유합니다.
|
|
209
224
|
|
|
210
|
-
**per-message opt-out:**
|
|
211
|
-
```
|
|
212
|
-
> harness skip 그냥 이것만 답해줘
|
|
213
|
-
> harness 없이 질문 하나만
|
|
214
|
-
> without harness ...
|
|
215
225
|
```
|
|
226
|
+
# per-message opt-out
|
|
227
|
+
❯ harness skip 그냥 답해줘
|
|
216
228
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
// .harness/config.json
|
|
220
|
-
{ "behavior": { "auto_route_dispatcher": false } }
|
|
229
|
+
# 전역 비활성
|
|
230
|
+
.harness/config.json → behavior.auto_route_dispatcher = false
|
|
221
231
|
```
|
|
222
232
|
|
|
223
233
|
### API Contract — 진실의 원천
|
|
224
234
|
|
|
225
|
-
`api-contract.json`이
|
|
226
|
-
|
|
227
|
-
### AGENTS.md — 에이전트 불문 범용 컨텍스트
|
|
235
|
+
`api-contract.json`이 FE ↔ Gateway ↔ Services 간 유일한 계약서입니다.
|
|
228
236
|
|
|
229
|
-
|
|
237
|
+
### Gotcha 시스템
|
|
230
238
|
|
|
231
|
-
|
|
232
|
-
├── apps/gateway/ # [BE] API Gateway → Generator-Backend
|
|
233
|
-
├── apps/service-a/ # [BE] Microservice → Generator-Backend
|
|
234
|
-
├── apps/web/ # [FE] Frontend App → Generator-Frontend
|
|
235
|
-
└── .harness/ # [HARNESS] 하네스 시스템 → Planner
|
|
236
|
-
```
|
|
239
|
+
사용자가 실수를 지적하면 Dispatcher가 자동 기록 → 이후 세션에서 반복 방지.
|
|
237
240
|
|
|
238
|
-
###
|
|
241
|
+
### Pre-Eval Gate
|
|
239
242
|
|
|
240
|
-
|
|
243
|
+
Generator → Evaluator 전환 전 결정론적 검증 (tsc, eslint, test). 실패 시 Evaluator 세션을 열지 않고 Generator로 자동 리라우팅.
|
|
241
244
|
|
|
245
|
+
```json
|
|
246
|
+
// .harness/config.json
|
|
247
|
+
"pre_eval_gate": {
|
|
248
|
+
"frontend_checks": ["npx tsc --noEmit", "npx eslint src/", "npx vitest run --bail 1"],
|
|
249
|
+
"frontend_cwd": "path/to/frontend" // 프로젝트 루트와 다른 경우
|
|
250
|
+
}
|
|
242
251
|
```
|
|
243
|
-
사용자: "API 응답에 created_at은 ISO 8601로 반환해야 해"
|
|
244
|
-
→ Dispatcher: generator-backend.md에 [G-001] 기록
|
|
245
|
-
→ Generator-BE 다음 세션: gotchas 읽기 → 같은 실수 방지
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
### IA Structure Compliance — Step 0 Gate
|
|
249
252
|
|
|
250
|
-
|
|
253
|
+
### Evaluation System
|
|
251
254
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
- Flutter 프로젝트 자동 감지 (`pubspec.yaml` + `flutter:`)
|
|
255
|
+
| 설정 | 값 |
|
|
256
|
+
|------|------|
|
|
257
|
+
| PASS 기준 | 2.80 / 3.00 이상 |
|
|
258
|
+
| FAIL 기준 | 2.79 이하 (예외 없음) |
|
|
259
|
+
| Evidence 없는 Score | 0점 강제 |
|
|
260
|
+
| Regression 실패 | 1건이라도 → 전체 FAIL |
|
|
259
261
|
|
|
260
262
|
---
|
|
261
263
|
|
|
262
|
-
##
|
|
264
|
+
## Harness Studio (tmux)
|
|
263
265
|
|
|
264
|
-
###
|
|
266
|
+
### Studio v3 (Classic)
|
|
265
267
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
| Evaluator-Func | `harness-evaluator-functional` — Playwright MCP (`browser_*`) E2E 테스트 |
|
|
270
|
-
| Evaluator-Visual | `harness-evaluator-visual` — 스크린샷 기반 디자인/접근성/반응형 검증 |
|
|
271
|
-
| 레퍼런스 | Vercel Best Practices, Design System Rules, AI Forbidden Patterns, Component Patterns |
|
|
272
|
-
|
|
273
|
-
### Flutter (Dart) — Web / Mobile / Desktop
|
|
274
|
-
|
|
275
|
-
| 항목 | Web (`fe_target=web`) | Mobile/Desktop (`fe_target=mobile`/`desktop`) |
|
|
276
|
-
|------|----------------------|---------------------------------------------|
|
|
277
|
-
| Generator | `harness-generator-frontend-flutter` (Web 가이드 활성) | `harness-generator-frontend-flutter` (Mobile/Desktop 가이드) |
|
|
278
|
-
| Eval-Func | **`harness-evaluator-functional`** (Playwright MCP) | `harness-evaluator-functional-flutter` (`flutter analyze` + `flutter test` + build_runner drift + FL-01~FL-08 정적 검증) |
|
|
279
|
-
| Eval-Visual | **`harness-evaluator-visual`** (Playwright 스크린샷/반응형/접근성) | **SKIP** (브라우저 없음) |
|
|
280
|
-
| 레퍼런스 공통 | API Layer Pattern, Riverpod Pattern, i18n Pattern, Anti-Patterns | 동일 |
|
|
281
|
-
| 레퍼런스 추가 | **Flutter Web Pattern** (`dart:html`/`package:web` 허용, go_router, CORS, PWA, 빌드/호스팅) | — |
|
|
282
|
-
| dart:html / package:web | **허용** (가능하면 conditional import 권장) | **금지** (모바일/데스크톱 빌드 실패 유발) |
|
|
283
|
-
| 빌드 명령 | `flutter build web --release` / `flutter run -d chrome` | `flutter build apk` / `flutter build ios` / `flutter build macos` 등 |
|
|
284
|
-
|
|
285
|
-
### FE 스택 + 타겟 감지
|
|
286
|
-
|
|
287
|
-
1. `scan-project.sh`가 `pubspec.yaml` + `flutter:` 키를 탐지하여 `fe_stack = "flutter"` 설정
|
|
288
|
-
2. 같은 스캔이 `web/index.html`, `android/`, `ios/`, `macos/`/`windows/`/`linux/` 존재 여부로 `fe_target` 자동 판정 (`web` / `mobile` / `desktop`)
|
|
289
|
-
3. Planner 가 모호한 경우(멀티 타겟, unknown) 사용자에게 단 한 번 확인하고 `pipeline.json.fe_target` 확정
|
|
290
|
-
4. `harness-next.sh`가 `fe_stack + fe_target` 조합에 따라 FE 에이전트를 자동 치환 (Agent Bar에도 반영)
|
|
291
|
-
|
|
292
|
-
---
|
|
293
|
-
|
|
294
|
-
## 스크립트 레퍼런스
|
|
268
|
+
```bash
|
|
269
|
+
npx walwal-harness studio
|
|
270
|
+
```
|
|
295
271
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
272
|
+
```
|
|
273
|
+
┌──────────────┬──────────────┐
|
|
274
|
+
│ Dashboard │ Monitor │
|
|
275
|
+
│ (Progress) ├──────────────┤
|
|
276
|
+
├──────────────┤ Agent Session │
|
|
277
|
+
│ Control ├──────────────┤
|
|
278
|
+
│ (harness>) │ Eval Review │
|
|
279
|
+
└──────────────┴──────────────┘
|
|
280
|
+
```
|
|
303
281
|
|
|
304
|
-
|
|
282
|
+
### Studio v4 (Agent Teams)
|
|
305
283
|
|
|
306
|
-
|
|
284
|
+
```bash
|
|
285
|
+
npx walwal-harness v4
|
|
286
|
+
```
|
|
307
287
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
| Unit Testing | Jest (BE) + Vitest (FE-React) + flutter test (FE-Flutter) |
|
|
316
|
-
| Database | PostgreSQL / SQLite |
|
|
288
|
+
```
|
|
289
|
+
┌──────────────┬──────────────┬──────────────┐
|
|
290
|
+
│ │ Progress │ Team 1 │
|
|
291
|
+
│ Main ├──────────────┤ Team 2 │
|
|
292
|
+
│ (Claude) │ Prompts │ Team 3 │
|
|
293
|
+
└──────────────┴──────────────┴──────────────┘
|
|
294
|
+
```
|
|
317
295
|
|
|
318
296
|
---
|
|
319
297
|
|
|
320
|
-
##
|
|
321
|
-
|
|
322
|
-
하네스는 자체 reference 파일로 기본 가이드를 제공하지만, 아래 외부 스킬을 설치하면 품질이 향상됩니다. 초기화 시 자동으로 설치 여부를 체크하고 안내합니다.
|
|
298
|
+
## 프로젝트 구조
|
|
323
299
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
300
|
+
```
|
|
301
|
+
your-project/
|
|
302
|
+
├── AGENTS.md # 에이전트 공통 컨텍스트
|
|
303
|
+
├── CLAUDE.md → AGENTS.md # 심볼릭 링크
|
|
304
|
+
├── scripts/
|
|
305
|
+
│ ├── harness-next.sh # 세션 오케스트레이터
|
|
306
|
+
│ ├── harness-session-start.sh # SessionStart 훅
|
|
307
|
+
│ ├── harness-user-prompt-submit.sh # UserPromptSubmit 훅
|
|
308
|
+
│ ├── harness-queue-manager.sh # v4: Feature Queue 관리
|
|
309
|
+
│ ├── harness-team-worker.sh # v4: Team Worker (Gen→Eval 루프)
|
|
310
|
+
│ ├── harness-studio-v4.sh # v4: tmux 레이아웃
|
|
311
|
+
│ ├── harness-dashboard-v4.sh # v4: Progress 패널
|
|
312
|
+
│ ├── harness-prompts-v4.sh # v4: Prompts 패널
|
|
313
|
+
│ └── lib/
|
|
314
|
+
├── .harness/
|
|
315
|
+
│ ├── config.json # 하네스 설정
|
|
316
|
+
│ ├── HARNESS.md # 상세 가이드
|
|
317
|
+
│ ├── progress.json # 세션 간 상태
|
|
318
|
+
│ ├── progress.log # append-only 히스토리
|
|
319
|
+
│ ├── gotchas/ # 에이전트별 실수 기록
|
|
320
|
+
│ ├── actions/
|
|
321
|
+
│ │ ├── feature-list.json # 기능 목록 + AC
|
|
322
|
+
│ │ ├── feature-queue.json # v4: Feature Queue 상태
|
|
323
|
+
│ │ ├── api-contract.json # API 계약서
|
|
324
|
+
│ │ └── plan.md # 제품 사양
|
|
325
|
+
│ └── archive/ # 완료 스프린트 보관
|
|
326
|
+
└── .claude/
|
|
327
|
+
├── settings.json # 훅 등록
|
|
328
|
+
└── skills/ # Claude Code 스킬 (8개)
|
|
329
|
+
```
|
|
330
330
|
|
|
331
331
|
---
|
|
332
332
|
|
|
333
|
-
## Playwright MCP 설정
|
|
333
|
+
## Playwright MCP 설정
|
|
334
334
|
|
|
335
|
-
Evaluator
|
|
335
|
+
Evaluator가 브라우저 테스트를 수행하려면 Playwright MCP가 필요합니다:
|
|
336
336
|
|
|
337
337
|
```json
|
|
338
|
+
// ~/.mcp.json
|
|
338
339
|
{
|
|
339
340
|
"mcpServers": {
|
|
340
341
|
"playwright": {
|
|
@@ -345,8 +346,6 @@ Evaluator-Functional / Evaluator-Visual이 브라우저 테스트를 수행하
|
|
|
345
346
|
}
|
|
346
347
|
```
|
|
347
348
|
|
|
348
|
-
> **Flutter Web (`fe_target=web`) 프로젝트도 Playwright 가 필요합니다** — 컴파일 결과가 일반 웹앱과 동일하므로 React 와 같은 evaluator 를 재사용합니다. **Flutter Mobile/Desktop (`fe_target=mobile`/`desktop`) 프로젝트만** Playwright 없이 `flutter analyze` + `flutter test` 기반으로 동작합니다.
|
|
349
|
-
|
|
350
349
|
---
|
|
351
350
|
|
|
352
351
|
## 트러블슈팅
|
|
@@ -354,76 +353,30 @@ Evaluator-Functional / Evaluator-Visual이 브라우저 테스트를 수행하
|
|
|
354
353
|
### Skills가 로드되지 않아요
|
|
355
354
|
|
|
356
355
|
```bash
|
|
357
|
-
# Claude Code
|
|
358
|
-
|
|
359
|
-
# 프로젝트 디렉토리에서 재진입 후 확인
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
Claude Code는 세션 시작 시 `.claude/skills/`를 스캔합니다. `npm install` 이후 반드시 세션 재시작이 필요합니다.
|
|
363
|
-
|
|
364
|
-
### Dispatcher가 자동 호출되지 않아요
|
|
365
|
-
|
|
366
|
-
`UserPromptSubmit` 훅이 정상적으로 등록되었는지 확인:
|
|
367
|
-
|
|
368
|
-
```bash
|
|
369
|
-
cat .claude/settings.json
|
|
370
|
-
# hooks.UserPromptSubmit 배열에 harness-user-prompt-submit.sh가 있어야 함
|
|
356
|
+
/exit # Claude Code 세션 종료
|
|
357
|
+
# 프로젝트 디렉토리에서 재진입
|
|
371
358
|
```
|
|
372
359
|
|
|
373
|
-
|
|
360
|
+
### 스크립트가 구버전이에요
|
|
374
361
|
|
|
375
362
|
```bash
|
|
376
|
-
npx walwal-harness
|
|
377
|
-
# 재시작
|
|
363
|
+
npx walwal-harness # 코어 스크립트 자동 덮어쓰기
|
|
378
364
|
```
|
|
379
365
|
|
|
380
|
-
###
|
|
381
|
-
|
|
382
|
-
Brainstormer는 Dispatcher가 "브레인스토밍 필요합니까?" 물을 때만 실행됩니다. **N**으로 답하면 Planner로 직행합니다. 피드백/이터레이션/특정 에이전트 명령에서는 질문 자체가 나오지 않습니다.
|
|
383
|
-
|
|
384
|
-
### Flutter 프로젝트인데 React 에이전트가 실행돼요
|
|
366
|
+
### v4 Team이 "No features ready" 반복
|
|
385
367
|
|
|
386
368
|
```bash
|
|
387
|
-
#
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
# 재스캔
|
|
391
|
-
bash scripts/scan-project.sh .
|
|
392
|
-
# pipeline.json의 fe_stack + fe_target 값 확인
|
|
393
|
-
cat .harness/actions/pipeline.json | jq '.fe_stack, .fe_target'
|
|
369
|
+
# Studio를 재시작하면 자동으로 stale in_progress 복구
|
|
370
|
+
npx walwal-harness v4
|
|
394
371
|
```
|
|
395
372
|
|
|
396
|
-
###
|
|
397
|
-
|
|
398
|
-
`fe_target` 이 `web` 으로 감지되지 않아서 발생합니다. 다음 두 가지를 확인:
|
|
399
|
-
|
|
400
|
-
```bash
|
|
401
|
-
# 1) Flutter 프로젝트 루트에 web/index.html 이 존재하는가?
|
|
402
|
-
ls -la web/index.html
|
|
403
|
-
# 없으면 Flutter Web 활성화:
|
|
404
|
-
flutter config --enable-web
|
|
405
|
-
flutter create --platforms=web .
|
|
373
|
+
### Auto-routing 끄기
|
|
406
374
|
|
|
407
|
-
# 2) pipeline.json 의 fe_target 값 확인
|
|
408
|
-
jq '.fe_target' .harness/actions/pipeline.json
|
|
409
|
-
# "mobile" 또는 "unknown" 이면 수동 수정 또는 재스캔
|
|
410
|
-
bash scripts/scan-project.sh .
|
|
411
375
|
```
|
|
412
|
-
|
|
413
|
-
`fe_target = web` 으로 확정되면 Playwright 기반 `evaluator-functional` + `evaluator-visual` 이 자동 사용됩니다.
|
|
414
|
-
|
|
415
|
-
### Auto-routing을 끄고 싶어요
|
|
416
|
-
|
|
417
|
-
```json
|
|
418
|
-
// .harness/config.json
|
|
419
|
-
{ "behavior": { "auto_route_dispatcher": false } }
|
|
376
|
+
❯ harness skip 답만 해줘
|
|
420
377
|
```
|
|
421
378
|
|
|
422
|
-
또는
|
|
423
|
-
|
|
424
|
-
```
|
|
425
|
-
> harness skip 그냥 이것만 답해줘
|
|
426
|
-
```
|
|
379
|
+
또는 `.harness/config.json`에서 `behavior.auto_route_dispatcher = false`
|
|
427
380
|
|
|
428
381
|
---
|
|
429
382
|
|
|
@@ -432,7 +385,7 @@ bash scripts/scan-project.sh .
|
|
|
432
385
|
- [Anthropic: Effective Harnesses for Long-Running Agents](https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents)
|
|
433
386
|
- [Anthropic: Harness Design for Long-Running Application Development](https://www.anthropic.com/engineering/harness-design-long-running-apps)
|
|
434
387
|
- [Claude Code Skills Documentation](https://code.claude.com/docs/en/skills)
|
|
435
|
-
- [obra/superpowers](https://github.com/obra/superpowers) — Brainstorming skill
|
|
388
|
+
- [obra/superpowers](https://github.com/obra/superpowers) — Brainstorming skill (MIT License)
|
|
436
389
|
|
|
437
390
|
## License
|
|
438
391
|
|
package/bin/init.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walwal-harness/cli",
|
|
3
|
-
"version": "4.0.0-alpha.
|
|
3
|
+
"version": "4.0.0-alpha.15",
|
|
4
4
|
"description": "Production harness for AI agent engineering — Planner, Generator(BE/FE), Evaluator(Func/Visual), optional Brainstormer (requirements refinement). Supports React and Flutter FE stacks.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"walwal-harness": "bin/init.js"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# harness-dashboard-v4.sh — v4 Dashboard
|
|
3
|
-
#
|
|
2
|
+
# harness-dashboard-v4.sh — v4 Dashboard 상단: Planner Progress
|
|
3
|
+
# Queue + Teams + Features 를 auto-refresh. 고정 영역, 스크롤 없음.
|
|
4
4
|
|
|
5
5
|
set -uo pipefail
|
|
6
6
|
|
|
@@ -30,42 +30,34 @@ render_header() {
|
|
|
30
30
|
now=$(date +"%H:%M:%S")
|
|
31
31
|
project_name=$(jq -r '.project_name // "Unknown"' "$PROGRESS" 2>/dev/null)
|
|
32
32
|
|
|
33
|
-
echo -e "${BOLD}
|
|
34
|
-
echo -e "${BOLD}║ HARNESS v4 — Parallel Agent Teams ║${RESET}"
|
|
35
|
-
echo -e "${BOLD}╚════════════════════════════════════════════════╝${RESET}"
|
|
36
|
-
echo -e " ${DIM}${project_name} | ${now}${RESET}"
|
|
37
|
-
echo ""
|
|
33
|
+
echo -e "${BOLD}HARNESS v4${RESET} ${DIM}${project_name} | ${now}${RESET}"
|
|
38
34
|
}
|
|
39
35
|
|
|
40
36
|
render_queue_summary() {
|
|
41
37
|
if [ ! -f "$QUEUE" ]; then
|
|
42
|
-
echo -e " ${DIM}(queue not initialized
|
|
38
|
+
echo -e " ${DIM}(queue not initialized)${RESET}"
|
|
43
39
|
return
|
|
44
40
|
fi
|
|
45
41
|
|
|
46
|
-
local ready blocked in_prog passed failed total
|
|
42
|
+
local ready blocked in_prog passed failed total
|
|
47
43
|
ready=$(jq '.queue.ready | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
48
44
|
blocked=$(jq '.queue.blocked | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
49
45
|
in_prog=$(jq '.queue.in_progress | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
50
46
|
passed=$(jq '.queue.passed | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
51
47
|
failed=$(jq '.queue.failed | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
52
|
-
concurrency=$(jq '.concurrency // 3' "$QUEUE" 2>/dev/null || echo 3)
|
|
53
48
|
ready=${ready:-0}; blocked=${blocked:-0}; in_prog=${in_prog:-0}; passed=${passed:-0}; failed=${failed:-0}
|
|
54
49
|
total=$((ready + blocked + in_prog + passed + failed))
|
|
55
50
|
|
|
56
|
-
# Progress bar
|
|
57
51
|
local pct=0
|
|
58
52
|
if [ "$total" -gt 0 ]; then pct=$(( passed * 100 / total )); fi
|
|
59
|
-
local bar_w=
|
|
53
|
+
local bar_w=16
|
|
60
54
|
local filled=$(( pct * bar_w / 100 ))
|
|
61
55
|
local empty=$(( bar_w - filled ))
|
|
62
56
|
local bar=""
|
|
63
57
|
for ((i=0; i<filled; i++)); do bar+="█"; done
|
|
64
58
|
for ((i=0; i<empty; i++)); do bar+="░"; done
|
|
65
59
|
|
|
66
|
-
echo -e " ${
|
|
67
|
-
echo -e " Ready:${GREEN}${ready}${RESET} Blocked:${YELLOW}${blocked}${RESET} Progress:${CYAN}${in_prog}${RESET} Pass:${GREEN}${passed}${RESET} Fail:${RED}${failed}${RESET}"
|
|
68
|
-
echo ""
|
|
60
|
+
echo -e " ${bar} ${passed}/${total} (${pct}%) R:${GREEN}${ready}${RESET} B:${YELLOW}${blocked}${RESET} P:${CYAN}${in_prog}${RESET} ${GREEN}✓${passed}${RESET} ${RED}✗${failed}${RESET}"
|
|
69
61
|
}
|
|
70
62
|
|
|
71
63
|
render_teams() {
|
|
@@ -75,49 +67,44 @@ render_teams() {
|
|
|
75
67
|
team_count=$(jq '.teams | length' "$QUEUE" 2>/dev/null)
|
|
76
68
|
if [ "${team_count:-0}" -eq 0 ]; then return; fi
|
|
77
69
|
|
|
78
|
-
echo -e " ${BOLD}Teams${RESET}"
|
|
79
|
-
|
|
80
70
|
for i in $(seq 1 "$team_count"); do
|
|
81
71
|
local t_status t_feature t_phase t_attempt
|
|
82
72
|
t_status=$(jq -r ".teams[\"$i\"].status // \"idle\"" "$QUEUE" 2>/dev/null)
|
|
83
73
|
t_feature=$(jq -r ".teams[\"$i\"].feature // \"—\"" "$QUEUE" 2>/dev/null)
|
|
84
74
|
|
|
85
|
-
# Get phase from in_progress
|
|
86
75
|
if [ "$t_feature" != "—" ] && [ "$t_feature" != "null" ]; then
|
|
87
76
|
t_phase=$(jq -r --arg f "$t_feature" '.queue.in_progress[$f].phase // "?"' "$QUEUE" 2>/dev/null)
|
|
88
77
|
t_attempt=$(jq -r --arg f "$t_feature" '.queue.in_progress[$f].attempt // 1' "$QUEUE" 2>/dev/null)
|
|
89
78
|
else
|
|
90
|
-
t_phase="—"
|
|
91
|
-
t_attempt="—"
|
|
79
|
+
t_phase="—"; t_attempt=""
|
|
92
80
|
fi
|
|
93
81
|
|
|
94
82
|
local icon color
|
|
95
83
|
case "$t_status" in
|
|
96
|
-
busy) icon="▶"
|
|
97
|
-
idle) icon="○"
|
|
98
|
-
|
|
99
|
-
*) icon="?" ; color="$RESET" ;;
|
|
84
|
+
busy) icon="▶"; color="$GREEN" ;;
|
|
85
|
+
idle) icon="○"; color="$DIM" ;;
|
|
86
|
+
*) icon="?"; color="$RESET" ;;
|
|
100
87
|
esac
|
|
101
88
|
|
|
102
|
-
local
|
|
89
|
+
local phase_short=""
|
|
103
90
|
case "$t_phase" in
|
|
104
|
-
gen)
|
|
105
|
-
gate)
|
|
106
|
-
eval)
|
|
107
|
-
*)
|
|
91
|
+
gen) phase_short="${CYAN}G${RESET}" ;;
|
|
92
|
+
gate) phase_short="${YELLOW}K${RESET}" ;;
|
|
93
|
+
eval) phase_short="${MAGENTA}E${RESET}" ;;
|
|
94
|
+
*) phase_short="${DIM}-${RESET}" ;;
|
|
108
95
|
esac
|
|
109
96
|
|
|
110
|
-
printf " %b
|
|
97
|
+
printf " %b%b T%d %-7s %b" "$color" "$icon" "$i" "$t_feature" "$phase_short"
|
|
98
|
+
if [ -n "$t_attempt" ] && [ "$t_attempt" != "—" ]; then
|
|
99
|
+
printf " #%s" "$t_attempt"
|
|
100
|
+
fi
|
|
101
|
+
echo ""
|
|
111
102
|
done
|
|
112
|
-
echo ""
|
|
113
103
|
}
|
|
114
104
|
|
|
115
|
-
|
|
105
|
+
render_features() {
|
|
116
106
|
if [ ! -f "$QUEUE" ] || [ ! -f "$FEATURES" ]; then return; fi
|
|
117
107
|
|
|
118
|
-
echo -e " ${BOLD}Features${RESET}"
|
|
119
|
-
|
|
120
|
-
# Single jq call: merge feature-list + queue state → pre-formatted lines
|
|
121
108
|
jq -r --slurpfile q "$QUEUE" '
|
|
122
109
|
($q[0].queue.passed // []) as $passed |
|
|
123
110
|
($q[0].queue.failed // []) as $failed |
|
|
@@ -125,7 +112,7 @@ render_feature_list() {
|
|
|
125
112
|
($q[0].queue.in_progress // {}) as $prog |
|
|
126
113
|
.features[] |
|
|
127
114
|
.id as $fid |
|
|
128
|
-
(.name // .description // "?" | if length >
|
|
115
|
+
(.name // .description // "?" | if length > 18 then .[0:16] + ".." else . end) as $fname |
|
|
129
116
|
(if ($fid | IN($passed[])) then "P"
|
|
130
117
|
elif $prog[$fid] then "I|\($prog[$fid].team)|\($prog[$fid].phase)"
|
|
131
118
|
elif ($fid | IN($failed[])) then "F"
|
|
@@ -138,100 +125,25 @@ render_feature_list() {
|
|
|
138
125
|
F) printf " ${RED}✗${RESET} %-6s %s\n" "$fid" "$fname" ;;
|
|
139
126
|
R) printf " ${YELLOW}○${RESET} %-6s %s\n" "$fid" "$fname" ;;
|
|
140
127
|
B) printf " ${DIM}◌${RESET} %-6s %s\n" "$fid" "$fname" ;;
|
|
141
|
-
I\|*)
|
|
142
|
-
team=$(echo "$st" | cut -d'|' -f2)
|
|
128
|
+
I\|*) team=$(echo "$st" | cut -d'|' -f2)
|
|
143
129
|
phase=$(echo "$st" | cut -d'|' -f3)
|
|
144
|
-
printf " ${CYAN}◐${RESET} %-6s %-
|
|
130
|
+
printf " ${CYAN}◐${RESET} %-6s %-14s T%s:%s\n" "$fid" "$fname" "$team" "$phase" ;;
|
|
145
131
|
*) printf " ? %-6s %s\n" "$fid" "$fname" ;;
|
|
146
132
|
esac
|
|
147
133
|
done
|
|
148
|
-
|
|
149
|
-
echo ""
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
render_user_prompts() {
|
|
153
|
-
local log_file="$PROJECT_ROOT/.harness/progress.log"
|
|
154
|
-
if [ ! -f "$log_file" ]; then return; fi
|
|
155
|
-
|
|
156
|
-
# Extract user-prompt entries (newest first, max 5)
|
|
157
|
-
local user_lines
|
|
158
|
-
user_lines=$(grep 'user-prompt' "$log_file" 2>/dev/null | tail -r 2>/dev/null | head -5)
|
|
159
|
-
|
|
160
|
-
if [ -z "$user_lines" ]; then return; fi
|
|
161
|
-
|
|
162
|
-
echo -e " ${BOLD}Manual Prompts${RESET} ${DIM}(newest first)${RESET}"
|
|
163
|
-
|
|
164
|
-
echo "$user_lines" | while IFS= read -r line; do
|
|
165
|
-
local ts detail
|
|
166
|
-
ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
|
|
167
|
-
detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
|
|
168
|
-
|
|
169
|
-
# Short timestamp (HH:MM or MM-DD HH:MM)
|
|
170
|
-
local short_ts
|
|
171
|
-
short_ts=$(echo "$ts" | sed 's/^[0-9]*-//')
|
|
172
|
-
|
|
173
|
-
if [ ${#detail} -gt 55 ]; then detail="${detail:0:53}.."; fi
|
|
174
|
-
|
|
175
|
-
echo -e " ${BOLD}★${RESET} ${DIM}${short_ts}${RESET} ${detail}"
|
|
176
|
-
done
|
|
177
|
-
|
|
178
|
-
echo ""
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
render_team_activity() {
|
|
182
|
-
local log_file="$PROJECT_ROOT/.harness/progress.log"
|
|
183
|
-
if [ ! -f "$log_file" ]; then return; fi
|
|
184
|
-
|
|
185
|
-
local max_lines=8
|
|
186
|
-
|
|
187
|
-
echo -e " ${BOLD}Activity${RESET} ${DIM}(newest first)${RESET}"
|
|
188
|
-
|
|
189
|
-
# All non-user-prompt entries, newest first
|
|
190
|
-
grep -v '^#' "$log_file" 2>/dev/null | grep -v '^$' | grep -v 'user-prompt' | \
|
|
191
|
-
tail -r 2>/dev/null | head -"$max_lines" | \
|
|
192
|
-
while IFS= read -r line; do
|
|
193
|
-
local ts agent action detail
|
|
194
|
-
ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
|
|
195
|
-
agent=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$2); print $2}')
|
|
196
|
-
action=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
|
|
197
|
-
detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
|
|
198
|
-
|
|
199
|
-
local short_ts icon color
|
|
200
|
-
short_ts=$(echo "$ts" | sed 's/^[0-9]*-//')
|
|
201
|
-
|
|
202
|
-
case "$agent" in
|
|
203
|
-
dispatcher*) icon="▸"; color="$MAGENTA" ;;
|
|
204
|
-
team-*) icon="⚡"; color="$CYAN" ;;
|
|
205
|
-
manual) icon="★"; color="$BOLD" ;;
|
|
206
|
-
planner*) icon="□"; color="$YELLOW" ;;
|
|
207
|
-
gen*) icon="▶"; color="$GREEN" ;;
|
|
208
|
-
eval*) icon="✦"; color="$RED" ;;
|
|
209
|
-
system) icon="⚙"; color="$DIM" ;;
|
|
210
|
-
*) icon="·"; color="$DIM" ;;
|
|
211
|
-
esac
|
|
212
|
-
|
|
213
|
-
if [ ${#detail} -gt 45 ]; then detail="${detail:0:43}.."; fi
|
|
214
|
-
|
|
215
|
-
echo -e " ${color}${icon}${RESET} ${DIM}${short_ts}${RESET} ${agent} ${DIM}${action}${RESET} ${detail}"
|
|
216
|
-
done
|
|
217
|
-
|
|
218
|
-
echo ""
|
|
219
134
|
}
|
|
220
135
|
|
|
221
136
|
render_all() {
|
|
222
137
|
render_header
|
|
223
138
|
render_queue_summary
|
|
224
139
|
render_teams
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
render_feature_list
|
|
228
|
-
echo -e " ${DIM}Refreshing every 3s${RESET}"
|
|
140
|
+
echo ""
|
|
141
|
+
render_features
|
|
229
142
|
}
|
|
230
143
|
|
|
231
144
|
# ── Main loop ──
|
|
232
145
|
tput civis 2>/dev/null
|
|
233
146
|
trap 'tput cnorm 2>/dev/null; exit 0' EXIT INT TERM
|
|
234
|
-
|
|
235
147
|
clear
|
|
236
148
|
|
|
237
149
|
while true; do
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# harness-prompts-v4.sh — v4 Dashboard 하단: Manual Prompts + Activity
|
|
3
|
+
# progress.log에서 user-prompt와 team 활동을 newest-first로 표시.
|
|
4
|
+
# 스크롤 가능 영역 — 프롬프트가 많아져도 상단 Progress를 가리지 않음.
|
|
5
|
+
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
9
|
+
|
|
10
|
+
PROJECT_ROOT="${1:-}"
|
|
11
|
+
if [ -z "$PROJECT_ROOT" ]; then
|
|
12
|
+
source "$SCRIPT_DIR/lib/harness-render-progress.sh"
|
|
13
|
+
PROJECT_ROOT="$(resolve_harness_root ".")" || { echo "[prompts] .harness/ not found."; exit 1; }
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
LOG_FILE="$PROJECT_ROOT/.harness/progress.log"
|
|
17
|
+
|
|
18
|
+
BOLD="\033[1m"
|
|
19
|
+
DIM="\033[2m"
|
|
20
|
+
GREEN="\033[32m"
|
|
21
|
+
YELLOW="\033[33m"
|
|
22
|
+
RED="\033[31m"
|
|
23
|
+
CYAN="\033[36m"
|
|
24
|
+
MAGENTA="\033[35m"
|
|
25
|
+
RESET="\033[0m"
|
|
26
|
+
|
|
27
|
+
LAST_LINE_COUNT=0
|
|
28
|
+
|
|
29
|
+
render_prompts() {
|
|
30
|
+
if [ ! -f "$LOG_FILE" ]; then
|
|
31
|
+
echo -e " ${DIM}(progress.log not found)${RESET}"
|
|
32
|
+
return
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
local term_h
|
|
36
|
+
term_h=$(tput lines 2>/dev/null || echo 20)
|
|
37
|
+
local max_lines=$((term_h - 3)) # Leave room for header
|
|
38
|
+
if [ "$max_lines" -lt 5 ]; then max_lines=5; fi
|
|
39
|
+
|
|
40
|
+
echo -e "${BOLD}Prompts & Activity${RESET} ${DIM}(newest first)${RESET}"
|
|
41
|
+
echo ""
|
|
42
|
+
|
|
43
|
+
# Read all entries, reverse, limit to terminal height
|
|
44
|
+
grep -v '^#' "$LOG_FILE" 2>/dev/null | grep -v '^$' | \
|
|
45
|
+
tail -r 2>/dev/null | head -"$max_lines" | \
|
|
46
|
+
while IFS= read -r line; do
|
|
47
|
+
local ts agent action detail
|
|
48
|
+
ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
|
|
49
|
+
agent=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$2); print $2}')
|
|
50
|
+
action=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
|
|
51
|
+
detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
|
|
52
|
+
|
|
53
|
+
local short_ts icon color
|
|
54
|
+
short_ts=$(echo "$ts" | sed 's/^[0-9]*-//')
|
|
55
|
+
|
|
56
|
+
case "$agent" in
|
|
57
|
+
user-prompt)
|
|
58
|
+
icon="★"; color="$BOLD"
|
|
59
|
+
# User prompts get full width, highlighted
|
|
60
|
+
if [ ${#detail} -gt 60 ]; then detail="${detail:0:58}.."; fi
|
|
61
|
+
echo -e " ${color}${icon} ${short_ts}${RESET} ${detail}"
|
|
62
|
+
;;
|
|
63
|
+
dispatcher*)
|
|
64
|
+
icon="▸"; color="$MAGENTA"
|
|
65
|
+
if [ ${#detail} -gt 50 ]; then detail="${detail:0:48}.."; fi
|
|
66
|
+
echo -e " ${color}${icon}${RESET} ${DIM}${short_ts}${RESET} ${agent} ${DIM}${action}${RESET} ${detail}"
|
|
67
|
+
;;
|
|
68
|
+
team-*)
|
|
69
|
+
icon="⚡"; color="$CYAN"
|
|
70
|
+
if [ ${#detail} -gt 50 ]; then detail="${detail:0:48}.."; fi
|
|
71
|
+
# Color by action type
|
|
72
|
+
case "$action" in
|
|
73
|
+
*pass*) color="$GREEN"; icon="✓" ;;
|
|
74
|
+
*fail*) color="$RED"; icon="✗" ;;
|
|
75
|
+
*eval*) color="$MAGENTA"; icon="✦" ;;
|
|
76
|
+
*gen*) color="$CYAN"; icon="▶" ;;
|
|
77
|
+
*merge*) color="$GREEN"; icon="⊕" ;;
|
|
78
|
+
esac
|
|
79
|
+
echo -e " ${color}${icon}${RESET} ${DIM}${short_ts}${RESET} ${agent} ${DIM}${action}${RESET} ${detail}"
|
|
80
|
+
;;
|
|
81
|
+
manual)
|
|
82
|
+
icon="★"; color="$YELLOW"
|
|
83
|
+
if [ ${#detail} -gt 50 ]; then detail="${detail:0:48}.."; fi
|
|
84
|
+
echo -e " ${color}${icon}${RESET} ${DIM}${short_ts}${RESET} ${detail}"
|
|
85
|
+
;;
|
|
86
|
+
*)
|
|
87
|
+
icon="·"; color="$DIM"
|
|
88
|
+
if [ ${#detail} -gt 50 ]; then detail="${detail:0:48}.."; fi
|
|
89
|
+
echo -e " ${color}${icon}${RESET} ${DIM}${short_ts}${RESET} ${agent} ${DIM}${action}${RESET} ${detail}"
|
|
90
|
+
;;
|
|
91
|
+
esac
|
|
92
|
+
done
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# ── Main loop ──
|
|
96
|
+
tput civis 2>/dev/null
|
|
97
|
+
trap 'tput cnorm 2>/dev/null; exit 0' EXIT INT TERM
|
|
98
|
+
clear
|
|
99
|
+
|
|
100
|
+
while true; do
|
|
101
|
+
buf=$(render_prompts 2>&1)
|
|
102
|
+
tput cup 0 0 2>/dev/null
|
|
103
|
+
echo "$buf"
|
|
104
|
+
tput ed 2>/dev/null
|
|
105
|
+
sleep 3
|
|
106
|
+
done
|
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# harness-studio-v4.sh — Harness Studio v4: 3-Column
|
|
2
|
+
# harness-studio-v4.sh — Harness Studio v4: 3-Column + Dashboard Split
|
|
3
3
|
#
|
|
4
4
|
# ┌──────────────┬──────────────┬──────────────┐
|
|
5
|
-
# │ │
|
|
6
|
-
# │ │
|
|
7
|
-
# │ Main
|
|
8
|
-
# │ (
|
|
9
|
-
# │ │
|
|
5
|
+
# │ │ Progress │ Team 1 │
|
|
6
|
+
# │ │ (fixed) ├──────────────┤
|
|
7
|
+
# │ Main ├──────────────┤ Team 2 │
|
|
8
|
+
# │ (claude) │ Prompts ├──────────────┤
|
|
9
|
+
# │ │ (scroll) │ Team 3 │
|
|
10
10
|
# └──────────────┴──────────────┴──────────────┘
|
|
11
|
-
#
|
|
12
|
-
# Main: 사용자가 직접 Claude Code를 실행하는 대화형 세션
|
|
13
|
-
# Dashboard: feature-queue + team status 자동 갱신 (입력 불가)
|
|
14
|
-
# Team 1~3: claude -p headless worker (입력 불가, 로그만 표시)
|
|
15
|
-
#
|
|
16
|
-
# Usage:
|
|
17
|
-
# bash scripts/harness-studio-v4.sh [project-root]
|
|
18
|
-
# bash scripts/harness-studio-v4.sh --kill
|
|
19
11
|
|
|
20
12
|
set -euo pipefail
|
|
21
13
|
|
|
@@ -54,7 +46,7 @@ echo "Session: $SESSION_NAME"
|
|
|
54
46
|
|
|
55
47
|
tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true
|
|
56
48
|
|
|
57
|
-
# ──
|
|
49
|
+
# ── Init/recover queue ──
|
|
58
50
|
QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
|
|
59
51
|
if [ ! -f "$QUEUE" ]; then
|
|
60
52
|
echo "Initializing feature queue..."
|
|
@@ -65,47 +57,78 @@ else
|
|
|
65
57
|
fi
|
|
66
58
|
|
|
67
59
|
# ══════════════════════════════════════════
|
|
68
|
-
# 3
|
|
60
|
+
# Layout: 3 columns first, then split middle column
|
|
61
|
+
#
|
|
62
|
+
# Step 1: [Main]
|
|
63
|
+
# Step 2: [Main | Right] — h split 66%
|
|
64
|
+
# Step 3: [Main | Mid | Right] — h split Mid from Right at 50%
|
|
65
|
+
# Step 4: [Main | Progress / Prompts | Right] — v split Mid
|
|
66
|
+
# Step 5: [Main | Progress / Prompts | T1 / T2 / T3] — v split Right
|
|
69
67
|
# ══════════════════════════════════════════
|
|
70
68
|
|
|
71
|
-
#
|
|
69
|
+
# 1. Main
|
|
72
70
|
PANE_MAIN=$(tmux new-session -d -s "$SESSION_NAME" -c "$PROJECT_ROOT" -x 220 -y 55 \
|
|
73
71
|
-P -F '#{pane_id}')
|
|
74
72
|
|
|
75
|
-
#
|
|
76
|
-
|
|
77
|
-
-P -F '#{pane_id}'
|
|
78
|
-
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-dashboard-v4.sh\" \"${PROJECT_ROOT}\"'")
|
|
73
|
+
# 2. Right column (will become Team area)
|
|
74
|
+
PANE_RIGHT=$(tmux split-window -h -p 66 -t "$PANE_MAIN" -c "$PROJECT_ROOT" \
|
|
75
|
+
-P -F '#{pane_id}')
|
|
79
76
|
|
|
80
|
-
#
|
|
81
|
-
|
|
77
|
+
# 3. Middle column (split from Right, 50% = Mid gets left half, Right keeps right half)
|
|
78
|
+
PANE_MID=$(tmux split-window -h -p 50 -t "$PANE_RIGHT" -c "$PROJECT_ROOT" \
|
|
79
|
+
-P -F '#{pane_id}')
|
|
80
|
+
# Now: PANE_MID is the NEW pane (right side of split), PANE_RIGHT stays left
|
|
81
|
+
# Wait — tmux split-window -h on PANE_RIGHT creates new pane to the RIGHT of PANE_RIGHT
|
|
82
|
+
# So PANE_MID ends up on the right, and PANE_RIGHT is in the middle
|
|
83
|
+
# We need to swap: split PANE_MAIN's right, then the rightmost becomes teams
|
|
84
|
+
|
|
85
|
+
# Actually let me reconsider. After step 2:
|
|
86
|
+
# [Main 33%] [Right 66%]
|
|
87
|
+
# After step 3 (split Right horizontally at 50%):
|
|
88
|
+
# [Main 33%] [Right_left 33%] [Right_right 33%]
|
|
89
|
+
# Right_left = PANE_RIGHT (original), Right_right = PANE_MID (new)
|
|
90
|
+
# We want: Right_left = Dashboard area, Right_right = Team area
|
|
91
|
+
# So PANE_RIGHT becomes Dashboard, PANE_MID becomes Teams. But pane IDs...
|
|
92
|
+
# Actually: split-window creates the NEW pane. -h splits horizontally.
|
|
93
|
+
# The new pane goes to the right. So:
|
|
94
|
+
# PANE_RIGHT (original) = middle column (Dashboard)
|
|
95
|
+
# PANE_MID (new) = right column (Teams)
|
|
96
|
+
# Perfect!
|
|
97
|
+
|
|
98
|
+
# 4. Split middle column (PANE_RIGHT = Dashboard area) into Progress + Prompts
|
|
99
|
+
# Kill the shell in PANE_RIGHT first, replace with Progress
|
|
100
|
+
tmux send-keys -t "$PANE_RIGHT" "exec bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-dashboard-v4.sh\" \"${PROJECT_ROOT}\"'" Enter
|
|
101
|
+
|
|
102
|
+
PANE_PROMPTS=$(tmux split-window -v -p 40 -t "$PANE_RIGHT" -c "$PROJECT_ROOT" \
|
|
82
103
|
-P -F '#{pane_id}' \
|
|
83
|
-
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-
|
|
104
|
+
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-prompts-v4.sh\" \"${PROJECT_ROOT}\"'")
|
|
105
|
+
|
|
106
|
+
# 5. Split right column (PANE_MID = Team area) into Team 1/2/3
|
|
107
|
+
tmux send-keys -t "$PANE_MID" "exec bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-team-worker.sh\" 1 \"${PROJECT_ROOT}\"'" Enter
|
|
84
108
|
|
|
85
|
-
|
|
86
|
-
PANE_T2=$(tmux split-window -v -p 66 -t "$PANE_T1" -c "$PROJECT_ROOT" \
|
|
109
|
+
PANE_T2=$(tmux split-window -v -p 66 -t "$PANE_MID" -c "$PROJECT_ROOT" \
|
|
87
110
|
-P -F '#{pane_id}' \
|
|
88
111
|
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-team-worker.sh\" 2 \"${PROJECT_ROOT}\"'")
|
|
89
112
|
|
|
90
|
-
# Team 3 (split below Team 2)
|
|
91
113
|
PANE_T3=$(tmux split-window -v -p 50 -t "$PANE_T2" -c "$PROJECT_ROOT" \
|
|
92
114
|
-P -F '#{pane_id}' \
|
|
93
115
|
"bash --norc --noprofile -c 'exec bash \"${SCRIPT_DIR}/harness-team-worker.sh\" 3 \"${PROJECT_ROOT}\"'")
|
|
94
116
|
|
|
95
|
-
# ── Launch Claude in Main
|
|
117
|
+
# ── Launch Claude in Main ──
|
|
96
118
|
tmux send-keys -t "$PANE_MAIN" "unset npm_config_prefix 2>/dev/null; clear && claude --dangerously-skip-permissions" Enter
|
|
97
119
|
|
|
98
120
|
# ── Pane titles ──
|
|
99
|
-
tmux select-pane -t "$PANE_MAIN"
|
|
100
|
-
tmux select-pane -t "$
|
|
101
|
-
tmux select-pane -t "$
|
|
102
|
-
tmux select-pane -t "$
|
|
103
|
-
tmux select-pane -t "$
|
|
121
|
+
tmux select-pane -t "$PANE_MAIN" -T "Main"
|
|
122
|
+
tmux select-pane -t "$PANE_RIGHT" -T "Progress"
|
|
123
|
+
tmux select-pane -t "$PANE_PROMPTS" -T "Prompts"
|
|
124
|
+
tmux select-pane -t "$PANE_MID" -T "Team 1"
|
|
125
|
+
tmux select-pane -t "$PANE_T2" -T "Team 2"
|
|
126
|
+
tmux select-pane -t "$PANE_T3" -T "Team 3"
|
|
104
127
|
|
|
105
128
|
tmux set-option -t "$SESSION_NAME" pane-border-status top 2>/dev/null || true
|
|
106
129
|
tmux set-option -t "$SESSION_NAME" pane-border-format " #{pane_title} " 2>/dev/null || true
|
|
107
130
|
|
|
108
|
-
# ── Focus Main
|
|
131
|
+
# ── Focus Main ──
|
|
109
132
|
tmux select-pane -t "$PANE_MAIN"
|
|
110
133
|
|
|
111
134
|
# ── Attach ──
|
|
@@ -114,9 +137,10 @@ if [ -n "${TMUX:-}" ]; then
|
|
|
114
137
|
else
|
|
115
138
|
echo ""
|
|
116
139
|
echo "Launching Harness Studio v4..."
|
|
117
|
-
echo " Main (left)
|
|
118
|
-
echo "
|
|
119
|
-
echo "
|
|
140
|
+
echo " Main (left) : Claude interactive"
|
|
141
|
+
echo " Progress (mid↑) : Queue + Teams + Features"
|
|
142
|
+
echo " Prompts (mid↓) : Manual prompts + activity"
|
|
143
|
+
echo " Team 1-3 (right) : Parallel workers"
|
|
120
144
|
echo ""
|
|
121
145
|
tmux attach -t "$SESSION_NAME"
|
|
122
146
|
fi
|