leerness 1.14.0 → 1.15.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,58 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.15.0 — 2026-06-09 — 🛡️ [안정화/Stable] Karpathy 가이드라인 정렬 3부작 안정 minor
4
+
5
+ **🛡️ 안정화(Stable) minor.** Andrej Karpathy 코딩 가이드라인(생각하고 코딩 / 단순성 / 외과적 변경 / 목표 주도) 대비 외부 에이전트 검토에서 도출한 정렬 작업(1.14.1~1.14.3)을 검증·통합해 npm 공개. R-0011 정책의 6번째 minor. 영상은 HyperFrames 파이프라인 제작.
6
+
7
+ ### 이번 minor 통합 (1.14.1~1.14.3) — Karpathy 4원칙 정렬
8
+ - **원칙1·2 (생각하고 코딩 / 단순성)**: `review-request` 가 요청의 **범위 과대 신호**(전체·모두·리팩토링·재구성·rewrite/refactor everything)와 **투기적 신호**(나중에·확장 가능·유연하게·범용화·추상화·future-proof)를 탐지해 "더 작게 쪼갤 수 있나? 요청 범위만" 환기(advisory). `--json simplicitySignals`.
9
+ - **원칙4 (목표 주도 — 검증가능한 완료)**: `plan add --done-when "<조건>"` 로 milestone 에 성공 기준을 1급 필드(`Done-When`)로 저장 · `plan list` 표시 + `--json doneWhen` · 미정 시 환기. (부수: `--done-when` 값이 제목에 흡수되던 잠복 버그 수정.)
10
+ - **원칙2 (자기적용)**: 정적 분석으로 호출 0·동적참조 0 인 **죽은 함수 5개 제거**(무행위변경). 함수 476→471.
11
+
12
+ ### 검증 (회귀 0)
13
+ - **selftest 213 PASS** · **E2E 365/365 PASS** · npm gate=minor_bump. 각 기능 행위 재현(신호 탐지/오탐0, done-when 저장·표시, dead 제거 후 재스캔 0).
14
+
15
+ ### 안정화 표시 (R-0006)
16
+ CHANGELOG [안정화/Stable] · git tag annotation (Stable) · GitHub release (Stable) · npm dist-tag `stable` 시도.
17
+
18
+ ## 1.14.3 — 2026-06-09 — Karpathy 정렬④(완결): 자체 단순화 — 죽은 코드 제거 (원칙2)
19
+
20
+ **🧹 leerness 가 자신의 원칙2(단순성)를 자신에게 적용.** 정적 분석으로 "정의됐으나 호출 0 + 동적(문자열) 참조 0" 인 함수만 골라 제거 — 무행위변경(검증가능) 단순화.
21
+
22
+ ### 변경 (UR-0033)
23
+ - **죽은 함수 5개 제거**: `_isAutoLoopActive` · `_invalidateSkillsCache` · `_currentLang` · `_typewrite` · `_writeDomainCatalog` (전부 호출처 0, 과거 리팩토링/대체로 고립된 잔재). 함수 476→471, bin 19561줄.
24
+ - 제거 후 **재스캔 = 새 고아 0** (각 함수가 쓰던 헬퍼는 다른 곳에서도 사용 중 — 연쇄 dead 없음).
25
+ - 막연한 "큰 파일 줄이기"가 아닌, **검증가능·무행위변경** 범위로 한정(Karpathy 원칙3 외과적 변경 준수).
26
+
27
+ ### 검증 (회귀 0)
28
+ - **selftest 213 PASS** · **E2E 365/365 PASS** (행위 동일 — dead 코드라 출력/동작 불변). 제거 전 각 후보의 전체 참조(bin+lib+scripts+test)=정의 1줄만 직접 확인(맹신 X).
29
+ - patch(1.14.3) — npm 미배포(R-0011, GitHub). **Karpathy 백로그(UR-0031/0032/0033) 완결.**
30
+
31
+ ## 1.14.2 — 2026-06-09 — Karpathy 정렬③: plan --done-when 검증가능 완료조건 (원칙4)
32
+
33
+ **🎯 milestone 에 "성공 기준"을 1급 필드로.** Karpathy 원칙4(목표 주도 실행 — 검증가능한 완료 정의)의 빠진 절반. plan 에 done-criteria 개념이 없어 "언제 끝인지" 가 모호했던 것을 보강.
34
+
35
+ ### 변경 (UR-0032)
36
+ - **plan add `--done-when "<조건>"`**: milestone 에 `Done-When:` 라인으로 검증가능 완료조건 저장. 미지정 시 `(미정)`.
37
+ - **plan list 표시 + `--json doneWhen`**: 각 milestone 의 완료기준 노출. 누락(legacy/미정) 시 `⚠ 미정 — --done-when 권장 (Karpathy 원칙4)` 환기.
38
+ - **🐛 잠복 버그 수정(맹신 X)**: `nonFlagArgs()` 의 value-flag 집합(`withValue`)에 `--done-when` 이 없어, 값이 positional 로 누출돼 **milestone 제목에 흡수**(예: "결제 연동" → "결제 연동 Stripe e2e…")되던 것 차단. 행위 재현으로 발견·수정.
39
+
40
+ ### 검증 (회귀 0)
41
+ - **selftest 212→213**, 행위: `plan add "결제 연동" --done-when "Stripe e2e 통과"` → 제목="결제 연동"(흡수 없음)·Done-When 분리 저장·plan list/json 노출; 미지정 → (미정)+환기.
42
+ - patch(1.14.2) — npm 미배포(R-0011, GitHub). 잔여 Karpathy: UR-0033(자체 단순화).
43
+
44
+ ## 1.14.1 — 2026-06-09 — Karpathy 정렬②: review-request 범위과대/투기적 신호 (원칙1+2)
45
+
46
+ **🤔 사전 검토 게이트에 "생각하고 코딩" + "단순성 우선" 신호 추가.** Karpathy 리뷰가 가장 약한 원칙으로 꼽은 1(트레이드오프 표면화)·2(단순성)를, 가장 많이 쓰는 `review-request`(작업 전 자동 호출)에서 보강.
47
+
48
+ ### 변경 (UR-0031)
49
+ - **review-request 단순성/범위 신호**: 요청 텍스트에서 **범위 과대 동사**(전체·모두·리팩토링·재구성·rewrite/refactor everything 등)와 **투기적 신호**(나중에·확장 가능·유연하게·범용화·추상화·future-proof 등)를 탐지해 `efficiencyHints` + `--json simplicitySignals` 로 표면화. "더 작게 쪼갤 수 있나? 요청 범위만" 환기. advisory(차단 X — 표면화만, Karpathy 원칙1).
50
+ - 신규 명령 0 — 기존 명령 확장(leerness 자신의 원칙2 준수).
51
+
52
+ ### 검증 (회귀 0)
53
+ - **selftest 211→212**, 행위 재현: "전체 코드베이스 리팩토링 + 나중에 유연하게 확장 가능하게 추상화" → broad=[전체,리팩토링] spec=[나중에,유연하게,확장 가능,추상화]; 단순 요청 → 0(오탐 없음).
54
+ - patch(1.14.1) — R-0011 정책상 npm 미배포(GitHub). 잔여 Karpathy 백로그: UR-0032(plan --done-when 성공기준), UR-0033(자체 단순화).
55
+
3
56
  ## 1.14.0 — 2026-06-09 — 🛡️ [안정화/Stable] 블라인드 리뷰 수정 + Karpathy 정렬 안정 minor
