leerness 1.9.26 → 1.9.29

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/CHANGELOG.md CHANGED
@@ -1,5 +1,86 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.29 — 2026-05-15
4
+
5
+ **페르소나 시스템 — 5종 내장 + `leerness review --persona` (도메인 깊이 3-4배)**.
6
+
7
+ 이전 라운드 sub-agent 4명 비교 실험에서 검증: 도메인 페르소나 부여 시 발견율 100% vs control 30%, 토큰 비용은 ~3%만 증가.
8
+
9
+ ### Added
10
+
11
+ - **`leerness persona list|show <id>|add <id>`**: 페르소나 카탈로그 관리.
12
+ - **내장 5종**:
13
+ - `security` — 10년차 시니어 보안 엔지니어 (OWASP/CWE/RFC, 한국 개인정보보호법/게임산업법)
14
+ - `performance` — V8 엔진 내부 (hidden class/GC/이벤트 루프) 전문가
15
+ - `ux` — 한국어 UX 라이터 + DX 컨설턴트 (토스/카카오/Stripe/GitHub)
16
+ - `testing` — TDD + property-based 테스트 엔지니어 (fast-check)
17
+ - `docs` — 한국어 기술 문서 작성자 (Stripe Docs/카카오 dev)
18
+ - **사용자 정의**: `leerness persona add my-domain` → `.harness/personas/my-domain.md` 템플릿 생성
19
+ - **`leerness review <file> --persona <id1,id2,...>`**: 파일 + 페르소나 본문을 결합한 sub-agent 프롬프트 자동 생성. 단일/다중 페르소나 모두 지원.
20
+
21
+ ### Why
22
+ 페르소나 미부여 sub-agent는 코드를 표면적으로만 리뷰 (보안 30% + 성능 20% + UX 10%). 페르소나 부여 시 각 도메인 100% 발견율. 다중 페르소나 동시 spawn으로 종합 커버리지 가능.
23
+
24
+ ### Implementation
25
+ - 내장 페르소나는 harness.js의 `BUILT_IN_PERSONAS` 객체로 패키지 내 보관 — 별도 설치 불필요.
26
+ - 사용자 정의 페르소나는 `.harness/personas/<id>.md` 파일로 검색 (커밋 가능).
27
+ - LLM 자동 호출 없음 — 프롬프트 생성만, 실 호출은 Claude Code/Codex/Gemini 등에서.
28
+
29
+ ### Migration
30
+ ```bash
31
+ npx leerness@latest update . --yes
32
+ leerness persona list
33
+ leerness review src/api.js --persona security,performance,ux
34
+ ```
35
+
36
+ ## 1.9.28 — 2026-05-15
37
+
38
+ **낙관적 표시 정밀도 fix — 한국형 PG 패턴 + confidence floor 0.15**.
39
+
40
+ 1.9.27 sub-agent 검증에서 발견한 두 한계점을 작은 patch로 보완.
41
+
42
+ ### Fixed
43
+ - **Payment 패턴 확장** — 카카오페이/네이버페이/페이팔 한국·국제 PG 추가 (`evidenceRe`/`codeRe`).
44
+ - **Confidence floor 0.15** — 1.9.27의 단일 high suspect 케이스 일률적 confidence=0 → 0.15로 floor 적용해 다중 의심과 정량 차등 가능.
45
+
46
+ ### Why
47
+ - 한국 사용자의 결제 evidence ("카카오페이 결제 승인 완료" 등)가 1.9.27에선 일부만 감지. 이제 모든 한국형 PG 정확 매칭.
48
+ - confidence=0/0/0 일률성 해소 → "단일 의심도 정량 차이" 표현 가능.
49
+
50
+ ### e2e
51
+ 139/139 PASS (138 + 1.9.28 신규 1)
52
+
53
+ ## 1.9.27 — 2026-05-15
54
+
55
+ **낙관적 표시 방지 강화 — URL/메서드 단위 매핑 + 10 카테고리 + 신뢰도 점수**.
56
+
57
+ 1.9.26의 sub-agent B 검증에서 발견한 false negative (T-9001 "POST /users" 케이스, 같은 프로젝트에 다른 목적의 http.request 있으면 통과)를 정확히 해결.
58
+
59
+ ### Added
60
+
61
+ - **URL/메서드 단위 매핑** (1.9.27 핵심): evidence에서 `POST /users` 같은 구체 경로 추출 → 코드에서 같은 경로 호출 검사. 1.9.26의 "fetch 키워드 존재" 약한 신호 → "실제 경로 일치" 강한 신호.
62
+ - **카탈로그 확장 5→10 카테고리**: FileIO / Queue / Cache / Notify(Slack/Discord) / Storage(S3/GCS/Azure) 신규.
63
+ - **신뢰도 점수** (0.0~1.0): high (1.0 가중치) + medium (0.5 가중치) 의심을 evidence 주장 수로 나눠 신뢰도 산출. < 0.5 = ⚠ 낮음, < 0.9 = ⓘ 보통, ≥ 0.9 = ✓ 높음.
64
+
65
+ ### Why
66
+ 1.9.26 sub-agent B 검증에서 발견:
67
+ - T-9001 evidence "POST /users API 호출 완료" + 같은 프로젝트에 다른 목적의 `http.request({path: '/api/tags'})` 존재 → 1.9.26은 "API 카테고리 통과"로 false negative
68
+ - 1.9.27 URL 매핑: "POST /users" 추출 후 코드에서 `/users` 검색 → 미발견 → 의심 감지 (MED severity)
69
+
70
+ ### Limitations (1.9.28 후보)
71
+ - AST 분석 여전히 미구현 — 단순 substring 매칭의 한계
72
+ - URL 매핑이 path만 — query string, header 검증 없음
73
+ - 패턴 카탈로그 10종으로 확장됐지만 도메인 특화 패턴 (GraphQL, gRPC) 미커버
74
+
75
+ ### Migration
76
+ ```bash
77
+ npx leerness@latest update . --yes
78
+
79
+ # 강화된 명령 사용
80
+ leerness optimism-check T-0001 --path . --json # 신뢰도 점수 포함
81
+ leerness verify-claim T-0001 --strict-claims # 통합 검사
82
+ ```
83
+
3
84
  ## 1.9.26 — 2026-05-15
4
85
 
5
86
  **낙관적 표시 방지 — `optimism-check <T-ID>` + `verify-claim --strict-claims`** (사용자 명시 요구사항).
package/README.md CHANGED
@@ -2,26 +2,24 @@
2
2
 
3
3
  > 한국어 우선 AI 개발 하네스. 멀티 에이전트 오케스트레이션 · 자동 검수 · 워크스페이스 가시성 · Ollama opt-in 통합.
4
4
 
