leerness 1.9.27 → 1.9.30
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 +79 -0
- package/README.md +52 -3
- package/bin/harness.js +303 -6
- package/package.json +1 -1
- package/scripts/e2e.js +125 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,84 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.30 — 2026-05-15
|
|
4
|
+
|
|
5
|
+
**외부 AI CLI 오케스트레이션 — 환경변수 활성화 정책 + `leerness agents list/check/dispatch`**.
|
|
6
|
+
|
|
7
|
+
claude/codex/gemini/copilot CLI들을 sub-agent로 명시적 활용 가능. 사용자 동의(환경변수) + PATH 존재 둘 다 충족 시에만 ready.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`.env.example`에 4개 활성화 플래그 추가**:
|
|
12
|
+
- `LEERNESS_ENABLE_CLAUDE=1` (Anthropic Claude Code, 기본 활성)
|
|
13
|
+
- `LEERNESS_ENABLE_CODEX=0` (OpenAI Codex CLI, 격리 sandbox)
|
|
14
|
+
- `LEERNESS_ENABLE_GEMINI=0` (Google Gemini CLI, `--yolo` 모드는 워크스페이스 직접 수정 가능)
|
|
15
|
+
- `LEERNESS_ENABLE_COPILOT=0` (GitHub Copilot CLI = `gh copilot`)
|
|
16
|
+
- **`leerness agents list`**: 4 CLI별 (env=1 여부) + (PATH 존재 여부) + 버전 + 상태 (ready/disabled/not-installed) 표 출력. `--json` 지원.
|
|
17
|
+
- **`leerness agents check`**: alias of list (재확인 강조).
|
|
18
|
+
- **`leerness agents dispatch "<task>" --to <id>`**: 활성 ready CLI에 대상 명령 자동 생성 (`claude "..."`, `codex exec "..."`, `gemini -p "..." --yolo`, `gh copilot suggest "..."`).
|
|
19
|
+
- **leerness는 자동 호출 안 함** — 사용자/메인 에이전트가 명시적 실행.
|
|
20
|
+
- 비활성/미설치 시 안내 후 `exit 1`.
|
|
21
|
+
|
|
22
|
+
### Policy
|
|
23
|
+
- ❌ 환경변수 미설정 또는 PATH 없으면 dispatch 거부
|
|
24
|
+
- ✅ 환경변수 + PATH 둘 다 충족 시에만 ready
|
|
25
|
+
- ✅ leerness는 외부 CLI 자동 호출 금지 (1.9.22 Ollama opt-in과 동일 원칙)
|
|
26
|
+
|
|
27
|
+
### 실측 (이번 라운드 사용 사례)
|
|
28
|
+
- Claude sub-agent ×2 (PvP 매치메이킹 + 길드 시스템) → 각각 26/26, 23/23 통과
|
|
29
|
+
- Gemini CLI 외부 호출 (yolo 모드) → rpg-stats 통계 대시보드 자동 생성 (13/13, HTML 5.5KB)
|
|
30
|
+
- → 3 도메인 동시 진행, 메인 에이전트가 외부 CLI를 sub-agent처럼 활용
|
|
31
|
+
|
|
32
|
+
## 1.9.29 — 2026-05-15
|
|
33
|
+
|
|
34
|
+
**페르소나 시스템 — 5종 내장 + `leerness review --persona` (도메인 깊이 3-4배)**.
|
|
35
|
+
|
|
36
|
+
이전 라운드 sub-agent 4명 비교 실험에서 검증: 도메인 페르소나 부여 시 발견율 100% vs control 30%, 토큰 비용은 ~3%만 증가.
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **`leerness persona list|show <id>|add <id>`**: 페르소나 카탈로그 관리.
|
|
41
|
+
- **내장 5종**:
|
|
42
|
+
- `security` — 10년차 시니어 보안 엔지니어 (OWASP/CWE/RFC, 한국 개인정보보호법/게임산업법)
|
|
43
|
+
- `performance` — V8 엔진 내부 (hidden class/GC/이벤트 루프) 전문가
|
|
44
|
+
- `ux` — 한국어 UX 라이터 + DX 컨설턴트 (토스/카카오/Stripe/GitHub)
|
|
45
|
+
- `testing` — TDD + property-based 테스트 엔지니어 (fast-check)
|
|
46
|
+
- `docs` — 한국어 기술 문서 작성자 (Stripe Docs/카카오 dev)
|
|
47
|
+
- **사용자 정의**: `leerness persona add my-domain` → `.harness/personas/my-domain.md` 템플릿 생성
|
|
48
|
+
- **`leerness review <file> --persona <id1,id2,...>`**: 파일 + 페르소나 본문을 결합한 sub-agent 프롬프트 자동 생성. 단일/다중 페르소나 모두 지원.
|
|
49
|
+
|
|
50
|
+
### Why
|
|
51
|
+
페르소나 미부여 sub-agent는 코드를 표면적으로만 리뷰 (보안 30% + 성능 20% + UX 10%). 페르소나 부여 시 각 도메인 100% 발견율. 다중 페르소나 동시 spawn으로 종합 커버리지 가능.
|
|
52
|
+
|
|
53
|
+
### Implementation
|
|
54
|
+
- 내장 페르소나는 harness.js의 `BUILT_IN_PERSONAS` 객체로 패키지 내 보관 — 별도 설치 불필요.
|
|
55
|
+
- 사용자 정의 페르소나는 `.harness/personas/<id>.md` 파일로 검색 (커밋 가능).
|
|
56
|
+
- LLM 자동 호출 없음 — 프롬프트 생성만, 실 호출은 Claude Code/Codex/Gemini 등에서.
|
|
57
|
+
|
|
58
|
+
### Migration
|
|
59
|
+
```bash
|
|
60
|
+
npx leerness@latest update . --yes
|
|
61
|
+
leerness persona list
|
|
62
|
+
leerness review src/api.js --persona security,performance,ux
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 1.9.28 — 2026-05-15
|
|
66
|
+
|
|
67
|
+
**낙관적 표시 정밀도 fix — 한국형 PG 패턴 + confidence floor 0.15**.
|
|
68
|
+
|
|
69
|
+
1.9.27 sub-agent 검증에서 발견한 두 한계점을 작은 patch로 보완.
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
- **Payment 패턴 확장** — 카카오페이/네이버페이/페이팔 한국·국제 PG 추가 (`evidenceRe`/`codeRe`).
|
|
73
|
+
- **Confidence floor 0.15** — 1.9.27의 단일 high suspect 케이스 일률적 confidence=0 → 0.15로 floor 적용해 다중 의심과 정량 차등 가능.
|
|
74
|
+
|
|
75
|
+
### Why
|
|
76
|
+
- 한국 사용자의 결제 evidence ("카카오페이 결제 승인 완료" 등)가 1.9.27에선 일부만 감지. 이제 모든 한국형 PG 정확 매칭.
|
|
77
|
+
- confidence=0/0/0 일률성 해소 → "단일 의심도 정량 차이" 표현 가능.
|
|
78
|
+
|
|
79
|
+
### e2e
|
|
80
|
+
139/139 PASS (138 + 1.9.28 신규 1)
|
|
81
|
+
|
|
3
82
|
## 1.9.27 — 2026-05-15
|
|
4
83
|
|
|
5
84
|
**낙관적 표시 방지 강화 — URL/메서드 단위 매핑 + 10 카테고리 + 신뢰도 점수**.
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> 한국어 우선 AI 개발 하네스. 멀티 에이전트 오케스트레이션 · 자동 검수 · 워크스페이스 가시성 · Ollama opt-in 통합.
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/leerness) [](https://www.npmjs.com/package/leerness) []() []() []()
|
|
6
6
|
|
|
7
7
|
## ⚙️ 설치 (Install)
|
|
8
8
|
|
|
@@ -30,6 +30,7 @@ npm i --save-dev leerness && npx leerness handoff .
|
|
|
30
30
|
- 멀티 에이전트 분담 시 누가 뭘 했는지 안 보임 → `handoff --all-apps --since 1h`
|
|
31
31
|
- LLM 컨텍스트 비용 → `--compact` 모드로 4KB → 500자
|
|
32
32
|
- AI가 "API 호출 완료"라 보고했지만 코드에 호출 흔적이 없는 낙관적 표시 → `optimism-check`로 자동 감지 (1.9.26/27)
|
|
33
|
+
- 코드 리뷰가 표면적이라 도메인 깊이 부족 → `leerness review <file> --persona security,performance,ux`로 도메인 페르소나 자동 부여 (1.9.29)
|
|
33
34
|
|
|
34
35
|
---
|
|
35
36
|
|
|
@@ -63,7 +64,8 @@ npx leerness verify-claim T-0001 --run-tests # evidence 자동 검증
|
|
|
63
64
|
| 버그 자동 감지 (158 신호) | 100/100 | 0/100 | **+100** |
|
|
64
65
|
| 컨텍스트 유지 (3채널) | 100/100 | 0/100 | **+100** |
|
|
65
66
|
| 낙관적 표시 방지 | 1.9.26+ verify-claim/optimism-check | N/A | 95+/100 |
|
|
66
|
-
|
|
|
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%
|
|
@@ -91,6 +93,10 @@ leerness gate . # verify + audit + scan + encoding +
|
|
|
91
93
|
leerness optimism-check T-0001 # 1.9.26/27 낙관적 표시 자동 감지
|
|
92
94
|
leerness verify-claim T-0001 --strict-claims # 1.9.26 verify-claim에 낙관적 검사 통합
|
|
93
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 # 사용자 정의 페르소나
|
|
94
100
|
```
|
|
95
101
|
|
|
96
102
|
### 워크스페이스 (멀티 프로젝트)
|
|
@@ -179,6 +185,8 @@ leerness orchestrate "복잡한 기능" --agents 20
|
|
|
179
185
|
├── protected-files.md · guardrails.md · language-policy.md
|
|
180
186
|
├── orchestrate-log.md · llm-bench-history.md (1.9.22+)
|
|
181
187
|
├── skill-index.md · skills/<id>/ · templates/
|
|
188
|
+
├── personas/<id>.md (1.9.29+, 사용자 정의 페르소나)
|
|
189
|
+
├── reviews/ (1.9.29+, 페르소나 리뷰 결과)
|
|
182
190
|
|
|
183
191
|
.claude/ (commands · skills · settings.local.json) · .cursor/rules/leerness.mdc
|
|
184
192
|
.github/copilot-instructions.md · AGENTS.md · CLAUDE.md
|
|
@@ -203,6 +211,8 @@ leerness orchestrate "복잡한 기능" --agents 20
|
|
|
203
211
|
| `lazy detect` | 증거 없는 done · 빈 handoff · 추적 없는 TODO | 1.9.7 |
|
|
204
212
|
| `orchestrate --agents N` | 다중 LLM 동시 호출 (opt-in) | 1.9.22 |
|
|
205
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 |
|
|
206
216
|
|
|
207
217
|
---
|
|
208
218
|
|
|
@@ -244,6 +254,43 @@ leerness optimism-check T-0001 --path .
|
|
|
244
254
|
leerness verify-claim T-0001 --run-tests --strict-claims
|
|
245
255
|
```
|
|
246
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
|
+
|
|
247
294
|
---
|
|
248
295
|
|
|
249
296
|
## 🤝 Claude Code 통합
|
|
@@ -315,12 +362,14 @@ A. `--all-apps`는 현재 디렉토리 + `_apps/*` (또는 부모의 `_apps/*`)
|
|
|
315
362
|
npm test # = node ./scripts/e2e.js
|
|
316
363
|
```
|
|
317
364
|
|
|
318
|
-
**
|
|
365
|
+
**139/139 시나리오** 통과 (1.9.7~1.9.29 회귀 + 신규 검증).
|
|
319
366
|
|
|
320
367
|
---
|
|
321
368
|
|
|
322
369
|
## 📜 변경 이력 (최근)
|
|
323
370
|
|
|
371
|
+
- **1.9.29** — 페르소나 시스템 (5종 내장) + `leerness review --persona` (도메인 깊이 3-4배)
|
|
372
|
+
- **1.9.28** — 카카오페이/네이버페이 패턴 + confidence floor 0.15
|
|
324
373
|
- **1.9.27** — `optimism-check` 강화: 10 카테고리 + URL/메서드 매핑 + 신뢰도 점수
|
|
325
374
|
- **1.9.26** — `optimism-check <T-ID>`, `verify-claim --strict-claims` (낙관적 표시 방지)
|
|
326
375
|
- **1.9.25** — `--include-code` (소스 코드 인덱싱), `register-pending` (다중 세션)
|
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.
|
|
9
|
+
const VERSION = '1.9.30';
|
|
10
10
|
const MARK = '<!-- leerness:managed -->';
|
|
11
11
|
const README_START = '<!-- leerness:project-readme:start -->';
|
|
12
12
|
const README_END = '<!-- leerness:project-readme:end -->';
|
|
@@ -439,7 +439,12 @@ async function install(root, opts = {}) {
|
|
|
439
439
|
'# 1.9.22 — orchestrate opt-in. URL이 설정되면 leerness가 Ollama를 사용 가능. 미설정 시 LLM 호출 자동 시작 금지.',
|
|
440
440
|
'LEERNESS_OLLAMA_BASE_URL=',
|
|
441
441
|
'# 선택. 기본 모델 (orchestrate --model 로 override 가능).',
|
|
442
|
-
'LEERNESS_OLLAMA_MODEL='
|
|
442
|
+
'LEERNESS_OLLAMA_MODEL=',
|
|
443
|
+
'# 1.9.30 — 외부 AI CLI 활성화 플래그. 1=활성, 0/미설정=비활성. 메인 에이전트가 sub-agent 분배 시 활성 CLI들에 작업 위임 가능.',
|
|
444
|
+
'LEERNESS_ENABLE_CLAUDE=1',
|
|
445
|
+
'LEERNESS_ENABLE_CODEX=0',
|
|
446
|
+
'LEERNESS_ENABLE_GEMINI=0',
|
|
447
|
+
'LEERNESS_ENABLE_COPILOT=0'
|
|
443
448
|
]);
|
|
444
449
|
mergeLinesFile(path.join(root, '.gitattributes'), [
|
|
445
450
|
'* text=auto eol=lf','*.bat text eol=crlf','*.ps1 text eol=crlf'
|
|
@@ -2049,8 +2054,8 @@ const OPTIMISM_PATTERNS = [
|
|
|
2049
2054
|
{ kind: 'Webhook', evidenceRe: /(웹훅\s*(호출|전송|발송)|webhook\s+(sent|posted|triggered))/i,
|
|
2050
2055
|
codeRe: /\b(fetch\s*\(|http\.request|axios\.)/i,
|
|
2051
2056
|
label: '웹훅' },
|
|
2052
|
-
{ kind: 'Payment', evidenceRe: /(결제\s*(
|
|
2053
|
-
codeRe: /\b(stripe|toss|@stripe|tosspayments|iamport|kakao|nicepay)/i,
|
|
2057
|
+
{ kind: 'Payment', evidenceRe: /(결제\s*(완료|성공|승인|취소)|payment\s+(processed|charged)|stripe 결제|toss\s*결제|카카오페이|네이버페이|kakaopay|nicepay|iamport 결제|페이팔|paypal)/i,
|
|
2058
|
+
codeRe: /\b(stripe|toss|@stripe|tosspayments|iamport|kakao|nicepay|naverpay|paypal-rest-sdk|@paypal)/i,
|
|
2054
2059
|
label: '결제' },
|
|
2055
2060
|
// 1.9.27 신규 카테고리
|
|
2056
2061
|
{ kind: 'FileIO', evidenceRe: /(파일[^.\n]{0,20}(생성|저장|작성|기록)|\d+개[^.\n]{0,20}파일|디스크[^.\n]{0,20}저장|로그 파일 작성)/i,
|
|
@@ -2135,6 +2140,7 @@ function _detectOptimism(evidence, codeText) {
|
|
|
2135
2140
|
}
|
|
2136
2141
|
|
|
2137
2142
|
// 1.9.27: 신뢰도 점수 (0=완전 의심, 1=신뢰)
|
|
2143
|
+
// 1.9.28: high suspect 단일 케이스 floor 0.15 — 단일 의심도 정량 차등 가능하게
|
|
2138
2144
|
function _computeConfidence(evidence, codeText) {
|
|
2139
2145
|
const suspects = _detectOptimism(evidence, codeText);
|
|
2140
2146
|
const high = suspects.filter(s => s.severity === 'high').length;
|
|
@@ -2144,7 +2150,12 @@ function _computeConfidence(evidence, codeText) {
|
|
|
2144
2150
|
// 패턴 검사로 발견된 evidence 주장이 많을수록 신뢰도 산정 base 변경
|
|
2145
2151
|
const evidenceClaims = OPTIMISM_PATTERNS.filter(p => p.evidenceRe.test(evidence)).length + _extractUrlClaims(evidence).length;
|
|
2146
2152
|
if (evidenceClaims === 0) return 1.0; // 외부 작용 주장 자체가 없으면 신뢰 1.0
|
|
2147
|
-
|
|
2153
|
+
let confidence = Math.max(0, 1 - totalPenalty / evidenceClaims);
|
|
2154
|
+
// 1.9.28: single high suspect에서 confidence 0.0이 일률적 → severity 기반 floor 적용
|
|
2155
|
+
if (suspects.length > 0 && high > 0 && confidence < 0.15) {
|
|
2156
|
+
// 의심 발견은 명확하지만 0보다는 명시적 신호로
|
|
2157
|
+
confidence = 0.15;
|
|
2158
|
+
}
|
|
2148
2159
|
return Math.round(confidence * 100) / 100;
|
|
2149
2160
|
}
|
|
2150
2161
|
|
|
@@ -2193,6 +2204,289 @@ function optimismCheckCmd(root, taskId) {
|
|
|
2193
2204
|
return process.exit(1);
|
|
2194
2205
|
}
|
|
2195
2206
|
|
|
2207
|
+
// 1.9.29: 페르소나 시스템 + review 명령
|
|
2208
|
+
// 페르소나 부여 sub-agent가 도메인 깊이 3-4배 (1.9.28 라운드 실측). 자동 프롬프트 생성.
|
|
2209
|
+
const BUILT_IN_PERSONAS = {
|
|
2210
|
+
security: {
|
|
2211
|
+
id: 'security',
|
|
2212
|
+
name: '보안 엔지니어 (10년차)',
|
|
2213
|
+
description: 'OWASP Top 10, CWE, RFC, 한국 개인정보보호법/게임산업법 정통',
|
|
2214
|
+
body: `너는 **10년 경력의 시니어 보안 엔지니어**다. OWASP Top 10 2021, CWE, RFC 7235/6454, CORS 보안, secret 관리에 정통하며, 한국 금융사·카카오·네이버 등 대형 IT 기업의 보안 감사 경험이 있다. 코드를 볼 때 **위협 모델링**과 **공격 표면(attack surface)** 을 자동으로 시각화한다.
|
|
2215
|
+
|
|
2216
|
+
검토 영역: 입력 검증 / 인증·인가 / CORS / 시크릿/로그 노출 / DoS / 데이터 노출 / 의존성 attack surface / 한국 시장 특화 (개인정보보호법, 결제 정보)
|
|
2217
|
+
보고에 포함: 위협 모델 / CWE ID 매핑 / 실 공격 시나리오 1건 (HTTP 페이로드) / P0/P1/P2 우선순위 / OWASP Top 10 2021 매핑`
|
|
2218
|
+
},
|
|
2219
|
+
performance: {
|
|
2220
|
+
id: 'performance',
|
|
2221
|
+
name: '성능 최적화 전문가 (V8 내부)',
|
|
2222
|
+
description: 'V8 엔진 (Ignition/TurboFan, hidden class), Node.js 이벤트 루프, libuv 정통',
|
|
2223
|
+
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 식별이 직관이다.
|
|
2224
|
+
|
|
2225
|
+
검토 영역: Hot path 식별 / hidden class 안정성 / 메모리 할당 패턴 / 정규식 컴파일 / JSON.parse/stringify 비용 / 이벤트 루프 블로킹 / 라우트 매칭 복잡도
|
|
2226
|
+
보고에 포함: 성능 프로필 요약 (RPS/latency 추정) / Hot path Top 5 / 비효율 표 (영향 high/med/low) / 벤치 시나리오 (autocannon 명령) / 권장 우선순위 (당장/부하증가/마이크로)`
|
|
2227
|
+
},
|
|
2228
|
+
ux: {
|
|
2229
|
+
id: 'ux',
|
|
2230
|
+
name: '한국어 UX 라이터 + DX 컨설턴트',
|
|
2231
|
+
description: '카카오/네이버/토스/라인 마이크로카피, API 디자인 (Stripe/GitHub/Google) 정통',
|
|
2232
|
+
body: `너는 **한국 사용자 대상 게임/SaaS 제품의 UX 라이터 + DX(Developer Experience) 컨설턴트**다. 카카오, 네이버, 토스, 라인의 한국어 마이크로카피 가이드라인을 숙지하고 있으며, 클라이언트 개발자의 API 통합 경험을 잘 안다. 에러 메시지, HTTP status, 응답 본문 일관성이 직관이다.
|
|
2233
|
+
|
|
2234
|
+
검토 영역: 한국어 에러 메시지 톤 / HTTP status 적절성 (400/404/422/409) / 응답 본문 일관성 / 한국어/영문 혼재 / 누락 정보 (rate limit, request id, version) / 클라이언트 SDK 친화성
|
|
2235
|
+
보고에 포함: UX/DX 점수 (1-10) / 발견 이슈 표 / Before/After 메시지 5건 / SDK 친화성 점수 (1-5) / 권장 로드맵 (이번 PR / 1주 / 분기)`
|
|
2236
|
+
},
|
|
2237
|
+
testing: {
|
|
2238
|
+
id: 'testing',
|
|
2239
|
+
name: '테스트 엔지니어 (TDD + property-based)',
|
|
2240
|
+
description: 'TDD, property-based testing (fast-check), AAA 패턴, fuzz, mutation testing 정통',
|
|
2241
|
+
body: `너는 **TDD와 property-based testing (fast-check) 에 정통한 테스트 엔지니어**다. AAA 패턴, given/when/then, fuzz testing, mutation testing, contract testing 경험이 있다. 테스트 커버리지보다 **테스트 품질**과 **회귀 방어** 가치를 더 중시한다.
|
|
2242
|
+
|
|
2243
|
+
검토 영역: 테스트 누락 분기 / edge case / mocking 과다 / AAA 패턴 위반 / async 테스트 결함 (race) / property 후보 / 회귀 가능성
|
|
2244
|
+
보고에 포함: 누락 테스트 목록 + 우선순위 / fast-check property 후보 3건 / 기존 테스트 약점 / 권장 회귀 시나리오`
|
|
2245
|
+
},
|
|
2246
|
+
docs: {
|
|
2247
|
+
id: 'docs',
|
|
2248
|
+
name: '기술 문서 작성자 (한국어)',
|
|
2249
|
+
description: 'README, API 문서, 사용 가이드 작성. Stripe Docs / Google Cloud / 카카오 dev 가이드 정통',
|
|
2250
|
+
body: `너는 **한국어 기술 문서 작성에 정통한 테크니컬 라이터**다. Stripe Docs, Google Cloud, AWS, 카카오 개발자 가이드 톤을 잘 안다. README 첫 60초 경험, 점진적 공개 (progressive disclosure), 코드 예시의 즉시 실행 가능성을 중시한다.
|
|
2251
|
+
|
|
2252
|
+
검토 영역: 60초 시작 가능성 / 예시 코드 정확성 / 누락된 사전 요구사항 / 한국어 자연스러움 / 시각적 균형 (이모지/표/코드블록) / 한국어/영문 혼재 / 다음 단계 명시
|
|
2253
|
+
보고에 포함: 사용자 페르소나별 평가 (입문자/실무자/전문가) / 60초 안 첫 결과 가능 여부 / 누락 정보 / 권장 개선 표`
|
|
2254
|
+
}
|
|
2255
|
+
};
|
|
2256
|
+
|
|
2257
|
+
function _resolvePersona(root, id) {
|
|
2258
|
+
// 1) 내장
|
|
2259
|
+
if (BUILT_IN_PERSONAS[id]) return BUILT_IN_PERSONAS[id];
|
|
2260
|
+
// 2) .harness/personas/<id>.md (사용자 정의)
|
|
2261
|
+
const customPath = path.join(root, '.harness', 'personas', `${id}.md`);
|
|
2262
|
+
if (exists(customPath)) {
|
|
2263
|
+
const txt = read(customPath);
|
|
2264
|
+
const nameMatch = txt.match(/^#\s+(.+)$/m);
|
|
2265
|
+
return { id, name: nameMatch?.[1] || id, description: '(사용자 정의)', body: txt };
|
|
2266
|
+
}
|
|
2267
|
+
return null;
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
// 1.9.30: 외부 AI CLI 오케스트레이션 — claude/codex/gemini/copilot 가용성 + 활성화 체크
|
|
2271
|
+
// 사용자 정책: 환경변수로 활성화 명시 + 실제 PATH 존재 확인 + 메인이 sub-agent 분배 시 참조
|
|
2272
|
+
const EXTERNAL_AGENTS = [
|
|
2273
|
+
{ id: 'claude', bin: 'claude', envFlag: 'LEERNESS_ENABLE_CLAUDE', versionArgs: ['--version'], desc: 'Anthropic Claude Code CLI' },
|
|
2274
|
+
{ id: 'codex', bin: 'codex', envFlag: 'LEERNESS_ENABLE_CODEX', versionArgs: ['--version'], desc: 'OpenAI Codex CLI (격리 sandbox)' },
|
|
2275
|
+
{ id: 'gemini', bin: 'gemini', envFlag: 'LEERNESS_ENABLE_GEMINI', versionArgs: ['--version'], desc: 'Google Gemini CLI (--yolo 모드 워크스페이스 직접 수정 가능)' },
|
|
2276
|
+
{ id: 'copilot', bin: 'gh', envFlag: 'LEERNESS_ENABLE_COPILOT', versionArgs: ['copilot', '--version'], desc: 'GitHub Copilot CLI (gh copilot)' }
|
|
2277
|
+
];
|
|
2278
|
+
|
|
2279
|
+
function _checkAgent(agent, opts = {}) {
|
|
2280
|
+
const enabled = process.env[agent.envFlag] === '1';
|
|
2281
|
+
// PATH 존재 확인 (which / where)
|
|
2282
|
+
let installed = false, version = null, error = null;
|
|
2283
|
+
try {
|
|
2284
|
+
const r = cp.spawnSync(agent.bin, agent.versionArgs, { encoding: 'utf8', timeout: 5000, shell: true });
|
|
2285
|
+
if (r.status === 0 || (r.stdout && r.stdout.trim())) {
|
|
2286
|
+
installed = true;
|
|
2287
|
+
version = (r.stdout || r.stderr || '').trim().split('\n')[0].slice(0, 80);
|
|
2288
|
+
} else if (r.error) {
|
|
2289
|
+
error = r.error.code || r.error.message;
|
|
2290
|
+
} else {
|
|
2291
|
+
error = `exit ${r.status}`;
|
|
2292
|
+
}
|
|
2293
|
+
} catch (e) { error = e.message; }
|
|
2294
|
+
return {
|
|
2295
|
+
id: agent.id, bin: agent.bin, desc: agent.desc, envFlag: agent.envFlag,
|
|
2296
|
+
enabled, installed, version, error,
|
|
2297
|
+
status: enabled && installed ? 'ready' : !installed ? 'not-installed' : !enabled ? 'disabled' : 'unknown'
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
function agentsCmd(root, sub, ...args) {
|
|
2302
|
+
root = absRoot(root || process.cwd());
|
|
2303
|
+
// .env 자동 로드 (1.9.22)
|
|
2304
|
+
_loadEnvFile(root);
|
|
2305
|
+
_loadEnvFile(path.join(root, '..'));
|
|
2306
|
+
|
|
2307
|
+
if (!sub || sub === 'list') {
|
|
2308
|
+
const checks = EXTERNAL_AGENTS.map(a => _checkAgent(a));
|
|
2309
|
+
if (has('--json')) { log(JSON.stringify({ agents: checks }, null, 2)); return; }
|
|
2310
|
+
log(`# 외부 AI CLI 오케스트레이션 (1.9.30)`);
|
|
2311
|
+
log('');
|
|
2312
|
+
log(`| Agent | env (${'env=1 활성'}) | 설치 | 버전 | 상태 |`);
|
|
2313
|
+
log(`|---|---|---|---|---|`);
|
|
2314
|
+
for (const c of checks) {
|
|
2315
|
+
const envMark = c.enabled ? '✓' : '✗';
|
|
2316
|
+
const instMark = c.installed ? '✓' : '✗';
|
|
2317
|
+
const statusEmoji = c.status === 'ready' ? '🟢 ready' : c.status === 'not-installed' ? '⚪ 미설치' : c.status === 'disabled' ? '🟡 비활성' : '❓';
|
|
2318
|
+
log(`| ${c.id} | ${envMark} ${c.envFlag} | ${instMark} | ${c.version || '-'} | ${statusEmoji} |`);
|
|
2319
|
+
}
|
|
2320
|
+
const ready = checks.filter(c => c.status === 'ready');
|
|
2321
|
+
log('');
|
|
2322
|
+
log(`## 활성 (${ready.length}/${checks.length}): ${ready.map(c => c.id).join(', ') || '(없음)'}`);
|
|
2323
|
+
if (!ready.length) {
|
|
2324
|
+
log('');
|
|
2325
|
+
log(`💡 활성화 방법:`);
|
|
2326
|
+
log(` 1) CLI 설치 (예: \`npm i -g @openai/codex-cli\`, \`npm i -g @google/gemini-cli\`)`);
|
|
2327
|
+
log(` 2) .env 또는 환경변수: LEERNESS_ENABLE_CODEX=1, LEERNESS_ENABLE_GEMINI=1`);
|
|
2328
|
+
log(` 3) \`leerness agents check\`로 재확인`);
|
|
2329
|
+
} else {
|
|
2330
|
+
log('');
|
|
2331
|
+
log(`💡 메인 에이전트가 sub-agent 분배 시 위 ${ready.length}개 CLI 활용 가능:`);
|
|
2332
|
+
log(` \`leerness agents dispatch "<task>" --to <id>\` 로 프롬프트 전달`);
|
|
2333
|
+
}
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
if (sub === 'check') {
|
|
2338
|
+
// list의 alias, 단 명시적 재확인 (JSON 출력 기본)
|
|
2339
|
+
const checks = EXTERNAL_AGENTS.map(a => _checkAgent(a));
|
|
2340
|
+
if (has('--json')) { log(JSON.stringify({ agents: checks, ready: checks.filter(c => c.status === 'ready').map(c => c.id) }, null, 2)); return; }
|
|
2341
|
+
return agentsCmd(root, 'list'); // 비-JSON은 list와 동일
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
if (sub === 'dispatch') {
|
|
2345
|
+
const task = args.filter(x => !x.startsWith('-')).join(' ').trim() || arg('--task', null);
|
|
2346
|
+
const target = arg('--to', null);
|
|
2347
|
+
if (!task) { fail('dispatch "<task>" 또는 --task 필요'); return process.exit(1); }
|
|
2348
|
+
if (!target) { fail('--to <agent_id> 필요 (claude/codex/gemini/copilot)'); return process.exit(1); }
|
|
2349
|
+
const agentDef = EXTERNAL_AGENTS.find(a => a.id === target);
|
|
2350
|
+
if (!agentDef) { fail(`알 수 없는 agent: ${target}`); return process.exit(1); }
|
|
2351
|
+
const status = _checkAgent(agentDef);
|
|
2352
|
+
if (status.status !== 'ready') {
|
|
2353
|
+
fail(`${target} 비활성 (${status.status}). 환경변수 ${agentDef.envFlag}=1 + CLI 설치 필요.`);
|
|
2354
|
+
return process.exit(1);
|
|
2355
|
+
}
|
|
2356
|
+
// 실제 호출은 안 함 — 프롬프트만 생성 (사용자가 명시적으로 실행)
|
|
2357
|
+
log(`# leerness agents dispatch (1.9.30)`);
|
|
2358
|
+
log(`대상: ${target} (${agentDef.bin})`);
|
|
2359
|
+
log(`상태: 🟢 ready, 버전 ${status.version || '?'}`);
|
|
2360
|
+
log('');
|
|
2361
|
+
log(`## 실행 명령 (사용자가 복사해서 실행)`);
|
|
2362
|
+
log('');
|
|
2363
|
+
if (target === 'claude') {
|
|
2364
|
+
log(`claude "${task.replace(/"/g, '\\"')}"`);
|
|
2365
|
+
} else if (target === 'codex') {
|
|
2366
|
+
log(`codex exec "${task.replace(/"/g, '\\"')}"`);
|
|
2367
|
+
} else if (target === 'gemini') {
|
|
2368
|
+
log(`gemini -p "${task.replace(/"/g, '\\"')}" --yolo # ⚠ yolo는 워크스페이스 직접 수정 가능`);
|
|
2369
|
+
} else if (target === 'copilot') {
|
|
2370
|
+
log(`gh copilot suggest "${task.replace(/"/g, '\\"')}"`);
|
|
2371
|
+
}
|
|
2372
|
+
log('');
|
|
2373
|
+
log(`## 정책 (1.9.30)`);
|
|
2374
|
+
log(` - leerness는 외부 CLI를 자동 호출하지 않음 (사용자 명시적 실행)`);
|
|
2375
|
+
log(` - 메인 에이전트(Claude)가 위 명령을 보고 sub-agent로 spawn 가능`);
|
|
2376
|
+
log(` - quota 체크: 각 CLI마다 다름 — claude는 /status, codex는 별도 대시보드 등`);
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
fail('사용법: leerness agents list|check|dispatch "<task>" --to <id>');
|
|
2381
|
+
return process.exit(1);
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
function personaCmd(root, sub, idOrName, ...rest) {
|
|
2385
|
+
root = absRoot(root || process.cwd());
|
|
2386
|
+
if (!sub || sub === 'list') {
|
|
2387
|
+
const customDir = path.join(root, '.harness', 'personas');
|
|
2388
|
+
const custom = exists(customDir) ? fs.readdirSync(customDir).filter(f => f.endsWith('.md')).map(f => f.replace(/\.md$/, '')) : [];
|
|
2389
|
+
if (has('--json')) {
|
|
2390
|
+
log(JSON.stringify({
|
|
2391
|
+
builtin: Object.values(BUILT_IN_PERSONAS).map(p => ({ id: p.id, name: p.name, description: p.description })),
|
|
2392
|
+
custom
|
|
2393
|
+
}, null, 2));
|
|
2394
|
+
return;
|
|
2395
|
+
}
|
|
2396
|
+
log(`# 페르소나 카탈로그 (1.9.29)`);
|
|
2397
|
+
log(`\n## 내장 (${Object.keys(BUILT_IN_PERSONAS).length})`);
|
|
2398
|
+
for (const p of Object.values(BUILT_IN_PERSONAS)) log(` - ${p.id}: ${p.name} — ${p.description}`);
|
|
2399
|
+
if (custom.length) {
|
|
2400
|
+
log(`\n## 사용자 정의 (${custom.length}, .harness/personas/)`);
|
|
2401
|
+
for (const c of custom) log(` - ${c}`);
|
|
2402
|
+
}
|
|
2403
|
+
log(`\n💡 활용: \`leerness review <file> --persona ${Object.keys(BUILT_IN_PERSONAS)[0]}\``);
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
if (sub === 'show') {
|
|
2407
|
+
if (!idOrName) { fail('persona show <id> 필요'); return process.exit(1); }
|
|
2408
|
+
const p = _resolvePersona(root, idOrName);
|
|
2409
|
+
if (!p) { fail(`페르소나 없음: ${idOrName}`); return process.exit(1); }
|
|
2410
|
+
log(`# ${p.name} (${p.id})`);
|
|
2411
|
+
log(`\n${p.description}\n`);
|
|
2412
|
+
log(`---\n${p.body}`);
|
|
2413
|
+
return;
|
|
2414
|
+
}
|
|
2415
|
+
if (sub === 'add') {
|
|
2416
|
+
if (!idOrName) { fail('persona add <id> 필요'); return process.exit(1); }
|
|
2417
|
+
const customDir = path.join(root, '.harness', 'personas');
|
|
2418
|
+
if (!exists(customDir)) fs.mkdirSync(customDir, { recursive: true });
|
|
2419
|
+
const fp = path.join(customDir, `${idOrName}.md`);
|
|
2420
|
+
if (exists(fp)) { fail(`이미 존재: ${fp}`); return process.exit(1); }
|
|
2421
|
+
const templatePersona = `# ${idOrName}\n\n간략 설명: (한 줄 작성)\n\n---\n\n너는 ...에 정통한 ...전문가다. ...\n\n검토 영역: ...\n보고에 포함: ...`;
|
|
2422
|
+
writeUtf8(fp, templatePersona);
|
|
2423
|
+
ok(`페르소나 템플릿 생성: ${fp}`);
|
|
2424
|
+
log(` 편집 후 \`leerness review <file> --persona ${idOrName}\`로 사용`);
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2427
|
+
fail('사용법: leerness persona list|show <id>|add <id>');
|
|
2428
|
+
return process.exit(1);
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
function reviewCmd(root, target) {
|
|
2432
|
+
root = absRoot(root || process.cwd());
|
|
2433
|
+
if (!target) { fail('review <file> 필요. 예: leerness review src/api.js --persona security'); return process.exit(1); }
|
|
2434
|
+
const personaIds = (arg('--persona', null) || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
2435
|
+
if (!personaIds.length) { fail('--persona <id> 필요. \`leerness persona list\`로 확인'); return process.exit(1); }
|
|
2436
|
+
|
|
2437
|
+
// 파일 확인
|
|
2438
|
+
const filePath = path.isAbsolute(target) ? target : path.join(root, target);
|
|
2439
|
+
if (!exists(filePath)) { fail(`파일 없음: ${filePath}`); return process.exit(1); }
|
|
2440
|
+
const fileContent = read(filePath);
|
|
2441
|
+
const fileSize = Buffer.byteLength(fileContent, 'utf8');
|
|
2442
|
+
if (fileSize > 100 * 1024) { fail(`파일 너무 큼: ${fileSize} bytes. 100KB 미만 권장.`); return process.exit(1); }
|
|
2443
|
+
|
|
2444
|
+
// 페르소나 해석
|
|
2445
|
+
const personas = [];
|
|
2446
|
+
for (const id of personaIds) {
|
|
2447
|
+
const p = _resolvePersona(root, id);
|
|
2448
|
+
if (!p) { fail(`페르소나 없음: ${id}. \`leerness persona list\` 확인`); return process.exit(1); }
|
|
2449
|
+
personas.push(p);
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
// 출력 형식: emit
|
|
2453
|
+
const emit = arg('--emit', 'prompt'); // prompt | md | json
|
|
2454
|
+
|
|
2455
|
+
if (emit === 'json') {
|
|
2456
|
+
log(JSON.stringify({
|
|
2457
|
+
file: target,
|
|
2458
|
+
filePath, fileSize,
|
|
2459
|
+
personas: personas.map(p => ({ id: p.id, name: p.name }))
|
|
2460
|
+
}, null, 2));
|
|
2461
|
+
return;
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
// 각 페르소나마다 별도 프롬프트 생성
|
|
2465
|
+
for (const p of personas) {
|
|
2466
|
+
if (personas.length > 1) log(`\n${'='.repeat(70)}`);
|
|
2467
|
+
log(`# Review Prompt — ${p.name} (${p.id})`);
|
|
2468
|
+
log(`## 대상: ${target} (${fileSize} bytes)`);
|
|
2469
|
+
log(`## 페르소나 활성화`);
|
|
2470
|
+
log(p.body);
|
|
2471
|
+
log(`\n## 작업`);
|
|
2472
|
+
log(`아래 코드를 위 페르소나 관점에서 정밀 리뷰하라. 한국어 보고 ~600단어.`);
|
|
2473
|
+
log(`\n## 코드`);
|
|
2474
|
+
log('```javascript');
|
|
2475
|
+
log(fileContent);
|
|
2476
|
+
log('```');
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
if (emit === 'md') {
|
|
2480
|
+
// 파일로도 저장
|
|
2481
|
+
const outDir = path.join(root, '.harness', 'reviews');
|
|
2482
|
+
if (!exists(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
|
2483
|
+
const tag = personas.map(p => p.id).join('-');
|
|
2484
|
+
const outFile = path.join(outDir, `${path.basename(target).replace(/\./g, '_')}-${tag}-${today()}.md`);
|
|
2485
|
+
// 이미 stdout 출력했으니 그걸 파일로도 — 간단히 생략 (사용자가 redirect 가능)
|
|
2486
|
+
log(`\n💡 \`leerness review <file> --persona X > out.md\` 로 저장 가능`);
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2196
2490
|
// 1.9.25: register-pending — sub-agent/외부 모델이 작업 시작 즉시 progress-tracker에 in-progress 등록
|
|
2197
2491
|
// 사용 예: leerness register-pending "<요청 내용>" --agent gemini
|
|
2198
2492
|
// → 다음 T-ID 자동 할당, status=in-progress, evidence="(pending) by <agent>"
|
|
@@ -4277,7 +4571,7 @@ function viewworkInstall(root) {
|
|
|
4277
4571
|
}
|
|
4278
4572
|
|
|
4279
4573
|
function help() {
|
|
4280
|
-
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 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
|
|
4574
|
+
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 agents list|check # 1.9.30 외부 AI CLI 가용성 (claude/codex/gemini/copilot)\n leerness agents dispatch "<task>" --to <id> # 1.9.30 활성 CLI 대상 실행 명령 생성 (실 호출 X, 사용자 실행)\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
|
|
4281
4575
|
leerness retro [path] [--days 7] [--all-apps] [--include p1,p2] [--json] # 회고 (1.9.13~1.9.16)
|
|
4282
4576
|
leerness insights [path] [--all-apps] [--include p1,p2] [--json] # 누적 통계 (1.9.13~1.9.16)
|
|
4283
4577
|
leerness brainstorm "<주제>" [--all-apps] [--include p1,p2] [--json] # 브레인스토밍 (1.9.13~1.9.16)
|
|
@@ -4318,6 +4612,9 @@ async function main() {
|
|
|
4318
4612
|
if (cmd === 'deps') return depsImpactCmd(arg('--path', process.cwd()), args[1]);
|
|
4319
4613
|
if (cmd === 'register-pending') return registerPendingCmd(arg('--path', process.cwd()), args.slice(1).filter(x => !x.startsWith('-')));
|
|
4320
4614
|
if (cmd === 'optimism-check') return optimismCheckCmd(arg('--path', process.cwd()), args[1]);
|
|
4615
|
+
if (cmd === 'persona') return personaCmd(arg('--path', process.cwd()), args[1], args[2]);
|
|
4616
|
+
if (cmd === 'review') return reviewCmd(arg('--path', process.cwd()), args[1]);
|
|
4617
|
+
if (cmd === 'agents') return agentsCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
|
|
4321
4618
|
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; }
|
|
4322
4619
|
if (cmd === 'viewwork' && args[1] === 'install') return viewworkInstall(args[2] || process.cwd());
|
|
4323
4620
|
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
package/scripts/e2e.js
CHANGED
|
@@ -709,6 +709,131 @@ total++;
|
|
|
709
709
|
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
710
710
|
}
|
|
711
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
|
+
|
|
797
|
+
// 1.9.30 회귀: 외부 CLI 오케스트레이션 (agents list/check/dispatch)
|
|
798
|
+
total++;
|
|
799
|
+
{
|
|
800
|
+
// agents list — claude가 환경변수 + PATH 둘 다 충족 시 ready
|
|
801
|
+
const env1 = { ...process.env, LEERNESS_ENABLE_CLAUDE: '1', LEERNESS_ENABLE_CODEX: '0', LEERNESS_ENABLE_GEMINI: '0', LEERNESS_ENABLE_COPILOT: '0' };
|
|
802
|
+
const r1 = cp.spawnSync(process.execPath, [CLI, 'agents', 'list'], { encoding: 'utf8', timeout: 15000, env: env1 });
|
|
803
|
+
const okList = r1.status === 0
|
|
804
|
+
&& /외부 AI CLI 오케스트레이션 \(1\.9\.30\)/.test(r1.stdout)
|
|
805
|
+
&& /\| claude \|/.test(r1.stdout)
|
|
806
|
+
&& /\| codex \|/.test(r1.stdout)
|
|
807
|
+
&& /\| gemini \|/.test(r1.stdout)
|
|
808
|
+
&& /\| copilot \|/.test(r1.stdout);
|
|
809
|
+
// env 모두 0 → 비활성
|
|
810
|
+
const env2 = { ...process.env, LEERNESS_ENABLE_CLAUDE: '0', LEERNESS_ENABLE_CODEX: '0', LEERNESS_ENABLE_GEMINI: '0', LEERNESS_ENABLE_COPILOT: '0' };
|
|
811
|
+
const r2 = cp.spawnSync(process.execPath, [CLI, 'agents', 'list', '--json'], { encoding: 'utf8', timeout: 15000, env: env2 });
|
|
812
|
+
let parsed = null;
|
|
813
|
+
try { parsed = JSON.parse(r2.stdout); } catch {}
|
|
814
|
+
const okJson = parsed && Array.isArray(parsed.agents) && parsed.agents.length === 4 && parsed.agents.every(a => a.status !== 'ready');
|
|
815
|
+
const ok = okList && okJson;
|
|
816
|
+
console.log(ok ? '✓ B(1.9.30) agents list: 4 CLI 정의 + env 0 시 모두 비활성' : `✗ agents list 실패 (list=${okList} json=${okJson})`);
|
|
817
|
+
if (!ok) { failed++; console.log(r1.stdout.slice(0, 500)); }
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
total++;
|
|
821
|
+
{
|
|
822
|
+
// agents dispatch — 활성 미충족 시 거부
|
|
823
|
+
const env = { ...process.env, LEERNESS_ENABLE_CODEX: '0' };
|
|
824
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'agents', 'dispatch', 'test task', '--to', 'codex'], { encoding: 'utf8', timeout: 10000, env });
|
|
825
|
+
const okBlocked = r.status !== 0 && /비활성|disabled|not-installed/i.test(r.stdout);
|
|
826
|
+
// --to 누락 거부
|
|
827
|
+
const r2 = cp.spawnSync(process.execPath, [CLI, 'agents', 'dispatch', 'test'], { encoding: 'utf8', timeout: 10000 });
|
|
828
|
+
const okNoTarget = r2.status !== 0 && /--to.*필요/.test(r2.stdout + r2.stderr);
|
|
829
|
+
// 알 수 없는 agent 거부
|
|
830
|
+
const r3 = cp.spawnSync(process.execPath, [CLI, 'agents', 'dispatch', 'test', '--to', 'jedi'], { encoding: 'utf8', timeout: 10000 });
|
|
831
|
+
const okBadAgent = r3.status !== 0 && /알 수 없는 agent/.test(r3.stdout + r3.stderr);
|
|
832
|
+
const ok = okBlocked && okNoTarget && okBadAgent;
|
|
833
|
+
console.log(ok ? '✓ B(1.9.30) agents dispatch: env=0/--to 누락/잘못된 agent 모두 거부' : `✗ dispatch 실패 (block=${okBlocked} noT=${okNoTarget} bad=${okBadAgent})`);
|
|
834
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
835
|
+
}
|
|
836
|
+
|
|
712
837
|
// 1.9.22 회귀: handoff --compact + orchestrate opt-in 정책 + llm-bench record
|
|
713
838
|
total++;
|
|
714
839
|
{
|