leerness 1.9.418 → 1.9.420

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,49 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.420 — 2026-06-07 — 무거움 점진 해소: reviewRequestCmd → lib/review-request.js 모듈화 (UR-0025/UR-0125)
4
+
5
+ **🪶 `bin/leerness.js`(21k줄/1.3MB) 무거움 점진 해소 — `review-request` 핸들러(277줄)를 lib/ 로 DI 분리.**
6
+
7
+ ### 배경
8
+ 실현가능성 조사(3-에이전트)에서 무거움=MODERATE, 큰 핸들러 점진 추출 권장. 후보별 외부 의존 footprint 측정 결과 **reviewRequestCmd(285줄, 의존 최소)**가 최저 위험 → 첫 추출 대상으로 선정(안정성 우선).
9
+
10
+ ### 변경
11
+ - `lib/review-request.js` 신설(288줄): `reviewRequestCmd(root, request, deps)` — io 프리미티브는 `./io`, `cp`/`path` 빌트인, harness 고유 의존(`has`·`harnessPath`·`_checkRequestConstraints`·`_recordRun`)은 **DI 주입**(기존 doctor/team/migrate/feature 패턴 동일).
12
+ - `bin/leerness.js`: 277줄 함수 → **3줄 thin wrapper** 위임. **21,177 → 20,903줄(−274)**.
13
+ - 동작/출력 **무변경**(텍스트·--json·빈입력 가드 동일).
14
+
15
+ ### 검증 (회귀 0)
16
+ - **selftest 165→166 PASS** (모듈 존재 + DI 위임 + 인라인 제거 + estimatedType/recommendedSteps 동작).
17
+ - **E2E 419→420 PASS**.
18
+
19
+ ### 무거움 진행 (UR-0125, 다음 라운드)
20
+ 다음 추출 후보(의존 footprint 순): audit(~9) → driftCheck(~13) → healthCmd(~24) → handoff(1434줄, 최대/최고결합). 라운드마다 1개씩 안전 추출.
21
+
22
+ ## 1.9.419 — 2026-06-07 — 엔트리 파일명 변경: bin/harness.js → bin/leerness.js (네이밍 일관성, UR-0126)
23
+
24
+ **📛 패키지 엔트리 파일명을 `bin/harness.js` → `bin/leerness.js` 로 변경 — 명령어(`leerness`)와 파일명 일치.**
25
+
26
+ ### 배경
27
+ 파일명 `harness.js` 는 초기 잔재로, 명령어/패키지명(`leerness`)과 불일치했습니다. 3-에이전트 실현가능성 조사 결과 **EASY/안전**(명령어는 이미 `leerness`, 내부 참조는 `__filename`/`path.resolve` 기반이라 자동 대응)으로 판정되어 실행.
28
+
29
+ ### 변경
30
+ - `git mv bin/harness.js bin/leerness.js` (히스토리 보존).
31
+ - `package.json`: `bin.leerness` / `main` / npm scripts(test/test:fast/prepack) → `bin/leerness.js`.
32
+ - `scripts/e2e.js`(35건)·`scripts/smoke.js`·`.github/workflows/ci.yml`·`docs/PUBLISH_PRECHECK.md` 경로 참조 갱신.
33
+ - 내부 self-invocation/진단(`harnessPath: __filename`)·selftest(`read(__filename)`)는 런타임 자동 — 무변경.
34
+ - next-action 제안 문자열 1건 `node ./bin/harness.js whats-new` → `leerness whats-new`.
35
+
36
+ ### 사용자 영향
37
+ **없음** — 설치/실행은 `leerness` 명령(불변). 파일을 직접 호출하지 않음. 기존 워크스페이스(.harness)·상태도 불변.
38
+
39
+ ### 검증 (회귀 0)
40
+ - **selftest 164→165 PASS** (신규 구조 가드: bin 파일=leerness.js + package.json bin/main 일치).
41
+ - **E2E 418→419 PASS** (CLI 경로 변경 후 전체 스모크).
42
+
43
+ ### 참고 (조사 결과 — 다음 라운드)
44
+ - harness.js 무거움(21k줄) → UR-0125 점진 모듈화(handoff 등 DI 추출).
45
+ - `.harness`→`.leerness` 폴더명 → UR-0127 **직접 변경 비권장**(기존 .leerness 상태 substrate 충돌) — 상태기판 격리(대안 A) 권장.
46
+
3
47
  ## 1.9.418 — 2026-06-07 — health 보안 정직화 + status "healthy" 의미 명시 (9번째 외부평가 Codex P2, UR-0121 잔여)
4
48
 
5
49
  **🩺 `health`/`status` 의 "healthy" 가 프로젝트 안전을 뜻하는 듯 오해되던 것(Codex P2) 보강 — 정직성 일관 적용.**
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > **AI 코딩 에이전트의 거짓 완료·중복·망각·충돌을 막아주는 검수·기억·협업 CLI 하네스.**
4
4
  > **A CLI harness that stops AI coding agents from faking completion, duplicating work, forgetting context, and colliding.**
5
5
 