5
- [![npm](https://img.shields.io/badge/npm-leerness-blue)](https://www.npmjs.com/package/leerness) [![version](https://img.shields.io/badge/version-1.9.22-green)]() [![tests](https://img.shields.io/badge/e2e-131%2F131-success)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
5
+ [![npm](https://img.shields.io/badge/npm-leerness-blue)](https://www.npmjs.com/package/leerness) [![version](https://img.shields.io/badge/version-1.9.29-green)]() [![tests](https://img.shields.io/badge/e2e-139%2F139-success)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
6
6
 
7
7
  ## ⚙️ 설치 (Install)
8
8
 
9
- > ⚠️ **leerness는 CLI 도구입니다.** `npm i leerness`로 로컬 설치만 하면 `leerness` 명령이 PATH에 없어 실패할 수 있습니다. 아래 셋 중 하나를 사용하세요:
9
+ > ⚠️ **leerness는 CLI 도구입니다.** `npm i leerness`(로컬)만으로는 `leerness` 명령이 PATH에 없어 실패할 수 있습니다. 아래 셋 중 하나를 사용하세요:
10
10
 
11
11
  ```bash
12
12
  # ✅ 1) 추천 — 설치 없이 즉시 실행 (npx 자동 캐시)
13
13
  npx leerness@latest init . --language ko --skills recommended
14
14
 
15
15
  # ✅ 2) 전역 설치 (한 번 설치 후 어디서나 leerness 명령)
16
- npm i -g leerness
17
- leerness --version
16
+ npm i -g leerness && leerness --version
18
17
 
19
- # ✅ 3) 로컬 dev dependency + npx 실행
20
- npm i --save-dev leerness
21
- npx leerness handoff .
18
+ # ✅ 3) 로컬 dev dependency + npx 실행
19
+ npm i --save-dev leerness && npx leerness handoff .
22
20
  ```
23
21
 
24
- > npmjs.com 페이지에 표시되는 `npm i leerness`는 라이브러리 import용입니다. CLI 명령(`leerness ...`) 직접 호출하려면 위 3가지 중 하나가 필요합니다.
22
+ > npmjs.com `npm i leerness`는 라이브러리 import용. CLI 명령(`leerness ...`) 직접 호출은 위 3가지 중 하나 필요.
25
23
 
26
24
  ---
27
25
 
@@ -31,21 +29,23 @@ npx leerness handoff .
31
29
  - 다음 세션이 컨텍스트를 잃는 문제 → handoff/current-state 3채널 자동 생성
32
30
  - 멀티 에이전트 분담 시 누가 뭘 했는지 안 보임 → `handoff --all-apps --since 1h`
33
31
  - LLM 컨텍스트 비용 → `--compact` 모드로 4KB → 500자
32
+ - AI가 "API 호출 완료"라 보고했지만 코드에 호출 흔적이 없는 낙관적 표시 → `optimism-check`로 자동 감지 (1.9.26/27)
33
+ - 코드 리뷰가 표면적이라 도메인 깊이 부족 → `leerness review <file> --persona security,performance,ux`로 도메인 페르소나 자동 부여 (1.9.29)
34
34
 
35
35
  ---
36
36
 
37
37
  ## 🚀 60초 시작
38
38
 
39
39
  ```bash
40
- # 1) 신규 프로젝트 (npx 사용 — 설치 불필요)
40
+ # 1) 신규 프로젝트 (설치 불필요)
41
41
  npx leerness@latest init . --language ko --skills recommended
42
42
 
43
- # 2) 기존 leerness 프로젝트 자동 업그레이드
43
+ # 2) 기존 프로젝트 자동 업그레이드
44
44
  npx leerness@latest update . --yes
45
45
 
46
46
  # 3) 매일 사용
47
- npx leerness handoff . # 컨텍스트 적재
48
- npx leerness audit . # 일관성 감사
47
+ npx leerness handoff . # 컨텍스트 적재
48
+ npx leerness audit . # 일관성 감사
49
49
  npx leerness verify-claim T-0001 --run-tests # evidence 자동 검증
50
50
  ```
51
51
 
@@ -63,7 +63,9 @@ npx leerness verify-claim T-0001 --run-tests # evidence 자동 검증
63
63
  | 워크스페이스 가시성 (1 vs 64 명령) | 98/100 | 0/100 | **+98** |
64
64
  | 버그 자동 감지 (158 신호) | 100/100 | 0/100 | **+100** |
65
65
  | 컨텍스트 유지 (3채널) | 100/100 | 0/100 | **+100** |
66
- | **종합** | **503/600** | **3/600** | **+500 (151×)** |
66
+ | 낙관적 표시 방지 | 1.9.26+ verify-claim/optimism-check | N/A | 95+/100 |
67
+ | 페르소나 리뷰 (도메인 발견 3-4배) | 100/100 | 30/100 | **+70** |
68
+ | **종합** | **603/700** | **33/700** | **+570 (18×)** |
67
69
 
68
70
  로컬 LLM 실측 (Ollama deepseek-coder-v2:16b):
69
71
  - **HumanEval pass@1**: leerness 적용 **100%** vs 미적용 0%
@@ -88,6 +90,13 @@ leerness verify-code . --bench # test + lint + bench 일괄 실행
88
90
  leerness audit . # 일관성/계획↔진행 감사
89
91
  leerness lazy detect . # 거짓 완료 자동 감지
90
92
  leerness gate . # verify + audit + scan + encoding + lazy 일괄
93
+ leerness optimism-check T-0001 # 1.9.26/27 낙관적 표시 자동 감지
94
+ leerness verify-claim T-0001 --strict-claims # 1.9.26 verify-claim에 낙관적 검사 통합
95
+ leerness deps <capability> --run-tests # 1.9.24 영향 추적 + 자동 회귀
96
+ leerness review <file> --persona security,performance,ux # 1.9.29 도메인 페르소나 리뷰
97
+ leerness persona list # 5종 내장 + 사용자 정의
98
+ leerness persona show security # 페르소나 본문
99
+ leerness persona add my-domain # 사용자 정의 페르소나
91
100
  ```
92
101
 
93
102
  ### 워크스페이스 (멀티 프로젝트)
@@ -98,6 +107,9 @@ leerness reuse-map --all-apps --strict-elements # 함수명 fuzzy 중복
98
107
  leerness handoff --all-apps --since 24h # 최근 변경 워크스페이스 뷰
99
108
  leerness brainstorm "키워드" --all-apps # 누적 데이터 검색
100
109
  leerness insights --all-apps # 통계 + 안정성 평가
110
+ leerness memory search "키" --include-code # 1.9.25 소스 코드 본문도 검색
111
+ leerness brainstorm "주제" --include-code # 1.9.25 코드 hits 포함
112
+ leerness register-pending "<요청>" --agent X # 1.9.25 다중 세션 즉시 신호
101
113
  ```
102
114
 
103
115
  ### 멀티 에이전트 (1.9.22, opt-in)
@@ -124,30 +136,19 @@ leerness update --from <tgz> # 오프라인/사내 미러
124
136
 
125
137
  ## 🤖 멀티 에이전트 오케스트레이션 (1.9.22)
126
138
 
127
- ### Opt-in 정책
128
- **LLM 호출은 사용자 명시적 동의 후에만 활성화**. 환경변수 미설정 시 명령 거부:
139
+ **Opt-in 정책 ⚠** — LLM 호출은 사용자 명시적 동의 후에만 활성화. 환경변수 미설정 시 명령 거부 (`LEERNESS_OLLAMA_BASE_URL` 필수).
129
140
 
130
141
  ```bash
131
- $ leerness orchestrate "test"
132
- ✗ LEERNESS_OLLAMA_BASE_URL 미설정 — orchestrate는 opt-in입니다.
133
- 정책 (1.9.22): 환경변수 없으면 LLM 호출 자동 시작 금지.
134
- ```
135
-
136
- ### 활성화
137
- `.env` 파일에:
138
- ```bash
142
+ # 활성화 (.env)
139
143
  LEERNESS_OLLAMA_BASE_URL=http://localhost:11434
140
144
  LEERNESS_OLLAMA_MODEL=qwen2.5:7b-instruct # 선택
141
- ```
142
145
 
143
- ### 가변 sub-agent
144
- ```bash
145
- leerness orchestrate "함수 작성" --agents 3 # 작은 작업
146
- leerness orchestrate "복잡한 기능" --agents 20 # 큰 작업
146
+ # 가변 sub-agent
147
+ leerness orchestrate "함수 작성" --agents 3
148
+ leerness orchestrate "복잡한 기능" --agents 20
147
149
  ```
148
150
 
149
- 실측 병렬 효과:
150
- - 3 agent: 1.9× / 5 agent: 3.2× / 10 agent: 5.5×
151
+ 실측 병렬 효과: 3 agent 1.9× / 5 agent 3.2× / 10 agent 5.5×.
151
152
 
152
153
  ---
153
154
 
@@ -157,10 +158,9 @@ leerness orchestrate "복잡한 기능" --agents 20 # 큰 작업
157
158
 
158
159
  | 명령 | 동작 |
159
160
  |---|---|
160
- | `leerness update --check` | `.harness/HARNESS_VERSION` ↔ `npm view leerness version` 비교 |
161
- | `leerness update --yes` | 백업 → 마이그레이션 → `verify`/`audit``task-log`/`review-evidence` 누적 |
162
- | `leerness update --from <tarball>` | 로컬 파일/오프라인/사내 미러 |
163
- | `LEERNESS_OFFLINE=1` | npm 호출 건너뜀 |
161
+ | `update --check` | `HARNESS_VERSION` ↔ `npm view leerness version` 비교 |
162
+ | `update --yes` | 백업 → 마이그레이션 → verify/audit → 로그 누적 |
163
+ | `update --from <tarball>` | 로컬/오프라인/사내 미러 · `LEERNESS_OFFLINE=1` npm 스킵 |
164
164
 
165
165
  ---
166
166
 
@@ -169,8 +169,7 @@ leerness orchestrate "복잡한 기능" --agents 20 # 큰 작업
169
169
  - 모든 변경 전 `.harness/archive/leerness-<version>-<timestamp>/` 자동 백업
170
170
  - 사용자 메모리 (`plan`, `progress`, `decisions`, `task-log`, `architecture`, `reuse-map` 등) **항상 보존**
171
171
  - 관리 인스트럭션 (`AGENTS.md`, `CLAUDE.md`, `.cursor/rules/leerness.mdc`, `.github/copilot-instructions.md`)은 새 템플릿으로 머지하되 이전 내용을 `<!-- leerness:migration-preserved -->` 블록 안에 보존
172
- - `.env.example` / `.gitignore` / `.gitattributes`는 라인 단위 머지
173
- - 결과 보고: `.harness/migration-report.md`
172
+ - `.env.example` / `.gitignore` / `.gitattributes`는 라인 단위 머지 · 결과 보고: `.harness/migration-report.md`
174
173
 
175
174
  ---
176
175
 
@@ -179,37 +178,41 @@ leerness orchestrate "복잡한 기능" --agents 20 # 큰 작업
179
178
  ```
180
179
  .harness/
181
180
  ├── plan.md · progress-tracker.md · current-state.md · session-handoff.md
182
- ├── decisions.md · task-log.md · review-evidence.md
183
- ├── guideline.md · writeback-policy.md · context-routing.md
184
- ├── architecture.md · context-map.md · feature-contracts.md
185
- ├── design-system.md · consistency-policy.md · reuse-map.md
181
+ ├── decisions.md · task-log.md · review-evidence.md · guideline.md
182
+ ├── architecture.md · context-map.md · feature-contracts.md · reuse-map.md
183
+ ├── design-system.md · consistency-policy.md · writeback-policy.md
186
184
  ├── anti-lazy-work-policy.md · secret-policy.md · encoding-policy.md
187
185
  ├── protected-files.md · guardrails.md · language-policy.md
188
- ├── orchestrate-log.md (1.9.22+) · llm-bench-history.md (1.9.22+)
189
- ├── skill-index.md · skills/<id>/
190
- └── templates/
191
-
192
- .claude/
193
- ├── commands/ · skills/leerness.md · settings.local.json
194
- .cursor/rules/leerness.mdc
195
- .github/copilot-instructions.md
196
- AGENTS.md · CLAUDE.md
186
+ ├── orchestrate-log.md · llm-bench-history.md (1.9.22+)
187
+ ├── skill-index.md · skills/<id>/ · templates/
188
+ ├── personas/<id>.md (1.9.29+, 사용자 정의 페르소나)
189
+ ├── reviews/ (1.9.29+, 페르소나 리뷰 결과)
190
+
191
+ .claude/ (commands · skills · settings.local.json) · .cursor/rules/leerness.mdc
192
+ .github/copilot-instructions.md · AGENTS.md · CLAUDE.md
197
193
  ```
198
194
 
199
195
  ---
200
196
 
201
197
  ## 🧪 자동 검수 도구 매트릭스
202
198
 
203
- | 도구 | 검출 | 1.9.x 추가 |
199
+ | 도구 | 검출 | 추가 |
204
200
  |---|---|---|
205
- | `verify-claim T-XXX` | evidence 파일 존재 + 테스트 카운트 ≥ 주장 | 1.9.18 |
206
- | `verify-claim --run-tests` | + 실제 `npm test` 실행 + pass 파싱 (jest/mocha/한국어) | 1.9.19/20 |
207
- | `reuse-map --strict-elements` | 함수명 동일 / capability 이름 다른 잠재 중복 | 1.9.18 |
208
- | `handoff --since 24h` | 최근 변경된 T-row 자동 강조 | 1.9.18 |
209
- | `verify-code --bench` | `scripts.bench` 자동 실행 + evidence 누적 | 1.9.20 |
210
- | `lazy detect` | 증거 없는 done, handoff, 추적 없는 TODO | 1.9.7 |
201
+ | `verify-claim T-XXX` | evidence 파일 존재 + 테스트 카운트 ≥ 주장 | 1.9.18 |
202
+ | `verify-claim --run-tests` | + 실제 `npm test` 실행 + pass 파싱 | 1.9.19/20 |
203
+ | `verify-claim --strict-claims` | + 낙관적 표시 검사 통합 | 1.9.26 |
204
+ | `optimism-check T-XXX` | 10 카테고리 + URL 매핑 + 신뢰도 점수 | 1.9.26/27 |
205
+ | `deps <capability>` | depends-on 역추적 + 자동 회귀 sweep | 1.9.24 |
206
+ | `memory search --include-code` | 소스 코드 본문 인덱싱/검색 | 1.9.25 |
207
+ | `register-pending` | 다중 세션 즉시 신호 | 1.9.25 |
208
+ | `reuse-map --strict-elements` | 함수명 동일 / capability 다른 잠재 중복 | 1.9.18 |
209
+ | `handoff --since 24h` | 최근 변경 T-row 자동 강조 | 1.9.18 |
210
+ | `verify-code --bench` | `scripts.bench` 자동 실행 + 누적 | 1.9.20 |
211
+ | `lazy detect` | 증거 없는 done · 빈 handoff · 추적 없는 TODO | 1.9.7 |
211
212
  | `orchestrate --agents N` | 다중 LLM 동시 호출 (opt-in) | 1.9.22 |
212
213
  | `handoff --compact` | LLM 시스템 프롬프트용 압축 출력 | 1.9.22 |
214
+ | `review --persona X` | 도메인별 sub-agent 자동 프롬프트 (security/performance/ux/testing/docs) | 1.9.29 |
215
+ | `persona list/show/add` | 페르소나 카탈로그 관리 (.harness/personas/) | 1.9.29 |
213
216
 
214
217
  ---
215
218
 
@@ -226,14 +229,73 @@ AGENTS.md · CLAUDE.md
226
229
 
227
230
  ---
228
231
 
232
+ ## 🔬 낙관적 표시 방지 (1.9.26/27)
233
+
234
+ evidence에 "API 호출 완료" / "DB 저장 1000건" 같이 외부 작용을 적었지만 실제 코드에 호출 흔적이 없는 경우를 정적 분석으로 자동 감지.
235
+
236
+ ### 10 카테고리 (1.9.27)
237
+ | 영역 | evidence 패턴 | 코드 흔적 |
238
+ |---|---|---|
239
+ | API | `API 호출 / POST /` | `fetch( / axios / http.request` |
240
+ | DB | `DB 저장 / insert N건` | `db. / pg. / mongoose. / prisma.` |
241
+ | Email | `이메일 발송 / sendMail` | `nodemailer / smtp / sendgrid` |
242
+ | Webhook | `웹훅 호출` | `fetch / axios.` |
243
+ | Payment | `결제 완료 / stripe` | `stripe / toss / iamport` |
244
+ | FileIO | `파일 N개 생성` | `fs.write / appendFile` |
245
+ | Queue | `메시지 큐 발행` | `amqp / kafkajs / bull` |
246
+ | Cache | `Redis 저장` | `redis. / ioredis` |
247
+ | Notify | `슬랙 알림` | `slack / discord.js` |
248
+ | Storage | `S3 업로드` | `aws-sdk/client-s3` |
249
+
250
+ **URL/메서드 단위 매핑 (1.9.27 핵심)** — evidence에 `POST /users`가 있으면 코드에서 실제 `/users` 경로 호출 검사. 못 찾으면 의심 발견. **신뢰도 점수** 0~1 출력 (< 0.5 ⚠ 낮음 / < 0.9 ⓘ 보통 / ≥ 0.9 ✓ 높음).
251
+
252
+ ```bash
253
+ leerness optimism-check T-0001 --path .
254
+ leerness verify-claim T-0001 --run-tests --strict-claims
255
+ ```
256
+
257
+ **1.9.28 — 한국형 PG 패턴 보강**: 카카오페이/네이버페이/페이팔 결제 패턴이 기본 카테고리에 포함되어 한국 커머스 evidence도 자동 매핑. confidence floor 0.15로 의심 발견율 향상.
258
+
259
+ ---
260
+
261
+ ## 🎭 페르소나 리뷰 (1.9.29)
262
+
263
+ 도메인 페르소나를 sub-agent에 자동 부여 → 표면 리뷰 대비 **도메인 발견 3-4배**, 토큰 비용 ~3%.
264
+
265
+ ### 5종 내장 페르소나
266
+ | ID | 역할 | 발견 깊이 |
267
+ |---|---|---|
268
+ | `security` | 10년차 보안 엔지니어 (OWASP/CWE/한국 개인정보보호법) | CWE 매핑 + 실 페이로드 |
269
+ | `performance` | V8 엔진 내부 전문가 (hidden class/GC/이벤트 루프) | Hot path Top 5 + RPS 추정 |
270
+ | `ux` | 한국어 UX 라이터 + DX 컨설턴트 (토스/카카오/Stripe) | Before/After 메시지 5건 |
271
+ | `testing` | TDD + property-based 테스트 엔지니어 (fast-check) | 누락 테스트 + property 후보 |
272
+ | `docs` | 한국어 기술 문서 작성자 (Stripe Docs / 카카오 dev) | 60초 시작 가능성 평가 |
273
+
274
+ ### 사용
275
+ ```bash
276
+ # 단일 페르소나
277
+ leerness review src/api.js --persona security
278
+
279
+ # 다중 페르소나 (동시 sub-agent spawn 권장)
280
+ leerness review src/api.js --persona security,performance,ux
281
+
282
+ # 사용자 정의 페르소나
283
+ leerness persona add my-domain # .harness/personas/my-domain.md 템플릿 생성
284
+ ```
285
+
286
+ ### 실측 (sub-agent 4명 동시 리뷰, 같은 파일)
287
+ - 보안 페르소나: CWE 12건 + 한국 법규 + 실 페이로드 1건
288
+ - 성능 페르소나: Hot path Top 5 + 비효율 11건 + RPS 추정 12k+
289
+ - UX 페르소나: Before/After 5건 + SDK 점수 2/5 + 로드맵 10건
290
+ - **페르소나 없음 (control)**: 14건 (표면적, 분야 분산)
291
+
292
+ → 페르소나 적용 = 도메인 발견율 100%, control = 30%. 토큰 비용 ~3% 추가만.
293
+
294
+ ---
295
+
229
296
  ## 🤝 Claude Code 통합
230
297
 
231
- 설치 시 자동 등록:
232
- - `.claude/commands/{handoff, session-close, audit, lazy-detect, update}.md`
233
- - `.claude/skills/leerness.md` — Claude Code 스킬 정의
234
- - `.claude/settings.local.json` — SessionStart hook (`update --check`)
235
- - `.cursor/rules/leerness.mdc` — Cursor
236
- - `.github/copilot-instructions.md` — Copilot
298
+ 설치 시 자동 등록: `.claude/commands/{handoff, session-close, audit, lazy-detect, update}.md` · `.claude/skills/leerness.md` (스킬 정의) · `.claude/settings.local.json` (SessionStart hook `update --check`) · `.cursor/rules/leerness.mdc` (Cursor) · `.github/copilot-instructions.md` (Copilot)
237
299
 
238
300
  ---
239
301
 
@@ -281,16 +343,16 @@ leerness skill consolidate
281
343
  ## ❓ FAQ
282
344
 
283
345
  **Q. leerness가 내 코드를 변경하나요?**
284
- A. 사용자 메모리(plan/progress/decisions/architecture/reuse-map 등)는 **항상 보존**. 관리 인스트럭션(AGENTS.md/CLAUDE.md 등)은 머지 + 이전 내용을 preserved 블록으로 보존. 모든 변경 전 `.harness/archive/`에 자동 백업.
346
+ A. 사용자 메모리(plan/progress/decisions/architecture/reuse-map 등)는 **항상 보존**. 관리 인스트럭션은 머지 + preserved 블록 보존. 모든 변경 전 `.harness/archive/`에 자동 백업.
285
347
 
286
348
  **Q. 로컬 LLM을 사용하지 않고 싶어요.**
287
- A. 기본 동작입니다. `LEERNESS_OLLAMA_BASE_URL` 환경변수를 설정하지 않으면 LLM 호출 절대 발생 안 함. `orchestrate` 명령만 거부됩니다 (다른 명령은 LLM 없이 동작).
349
+ A. 기본 동작입니다. `LEERNESS_OLLAMA_BASE_URL` 미설정 LLM 호출 절대 발생 안 함. `orchestrate` 명령만 거부 (다른 명령은 LLM 없이 동작).
288
350
 
289
351
  **Q. CI에서 사용 가능?**
290
- A. 네. `--json` 옵션 (retro/insights/brainstorm/handoff/reuse-map/verify-claim) + `exit code`로 통합. `verify-claim T-XXX --run-tests --json | jq '.verdict.runTests'`.
352
+ A. 네. `--json` (retro/insights/brainstorm/handoff/reuse-map/verify-claim) + exit code 통합. `verify-claim T-XXX --run-tests --json | jq '.verdict.runTests'`.
291
353
 
292
354
  **Q. 다른 워크스페이스 모드 명령은?**
293
- A. `--all-apps`는 현재 디렉토리 + `_apps/*` (또는 부모의 `_apps/*`)의 모든 leerness 프로젝트를 발견. `--include p1,p2`로 명시도 가능.
355
+ A. `--all-apps`는 현재 디렉토리 + `_apps/*` (또는 부모의 `_apps/*`)의 모든 leerness 프로젝트 발견. `--include p1,p2`로 명시도 가능.
294
356
 
295
357
  ---
296
358
 
@@ -300,12 +362,19 @@ A. `--all-apps`는 현재 디렉토리 + `_apps/*` (또는 부모의 `_apps/*`)
300
362
  npm test # = node ./scripts/e2e.js
301
363
  ```
302
364
 
303
- **131/131 시나리오** 통과 (1.9.7~1.9.22 회귀 + 신규 검증).
365
+ **139/139 시나리오** 통과 (1.9.7~1.9.29 회귀 + 신규 검증).
304
366
 
305
367
  ---
306
368
 
307
369
  ## 📜 변경 이력 (최근)
308
370
 
371
+ - **1.9.29** — 페르소나 시스템 (5종 내장) + `leerness review --persona` (도메인 깊이 3-4배)
372
+ - **1.9.28** — 카카오페이/네이버페이 패턴 + confidence floor 0.15
373
+ - **1.9.27** — `optimism-check` 강화: 10 카테고리 + URL/메서드 매핑 + 신뢰도 점수
374
+ - **1.9.26** — `optimism-check <T-ID>`, `verify-claim --strict-claims` (낙관적 표시 방지)
375
+ - **1.9.25** — `--include-code` (소스 코드 인덱싱), `register-pending` (다중 세션)
376
+ - **1.9.24** — `deps <capability>` (depends-on 역추적 + 자동 회귀)
377
+ - **1.9.23** — `preferGlobal`, README Install 가이드
309
378
  - **1.9.22** — Ollama opt-in 통합 (`orchestrate --agents N`), `handoff --compact`, `llm-bench record`
310
379
  - **1.9.21** — `.cfg`/`.ini`/`.env`/`.toml`/`.lock` 메타 파일 verify-claim regex 확장
311
380
  - **1.9.20** — Godot/jest/mocha 지원, `verify-code --bench`, file regex 도메인 폴더 자동 인식
package/bin/harness.js CHANGED
@@ -6,7 +6,7 @@ const path = require('path');
6
6
  const cp = require('child_process');
7
7
  const readline = require('readline');
8
8
 
9
- const VERSION = '1.9.26';
9
+ const VERSION = '1.9.29';
10
10
  const MARK = '<!-- leerness:managed -->';
11
11
  const README_START = '<!-- leerness:project-readme:start -->';
12
12
  const README_END = '<!-- leerness:project-readme:end -->';
@@ -2035,8 +2035,9 @@ function depsImpactCmd(root, targetCapability) {
2035
2035
  // evidence에 "API 호출" / "HTTP 200|201" / "POST /" / "응답 확인" → 코드에 fetch/http.request/axios 흔적 없으면 의심
2036
2036
  // evidence에 "DB 저장" / "insert N건" / "DB에" → db.*/pg.*/mysql.*/mongoose.*/prisma.* 없으면 의심
2037
2037
  // evidence에 "이메일 발송" / "메일 전송" → sendMail/nodemailer/smtp 없으면 의심
2038
+ // 1.9.27: 패턴 카탈로그 확장 (5 → 10) + URL/메서드 단위 매핑 추가
2038
2039
  const OPTIMISM_PATTERNS = [
2039
- { kind: 'API', evidenceRe: /(API\s*호출|HTTP\s*\d{3}|POST\s*\/|GET\s*\/|PUT\s*\/|DELETE\s*\/|fetch|REST 응답|응답 확인|endpoint)/i,
2040
+ { kind: 'API', evidenceRe: /(API\s*호출|HTTP\s*\d{3}|POST\s*\/|GET\s*\/|PUT\s*\/|DELETE\s*\/|fetch|REST 응답|응답 확인|endpoint|엔드포인트)/i,
2040
2041
  codeRe: /\b(fetch\s*\(|http\.request|https\.request|axios\.|got\.|undici|node-fetch)/i,
2041
2042
  label: 'API/HTTP 호출' },
2042
2043
  { kind: 'DB', evidenceRe: /(DB에?\s*저장|insert\s+\d+|데이터베이스|SQL\s*(INSERT|UPDATE|DELETE)|migration|마이그레이션 적용)/i,
@@ -2045,14 +2046,50 @@ const OPTIMISM_PATTERNS = [
2045
2046
  { kind: 'Email', evidenceRe: /(이메일[^.\n]{0,30}(발송|전송|보냈|보냄|완료)|메일[^.\n]{0,30}(발송|전송|보냈|보냄)|sendMail|smtp\s*(전송|발송))/i,
2046
2047
  codeRe: /\b(sendMail|nodemailer|smtp|@sendgrid|mailgun|aws-sdk\/ses|resend\.)/i,
2047
2048
  label: '이메일 전송' },
2048
- { kind: 'Webhook', evidenceRe: /(웹훅\s*(호출|전송)|webhook\s+(sent|posted|triggered))/i,
2049
+ { kind: 'Webhook', evidenceRe: /(웹훅\s*(호출|전송|발송)|webhook\s+(sent|posted|triggered))/i,
2049
2050
  codeRe: /\b(fetch\s*\(|http\.request|axios\.)/i,
2050
2051
  label: '웹훅' },
2051
- { kind: 'Payment', evidenceRe: /(결제\s*(완료|성공)|payment\s+(processed|charged)|stripe|toss)/i,
2052
- codeRe: /\b(stripe|toss|@stripe|tosspayments|iamport)/i,
2053
- label: '결제' }
2052
+ { kind: 'Payment', evidenceRe: /(결제\s*(완료|성공|승인|취소)|payment\s+(processed|charged)|stripe 결제|toss\s*결제|카카오페이|네이버페이|kakaopay|nicepay|iamport 결제|페이팔|paypal)/i,
2053
+ codeRe: /\b(stripe|toss|@stripe|tosspayments|iamport|kakao|nicepay|naverpay|paypal-rest-sdk|@paypal)/i,
2054
+ label: '결제' },
2055
+ // 1.9.27 신규 카테고리
2056
+ { kind: 'FileIO', evidenceRe: /(파일[^.\n]{0,20}(생성|저장|작성|기록)|\d+개[^.\n]{0,20}파일|디스크[^.\n]{0,20}저장|로그 파일 작성)/i,
2057
+ codeRe: /\b(fs\.write|fs\.appendFile|writeFileSync|appendFileSync|fs\/promises|fs\.createWriteStream)/i,
2058
+ label: '파일 I/O 쓰기' },
2059
+ { kind: 'Queue', evidenceRe: /(메시지\s*큐|발행\s*완료|publish\s*(완료|성공)|RabbitMQ|Kafka|SQS|Redis Pub|이벤트 발행)/i,
2060
+ codeRe: /\b(amqp|kafkajs|rabbit|redis\.(publish|xadd)|@aws-sdk\/client-sqs|bull|bullmq)/i,
2061
+ label: '메시지 큐 발행' },
2062
+ { kind: 'Cache', evidenceRe: /(Redis[^.\n]{0,20}(저장|set|get)|캐시[^.\n]{0,20}(저장|기록|적중)|memcache)/i,
2063
+ codeRe: /\b(redis\.|ioredis|memcached|node-cache|@upstash\/redis|connect-redis)/i,
2064
+ label: '캐시 저장' },
2065
+ { kind: 'Notify', evidenceRe: /(슬랙\s*(알림|발송|전송)|Slack\s+(notification|sent|posted)|Discord\s+(알림|발송|webhook)|푸시 알림 전송)/i,
2066
+ codeRe: /\b(@slack\/web-api|slack-webhook|discord\.js|discord-webhook|@discordjs|firebase\/messaging|expo-notifications)/i,
2067
+ label: '슬랙/Discord 알림' },
2068
+ { kind: 'Storage', evidenceRe: /(S3\s*(업로드|저장)|GCS\s*업로드|Azure Blob|클라우드 스토리지 업로드|object storage 저장)/i,
2069
+ codeRe: /\b(@aws-sdk\/client-s3|aws-sdk[^a-z]|@google-cloud\/storage|@azure\/storage-blob|aws-s3)/i,
2070
+ label: '클라우드 스토리지' }
2054
2071
  ];
2055
2072
 
2073
+ // 1.9.27: URL/메서드 단위 매핑 — evidence에서 "POST /users" 같은 구체 경로를 추출하고 코드에 같은 경로 존재 확인
2074
+ function _extractUrlClaims(evidence) {
2075
+ const claims = [];
2076
+ // "POST /users" / "GET /api/v1/items" 등
2077
+ const re = /\b(GET|POST|PUT|DELETE|PATCH)\s+(\/[\w\-\/]*)/gi;
2078
+ let m;
2079
+ while ((m = re.exec(evidence)) !== null) {
2080
+ claims.push({ method: m[1].toUpperCase(), path: m[2] });
2081
+ }
2082
+ return claims;
2083
+ }
2084
+ function _verifyUrlClaim(claim, codeText) {
2085
+ // claim.path 가 코드에 등장해야 함 (fetch('https://.../users') 또는 라우트 정의 'POST /users')
2086
+ if (!claim.path || claim.path.length < 2) return true;
2087
+ // path를 그대로 검색 (URL 또는 라우트 정의)
2088
+ const escaped = claim.path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2089
+ const re = new RegExp(escaped, 'i');
2090
+ return re.test(codeText);
2091
+ }
2092
+
2056
2093
  function _scanCodeForPatterns(root) {
2057
2094
  // src/, bin/, lib/, scripts/ 의 .js/.ts/.gd/.py 파일 본문 통합
2058
2095
  let combined = '';
@@ -2079,12 +2116,44 @@ function _detectOptimism(evidence, codeText) {
2079
2116
  const suspects = [];
2080
2117
  for (const p of OPTIMISM_PATTERNS) {
2081
2118
  if (p.evidenceRe.test(evidence) && !p.codeRe.test(codeText)) {
2082
- suspects.push({ kind: p.kind, label: p.label });
2119
+ suspects.push({ kind: p.kind, label: p.label, severity: 'high' });
2120
+ }
2121
+ }
2122
+ // 1.9.27: URL/메서드 단위 매핑 — API 패턴에선 통과해도 구체 경로가 코드에 없으면 추가 의심
2123
+ const urlClaims = _extractUrlClaims(evidence);
2124
+ for (const claim of urlClaims) {
2125
+ if (!_verifyUrlClaim(claim, codeText)) {
2126
+ suspects.push({
2127
+ kind: 'URL',
2128
+ label: `구체 경로 "${claim.method} ${claim.path}" 코드에 미발견`,
2129
+ severity: 'medium',
2130
+ claim
2131
+ });
2083
2132
  }
2084
2133
  }
2085
2134
  return suspects;
2086
2135
  }
2087
2136
 
2137
+ // 1.9.27: 신뢰도 점수 (0=완전 의심, 1=신뢰)
2138
+ // 1.9.28: high suspect 단일 케이스 floor 0.15 — 단일 의심도 정량 차등 가능하게
2139
+ function _computeConfidence(evidence, codeText) {
2140
+ const suspects = _detectOptimism(evidence, codeText);
2141
+ const high = suspects.filter(s => s.severity === 'high').length;
2142
+ const medium = suspects.filter(s => s.severity === 'medium').length;
2143
+ // 가중치: high 1.0 / medium 0.5
2144
+ const totalPenalty = high * 1.0 + medium * 0.5;
2145
+ // 패턴 검사로 발견된 evidence 주장이 많을수록 신뢰도 산정 base 변경
2146
+ const evidenceClaims = OPTIMISM_PATTERNS.filter(p => p.evidenceRe.test(evidence)).length + _extractUrlClaims(evidence).length;
2147
+ if (evidenceClaims === 0) return 1.0; // 외부 작용 주장 자체가 없으면 신뢰 1.0
2148
+ let confidence = Math.max(0, 1 - totalPenalty / evidenceClaims);
2149
+ // 1.9.28: single high suspect에서 confidence 0.0이 일률적 → severity 기반 floor 적용
2150
+ if (suspects.length > 0 && high > 0 && confidence < 0.15) {
2151
+ // 의심 발견은 명확하지만 0보다는 명시적 신호로
2152
+ confidence = 0.15;
2153
+ }
2154
+ return Math.round(confidence * 100) / 100;
2155
+ }
2156
+
2088
2157
  function optimismCheckCmd(root, taskId) {
2089
2158
  root = absRoot(root || process.cwd());
2090
2159
  if (!taskId) return fail('optimism-check <T-ID> 필요. 예: leerness optimism-check T-0001');
@@ -2094,11 +2163,13 @@ function optimismCheckCmd(root, taskId) {
2094
2163
 
2095
2164
  const codeText = _scanCodeForPatterns(root);
2096
2165
  const suspects = _detectOptimism(row.evidence || '', codeText);
2166
+ const confidence = _computeConfidence(row.evidence || '', codeText);
2097
2167
 
2098
2168
  if (has('--json')) {
2099
2169
  log(JSON.stringify({
2100
2170
  project: path.basename(root), taskId, row,
2101
- suspects, ok: suspects.length === 0,
2171
+ suspects, confidence,
2172
+ ok: suspects.length === 0,
2102
2173
  codeFilesScanned: codeText.length > 0
2103
2174
  }, null, 2));
2104
2175
  if (suspects.length > 0) return process.exit(1);
@@ -2107,13 +2178,17 @@ function optimismCheckCmd(root, taskId) {
2107
2178
 
2108
2179
  log(`# leerness optimism-check ${taskId} (${path.basename(root)})`);
2109
2180
  log(`Evidence: ${(row.evidence || '').slice(0, 200)}${(row.evidence || '').length > 200 ? '…' : ''}`);
2181
+ log(`신뢰도 (1.9.27): ${confidence.toFixed(2)} / 1.00${confidence < 0.5 ? ' ⚠ 낮음' : confidence < 0.9 ? ' ⓘ 보통' : ' ✓ 높음'}`);
2110
2182
  log('');
2111
2183
  if (!suspects.length) {
2112
2184
  log(` ✓ 낙관적 표시 의심 없음 — evidence의 주장이 실제 코드 호출 흔적과 일관`);
2113
2185
  return;
2114
2186
  }
2115
2187
  log(` ⚠ 낙관적 표시 의심 ${suspects.length}건 — evidence에 주장 있는데 코드에 호출 흔적 없음`);
2116
- for (const s of suspects) log(` · [${s.kind}] ${s.label} 주장 ↔ 코드 호출 미발견`);
2188
+ for (const s of suspects) {
2189
+ const sev = s.severity === 'high' ? '⚠ HIGH' : 'ⓘ MED';
2190
+ log(` · [${s.kind}] ${sev} ${s.label}`);
2191
+ }
2117
2192
  log('');
2118
2193
  log(`💡 가능한 해석:`);
2119
2194
  log(` 1) evidence 작성자가 실제 동작 없이 낙관적으로 표시 (검증 필요)`);
@@ -2124,6 +2199,175 @@ function optimismCheckCmd(root, taskId) {
2124
2199
  return process.exit(1);
2125
2200
  }
2126
2201
 
2202
+ // 1.9.29: 페르소나 시스템 + review 명령
2203
+ // 페르소나 부여 sub-agent가 도메인 깊이 3-4배 (1.9.28 라운드 실측). 자동 프롬프트 생성.
2204
+ const BUILT_IN_PERSONAS = {
2205
+ security: {
2206
+ id: 'security',
2207
+ name: '보안 엔지니어 (10년차)',
2208
+ description: 'OWASP Top 10, CWE, RFC, 한국 개인정보보호법/게임산업법 정통',
2209
+ body: `너는 **10년 경력의 시니어 보안 엔지니어**다. OWASP Top 10 2021, CWE, RFC 7235/6454, CORS 보안, secret 관리에 정통하며, 한국 금융사·카카오·네이버 등 대형 IT 기업의 보안 감사 경험이 있다. 코드를 볼 때 **위협 모델링**과 **공격 표면(attack surface)** 을 자동으로 시각화한다.
2210
+
2211
+ 검토 영역: 입력 검증 / 인증·인가 / CORS / 시크릿/로그 노출 / DoS / 데이터 노출 / 의존성 attack surface / 한국 시장 특화 (개인정보보호법, 결제 정보)
2212
+ 보고에 포함: 위협 모델 / CWE ID 매핑 / 실 공격 시나리오 1건 (HTTP 페이로드) / P0/P1/P2 우선순위 / OWASP Top 10 2021 매핑`
2213
+ },
2214
+ performance: {
2215
+ id: 'performance',
2216
+ name: '성능 최적화 전문가 (V8 내부)',
2217
+ description: 'V8 엔진 (Ignition/TurboFan, hidden class), Node.js 이벤트 루프, libuv 정통',
2218
+ body: `너는 **V8 엔진 내부 (Ignition, TurboFan, hidden class)와 Node.js 이벤트 루프, libuv에 정통한 성능 최적화 전문가**다. Linux perf, node --prof, clinic.js, autocannon, FlameGraph 활용 경험이 풍부하다. 메모리 압박(GC pressure), CPU bound vs I/O bound 구분, hot path 식별이 직관이다.
2219
+
2220
+ 검토 영역: Hot path 식별 / hidden class 안정성 / 메모리 할당 패턴 / 정규식 컴파일 / JSON.parse/stringify 비용 / 이벤트 루프 블로킹 / 라우트 매칭 복잡도
2221
+ 보고에 포함: 성능 프로필 요약 (RPS/latency 추정) / Hot path Top 5 / 비효율 표 (영향 high/med/low) / 벤치 시나리오 (autocannon 명령) / 권장 우선순위 (당장/부하증가/마이크로)`
2222
+ },
2223
+ ux: {
2224
+ id: 'ux',
2225
+ name: '한국어 UX 라이터 + DX 컨설턴트',
2226
+ description: '카카오/네이버/토스/라인 마이크로카피, API 디자인 (Stripe/GitHub/Google) 정통',
2227
+ body: `너는 **한국 사용자 대상 게임/SaaS 제품의 UX 라이터 + DX(Developer Experience) 컨설턴트**다. 카카오, 네이버, 토스, 라인의 한국어 마이크로카피 가이드라인을 숙지하고 있으며, 클라이언트 개발자의 API 통합 경험을 잘 안다. 에러 메시지, HTTP status, 응답 본문 일관성이 직관이다.
2228
+
2229
+ 검토 영역: 한국어 에러 메시지 톤 / HTTP status 적절성 (400/404/422/409) / 응답 본문 일관성 / 한국어/영문 혼재 / 누락 정보 (rate limit, request id, version) / 클라이언트 SDK 친화성
2230
+ 보고에 포함: UX/DX 점수 (1-10) / 발견 이슈 표 / Before/After 메시지 5건 / SDK 친화성 점수 (1-5) / 권장 로드맵 (이번 PR / 1주 / 분기)`
2231
+ },
2232
+ testing: {
2233
+ id: 'testing',
2234
+ name: '테스트 엔지니어 (TDD + property-based)',
2235
+ description: 'TDD, property-based testing (fast-check), AAA 패턴, fuzz, mutation testing 정통',
2236
+ body: `너는 **TDD와 property-based testing (fast-check) 에 정통한 테스트 엔지니어**다. AAA 패턴, given/when/then, fuzz testing, mutation testing, contract testing 경험이 있다. 테스트 커버리지보다 **테스트 품질**과 **회귀 방어** 가치를 더 중시한다.
2237
+
2238
+ 검토 영역: 테스트 누락 분기 / edge case / mocking 과다 / AAA 패턴 위반 / async 테스트 결함 (race) / property 후보 / 회귀 가능성
2239
+ 보고에 포함: 누락 테스트 목록 + 우선순위 / fast-check property 후보 3건 / 기존 테스트 약점 / 권장 회귀 시나리오`
2240
+ },
2241
+ docs: {
2242
+ id: 'docs',
2243
+ name: '기술 문서 작성자 (한국어)',
2244
+ description: 'README, API 문서, 사용 가이드 작성. Stripe Docs / Google Cloud / 카카오 dev 가이드 정통',
2245
+ body: `너는 **한국어 기술 문서 작성에 정통한 테크니컬 라이터**다. Stripe Docs, Google Cloud, AWS, 카카오 개발자 가이드 톤을 잘 안다. README 첫 60초 경험, 점진적 공개 (progressive disclosure), 코드 예시의 즉시 실행 가능성을 중시한다.
2246
+
2247
+ 검토 영역: 60초 시작 가능성 / 예시 코드 정확성 / 누락된 사전 요구사항 / 한국어 자연스러움 / 시각적 균형 (이모지/표/코드블록) / 한국어/영문 혼재 / 다음 단계 명시
2248
+ 보고에 포함: 사용자 페르소나별 평가 (입문자/실무자/전문가) / 60초 안 첫 결과 가능 여부 / 누락 정보 / 권장 개선 표`
2249
+ }
2250
+ };
2251
+
2252
+ function _resolvePersona(root, id) {
2253
+ // 1) 내장
2254
+ if (BUILT_IN_PERSONAS[id]) return BUILT_IN_PERSONAS[id];
2255
+ // 2) .harness/personas/<id>.md (사용자 정의)
2256
+ const customPath = path.join(root, '.harness', 'personas', `${id}.md`);
2257
+ if (exists(customPath)) {
2258
+ const txt = read(customPath);
2259
+ const nameMatch = txt.match(/^#\s+(.+)$/m);
2260
+ return { id, name: nameMatch?.[1] || id, description: '(사용자 정의)', body: txt };
2261
+ }
2262
+ return null;
2263
+ }
2264
+
2265
+ function personaCmd(root, sub, idOrName, ...rest) {
2266
+ root = absRoot(root || process.cwd());
2267
+ if (!sub || sub === 'list') {
2268
+ const customDir = path.join(root, '.harness', 'personas');
2269
+ const custom = exists(customDir) ? fs.readdirSync(customDir).filter(f => f.endsWith('.md')).map(f => f.replace(/\.md$/, '')) : [];
2270
+ if (has('--json')) {
2271
+ log(JSON.stringify({
2272
+ builtin: Object.values(BUILT_IN_PERSONAS).map(p => ({ id: p.id, name: p.name, description: p.description })),
2273
+ custom
2274
+ }, null, 2));
2275
+ return;
2276
+ }
2277
+ log(`# 페르소나 카탈로그 (1.9.29)`);
2278
+ log(`\n## 내장 (${Object.keys(BUILT_IN_PERSONAS).length})`);
2279
+ for (const p of Object.values(BUILT_IN_PERSONAS)) log(` - ${p.id}: ${p.name} — ${p.description}`);
2280
+ if (custom.length) {
2281
+ log(`\n## 사용자 정의 (${custom.length}, .harness/personas/)`);
2282
+ for (const c of custom) log(` - ${c}`);
2283
+ }
2284
+ log(`\n💡 활용: \`leerness review <file> --persona ${Object.keys(BUILT_IN_PERSONAS)[0]}\``);
2285
+ return;
2286
+ }
2287
+ if (sub === 'show') {
2288
+ if (!idOrName) { fail('persona show <id> 필요'); return process.exit(1); }
2289
+ const p = _resolvePersona(root, idOrName);
2290
+ if (!p) { fail(`페르소나 없음: ${idOrName}`); return process.exit(1); }
2291
+ log(`# ${p.name} (${p.id})`);
2292
+ log(`\n${p.description}\n`);
2293
+ log(`---\n${p.body}`);
2294
+ return;
2295
+ }
2296
+ if (sub === 'add') {
2297
+ if (!idOrName) { fail('persona add <id> 필요'); return process.exit(1); }
2298
+ const customDir = path.join(root, '.harness', 'personas');
2299
+ if (!exists(customDir)) fs.mkdirSync(customDir, { recursive: true });
2300
+ const fp = path.join(customDir, `${idOrName}.md`);
2301
+ if (exists(fp)) { fail(`이미 존재: ${fp}`); return process.exit(1); }
2302
+ const templatePersona = `# ${idOrName}\n\n간략 설명: (한 줄 작성)\n\n---\n\n너는 ...에 정통한 ...전문가다. ...\n\n검토 영역: ...\n보고에 포함: ...`;
2303
+ writeUtf8(fp, templatePersona);
2304
+ ok(`페르소나 템플릿 생성: ${fp}`);
2305
+ log(` 편집 후 \`leerness review <file> --persona ${idOrName}\`로 사용`);
2306
+ return;
2307
+ }
2308
+ fail('사용법: leerness persona list|show <id>|add <id>');
2309
+ return process.exit(1);
2310
+ }
2311
+
2312
+ function reviewCmd(root, target) {
2313
+ root = absRoot(root || process.cwd());
2314
+ if (!target) { fail('review <file> 필요. 예: leerness review src/api.js --persona security'); return process.exit(1); }
2315
+ const personaIds = (arg('--persona', null) || '').split(',').map(s => s.trim()).filter(Boolean);
2316
+ if (!personaIds.length) { fail('--persona <id> 필요. \`leerness persona list\`로 확인'); return process.exit(1); }
2317
+
2318
+ // 파일 확인
2319
+ const filePath = path.isAbsolute(target) ? target : path.join(root, target);
2320
+ if (!exists(filePath)) { fail(`파일 없음: ${filePath}`); return process.exit(1); }
2321
+ const fileContent = read(filePath);
2322
+ const fileSize = Buffer.byteLength(fileContent, 'utf8');
2323
+ if (fileSize > 100 * 1024) { fail(`파일 너무 큼: ${fileSize} bytes. 100KB 미만 권장.`); return process.exit(1); }
2324
+
2325
+ // 페르소나 해석
2326
+ const personas = [];
2327
+ for (const id of personaIds) {
2328
+ const p = _resolvePersona(root, id);
2329
+ if (!p) { fail(`페르소나 없음: ${id}. \`leerness persona list\` 확인`); return process.exit(1); }
2330
+ personas.push(p);
2331
+ }
2332
+
2333
+ // 출력 형식: emit
2334
+ const emit = arg('--emit', 'prompt'); // prompt | md | json
2335
+
2336
+ if (emit === 'json') {
2337
+ log(JSON.stringify({
2338
+ file: target,
2339
+ filePath, fileSize,
2340
+ personas: personas.map(p => ({ id: p.id, name: p.name }))
2341
+ }, null, 2));
2342
+ return;
2343
+ }
2344
+
2345
+ // 각 페르소나마다 별도 프롬프트 생성
2346
+ for (const p of personas) {
2347
+ if (personas.length > 1) log(`\n${'='.repeat(70)}`);
2348
+ log(`# Review Prompt — ${p.name} (${p.id})`);
2349
+ log(`## 대상: ${target} (${fileSize} bytes)`);
2350
+ log(`## 페르소나 활성화`);
2351
+ log(p.body);
2352
+ log(`\n## 작업`);
2353
+ log(`아래 코드를 위 페르소나 관점에서 정밀 리뷰하라. 한국어 보고 ~600단어.`);
2354
+ log(`\n## 코드`);
2355
+ log('```javascript');
2356
+ log(fileContent);
2357
+ log('```');
2358
+ }
2359
+
2360
+ if (emit === 'md') {
2361
+ // 파일로도 저장
2362
+ const outDir = path.join(root, '.harness', 'reviews');
2363
+ if (!exists(outDir)) fs.mkdirSync(outDir, { recursive: true });
2364
+ const tag = personas.map(p => p.id).join('-');
2365
+ const outFile = path.join(outDir, `${path.basename(target).replace(/\./g, '_')}-${tag}-${today()}.md`);
2366
+ // 이미 stdout 출력했으니 그걸 파일로도 — 간단히 생략 (사용자가 redirect 가능)
2367
+ log(`\n💡 \`leerness review <file> --persona X > out.md\` 로 저장 가능`);
2368
+ }
2369
+ }
2370
+
2127
2371
  // 1.9.25: register-pending — sub-agent/외부 모델이 작업 시작 즉시 progress-tracker에 in-progress 등록
2128
2372
  // 사용 예: leerness register-pending "<요청 내용>" --agent gemini
2129
2373
  // → 다음 T-ID 자동 할당, status=in-progress, evidence="(pending) by <agent>"
@@ -4208,7 +4452,7 @@ function viewworkInstall(root) {
4208
4452
  }
4209
4453
 
4210
4454
  function help() {
4211
- log(`Leerness v${VERSION}\n\nUsage:\n leerness init [path] [--language auto|ko|en] [--skills recommended|all|a,b]\n leerness migrate [path] [--dry-run] [--force]\n leerness update [path] [--check|--yes|--force|--from <tarball>]\n leerness auto-update install [path]\n leerness status [path]\n leerness verify [path]\n leerness debug [path]\n leerness audit [path]\n leerness check [path]\n leerness scan secrets [path]\n leerness encoding check [path]\n leerness lazy detect [path]\n leerness memory search "query" [--limit 5]\n leerness handoff [path] [--all-apps] [--include p1,p2] [--since 24h|3d] [--compact] [--json] # 1.9.17-22 워크스페이스 (--compact: LLM 시스템 프롬프트용 1줄 요약)\n leerness orchestrate "<목표>" [--agents N] [--model qwen2.5:7b-instruct] [--retry-on-fail K] # 1.9.22 Ollama opt-in (LEERNESS_OLLAMA_BASE_URL 필요)\n leerness llm-bench record --score N --model X [--label L] [--tokens T] # 1.9.22 LLM 벤치 히스토리 누적\n leerness deps <capability> [--run-tests] [--json] # 1.9.24 depends-on 역방향 추적 + 자동 회귀 sweep\n leerness memory search "키" [--include-code] # 1.9.25 소스 코드 본문도 검색 (모순 감지 핵심)\n leerness brainstorm "주제" [--include-code] # 1.9.25 코드 본문 hits 포함\n leerness register-pending "<요청>" [--agent X] [--note Y] # 1.9.25 다중 세션 in-progress 즉시 등록\n leerness optimism-check <T-ID> [--json] # 1.9.26 낙관적 표시 감지 (evidence vs 코드 호출 흔적)\n leerness verify-claim <T-ID> ... [--strict-claims] # 1.9.26 verify-claim에 낙관적 표시 자동 검사 통합\n leerness reuse-map [path] [--all-apps] [--include p1,p2] [--strict-elements] [--json] # 1.9.18 중복/잠재중복/depends-on\n leerness verify-claim <T-ID> [--path .] [--run-tests] [--json] # 1.9.18-20 evidence 자동 검증 (1.9.20: scenes/scripts 등 도메인 폴더 + jest/mocha 파싱)\n leerness verify-code [path] [--build] [--bench] # 1.9.20 --bench: scripts.bench 추가 실행 + evidence 누적\n leerness session close [path]\n leerness viewwork install [path]\n leerness viewwork emit [path] [--action a] [--note n] [--agent x] [--tool t]\n leerness route <task-type>\n leerness self check [path]\n leerness readme sync [path]\n leerness consistency check [path]\n leerness consistency merge-design-guide [path]\n leerness plan show|init|add|drop|progress|sync [args]\n leerness task list|add|update|drop|fix-evidence|relink [args]\n leerness skill list|info <name>\n leerness skill learn <id> --doc <url> --command "..." --capability "..." [--note ...]\n leerness skill use <id> [--note ...]\n leerness skill optimize <id> --before "..." --after "..." [--note ...]\n leerness skill remove <id>\n leerness skill consolidate [--threshold 0.3]\n leerness gate [path] # verify+audit+scan+encoding+lazy
4455
+ log(`Leerness v${VERSION}\n\nUsage:\n leerness init [path] [--language auto|ko|en] [--skills recommended|all|a,b]\n leerness migrate [path] [--dry-run] [--force]\n leerness update [path] [--check|--yes|--force|--from <tarball>]\n leerness auto-update install [path]\n leerness status [path]\n leerness verify [path]\n leerness debug [path]\n leerness audit [path]\n leerness check [path]\n leerness scan secrets [path]\n leerness encoding check [path]\n leerness lazy detect [path]\n leerness memory search "query" [--limit 5]\n leerness handoff [path] [--all-apps] [--include p1,p2] [--since 24h|3d] [--compact] [--json] # 1.9.17-22 워크스페이스 (--compact: LLM 시스템 프롬프트용 1줄 요약)\n leerness orchestrate "<목표>" [--agents N] [--model qwen2.5:7b-instruct] [--retry-on-fail K] # 1.9.22 Ollama opt-in (LEERNESS_OLLAMA_BASE_URL 필요)\n leerness llm-bench record --score N --model X [--label L] [--tokens T] # 1.9.22 LLM 벤치 히스토리 누적\n leerness deps <capability> [--run-tests] [--json] # 1.9.24 depends-on 역방향 추적 + 자동 회귀 sweep\n leerness memory search "키" [--include-code] # 1.9.25 소스 코드 본문도 검색 (모순 감지 핵심)\n leerness brainstorm "주제" [--include-code] # 1.9.25 코드 본문 hits 포함\n leerness register-pending "<요청>" [--agent X] [--note Y] # 1.9.25 다중 세션 in-progress 즉시 등록\n leerness optimism-check <T-ID> [--json] # 1.9.26/27 낙관적 표시 감지 (1.9.27: 10 카테고리 + URL/메서드 매핑 + 신뢰도 점수)\n leerness persona list|show <id>|add <id> # 1.9.29 페르소나 카탈로그 (보안/성능/UX/testing/docs 5종 내장)\n leerness review <file> --persona <id1,id2,...> # 1.9.29 도메인 페르소나 리뷰 프롬프트 자동 생성\n leerness verify-claim <T-ID> ... [--strict-claims] # 1.9.26 verify-claim에 낙관적 표시 자동 검사 통합\n leerness reuse-map [path] [--all-apps] [--include p1,p2] [--strict-elements] [--json] # 1.9.18 중복/잠재중복/depends-on\n leerness verify-claim <T-ID> [--path .] [--run-tests] [--json] # 1.9.18-20 evidence 자동 검증 (1.9.20: scenes/scripts 등 도메인 폴더 + jest/mocha 파싱)\n leerness verify-code [path] [--build] [--bench] # 1.9.20 --bench: scripts.bench 추가 실행 + evidence 누적\n leerness session close [path]\n leerness viewwork install [path]\n leerness viewwork emit [path] [--action a] [--note n] [--agent x] [--tool t]\n leerness route <task-type>\n leerness self check [path]\n leerness readme sync [path]\n leerness consistency check [path]\n leerness consistency merge-design-guide [path]\n leerness plan show|init|add|drop|progress|sync [args]\n leerness task list|add|update|drop|fix-evidence|relink [args]\n leerness skill list|info <name>\n leerness skill learn <id> --doc <url> --command "..." --capability "..." [--note ...]\n leerness skill use <id> [--note ...]\n leerness skill optimize <id> --before "..." --after "..." [--note ...]\n leerness skill remove <id>\n leerness skill consolidate [--threshold 0.3]\n leerness gate [path] # verify+audit+scan+encoding+lazy
4212
4456
  leerness retro [path] [--days 7] [--all-apps] [--include p1,p2] [--json] # 회고 (1.9.13~1.9.16)
4213
4457
  leerness insights [path] [--all-apps] [--include p1,p2] [--json] # 누적 통계 (1.9.13~1.9.16)
4214
4458
  leerness brainstorm "<주제>" [--all-apps] [--include p1,p2] [--json] # 브레인스토밍 (1.9.13~1.9.16)
@@ -4249,6 +4493,8 @@ async function main() {
4249
4493
  if (cmd === 'deps') return depsImpactCmd(arg('--path', process.cwd()), args[1]);
4250
4494
  if (cmd === 'register-pending') return registerPendingCmd(arg('--path', process.cwd()), args.slice(1).filter(x => !x.startsWith('-')));
4251
4495
  if (cmd === 'optimism-check') return optimismCheckCmd(arg('--path', process.cwd()), args[1]);
4496
+ if (cmd === 'persona') return personaCmd(arg('--path', process.cwd()), args[1], args[2]);
4497
+ if (cmd === 'review') return reviewCmd(arg('--path', process.cwd()), args[1]);
4252
4498
  if (cmd === 'session' && args[1] === 'close') { const r = sessionClose(args[2] || process.cwd()); viewworkEmit(args[2] || process.cwd(), { action: 'task', tool: 'session-close', note: 'session close' }); return r; }
4253
4499
  if (cmd === 'viewwork' && args[1] === 'install') return viewworkInstall(args[2] || process.cwd());
4254
4500
  if (cmd === 'viewwork' && args[1] === 'emit') return viewworkEmit(args[2] || process.cwd(), { action: arg('--action','task'), note: arg('--note',''), agent: arg('--agent','leerness'), tool: arg('--tool','leerness-cli') });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.26",
3
+ "version": "1.9.29",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -656,6 +656,144 @@ total++;
656
656
  if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
657
657
  }
658
658
 
659
+ // 1.9.27 회귀: URL/메서드 매핑 + 신뢰도 점수 + 신규 카테고리 (FileIO/Queue/Cache/Notify/Storage)
660
+ total++;
661
+ {
662
+ // T-9001 false negative 해결 검증: 코드에 다른 목적의 http.request 있어도 URL 미매치로 잡아냄
663
+ const tmpU = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-url-'));
664
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpU, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
665
+ fs.mkdirSync(path.join(tmpU, 'src'), { recursive: true });
666
+ // 다른 목적의 http.request (Ollama 호출 패턴)
667
+ fs.writeFileSync(path.join(tmpU, 'src/ollama.js'), "http.request({host:'localhost',path:'/api/tags',method:'GET'})");
668
+ // evidence: 전혀 다른 경로 주장
669
+ fs.appendFileSync(path.join(tmpU, '.harness/progress-tracker.md'),
670
+ '| T-9001 | done | API 사용자 등록 | POST /users API 호출 완료, HTTP 201 응답 확인 | next | 2026-05-15 |\n');
671
+ const r = cp.spawnSync(process.execPath, [CLI, 'optimism-check', 'T-9001', '--path', tmpU], { encoding: 'utf8', timeout: 10000 });
672
+ const ok = r.status !== 0
673
+ && /\[URL\]/.test(r.stdout)
674
+ && /POST \/users/.test(r.stdout)
675
+ && /신뢰도 \(1\.9\.27\):/.test(r.stdout);
676
+ console.log(ok ? '✓ B(1.9.27) URL 매핑: 1.9.26 false negative 해결 (POST /users 미발견)' : '✗ URL 매핑 실패');
677
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
678
+ }
679
+
680
+ total++;
681
+ {
682
+ // 신규 카테고리 (Slack/Notify)
683
+ const tmpN = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-notify-'));
684
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpN, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
685
+ fs.mkdirSync(path.join(tmpN, 'src'), { recursive: true });
686
+ fs.writeFileSync(path.join(tmpN, 'src/x.js'), 'module.exports={};\n');
687
+ fs.appendFileSync(path.join(tmpN, '.harness/progress-tracker.md'),
688
+ '| T-9100 | done | Slack 알림 | 슬랙 알림 발송 완료, #general 채널에 통보 | next | 2026-05-15 |\n');
689
+ const r = cp.spawnSync(process.execPath, [CLI, 'optimism-check', 'T-9100', '--path', tmpN], { encoding: 'utf8', timeout: 10000 });
690
+ const ok = r.status !== 0 && /\[Notify\]/.test(r.stdout) && /슬랙\/Discord 알림/.test(r.stdout);
691
+ console.log(ok ? '✓ B(1.9.27) 신규 카테고리 Notify: 슬랙 알림 거짓 감지' : '✗ Notify 카테고리 실패');
692
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
693
+ }
694
+
695
+ total++;
696
+ {
697
+ // 신뢰도 점수 — 정상 케이스는 1.0
698
+ const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-conf-'));
699
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
700
+ fs.mkdirSync(path.join(tmpC, 'src'), { recursive: true });
701
+ fs.writeFileSync(path.join(tmpC, 'src/x.js'), 'module.exports={};\n');
702
+ fs.appendFileSync(path.join(tmpC, '.harness/progress-tracker.md'),
703
+ '| T-9200 | done | pure compute | src/x.js 모듈 추가 | next | 2026-05-15 |\n');
704
+ const r = cp.spawnSync(process.execPath, [CLI, 'optimism-check', 'T-9200', '--path', tmpC, '--json'], { encoding: 'utf8', timeout: 10000 });
705
+ let parsed = null;
706
+ try { parsed = JSON.parse(r.stdout); } catch {}
707
+ const ok = r.status === 0 && parsed && parsed.confidence === 1.0 && parsed.suspects.length === 0;
708
+ console.log(ok ? '✓ B(1.9.27) 신뢰도 점수: 정상 evidence → 1.00' : '✗ 신뢰도 점수 실패');
709
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
710
+ }
711
+
712
+ // 1.9.28 회귀: 한국형 PG (카카오페이) 패턴 + confidence floor 0.15
713
+ total++;
714
+ {
715
+ const tmpK = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-kpay-'));
716
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpK, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
717
+ fs.mkdirSync(path.join(tmpK, 'src'), { recursive: true });
718
+ fs.writeFileSync(path.join(tmpK, 'src/x.js'), 'module.exports={};\n');
719
+ fs.appendFileSync(path.join(tmpK, '.harness/progress-tracker.md'),
720
+ '| T-9100 | done | 결제 | 카카오페이 결제 승인 완료 | next | 2026-05-15 |\n');
721
+ const r = cp.spawnSync(process.execPath, [CLI, 'optimism-check', 'T-9100', '--path', tmpK, '--json'], { encoding: 'utf8', timeout: 10000 });
722
+ let parsed = null;
723
+ try { parsed = JSON.parse(r.stdout); } catch {}
724
+ const okPay = r.status !== 0 && parsed && parsed.suspects.some(s => s.kind === 'Payment');
725
+ const okFloor = parsed && parsed.confidence === 0.15;
726
+ const ok = okPay && okFloor;
727
+ console.log(ok ? '✓ B(1.9.28) 카카오페이 결제 + confidence floor 0.15' : `✗ 1.9.28 실패 (pay=${okPay} floor=${okFloor})`);
728
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
729
+ }
730
+
731
+ // 1.9.29 회귀: 페르소나 시스템 + review 명령
732
+ total++;
733
+ {
734
+ // persona list — 5 내장
735
+ const r1 = cp.spawnSync(process.execPath, [CLI, 'persona', 'list'], { encoding: 'utf8', timeout: 10000 });
736
+ const okList = r1.status === 0
737
+ && /security: 보안 엔지니어/.test(r1.stdout)
738
+ && /performance: 성능 최적화/.test(r1.stdout)
739
+ && /ux: 한국어 UX/.test(r1.stdout)
740
+ && /testing:/.test(r1.stdout)
741
+ && /docs:/.test(r1.stdout);
742
+ // persona show
743
+ const r2 = cp.spawnSync(process.execPath, [CLI, 'persona', 'show', 'security'], { encoding: 'utf8', timeout: 10000 });
744
+ const okShow = r2.status === 0 && /10년 경력/.test(r2.stdout) && /OWASP Top 10/.test(r2.stdout);
745
+ // 알 수 없는 페르소나
746
+ const r3 = cp.spawnSync(process.execPath, [CLI, 'persona', 'show', 'unknown999'], { encoding: 'utf8', timeout: 10000 });
747
+ const okMissing = r3.status !== 0 && /페르소나 없음/.test(r3.stdout + r3.stderr);
748
+ const ok = okList && okShow && okMissing;
749
+ console.log(ok ? '✓ B(1.9.29) persona list/show/없는 ID 거부' : `✗ persona 실패 (list=${okList} show=${okShow} miss=${okMissing})`);
750
+ if (!ok) { failed++; console.log(r1.stdout.slice(0, 400)); }
751
+ }
752
+
753
+ total++;
754
+ {
755
+ // review <file> --persona X
756
+ const tmpR = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-rev-'));
757
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpR, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
758
+ fs.mkdirSync(path.join(tmpR, 'src'), { recursive: true });
759
+ fs.writeFileSync(path.join(tmpR, 'src/sample.js'), "function add(a, b) { return a + b; } module.exports = { add };\n");
760
+ // 단일 페르소나
761
+ const r = cp.spawnSync(process.execPath, [CLI, 'review', 'src/sample.js', '--persona', 'security', '--path', tmpR], { encoding: 'utf8', timeout: 10000 });
762
+ const okSingle = r.status === 0 && /Review Prompt/.test(r.stdout) && /보안 엔지니어/.test(r.stdout) && /add\(a, b\)/.test(r.stdout);
763
+ // 다중 페르소나
764
+ const r2 = cp.spawnSync(process.execPath, [CLI, 'review', 'src/sample.js', '--persona', 'security,performance,ux', '--path', tmpR], { encoding: 'utf8', timeout: 10000 });
765
+ const okMulti = r2.status === 0
766
+ && /보안 엔지니어/.test(r2.stdout)
767
+ && /성능 최적화/.test(r2.stdout)
768
+ && /UX 라이터/.test(r2.stdout);
769
+ // 잘못된 페르소나
770
+ const r3 = cp.spawnSync(process.execPath, [CLI, 'review', 'src/sample.js', '--persona', 'jedi', '--path', tmpR], { encoding: 'utf8', timeout: 10000 });
771
+ const okBad = r3.status !== 0 && /페르소나 없음/.test(r3.stdout + r3.stderr);
772
+ // --persona 누락
773
+ const r4 = cp.spawnSync(process.execPath, [CLI, 'review', 'src/sample.js', '--path', tmpR], { encoding: 'utf8', timeout: 10000 });
774
+ const okNoPersona = r4.status !== 0 && /--persona.*필요/.test(r4.stdout + r4.stderr);
775
+ const ok = okSingle && okMulti && okBad && okNoPersona;
776
+ console.log(ok ? '✓ B(1.9.29) review --persona: 단일/다중/잘못된/누락 모두 정확' : `✗ review 실패 (single=${okSingle} multi=${okMulti} bad=${okBad} noP=${okNoPersona})`);
777
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
778
+ }
779
+
780
+ total++;
781
+ {
782
+ // 사용자 정의 페르소나 add
783
+ const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-padd-'));
784
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
785
+ const r = cp.spawnSync(process.execPath, [CLI, 'persona', 'add', 'my-domain', '--path', tmpC], { encoding: 'utf8', timeout: 10000 });
786
+ const okAdd = r.status === 0
787
+ && fs.existsSync(path.join(tmpC, '.harness/personas/my-domain.md'))
788
+ && /\.harness\/personas\/my-domain\.md/.test(r.stdout.replace(/\\/g, '/'));
789
+ // list 시 사용자 정의 포함
790
+ const r2 = cp.spawnSync(process.execPath, [CLI, 'persona', 'list', '--path', tmpC], { encoding: 'utf8', timeout: 10000 });
791
+ const okList = /사용자 정의 \(1/.test(r2.stdout) && /my-domain/.test(r2.stdout);
792
+ const ok = okAdd && okList;
793
+ console.log(ok ? '✓ B(1.9.29) persona add: 사용자 정의 템플릿 생성 + list 표시' : `✗ persona add 실패 (add=${okAdd} list=${okList})`);
794
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
795
+ }
796
+
659
797
  // 1.9.22 회귀: handoff --compact + orchestrate opt-in 정책 + llm-bench record
660
798
  total++;
661
799
  {