4
57
 
5
58
  **🛡️ 안정화(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.15.0 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
190
190
 
191
191
  ### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
192
192
 
@@ -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.15.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.15.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.15.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.15.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];
@@ -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,20 @@ 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
+ } },
3569
3542
  { name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
3570
3543
  ];
3571
3544
  }
@@ -4810,13 +4783,6 @@ function _loadDomainCatalog(root) {
4810
4783
  return merged;
4811
4784
  } catch { return _DEFAULT_DOMAIN_CATALOG; }
4812
4785
  }
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
4786
  // 1.9.325 (UR-0025): _classifyIntent → lib/pure-utils.js 로 이동 (순수 intent 분류, require 사용).
4821
4787
  function _detectDomain(text, root) {
4822
4788
  return _matchDomain(_loadDomainCatalog(root), text);
@@ -6467,6 +6433,7 @@ function planListCmd(root, opts = {}) {
6467
6433
  const title = headerMatch[2].trim();
6468
6434
  const statusMatch = b.match(/^Status:\s*(.+)$/m);
6469
6435
  const progressMatch = b.match(/^Progress:\s*(.+)$/m);
6436
+ const doneWhenMatch = b.match(/^Done-When:\s*(.+)$/m); // 1.14.2 (Karpathy 원칙4, UR-0032): 검증가능 완료조건
6470
6437
  // Tasks 블록 (- [ ] 또는 - [x])
6471
6438
  const tasks = [];
6472
6439
  const tasksSection = b.match(/Tasks:\s*\n([\s\S]+?)(?=\n###|\n## |$)/);
@@ -6481,6 +6448,7 @@ function planListCmd(root, opts = {}) {
6481
6448
  title,
6482
6449
  status: statusMatch ? statusMatch[1].trim() : null,
6483
6450
  progress: progressMatch ? progressMatch[1].trim() : null,
6451
+ doneWhen: doneWhenMatch ? doneWhenMatch[1].trim() : null,
6484
6452
  tasks,
6485
6453
  });
6486
6454
  }
@@ -6495,6 +6463,7 @@ function planListCmd(root, opts = {}) {
6495
6463
  log(`\n[${m.id}] ${m.title}`);
6496
6464
  if (m.status) log(` Status: ${m.status}`);
6497
6465
  if (m.progress) log(` Progress: ${m.progress}`);
6466
+ log(` 완료기준(Done-When): ${m.doneWhen || '⚠ 미정 — plan add ... --done-when "<검증가능 조건>" 권장 (Karpathy 원칙4)'}`);
6498
6467
  if (m.tasks.length) log(` Tasks: ${m.tasks.length}개 (${m.tasks.filter(t => t.done).length} 완료)`);
6499
6468
  }
6500
6469
  }
@@ -6503,10 +6472,12 @@ function planAdd(root, text) {
6503
6472
  if (!_requireInit(root, 'plan add')) return; // 1.9.311 (UR-0047): init 가드
6504
6473
  if (!_validateChoice(arg('--status', null), TASK_STATUSES, 'plan status')) { process.exitCode = 1; return; } // 1.9.310 (UR-0046)
6505
6474
  const status = arg('--status','planned'), progress = arg('--progress','0'), nextAction = arg('--next', '다음 액션 작성');
6475
+ // 1.14.2 (Karpathy 원칙4 "성공기준 정의", UR-0032): --done-when 으로 검증가능 완료조건을 milestone 에 기록. 미지정 시 (미정) — plan show/audit 가 환기.
6476
+ const doneWhen = _lineSafe(arg('--done-when', '') || '(미정)');
6506
6477
  // 1.9.303 (UR-0043): M-id append + T-id upsert 를 하나의 락으로 — 동시 plan add ID 충돌 방지.
6507
6478
  const { id, tid } = _withLock(progressPath(root), () => {
6508
6479
  const id = nextId(root, 'M');
6509
- append(planPath(root), `\n### ${id}. ${text}\nStatus: ${status}\nProgress: ${progress}%\n\nTasks:\n- [ ] ${text}\n`);
6480
+ append(planPath(root), `\n### ${id}. ${text}\nStatus: ${status}\nProgress: ${progress}%\nDone-When: ${doneWhen}\n\nTasks:\n- [ ] ${text}\n`);
6510
6481
  const tid = nextId(root, 'T');
6511
6482
  upsertProgress(root, { id: tid, status, request: text, evidence: `plan:${id}`, nextAction });
6512
6483
  return { id, tid };
@@ -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.15.0",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",