leerness 1.14.0 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,90 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.16.0 — 2026-06-09 — 🛡️ [안정화/Stable] 16번째 버그헌트 안정 minor
4
+
5
+ **🛡️ 안정화(Stable) minor.** 16번째 멀티에이전트 버그헌트(8건 발굴·전부 직접 재현)의 코어 수정(1.15.1)을 검증·통합해 npm 공개. R-0011 정책의 7번째 minor. **이 릴리스 영상부터 개선된 "문제→해소" 디자인**(짧은 인트로 + 이전/이제 모션 + 의미 보존 하이라이트)으로 제작됩니다.
6
+
7
+ ### 이번 minor 통합 (1.15.1 — 전부 재현 검증)
8
+ - **🔴 시크릿 스캐너 FN 차단**: 한 파일에 같은 패턴 시크릿이 2개 이상이면 첫 번째만 보고하고 나머지를 놓치던 문제(루프 break) 수정 → 모두 탐지.
9
+ - **🟠 task/rule list 표 정렬**: 제목에 파이프(`|`)가 있으면 마크다운 표 칼럼이 깨지던 표시 버그 → `_cellSafe` 적용.
10
+ - **📖 문서 정합**: `encoding check` 도움말이 실제 미검사 항목(CRLF)을 광고하던 것 정정.
11
+ - (사이트 영상 파이프라인 4버그 — 큐 키/빈 제목/빈 WAV 통과/check-only 부작용 — 도 동시 수정.)
12
+
13
+ ### 검증 (회귀 0)
14
+ - **selftest 214 PASS** · **E2E 365/365 PASS** · npm gate=minor_bump. 시크릿 멀티매치·표 파이프·도움말 정합 행위 재현.
15
+
16
+ ### 안정화 표시 (R-0006)
17
+ CHANGELOG [안정화/Stable] · git tag annotation (Stable) · GitHub release (Stable) · npm dist-tag `stable` 시도.
18
+
19
+ ## 1.15.1 — 2026-06-09 — 16번째 버그헌트: 시크릿 스캐너 FN + 표 파이프 + 문서정합
20
+
21
+ **🔬 멀티에이전트 버그헌트(신규코드 0건·코어 4건·사이트 4건 발굴) → 8건 전부 직접 재현 후 진짜만 수정.** 맹신 X: 각 지적을 temp 에서 직접 재현하고, 의도된 동작은 수정 제외.
22
+
23
+ ### 수정 (코어 3건 — 전부 재현 검증)
24
+ - **🔴 시크릿 스캐너 FN (F1)**: 한 파일에 같은 패턴의 시크릿이 2개 이상(예: `secret:` 와 `api_key:` 둘 다 'Hardcoded password' 패턴)이면 **첫 번째만 잡고 나머지 누락**(루프 `break`). break 제거 + zero-width 가드 → 모두 탐지.
25
+ - **🟠 task/rule list 표 깨짐 (F2)**: 항목 제목에 파이프(`|`)가 있으면 마크다운 표 칼럼이 어긋남(저장은 안전했으나 list **표시**가 raw). 표시도 `_cellSafe` 적용(`\|`).
26
+ - **문서정합 (F3)**: `encoding check` 도움말이 'CRLF 검사'를 광고했으나 실제 미검사(정책은 BOM/NUL/.bat/roundtrip). 도움말을 실제 동작과 일치하게 정정.
27
+
28
+ ### 의도된 동작(수정 안 함 — 맹신 X)
29
+ - `status --json` 가 `healthy:false` 여도 exit 0: **설계상 정보성 명령**(`scope:'install'` + `healthyMeaning` 명시, 게이트는 `verify`/`gate`). 버그 아님.
30
+
31
+ ### 검증 (회귀 0)
32
+ - **selftest 213→214** · **E2E 365/365** · F1(2건 탐지)·F2(파이프 `\|` 이스케이프)·F3(help CRLF 제거) 행위 재현. (사이트 파이프라인 4건은 leerness-site 에서 별도 수정.)
33
+ - patch(1.15.1) — npm 미배포(R-0011, GitHub).
34
+
35
+ ## 1.15.0 — 2026-06-09 — 🛡️ [안정화/Stable] Karpathy 가이드라인 정렬 3부작 안정 minor
36
+
37
+ **🛡️ 안정화(Stable) minor.** Andrej Karpathy 코딩 가이드라인(생각하고 코딩 / 단순성 / 외과적 변경 / 목표 주도) 대비 외부 에이전트 검토에서 도출한 정렬 작업(1.14.1~1.14.3)을 검증·통합해 npm 공개. R-0011 정책의 6번째 minor. 영상은 HyperFrames 파이프라인 제작.
38
+
39
+ ### 이번 minor 통합 (1.14.1~1.14.3) — Karpathy 4원칙 정렬
40
+ - **원칙1·2 (생각하고 코딩 / 단순성)**: `review-request` 가 요청의 **범위 과대 신호**(전체·모두·리팩토링·재구성·rewrite/refactor everything)와 **투기적 신호**(나중에·확장 가능·유연하게·범용화·추상화·future-proof)를 탐지해 "더 작게 쪼갤 수 있나? 요청 범위만" 환기(advisory). `--json simplicitySignals`.
41
+ - **원칙4 (목표 주도 — 검증가능한 완료)**: `plan add --done-when "<조건>"` 로 milestone 에 성공 기준을 1급 필드(`Done-When`)로 저장 · `plan list` 표시 + `--json doneWhen` · 미정 시 환기. (부수: `--done-when` 값이 제목에 흡수되던 잠복 버그 수정.)
42
+ - **원칙2 (자기적용)**: 정적 분석으로 호출 0·동적참조 0 인 **죽은 함수 5개 제거**(무행위변경). 함수 476→471.
43
+
44
+ ### 검증 (회귀 0)
45
+ - **selftest 213 PASS** · **E2E 365/365 PASS** · npm gate=minor_bump. 각 기능 행위 재현(신호 탐지/오탐0, done-when 저장·표시, dead 제거 후 재스캔 0).
46
+
47
+ ### 안정화 표시 (R-0006)
48
+ CHANGELOG [안정화/Stable] · git tag annotation (Stable) · GitHub release (Stable) · npm dist-tag `stable` 시도.
49
+
50
+ ## 1.14.3 — 2026-06-09 — Karpathy 정렬④(완결): 자체 단순화 — 죽은 코드 제거 (원칙2)
51
+
52
+ **🧹 leerness 가 자신의 원칙2(단순성)를 자신에게 적용.** 정적 분석으로 "정의됐으나 호출 0 + 동적(문자열) 참조 0" 인 함수만 골라 제거 — 무행위변경(검증가능) 단순화.
53
+
54
+ ### 변경 (UR-0033)
55
+ - **죽은 함수 5개 제거**: `_isAutoLoopActive` · `_invalidateSkillsCache` · `_currentLang` · `_typewrite` · `_writeDomainCatalog` (전부 호출처 0, 과거 리팩토링/대체로 고립된 잔재). 함수 476→471, bin 19561줄.
56
+ - 제거 후 **재스캔 = 새 고아 0** (각 함수가 쓰던 헬퍼는 다른 곳에서도 사용 중 — 연쇄 dead 없음).
57
+ - 막연한 "큰 파일 줄이기"가 아닌, **검증가능·무행위변경** 범위로 한정(Karpathy 원칙3 외과적 변경 준수).
58
+
59
+ ### 검증 (회귀 0)
60
+ - **selftest 213 PASS** · **E2E 365/365 PASS** (행위 동일 — dead 코드라 출력/동작 불변). 제거 전 각 후보의 전체 참조(bin+lib+scripts+test)=정의 1줄만 직접 확인(맹신 X).
61
+ - patch(1.14.3) — npm 미배포(R-0011, GitHub). **Karpathy 백로그(UR-0031/0032/0033) 완결.**
62
+
63
+ ## 1.14.2 — 2026-06-09 — Karpathy 정렬③: plan --done-when 검증가능 완료조건 (원칙4)
64
+
65
+ **🎯 milestone 에 "성공 기준"을 1급 필드로.** Karpathy 원칙4(목표 주도 실행 — 검증가능한 완료 정의)의 빠진 절반. plan 에 done-criteria 개념이 없어 "언제 끝인지" 가 모호했던 것을 보강.
66
+
67
+ ### 변경 (UR-0032)
68
+ - **plan add `--done-when "<조건>"`**: milestone 에 `Done-When:` 라인으로 검증가능 완료조건 저장. 미지정 시 `(미정)`.
69
+ - **plan list 표시 + `--json doneWhen`**: 각 milestone 의 완료기준 노출. 누락(legacy/미정) 시 `⚠ 미정 — --done-when 권장 (Karpathy 원칙4)` 환기.
70
+ - **🐛 잠복 버그 수정(맹신 X)**: `nonFlagArgs()` 의 value-flag 집합(`withValue`)에 `--done-when` 이 없어, 값이 positional 로 누출돼 **milestone 제목에 흡수**(예: "결제 연동" → "결제 연동 Stripe e2e…")되던 것 차단. 행위 재현으로 발견·수정.
71
+
72
+ ### 검증 (회귀 0)
73
+ - **selftest 212→213**, 행위: `plan add "결제 연동" --done-when "Stripe e2e 통과"` → 제목="결제 연동"(흡수 없음)·Done-When 분리 저장·plan list/json 노출; 미지정 → (미정)+환기.
74
+ - patch(1.14.2) — npm 미배포(R-0011, GitHub). 잔여 Karpathy: UR-0033(자체 단순화).
75
+
76
+ ## 1.14.1 — 2026-06-09 — Karpathy 정렬②: review-request 범위과대/투기적 신호 (원칙1+2)
77
+
78
+ **🤔 사전 검토 게이트에 "생각하고 코딩" + "단순성 우선" 신호 추가.** Karpathy 리뷰가 가장 약한 원칙으로 꼽은 1(트레이드오프 표면화)·2(단순성)를, 가장 많이 쓰는 `review-request`(작업 전 자동 호출)에서 보강.
79
+
80
+ ### 변경 (UR-0031)
81
+ - **review-request 단순성/범위 신호**: 요청 텍스트에서 **범위 과대 동사**(전체·모두·리팩토링·재구성·rewrite/refactor everything 등)와 **투기적 신호**(나중에·확장 가능·유연하게·범용화·추상화·future-proof 등)를 탐지해 `efficiencyHints` + `--json simplicitySignals` 로 표면화. "더 작게 쪼갤 수 있나? 요청 범위만" 환기. advisory(차단 X — 표면화만, Karpathy 원칙1).
82
+ - 신규 명령 0 — 기존 명령 확장(leerness 자신의 원칙2 준수).
83
+
84
+ ### 검증 (회귀 0)
85
+ - **selftest 211→212**, 행위 재현: "전체 코드베이스 리팩토링 + 나중에 유연하게 확장 가능하게 추상화" → broad=[전체,리팩토링] spec=[나중에,유연하게,확장 가능,추상화]; 단순 요청 → 0(오탐 없음).
86
+ - patch(1.14.1) — R-0011 정책상 npm 미배포(GitHub). 잔여 Karpathy 백로그: UR-0032(plan --done-when 성공기준), UR-0033(자체 단순화).
87
+
3
88
  ## 1.14.0 — 2026-06-09 — 🛡️ [안정화/Stable] 블라인드 리뷰 수정 + Karpathy 정렬 안정 minor
