leerness 1.9.422 → 1.9.423

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,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.423 — 2026-06-07 — 무거움 점진 해소 4: healthCmd → lib/health.js 모듈화 (UR-0025/UR-0125)
4
+
5
+ **🪶 `bin/leerness.js` 무거움 점진 해소 4단계 — `health` 종합 진단 핸들러(334줄, 의존 최다)를 lib/ 로 DI 분리.**
6
+
7
+ ### 배경
8
+ 네 번째 큰 핸들러 추출. healthCmd는 drift/security/skill/usage/tasks/memory/featureGraph/roundHistory/milestones/recentChanges/pyFiles/envInfo/apiSkills/capabilityMatrix를 집계하는 **최다 의존(26종)** 핸들러. **사전 종합 의존 스캔**(drift 라운드 교훈)으로 전체 DI 목록을 일괄 확보 → **추출 첫 시도에 누락 deps 0**.
9
+
10
+ ### 변경
11
+ - `lib/health.js` 신설(348줄): `healthCmd(root, deps)` — io는 `./io`, `_parseArchiveBlocks`는 `./pure-utils`, cp/os/path/fs 빌트인, harness 고유 의존 **26종 DI 주입**, `__filename`→`harnessPath`(drift spawn) 변환.
12
+ - `bin/leerness.js`: 334줄 → **3줄 thin wrapper**. **20,318 → 20,008줄(−310)**.
13
+ - 동작/출력 무변경(health 14 통합 필드 + --json + --strict 동일).
14
+
15
+ ### 검증 (회귀 0)
16
+ - **selftest 168→169 PASS** (모듈 + DI 위임 + 본문 이동(capabilityMatrix) + healthy/checks 동작 — 첫 시도 통과).
17
+ - **E2E 422→423 PASS**.
18
+
19
+ ### 누적 효과 (UR-0125) — 4회 추출
20
+ bin **21,177 → 20,008줄(−1,169, 5.5%)**. lib/ 모듈 15개. 남은 최대 후보: handoff(1434줄, 최고결합 — 별도 신중 작업).
21
+
3
22
  ## 1.9.422 — 2026-06-07 — 무거움 점진 해소 3: driftCheckCmd → lib/drift.js 모듈화 (UR-0025/UR-0125)
4
23
 