6
- [![npm](https://img.shields.io/badge/npm-leerness-blue)](https://www.npmjs.com/package/leerness) [![version](https://img.shields.io/badge/version-1.9.418-green)]() [![tests](https://img.shields.io/badge/e2e-357%2F357-success)]() [![selftest](https://img.shields.io/badge/selftest-164-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-85-brightgreen)]() [![providers](https://img.shields.io/badge/AI_providers-10-brightgreen)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
6
+ [![npm](https://img.shields.io/badge/npm-leerness-blue)](https://www.npmjs.com/package/leerness) [![version](https://img.shields.io/badge/version-1.9.420-green)]() [![tests](https://img.shields.io/badge/e2e-357%2F357-success)]() [![selftest](https://img.shields.io/badge/selftest-166-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-85-brightgreen)]() [![providers](https://img.shields.io/badge/AI_providers-10-brightgreen)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
7
7
 
8
8
  ```
9
9
  ╔══════════════════════════════════════════════════════════════╗
@@ -471,7 +471,7 @@ MIT — © leerness contributors
471
471
  <!-- leerness:project-readme:start -->
472
472
  ## Leerness Project Harness
473
473
 
474
- 이 프로젝트는 Leerness v1.9.418 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
474
+ 이 프로젝트는 Leerness v1.9.420 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
475
475
 
476
476
  ### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
477
477
 
@@ -525,7 +525,7 @@ leerness memory restore decision <date|title>
525
525
 
526
526
  ### MCP server (외부 AI 통합)
527
527
 
528
- Leerness v1.9.418는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
528
+ Leerness v1.9.420는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
529
529
 
530
530
  ```jsonc
531
531
  // 카테고리별
@@ -546,7 +546,7 @@ Leerness v1.9.418는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
546
546
  `<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
547
547
  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) 다음 라운드 예약.
548
548
 
549
- 현재 누적: **70 라운드 (1.9.40 → 1.9.418)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
549
+ 현재 누적: **70 라운드 (1.9.40 → 1.9.420)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
550
550
 
551
551
  ### 성능 가이드 (1.9.140 측정)
552
552
 
@@ -584,6 +584,6 @@ leerness release pack --close --auto-main-push
584
584
  - `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
585
585
  - `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
586
586
 
587
- Last synced by Leerness v1.9.418: 2026-06-07
587
+ Last synced by Leerness v1.9.420: 2026-06-07
588
588
  <!-- leerness:project-readme:end -->
589
589
 
@@ -31,7 +31,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
31
31
  // 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
32
32
  const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST, _DEFAULT_PLATFORM_CONSTRAINTS, _DEFAULT_DOMAIN_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 분리 (MERGE_OVERWRITE_FILES/MINIMAL_SKIP_KEYS 포함)
33
33
 
34
- const VERSION = '1.9.418';
34
+ const VERSION = '1.9.420';
35
35
 
36
36
  // 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
37
37
  // 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
@@ -3072,6 +3072,28 @@ function _selfTestCases() {
3072
3072
  return handoffWired && scanJson && encJson && contractExit && behav;
3073
3073
  } },
3074
3074
  { name: '9라운드 (UR-0119/0120): team review(메인 검수) — _composeTeamPlan reviewStep + handoff 검수필요 + team add 와이어 (1.9.414)', run: () => { const m = require('../lib/pure-utils'); const on = m._composeTeamPlan({ id: 't', members: ['a', 'b'], personas: ['security'] }, '점검'); const off = m._composeTeamPlan({ id: 't', members: ['a'], review: false }, '점검'); const planOk = on.review === true && !!on.reviewStep && on.reviewStep.suggestedCommand.includes('verify-claim') && off.review === false && !off.reviewStep; const rem = m._teamHandoffReminders([{ id: 'r', schedule: 'every-session', status: 'active', members: ['a'], review: true }]); const remOk = rem.length === 1 && rem[0].includes('검수필요'); const teamSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'team.js')); const wired = teamSrc.includes("review: !has('--no-review')") && teamSrc.includes('메인 검수 (필수)'); return planOk && remOk && wired; } },
3075
+ { name: '파일명 변경 (UR-0126): bin 파일=leerness.js + package.json bin/main 일치 (1.9.419)', run: () => { const okName = path.basename(__filename) === 'leerness.js'; let pkg; try { pkg = require('../package.json'); } catch { return false; } const okBin = pkg && pkg.bin && pkg.bin.leerness === 'bin/leerness.js' && pkg.main === 'bin/leerness.js'; return okName && okBin; } },
3076
+ { name: 'UR-0025 큰핸들러 모듈화 5번째: reviewRequestCmd → lib/review-request.js + DI 위임 + 동작 (1.9.420)', run: () => {
3077
+ const m = require('../lib/review-request');
3078
+ const expOk = typeof m.reviewRequestCmd === 'function';
3079
+ const src = read(__filename);
3080
+ const delegated = src.includes("require('../lib/review-request')") && src.includes('_reviewRequest.reviewRequestCmd(root, request,');
3081
+ const modSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'review-request.js'));
3082
+ // thin wrapper 는 같은 시그니처를 유지하므로(시그니처 부재로 판정 X), 본문 고유 마커(routeKw)가 lib 로 이동했는지로 검증.
3083
+ const bodyMarker = 'const route' + 'Kw = {'; // split-literal: 자기참조(이 케이스 코드가 src 에 포함) 회피
3084
+ const movedToLib = modSrc.includes("require('./io')") && modSrc.includes('estimatedType') && modSrc.includes('harnessPath') && modSrc.includes(bodyMarker) && !src.includes(bodyMarker);
3085
+ let behavOk = false;
3086
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_rr_'));
3087
+ const _w = process.stdout.write; let out = '';
3088
+ try {
3089
+ fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true });
3090
+ process.stdout.write = s => { out += s; return true; };
3091
+ // harnessPath 를 존재하지 않는 경로로 → 내부 brainstorm spawn 즉시 실패(무부작용). estimatedType/steps 는 spawn 이전 계산이라 검증 가능.
3092
+ m.reviewRequestCmd(tmp, '결제 기능 추가 구현', { has: () => true, harnessPath: path.join(tmp, '__nope.js'), _checkRequestConstraints: () => ({ matched: [], suggestions: [] }), _recordRun: () => {} });
3093
+ } catch {} finally { process.stdout.write = _w; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
3094
+ try { const j = JSON.parse(out); behavOk = j.estimatedType === 'feature' && Array.isArray(j.recommendedSteps) && j.recommendedSteps.length === 5; } catch {}
3095
+ return expOk && delegated && movedToLib && behavOk;
3096
+ } },
3075
3097
  { name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
3076
3098
  ];
3077
3099
  }
@@ -5709,7 +5731,7 @@ function _suggestNextActions(root, latestRow, keyword) {
5709
5731
  // 6) CHANGELOG / README 버전 일치 확인 (1.9.X bump 후 자주 누락되는 단계)
5710
5732
  try {
5711
5733
  if (/(\d\.\d\.\d+)/.test(taskText) || /(bump|version|release)/i.test(taskText)) {
5712
- actions.push({ icon: '📝', title: `버전 변동 task — CHANGELOG.md / README.md 갱신 확인`, command: `node ./bin/harness.js whats-new .` });
5734
+ actions.push({ icon: '📝', title: `버전 변동 task — CHANGELOG.md / README.md 갱신 확인`, command: `leerness whats-new .` });
5713
5735
  }
5714
5736
  } catch {}
5715
5737
 
@@ -20417,283 +20439,9 @@ function lspCmd(root, sub, ...args) {
20417
20439
  //
20418
20440
  // REPL: :review <request> (1.9.175 slash 패턴)
20419
20441
  // MCP : leerness_review_request (외부 AI 직접 호출)
20420
- function reviewRequestCmd(root, request) {
20421
- root = absRoot(root || process.cwd());
20422
- if (!request || !String(request).trim()) {
20423
- return fail('leerness review-request "<request>" — 사용자 요청 텍스트 필요');
20424
- }
20425
- const t0 = Date.now();
20426
- const text = String(request).trim();
20427
-
20428
- // 1) 작업 유형 추정 (route 기반 키워드 매핑)
20429
- const lower = text.toLowerCase();
20430
- const routeKw = {
20431
- bugfix: ['버그', '오류', '에러', '수정', '고쳐', '실패', 'fix', 'bug', 'error'],
20432
- refactor: ['리팩토', '재구성', '정리', '개선', 'refactor', 'cleanup'],
20433
- feature: ['추가', '구현', '만들', '새', '기능', 'add', 'implement', 'feature', 'create', 'new'],
20434
- research: ['조사', '분석', '비교', '검토', '연구', 'research', 'analyze', 'compare', 'investigate'],
20435
- planning: ['계획', '설계', '로드맵', 'plan', 'design', 'architecture', 'roadmap'],
20436
- release: ['배포', '릴리즈', '버전', 'release', 'deploy', 'publish'],
20437
- consistency: ['일관성', '통합', '동기화', '맞춰', 'consistency', 'sync', 'align']
20438
- };
20439
- let estimatedType = 'feature'; // default
20440
- let maxScore = 0;
20441
- for (const [type, kws] of Object.entries(routeKw)) {
20442
- const score = kws.filter(k => lower.includes(k)).length;
20443
- if (score > maxScore) { maxScore = score; estimatedType = type; }
20444
- }
20445
-
20446
- // 2) 기존 자원 회수 — brainstorm spawn (모든 surface 통합 회수)
20447
- const conflictHints = []; // ⚠ 같은 키워드 + 실패/오류 패턴
20448
- const reuseCandidates = []; // 🔁 기존 skill / reuse-map / decision 후보
20449
- const lessonsRecall = []; // 🧠 과거 lesson
20450
- const planConflicts = []; // 📋 진행 중 milestone과 충돌 가능
20451
-
20452
- // brainstorm 호출 (1.9.13~) — JSON 결과 회수
20453
- try {
20454
- const r = cp.spawnSync(process.execPath, [__filename, 'brainstorm', text, '--path', root, '--json'], {
20455
- encoding: 'utf8', timeout: 12000,
20456
- env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_BANNER: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' }
20457
- });
20458
- if (r.stdout) {
20459
- const j = JSON.parse(r.stdout);
20460
- const hits = j.hits || {};
20461
- // decisions — 과거 결정 후보
20462
- (hits.decisions || []).slice(0, 5).forEach(d => {
20463
- lessonsRecall.push({ kind: 'decision', title: d.title, line: d.line, preview: (d.preview || '').slice(0, 100) });
20464
- });
20465
- // lessons — 과거 교훈 (특히 실패 키워드)
20466
- (hits.lessons || []).slice(0, 5).forEach(l => {
20467
- const preview = (l.text || l.preview || '').slice(0, 100);
20468
- const isFailure = /실패|오류|에러|fail|error|bug|문제|warning/i.test(preview);
20469
- if (isFailure) {
20470
- conflictHints.push({ kind: 'lesson-failure', preview, tags: l.tags });
20471
- } else {
20472
- lessonsRecall.push({ kind: 'lesson', preview, tags: l.tags });
20473
- }
20474
- });
20475
- // skills — 기존 skill 후보
20476
- (hits.skills || []).slice(0, 3).forEach(s => {
20477
- reuseCandidates.push({ kind: 'skill', id: s.id, displayNameKo: s.displayNameKo, capabilities: s.capabilities });
20478
- });
20479
- // tasks — 진행 중 task 충돌
20480
- (hits.tasks || []).slice(0, 3).forEach(tsk => {
20481
- if (tsk.status && /in-progress|진행/.test(String(tsk.status))) {
20482
- conflictHints.push({ kind: 'task-in-progress', id: tsk.id, title: tsk.title });
20483
- }
20484
- });
20485
- // plan milestones — 진행 중 milestone
20486
- (hits.planMilestones || []).slice(0, 3).forEach(m => {
20487
- if (m.status && /in-progress|진행/.test(String(m.status))) {
20488
- planConflicts.push({ kind: 'milestone-in-progress', id: m.id, title: m.title });
20489
- }
20490
- });
20491
- // taskLogFails — 과거 같은 키워드 실패 흔적
20492
- (hits.taskLogFails || []).slice(0, 3).forEach(f => {
20493
- conflictHints.push({ kind: 'task-log-failure', preview: (f.preview || f.text || '').slice(0, 100) });
20494
- });
20495
- }
20496
- } catch {}
20497
-
20498
- // 3) reuse-map 매칭 — 기존 capability 등록 후보
20499
- try {
20500
- const reusePath = path.join(root, '.harness/reuse-map.md');
20501
- if (exists(reusePath)) {
20502
- const reuseLines = read(reusePath).split('\n');
20503
- const tokens = lower.split(/\s+/).filter(t => t.length >= 3);
20504
- for (const line of reuseLines) {
20505
- if (!/^\| /.test(line)) continue; // 테이블 row만
20506
- const ll = line.toLowerCase();
20507
- const matched = tokens.filter(t => ll.includes(t)).length;
20508
- if (matched > 0) {
20509
- const cols = line.split('|').map(s => s.trim());
20510
- if (cols[1]) {
20511
- reuseCandidates.push({ kind: 'reuse-map', capability: cols[1], where: cols[2] || '', note: cols[3] || '' });
20512
- }
20513
- }
20514
- }
20515
- }
20516
- } catch {}
20517
-
20518
- // 4) feature_graph — 같은 영역 변경 가능성
20519
- const featureConflicts = [];
20520
- try {
20521
- const fgPath = path.join(root, '.harness/feature_graph.md');
20522
- if (exists(fgPath)) {
20523
- const fg = read(fgPath);
20524
- const tokens = lower.split(/\s+/).filter(t => t.length >= 4);
20525
- // F-XXXX 노드 라인 추출
20526
- const nodeBlocks = fg.split(/\n### /);
20527
- for (const blk of nodeBlocks.slice(1)) {
20528
- const bl = blk.toLowerCase();
20529
- const matched = tokens.filter(t => bl.includes(t)).length;
20530
- if (matched > 0) {
20531
- const titleMatch = blk.match(/^([^\n]+)/);
20532
- const idMatch = blk.match(/F-\d+/);
20533
- if (titleMatch && idMatch) {
20534
- featureConflicts.push({ kind: 'feature', id: idMatch[0], title: titleMatch[1].trim() });
20535
- }
20536
- }
20537
- }
20538
- }
20539
- } catch {}
20540
-
20541
- // 5) 권장 단계 (작업 유형별)
20542
- const recommendedSteps = {
20543
- feature: [
20544
- '1) leerness reuse-check "<기능>" — 외부 OSS 빌드 vs 재사용 판단 (1.9.285)',
20545
- '2) leerness reuse find "<핵심 capability>" — 내부 중복 구현 사전 차단',
20546
- '3) leerness plan add "<milestone>" — 진행 추적',
20547
- '4) leerness contract verify SPEC.md src/<mod>.js — 사양 ↔ 구현 일치 검증',
20548
- '5) verify-claim --run-tests 로 evidence 의무화'
20549
- ],
20550
- bugfix: [
20551
- '1) leerness brainstorm "<버그 키워드>" — 과거 같은 영역 lesson 회수',
20552
- '2) leerness verify-claim T-XXX --strict-claims — 낙관적 표시 사전 감지',
20553
- '3) verify-code --run-tests — 재현 + fix 검증',
20554
- '4) leerness lesson save "<root cause>" — 같은 실수 재발 차단'
20555
- ],
20556
- refactor: [
20557
- '1) leerness reuse-map — 영향 범위 파악',
20558
- '2) leerness impact <file> — 강한/약한 참조 분리',
20559
- '3) leerness contract verify — 외부 인터페이스 보존 확인',
20560
- '4) verify-code --run-tests + 회귀 테스트'
20561
- ],
20562
- research: [
20563
- '1) leerness brainstorm "<주제>" — 누적 컨텍스트 회수',
20564
- '2) leerness lessons --query "<주제>" — 과거 같은 영역 결정',
20565
- '3) leerness review <file> --persona research — 깊이 검토',
20566
- '4) leerness decision add "<결론>" — 회수 가능하게 영구화'
20567
- ],
20568
- planning: [
20569
- '1) leerness plan add "<milestone>" — 분해 시작',
20570
- '2) leerness reuse-map — 기존 자원 인벤토리',
20571
- '3) leerness agents recommend planning — sub-agent 분배',
20572
- '4) leerness session close — 결정 영구화'
20573
- ],
20574
- release: [
20575
- '1) leerness health — production-ready 확인',
20576
- '2) leerness audit + verify-code — 보안 + 검수',
20577
- '3) leerness release bump + note + publish'
20578
- ],
20579
- consistency: [
20580
- '1) leerness audit — design/reuse/handoff 일관성 검사',
20581
- '2) leerness consistency check — 잠재 일관성 위반',
20582
- '3) leerness drift check --auto-fix — 자동 회복'
20583
- ]
20584
- }[estimatedType] || [];
20585
-
20586
- // 6) 효율 제안 (적용 가능한 sub-agent + skill)
20587
- const efficiencyHints = [];
20588
- if (reuseCandidates.length > 0) {
20589
- efficiencyHints.push(`🔁 기존 자원 ${reuseCandidates.length}건 발견 — 신규 구현 전 재사용 검토 권장`);
20590
- }
20591
- if (conflictHints.length > 0) {
20592
- efficiencyHints.push(`⚠ 충돌 신호 ${conflictHints.length}건 — 과거 실패 lesson / 진행 중 task 확인 필요`);
20593
- }
20594
- if (planConflicts.length > 0) {
20595
- efficiencyHints.push(`📋 진행 중 milestone ${planConflicts.length}건과 영역 겹침 가능 — plan 정렬 권장`);
20596
- }
20597
- if (featureConflicts.length > 0) {
20598
- efficiencyHints.push(`🕸 Feature Graph ${featureConflicts.length}건 영역 겹침 — 의존성 사전 확인`);
20599
- }
20600
- // 다중 에이전트 분배 추천
20601
- if (estimatedType === 'feature' || estimatedType === 'planning') {
20602
- efficiencyHints.push(`👥 leerness agents recommend ${estimatedType} — 작업 유형별 sub-agent 매핑 활용 가능`);
20603
- }
20604
- if (efficiencyHints.length === 0) {
20605
- efficiencyHints.push('✨ 충돌 신호 없음 — 즉시 진행 안전');
20606
- }
20607
-
20608
- // 6.5) 1.9.208: 플랫폼/API 제약 사전 체크 — 사용자 명시 ("호출속도 초당 5회" 같은 규정 사전 확인)
20609
- let constraintsCheck = { matched: [], suggestions: [] };
20610
- try {
20611
- constraintsCheck = _checkRequestConstraints(root, text);
20612
- if (constraintsCheck.matched.length > 0) {
20613
- efficiencyHints.push(`⚠ 플랫폼 제약 ${constraintsCheck.matched.length}건 — leerness constraints check 로 상세 확인`);
20614
- }
20615
- } catch {}
20616
-
20617
- // 7) proceed 권장 (충돌 critical 시 false)
20618
- const proceed = conflictHints.length < 3 && planConflicts.length === 0;
20619
-
20620
- const dt = Date.now() - t0;
20621
- const out = {
20622
- request: text,
20623
- estimatedType,
20624
- conflicts: conflictHints,
20625
- reuseCandidates,
20626
- lessonsRecall,
20627
- planConflicts,
20628
- featureConflicts,
20629
- recommendedSteps,
20630
- efficiencyHints,
20631
- platformConstraints: constraintsCheck.matched,
20632
- constraintSuggestions: constraintsCheck.suggestions,
20633
- proceed,
20634
- proceedReason: proceed ? '안전 — 충돌 신호 < 3 + plan 충돌 0' : '⚠ 충돌 critical — 사용자 확인 후 진행',
20635
- durationMs: dt
20636
- };
20637
-
20638
- try { _recordRun(root, { kind: 'review_request', estimatedType, conflicts: conflictHints.length, reuse: reuseCandidates.length, durationMs: dt, ok: true }); } catch {}
20639
-
20640
- if (has('--json')) {
20641
- log(JSON.stringify(out, null, 2));
20642
- return;
20643
- }
20644
-
20645
- log(`# leerness review-request (1.9.176 사전 검토)`);
20646
- log(`요청: "${text.slice(0, 200)}${text.length > 200 ? '…' : ''}"`);
20647
- log(`추정 작업 유형: ${estimatedType}`);
20648
- log('');
20649
- if (conflictHints.length) {
20650
- log(`## ⚠ 충돌 신호 (${conflictHints.length})`);
20651
- conflictHints.slice(0, 5).forEach(c => log(` - [${c.kind}] ${c.title || c.id || ''} ${c.preview || ''}`.trim()));
20652
- log('');
20653
- }
20654
- if (reuseCandidates.length) {
20655
- log(`## 🔁 재사용 후보 (${reuseCandidates.length}) — 신규 구현 전 검토`);
20656
- reuseCandidates.slice(0, 5).forEach(r => {
20657
- if (r.kind === 'skill') log(` - [skill] ${r.id}${r.displayNameKo ? ' · ' + r.displayNameKo : ''}`);
20658
- else if (r.kind === 'reuse-map') log(` - [reuse] ${r.capability} @ ${r.where}`);
20659
- });
20660
- log('');
20661
- }
20662
- if (lessonsRecall.length) {
20663
- log(`## 🧠 과거 컨텍스트 (${lessonsRecall.length}) — 관련 결정/교훈`);
20664
- lessonsRecall.slice(0, 3).forEach(l => log(` - [${l.kind}] ${l.title || l.preview}`));
20665
- log('');
20666
- }
20667
- if (planConflicts.length || featureConflicts.length) {
20668
- log(`## 📋 진행 중 영역 (${planConflicts.length + featureConflicts.length})`);
20669
- planConflicts.forEach(m => log(` - [milestone] ${m.id}: ${m.title}`));
20670
- featureConflicts.slice(0, 5).forEach(f => log(` - [feature] ${f.id}: ${f.title}`));
20671
- log('');
20672
- }
20673
- log(`## 💡 효율 제안`);
20674
- efficiencyHints.forEach(h => log(` ${h}`));
20675
- log('');
20676
- // 1.9.208: 플랫폼/API 제약 사전 노출 (사용자 명시)
20677
- if (constraintsCheck.matched.length > 0) {
20678
- log(`## 🚦 플랫폼/API 제약 사전 체크 (${constraintsCheck.matched.length})`);
20679
- for (const m of constraintsCheck.matched) {
20680
- log(` - 📦 ${m.platform} (docs: ${m.docs || '-'})`);
20681
- for (const c of (m.constraints || []).slice(0, 3)) {
20682
- log(` • [${c.kind}] ${c.detail}`);
20683
- }
20684
- }
20685
- log(` → leerness constraints check "${text.slice(0, 40)}…" 로 상세 확인`);
20686
- log('');
20687
- }
20688
- if (recommendedSteps.length) {
20689
- log(`## 📍 권장 단계 (${estimatedType})`);
20690
- recommendedSteps.forEach(s => log(` ${s}`));
20691
- log('');
20692
- }
20693
- log(`## ▶ 진행 권장: ${proceed ? '✓ 진행 안전' : '⚠ 사용자 확인 필요'}`);
20694
- log(` 사유: ${out.proceedReason}`);
20695
- log(` 분석 소요: ${dt}ms`);
20696
- }
20442
+ const _reviewRequest = require('../lib/review-request');
20443
+ // 1.9.420 (UR-0125 핸들러 모듈화 5번째): reviewRequestCmd → lib/review-request.js (DI 위임, thin wrapper)
20444
+ function reviewRequestCmd(root, request) { return _reviewRequest.reviewRequestCmd(root, request, { has, harnessPath: __filename, _checkRequestConstraints, _recordRun }); }
20697
20445
 
20698
20446
  // 1.9.164: leerness which — 진단 도구 (구버전 충돌 / npx 캐시 / PATH 충돌 해결)
20699
20447
  // 사용자가 "최신 버전 작동 안 함" 의심 시: 실제 실행 중인 leerness 의 경로 / 버전 / npm 캐시 / PATH 후보 표시.
@@ -1,93 +1,93 @@
1
- # Publish Pre-check (leerness 1.9.0)
2
-
3
- > ⚠ **Owner 권한 필수**: npm `leerness`의 메인테이너는 `gytlrgpfl <gytlrgpfl96@gmail.com>` 입니다. 이 계정으로 로그인되어 있거나 collaborator로 등록되어 있어야 publish가 통과합니다. 권한이 없으면 `E403 Forbidden`이 떨어집니다.
4
-
5
- ## 1. 메타데이터 보강 (선택)
6
-
7
- `package.json`의 다음 필드는 비어 있거나 일반적입니다. 사용자 정보로 채우는 것을 권장합니다.
8
-
9
- ```json
10
- {
11
- "author": "leerness contributors",
12
- "repository": { /* 비어있음 */ },
13
- "bugs": { /* 비어있음 */ },
14
- "homepage": ""
15
- }
16
- ```
17
-
18
- 예:
19
-
20
- ```json
21
- "author": "Your Name <you@example.com>",
22
- "repository": { "type": "git", "url": "git+https://github.com/<user>/leerness.git" },
23
- "bugs": { "url": "https://github.com/<user>/leerness/issues" },
24
- "homepage": "https://github.com/<user>/leerness#readme"
25
- ```
26
-
27
- publish 필수는 아니지만, npm 페이지 신뢰도와 사용자 경험을 위해 채워두면 좋습니다.
28
-
29
- ## 2. 권한 확인
30
-
31
- ```bash
32
- npm whoami
33
- # → 응답이 leerness의 owner인지 확인
34
- npm owner ls leerness
35
- # → 자신의 계정이 목록에 있는지 확인
36
- ```
37
-
38
- 권한이 없으면 collaborator 추가를 owner에게 요청해야 합니다.
39
-
40
- ## 3. 로컬 검증
41
-
42
- ```bash
43
- node ./bin/harness.js --version # → 1.9.0
44
- npm pack --dry-run # 패키지 내용 미리보기
45
- node ./scripts/e2e.js # 30+개 시나리오 통과
46
- ```
47
-
48
- ## 4. 1.8.0 → 1.9.0 자동 업그레이드 시연 (선택)
49
-
50
- ```bash
51
- mkdir /tmp/lr-old; cd /tmp/lr-old
52
- npx -y leerness@1.8.0 init . --language ko --skills recommended
53
- LEERNESS_OFFLINE=1 npx -y -p /path/to/leerness-1.9.0.tgz leerness update . --yes
54
- cat .harness/HARNESS_VERSION # → 1.9.0
55
- ```
56
-
57
- ## 5. publish dry-run
58
-
59
- ```bash
60
- npm publish --dry-run
61
- # 또는
62
- npm publish leerness-1.9.0.tgz --dry-run
63
- ```
64
-
65
- 확인:
66
- - `package.json#files`와 실제 tarball 내용 일치
67
- - 총 크기 (≈30~40 kB)
68
- - bin → harness.js 매핑
69
-
70
- ## 6. 실 publish
71
-
72
- ```bash
73
- npm login # 권한 있는 계정으로
74
- npm publish --access public
75
- ```
76
-
77
- ## 7. publish 후 검증
78
-
79
- ```bash
80
- npm view leerness version # → 1.9.0
81
- npx -y leerness@latest --version # → 1.9.0
82
- mkdir test-install && cd test-install
83
- npx -y leerness@latest init . --yes --language ko --skills recommended
84
- npx -y leerness@latest verify .
85
- ```
86
-
87
- ## 8. 롤백
88
-
89
- 24시간 이내라면 `npm unpublish leerness@1.9.0` 가능. 이후엔 1.9.1 패치 또는 deprecate.
90
-
91
- ```bash
92
- npm deprecate leerness@1.9.0 "Use 1.9.1+"
93
- ```
1
+ # Publish Pre-check (leerness 1.9.0)
2
+
3
+ > ⚠ **Owner 권한 필수**: npm `leerness`의 메인테이너는 `gytlrgpfl <gytlrgpfl96@gmail.com>` 입니다. 이 계정으로 로그인되어 있거나 collaborator로 등록되어 있어야 publish가 통과합니다. 권한이 없으면 `E403 Forbidden`이 떨어집니다.
4
+
5
+ ## 1. 메타데이터 보강 (선택)
6
+
7
+ `package.json`의 다음 필드는 비어 있거나 일반적입니다. 사용자 정보로 채우는 것을 권장합니다.
8
+
9
+ ```json
10
+ {
11
+ "author": "leerness contributors",
12
+ "repository": { /* 비어있음 */ },
13
+ "bugs": { /* 비어있음 */ },
14
+ "homepage": ""
15
+ }
16
+ ```
17
+
18
+ 예:
19
+
20
+ ```json
21
+ "author": "Your Name <you@example.com>",
22
+ "repository": { "type": "git", "url": "git+https://github.com/<user>/leerness.git" },
23
+ "bugs": { "url": "https://github.com/<user>/leerness/issues" },
24
+ "homepage": "https://github.com/<user>/leerness#readme"
25
+ ```
26
+
27
+ publish 필수는 아니지만, npm 페이지 신뢰도와 사용자 경험을 위해 채워두면 좋습니다.
28
+
29
+ ## 2. 권한 확인
30
+
31
+ ```bash
32
+ npm whoami
33
+ # → 응답이 leerness의 owner인지 확인
34
+ npm owner ls leerness
35
+ # → 자신의 계정이 목록에 있는지 확인
36
+ ```
37
+
38
+ 권한이 없으면 collaborator 추가를 owner에게 요청해야 합니다.
39
+
40
+ ## 3. 로컬 검증
41
+
42
+ ```bash
43
+ node ./bin/leerness.js --version # → 1.9.0
44
+ npm pack --dry-run # 패키지 내용 미리보기
45
+ node ./scripts/e2e.js # 30+개 시나리오 통과
46
+ ```
47
+
48
+ ## 4. 1.8.0 → 1.9.0 자동 업그레이드 시연 (선택)
49
+
50
+ ```bash
51
+ mkdir /tmp/lr-old; cd /tmp/lr-old
52
+ npx -y leerness@1.8.0 init . --language ko --skills recommended
53
+ LEERNESS_OFFLINE=1 npx -y -p /path/to/leerness-1.9.0.tgz leerness update . --yes
54
+ cat .harness/HARNESS_VERSION # → 1.9.0
55
+ ```
56
+
57
+ ## 5. publish dry-run
58
+
59
+ ```bash
60
+ npm publish --dry-run
61
+ # 또는
62
+ npm publish leerness-1.9.0.tgz --dry-run
63
+ ```
64
+
65
+ 확인:
66
+ - `package.json#files`와 실제 tarball 내용 일치
67
+ - 총 크기 (≈30~40 kB)
68
+ - bin → leerness.js 매핑
69
+
70
+ ## 6. 실 publish
71
+
72
+ ```bash
73
+ npm login # 권한 있는 계정으로
74
+ npm publish --access public
75
+ ```
76
+
77
+ ## 7. publish 후 검증
78
+
79
+ ```bash
80
+ npm view leerness version # → 1.9.0
81
+ npx -y leerness@latest --version # → 1.9.0
82
+ mkdir test-install && cd test-install
83
+ npx -y leerness@latest init . --yes --language ko --skills recommended
84
+ npx -y leerness@latest verify .
85
+ ```
86
+
87
+ ## 8. 롤백
88
+
89
+ 24시간 이내라면 `npm unpublish leerness@1.9.0` 가능. 이후엔 1.9.1 패치 또는 deprecate.
90
+
91
+ ```bash
92
+ npm deprecate leerness@1.9.0 "Use 1.9.1+"
93
+ ```
@@ -0,0 +1,288 @@
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 } = 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
+ return fail('leerness review-request "<request>" — 사용자 요청 텍스트 필요');
14
+ }
15
+ const t0 = Date.now();
16
+ const text = String(request).trim();
17
+
18
+ // 1) 작업 유형 추정 (route 기반 키워드 매핑)
19
+ const lower = text.toLowerCase();
20
+ const routeKw = {
21
+ bugfix: ['버그', '오류', '에러', '수정', '고쳐', '실패', 'fix', 'bug', 'error'],
22
+ refactor: ['리팩토', '재구성', '정리', '개선', 'refactor', 'cleanup'],
23
+ feature: ['추가', '구현', '만들', '새', '기능', 'add', 'implement', 'feature', 'create', 'new'],
24
+ research: ['조사', '분석', '비교', '검토', '연구', 'research', 'analyze', 'compare', 'investigate'],
25
+ planning: ['계획', '설계', '로드맵', 'plan', 'design', 'architecture', 'roadmap'],
26
+ release: ['배포', '릴리즈', '버전', 'release', 'deploy', 'publish'],
27
+ consistency: ['일관성', '통합', '동기화', '맞춰', 'consistency', 'sync', 'align']
28
+ };
29
+ let estimatedType = 'feature'; // default
30
+ let maxScore = 0;
31
+ for (const [type, kws] of Object.entries(routeKw)) {
32
+ const score = kws.filter(k => lower.includes(k)).length;
33
+ if (score > maxScore) { maxScore = score; estimatedType = type; }
34
+ }
35
+
36
+ // 2) 기존 자원 회수 — brainstorm spawn (모든 surface 통합 회수)
37
+ const conflictHints = []; // ⚠ 같은 키워드 + 실패/오류 패턴
38
+ const reuseCandidates = []; // 🔁 기존 skill / reuse-map / decision 후보
39
+ const lessonsRecall = []; // 🧠 과거 lesson
40
+ const planConflicts = []; // 📋 진행 중 milestone과 충돌 가능
41
+
42
+ // brainstorm 호출 (1.9.13~) — JSON 결과 회수
43
+ try {
44
+ const r = cp.spawnSync(process.execPath, [harnessPath, 'brainstorm', text, '--path', root, '--json'], {
45
+ encoding: 'utf8', timeout: 12000,
46
+ env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_BANNER: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' }
47
+ });
48
+ if (r.stdout) {
49
+ const j = JSON.parse(r.stdout);
50
+ const hits = j.hits || {};
51
+ // decisions — 과거 결정 후보
52
+ (hits.decisions || []).slice(0, 5).forEach(d => {
53
+ lessonsRecall.push({ kind: 'decision', title: d.title, line: d.line, preview: (d.preview || '').slice(0, 100) });
54
+ });
55
+ // lessons — 과거 교훈 (특히 실패 키워드)
56
+ (hits.lessons || []).slice(0, 5).forEach(l => {
57
+ const preview = (l.text || l.preview || '').slice(0, 100);
58
+ const isFailure = /실패|오류|에러|fail|error|bug|문제|warning/i.test(preview);
59
+ if (isFailure) {
60
+ conflictHints.push({ kind: 'lesson-failure', preview, tags: l.tags });
61
+ } else {
62
+ lessonsRecall.push({ kind: 'lesson', preview, tags: l.tags });
63
+ }
64
+ });
65
+ // skills — 기존 skill 후보
66
+ (hits.skills || []).slice(0, 3).forEach(s => {
67
+ reuseCandidates.push({ kind: 'skill', id: s.id, displayNameKo: s.displayNameKo, capabilities: s.capabilities });
68
+ });
69
+ // tasks — 진행 중 task 충돌
70
+ (hits.tasks || []).slice(0, 3).forEach(tsk => {
71
+ if (tsk.status && /in-progress|진행/.test(String(tsk.status))) {
72
+ conflictHints.push({ kind: 'task-in-progress', id: tsk.id, title: tsk.title });
73
+ }
74
+ });
75
+ // plan milestones — 진행 중 milestone
76
+ (hits.planMilestones || []).slice(0, 3).forEach(m => {
77
+ if (m.status && /in-progress|진행/.test(String(m.status))) {
78
+ planConflicts.push({ kind: 'milestone-in-progress', id: m.id, title: m.title });
79
+ }
80
+ });
81
+ // taskLogFails — 과거 같은 키워드 실패 흔적
82
+ (hits.taskLogFails || []).slice(0, 3).forEach(f => {
83
+ conflictHints.push({ kind: 'task-log-failure', preview: (f.preview || f.text || '').slice(0, 100) });
84
+ });
85
+ }
86
+ } catch {}
87
+
88
+ // 3) reuse-map 매칭 — 기존 capability 등록 후보
89
+ try {
90
+ const reusePath = path.join(root, '.harness/reuse-map.md');
91
+ if (exists(reusePath)) {
92
+ const reuseLines = read(reusePath).split('\n');
93
+ const tokens = lower.split(/\s+/).filter(t => t.length >= 3);
94
+ for (const line of reuseLines) {
95
+ if (!/^\| /.test(line)) continue; // 테이블 row만
96
+ const ll = line.toLowerCase();
97
+ const matched = tokens.filter(t => ll.includes(t)).length;
98
+ if (matched > 0) {
99
+ const cols = line.split('|').map(s => s.trim());
100
+ if (cols[1]) {
101
+ reuseCandidates.push({ kind: 'reuse-map', capability: cols[1], where: cols[2] || '', note: cols[3] || '' });
102
+ }
103
+ }
104
+ }
105
+ }
106
+ } catch {}
107
+
108
+ // 4) feature_graph — 같은 영역 변경 가능성
109
+ const featureConflicts = [];
110
+ try {
111
+ const fgPath = path.join(root, '.harness/feature_graph.md');
112
+ if (exists(fgPath)) {
113
+ const fg = read(fgPath);
114
+ const tokens = lower.split(/\s+/).filter(t => t.length >= 4);
115
+ // F-XXXX 노드 라인 추출
116
+ const nodeBlocks = fg.split(/\n### /);
117
+ for (const blk of nodeBlocks.slice(1)) {
118
+ const bl = blk.toLowerCase();
119
+ const matched = tokens.filter(t => bl.includes(t)).length;
120
+ if (matched > 0) {
121
+ const titleMatch = blk.match(/^([^\n]+)/);
122
+ const idMatch = blk.match(/F-\d+/);
123
+ if (titleMatch && idMatch) {
124
+ featureConflicts.push({ kind: 'feature', id: idMatch[0], title: titleMatch[1].trim() });
125
+ }
126
+ }
127
+ }
128
+ }
129
+ } catch {}
130
+
131
+ // 5) 권장 단계 (작업 유형별)
132
+ const recommendedSteps = {
133
+ feature: [
134
+ '1) leerness reuse-check "<기능>" — 외부 OSS 빌드 vs 재사용 판단 (1.9.285)',
135
+ '2) leerness reuse find "<핵심 capability>" — 내부 중복 구현 사전 차단',
136
+ '3) leerness plan add "<milestone>" — 진행 추적',
137
+ '4) leerness contract verify SPEC.md src/<mod>.js — 사양 ↔ 구현 일치 검증',
138
+ '5) verify-claim --run-tests 로 evidence 의무화'
139
+ ],
140
+ bugfix: [
141
+ '1) leerness brainstorm "<버그 키워드>" — 과거 같은 영역 lesson 회수',
142
+ '2) leerness verify-claim T-XXX --strict-claims — 낙관적 표시 사전 감지',
143
+ '3) verify-code --run-tests — 재현 + fix 검증',
144
+ '4) leerness lesson save "<root cause>" — 같은 실수 재발 차단'
145
+ ],
146
+ refactor: [
147
+ '1) leerness reuse-map — 영향 범위 파악',
148
+ '2) leerness impact <file> — 강한/약한 참조 분리',
149
+ '3) leerness contract verify — 외부 인터페이스 보존 확인',
150
+ '4) verify-code --run-tests + 회귀 테스트'
151
+ ],
152
+ research: [
153
+ '1) leerness brainstorm "<주제>" — 누적 컨텍스트 회수',
154
+ '2) leerness lessons --query "<주제>" — 과거 같은 영역 결정',
155
+ '3) leerness review <file> --persona research — 깊이 검토',
156
+ '4) leerness decision add "<결론>" — 회수 가능하게 영구화'
157
+ ],
158
+ planning: [
159
+ '1) leerness plan add "<milestone>" — 분해 시작',
160
+ '2) leerness reuse-map — 기존 자원 인벤토리',
161
+ '3) leerness agents recommend planning — sub-agent 분배',
162
+ '4) leerness session close — 결정 영구화'
163
+ ],
164
+ release: [
165
+ '1) leerness health — production-ready 확인',
166
+ '2) leerness audit + verify-code — 보안 + 검수',
167
+ '3) leerness release bump + note + publish'
168
+ ],
169
+ consistency: [
170
+ '1) leerness audit — design/reuse/handoff 일관성 검사',
171
+ '2) leerness consistency check — 잠재 일관성 위반',
172
+ '3) leerness drift check --auto-fix — 자동 회복'
173
+ ]
174
+ }[estimatedType] || [];
175
+
176
+ // 6) 효율 제안 (적용 가능한 sub-agent + skill)
177
+ const efficiencyHints = [];
178
+ if (reuseCandidates.length > 0) {
179
+ efficiencyHints.push(`🔁 기존 자원 ${reuseCandidates.length}건 발견 — 신규 구현 전 재사용 검토 권장`);
180
+ }
181
+ if (conflictHints.length > 0) {
182
+ efficiencyHints.push(`⚠ 충돌 신호 ${conflictHints.length}건 — 과거 실패 lesson / 진행 중 task 확인 필요`);
183
+ }
184
+ if (planConflicts.length > 0) {
185
+ efficiencyHints.push(`📋 진행 중 milestone ${planConflicts.length}건과 영역 겹침 가능 — plan 정렬 권장`);
186
+ }
187
+ if (featureConflicts.length > 0) {
188
+ efficiencyHints.push(`🕸 Feature Graph ${featureConflicts.length}건 영역 겹침 — 의존성 사전 확인`);
189
+ }
190
+ // 다중 에이전트 분배 추천
191
+ if (estimatedType === 'feature' || estimatedType === 'planning') {
192
+ efficiencyHints.push(`👥 leerness agents recommend ${estimatedType} — 작업 유형별 sub-agent 매핑 활용 가능`);
193
+ }
194
+ if (efficiencyHints.length === 0) {
195
+ efficiencyHints.push('✨ 충돌 신호 없음 — 즉시 진행 안전');
196
+ }
197
+
198
+ // 6.5) 1.9.208: 플랫폼/API 제약 사전 체크 — 사용자 명시 ("호출속도 초당 5회" 같은 규정 사전 확인)
199
+ let constraintsCheck = { matched: [], suggestions: [] };
200
+ try {
201
+ constraintsCheck = _checkRequestConstraints(root, text);
202
+ if (constraintsCheck.matched.length > 0) {
203
+ efficiencyHints.push(`⚠ 플랫폼 제약 ${constraintsCheck.matched.length}건 — leerness constraints check 로 상세 확인`);
204
+ }
205
+ } catch {}
206
+
207
+ // 7) proceed 권장 (충돌 critical 시 false)
208
+ const proceed = conflictHints.length < 3 && planConflicts.length === 0;
209
+
210
+ const dt = Date.now() - t0;
211
+ const out = {
212
+ request: text,
213
+ estimatedType,
214
+ conflicts: conflictHints,
215
+ reuseCandidates,
216
+ lessonsRecall,
217
+ planConflicts,
218
+ featureConflicts,
219
+ recommendedSteps,
220
+ efficiencyHints,
221
+ platformConstraints: constraintsCheck.matched,
222
+ constraintSuggestions: constraintsCheck.suggestions,
223
+ proceed,
224
+ proceedReason: proceed ? '안전 — 충돌 신호 < 3 + plan 충돌 0' : '⚠ 충돌 critical — 사용자 확인 후 진행',
225
+ durationMs: dt
226
+ };
227
+
228
+ try { _recordRun(root, { kind: 'review_request', estimatedType, conflicts: conflictHints.length, reuse: reuseCandidates.length, durationMs: dt, ok: true }); } catch {}
229
+
230
+ if (has('--json')) {
231
+ log(JSON.stringify(out, null, 2));
232
+ return;
233
+ }
234
+
235
+ log(`# leerness review-request (1.9.176 사전 검토)`);
236
+ log(`요청: "${text.slice(0, 200)}${text.length > 200 ? '…' : ''}"`);
237
+ log(`추정 작업 유형: ${estimatedType}`);
238
+ log('');
239
+ if (conflictHints.length) {
240
+ log(`## ⚠ 충돌 신호 (${conflictHints.length})`);
241
+ conflictHints.slice(0, 5).forEach(c => log(` - [${c.kind}] ${c.title || c.id || ''} ${c.preview || ''}`.trim()));
242
+ log('');
243
+ }
244
+ if (reuseCandidates.length) {
245
+ log(`## 🔁 재사용 후보 (${reuseCandidates.length}) — 신규 구현 전 검토`);
246
+ reuseCandidates.slice(0, 5).forEach(r => {
247
+ if (r.kind === 'skill') log(` - [skill] ${r.id}${r.displayNameKo ? ' · ' + r.displayNameKo : ''}`);
248
+ else if (r.kind === 'reuse-map') log(` - [reuse] ${r.capability} @ ${r.where}`);
249
+ });
250
+ log('');
251
+ }
252
+ if (lessonsRecall.length) {
253
+ log(`## 🧠 과거 컨텍스트 (${lessonsRecall.length}) — 관련 결정/교훈`);
254
+ lessonsRecall.slice(0, 3).forEach(l => log(` - [${l.kind}] ${l.title || l.preview}`));
255
+ log('');
256
+ }
257
+ if (planConflicts.length || featureConflicts.length) {
258
+ log(`## 📋 진행 중 영역 (${planConflicts.length + featureConflicts.length})`);
259
+ planConflicts.forEach(m => log(` - [milestone] ${m.id}: ${m.title}`));
260
+ featureConflicts.slice(0, 5).forEach(f => log(` - [feature] ${f.id}: ${f.title}`));
261
+ log('');
262
+ }
263
+ log(`## 💡 효율 제안`);
264
+ efficiencyHints.forEach(h => log(` ${h}`));
265
+ log('');
266
+ // 1.9.208: 플랫폼/API 제약 사전 노출 (사용자 명시)
267
+ if (constraintsCheck.matched.length > 0) {
268
+ log(`## 🚦 플랫폼/API 제약 사전 체크 (${constraintsCheck.matched.length})`);
269
+ for (const m of constraintsCheck.matched) {
270
+ log(` - 📦 ${m.platform} (docs: ${m.docs || '-'})`);
271
+ for (const c of (m.constraints || []).slice(0, 3)) {
272
+ log(` • [${c.kind}] ${c.detail}`);
273
+ }
274
+ }
275
+ log(` → leerness constraints check "${text.slice(0, 40)}…" 로 상세 확인`);
276
+ log('');
277
+ }
278
+ if (recommendedSteps.length) {
279
+ log(`## 📍 권장 단계 (${estimatedType})`);
280
+ recommendedSteps.forEach(s => log(` ${s}`));
281
+ log('');
282
+ }
283
+ log(`## ▶ 진행 권장: ${proceed ? '✓ 진행 안전' : '⚠ 사용자 확인 필요'}`);
284
+ log(` 사유: ${out.proceedReason}`);
285
+ log(` 분석 소요: ${dt}ms`);
286
+ }
287
+
288
+ module.exports = { reviewRequestCmd };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.418",
3
+ "version": "1.9.420",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
@@ -28,13 +28,13 @@
28
28
  "license": "MIT",
29
29
  "author": "leerness contributors",
30
30
  "type": "commonjs",
31
- "main": "bin/harness.js",
31
+ "main": "bin/leerness.js",
32
32
  "preferGlobal": true,
33
33
  "engines": {
34
34
  "node": ">=18"
35
35
  },
36
36
  "bin": {
37
- "leerness": "bin/harness.js"
37
+ "leerness": "bin/leerness.js"
38
38
  },
39
39
  "files": [
40
40
  "bin",
@@ -47,10 +47,10 @@
47
47
  "LICENSE"
48
48
  ],
49
49
  "scripts": {
50
- "test": "node ./bin/harness.js --version && node ./bin/harness.js selftest && node ./scripts/e2e.js",
51
- "test:fast": "node ./bin/harness.js selftest && node ./scripts/smoke.js",
50
+ "test": "node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e.js",
51
+ "test:fast": "node ./bin/leerness.js selftest && node ./scripts/smoke.js",
52
52
  "test:smoke": "node ./scripts/e2e.js",
53
- "prepack": "node ./bin/harness.js readme sync . && node ./bin/harness.js --version"
53
+ "prepack": "node ./bin/leerness.js readme sync . && node ./bin/leerness.js --version"
54
54
  },
55
55
  "publishConfig": {
56
56
  "access": "public"
package/scripts/e2e.js CHANGED
@@ -11,7 +11,7 @@ process.env.LEERNESS_OFFLINE = process.env.LEERNESS_OFFLINE || '1';
11
11
  // 1.9.284 (UR-0029): e2e 속도 — 기본 roadmap.html(70KB HTML) 자동 생성 OFF (roadmap 전용 테스트 블록만 일시 ON).
12
12
  // 대부분의 init/session 테스트는 roadmap 을 검증하지 않으므로 생성 비용 제거 → 5분 내 완료.
13
13
  process.env.LEERNESS_NO_AUTO_ROADMAP = '1';
14
- const CLI = path.resolve(__dirname, '..', 'bin', 'harness.js');
14
+ const CLI = path.resolve(__dirname, '..', 'bin', 'leerness.js');
15
15
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-e2e-'));
16
16
  let failed = 0; let total = 0;
17
17
  const _e2eStart = Date.now(); // 1.9.284 (UR-0029): 총 소요시간 투명성
@@ -3081,7 +3081,7 @@ total++;
3081
3081
  {
3082
3082
  let ok = false;
3083
3083
  try {
3084
- const h = require(path.resolve(__dirname, '..', 'bin', 'harness.js'));
3084
+ const h = require(path.resolve(__dirname, '..', 'bin', 'leerness.js'));
3085
3085
  const q = h._shellQuoteArg('a; rm -rf / && echo $(whoami)');
3086
3086
  const win = process.platform === 'win32';
3087
3087
  // 따옴표로 감싸 메타문자가 단일 리터럴 인자가 됨 (POSIX 단일/Windows 이중)
@@ -3104,7 +3104,7 @@ total++;
3104
3104
  "const survived=process.listeners('warning').includes(L);" +
3105
3105
  "const polluted=(process.env.NODE_OPTIONS||'')!==o;" +
3106
3106
  "process.exit(survived&&!polluted?0:1);";
3107
- const harnessPath = path.resolve(__dirname, '..', 'bin', 'harness.js');
3107
+ const harnessPath = path.resolve(__dirname, '..', 'bin', 'leerness.js');
3108
3108
  const r = cp.spawnSync(process.execPath, ['-e', probe, harnessPath], { encoding: 'utf8', timeout: 20000 });
3109
3109
  ok = r.status === 0;
3110
3110
  } catch {}
@@ -3118,14 +3118,14 @@ total++;
3118
3118
  let ok = false;
3119
3119
  try {
3120
3120
  const reg = require(path.resolve(__dirname, '..', 'lib', 'agent-registry.js'));
3121
- const h = require(path.resolve(__dirname, '..', 'bin', 'harness.js'));
3121
+ const h = require(path.resolve(__dirname, '..', 'bin', 'leerness.js'));
3122
3122
  const dataOk = Array.isArray(reg.EXTERNAL_AGENTS) && reg.EXTERNAL_AGENTS.length === 10 &&
3123
3123
  reg.EXTERNAL_AGENTS.every(a => a.id && a.bin && a.envFlag) &&
3124
3124
  reg.AGENT_SLASH_COMMANDS && Object.keys(reg.AGENT_SLASH_COMMANDS).length === 9;
3125
3125
  // harness 가 모듈을 단일출처로 사용 (같은 객체 참조)
3126
3126
  const singleSource = h.AGENT_SLASH_COMMANDS === reg.AGENT_SLASH_COMMANDS;
3127
3127
  // harness.js 소스에 인라인 정의가 더 이상 없음 (모듈로 이동 완료)
3128
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
3128
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
3129
3129
  const movedOut = !/const EXTERNAL_AGENTS = \[/.test(harnessSrc) && /require\('\.\.\/lib\/agent-registry'\)/.test(harnessSrc);
3130
3130
  ok = dataOk && singleSource && movedOut;
3131
3131
  } catch {}
@@ -3143,7 +3143,7 @@ total++;
3143
3143
  const structOk = r.status === 0 && j.schemaVersion === 1 && !!j.version && !!j.project && ('currentTask' in j) &&
3144
3144
  j.openRequests && typeof j.openRequests.count === 'number' && Array.isArray(j.recentDecisions) &&
3145
3145
  Array.isArray(j.activeRules) && Array.isArray(j.nextActions) && j.memory && typeof j.memory.rulesActive === 'number';
3146
- const h = require(path.resolve(__dirname, '..', 'bin', 'harness.js'));
3146
+ const h = require(path.resolve(__dirname, '..', 'bin', 'leerness.js'));
3147
3147
  const mcpOk = h._mcpToolCount && h._mcpToolCount() >= 80;
3148
3148
  ok = structOk && mcpOk;
3149
3149
  } catch {}
@@ -3188,7 +3188,7 @@ total++;
3188
3188
  reg._PROVIDER_MODEL_CATALOG && Object.keys(reg._PROVIDER_MODEL_CATALOG).length === 10 &&
3189
3189
  reg._ROLE_ALIASES && Object.keys(reg._ROLE_ALIASES).length >= 14 &&
3190
3190
  reg._AGENT_ROLE_PROMPTS && reg._AGENT_ROLE_PROMPTS.actor;
3191
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
3191
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
3192
3192
  const movedOut = !/const ROLE_CATALOG = \{/.test(harnessSrc) && /require\('\.\.\/lib\/role-catalog'\)/.test(harnessSrc);
3193
3193
  // roles 명령이 여전히 동작 (모듈 require 후) — 회귀 방지
3194
3194
  const rr = cp.spawnSync(process.execPath, [CLI, 'roles', 'list', '--path', tmp, '--json'], { cwd: tmp, encoding: 'utf8', timeout: 20000 });
@@ -3210,7 +3210,7 @@ total++;
3210
3210
  reg.ADAPTERS && Object.keys(reg.ADAPTERS).length === 9 &&
3211
3211
  Array.isArray(reg.REUSE_CATEGORIES) && reg.REUSE_CATEGORIES.length === 15 &&
3212
3212
  Array.isArray(reg.REUSE_CHECKLIST) && reg.REUSE_CHECKLIST.length === 6;
3213
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
3213
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
3214
3214
  const movedOut = !/const CAPABILITY_SURFACE = \{/.test(harnessSrc) && !/const ADAPTERS = \{/.test(harnessSrc) && /require\('\.\.\/lib\/catalogs'\)/.test(harnessSrc);
3215
3215
  // 소비 명령 회귀: capabilities + reuse-check (카탈로그 require 후 동작)
3216
3216
  const cap = cp.spawnSync(process.execPath, [CLI, 'capabilities', '--json'], { cwd: tmp, encoding: 'utf8', timeout: 20000 });
@@ -3253,12 +3253,12 @@ total++;
3253
3253
  try {
3254
3254
  const T = require(path.resolve(__dirname, '..', 'lib', 'mcp-tools.js'));
3255
3255
  const dataOk = Array.isArray(T) && T.length >= 81 && T.every(t => t.name && t.description && t.inputSchema) && T[0].name === 'leerness_handoff';
3256
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
3256
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
3257
3257
  const movedOut = !/const TOOLS = \[/.test(harnessSrc) && /require\('\.\.\/lib\/mcp-tools'\)/.test(harnessSrc);
3258
3258
  // tools/list(라이브 MCP) == 모듈 length (단일출처 일치)
3259
3259
  const ml = cp.spawnSync(process.execPath, [CLI, 'mcp', 'serve'], { cwd: tmp, encoding: 'utf8', timeout: 15000, input: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }) + '\n' });
3260
3260
  const live = JSON.parse(ml.stdout.split('\n').filter(Boolean)[0]).result.tools.length;
3261
- const h = require(path.resolve(__dirname, '..', 'bin', 'harness.js'));
3261
+ const h = require(path.resolve(__dirname, '..', 'bin', 'leerness.js'));
3262
3262
  const singleSource = live === T.length && h._mcpToolCount() === T.length;
3263
3263
  ok = dataOk && movedOut && singleSource;
3264
3264
  } catch {}
@@ -3325,7 +3325,7 @@ total++;
3325
3325
  {
3326
3326
  let ok = false;
3327
3327
  try {
3328
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
3328
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
3329
3329
  // (1) 소스: cp.exec 템플릿 제거 + execFile args 배열(view pkg version) + argList 인용
3330
3330
  const srcOk = /'view', pkg, 'version'/.test(harnessSrc) && // 1.9.360(CV-2/UR-0077): cmd.exe /d /s /c npm view (args 배열) 형태
3331
3331
  !/cp\.exec\(.npm view \$\{pkg\}/.test(harnessSrc) &&
@@ -3442,7 +3442,7 @@ total++;
3442
3442
  a._evidenceQuality('src/api.js 수정, 12/12 통과 (Exit: 0)').ok === true &&
3443
3443
  a._shellGuardAnalyze('a && b', { shell: 'powershell', psVersion: '5' }).issues.some(i => i.rule === 'ps5-chain') &&
3444
3444
  a._claimFileInGit('src/api.js', new Set(['src/api.js'])) === true;
3445
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
3445
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
3446
3446
  const movedOut = !/function _evidenceQuality\(evidence\) \{/.test(harnessSrc) && !/function _shellGuardAnalyze\(cmd, ctx\) \{/.test(harnessSrc) && /require\('\.\.\/lib\/analyzers'\)/.test(harnessSrc);
3447
3447
  // 소비 명령 회귀: shell-guard (_shellGuardAnalyze 사용)
3448
3448
  const sg = cp.spawnSync(process.execPath, [CLI, 'shell-guard', 'a && b', '--json'], { cwd: tmp, encoding: 'utf8', timeout: 20000 });
@@ -3787,7 +3787,7 @@ total++;
3787
3787
  const work = m._htmlToText('<p>a <b>b</b></p>') === 'a b'
3788
3788
  && m._extractTitle('<title>T &amp; U</title>') === 'T & U'
3789
3789
  && m._extractLinks('<a href="/x">x</a><a href="https://o.com/y">y</a>', 'https://h.com/').length === 1; // same-domain only
3790
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
3790
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
3791
3791
  const movedOut = !/function _htmlToText\(html\) \{/.test(harnessSrc) && harnessSrc.includes('_htmlToText, _extractTitle, _extractLinks') && /require\('\.\.\/lib\/pure-utils'\)/.test(harnessSrc); // 1.9.324: import 순서 비의존(이후 import 추가 허용)
3792
3792
  const r = cp.spawnSync(process.execPath, [CLI, 'api-skill'], { encoding: 'utf8', timeout: 15000 }); // 소비 명령 로드
3793
3793
  const cmdOk = /api-skill/.test(r.stdout || '');
@@ -3900,7 +3900,7 @@ total++;
3900
3900
  const fnOk = typeof m._countDatedBlocks === 'function' && typeof m._extractDecisionBlocks === 'function';
3901
3901
  const work = m._countDatedBlocks('```md\n### 2026-01-01 — T\n```\n### 2026-06-05 — R\n') === 1
3902
3902
  && m._extractDecisionBlocks('### 2026-06-05 — A\n- Decision: x\n').length === 1;
3903
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
3903
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
3904
3904
  // 1.9.325: import 순서 비의존 — pure-utils 구조분해 블록을 추출해 이름 포함 확인(이후 import 추가 허용)
3905
3905
  const _puImport = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
3906
3906
  const movedOut = !/function _countDatedBlocks\(/.test(harnessSrc) && !/function _compareSemver\(/.test(harnessSrc)
@@ -3928,7 +3928,7 @@ total++;
3928
3928
  const work = m._classifyIntent('정확히 그것만').intent === 'precise'
3929
3929
  && m._classifyIntent('전체 다양한 기능').intent === 'broad'
3930
3930
  && m._classifyIntent('로그인 구현').intent === 'default';
3931
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
3931
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
3932
3932
  // import 순서 비의존: pure-utils 구조분해 블록 추출 후 이름 포함 확인
3933
3933
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
3934
3934
  const movedOut = !/function _classifyIntent\(/.test(harnessSrc) && _puImp.includes('_classifyIntent');
@@ -3955,7 +3955,7 @@ total++;
3955
3955
  && m._detectPwshFromEnv({ POWERSHELL_DISTRIBUTION_CHANNEL: 'X' }).version === '7'
3956
3956
  && m._detectPwshFromEnv({}).isPowerShell === false
3957
3957
  && /^['"]/.test(m._shellQuoteArg('a b'));
3958
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
3958
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
3959
3959
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0]; // import 순서 비의존
3960
3960
  const movedOut = !/function _sanitizeFences\(/.test(harnessSrc) && !/function _shellQuoteArg\(/.test(harnessSrc) && !/function _detectPwshFromEnv\(/.test(harnessSrc)
3961
3961
  && _puImp.includes('_sanitizeFences') && _puImp.includes('_shellQuoteArg') && _puImp.includes('_detectPwshFromEnv');
@@ -3978,7 +3978,7 @@ total++;
3978
3978
  const work = m._formatLocal('2026-06-05T01:13:00.000Z', { tz: 'Asia/Seoul' }) === '2026-06-05 10:13 KST' // UTC→KST +9h
3979
3979
  && m._formatLocal('2026-06-05T01:13:00.000Z', { tz: 'Asia/Seoul', dateOnly: true }) === '2026-06-05'
3980
3980
  && m._formatLocal('') === '?' && typeof m._getLocalTz() === 'string';
3981
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
3981
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
3982
3982
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0]; // import 순서 비의존
3983
3983
  const movedOut = !/function _formatLocal\(/.test(harnessSrc) && !/function _getLocalTz\(/.test(harnessSrc)
3984
3984
  && _puImp.includes('_getLocalTz') && _puImp.includes('_formatLocal');
@@ -3997,7 +3997,7 @@ total++;
3997
3997
  const work = typeof m._truncate === 'function' && typeof m._splitList === 'function'
3998
3998
  && m._truncate('hello world', 8) === 'hello w…' && m._truncate('hi', 8) === 'hi'
3999
3999
  && JSON.stringify(m._splitList('a, b ,c,')) === '["a","b","c"]';
4000
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4000
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4001
4001
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0]; // import 순서 비의존
4002
4002
  const movedOut = !/function _truncate\(/.test(harnessSrc) && !/function _splitList\(/.test(harnessSrc)
4003
4003
  && _puImp.includes('_truncate') && _puImp.includes('_splitList');
@@ -4017,7 +4017,7 @@ total++;
4017
4017
  && m._roadmapMapStatus('REQUESTED') === 'planned' && m._roadmapMapStatus('done') === 'done'
4018
4018
  && m._roadmapParseMilestones('### M-0001. 로그인\nStatus: in-progress\nProgress: 40%')[0].progress === 40
4019
4019
  && m._roadmapParseTokens('| color | #fff |').color === '#fff';
4020
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4020
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4021
4021
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0]; // import 순서 비의존
4022
4022
  const movedOut = !/function _roadmapMapStatus\(/.test(harnessSrc) && !/function _roadmapParseMilestones\(/.test(harnessSrc) && !/function _roadmapParseTokens\(/.test(harnessSrc)
4023
4023
  && _puImp.includes('_roadmapMapStatus') && _puImp.includes('_roadmapParseMilestones') && _puImp.includes('_roadmapParseTokens');
@@ -4035,7 +4035,7 @@ total++;
4035
4035
  const m = require(path.resolve(__dirname, '..', 'lib', 'pure-utils.js'));
4036
4036
  const cfgOk = Array.isArray(m._BRIEF_FIELDS) && m._BRIEF_FIELDS.length === 10 && m._BRIEF_FIELDS[0].key === 'intro';
4037
4037
  const work = m._briefFilled({ intro: 'x', features: ['a'] }) === 2 && m._briefFilled({}) === 0;
4038
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4038
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4039
4039
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0]; // import 순서 비의존
4040
4040
  const movedOut = !/const _BRIEF_FIELDS = \[/.test(harnessSrc) && !/function _briefFilled\(/.test(harnessSrc)
4041
4041
  && _puImp.includes('_BRIEF_FIELDS') && _puImp.includes('_briefFilled');
@@ -4062,7 +4062,7 @@ total++;
4062
4062
  const work = typeof m._briefReadmeBlock === 'function' && typeof m._briefBlueprint === 'function'
4063
4063
  && m._briefReadmeBlock(b).includes(m.BRIEF_START) && /f1/.test(m._briefReadmeBlock(b))
4064
4064
  && /Blueprint/.test(m._briefBlueprint(b, '9.9.9')) && /leerness v9\.9\.9/.test(m._briefBlueprint(b, '9.9.9')); // VERSION 주입
4065
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4065
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4066
4066
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0]; // import 순서 비의존
4067
4067
  const movedOut = !/function _briefReadmeBlock\(/.test(harnessSrc) && !/function _briefBlueprint\(/.test(harnessSrc) && !/^const BRIEF_START =/m.test(harnessSrc)
4068
4068
  && _puImp.includes('_briefReadmeBlock') && _puImp.includes('_briefBlueprint') && _puImp.includes('BRIEF_START');
@@ -4088,7 +4088,7 @@ total++;
4088
4088
  const m = require(path.resolve(__dirname, '..', 'lib', 'pure-utils.js'));
4089
4089
  const r = m._parseLessonEntries('### 2026-06-05\n- Lesson: A\n- Tag: t\n\n### 2026-06-04\n- Lesson: B');
4090
4090
  const work = typeof m._parseLessonEntries === 'function' && r.length === 2 && r[0].text === 'A' && r[0].tag === 't' && r[1].tag === null;
4091
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4091
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4092
4092
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0]; // import 순서 비의존
4093
4093
  const movedOut = !/function _parseLessonEntries\(/.test(harnessSrc) && _puImp.includes('_parseLessonEntries');
4094
4094
  // 소비 명령 회귀: lesson save + list --json
@@ -4114,7 +4114,7 @@ total++;
4114
4114
  const catOk = c._DEFAULT_PLATFORM_CONSTRAINTS && Object.keys(c._DEFAULT_PLATFORM_CONSTRAINTS.platforms).length === 6;
4115
4115
  const r = m._matchConstraints(c._DEFAULT_PLATFORM_CONSTRAINTS, 'stripe 결제');
4116
4116
  const work = catOk && r.matched.length === 1 && r.matched[0].platform === 'stripe' && r.totalPlatforms === 6 && m._matchConstraints(null, 'x').matched.length === 0;
4117
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4117
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4118
4118
  // 1.9.334: catalogs import 블록 추출 후 이름 포함 확인(순서/추가 비의존 — 이후 import 추가 허용)
4119
4119
  const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0];
4120
4120
  const movedOut = !/const _DEFAULT_PLATFORM_CONSTRAINTS = \{/.test(harnessSrc) && harnessSrc.includes('_matchConstraints(_loadPlatformConstraints(root), text)')
@@ -4141,7 +4141,7 @@ total++;
4141
4141
  const catOk = c._DEFAULT_DOMAIN_CATALOG && Object.keys(c._DEFAULT_DOMAIN_CATALOG.domains).length === 5;
4142
4142
  const r = m._matchDomain(c._DEFAULT_DOMAIN_CATALOG, 'unity 게임');
4143
4143
  const work = catOk && typeof m._matchDomain === 'function' && r.domain === 'game' && Array.isArray(r.components) && m._matchDomain(null, 'x').domain === null;
4144
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4144
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4145
4145
  const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
4146
4146
  const movedOut = !/const _DEFAULT_DOMAIN_CATALOG = \{/.test(harnessSrc) && harnessSrc.includes('_matchDomain(_loadDomainCatalog(root), text)')
4147
4147
  && _catImp.includes('_DEFAULT_DOMAIN_CATALOG');
@@ -4168,7 +4168,7 @@ total++;
4168
4168
  const langOk = m._detectLspLang('x.py') === 'python' && m._detectLspLang('y.rs') === 'rust' && m._detectLspLang('z.txt') === 'javascript';
4169
4169
  const sy = m._matchLspSymbols(c._LSP_LANG_PATTERNS, 'def foo():\n pass\nclass Bar:\n pass', 'python');
4170
4170
  const work = catOk && langOk && typeof m._matchLspSymbols === 'function' && sy.length === 2 && sy[0].name === 'foo' && sy[0].kind === 'function' && sy[1].name === 'Bar' && m._matchLspSymbols(null, 'x', 'javascript').length === 0;
4171
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4171
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4172
4172
  const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
4173
4173
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
4174
4174
  const movedOut = !/const _LSP_LANG_PATTERNS = \{/.test(harnessSrc) && !/function _detectLspLang\(/.test(harnessSrc)
@@ -4199,7 +4199,7 @@ total++;
4199
4199
  const work = catOk && typeof m._detectOptimism === 'function' && sus.some(s => s.kind === 'API' && s.severity === 'high') && conf < 0.5
4200
4200
  && m._computeConfidence(c.OPTIMISM_PATTERNS, '그냥 정리함', 'x') === 1 && m._detectOptimism(null, ev, 'x').length === 0
4201
4201
  && m._extractUrlClaims('POST /a/b').length === 1 && m._verifyUrlClaim({ path: '/a/b' }, 'has /a/b') === true;
4202
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4202
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4203
4203
  const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
4204
4204
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
4205
4205
  const movedOut = !/const OPTIMISM_PATTERNS = \[/.test(harnessSrc) && !/function _extractUrlClaims\(/.test(harnessSrc)
@@ -4228,7 +4228,7 @@ total++;
4228
4228
  const catOk = c.BUILT_IN_PERSONAS && Object.keys(c.BUILT_IN_PERSONAS).length === 5 && c.BUILT_IN_PERSONAS.security && typeof c.BUILT_IN_PERSONAS.security.body === 'string';
4229
4229
  const sm = m._personaSummaries(c.BUILT_IN_PERSONAS);
4230
4230
  const work = catOk && typeof m._personaSummaries === 'function' && Array.isArray(sm) && sm.length === 5 && sm[0].id === 'security' && sm[0].body === undefined && m._personaSummaries(null).length === 0;
4231
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4231
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4232
4232
  const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
4233
4233
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
4234
4234
  const movedOut = !/const BUILT_IN_PERSONAS = \{/.test(harnessSrc) && _catImp.includes('BUILT_IN_PERSONAS') && _puImp.includes('_personaSummaries');
@@ -4262,7 +4262,7 @@ total++;
4262
4262
  && m._translate(c.STRINGS, 'no.such.key', 'en') === 'no.such.key'
4263
4263
  && m._translate(null, 'x', 'ko') === 'x'
4264
4264
  && m._translate({ k: { ko: '케이' } }, 'k', 'en') === '케이';
4265
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4265
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4266
4266
  const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
4267
4267
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
4268
4268
  // _t 박막: 인라인 STRINGS 정의 제거 + import + _translate(STRINGS,..) 호출
@@ -4378,7 +4378,7 @@ total++;
4378
4378
  const work = catOk && typeof m._withBuiltinSource === 'function' && Object.keys(out).length === 9
4379
4379
  && Object.values(out).every(v => v._source === 'builtin') && Array.isArray(out.office.capabilities)
4380
4380
  && Object.keys(m._withBuiltinSource(null)).length === 0;
4381
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4381
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4382
4382
  const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
4383
4383
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
4384
4384
  const movedOut = !/const BUILTIN_CATALOG = \{/.test(harnessSrc) && _catImp.includes('BUILTIN_CATALOG') && _puImp.includes('_withBuiltinSource')
@@ -4405,7 +4405,7 @@ total++;
4405
4405
  const mapsOk = c.ROADMAP_STATUS_LABEL && c.ROADMAP_STATUS_COLOR
4406
4406
  && Object.keys(c.ROADMAP_STATUS_LABEL).length === 11 && Object.keys(c.ROADMAP_STATUS_COLOR).length === 11
4407
4407
  && c.ROADMAP_STATUS_LABEL.done === '완료' && c.ROADMAP_STATUS_COLOR.done === '#16a34a' && c.ROADMAP_STATUS_COLOR.skill === '#8b5cf6';
4408
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4408
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4409
4409
  const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
4410
4410
  const movedOut = !/const ROADMAP_STATUS_LABEL = \{/.test(harnessSrc) && !/const ROADMAP_STATUS_COLOR = \{/.test(harnessSrc)
4411
4411
  && _catImp.includes('ROADMAP_STATUS_LABEL') && _catImp.includes('ROADMAP_STATUS_COLOR');
@@ -4433,7 +4433,7 @@ total++;
4433
4433
  const hit = (s) => c.SECRET_PATTERNS.some(p => { p.re.lastIndex = 0; return p.re.test(s); });
4434
4434
  const catOk = Array.isArray(c.SECRET_PATTERNS) && c.SECRET_PATTERNS.length === 20
4435
4435
  && hit('AKIA' + 'ABCD1234EFGH5678') && hit('sk-' + 'ant-api03-' + A + '_' + A) && !hit('const u = "john' + '_doe_2024";');
4436
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4436
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4437
4437
  const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
4438
4438
  // 모듈 레벨 정의 제거(블록 지역 .env 배열은 보존) + import
4439
4439
  const movedOut = !/const SECRET_PATTERNS = \[\r?\n\s*\{ name:/.test(harnessSrc) && _catImp.includes('SECRET_PATTERNS')
@@ -4467,7 +4467,7 @@ total++;
4467
4467
  const catOk = c.SKILL_CATALOG_PRESETS && Object.keys(c.SKILL_CATALOG_PRESETS).length === 2
4468
4468
  && c.SKILL_CATALOG_PRESETS.vercel && c.SKILL_CATALOG_PRESETS.vercel.owner === 'vercel-labs'
4469
4469
  && c.SKILL_CATALOG_PRESETS.anthropic && c.SKILL_CATALOG_PRESETS.anthropic.repo === 'skills';
4470
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4470
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4471
4471
  const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
4472
4472
  const movedOut = !/const SKILL_CATALOG_PRESETS = \{/.test(harnessSrc) && _catImp.includes('SKILL_CATALOG_PRESETS');
4473
4473
  // 소비 회귀: skill discover 가 preset 목록을 catalog 에서 노출 (네트워크 없이 unknown preset → 사용가능 목록)
@@ -4493,7 +4493,7 @@ total++;
4493
4493
  && m._esc('&<>"\'') === '&amp;&lt;&gt;&quot;&#39;'
4494
4494
  && m._esc('<script>x</script>') === '&lt;script&gt;x&lt;/script&gt;'
4495
4495
  && m._esc(null) === '' && m._esc(undefined) === '' && m._esc(42) === '42';
4496
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4496
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4497
4497
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
4498
4498
  const movedOut = !/function _esc\(/.test(harnessSrc) && _puImp.includes('_esc');
4499
4499
  // 소비 회귀: roadmap.html 이 악성 task 제목을 이스케이프 (인젝션 방지)
@@ -4521,7 +4521,7 @@ total++;
4521
4521
  const pureOk = typeof m._roadmapTokenStyles === 'function' && out.startsWith(':root {')
4522
4522
  && out.includes('--lr-primary: #2563eb') && out.includes('--lr-surface: #fff') && out.includes('--lr-custom: #abc')
4523
4523
  && out.includes('--lr-card-bg') && out.includes('--lr-page-bg') && m._roadmapTokenStyles(null, null).startsWith(':root {');
4524
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4524
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4525
4525
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
4526
4526
  const movedOut = !/function _roadmapTokenStyles\(/.test(harnessSrc) && _puImp.includes('_roadmapTokenStyles');
4527
4527
  // 소비 회귀: roadmap.html 이 :root CSS 변수 주입
@@ -4548,7 +4548,7 @@ total++;
4548
4548
  const pureOk = typeof m._parseSkillMd === 'function' && r.meta.name === 's1' && r.meta.description === 'd1' && r.body === 'body'
4549
4549
  && m._parseSkillMd('---\nname: b\n---\nx').meta.name === 'b'
4550
4550
  && Object.keys(m._parseSkillMd('plain').meta).length === 0 && m._parseSkillMd(null).body === '';
4551
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4551
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4552
4552
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
4553
4553
  const movedOut = !/function _parseSkillMd\(/.test(harnessSrc) && _puImp.includes('_parseSkillMd');
4554
4554
  // 소비 회귀: skill install 이 BOM 포함 SKILL.md 를 정상 설치 (frontmatter name 파싱)
package/scripts/smoke.js CHANGED
@@ -12,7 +12,7 @@ const path = require('path');
12
12
  const cp = require('child_process');
13
13
 
14
14
  process.env.LEERNESS_OFFLINE = process.env.LEERNESS_OFFLINE || '1';
15
- const CLI = path.resolve(__dirname, '..', 'bin', 'harness.js');
15
+ const CLI = path.resolve(__dirname, '..', 'bin', 'leerness.js');
16
16
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-smoke-'));
17
17
  let failed = 0, total = 0;
18
18
  const t0 = Date.now();