4
89
 
5
90
  **🛡️ 안정화(Stable) minor.** 블라인드 3-모델 리뷰(codex/Sonnet/Opus) 수정 + Karpathy 가이드라인 정렬(1.13.1~1.13.2)을 검증·통합해 npm 공개. R-0011 정책의 5번째 minor. (이 릴리스의 소개 영상부터 **HyperFrames 파이프라인**으로 자동 제작됩니다.)
package/README.md CHANGED
@@ -186,7 +186,7 @@ MIT
186
186
  <!-- leerness:project-readme:start -->
187
187
  ## Leerness Project Harness
188
188
 
189
- 이 프로젝트는 Leerness v1.14.0 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
189
+ 이 프로젝트는 Leerness v1.16.0 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
190
190
 
191
191
  ### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
192
192
 
@@ -208,7 +208,7 @@ leerness status . # 설치 상태
208
208
  leerness verify . # 필수 파일 검증
209
209
  leerness audit . # 일관성·계획-진행 정렬 감사
210
210
  leerness scan secrets . # 시크릿 패턴 스캔
211
- leerness encoding check . # UTF-8 / BOM / CRLF 검사
211
+ leerness encoding check . # UTF-8 / BOM / NUL / .bat 인코딩 검사
212
212
  leerness lazy detect . # 게으름 방지 자동 평가
213
213
  leerness memory search "키" # 결정/이력 검색
214
214
  leerness session close . # 세션 종료 + handoff 자동 작성
@@ -240,7 +240,7 @@ leerness memory restore decision <date|title>
240
240
 
241
241
  ### MCP server (외부 AI 통합)
242
242
 
243
- Leerness v1.14.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
243
+ Leerness v1.16.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
244
244
 
245
245
  ```jsonc
246
246
  // 카테고리별
@@ -261,7 +261,7 @@ Leerness v1.14.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code
261
261
  `<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
262
262
  1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) stress-v* 신규 작성 + 누적 회귀 → 4) e2e 219/219 → 5) npm pack + git tag + GitHub release → 6) main 자동 push (1.9.140+) → 7) session close → 8) 다음 라운드 예약.
263
263
 
264
- 현재 누적: **70 라운드 (1.9.40 → 1.14.0)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
264
+ 현재 누적: **70 라운드 (1.9.40 → 1.16.0)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
265
265
 
266
266
  ### 성능 가이드 (1.9.140 측정)
267
267
 
@@ -299,6 +299,6 @@ leerness release pack --close --auto-main-push
299
299
  - `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
300
300
  - `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
301
301
 
302
- Last synced by Leerness v1.14.0: 2026-06-09
302
+ Last synced by Leerness v1.16.0: 2026-06-09
303
303
  <!-- leerness:project-readme:end -->
304
304
 
package/bin/leerness.js CHANGED
@@ -32,7 +32,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
32
32
  // 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
33
33
  const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST, _DEFAULT_PLATFORM_CONSTRAINTS, _DEFAULT_DOMAIN_CATALOG, _TOOL_CATALOG, _LSP_LANG_PATTERNS, OPTIMISM_PATTERNS, BUILT_IN_PERSONAS, STRINGS, BUILTIN_CATALOG, ROADMAP_STATUS_LABEL, ROADMAP_STATUS_COLOR, SECRET_PATTERNS, MERGE_OVERWRITE_FILES, MINIMAL_SKIP_KEYS, REQUIRED_WORKSPACE_FILES, KEYWORD_STOPWORDS, SKILL_CATALOG_PRESETS } = require('../lib/catalogs'); // 1.9.344/368/369 (UR-0025): catalog 분리 · 1.11.4 (UR-0007): _TOOL_CATALOG
34
34
 
35
- const VERSION = '1.14.0';
35
+ const VERSION = '1.16.0';
36
36
 
37
37
  // 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
38
38
  // 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
@@ -190,12 +190,6 @@ function _withLock(targetPath, fn, opts = {}) {
190
190
  }
191
191
  // 1.9.327 (UR-0025): _getLocalTz / _formatLocal → lib/pure-utils.js 로 이동 (순수 TZ/날짜 포맷, require 사용).
192
192
  // 자동 모드 활성 여부 (R-XXXX every-round 룰 존재 시 true)