5
24
  **🪶 `bin/leerness.js` 무거움 점진 해소 3단계 — `drift check` 핸들러(322줄, 내부 재귀 포함)를 lib/ 로 DI 분리.**
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.422-green)]() [![tests](https://img.shields.io/badge/e2e-357%2F357-success)]() [![selftest](https://img.shields.io/badge/selftest-168-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.423-green)]() [![tests](https://img.shields.io/badge/e2e-357%2F357-success)]() [![selftest](https://img.shields.io/badge/selftest-169-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.422 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
474
+ 이 프로젝트는 Leerness v1.9.423 하네스를 사용합니다. 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.422는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
528
+ Leerness v1.9.423는 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.422는 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.422)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
549
+ 현재 누적: **70 라운드 (1.9.40 → 1.9.423)** · 매 라운드 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.422: 2026-06-07
587
+ Last synced by Leerness v1.9.423: 2026-06-07
588
588
  <!-- leerness:project-readme:end -->
589
589
 
package/bin/leerness.js CHANGED
@@ -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.422';
34
+ const VERSION = '1.9.423';
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') 시 호스트 프로세스 오염.
@@ -3073,6 +3073,25 @@ function _selfTestCases() {
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
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 큰핸들러 모듈화 8번째: healthCmd → lib/health.js + DI 위임 + 동작 (1.9.423)', run: () => {
3077
+ const m = require('../lib/health');
3078
+ const expOk = typeof m.healthCmd === 'function';
3079
+ const src = read(__filename);
3080
+ const delegated = src.includes("require('../lib/health')") && src.includes('_health.healthCmd(root,');
3081
+ const modSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'health.js'));
3082
+ const bodyMarker = 'capability' + 'Matrix'; // health 본문 고유(split-literal 자기참조 회피)
3083
+ const movedToLib = modSrc.includes("require('./io')") && modSrc.includes("require('./pure-utils')") && modSrc.includes(bodyMarker) && !src.includes(bodyMarker);
3084
+ let behavOk = false;
3085
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_health_'));
3086
+ const _w = process.stdout.write; let out = '';
3087
+ try {
3088
+ fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true });
3089
+ process.stdout.write = s => { out += s; return true; };
3090
+ m.healthCmd(tmp, { VERSION, has: f => f === '--json', arg: (k, d) => d, harnessPath: path.join(tmp, '__nope.js'), listAllSkills, planPath, readProgressRows, readRules, envDiff, _collectSecretFindings, _readUsageStats, _loadDecisions, _loadLessons, _loadShellFailures, _readFeatureGraph, _scanShellScriptsEncoding, _shellEnvDrift, _computeMilestones, _computeRecentChanges, _computeRoundHistory, _collectPyFiles, _analyzePyFile, _collectRuntimeEnv, _listAPISkills, _matchAPISkills, _mcpToolCount });
3091
+ } catch (e) { out = 'ERR:' + e.message; } finally { process.stdout.write = _w; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
3092
+ try { const j = JSON.parse(out); behavOk = typeof j.healthy === 'boolean' && !!j.checks; } catch {}
3093
+ return expOk && delegated && movedToLib && behavOk;
3094
+ } },
3076
3095
  { name: 'UR-0025 큰핸들러 모듈화 7번째: driftCheckCmd → lib/drift.js + DI 위임 + 재귀/동작 (1.9.422)', run: () => {
3077
3096
  const m = require('../lib/drift');
3078
3097
  const expOk = typeof m.driftCheckCmd === 'function';
@@ -18815,340 +18834,9 @@ async function deployAutoCmd(root, service) {
18815
18834
  }
18816
18835
 
18817
18836
  // 1.9.85: leerness health — 종합 헬스 체크 (drift + 보안 + skill + MCP + 누적)
18818
- function healthCmd(root) {
18819
- root = absRoot(root || process.cwd());
18820
- const out = { root, generatedAt: new Date().toISOString(), checks: {} };
18821
- // 1) drift level
18822
- try {
18823
- const r = cp.spawnSync(process.execPath, [__filename, 'drift', 'check', root, '--json'],
18824
- { encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '0' } });
18825
- const j = JSON.parse(r.stdout.trim());
18826
- out.checks.drift = { level: j.level, score: j.score, firedCount: (j.fired || []).length };
18827
- } catch { out.checks.drift = { error: 'drift check 실패' }; }
18828
- // 2) 보안 상태 (1.9.418, 9th 외부평가 Codex P2): .env/.gitignore + **실제 하드코딩 시크릿 스캔**.
18829
- // 기존엔 .env 가 .gitignore 에 있으면 critical:false 라 커밋된 하드코딩 시크릿이 있어도 health 가 healthy:true 였음(false-OK).
18830
- // handoff/scan secrets 와 동일하게 _collectSecretFindings 로 커밋 대상 시크릿을 반영(정직성).
18831
- try {
18832
- const sec = _collectSecretFindings(root);
18833
- const committedSecrets = sec.committed.length;
18834
- const envPath = path.join(root, '.env');
18835
- const hasDotEnv = exists(envPath);
18836
- const s = { hasDotEnv, committedSecrets };
18837
- if (hasDotEnv) {
18838
- const d = envDiff(root);
18839
- const giText = exists(path.join(root, '.gitignore')) ? read(path.join(root, '.gitignore')) : '';
18840
- const giLines = giText.split('\n').map(l => l.trim());
18841
- const envInGi = giLines.includes('.env') || giLines.includes('/.env');
18842
- const SECRET_PATTERNS = ['.env', '.env.local', '.env.production', '.env.*.local', '*.pem', 'credentials.json'];
18843
- s.envInGitignore = envInGi;
18844
- s.envExampleMissing = d.inEnvOnly;
18845
- s.gitignoreMissingSecrets = SECRET_PATTERNS.filter(p => !giLines.some(l => l === p || l === '/' + p));
18846
- s.critical = !envInGi || committedSecrets > 0;
18847
- } else {
18848
- s.critical = committedSecrets > 0;
18849
- }
18850
- out.checks.security = s;
18851
- } catch { out.checks.security = { error: '보안 점검 실패' }; }
18852
- // 3) skill 수 + skill query 누적
18853
- try {
18854
- const all = listAllSkills(root);
18855
- const skillCount = Object.keys(all).length;
18856
- let queryCount = 0;
18857
- const histPath = path.join(root, '.harness', 'skill-suggestions.md');
18858
- if (exists(histPath)) {
18859
- queryCount = (read(histPath).match(/^## [\d-]+ [\d:]+ — query/gm) || []).length;
18860
- }
18861
- out.checks.skills = { installed: skillCount, queryHistoryCount: queryCount };
18862
- } catch { out.checks.skills = { error: 'skill 점검 실패' }; }
18863
- // 4) MCP + 명령 호출 누적
18864
- try {
18865
- const stats = _readUsageStats(root);
18866
- const cmdTotal = Object.values(stats.commands || {}).reduce((s, n) => s + n, 0);
18867
- const mcpTotal = stats.mcp?.tools ? Object.values(stats.mcp.tools).reduce((s, n) => s + n, 0) : 0;
18868
- out.checks.usage = {
18869
- commandTotal: cmdTotal,
18870
- commandKinds: Object.keys(stats.commands || {}).length,
18871
- mcpTotal,
18872
- mcpToolKinds: stats.mcp?.tools ? Object.keys(stats.mcp.tools).length : 0,
18873
- since: stats.since || null
18874
- };
18875
- } catch { out.checks.usage = { error: 'usage 점검 실패' }; }
18876
- // 5) tasks (progress-tracker)
18877
- try {
18878
- const rows = readProgressRows(root);
18879
- const byStatus = {};
18880
- for (const r of rows) byStatus[r.status] = (byStatus[r.status] || 0) + 1;
18881
- out.checks.tasks = { total: rows.length, byStatus };
18882
- } catch { out.checks.tasks = { error: 'tasks 점검 실패' }; }
18883
- // 1.9.123: memorySurface 통합 (handoff --json 1.9.115 / session close --json 1.9.122 와 동일 패턴)
18884
- try {
18885
- const rows = readProgressRows(root);
18886
- const tasksByStatus = {};
18887
- for (const s of STATUSES) tasksByStatus[s] = 0;
18888
- for (const r of rows) tasksByStatus[r.status] = (tasksByStatus[r.status] || 0) + 1;
18889
- const tasksInProgress = tasksByStatus['in-progress'] || 0;
18890
- const decisionsCount = _loadDecisions(root).length; // 1.9.339 (UR-0053): canonical 단일 진실소스
18891
- const rules = readRules(root);
18892
- const rulesActive = rules.filter(r => r.status === 'active').length;
18893
- const planText = exists(planPath(root)) ? read(planPath(root)) : '';
18894
- const milestones = (planText.match(/^### M-\d{4}\./gm) || []).length;
18895
- const lessonsCount = _loadLessons(root).length;
18896
- out.memorySurface = {
18897
- tasks: { inProgress: tasksInProgress, total: rows.length, byStatus: tasksByStatus },
18898
- decisions: { count: decisionsCount },
18899
- rules: { active: rulesActive, total: rules.length },
18900
- plan: { milestones },
18901
- lessons: { count: lessonsCount },
18902
- archive: (() => {
18903
- // 1.9.130: archive 카운트 통합
18904
- const a = { decisions: 0, lessons: 0, plan: 0, total: 0 };
18905
- try {
18906
- const hdHe = path.join(root, '.harness');
18907
- for (const [key, file] of [['decisions', 'decisions.archive.md'], ['lessons', 'lessons.archive.md'], ['plan', 'plan.archive.md']]) {
18908
- const fpHe = path.join(hdHe, file);
18909
- if (exists(fpHe)) {
18910
- const entries = _parseArchiveBlocks(read(fpHe));
18911
- a[key] = entries.length;
18912
- a.total += entries.length;
18913
- }
18914
- }
18915
- } catch {}
18916
- return a;
18917
- })(),
18918
- summary: `T${tasksInProgress}/D${decisionsCount}/R${rulesActive}/P${milestones}/L${lessonsCount}`,
18919
- };
18920
- } catch { out.memorySurface = { error: 'memorySurface 점검 실패' }; }
18921
- // 1.9.143: health --json featureGraph 통합 (handoff/session close 와 동일 패턴 — JSON 4종 완성)
18922
- try {
18923
- const { nodes: fNodesHe } = _readFeatureGraph(root);
18924
- const edgeCount = fNodesHe.reduce((s, n) => s + (n.dependsOn?.length || 0) + (n.affects?.length || 0) + (n.coChangesWith?.length || 0), 0);
18925
- const linkedSet = new Set();
18926
- for (const n of fNodesHe) {
18927
- for (const x of [...(n.dependsOn||[]), ...(n.affects||[]), ...(n.coChangesWith||[])]) { linkedSet.add(n.id); linkedSet.add(x); }
18928
- }
18929
- const isolated = fNodesHe.length ? (fNodesHe.length - linkedSet.size) : 0;
18930
- out.featureGraph = {
18931
- total: fNodesHe.length,
18932
- edges: edgeCount,
18933
- isolated: Math.max(0, isolated),
18934
- summary: `F${fNodesHe.length}/E${edgeCount}${isolated > 0 ? `/iso${isolated}` : ''}`
18935
- };
18936
- } catch { out.featureGraph = { error: 'featureGraph 점검 실패' }; }
18937
- // 1.9.228: health --json roundHistory 통합 (handoff/session close 와 동일 — JSON 3 명령 일관성 + 6 통합 필드 완성)
18938
- try {
18939
- const rh = _computeRoundHistory(root);
18940
- out.roundHistory = {
18941
- roundCount: rh.roundCount,
18942
- baselineVersion: rh.baselineVersion,
18943
- nextMilestone: rh.nextMilestone,
18944
- roundsToNextMilestone: rh.roundsToNextMilestone,
18945
- daysActive: rh.daysActive,
18946
- avgRoundsPerDay: rh.avgRoundsPerDay
18947
- };
18948
- } catch { out.roundHistory = { error: 'roundHistory 점검 실패' }; }
18949
- // 1.9.230: health --json milestones 통합 (handoff/session close/health 3 명령 일관성 유지)
18950
- try {
18951
- const ms = _computeMilestones(root);
18952
- out.milestones = {
18953
- reachedCount: ms.reached.length,
18954
- reached: ms.reached.map(m => ({ milestone: m.milestone, version: m.version, reachedAt: m.reachedAt })),
18955
- next: ms.next,
18956
- avgRoundsPerDay: ms.avgRoundsPerDay
18957
- };
18958
- } catch { out.milestones = { error: 'milestones 점검 실패' }; }
18959
- // 1.9.234: health --json recentChanges 통합 (3 명령 8 필드 일관성)
18960
- try {
18961
- out.recentChanges = _computeRecentChanges(root, 5);
18962
- } catch { out.recentChanges = { error: 'recentChanges 점검 실패' }; }
18963
- // 1.9.240: health --json pyFiles 통합 (3 명령 9 필드 — UR-0013 2단계)
18964
- try {
18965
- const pyFiles = _collectPyFiles(root, 200);
18966
- const analyses = pyFiles.slice(0, 200).map(f => _analyzePyFile(f)).filter(Boolean);
18967
- out.pyFiles = {
18968
- total: pyFiles.length,
18969
- analyzed: analyses.length,
18970
- totalLOC: analyses.reduce((s, a) => s + a.loc, 0),
18971
- totalImports: analyses.reduce((s, a) => s + a.imports, 0),
18972
- totalFuncs: analyses.reduce((s, a) => s + a.funcs, 0),
18973
- totalClasses: analyses.reduce((s, a) => s + a.classes, 0)
18974
- };
18975
- } catch { out.pyFiles = { error: 'pyFiles 점검 실패' }; }
18976
- // 1.9.242: health --json envInfo 통합 (3 명령 10 필드 — UR-0014 2단계)
18977
- try {
18978
- const runtimeEnv = _collectRuntimeEnv();
18979
- const encScan = _scanShellScriptsEncoding(root);
18980
- out.envInfo = {
18981
- os: runtimeEnv.os.platform,
18982
- isKoreanWindows: runtimeEnv.locale.isKoreanWindows || false,
18983
- codepage: runtimeEnv.locale.codepage || null,
18984
- nodeVersion: runtimeEnv.node.version,
18985
- shellScriptsScanned: encScan.scanned,
18986
- encodingRiskCount: encScan.atRisk.length,
18987
- encodingRiskFiles: encScan.atRisk.slice(0, 5).map(r => r.file),
18988
- // 1.9.249 (UR-0018): 터미널 출력 인코딩 안전 여부 + 자동 회복 결과
18989
- terminalEncodingOk: runtimeEnv.locale.codepage === 65001 || !runtimeEnv.locale.isKoreanWindows,
18990
- autoChcpApplied: process.env._LEERNESS_AUTOCHCP_APPLIED || null,
18991
- // 1.9.250 (UR-0018 2단계): POSIX (Linux/macOS/WSL) terminal encoding 점검
18992
- posixEncodingOk: runtimeEnv.locale.posixEncodingOk,
18993
- isWSL: runtimeEnv.locale.isWSL || false
18994
- };
18995
- } catch { out.envInfo = { error: 'envInfo 점검 실패' }; }
18996
- // 1.9.245: health --json apiSkills 통합 (3 명령 11 필드 — UR-0015)
18997
- try {
18998
- const allSkills = _listAPISkills(root);
18999
- let currentTaskText = '';
19000
- try {
19001
- const rows = readProgressRows(root);
19002
- const ip = rows.find(r => r.status === 'in-progress');
19003
- if (ip) currentTaskText = (ip.title || '') + ' ' + (ip.notes || '');
19004
- } catch {}
19005
- const matched = currentTaskText ? _matchAPISkills(root, currentTaskText) : [];
19006
- out.apiSkills = {
19007
- total: allSkills.length,
19008
- matched: matched.length,
19009
- matchedIds: matched.slice(0, 5).map(s => s.id),
19010
- ids: allSkills.slice(0, 10).map(s => s.id)
19011
- };
19012
- } catch { out.apiSkills = { error: 'apiSkills 점검 실패' }; }
19013
- // 1.9.264: shellGuard 통합 (health JSON 12번째 통합 필드 — handoff/session close 와 JSON 3 명령 일관성) — UR-0020
19014
- try {
19015
- const sf = _loadShellFailures(root);
19016
- const drift = _shellEnvDrift(root);
19017
- out.shellGuard = {
19018
- failureCount: sf.failures.length,
19019
- recent: sf.failures.slice(-3).map(f => ({ cmd: (f.cmd || '').slice(0, 50), exitCode: f.exitCode, shell: f.shell, rules: f.issues || [] })),
19020
- envDriftChanges: drift && drift.changes ? drift.changes.length : 0,
19021
- envDrift: drift ? drift.changes : null
19022
- };
19023
- } catch { out.shellGuard = { error: 'shellGuard 점검 실패' }; }
19024
- // 1.9.163: 5능력 매트릭스 자동 평가 (1.9.155 sub-agent 점검 → 코드 기반 자동화)
19025
- // 각 능력을 코드 grep 으로 검출 → 0~100 점수. 사용자가 매 health 호출 시 leerness 자기 평가 확인.
19026
- try {
19027
- const harnessSrc = read(__filename);
19028
- const cap = {};
19029
- // (1) 웹 자동화 — 1.9.165 playwright bridge 통합 + 실제 playwright 설치 detect
19030
- const hasWebBridge = /function webCmd\(root, sub/.test(harnessSrc);
19031
- // 사용자가 playwright 설치했는지 실시간 detect (require try)
19032
- let playwrightInstalled = false;
19033
- try { require('playwright'); playwrightInstalled = true; }
19034
- catch { try { require('playwright-core'); playwrightInstalled = true; } catch {} }
19035
- if (hasWebBridge && playwrightInstalled) {
19036
- cap.webAutomation = { score: 90, status: '✓', evidence: 'playwright 설치 + leerness web bridge (1.9.165)' };
19037
- } else if (hasWebBridge) {
19038
- cap.webAutomation = { score: 50, status: '⚠', evidence: 'leerness web bridge 있음, playwright 미설치 (npm i -g playwright)' };
19039
- } else {
19040
- cap.webAutomation = { score: 5, status: '❌', evidence: 'permissions.browser=toggle만 (실 코드 미구현)' };
19041
- }
19042
- // (2) PC 조작 — 1.9.166 robotjs/nut-tree bridge + 실제 설치 detect
19043
- const hasPCBridge = /function pcCmd\(root, sub/.test(harnessSrc);
19044
- let pcInstalled = false;
19045
- try { require('robotjs'); pcInstalled = true; }
19046
- catch { try { require('@nut-tree/nut-js'); pcInstalled = true; } catch {} }
19047
- if (hasPCBridge && pcInstalled) {
19048
- cap.pcAutomation = { score: 90, status: '✓', evidence: 'robotjs/nut-tree 설치 + leerness pc bridge (1.9.166)' };
19049
- } else if (hasPCBridge) {
19050
- cap.pcAutomation = { score: 50, status: '⚠', evidence: 'leerness pc bridge 있음, robotjs 미설치 (npm i -g robotjs)' };
19051
- } else {
19052
- cap.pcAutomation = { score: 5, status: '❌', evidence: 'permissions.mouse/keyboard=필드만 (실 사용처 0)' };
19053
- }
19054
- // (3) 멀티 에이전트 오케스트레이션 — agents multi --execute + consensus 로직?
19055
- const hasExecute = /const execute = has\('--execute'\)/.test(harnessSrc);
19056
- const hasConsensus = /multi-signal consensus/.test(harnessSrc);
19057
- cap.multiAgentOrchestration = (hasExecute && hasConsensus)
19058
- ? { score: 90, status: '✓', evidence: '실 spawn + multi-signal consensus (1.9.156+1.9.155)' }
19059
- : { score: 50, status: '⚠', evidence: '명령 출력만 (1.9.152 기본 모드)' };
19060
- // (4) REPL multi-provider — _agentRepl + _cliChat 5종?
19061
- const hasRepl = /async function _agentRepl/.test(harnessSrc);
19062
- const hasCliChat = /async function _cliChat/.test(harnessSrc);
19063
- cap.replMultiProvider = (hasRepl && hasCliChat)
19064
- ? { score: 90, status: '✓', evidence: 'ollama/claude/codex/agy/copilot 5종 (1.9.149+1.9.153)' }
19065
- : { score: 30, status: '⚠', evidence: 'REPL 미완성' };
19066
- // (5) MCP 도구 — tools array 카운트 (1.9.288: 정확한 도구 정의 패턴 — 자기-매칭 오탐 제거, Codex #5)
19067
- const toolCount = _mcpToolCount();
19068
- cap.mcpTools = toolCount >= 50
19069
- ? { score: 100, status: '✓', evidence: `${toolCount}/50+ 도구 (1.9.159 CRUD 완성)` }
19070
- : { score: Math.round((toolCount / 50) * 100), status: toolCount > 30 ? '✓' : '⚠', evidence: `${toolCount} 도구` };
19071
- // (6) 코드 인텔리전스 — 1.9.167 LSP 어댑터 + typescript 설치 detect
19072
- const hasLspBridge = /function lspCmd\(root, sub/.test(harnessSrc);
19073
- let tsInstalled = false;
19074
- try { require('typescript'); tsInstalled = true; } catch {}
19075
- if (hasLspBridge && tsInstalled) {
19076
- cap.codeIntel = { score: 90, status: '✓', evidence: 'typescript 설치 + leerness lsp bridge (1.9.167, Compiler API)' };
19077
- } else if (hasLspBridge) {
19078
- cap.codeIntel = { score: 50, status: '⚠', evidence: 'leerness lsp bridge 있음, typescript 미설치 (regex fallback 동작, npm i -g typescript)' };
19079
- } else {
19080
- cap.codeIntel = { score: 5, status: '❌', evidence: 'LSP 어댑터 미구현 (코드 인텔리전스 없음)' };
19081
- }
19082
- const avgScore = Math.round((cap.webAutomation.score + cap.pcAutomation.score + cap.multiAgentOrchestration.score + cap.replMultiProvider.score + cap.mcpTools.score + cap.codeIntel.score) / 6);
19083
- out.capabilityMatrix = {
19084
- capabilities: cap,
19085
- overallScore: avgScore,
19086
- summary: `웹${cap.webAutomation.score}/PC${cap.pcAutomation.score}/멀티${cap.multiAgentOrchestration.score}/REPL${cap.replMultiProvider.score}/MCP${cap.mcpTools.score}/LSP${cap.codeIntel.score} · 종합 ${avgScore}%`,
19087
- assessment: avgScore >= 70 ? 'production-ready' : avgScore >= 50 ? 'beta-ready' : 'mvp'
19088
- };
19089
- } catch { out.capabilityMatrix = { error: '5능력 매트릭스 평가 실패' }; }
19090
- // 6) issues 요약 (사용자 글로벌 룰 가시화)
19091
- const issues = [];
19092
- if (out.checks.drift?.level && !/healthy/.test(out.checks.drift.level)) issues.push(`drift ${out.checks.drift.level}`);
19093
- if (out.checks.security?.committedSecrets > 0) issues.push(`🚨 커밋 대상 하드코딩 시크릿 ${out.checks.security.committedSecrets}건 (보안 CRITICAL)`); // 1.9.418 (9th 외부평가 Codex P2)
19094
- if (out.checks.security?.hasDotEnv && out.checks.security?.envInGitignore === false) issues.push('🚨 .env가 .gitignore에 누락 (보안 CRITICAL)');
19095
- if (out.checks.security?.envExampleMissing?.length) issues.push(`.env→.env.example 누락 ${out.checks.security.envExampleMissing.length}건`);
19096
- if (out.checks.security?.gitignoreMissingSecrets?.length) issues.push(`.gitignore 시크릿 누락 ${out.checks.security.gitignoreMissingSecrets.length}건`);
19097
- out.issues = issues;
19098
- out.healthy = issues.length === 0;
19099
-
19100
- // --strict: issue 있으면 exit 1
19101
- if (has('--strict') && !out.healthy) process.exitCode = 1;
19102
-
19103
- if (has('--json')) { log(JSON.stringify(out, null, 2)); return; }
19104
- log(`# leerness health (1.9.85)`);
19105
- log(`Date: ${out.generatedAt}`);
19106
- log(`Status: ${out.healthy ? '✅ healthy' : `⚠ ${issues.length} issues`}`);
19107
- log('');
19108
- log(`## drift`);
19109
- log(` level: ${out.checks.drift?.level || 'n/a'} (score ${out.checks.drift?.score || 0}, fired ${out.checks.drift?.firedCount || 0})`);
19110
- log('');
19111
- log(`## 보안`);
19112
- if (out.checks.security?.hasDotEnv) {
19113
- log(` .env 존재 · .gitignore에 .env 포함: ${out.checks.security.envInGitignore ? '✓' : '✗ CRITICAL'}`);
19114
- log(` .env.example 누락 키: ${out.checks.security.envExampleMissing?.length || 0}건`);
19115
- log(` .gitignore 시크릿 패턴 누락: ${out.checks.security.gitignoreMissingSecrets?.length || 0}건`);
19116
- } else {
19117
- log(` .env 없음 (검증 불필요)`);
19118
- }
19119
- log('');
19120
- log(`## skills`);
19121
- log(` 설치: ${out.checks.skills?.installed || 0}개 · skill query 누적: ${out.checks.skills?.queryHistoryCount || 0}회`);
19122
- log('');
19123
- log(`## usage`);
19124
- log(` 명령 호출: ${out.checks.usage?.commandTotal || 0}회 / ${out.checks.usage?.commandKinds || 0}종`);
19125
- log(` MCP 호출: ${out.checks.usage?.mcpTotal || 0}회 / ${out.checks.usage?.mcpToolKinds || 0}종 도구`);
19126
- log(` since: ${out.checks.usage?.since || 'unknown'}`);
19127
- log('');
19128
- log(`## tasks`);
19129
- const tb = out.checks.tasks?.byStatus || {};
19130
- log(` 총 ${out.checks.tasks?.total || 0}건: ${Object.entries(tb).map(([s, n]) => `${s}=${n}`).join(', ') || '없음'}`);
19131
- // 1.9.163: 5능력 매트릭스 — 1.9.155 sub-agent 점검의 코드 기반 자동 평가
19132
- if (out.capabilityMatrix && !out.capabilityMatrix.error) {
19133
- log('');
19134
- log(`## 🧪 6능력 매트릭스 (1.9.167 자동 평가)`);
19135
- const cm = out.capabilityMatrix;
19136
- log(` 종합: ${cm.overallScore}% (${cm.assessment})`);
19137
- log(` (1) 웹 자동화 ${cm.capabilities.webAutomation.status} ${cm.capabilities.webAutomation.score}% · ${cm.capabilities.webAutomation.evidence}`);
19138
- log(` (2) PC 조작 ${cm.capabilities.pcAutomation.status} ${cm.capabilities.pcAutomation.score}% · ${cm.capabilities.pcAutomation.evidence}`);
19139
- log(` (3) 멀티 오케스트레이션 ${cm.capabilities.multiAgentOrchestration.status} ${cm.capabilities.multiAgentOrchestration.score}% · ${cm.capabilities.multiAgentOrchestration.evidence}`);
19140
- log(` (4) REPL multi-provider ${cm.capabilities.replMultiProvider.status} ${cm.capabilities.replMultiProvider.score}% · ${cm.capabilities.replMultiProvider.evidence}`);
19141
- log(` (5) MCP 도구 ${cm.capabilities.mcpTools.status} ${cm.capabilities.mcpTools.score}% · ${cm.capabilities.mcpTools.evidence}`);
19142
- log(` (6) 코드 인텔리전스 ${cm.capabilities.codeIntel.status} ${cm.capabilities.codeIntel.score}% · ${cm.capabilities.codeIntel.evidence}`);
19143
- }
19144
- if (issues.length) {
19145
- log('');
19146
- log(`## ⚠ Issues (${issues.length})`);
19147
- for (const i of issues) log(` - ${i}`);
19148
- log('');
19149
- log(`💡 자동 회복: leerness drift check --auto-fix · leerness audit --fix`);
19150
- }
19151
- }
18837
+ const _health = require('../lib/health');
18838
+ // 1.9.423 (UR-0025/UR-0125 핸들러 모듈화 8번째): healthCmd → lib/health.js (DI 위임, thin wrapper)
18839
+ function healthCmd(root) { return _health.healthCmd(root, { VERSION, has, arg, harnessPath: __filename, listAllSkills, planPath, readProgressRows, readRules, envDiff, _collectSecretFindings, _readUsageStats, _loadDecisions, _loadLessons, _loadShellFailures, _readFeatureGraph, _scanShellScriptsEncoding, _shellEnvDrift, _computeMilestones, _computeRecentChanges, _computeRoundHistory, _collectPyFiles, _analyzePyFile, _collectRuntimeEnv, _listAPISkills, _matchAPISkills, _mcpToolCount }); }
19152
18840
 
19153
18841
  function usageStatsCmd(root) {
19154
18842
  root = absRoot(root || process.cwd());
package/lib/health.js ADDED
@@ -0,0 +1,348 @@
1
+ // lib/health.js — health 종합 진단 핸들러 (UR-0025/UR-0125 큰 핸들러 모듈화 8번째, 1.9.423)
2
+ // bin/leerness.js 에서 healthCmd(334줄) 분리. DI: harness 고유 의존 다수 주입.
3
+ // io 프리미티브는 ./io, _parseArchiveBlocks 는 ./pure-utils, fs/cp/os/path 빌트인. 동작/출력 무변경.
4
+ 'use strict';
5
+ const cp = require('child_process');
6
+ const os = require('os');
7
+ const path = require('path');
8
+ const fs = require('fs');
9
+ const { log, ok, warn, fail, failJson, today, now, absRoot, exists, read, readBuf, mkdirp, writeUtf8, append, rel } = require('./io');
10
+ const { _parseArchiveBlocks } = require('./pure-utils');
11
+
12
+ function healthCmd(root, deps = {}) {
13
+ const { VERSION, has, arg, harnessPath, listAllSkills, planPath, readProgressRows, readRules, envDiff, _collectSecretFindings, _readUsageStats, _loadDecisions, _loadLessons, _loadShellFailures, _readFeatureGraph, _scanShellScriptsEncoding, _shellEnvDrift, _computeMilestones, _computeRecentChanges, _computeRoundHistory, _collectPyFiles, _analyzePyFile, _collectRuntimeEnv, _listAPISkills, _matchAPISkills, _mcpToolCount } = deps;
14
+ root = absRoot(root || process.cwd());
15
+ const out = { root, generatedAt: new Date().toISOString(), checks: {} };
16
+ // 1) drift level
17
+ try {
18
+ const r = cp.spawnSync(process.execPath, [harnessPath, 'drift', 'check', root, '--json'],
19
+ { encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '0' } });
20
+ const j = JSON.parse(r.stdout.trim());
21
+ out.checks.drift = { level: j.level, score: j.score, firedCount: (j.fired || []).length };
22
+ } catch { out.checks.drift = { error: 'drift check 실패' }; }
23
+ // 2) 보안 상태 (1.9.418, 9th 외부평가 Codex P2): .env/.gitignore + **실제 하드코딩 시크릿 스캔**.
24
+ // 기존엔 .env 가 .gitignore 에 있으면 critical:false 라 커밋된 하드코딩 시크릿이 있어도 health 가 healthy:true 였음(false-OK).
25
+ // handoff/scan secrets 와 동일하게 _collectSecretFindings 로 커밋 대상 시크릿을 반영(정직성).
26
+ try {
27
+ const sec = _collectSecretFindings(root);
28
+ const committedSecrets = sec.committed.length;
29
+ const envPath = path.join(root, '.env');
30
+ const hasDotEnv = exists(envPath);
31
+ const s = { hasDotEnv, committedSecrets };
32
+ if (hasDotEnv) {
33
+ const d = envDiff(root);
34
+ const giText = exists(path.join(root, '.gitignore')) ? read(path.join(root, '.gitignore')) : '';
35
+ const giLines = giText.split('\n').map(l => l.trim());
36
+ const envInGi = giLines.includes('.env') || giLines.includes('/.env');
37
+ const SECRET_PATTERNS = ['.env', '.env.local', '.env.production', '.env.*.local', '*.pem', 'credentials.json'];
38
+ s.envInGitignore = envInGi;
39
+ s.envExampleMissing = d.inEnvOnly;
40
+ s.gitignoreMissingSecrets = SECRET_PATTERNS.filter(p => !giLines.some(l => l === p || l === '/' + p));
41
+ s.critical = !envInGi || committedSecrets > 0;
42
+ } else {
43
+ s.critical = committedSecrets > 0;
44
+ }
45
+ out.checks.security = s;
46
+ } catch { out.checks.security = { error: '보안 점검 실패' }; }
47
+ // 3) skill 수 + skill query 누적
48
+ try {
49
+ const all = listAllSkills(root);
50
+ const skillCount = Object.keys(all).length;
51
+ let queryCount = 0;
52
+ const histPath = path.join(root, '.harness', 'skill-suggestions.md');
53
+ if (exists(histPath)) {
54
+ queryCount = (read(histPath).match(/^## [\d-]+ [\d:]+ — query/gm) || []).length;
55
+ }
56
+ out.checks.skills = { installed: skillCount, queryHistoryCount: queryCount };
57
+ } catch { out.checks.skills = { error: 'skill 점검 실패' }; }
58
+ // 4) MCP + 명령 호출 누적
59
+ try {
60
+ const stats = _readUsageStats(root);
61
+ const cmdTotal = Object.values(stats.commands || {}).reduce((s, n) => s + n, 0);
62
+ const mcpTotal = stats.mcp?.tools ? Object.values(stats.mcp.tools).reduce((s, n) => s + n, 0) : 0;
63
+ out.checks.usage = {
64
+ commandTotal: cmdTotal,
65
+ commandKinds: Object.keys(stats.commands || {}).length,
66
+ mcpTotal,
67
+ mcpToolKinds: stats.mcp?.tools ? Object.keys(stats.mcp.tools).length : 0,
68
+ since: stats.since || null
69
+ };
70
+ } catch { out.checks.usage = { error: 'usage 점검 실패' }; }
71
+ // 5) tasks (progress-tracker)
72
+ try {
73
+ const rows = readProgressRows(root);
74
+ const byStatus = {};
75
+ for (const r of rows) byStatus[r.status] = (byStatus[r.status] || 0) + 1;
76
+ out.checks.tasks = { total: rows.length, byStatus };
77
+ } catch { out.checks.tasks = { error: 'tasks 점검 실패' }; }
78
+ // 1.9.123: memorySurface 통합 (handoff --json 1.9.115 / session close --json 1.9.122 와 동일 패턴)
79
+ try {
80
+ const rows = readProgressRows(root);
81
+ const tasksByStatus = {};
82
+ for (const s of STATUSES) tasksByStatus[s] = 0;
83
+ for (const r of rows) tasksByStatus[r.status] = (tasksByStatus[r.status] || 0) + 1;
84
+ const tasksInProgress = tasksByStatus['in-progress'] || 0;
85
+ const decisionsCount = _loadDecisions(root).length; // 1.9.339 (UR-0053): canonical 단일 진실소스
86
+ const rules = readRules(root);
87
+ const rulesActive = rules.filter(r => r.status === 'active').length;
88
+ const planText = exists(planPath(root)) ? read(planPath(root)) : '';
89
+ const milestones = (planText.match(/^### M-\d{4}\./gm) || []).length;
90
+ const lessonsCount = _loadLessons(root).length;
91
+ out.memorySurface = {
92
+ tasks: { inProgress: tasksInProgress, total: rows.length, byStatus: tasksByStatus },
93
+ decisions: { count: decisionsCount },
94
+ rules: { active: rulesActive, total: rules.length },
95
+ plan: { milestones },
96
+ lessons: { count: lessonsCount },
97
+ archive: (() => {
98
+ // 1.9.130: archive 카운트 통합
99
+ const a = { decisions: 0, lessons: 0, plan: 0, total: 0 };
100
+ try {
101
+ const hdHe = path.join(root, '.harness');
102
+ for (const [key, file] of [['decisions', 'decisions.archive.md'], ['lessons', 'lessons.archive.md'], ['plan', 'plan.archive.md']]) {
103
+ const fpHe = path.join(hdHe, file);
104
+ if (exists(fpHe)) {
105
+ const entries = _parseArchiveBlocks(read(fpHe));
106
+ a[key] = entries.length;
107
+ a.total += entries.length;
108
+ }
109
+ }
110
+ } catch {}
111
+ return a;
112
+ })(),
113
+ summary: `T${tasksInProgress}/D${decisionsCount}/R${rulesActive}/P${milestones}/L${lessonsCount}`,
114
+ };
115
+ } catch { out.memorySurface = { error: 'memorySurface 점검 실패' }; }
116
+ // 1.9.143: health --json featureGraph 통합 (handoff/session close 와 동일 패턴 — JSON 4종 완성)
117
+ try {
118
+ const { nodes: fNodesHe } = _readFeatureGraph(root);
119
+ const edgeCount = fNodesHe.reduce((s, n) => s + (n.dependsOn?.length || 0) + (n.affects?.length || 0) + (n.coChangesWith?.length || 0), 0);
120
+ const linkedSet = new Set();
121
+ for (const n of fNodesHe) {
122
+ for (const x of [...(n.dependsOn||[]), ...(n.affects||[]), ...(n.coChangesWith||[])]) { linkedSet.add(n.id); linkedSet.add(x); }
123
+ }
124
+ const isolated = fNodesHe.length ? (fNodesHe.length - linkedSet.size) : 0;
125
+ out.featureGraph = {
126
+ total: fNodesHe.length,
127
+ edges: edgeCount,
128
+ isolated: Math.max(0, isolated),
129
+ summary: `F${fNodesHe.length}/E${edgeCount}${isolated > 0 ? `/iso${isolated}` : ''}`
130
+ };
131
+ } catch { out.featureGraph = { error: 'featureGraph 점검 실패' }; }
132
+ // 1.9.228: health --json roundHistory 통합 (handoff/session close 와 동일 — JSON 3 명령 일관성 + 6 통합 필드 완성)
133
+ try {
134
+ const rh = _computeRoundHistory(root);
135
+ out.roundHistory = {
136
+ roundCount: rh.roundCount,
137
+ baselineVersion: rh.baselineVersion,
138
+ nextMilestone: rh.nextMilestone,
139
+ roundsToNextMilestone: rh.roundsToNextMilestone,
140
+ daysActive: rh.daysActive,
141
+ avgRoundsPerDay: rh.avgRoundsPerDay
142
+ };
143
+ } catch { out.roundHistory = { error: 'roundHistory 점검 실패' }; }
144
+ // 1.9.230: health --json milestones 통합 (handoff/session close/health 3 명령 일관성 유지)
145
+ try {
146
+ const ms = _computeMilestones(root);
147
+ out.milestones = {
148
+ reachedCount: ms.reached.length,
149
+ reached: ms.reached.map(m => ({ milestone: m.milestone, version: m.version, reachedAt: m.reachedAt })),
150
+ next: ms.next,
151
+ avgRoundsPerDay: ms.avgRoundsPerDay
152
+ };
153
+ } catch { out.milestones = { error: 'milestones 점검 실패' }; }
154
+ // 1.9.234: health --json recentChanges 통합 (3 명령 8 필드 일관성)
155
+ try {
156
+ out.recentChanges = _computeRecentChanges(root, 5);
157
+ } catch { out.recentChanges = { error: 'recentChanges 점검 실패' }; }
158
+ // 1.9.240: health --json pyFiles 통합 (3 명령 9 필드 — UR-0013 2단계)
159
+ try {
160
+ const pyFiles = _collectPyFiles(root, 200);
161
+ const analyses = pyFiles.slice(0, 200).map(f => _analyzePyFile(f)).filter(Boolean);
162
+ out.pyFiles = {
163
+ total: pyFiles.length,
164
+ analyzed: analyses.length,
165
+ totalLOC: analyses.reduce((s, a) => s + a.loc, 0),
166
+ totalImports: analyses.reduce((s, a) => s + a.imports, 0),
167
+ totalFuncs: analyses.reduce((s, a) => s + a.funcs, 0),
168
+ totalClasses: analyses.reduce((s, a) => s + a.classes, 0)
169
+ };
170
+ } catch { out.pyFiles = { error: 'pyFiles 점검 실패' }; }
171
+ // 1.9.242: health --json envInfo 통합 (3 명령 10 필드 — UR-0014 2단계)
172
+ try {
173
+ const runtimeEnv = _collectRuntimeEnv();
174
+ const encScan = _scanShellScriptsEncoding(root);
175
+ out.envInfo = {
176
+ os: runtimeEnv.os.platform,
177
+ isKoreanWindows: runtimeEnv.locale.isKoreanWindows || false,
178
+ codepage: runtimeEnv.locale.codepage || null,
179
+ nodeVersion: runtimeEnv.node.version,
180
+ shellScriptsScanned: encScan.scanned,
181
+ encodingRiskCount: encScan.atRisk.length,
182
+ encodingRiskFiles: encScan.atRisk.slice(0, 5).map(r => r.file),
183
+ // 1.9.249 (UR-0018): 터미널 출력 인코딩 안전 여부 + 자동 회복 결과
184
+ terminalEncodingOk: runtimeEnv.locale.codepage === 65001 || !runtimeEnv.locale.isKoreanWindows,
185
+ autoChcpApplied: process.env._LEERNESS_AUTOCHCP_APPLIED || null,
186
+ // 1.9.250 (UR-0018 2단계): POSIX (Linux/macOS/WSL) terminal encoding 점검
187
+ posixEncodingOk: runtimeEnv.locale.posixEncodingOk,
188
+ isWSL: runtimeEnv.locale.isWSL || false
189
+ };
190
+ } catch { out.envInfo = { error: 'envInfo 점검 실패' }; }
191
+ // 1.9.245: health --json apiSkills 통합 (3 명령 11 필드 — UR-0015)
192
+ try {
193
+ const allSkills = _listAPISkills(root);
194
+ let currentTaskText = '';
195
+ try {
196
+ const rows = readProgressRows(root);
197
+ const ip = rows.find(r => r.status === 'in-progress');
198
+ if (ip) currentTaskText = (ip.title || '') + ' ' + (ip.notes || '');
199
+ } catch {}
200
+ const matched = currentTaskText ? _matchAPISkills(root, currentTaskText) : [];
201
+ out.apiSkills = {
202
+ total: allSkills.length,
203
+ matched: matched.length,
204
+ matchedIds: matched.slice(0, 5).map(s => s.id),
205
+ ids: allSkills.slice(0, 10).map(s => s.id)
206
+ };
207
+ } catch { out.apiSkills = { error: 'apiSkills 점검 실패' }; }
208
+ // 1.9.264: shellGuard 통합 (health JSON 12번째 통합 필드 — handoff/session close 와 JSON 3 명령 일관성) — UR-0020
209
+ try {
210
+ const sf = _loadShellFailures(root);
211
+ const drift = _shellEnvDrift(root);
212
+ out.shellGuard = {
213
+ failureCount: sf.failures.length,
214
+ recent: sf.failures.slice(-3).map(f => ({ cmd: (f.cmd || '').slice(0, 50), exitCode: f.exitCode, shell: f.shell, rules: f.issues || [] })),
215
+ envDriftChanges: drift && drift.changes ? drift.changes.length : 0,
216
+ envDrift: drift ? drift.changes : null
217
+ };
218
+ } catch { out.shellGuard = { error: 'shellGuard 점검 실패' }; }
219
+ // 1.9.163: 5능력 매트릭스 자동 평가 (1.9.155 sub-agent 점검 → 코드 기반 자동화)
220
+ // 각 능력을 코드 grep 으로 검출 → 0~100 점수. 사용자가 매 health 호출 시 leerness 자기 평가 확인.
221
+ try {
222
+ const harnessSrc = read(harnessPath);
223
+ const cap = {};
224
+ // (1) 웹 자동화 — 1.9.165 playwright bridge 통합 + 실제 playwright 설치 detect
225
+ const hasWebBridge = /function webCmd\(root, sub/.test(harnessSrc);
226
+ // 사용자가 playwright 설치했는지 실시간 detect (require try)
227
+ let playwrightInstalled = false;
228
+ try { require('playwright'); playwrightInstalled = true; }
229
+ catch { try { require('playwright-core'); playwrightInstalled = true; } catch {} }
230
+ if (hasWebBridge && playwrightInstalled) {
231
+ cap.webAutomation = { score: 90, status: '✓', evidence: 'playwright 설치 + leerness web bridge (1.9.165)' };
232
+ } else if (hasWebBridge) {
233
+ cap.webAutomation = { score: 50, status: '⚠', evidence: 'leerness web bridge 있음, playwright 미설치 (npm i -g playwright)' };
234
+ } else {
235
+ cap.webAutomation = { score: 5, status: '❌', evidence: 'permissions.browser=toggle만 (실 코드 미구현)' };
236
+ }
237
+ // (2) PC 조작 — 1.9.166 robotjs/nut-tree bridge + 실제 설치 detect
238
+ const hasPCBridge = /function pcCmd\(root, sub/.test(harnessSrc);
239
+ let pcInstalled = false;
240
+ try { require('robotjs'); pcInstalled = true; }
241
+ catch { try { require('@nut-tree/nut-js'); pcInstalled = true; } catch {} }
242
+ if (hasPCBridge && pcInstalled) {
243
+ cap.pcAutomation = { score: 90, status: '✓', evidence: 'robotjs/nut-tree 설치 + leerness pc bridge (1.9.166)' };
244
+ } else if (hasPCBridge) {
245
+ cap.pcAutomation = { score: 50, status: '⚠', evidence: 'leerness pc bridge 있음, robotjs 미설치 (npm i -g robotjs)' };
246
+ } else {
247
+ cap.pcAutomation = { score: 5, status: '❌', evidence: 'permissions.mouse/keyboard=필드만 (실 사용처 0)' };
248
+ }
249
+ // (3) 멀티 에이전트 오케스트레이션 — agents multi --execute + consensus 로직?
250
+ const hasExecute = /const execute = has\('--execute'\)/.test(harnessSrc);
251
+ const hasConsensus = /multi-signal consensus/.test(harnessSrc);
252
+ cap.multiAgentOrchestration = (hasExecute && hasConsensus)
253
+ ? { score: 90, status: '✓', evidence: '실 spawn + multi-signal consensus (1.9.156+1.9.155)' }
254
+ : { score: 50, status: '⚠', evidence: '명령 출력만 (1.9.152 기본 모드)' };
255
+ // (4) REPL multi-provider — _agentRepl + _cliChat 5종?
256
+ const hasRepl = /async function _agentRepl/.test(harnessSrc);
257
+ const hasCliChat = /async function _cliChat/.test(harnessSrc);
258
+ cap.replMultiProvider = (hasRepl && hasCliChat)
259
+ ? { score: 90, status: '✓', evidence: 'ollama/claude/codex/agy/copilot 5종 (1.9.149+1.9.153)' }
260
+ : { score: 30, status: '⚠', evidence: 'REPL 미완성' };
261
+ // (5) MCP 도구 — tools array 카운트 (1.9.288: 정확한 도구 정의 패턴 — 자기-매칭 오탐 제거, Codex #5)
262
+ const toolCount = _mcpToolCount();
263
+ cap.mcpTools = toolCount >= 50
264
+ ? { score: 100, status: '✓', evidence: `${toolCount}/50+ 도구 (1.9.159 CRUD 완성)` }
265
+ : { score: Math.round((toolCount / 50) * 100), status: toolCount > 30 ? '✓' : '⚠', evidence: `${toolCount} 도구` };
266
+ // (6) 코드 인텔리전스 — 1.9.167 LSP 어댑터 + typescript 설치 detect
267
+ const hasLspBridge = /function lspCmd\(root, sub/.test(harnessSrc);
268
+ let tsInstalled = false;
269
+ try { require('typescript'); tsInstalled = true; } catch {}
270
+ if (hasLspBridge && tsInstalled) {
271
+ cap.codeIntel = { score: 90, status: '✓', evidence: 'typescript 설치 + leerness lsp bridge (1.9.167, Compiler API)' };
272
+ } else if (hasLspBridge) {
273
+ cap.codeIntel = { score: 50, status: '⚠', evidence: 'leerness lsp bridge 있음, typescript 미설치 (regex fallback 동작, npm i -g typescript)' };
274
+ } else {
275
+ cap.codeIntel = { score: 5, status: '❌', evidence: 'LSP 어댑터 미구현 (코드 인텔리전스 없음)' };
276
+ }
277
+ const avgScore = Math.round((cap.webAutomation.score + cap.pcAutomation.score + cap.multiAgentOrchestration.score + cap.replMultiProvider.score + cap.mcpTools.score + cap.codeIntel.score) / 6);
278
+ out.capabilityMatrix = {
279
+ capabilities: cap,
280
+ overallScore: avgScore,
281
+ summary: `웹${cap.webAutomation.score}/PC${cap.pcAutomation.score}/멀티${cap.multiAgentOrchestration.score}/REPL${cap.replMultiProvider.score}/MCP${cap.mcpTools.score}/LSP${cap.codeIntel.score} · 종합 ${avgScore}%`,
282
+ assessment: avgScore >= 70 ? 'production-ready' : avgScore >= 50 ? 'beta-ready' : 'mvp'
283
+ };
284
+ } catch { out.capabilityMatrix = { error: '5능력 매트릭스 평가 실패' }; }
285
+ // 6) issues 요약 (사용자 글로벌 룰 가시화)
286
+ const issues = [];
287
+ if (out.checks.drift?.level && !/healthy/.test(out.checks.drift.level)) issues.push(`drift ${out.checks.drift.level}`);
288
+ if (out.checks.security?.committedSecrets > 0) issues.push(`🚨 커밋 대상 하드코딩 시크릿 ${out.checks.security.committedSecrets}건 (보안 CRITICAL)`); // 1.9.418 (9th 외부평가 Codex P2)
289
+ if (out.checks.security?.hasDotEnv && out.checks.security?.envInGitignore === false) issues.push('🚨 .env가 .gitignore에 누락 (보안 CRITICAL)');
290
+ if (out.checks.security?.envExampleMissing?.length) issues.push(`.env→.env.example 누락 ${out.checks.security.envExampleMissing.length}건`);
291
+ if (out.checks.security?.gitignoreMissingSecrets?.length) issues.push(`.gitignore 시크릿 누락 ${out.checks.security.gitignoreMissingSecrets.length}건`);
292
+ out.issues = issues;
293
+ out.healthy = issues.length === 0;
294
+
295
+ // --strict: issue 있으면 exit 1
296
+ if (has('--strict') && !out.healthy) process.exitCode = 1;
297
+
298
+ if (has('--json')) { log(JSON.stringify(out, null, 2)); return; }
299
+ log(`# leerness health (1.9.85)`);
300
+ log(`Date: ${out.generatedAt}`);
301
+ log(`Status: ${out.healthy ? '✅ healthy' : `⚠ ${issues.length} issues`}`);
302
+ log('');
303
+ log(`## drift`);
304
+ log(` level: ${out.checks.drift?.level || 'n/a'} (score ${out.checks.drift?.score || 0}, fired ${out.checks.drift?.firedCount || 0})`);
305
+ log('');
306
+ log(`## 보안`);
307
+ if (out.checks.security?.hasDotEnv) {
308
+ log(` .env 존재 · .gitignore에 .env 포함: ${out.checks.security.envInGitignore ? '✓' : '✗ CRITICAL'}`);
309
+ log(` .env.example 누락 키: ${out.checks.security.envExampleMissing?.length || 0}건`);
310
+ log(` .gitignore 시크릿 패턴 누락: ${out.checks.security.gitignoreMissingSecrets?.length || 0}건`);
311
+ } else {
312
+ log(` .env 없음 (검증 불필요)`);
313
+ }
314
+ log('');
315
+ log(`## skills`);
316
+ log(` 설치: ${out.checks.skills?.installed || 0}개 · skill query 누적: ${out.checks.skills?.queryHistoryCount || 0}회`);
317
+ log('');
318
+ log(`## usage`);
319
+ log(` 명령 호출: ${out.checks.usage?.commandTotal || 0}회 / ${out.checks.usage?.commandKinds || 0}종`);
320
+ log(` MCP 호출: ${out.checks.usage?.mcpTotal || 0}회 / ${out.checks.usage?.mcpToolKinds || 0}종 도구`);
321
+ log(` since: ${out.checks.usage?.since || 'unknown'}`);
322
+ log('');
323
+ log(`## tasks`);
324
+ const tb = out.checks.tasks?.byStatus || {};
325
+ log(` 총 ${out.checks.tasks?.total || 0}건: ${Object.entries(tb).map(([s, n]) => `${s}=${n}`).join(', ') || '없음'}`);
326
+ // 1.9.163: 5능력 매트릭스 — 1.9.155 sub-agent 점검의 코드 기반 자동 평가
327
+ if (out.capabilityMatrix && !out.capabilityMatrix.error) {
328
+ log('');
329
+ log(`## 🧪 6능력 매트릭스 (1.9.167 자동 평가)`);
330
+ const cm = out.capabilityMatrix;
331
+ log(` 종합: ${cm.overallScore}% (${cm.assessment})`);
332
+ log(` (1) 웹 자동화 ${cm.capabilities.webAutomation.status} ${cm.capabilities.webAutomation.score}% · ${cm.capabilities.webAutomation.evidence}`);
333
+ log(` (2) PC 조작 ${cm.capabilities.pcAutomation.status} ${cm.capabilities.pcAutomation.score}% · ${cm.capabilities.pcAutomation.evidence}`);
334
+ log(` (3) 멀티 오케스트레이션 ${cm.capabilities.multiAgentOrchestration.status} ${cm.capabilities.multiAgentOrchestration.score}% · ${cm.capabilities.multiAgentOrchestration.evidence}`);
335
+ log(` (4) REPL multi-provider ${cm.capabilities.replMultiProvider.status} ${cm.capabilities.replMultiProvider.score}% · ${cm.capabilities.replMultiProvider.evidence}`);
336
+ log(` (5) MCP 도구 ${cm.capabilities.mcpTools.status} ${cm.capabilities.mcpTools.score}% · ${cm.capabilities.mcpTools.evidence}`);
337
+ log(` (6) 코드 인텔리전스 ${cm.capabilities.codeIntel.status} ${cm.capabilities.codeIntel.score}% · ${cm.capabilities.codeIntel.evidence}`);
338
+ }
339
+ if (issues.length) {
340
+ log('');
341
+ log(`## ⚠ Issues (${issues.length})`);
342
+ for (const i of issues) log(` - ${i}`);
343
+ log('');
344
+ log(`💡 자동 회복: leerness drift check --auto-fix · leerness audit --fix`);
345
+ }
346
+ }
347
+
348
+ module.exports = { healthCmd };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.422",
3
+ "version": "1.9.423",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",