193
- function _isAutoLoopActive(root) {
194
- try {
195
- const rules = readRules(root);
196
- return rules.some(r => r.status === 'active' && /every-round|every-session/i.test(r.trigger || ''));
197
- } catch { return false; }
198
- }
199
193
  function _getAutoLoopRule(root) {
200
194
  try {
201
195
  return readRules(root).find(r => r.status === 'active' && /every-round/i.test(r.trigger || '')) || null;
@@ -222,7 +216,7 @@ function _resolveRoot(positional) {
222
216
  }
223
217
  function nonFlagArgs() {
224
218
  const out = [];
225
- const withValue = new Set(['--language','--skills','--path','--status','--progress','--goal','--reason','--next','--target','--token-env','--package','--out','--from','--repo','--id','--note','--evidence','--query','--limit','--action','--agent','--tool','--doc','--command','--capability','--before','--after','--display','--threshold','--trigger','--check','--set','--min-score','--include','--days','--gh-pages-src','--roadmap','--since','--agents','--model','--timeout','--retry-on-fail','--label','--score','--tokens','--alternatives','--impact','--tag','--surface','--depends-on','--affects','--co-changes-with','--files','--branch','--remote','--task-add','--next-action','--role','--provider','--env-var','--deploy','--token-lifetime-hours','--port','--secret','--keep','--shell','--ps-version']);
219
+ const withValue = new Set(['--language','--skills','--path','--status','--progress','--goal','--reason','--next','--target','--token-env','--package','--out','--from','--repo','--id','--note','--evidence','--query','--limit','--action','--agent','--tool','--doc','--command','--capability','--before','--after','--display','--threshold','--trigger','--check','--set','--min-score','--include','--days','--gh-pages-src','--roadmap','--since','--agents','--model','--timeout','--retry-on-fail','--label','--score','--tokens','--alternatives','--impact','--tag','--surface','--depends-on','--affects','--co-changes-with','--files','--branch','--remote','--task-add','--next-action','--role','--provider','--env-var','--deploy','--token-lifetime-hours','--port','--secret','--keep','--shell','--ps-version','--done-when']); // 1.14.2 (UR-0032): --done-when 값이 positional 로 누출돼 milestone 제목에 흡수되던 것 차단
226
220
  const a = process.argv.slice(2);
227
221
  for (let i = 0; i < a.length; i++) {
228
222
  const x = a[i];
@@ -303,7 +297,7 @@ function managedReadmeBlock(project) {
303
297
  'leerness verify . # 필수 파일 검증',
304
298
  'leerness audit . # 일관성·계획-진행 정렬 감사',
305
299
  'leerness scan secrets . # 시크릿 패턴 스캔',
306
- 'leerness encoding check . # UTF-8 / BOM / CRLF 검사',
300
+ 'leerness encoding check . # UTF-8 / BOM / NUL / .bat 인코딩 검사',
307
301
  'leerness lazy detect . # 게으름 방지 자동 평가',
308
302
  'leerness memory search "키" # 결정/이력 검색',
309
303
  'leerness session close . # 세션 종료 + handoff 자동 작성',
@@ -630,7 +624,7 @@ leerness memory restore <surface> <target> # archive → active 복귀 (DELETE
630
624
  '.claude/commands/audit.md': `# /audit\n\n계획-진행 정렬, 디자인/재사용 일관성, 시크릿/인코딩을 일괄 점검합니다.\n\n\`\`\`\n!leerness audit .\n!leerness scan secrets .\n!leerness encoding check .\n\`\`\`\n`,
631
625
  '.claude/commands/lazy-detect.md': `# /lazy-detect\n\n게으름 방지 자동 평가를 실행합니다.\n\n\`\`\`\n!leerness lazy detect .\n\`\`\`\n`,
632
626
  '.claude/commands/update.md': `# /update\n\nleerness 자동 업데이트를 실행합니다 (감지 → 마이그레이션 → 검증).\n\n\`\`\`\n!leerness update --yes\n\`\`\`\n`,
633
- '.claude/skills/leerness.md': `---\nname: leerness\ndescription: Leerness harness commands - handoff, audit, scan secrets, encoding check, lazy detect, session close, update. Use when the user asks to load project context, verify work quality, scan secrets, check encoding, or end a session.\n---\n\n# leerness skill\n\n## When to use\n- 사용자가 프로젝트 컨텍스트를 로드해달라고 할 때\n- 완료 선언 전 자기 검증을 요청할 때\n- 세션을 종료하거나 인수인계를 요청할 때\n- 시크릿/한글 인코딩 점검을 요청할 때\n- 새 leerness 버전 적용을 요청할 때\n\n## Commands\n\n\`\`\`bash\nleerness handoff . # 컨텍스트 로드\nleerness check . # pre-action 체크\nleerness audit . # 일관성/계획 정렬 감사\nleerness scan secrets . # 시크릿 패턴 스캔\nleerness encoding check . # UTF-8/BOM/CRLF\nleerness lazy detect . # 게으름 평가\nleerness memory search "key" # 결정/이력 검색\nleerness session close . # 종료 보고 + handoff 자동 생성\nleerness update --yes # 자동 업데이트\n\`\`\`\n`,
627
+ '.claude/skills/leerness.md': `---\nname: leerness\ndescription: Leerness harness commands - handoff, audit, scan secrets, encoding check, lazy detect, session close, update. Use when the user asks to load project context, verify work quality, scan secrets, check encoding, or end a session.\n---\n\n# leerness skill\n\n## When to use\n- 사용자가 프로젝트 컨텍스트를 로드해달라고 할 때\n- 완료 선언 전 자기 검증을 요청할 때\n- 세션을 종료하거나 인수인계를 요청할 때\n- 시크릿/한글 인코딩 점검을 요청할 때\n- 새 leerness 버전 적용을 요청할 때\n\n## Commands\n\n\`\`\`bash\nleerness handoff . # 컨텍스트 로드\nleerness check . # pre-action 체크\nleerness audit . # 일관성/계획 정렬 감사\nleerness scan secrets . # 시크릿 패턴 스캔\nleerness encoding check . # UTF-8/BOM/NUL\nleerness lazy detect . # 게으름 평가\nleerness memory search "key" # 결정/이력 검색\nleerness session close . # 종료 보고 + handoff 자동 생성\nleerness update --yes # 자동 업데이트\n\`\`\`\n`,
634
628
  };
635
629
  // 1.9.276: minimal 모드 — 코어가 요구하지 않는 파일 제외 (verify 필수 파일은 유지).
636
630
  if (opts.minimal) { for (const k of MINIMAL_SKIP_KEYS) delete _files[k]; }
@@ -1257,10 +1251,6 @@ function _buildAllSkills(root) {
1257
1251
  return out;
1258
1252
  }
1259
1253
  // 1.9.66: skill 추가/제거 시 캐시 invalidate (외부 helper)
1260
- function _invalidateSkillsCache(root) {
1261
- try { _SKILLS_LIST_CACHE.delete(absRoot(root)); } catch {}
1262
- }
1263
-
1264
1254
  function skillList(root) {
1265
1255
  const all = listAllSkills(root);
1266
1256
  // 1.9.84: --json 옵션 (MCP 통합용)
@@ -1862,42 +1852,11 @@ async function skillAutoCacheCmd(root, sub) {
1862
1852
  // lang 결정: explicit > .harness/LANGUAGE > LEERNESS_LANG env > 'ko' (default)
1863
1853
  // 1.9.338 (UR-0025 심층): STRINGS (i18n ko/en catalog) 는 lib/catalogs.js 로 이전 (import). _t 는 _translate(STRINGS,..) 박막.
1864
1854
  // 현재 사용 언어 결정 (env > config > 'ko')
1865
- function _currentLang(root) {
1866
- if (process.env.LEERNESS_LANG) return process.env.LEERNESS_LANG === 'en' ? 'en' : 'ko';
1867
- try {
1868
- if (root) {
1869
- const fp = path.join(root, '.harness', 'LANGUAGE');
1870
- if (exists(fp)) {
1871
- const v = read(fp).trim().toLowerCase();
1872
- if (v === 'en' || v === 'english') return 'en';
1873
- if (v === 'ko' || v === 'korean' || v === 'kr') return 'ko';
1874
- }
1875
- }
1876
- } catch {}
1877
- return 'ko'; // default
1878
- }
1879
1855
  // 1.9.338 (UR-0025 심층): 순수 _translate(STRINGS, key, lang) (lib/pure-utils) 박막 — STRINGS catalog 주입.
1880
1856
  function _t(key, lang) {
1881
1857
  return _translate(STRINGS, key, lang);
1882
1858
  }
1883
1859
 
1884
- // 1.9.206: UI/UX 개선 — typewriter / fade-in 효과 (opt-in via LEERNESS_TYPEWRITER=1)
1885
- function _typewrite(text, delayMs) {
1886
- delayMs = delayMs || 15;
1887
- if (process.env.LEERNESS_TYPEWRITER !== '1' || !process.stdout.isTTY) {
1888
- process.stdout.write(text);
1889
- return Promise.resolve();
1890
- }
1891
- return new Promise((resolve) => {
1892
- let i = 0;
1893
- const step = () => {
1894
- if (i >= text.length) return resolve();
1895
- process.stdout.write(text[i++]);
1896
- setTimeout(step, delayMs);
1897
- };
1898
- step();
1899
- });
1900
- }
1901
1860
  // 색상 helper (TTY 시 ANSI, 비-TTY 시 plain)
1902
1861
  const _ui = {
1903
1862
  bold: s => process.stdout.isTTY ? `\x1b[1m${s}\x1b[0m` : s,
@@ -3566,6 +3525,26 @@ function _selfTestCases() {
3566
3525
  const src = read(__filename);
3567
3526
  return src.includes('const changedNotClaimed = gitApplicable') && src.includes('files.some(f => _claimFileInGit(f, new Set([g])))') && src.includes('scopeCreep:') && src.includes('외과적 변경 점검');
3568
3527
  } },
3528
+ { name: 'Karpathy 가이드라인1+2 (UR-0031): review-request 범위과대/투기적 신호 표면화 (1.14.1)', run: () => {
3529
+ const m = require('../lib/review-request');
3530
+ const rr = read(path.join(path.dirname(__filename), '..', 'lib', 'review-request.js'));
3531
+ const wired = rr.includes('const simplicitySignals = { broad: broadHits, speculative: specHits }') && rr.includes('범위 과대 신호') && rr.includes('투기적 신호') && rr.includes('simplicitySignals,');
3532
+ return typeof m.reviewRequestCmd === 'function' && wired;
3533
+ } },
3534
+ { name: 'Karpathy 가이드라인4 (UR-0032): plan --done-when 검증가능 완료조건 저장/파싱/표시 (1.14.2)', run: () => {
3535
+ const src = read(__filename);
3536
+ const wired = src.includes("const doneWhen = _lineSafe(arg('--done-when', '') || '(미정)')") && src.includes('Done-When: ${doneWhen}') && src.includes("const doneWhenMatch = b.match(/^Done-When:") && src.includes('doneWhen: doneWhenMatch ? doneWhenMatch[1].trim() : null')
3537
+ && src.includes("'--ps-version','--done-when'"); // 잠복버그 회귀가드: nonFlagArgs withValue 에 --done-when (제목 흡수 차단)
3538
+ const b = '### M-0001. 로그인\nStatus: planned\nProgress: 0%\nDone-When: 로그인 e2e 테스트 통과\n\nTasks:\n- [ ] x\n';
3539
+ const dw = (b.match(/^Done-When:\s*(.+)$/m) || [])[1];
3540
+ return wired && dw === '로그인 e2e 테스트 통과';
3541
+ } },
3542
+ { name: '16th 버그헌트 F1/F2: scan secrets 패턴당 멀티매치(break 제거) + task/rule list 파이프 셀안전 (1.15.1)', run: () => {
3543
+ const src = read(__filename);
3544
+ const f1 = src.includes('같은 패턴이 한 파일에 여러 번') && src.includes('if (re.lastIndex === m.index) re.lastIndex++;');
3545
+ const f2 = src.includes('_cellSafe(r.request)') && src.includes('_cellSafe(r.rule)');
3546
+ return f1 && f2;
3547
+ } },
3569
3548
  { name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
3570
3549
  ];
3571
3550
  }
@@ -4810,13 +4789,6 @@ function _loadDomainCatalog(root) {
4810
4789
  return merged;
4811
4790
  } catch { return _DEFAULT_DOMAIN_CATALOG; }
4812
4791
  }
4813
- function _writeDomainCatalog(root, catalog) {
4814
- try {
4815
- mkdirp(path.join(root, '.harness'));
4816
- writeUtf8(_domainCatalogPath(root), JSON.stringify({ ...catalog, updatedAt: new Date().toISOString() }, null, 2));
4817
- return true;
4818
- } catch { return false; }
4819
- }
4820
4792
  // 1.9.325 (UR-0025): _classifyIntent → lib/pure-utils.js 로 이동 (순수 intent 분류, require 사용).
4821
4793
  function _detectDomain(text, root) {
4822
4794
  return _matchDomain(_loadDomainCatalog(root), text);
@@ -6467,6 +6439,7 @@ function planListCmd(root, opts = {}) {
6467
6439
  const title = headerMatch[2].trim();
6468
6440
  const statusMatch = b.match(/^Status:\s*(.+)$/m);
6469
6441
  const progressMatch = b.match(/^Progress:\s*(.+)$/m);
6442
+ const doneWhenMatch = b.match(/^Done-When:\s*(.+)$/m); // 1.14.2 (Karpathy 원칙4, UR-0032): 검증가능 완료조건
6470
6443
  // Tasks 블록 (- [ ] 또는 - [x])
6471
6444
  const tasks = [];
6472
6445
  const tasksSection = b.match(/Tasks:\s*\n([\s\S]+?)(?=\n###|\n## |$)/);
@@ -6481,6 +6454,7 @@ function planListCmd(root, opts = {}) {
6481
6454
  title,
6482
6455
  status: statusMatch ? statusMatch[1].trim() : null,
6483
6456
  progress: progressMatch ? progressMatch[1].trim() : null,
6457
+ doneWhen: doneWhenMatch ? doneWhenMatch[1].trim() : null,
6484
6458
  tasks,
6485
6459
  });
6486
6460
  }
@@ -6495,6 +6469,7 @@ function planListCmd(root, opts = {}) {
6495
6469
  log(`\n[${m.id}] ${m.title}`);
6496
6470
  if (m.status) log(` Status: ${m.status}`);
6497
6471
  if (m.progress) log(` Progress: ${m.progress}`);
6472
+ log(` 완료기준(Done-When): ${m.doneWhen || '⚠ 미정 — plan add ... --done-when "<검증가능 조건>" 권장 (Karpathy 원칙4)'}`);
6498
6473
  if (m.tasks.length) log(` Tasks: ${m.tasks.length}개 (${m.tasks.filter(t => t.done).length} 완료)`);
6499
6474
  }
6500
6475
  }
@@ -6503,10 +6478,12 @@ function planAdd(root, text) {
6503
6478
  if (!_requireInit(root, 'plan add')) return; // 1.9.311 (UR-0047): init 가드
6504
6479
  if (!_validateChoice(arg('--status', null), TASK_STATUSES, 'plan status')) { process.exitCode = 1; return; } // 1.9.310 (UR-0046)
6505
6480
  const status = arg('--status','planned'), progress = arg('--progress','0'), nextAction = arg('--next', '다음 액션 작성');
6481
+ // 1.14.2 (Karpathy 원칙4 "성공기준 정의", UR-0032): --done-when 으로 검증가능 완료조건을 milestone 에 기록. 미지정 시 (미정) — plan show/audit 가 환기.
6482
+ const doneWhen = _lineSafe(arg('--done-when', '') || '(미정)');
6506
6483
  // 1.9.303 (UR-0043): M-id append + T-id upsert 를 하나의 락으로 — 동시 plan add ID 충돌 방지.
6507
6484
  const { id, tid } = _withLock(progressPath(root), () => {
6508
6485
  const id = nextId(root, 'M');
6509
- append(planPath(root), `\n### ${id}. ${text}\nStatus: ${status}\nProgress: ${progress}%\n\nTasks:\n- [ ] ${text}\n`);
6486
+ append(planPath(root), `\n### ${id}. ${text}\nStatus: ${status}\nProgress: ${progress}%\nDone-When: ${doneWhen}\n\nTasks:\n- [ ] ${text}\n`);
6510
6487
  const tid = nextId(root, 'T');
6511
6488
  upsertProgress(root, { id: tid, status, request: text, evidence: `plan:${id}`, nextAction });
6512
6489
  return { id, tid };
@@ -6618,7 +6595,7 @@ function taskList(root) {
6618
6595
  if (!filtered.length) return log('(no tasks)');
6619
6596
  log('| ID | Status | Request | Evidence | Next Action | Updated |');
6620
6597
  log('|---|---|---|---|---|---|');
6621
- for (const r of filtered) log(`| ${r.id} | ${r.status} | ${r.request} | ${r.evidence} | ${r.nextAction} | ${r.updated} |`);
6598
+ for (const r of filtered) log(`| ${r.id} | ${r.status} | ${_cellSafe(r.request)} | ${_cellSafe(r.evidence)} | ${_cellSafe(r.nextAction)} | ${r.updated} |`); // 16th 버그헌트 F2: 표시도 _cellSafe — 파이프(|)가 칼럼 깨던 것 차단(저장은 안전했으나 list 표시가 raw)
6622
6599
  }
6623
6600
  // 1.9.310 (UR-0046, 설치리뷰 3중수렴): CLI/MCP 입력 스키마 검증 — 무효 status/trigger 거부(--force 우회).
6624
6601
  // 이전: task --status nonsense / rule --trigger 오타가 그대로 등록돼 상태/정책 신뢰성 훼손.
@@ -7423,7 +7400,8 @@ function _collectSecretFindings(root) {
7423
7400
  if (valueGroup != null && requireSecretLike && !_looksSecretLike(val)) { if (re.lastIndex === m.index) re.lastIndex++; continue; }
7424
7401
  const line = text.slice(0, m.index).split('\n').length;
7425
7402
  findings.push({ file: fileRel, line, name, snippet: m[0].slice(0, 32), gitignored });
7426
- break;
7403
+ // 16th 버그헌트 F1: break 제거 — 같은 패턴이 한 파일에 여러 번(예: secret: + api_key: 둘 다 'Hardcoded password') 나오면 모두 보고(보안 FN 차단). zero-width 매치는 lastIndex 전진으로 무한루프 방지.
7404
+ if (re.lastIndex === m.index) re.lastIndex++;
7427
7405
  }
7428
7406
  }
7429
7407
  }
@@ -12966,7 +12944,7 @@ function ruleList(root) {
12966
12944
  if (!rules.length) return ok('등록된 룰 없음');
12967
12945
  log('| ID | Trigger | Rule | Status | Last Verified |');
12968
12946
  log('|---|---|---|---|---|');
12969
- for (const r of rules) log(`| ${r.id} | ${r.trigger} | ${r.rule} | ${r.status} | ${r.lastVerified} |`);
12947
+ for (const r of rules) log(`| ${r.id} | ${_cellSafe(r.trigger)} | ${_cellSafe(r.rule)} | ${r.status} | ${r.lastVerified} |`); // 16th 버그헌트 F2: 파이프 칼럼 깨짐 차단(표시 _cellSafe)
12970
12948
  }
12971
12949
 
12972
12950
  function ruleRemove(root, id) {
@@ -1,289 +1,297 @@
1
- // lib/review-request.js — review-request 핸들러 (UR-0125 큰 핸들러 모듈화, 1.9.420)
2
- // bin/leerness.js 에서 reviewRequestCmd(277줄) 분리. DI: harness 고유 의존(has · harnessPath · _checkRequestConstraints · _recordRun) 주입.
3
- // io 프리미티브는 ./io, cp/path 는 빌트인. 동작/출력 무변경(thin wrapper 위임).
4
- 'use strict';
5
- const cp = require('child_process');
6
- const path = require('path');
7
- const { absRoot, exists, read, log, fail, failJson } = require('./io');
8
-
9
- function reviewRequestCmd(root, request, deps = {}) {
10
- const { has, harnessPath, _checkRequestConstraints, _recordRun } = deps;
11
- root = absRoot(root || process.cwd());
12
- if (!request || !String(request).trim()) {
13
- // 1.9.428 (10th 외부평가 UR-0128): --json 오류 경로도 순수 JSON (failJson 이 모드 분기)
14
- return failJson(!!(has && has('--json')), 'review_request_empty', 'leerness review-request "<request>" — 사용자 요청 텍스트 필요');
15
- }
16
- const t0 = Date.now();
17
- const text = String(request).trim();
18
-
19
- // 1) 작업 유형 추정 (route 기반 키워드 매핑)
20
- const lower = text.toLowerCase();
21
- const routeKw = {
22
- bugfix: ['버그', '오류', '에러', '수정', '고쳐', '실패', 'fix', 'bug', 'error'],
23
- refactor: ['리팩토', '재구성', '정리', '개선', 'refactor', 'cleanup'],
24
- feature: ['추가', '구현', '만들', '새', '기능', 'add', 'implement', 'feature', 'create', 'new'],
25
- research: ['조사', '분석', '비교', '검토', '연구', 'research', 'analyze', 'compare', 'investigate'],
26
- planning: ['계획', '설계', '로드맵', 'plan', 'design', 'architecture', 'roadmap'],
27
- release: ['배포', '릴리즈', '버전', 'release', 'deploy', 'publish'],
28
- consistency: ['일관성', '통합', '동기화', '맞춰', 'consistency', 'sync', 'align']
29
- };
30
- let estimatedType = 'feature'; // default
31
- let maxScore = 0;
32
- for (const [type, kws] of Object.entries(routeKw)) {
33
- const score = kws.filter(k => lower.includes(k)).length;
34
- if (score > maxScore) { maxScore = score; estimatedType = type; }
35
- }
36
-
37
- // 2) 기존 자원 회수 — brainstorm spawn (모든 surface 통합 회수)
38
- const conflictHints = []; // ⚠ 같은 키워드 + 실패/오류 패턴
39
- const reuseCandidates = []; // 🔁 기존 skill / reuse-map / decision 후보
40
- const lessonsRecall = []; // 🧠 과거 lesson
41
- const planConflicts = []; // 📋 진행 중 milestone과 충돌 가능
42
-
43
- // brainstorm 호출 (1.9.13~) — JSON 결과 회수
44
- try {
45
- const r = cp.spawnSync(process.execPath, [harnessPath, 'brainstorm', text, '--path', root, '--json'], {
46
- encoding: 'utf8', timeout: 12000,
47
- env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_BANNER: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' }
48
- });
49
- if (r.stdout) {
50
- const j = JSON.parse(r.stdout);
51
- const hits = j.hits || {};
52
- // decisions — 과거 결정 후보
53
- (hits.decisions || []).slice(0, 5).forEach(d => {
54
- lessonsRecall.push({ kind: 'decision', title: d.title, line: d.line, preview: (d.preview || '').slice(0, 100) });
55
- });
56
- // lessons — 과거 교훈 (특히 실패 키워드)
57
- (hits.lessons || []).slice(0, 5).forEach(l => {
58
- const preview = (l.text || l.preview || '').slice(0, 100);
59
- const isFailure = /실패|오류|에러|fail|error|bug|문제|warning/i.test(preview);
60
- if (isFailure) {
61
- conflictHints.push({ kind: 'lesson-failure', preview, tags: l.tags });
62
- } else {
63
- lessonsRecall.push({ kind: 'lesson', preview, tags: l.tags });
64
- }
65
- });
66
- // skills — 기존 skill 후보
67
- (hits.skills || []).slice(0, 3).forEach(s => {
68
- reuseCandidates.push({ kind: 'skill', id: s.id, displayNameKo: s.displayNameKo, capabilities: s.capabilities });
69
- });
70
- // tasks — 진행 중 task 충돌
71
- (hits.tasks || []).slice(0, 3).forEach(tsk => {
72
- if (tsk.status && /in-progress|진행/.test(String(tsk.status))) {
73
- conflictHints.push({ kind: 'task-in-progress', id: tsk.id, title: tsk.title });
74
- }
75
- });
76
- // plan milestones — 진행 중 milestone
77
- (hits.planMilestones || []).slice(0, 3).forEach(m => {
78
- if (m.status && /in-progress|진행/.test(String(m.status))) {
79
- planConflicts.push({ kind: 'milestone-in-progress', id: m.id, title: m.title });
80
- }
81
- });
82
- // taskLogFails — 과거 같은 키워드 실패 흔적
83
- (hits.taskLogFails || []).slice(0, 3).forEach(f => {
84
- conflictHints.push({ kind: 'task-log-failure', preview: (f.preview || f.text || '').slice(0, 100) });
85
- });
86
- }
87
- } catch {}
88
-
89
- // 3) reuse-map 매칭 — 기존 capability 등록 후보
90
- try {
91
- const reusePath = path.join(root, '.harness/reuse-map.md');
92
- if (exists(reusePath)) {
93
- const reuseLines = read(reusePath).split('\n');
94
- const tokens = lower.split(/\s+/).filter(t => t.length >= 3);
95
- for (const line of reuseLines) {
96
- if (!/^\| /.test(line)) continue; // 테이블 row만
97
- const ll = line.toLowerCase();
98
- const matched = tokens.filter(t => ll.includes(t)).length;
99
- if (matched > 0) {
100
- const cols = line.split('|').map(s => s.trim());
101
- if (cols[1]) {
102
- reuseCandidates.push({ kind: 'reuse-map', capability: cols[1], where: cols[2] || '', note: cols[3] || '' });
103
- }
104
- }
105
- }
106
- }
107
- } catch {}
108
-
109
- // 4) feature_graph — 같은 영역 변경 가능성
110
- const featureConflicts = [];
111
- try {
112
- const fgPath = path.join(root, '.harness/feature_graph.md');
113
- if (exists(fgPath)) {
114
- const fg = read(fgPath);
115
- const tokens = lower.split(/\s+/).filter(t => t.length >= 4);
116
- // F-XXXX 노드 라인 추출
117
- const nodeBlocks = fg.split(/\n### /);
118
- for (const blk of nodeBlocks.slice(1)) {
119
- const bl = blk.toLowerCase();
120
- const matched = tokens.filter(t => bl.includes(t)).length;
121
- if (matched > 0) {
122
- const titleMatch = blk.match(/^([^\n]+)/);
123
- const idMatch = blk.match(/F-\d+/);
124
- if (titleMatch && idMatch) {
125
- featureConflicts.push({ kind: 'feature', id: idMatch[0], title: titleMatch[1].trim() });
126
- }
127
- }
128
- }
129
- }
130
- } catch {}
131
-
132
- // 5) 권장 단계 (작업 유형별)
133
- const recommendedSteps = {
134
- feature: [
135
- '1) leerness reuse-check "<기능>" — 외부 OSS 빌드 vs 재사용 판단 (1.9.285)',
136
- '2) leerness reuse find "<핵심 capability>" — 내부 중복 구현 사전 차단',
137
- '3) leerness plan add "<milestone>" — 진행 추적',
138
- '4) leerness contract verify SPEC.md src/<mod>.js — 사양 ↔ 구현 일치 검증',
139
- '5) verify-claim --run-tests 로 evidence 의무화'
140
- ],
141
- bugfix: [
142
- '1) leerness brainstorm "<버그 키워드>" — 과거 같은 영역 lesson 회수',
143
- '2) leerness verify-claim T-XXX --strict-claims — 낙관적 표시 사전 감지',
144
- '3) verify-code --run-tests — 재현 + fix 검증',
145
- '4) leerness lesson save "<root cause>" — 같은 실수 재발 차단'
146
- ],
147
- refactor: [
148
- '1) leerness reuse-map — 영향 범위 파악',
149
- '2) leerness impact <file> — 강한/약한 참조 분리',
150
- '3) leerness contract verify — 외부 인터페이스 보존 확인',
151
- '4) verify-code --run-tests + 회귀 테스트'
152
- ],
153
- research: [
154
- '1) leerness brainstorm "<주제>" — 누적 컨텍스트 회수',
155
- '2) leerness lessons --query "<주제>" — 과거 같은 영역 결정',
156
- '3) leerness review <file> --persona research — 깊이 검토',
157
- '4) leerness decision add "<결론>" — 회수 가능하게 영구화'
158
- ],
159
- planning: [
160
- '1) leerness plan add "<milestone>" — 분해 시작',
161
- '2) leerness reuse-map — 기존 자원 인벤토리',
162
- '3) leerness agents recommend planning — sub-agent 분배',
163
- '4) leerness session close — 결정 영구화'
164
- ],
165
- release: [
166
- '1) leerness health — production-ready 확인',
167
- '2) leerness audit + verify-code — 보안 + 검수',
168
- '3) leerness release bump + note + publish'
169
- ],
170
- consistency: [
171
- '1) leerness audit — design/reuse/handoff 일관성 검사',
172
- '2) leerness consistency check — 잠재 일관성 위반',
173
- '3) leerness drift check --auto-fix — 자동 회복'
174
- ]
175
- }[estimatedType] || [];
176
-
177
- // 6) 효율 제안 (적용 가능한 sub-agent + skill)
178
- const efficiencyHints = [];
179
- if (reuseCandidates.length > 0) {
180
- efficiencyHints.push(`🔁 기존 자원 ${reuseCandidates.length}건 발견 — 신규 구현 전 재사용 검토 권장`);
181
- }
182
- if (conflictHints.length > 0) {
183
- efficiencyHints.push(`⚠ 충돌 신호 ${conflictHints.length}건 — 과거 실패 lesson / 진행 중 task 확인 필요`);
184
- }
185
- if (planConflicts.length > 0) {
186
- efficiencyHints.push(`📋 진행 중 milestone ${planConflicts.length}건과 영역 겹침 가능 — plan 정렬 권장`);
187
- }
188
- if (featureConflicts.length > 0) {
189
- efficiencyHints.push(`🕸 Feature Graph ${featureConflicts.length}건 영역 겹침 — 의존성 사전 확인`);
190
- }
191
- // 다중 에이전트 분배 추천
192
- if (estimatedType === 'feature' || estimatedType === 'planning') {
193
- efficiencyHints.push(`👥 leerness agents recommend ${estimatedType} — 작업 유형별 sub-agent 매핑 활용 가능`);
194
- }
195
- if (efficiencyHints.length === 0) {
196
- efficiencyHints.push('✨ 충돌 신호 없음 — 즉시 진행 안전');
197
- }
198
-
199
- // 6.5) 1.9.208: 플랫폼/API 제약 사전 체크 — 사용자 명시 ("호출속도 초당 5회" 같은 규정 사전 확인)
200
- let constraintsCheck = { matched: [], suggestions: [] };
201
- try {
202
- constraintsCheck = _checkRequestConstraints(root, text);
203
- if (constraintsCheck.matched.length > 0) {
204
- efficiencyHints.push(`⚠ 플랫폼 제약 ${constraintsCheck.matched.length}건 — leerness constraints check 로 상세 확인`);
205
- }
206
- } catch {}
207
-
208
- // 7) proceed 권장 (충돌 critical false)
209
- const proceed = conflictHints.length < 3 && planConflicts.length === 0;
210
-
211
- const dt = Date.now() - t0;
212
- const out = {
213
- request: text,
214
- estimatedType,
215
- conflicts: conflictHints,
216
- reuseCandidates,
217
- lessonsRecall,
218
- planConflicts,
219
- featureConflicts,
220
- recommendedSteps,
221
- efficiencyHints,
222
- platformConstraints: constraintsCheck.matched,
223
- constraintSuggestions: constraintsCheck.suggestions,
224
- proceed,
225
- proceedReason: proceed ? '안전 — 충돌 신호 < 3 + plan 충돌 0' : '⚠ 충돌 critical — 사용자 확인 후 진행',
226
- durationMs: dt
227
- };
228
-
229
- try { _recordRun(root, { kind: 'review_request', estimatedType, conflicts: conflictHints.length, reuse: reuseCandidates.length, durationMs: dt, ok: true }); } catch {}
230
-
231
- if (has('--json')) {
232
- log(JSON.stringify(out, null, 2));
233
- return;
234
- }
235
-
236
- log(`# leerness review-request (1.9.176 사전 검토)`);
237
- log(`요청: "${text.slice(0, 200)}${text.length > 200 ? '…' : ''}"`);
238
- log(`추정 작업 유형: ${estimatedType}`);
239
- log('');
240
- if (conflictHints.length) {
241
- log(`## ⚠ 충돌 신호 (${conflictHints.length})`);
242
- conflictHints.slice(0, 5).forEach(c => log(` - [${c.kind}] ${c.title || c.id || ''} ${c.preview || ''}`.trim()));
243
- log('');
244
- }
245
- if (reuseCandidates.length) {
246
- log(`## 🔁 재사용 후보 (${reuseCandidates.length}) — 신규 구현 전 검토`);
247
- reuseCandidates.slice(0, 5).forEach(r => {
248
- if (r.kind === 'skill') log(` - [skill] ${r.id}${r.displayNameKo ? ' · ' + r.displayNameKo : ''}`);
249
- else if (r.kind === 'reuse-map') log(` - [reuse] ${r.capability} @ ${r.where}`);
250
- });
251
- log('');
252
- }
253
- if (lessonsRecall.length) {
254
- log(`## 🧠 과거 컨텍스트 (${lessonsRecall.length}) — 관련 결정/교훈`);
255
- lessonsRecall.slice(0, 3).forEach(l => log(` - [${l.kind}] ${l.title || l.preview}`));
256
- log('');
257
- }
258
- if (planConflicts.length || featureConflicts.length) {
259
- log(`## 📋 진행 중 영역 (${planConflicts.length + featureConflicts.length})`);
260
- planConflicts.forEach(m => log(` - [milestone] ${m.id}: ${m.title}`));
261
- featureConflicts.slice(0, 5).forEach(f => log(` - [feature] ${f.id}: ${f.title}`));
262
- log('');
263
- }
264
- log(`## 💡 효율 제안`);
265
- efficiencyHints.forEach(h => log(` ${h}`));
266
- log('');
267
- // 1.9.208: 플랫폼/API 제약 사전 노출 (사용자 명시)
268
- if (constraintsCheck.matched.length > 0) {
269
- log(`## 🚦 플랫폼/API 제약 사전 체크 (${constraintsCheck.matched.length})`);
270
- for (const m of constraintsCheck.matched) {
271
- log(` - 📦 ${m.platform} (docs: ${m.docs || '-'})`);
272
- for (const c of (m.constraints || []).slice(0, 3)) {
273
- log(` • [${c.kind}] ${c.detail}`);
274
- }
275
- }
276
- log(` leerness constraints check "${text.slice(0, 40)}…" 로 상세 확인`);
277
- log('');
278
- }
279
- if (recommendedSteps.length) {
280
- log(`## 📍 권장 단계 (${estimatedType})`);
281
- recommendedSteps.forEach(s => log(` ${s}`));
282
- log('');
283
- }
284
- log(`## 진행 권장: ${proceed ? '✓ 진행 안전' : '⚠ 사용자 확인 필요'}`);
285
- log(` 사유: ${out.proceedReason}`);
286
- log(` 분석 소요: ${dt}ms`);
287
- }
288
-
289
- module.exports = { reviewRequestCmd };
1
+ // lib/review-request.js — review-request 핸들러 (UR-0125 큰 핸들러 모듈화, 1.9.420)
2
+ // bin/leerness.js 에서 reviewRequestCmd(277줄) 분리. DI: harness 고유 의존(has · harnessPath · _checkRequestConstraints · _recordRun) 주입.
3
+ // io 프리미티브는 ./io, cp/path 는 빌트인. 동작/출력 무변경(thin wrapper 위임).
4
+ 'use strict';
5
+ const cp = require('child_process');
6
+ const path = require('path');
7
+ const { absRoot, exists, read, log, fail, failJson } = require('./io');
8
+
9
+ function reviewRequestCmd(root, request, deps = {}) {
10
+ const { has, harnessPath, _checkRequestConstraints, _recordRun } = deps;
11
+ root = absRoot(root || process.cwd());
12
+ if (!request || !String(request).trim()) {
13
+ // 1.9.428 (10th 외부평가 UR-0128): --json 오류 경로도 순수 JSON (failJson 이 모드 분기)
14
+ return failJson(!!(has && has('--json')), 'review_request_empty', 'leerness review-request "<request>" — 사용자 요청 텍스트 필요');
15
+ }
16
+ const t0 = Date.now();
17
+ const text = String(request).trim();
18
+
19
+ // 1) 작업 유형 추정 (route 기반 키워드 매핑)
20
+ const lower = text.toLowerCase();
21
+ const routeKw = {
22
+ bugfix: ['버그', '오류', '에러', '수정', '고쳐', '실패', 'fix', 'bug', 'error'],
23
+ refactor: ['리팩토', '재구성', '정리', '개선', 'refactor', 'cleanup'],
24
+ feature: ['추가', '구현', '만들', '새', '기능', 'add', 'implement', 'feature', 'create', 'new'],
25
+ research: ['조사', '분석', '비교', '검토', '연구', 'research', 'analyze', 'compare', 'investigate'],
26
+ planning: ['계획', '설계', '로드맵', 'plan', 'design', 'architecture', 'roadmap'],
27
+ release: ['배포', '릴리즈', '버전', 'release', 'deploy', 'publish'],
28
+ consistency: ['일관성', '통합', '동기화', '맞춰', 'consistency', 'sync', 'align']
29
+ };
30
+ let estimatedType = 'feature'; // default
31
+ let maxScore = 0;
32
+ for (const [type, kws] of Object.entries(routeKw)) {
33
+ const score = kws.filter(k => lower.includes(k)).length;
34
+ if (score > maxScore) { maxScore = score; estimatedType = type; }
35
+ }
36
+
37
+ // 2) 기존 자원 회수 — brainstorm spawn (모든 surface 통합 회수)
38
+ const conflictHints = []; // ⚠ 같은 키워드 + 실패/오류 패턴
39
+ const reuseCandidates = []; // 🔁 기존 skill / reuse-map / decision 후보
40
+ const lessonsRecall = []; // 🧠 과거 lesson
41
+ const planConflicts = []; // 📋 진행 중 milestone과 충돌 가능
42
+
43
+ // brainstorm 호출 (1.9.13~) — JSON 결과 회수
44
+ try {
45
+ const r = cp.spawnSync(process.execPath, [harnessPath, 'brainstorm', text, '--path', root, '--json'], {
46
+ encoding: 'utf8', timeout: 12000,
47
+ env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_BANNER: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' }
48
+ });
49
+ if (r.stdout) {
50
+ const j = JSON.parse(r.stdout);
51
+ const hits = j.hits || {};
52
+ // decisions — 과거 결정 후보
53
+ (hits.decisions || []).slice(0, 5).forEach(d => {
54
+ lessonsRecall.push({ kind: 'decision', title: d.title, line: d.line, preview: (d.preview || '').slice(0, 100) });
55
+ });
56
+ // lessons — 과거 교훈 (특히 실패 키워드)
57
+ (hits.lessons || []).slice(0, 5).forEach(l => {
58
+ const preview = (l.text || l.preview || '').slice(0, 100);
59
+ const isFailure = /실패|오류|에러|fail|error|bug|문제|warning/i.test(preview);
60
+ if (isFailure) {
61
+ conflictHints.push({ kind: 'lesson-failure', preview, tags: l.tags });
62
+ } else {
63
+ lessonsRecall.push({ kind: 'lesson', preview, tags: l.tags });
64
+ }
65
+ });
66
+ // skills — 기존 skill 후보
67
+ (hits.skills || []).slice(0, 3).forEach(s => {
68
+ reuseCandidates.push({ kind: 'skill', id: s.id, displayNameKo: s.displayNameKo, capabilities: s.capabilities });
69
+ });
70
+ // tasks — 진행 중 task 충돌
71
+ (hits.tasks || []).slice(0, 3).forEach(tsk => {
72
+ if (tsk.status && /in-progress|진행/.test(String(tsk.status))) {
73
+ conflictHints.push({ kind: 'task-in-progress', id: tsk.id, title: tsk.title });
74
+ }
75
+ });
76
+ // plan milestones — 진행 중 milestone
77
+ (hits.planMilestones || []).slice(0, 3).forEach(m => {
78
+ if (m.status && /in-progress|진행/.test(String(m.status))) {
79
+ planConflicts.push({ kind: 'milestone-in-progress', id: m.id, title: m.title });
80
+ }
81
+ });
82
+ // taskLogFails — 과거 같은 키워드 실패 흔적
83
+ (hits.taskLogFails || []).slice(0, 3).forEach(f => {
84
+ conflictHints.push({ kind: 'task-log-failure', preview: (f.preview || f.text || '').slice(0, 100) });
85
+ });
86
+ }
87
+ } catch {}
88
+
89
+ // 3) reuse-map 매칭 — 기존 capability 등록 후보
90
+ try {
91
+ const reusePath = path.join(root, '.harness/reuse-map.md');
92
+ if (exists(reusePath)) {
93
+ const reuseLines = read(reusePath).split('\n');
94
+ const tokens = lower.split(/\s+/).filter(t => t.length >= 3);
95
+ for (const line of reuseLines) {
96
+ if (!/^\| /.test(line)) continue; // 테이블 row만
97
+ const ll = line.toLowerCase();
98
+ const matched = tokens.filter(t => ll.includes(t)).length;
99
+ if (matched > 0) {
100
+ const cols = line.split('|').map(s => s.trim());
101
+ if (cols[1]) {
102
+ reuseCandidates.push({ kind: 'reuse-map', capability: cols[1], where: cols[2] || '', note: cols[3] || '' });
103
+ }
104
+ }
105
+ }
106
+ }
107
+ } catch {}
108
+
109
+ // 4) feature_graph — 같은 영역 변경 가능성
110
+ const featureConflicts = [];
111
+ try {
112
+ const fgPath = path.join(root, '.harness/feature_graph.md');
113
+ if (exists(fgPath)) {
114
+ const fg = read(fgPath);
115
+ const tokens = lower.split(/\s+/).filter(t => t.length >= 4);
116
+ // F-XXXX 노드 라인 추출
117
+ const nodeBlocks = fg.split(/\n### /);
118
+ for (const blk of nodeBlocks.slice(1)) {
119
+ const bl = blk.toLowerCase();
120
+ const matched = tokens.filter(t => bl.includes(t)).length;
121
+ if (matched > 0) {
122
+ const titleMatch = blk.match(/^([^\n]+)/);
123
+ const idMatch = blk.match(/F-\d+/);
124
+ if (titleMatch && idMatch) {
125
+ featureConflicts.push({ kind: 'feature', id: idMatch[0], title: titleMatch[1].trim() });
126
+ }
127
+ }
128
+ }
129
+ }
130
+ } catch {}
131
+
132
+ // 5) 권장 단계 (작업 유형별)
133
+ const recommendedSteps = {
134
+ feature: [
135
+ '1) leerness reuse-check "<기능>" — 외부 OSS 빌드 vs 재사용 판단 (1.9.285)',
136
+ '2) leerness reuse find "<핵심 capability>" — 내부 중복 구현 사전 차단',
137
+ '3) leerness plan add "<milestone>" — 진행 추적',
138
+ '4) leerness contract verify SPEC.md src/<mod>.js — 사양 ↔ 구현 일치 검증',
139
+ '5) verify-claim --run-tests 로 evidence 의무화'
140
+ ],
141
+ bugfix: [
142
+ '1) leerness brainstorm "<버그 키워드>" — 과거 같은 영역 lesson 회수',
143
+ '2) leerness verify-claim T-XXX --strict-claims — 낙관적 표시 사전 감지',
144
+ '3) verify-code --run-tests — 재현 + fix 검증',
145
+ '4) leerness lesson save "<root cause>" — 같은 실수 재발 차단'
146
+ ],
147
+ refactor: [
148
+ '1) leerness reuse-map — 영향 범위 파악',
149
+ '2) leerness impact <file> — 강한/약한 참조 분리',
150
+ '3) leerness contract verify — 외부 인터페이스 보존 확인',
151
+ '4) verify-code --run-tests + 회귀 테스트'
152
+ ],
153
+ research: [
154
+ '1) leerness brainstorm "<주제>" — 누적 컨텍스트 회수',
155
+ '2) leerness lessons --query "<주제>" — 과거 같은 영역 결정',
156
+ '3) leerness review <file> --persona research — 깊이 검토',
157
+ '4) leerness decision add "<결론>" — 회수 가능하게 영구화'
158
+ ],
159
+ planning: [
160
+ '1) leerness plan add "<milestone>" — 분해 시작',
161
+ '2) leerness reuse-map — 기존 자원 인벤토리',
162
+ '3) leerness agents recommend planning — sub-agent 분배',
163
+ '4) leerness session close — 결정 영구화'
164
+ ],
165
+ release: [
166
+ '1) leerness health — production-ready 확인',
167
+ '2) leerness audit + verify-code — 보안 + 검수',
168
+ '3) leerness release bump + note + publish'
169
+ ],
170
+ consistency: [
171
+ '1) leerness audit — design/reuse/handoff 일관성 검사',
172
+ '2) leerness consistency check — 잠재 일관성 위반',
173
+ '3) leerness drift check --auto-fix — 자동 회복'
174
+ ]
175
+ }[estimatedType] || [];
176
+
177
+ // 6) 효율 제안 (적용 가능한 sub-agent + skill)
178
+ const efficiencyHints = [];
179
+ if (reuseCandidates.length > 0) {
180
+ efficiencyHints.push(`🔁 기존 자원 ${reuseCandidates.length}건 발견 — 신규 구현 전 재사용 검토 권장`);
181
+ }
182
+ if (conflictHints.length > 0) {
183
+ efficiencyHints.push(`⚠ 충돌 신호 ${conflictHints.length}건 — 과거 실패 lesson / 진행 중 task 확인 필요`);
184
+ }
185
+ if (planConflicts.length > 0) {
186
+ efficiencyHints.push(`📋 진행 중 milestone ${planConflicts.length}건과 영역 겹침 가능 — plan 정렬 권장`);
187
+ }
188
+ if (featureConflicts.length > 0) {
189
+ efficiencyHints.push(`🕸 Feature Graph ${featureConflicts.length}건 영역 겹침 — 의존성 사전 확인`);
190
+ }
191
+ // 다중 에이전트 분배 추천
192
+ if (estimatedType === 'feature' || estimatedType === 'planning') {
193
+ efficiencyHints.push(`👥 leerness agents recommend ${estimatedType} — 작업 유형별 sub-agent 매핑 활용 가능`);
194
+ }
195
+ if (efficiencyHints.length === 0) {
196
+ efficiencyHints.push('✨ 충돌 신호 없음 — 즉시 진행 안전');
197
+ }
198
+
199
+ // 6.5) 1.9.208: 플랫폼/API 제약 사전 체크 — 사용자 명시 ("호출속도 초당 5회" 같은 규정 사전 확인)
200
+ let constraintsCheck = { matched: [], suggestions: [] };
201
+ try {
202
+ constraintsCheck = _checkRequestConstraints(root, text);
203
+ if (constraintsCheck.matched.length > 0) {
204
+ efficiencyHints.push(`⚠ 플랫폼 제약 ${constraintsCheck.matched.length}건 — leerness constraints check 로 상세 확인`);
205
+ }
206
+ } catch {}
207
+
208
+ // 6.7) 1.14.1 (Karpathy 가이드라인 1+2, UR-0031): 범위 과대 / 투기적 신호 표면화 — "생각하고 코딩"(트레이드오프 표면화) + "단순성 우선"(요청 범위만, 추측성 일반화 보류). advisory(차단 X).
209
+ const broadHits = [...new Set((text.match(/(전체|싹\s*다|모두|전부|모든\s*(?:코드|파일|것)|리팩토링|리팩터|재구성|재작성|갈아\s*엎|overhaul|rewrite\s+everything|refactor\s+everything)/gi) || []).map(s => s.trim()))];
210
+ const specHits = [...new Set((text.match(/(나중에|추후|미래\s*대비|확장\s*가능|유연하게|범용화|일반화|추상화|future[-\s]?proof|extensible|pluggable|일단\s*만들)/gi) || []).map(s => s.trim()))];
211
+ const simplicitySignals = { broad: broadHits, speculative: specHits };
212
+ if (broadHits.length) efficiencyHints.push(`🤔 범위 과대 신호(${broadHits.slice(0, 3).join(', ')}) — 더 작게 쪼갤 수 있나? 요청에 명시된 것만 (Karpathy 단순성)`);
213
+ if (specHits.length) efficiencyHints.push(`🧪 투기적 신호(${specHits.slice(0, 3).join(', ')}) — 지금 필요한 범위만, 추측성 일반화 보류 (Karpathy 단순성)`);
214
+
215
+ // 7) proceed 권장 (충돌 critical 시 false)
216
+ const proceed = conflictHints.length < 3 && planConflicts.length === 0;
217
+
218
+ const dt = Date.now() - t0;
219
+ const out = {
220
+ request: text,
221
+ estimatedType,
222
+ conflicts: conflictHints,
223
+ reuseCandidates,
224
+ lessonsRecall,
225
+ planConflicts,
226
+ featureConflicts,
227
+ recommendedSteps,
228
+ efficiencyHints,
229
+ simplicitySignals,
230
+ platformConstraints: constraintsCheck.matched,
231
+ constraintSuggestions: constraintsCheck.suggestions,
232
+ proceed,
233
+ proceedReason: proceed ? '안전 — 충돌 신호 < 3 + plan 충돌 0' : '⚠ 충돌 critical — 사용자 확인 후 진행',
234
+ durationMs: dt
235
+ };
236
+
237
+ try { _recordRun(root, { kind: 'review_request', estimatedType, conflicts: conflictHints.length, reuse: reuseCandidates.length, durationMs: dt, ok: true }); } catch {}
238
+
239
+ if (has('--json')) {
240
+ log(JSON.stringify(out, null, 2));
241
+ return;
242
+ }
243
+
244
+ log(`# leerness review-request (1.9.176 사전 검토)`);
245
+ log(`요청: "${text.slice(0, 200)}${text.length > 200 ? '…' : ''}"`);
246
+ log(`추정 작업 유형: ${estimatedType}`);
247
+ log('');
248
+ if (conflictHints.length) {
249
+ log(`## 충돌 신호 (${conflictHints.length})`);
250
+ conflictHints.slice(0, 5).forEach(c => log(` - [${c.kind}] ${c.title || c.id || ''} ${c.preview || ''}`.trim()));
251
+ log('');
252
+ }
253
+ if (reuseCandidates.length) {
254
+ log(`## 🔁 재사용 후보 (${reuseCandidates.length}) — 신규 구현 전 검토`);
255
+ reuseCandidates.slice(0, 5).forEach(r => {
256
+ if (r.kind === 'skill') log(` - [skill] ${r.id}${r.displayNameKo ? ' · ' + r.displayNameKo : ''}`);
257
+ else if (r.kind === 'reuse-map') log(` - [reuse] ${r.capability} @ ${r.where}`);
258
+ });
259
+ log('');
260
+ }
261
+ if (lessonsRecall.length) {
262
+ log(`## 🧠 과거 컨텍스트 (${lessonsRecall.length}) — 관련 결정/교훈`);
263
+ lessonsRecall.slice(0, 3).forEach(l => log(` - [${l.kind}] ${l.title || l.preview}`));
264
+ log('');
265
+ }
266
+ if (planConflicts.length || featureConflicts.length) {
267
+ log(`## 📋 진행 영역 (${planConflicts.length + featureConflicts.length})`);
268
+ planConflicts.forEach(m => log(` - [milestone] ${m.id}: ${m.title}`));
269
+ featureConflicts.slice(0, 5).forEach(f => log(` - [feature] ${f.id}: ${f.title}`));
270
+ log('');
271
+ }
272
+ log(`## 💡 효율 제안`);
273
+ efficiencyHints.forEach(h => log(` ${h}`));
274
+ log('');
275
+ // 1.9.208: 플랫폼/API 제약 사전 노출 (사용자 명시)
276
+ if (constraintsCheck.matched.length > 0) {
277
+ log(`## 🚦 플랫폼/API 제약 사전 체크 (${constraintsCheck.matched.length})`);
278
+ for (const m of constraintsCheck.matched) {
279
+ log(` - 📦 ${m.platform} (docs: ${m.docs || '-'})`);
280
+ for (const c of (m.constraints || []).slice(0, 3)) {
281
+ log(` • [${c.kind}] ${c.detail}`);
282
+ }
283
+ }
284
+ log(` → leerness constraints check "${text.slice(0, 40)}…" 상세 확인`);
285
+ log('');
286
+ }
287
+ if (recommendedSteps.length) {
288
+ log(`## 📍 권장 단계 (${estimatedType})`);
289
+ recommendedSteps.forEach(s => log(` ${s}`));
290
+ log('');
291
+ }
292
+ log(`## ▶ 진행 권장: ${proceed ? '✓ 진행 안전' : '⚠ 사용자 확인 필요'}`);
293
+ log(` 사유: ${out.proceedReason}`);
294
+ log(` 분석 소요: ${dt}ms`);
295
+ }
296
+
297
+ module.exports = { reviewRequestCmd };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.14.0",
3
+ "version": "1.16.0",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",