leerness 1.9.421 → 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/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.421';
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,46 @@ 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
+ } },
3095
+ { name: 'UR-0025 큰핸들러 모듈화 7번째: driftCheckCmd → lib/drift.js + DI 위임 + 재귀/동작 (1.9.422)', run: () => {
3096
+ const m = require('../lib/drift');
3097
+ const expOk = typeof m.driftCheckCmd === 'function';
3098
+ const src = read(__filename);
3099
+ const delegated = src.includes("require('../lib/drift')") && src.includes('_drift.driftCheckCmd(root, opts,');
3100
+ const modSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'drift.js'));
3101
+ const bodyMarker = 'auto-fix 활성 ' + '(1.9.82)'; // drift 본문 고유(split-literal 자기참조 회피)
3102
+ const recursionWired = modSrc.includes('driftCheckCmd(root, opts, deps)'); // 재귀에 deps 전달
3103
+ const movedToLib = modSrc.includes("require('./io')") && recursionWired && modSrc.includes(bodyMarker) && !src.includes(bodyMarker);
3104
+ let behavOk = false;
3105
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_drift_'));
3106
+ const _w = process.stdout.write; let out = '';
3107
+ try {
3108
+ fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true });
3109
+ process.stdout.write = s => { out += s; return true; };
3110
+ // has: --json 만 true(JSON 출력) → --auto-fix false 라 spawn 없음.
3111
+ m.driftCheckCmd(tmp, { json: true }, { VERSION, has: f => f === '--json', arg: (k, d) => d, harnessPath: __filename, readProgressRows, planPath, handoffPath, currentStatePath, taskLogPath, envDiff, _usageStatsPath, _readUsageStats, _updateUserRequest, _scanShellScriptsEncoding, _readFeatureGraph, _detectDeliveredRequests, _autoFixIdempotency });
3112
+ } catch (e) { out = 'ERR:' + e.message; } finally { process.stdout.write = _w; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
3113
+ try { const j = JSON.parse(out); behavOk = typeof j.score === 'number' && 'level' in j && Array.isArray(j.fired); } catch {}
3114
+ return expOk && delegated && movedToLib && behavOk;
3115
+ } },
3076
3116
  { name: 'UR-0025 큰핸들러 모듈화 6번째: audit → lib/audit.js + DI 위임 + 동작 (1.9.421)', run: () => {
3077
3117
  const m = require('../lib/audit');
3078
3118
  const expOk = typeof m.audit === 'function';
@@ -14792,328 +14832,9 @@ function autoUpdateInstall(root) {
14792
14832
  // 기존 프로젝트의 .viewwork/ 폴더는 그대로 유지 (leerness 가 삭제하지 않음 — 사용자 책임).
14793
14833
 
14794
14834
  // 1.9.37: drift detection — 메타파일 staleness 측정으로 "leerness 점점 안 쓰는" 현상 감지
14795
- function driftCheckCmd(root, opts = {}) {
14796
- root = absRoot(root || process.cwd());
14797
- const now = Date.now();
14798
- const _ageDays = (p) => {
14799
- if (!exists(p)) return null;
14800
- return (now - fs.statSync(p).mtimeMs) / 86400000;
14801
- };
14802
- // 각 메타파일의 마지막 갱신
14803
- const signals = [];
14804
- // 1. session-handoff.md - "Last generated" 라인 우선, 없으면 mtime
14805
- const shPath = handoffPath(root);
14806
- if (exists(shPath)) {
14807
- const txt = read(shPath);
14808
- // 1.9.316 (drift 마커 버그): 최신(마지막) 'Last generated' 사용 — 구 블록 중복 시 첫(구) 매치를 읽던 오발화 방어.
14809
- const allGen = [...txt.matchAll(/Last generated:\s*([\d\-T:.Z]+)/g)];
14810
- const m = allGen.length ? allGen[allGen.length - 1] : null;
14811
- let ageDays;
14812
- if (m) {
14813
- ageDays = (now - new Date(m[1]).getTime()) / 86400000;
14814
- } else {
14815
- ageDays = _ageDays(shPath);
14816
- }
14817
- signals.push({ file: 'session-handoff.md', ageDays, threshold: 1, weight: 30, label: 'session close 누락' });
14818
- }
14819
- // 2. current-state.md - "Updated: YYYY-MM-DD" 라인
14820
- const csPath = currentStatePath(root);
14821
- if (exists(csPath)) {
14822
- const m = read(csPath).match(/Updated:\s*(\d{4}-\d{2}-\d{2})/);
14823
- const ageDays = m ? (now - new Date(m[1]).getTime()) / 86400000 : _ageDays(csPath);
14824
- signals.push({ file: 'current-state.md', ageDays, threshold: 2, weight: 20, label: 'current-state 갱신 없음' });
14825
- }
14826
- // 3. progress-tracker.md 마지막 row의 updated 컬럼
14827
- const rows = readProgressRows(root);
14828
- if (rows.length) {
14829
- const dates = rows.map(r => (r.updated || '').match(/\d{4}-\d{2}-\d{2}/)).filter(Boolean).map(m => m[0]);
14830
- if (dates.length) {
14831
- dates.sort();
14832
- const latest = dates[dates.length - 1];
14833
- const ageDays = (now - new Date(latest).getTime()) / 86400000;
14834
- signals.push({ file: 'progress-tracker.md', ageDays, threshold: 1, weight: 30, label: 'task update 없음' });
14835
- }
14836
- } else {
14837
- signals.push({ file: 'progress-tracker.md', ageDays: 999, threshold: 1, weight: 25, label: 'progress-tracker 비어있음' });
14838
- }
14839
- // 4. task-log.md 마지막 entry "## YYYY-MM-DD"
14840
- const tlPath = taskLogPath(root);
14841
- if (exists(tlPath)) {
14842
- const dates = Array.from(read(tlPath).matchAll(/^## (\d{4}-\d{2}-\d{2})/gm)).map(m => m[1]);
14843
- if (dates.length) {
14844
- dates.sort();
14845
- const latest = dates[dates.length - 1];
14846
- const ageDays = (now - new Date(latest).getTime()) / 86400000;
14847
- signals.push({ file: 'task-log.md', ageDays, threshold: 2, weight: 20, label: 'task-log 갱신 없음' });
14848
- }
14849
- }
14850
- // 점수 계산
14851
- let totalScore = 0;
14852
- const fired = [];
14853
- for (const s of signals) {
14854
- if (s.ageDays > s.threshold) {
14855
- totalScore += s.weight;
14856
- fired.push(s);
14857
- }
14858
- }
14859
- // 1.9.78: 보안 신호 (env / .gitignore 누락) — 5번째 신호
14860
- try {
14861
- const envPath = path.join(root, '.env');
14862
- if (exists(envPath)) {
14863
- let secScore = 0;
14864
- const secIssues = [];
14865
- // (a) .env vs .env.example 동기화
14866
- try {
14867
- const d = envDiff(root);
14868
- if (d.inEnvOnly.length) {
14869
- secIssues.push(`.env→.env.example 누락 ${d.inEnvOnly.length}건`);
14870
- secScore += 15;
14871
- }
14872
- } catch {}
14873
- // (b) .gitignore 시크릿 패턴
14874
- try {
14875
- const giText = exists(path.join(root, '.gitignore')) ? read(path.join(root, '.gitignore')) : '';
14876
- const giLines = giText.split('\n').map(l => l.trim());
14877
- const SECRET_PATTERNS = ['.env', '.env.local', '.env.production', '.env.*.local', '*.pem', 'credentials.json'];
14878
- const missing = SECRET_PATTERNS.filter(p => !giLines.some(l => l === p || l === '/' + p));
14879
- if (missing.length) {
14880
- secIssues.push(`.gitignore 시크릿 누락 ${missing.length}건`);
14881
- // 누락이 .env 자체면 최우선 위험 — 15점 가중
14882
- if (missing.includes('.env')) secScore += 30;
14883
- else secScore += Math.min(20, missing.length * 5);
14884
- }
14885
- } catch {}
14886
- if (secScore > 0) {
14887
- totalScore += secScore;
14888
- fired.push({ file: '.env / .gitignore', ageDays: null, threshold: 0, weight: secScore, label: `보안 위험 (1.9.78): ${secIssues.join(' · ')}` });
14889
- }
14890
- }
14891
- } catch {}
14892
- // 1.9.143: Feature Graph 미사용 신호 — 노드는 있는데 edges 비율 낮으면 인과관계 정리 미진
14893
- try {
14894
- const { nodes: fGraphNodes } = _readFeatureGraph(root);
14895
- if (fGraphNodes.length >= 3) {
14896
- const edgeCount = fGraphNodes.reduce((s, n) => s + (n.dependsOn?.length || 0) + (n.affects?.length || 0) + (n.coChangesWith?.length || 0), 0);
14897
- const linkedSet = new Set();
14898
- for (const n of fGraphNodes) {
14899
- for (const x of [...(n.dependsOn||[]), ...(n.affects||[]), ...(n.coChangesWith||[])]) { linkedSet.add(n.id); linkedSet.add(x); }
14900
- }
14901
- const isolatedCount = Math.max(0, fGraphNodes.length - linkedSet.size);
14902
- const isolatedRatio = isolatedCount / fGraphNodes.length;
14903
- if (edgeCount === 0 || isolatedRatio >= 0.5) {
14904
- const fgScore = edgeCount === 0 ? 25 : 15;
14905
- totalScore += fgScore;
14906
- fired.push({ file: '.harness/feature-graph.md', ageDays: null, threshold: 0, weight: fgScore, label: `Feature Graph 미정리 (1.9.143): ${fGraphNodes.length} 노드, edges=${edgeCount}, isolated=${isolatedCount}` });
14907
- }
14908
- }
14909
- } catch {}
14910
- // 신규 _apps/* 에서 task 0건도 신호로
14911
- const appsDir = path.join(root, '_apps');
14912
- let appsZeroTask = [];
14913
- if (exists(appsDir)) {
14914
- for (const d of fs.readdirSync(appsDir)) {
14915
- const sub = path.join(appsDir, d);
14916
- if (!exists(path.join(sub, '.harness'))) continue;
14917
- const subRows = readProgressRows(sub);
14918
- if (!subRows.length) appsZeroTask.push(d);
14919
- }
14920
- if (appsZeroTask.length) {
14921
- const w = Math.min(50, appsZeroTask.length * 10);
14922
- totalScore += w;
14923
- fired.push({ file: `_apps/* (${appsZeroTask.length}개)`, ageDays: null, threshold: 0, weight: w, label: `task 0건 sub-app: ${appsZeroTask.slice(0, 3).join(', ')}${appsZeroTask.length > 3 ? '...' : ''}` });
14924
- }
14925
- }
14926
- // 레벨 판정
14927
- let level = '🟢 healthy';
14928
- if (totalScore >= 100) level = '🔴 critical';
14929
- else if (totalScore >= 50) level = '🟡 warning';
14930
- else if (totalScore >= 20) level = '🟠 attention';
14931
-
14932
- // 1.9.38 (D): drift critical 등급은 누적 카운트 (학습 신호)
14933
- try {
14934
- if (level === '🔴 critical') {
14935
- const stats = _readUsageStats(root);
14936
- stats.drift = stats.drift || {};
14937
- stats.drift.criticalSeen = (stats.drift.criticalSeen || 0) + 1;
14938
- const p = _usageStatsPath(root);
14939
- mkdirp(path.dirname(p));
14940
- writeUtf8(p, JSON.stringify(stats, null, 2) + '\n');
14941
- }
14942
- } catch {}
14943
- // 1.9.39: --auto-fix — critical 시 session close 자동 실행
14944
- // 1.9.82: --auto-fix가 보안 신호도 자동 회복 (audit --fix 호출)
14945
- const autoFix = has('--auto-fix');
14946
- // 1.9.82: 보안 신호가 fired에 있으면 우선 audit --fix 호출
14947
- const hasSecurityFired = fired.some(f => /보안 위험 \(1\.9\.78\)/.test(f.label));
14948
- if (autoFix && hasSecurityFired) {
14949
- log('');
14950
- log(`🔒 --auto-fix 활성 (1.9.82) — 보안 신호 회복: audit --fix 자동 실행 중...`);
14951
- try {
14952
- const r = cp.spawnSync(process.execPath, [__filename, 'audit', root, '--fix'],
14953
- { encoding: 'utf8', timeout: 30000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' } });
14954
- if (r.status === 0) {
14955
- log(`✓ audit --fix 완료 — .gitignore + .env.example 동기화`);
14956
- // 재검사 (보안 신호 회복 확인)
14957
- log('');
14958
- log(`재검사 중...`);
14959
- return driftCheckCmd(root); // 재귀 1회 (auto-fix 없이)
14960
- } else {
14961
- log(`⚠ audit --fix 실패 (exit ${r.status}) — 수동 \`leerness audit --fix\` 권장`);
14962
- }
14963
- } catch (e) {
14964
- log(`⚠ auto-fix 보안 회복 오류: ${e.message}`);
14965
- }
14966
- }
14967
- // 1.9.242: drift check --auto-fix 에 env encoding BOM 자동 추가 통합 (사용자 명시 UR-0014 2단계)
14968
- // 1.9.82 패턴 확장 — drift 회복 시 셸 스크립트 인코딩 위험도 자동 해결
14969
- if (autoFix) {
14970
- try {
14971
- const encScan = _scanShellScriptsEncoding(root);
14972
- if (encScan.atRisk && encScan.atRisk.length > 0) {
14973
- log('');
14974
- log(`🌐 --auto-fix 활성 (1.9.242) — 셸 스크립트 인코딩 위험 ${encScan.atRisk.length}건 BOM 자동 추가 중...`);
14975
- let ok = 0;
14976
- for (const r of encScan.atRisk) {
14977
- try {
14978
- const fullPath = path.join(root, r.file);
14979
- const orig = fs.readFileSync(fullPath);
14980
- const bom = Buffer.from([0xEF, 0xBB, 0xBF]);
14981
- const fixed = Buffer.concat([bom, orig]);
14982
- fs.writeFileSync(fullPath, fixed);
14983
- ok++;
14984
- } catch {}
14985
- }
14986
- log(`✓ UTF-8 BOM 추가 ${ok}/${encScan.atRisk.length}건 (1.9.242 UR-0014)`);
14987
- }
14988
- } catch (e) {
14989
- log(`⚠ env encoding auto-fix 오류 (1.9.242): ${e.message}`);
14990
- }
14991
- }
14992
- // 1.9.225: drift check --auto-fix 에 delivered 패턴 자동 적용 통합 (1.9.223/224 시스템 회수)
14993
- // 사용자 요청에 "구현 완료" 패턴이 누적되면 가짜 미답 신호가 drift score 를 가중시킬 수 있음 → 자동 정리.
14994
- // 1.9.82 audit --fix 패턴과 동일: --auto-fix 시 즉시 적용, 적용 후 재검사.
14995
- if (autoFix) {
14996
- try {
14997
- const delivered = _detectDeliveredRequests(root);
14998
- if (delivered.candidates && delivered.candidates.length > 0) {
14999
- log('');
15000
- log(`📥 --auto-fix 활성 (1.9.225) — delivered 패턴 ${delivered.candidates.length}건 자동 완료 중...`);
15001
- let ok = 0;
15002
- for (const c of delivered.candidates) {
15003
- const u = _updateUserRequest(root, c.id, { status: 'completed', autoCompletedAt: new Date().toISOString(), autoCompleteReason: 'drift-auto-fix-1.9.225' });
15004
- if (u) ok++;
15005
- }
15006
- log(`✓ delivered 자동 완료 ${ok}/${delivered.candidates.length}건`);
15007
- }
15008
- } catch (e) {
15009
- log(`⚠ delivered auto-apply 오류 (1.9.225): ${e.message}`);
15010
- }
15011
- }
15012
- // 1.9.293: drift check --auto-fix 에 idempotency task/user-request 중복 자동 정리 통합
15013
- // 누적 중복 task/요청이 idempotency 위반(medium)을 가중 → drift/handoff 노이즈. 안전: 완전중복 행 제거 + 동일텍스트 dropped 보존(id 유지).
15014
- if (autoFix) {
15015
- try {
15016
- const idemFixes = _autoFixIdempotency(root);
15017
- const totalFixed = idemFixes.reduce((n, f) => n + (f.removedExact || 0) + (f.droppedSameText || 0) + (f.count || 0), 0);
15018
- if (totalFixed > 0) {
15019
- log('');
15020
- log(`🔁 --auto-fix 활성 (1.9.293) — idempotency 중복 ${totalFixed}건 자동 정리 (task/user-request dedup)`);
15021
- }
15022
- } catch (e) {
15023
- log(`⚠ idempotency auto-fix 오류 (1.9.293): ${e.message}`);
15024
- }
15025
- }
15026
- // 1.9.236: drift check --auto-fix 에 release cleanup 통합 (1.9.235 회수)
15027
- // 누적된 50개+ release/* branches → abnormal-shutdown release-branch-pending 신호 가중
15028
- // 안전: keep 10 (최근 10개 유지), merged 만 삭제 (1.9.235 안전 가드)
15029
- // 임계: 50개 초과 시만 자동 정리 (소량 누적은 정상 운영)
15030
- if (autoFix) {
15031
- try {
15032
- const branchR = cp.spawnSync('git', ['branch', '--merged', 'main', '--list', 'release/*'], { cwd: root, encoding: 'utf8' });
15033
- if (branchR.status === 0) {
15034
- const merged = (branchR.stdout || '').split('\n')
15035
- .map(l => l.replace(/^\*?\s+/, '').trim())
15036
- .filter(l => l && /^release\/\d+\.\d+\.\d+$/.test(l));
15037
- if (merged.length > 50) {
15038
- log('');
15039
- log(`🗑 --auto-fix 활성 (1.9.236) — release/* merged ${merged.length}개 (50+) 자동 정리 (keep 10)...`);
15040
- // 정렬 (semver desc)
15041
- merged.sort((a, b) => {
15042
- const va = a.replace('release/', '').split('.').map(n => parseInt(n, 10) || 0);
15043
- const vb = b.replace('release/', '').split('.').map(n => parseInt(n, 10) || 0);
15044
- for (let i = 0; i < 3; i++) if (va[i] !== vb[i]) return vb[i] - va[i];
15045
- return 0;
15046
- });
15047
- const currentBranchR = cp.spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: root, encoding: 'utf8' });
15048
- const currentBranch = (currentBranchR.stdout || '').trim();
15049
- const toDelete = merged.slice(10).filter(b => b !== currentBranch);
15050
- let ok = 0;
15051
- for (const b of toDelete) {
15052
- const r = cp.spawnSync('git', ['branch', '-d', b], { cwd: root, encoding: 'utf8' });
15053
- if (r.status === 0) ok++;
15054
- }
15055
- log(`✓ release cleanup 자동 완료 ${ok}/${toDelete.length}건 (keep 10)`);
15056
- }
15057
- }
15058
- } catch (e) {
15059
- log(`⚠ release cleanup auto-fix 오류 (1.9.236): ${e.message}`);
15060
- }
15061
- }
15062
- if (autoFix && level === '🔴 critical' && !hasSecurityFired) {
15063
- log('');
15064
- log(`🔧 --auto-fix 활성 — session close 자동 실행 중...`);
15065
- try {
15066
- const r = cp.spawnSync(process.execPath, [__filename, 'session', 'close', root], { encoding: 'utf8', timeout: 60000, env: { ...process.env, LEERNESS_INTERNAL: '1' } });
15067
- if (r.status === 0) {
15068
- log(`✓ session close 자동 완료`);
15069
- // autoResolved 카운트
15070
- const stats = _readUsageStats(root);
15071
- stats.drift = stats.drift || {};
15072
- stats.drift.autoResolved = (stats.drift.autoResolved || 0) + 1;
15073
- const p = _usageStatsPath(root);
15074
- mkdirp(path.dirname(p));
15075
- writeUtf8(p, JSON.stringify(stats, null, 2) + '\n');
15076
- // 재검사
15077
- log('');
15078
- log(`재검사 중...`);
15079
- return driftCheckCmd(root); // 재귀 1회 (auto-fix 없이)
15080
- } else {
15081
- log(`⚠ session close 실패 (exit ${r.status}) — 수동 실행 필요`);
15082
- }
15083
- } catch (e) {
15084
- log(`⚠ auto-fix 오류: ${e.message}`);
15085
- }
15086
- }
15087
- if (has('--json')) {
15088
- log(JSON.stringify({ root, score: totalScore, level, signals, fired, appsZeroTask }, null, 2));
15089
- return;
15090
- }
15091
- log(`# leerness drift check (1.9.37)`);
15092
- log(`경로: ${root}`);
15093
- log('');
15094
- log(`상태: ${level} · 점수 ${totalScore}/200`);
15095
- log('');
15096
- log(`| 신호 | age | 임계 | 가중치 | 발화 |`);
15097
- log(`|---|---:|---:|---:|---|`);
15098
- for (const s of signals) {
15099
- const fire = s.ageDays > s.threshold ? '🔥' : '✓';
15100
- const age = s.ageDays === null ? '-' : `${s.ageDays.toFixed(1)}d`;
15101
- log(`| ${s.label} | ${age} | ${s.threshold}d | ${s.weight} | ${fire} |`);
15102
- }
15103
- if (appsZeroTask.length) {
15104
- log('');
15105
- log(`task 0건 sub-app (${appsZeroTask.length}개): ${appsZeroTask.join(', ')}`);
15106
- }
15107
- if (totalScore >= 50) {
15108
- log('');
15109
- log(`💡 권장 조치:`);
15110
- log(` - 즉시: leerness session close . (handoff/current-state 갱신)`);
15111
- log(` - 또는: leerness audit . --fix (자동 갱신 가능 항목 적용)`);
15112
- log(` - sub-app에 task 등록: cd _apps/X && leerness task add "..."`);
15113
- log(` - 이 검사 끄기: --no-drift-check 또는 LEERNESS_NO_DRIFT_CHECK=1`);
15114
- }
15115
- if (level === '🔴 critical') process.exitCode = 1;
15116
- }
14835
+ const _drift = require('../lib/drift');
14836
+ // 1.9.422 (UR-0025/UR-0125 핸들러 모듈화 7번째): driftCheckCmd → lib/drift.js (DI 위임, thin wrapper)
14837
+ function driftCheckCmd(root, opts = {}) { return _drift.driftCheckCmd(root, opts, { VERSION, has, arg, harnessPath: __filename, readProgressRows, planPath, handoffPath, currentStatePath, taskLogPath, envDiff, _usageStatsPath, _readUsageStats, _updateUserRequest, _scanShellScriptsEncoding, _readFeatureGraph, _detectDeliveredRequests, _autoFixIdempotency }); }
15117
14838
 
15118
14839
  // 1.9.69: skill-suggestions.md rolling history 인덱스 — mtime 기반 캐시
15119
14840
  // handoff에서 같은 키워드 과거 추천 결과를 즉시 노출 (재매칭 불필요)
@@ -19113,340 +18834,9 @@ async function deployAutoCmd(root, service) {
19113
18834
  }
19114
18835
 
19115
18836
  // 1.9.85: leerness health — 종합 헬스 체크 (drift + 보안 + skill + MCP + 누적)
19116
- function healthCmd(root) {
19117
- root = absRoot(root || process.cwd());
19118
- const out = { root, generatedAt: new Date().toISOString(), checks: {} };
19119
- // 1) drift level
19120
- try {
19121
- const r = cp.spawnSync(process.execPath, [__filename, 'drift', 'check', root, '--json'],
19122
- { encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '0' } });
19123
- const j = JSON.parse(r.stdout.trim());
19124
- out.checks.drift = { level: j.level, score: j.score, firedCount: (j.fired || []).length };
19125
- } catch { out.checks.drift = { error: 'drift check 실패' }; }
19126
- // 2) 보안 상태 (1.9.418, 9th 외부평가 Codex P2): .env/.gitignore + **실제 하드코딩 시크릿 스캔**.
19127
- // 기존엔 .env 가 .gitignore 에 있으면 critical:false 라 커밋된 하드코딩 시크릿이 있어도 health 가 healthy:true 였음(false-OK).
19128
- // handoff/scan secrets 와 동일하게 _collectSecretFindings 로 커밋 대상 시크릿을 반영(정직성).
19129
- try {
19130
- const sec = _collectSecretFindings(root);
19131
- const committedSecrets = sec.committed.length;
19132
- const envPath = path.join(root, '.env');
19133
- const hasDotEnv = exists(envPath);
19134
- const s = { hasDotEnv, committedSecrets };
19135
- if (hasDotEnv) {
19136
- const d = envDiff(root);
19137
- const giText = exists(path.join(root, '.gitignore')) ? read(path.join(root, '.gitignore')) : '';
19138
- const giLines = giText.split('\n').map(l => l.trim());
19139
- const envInGi = giLines.includes('.env') || giLines.includes('/.env');
19140
- const SECRET_PATTERNS = ['.env', '.env.local', '.env.production', '.env.*.local', '*.pem', 'credentials.json'];
19141
- s.envInGitignore = envInGi;
19142
- s.envExampleMissing = d.inEnvOnly;
19143
- s.gitignoreMissingSecrets = SECRET_PATTERNS.filter(p => !giLines.some(l => l === p || l === '/' + p));
19144
- s.critical = !envInGi || committedSecrets > 0;
19145
- } else {
19146
- s.critical = committedSecrets > 0;
19147
- }
19148
- out.checks.security = s;
19149
- } catch { out.checks.security = { error: '보안 점검 실패' }; }
19150
- // 3) skill 수 + skill query 누적
19151
- try {
19152
- const all = listAllSkills(root);
19153
- const skillCount = Object.keys(all).length;
19154
- let queryCount = 0;
19155
- const histPath = path.join(root, '.harness', 'skill-suggestions.md');
19156
- if (exists(histPath)) {
19157
- queryCount = (read(histPath).match(/^## [\d-]+ [\d:]+ — query/gm) || []).length;
19158
- }
19159
- out.checks.skills = { installed: skillCount, queryHistoryCount: queryCount };
19160
- } catch { out.checks.skills = { error: 'skill 점검 실패' }; }
19161
- // 4) MCP + 명령 호출 누적
19162
- try {
19163
- const stats = _readUsageStats(root);
19164
- const cmdTotal = Object.values(stats.commands || {}).reduce((s, n) => s + n, 0);
19165
- const mcpTotal = stats.mcp?.tools ? Object.values(stats.mcp.tools).reduce((s, n) => s + n, 0) : 0;
19166
- out.checks.usage = {
19167
- commandTotal: cmdTotal,
19168
- commandKinds: Object.keys(stats.commands || {}).length,
19169
- mcpTotal,
19170
- mcpToolKinds: stats.mcp?.tools ? Object.keys(stats.mcp.tools).length : 0,
19171
- since: stats.since || null
19172
- };
19173
- } catch { out.checks.usage = { error: 'usage 점검 실패' }; }
19174
- // 5) tasks (progress-tracker)
19175
- try {
19176
- const rows = readProgressRows(root);
19177
- const byStatus = {};
19178
- for (const r of rows) byStatus[r.status] = (byStatus[r.status] || 0) + 1;
19179
- out.checks.tasks = { total: rows.length, byStatus };
19180
- } catch { out.checks.tasks = { error: 'tasks 점검 실패' }; }
19181
- // 1.9.123: memorySurface 통합 (handoff --json 1.9.115 / session close --json 1.9.122 와 동일 패턴)
19182
- try {
19183
- const rows = readProgressRows(root);
19184
- const tasksByStatus = {};
19185
- for (const s of STATUSES) tasksByStatus[s] = 0;
19186
- for (const r of rows) tasksByStatus[r.status] = (tasksByStatus[r.status] || 0) + 1;
19187
- const tasksInProgress = tasksByStatus['in-progress'] || 0;
19188
- const decisionsCount = _loadDecisions(root).length; // 1.9.339 (UR-0053): canonical 단일 진실소스
19189
- const rules = readRules(root);
19190
- const rulesActive = rules.filter(r => r.status === 'active').length;
19191
- const planText = exists(planPath(root)) ? read(planPath(root)) : '';
19192
- const milestones = (planText.match(/^### M-\d{4}\./gm) || []).length;
19193
- const lessonsCount = _loadLessons(root).length;
19194
- out.memorySurface = {
19195
- tasks: { inProgress: tasksInProgress, total: rows.length, byStatus: tasksByStatus },
19196
- decisions: { count: decisionsCount },
19197
- rules: { active: rulesActive, total: rules.length },
19198
- plan: { milestones },
19199
- lessons: { count: lessonsCount },
19200
- archive: (() => {
19201
- // 1.9.130: archive 카운트 통합
19202
- const a = { decisions: 0, lessons: 0, plan: 0, total: 0 };
19203
- try {
19204
- const hdHe = path.join(root, '.harness');
19205
- for (const [key, file] of [['decisions', 'decisions.archive.md'], ['lessons', 'lessons.archive.md'], ['plan', 'plan.archive.md']]) {
19206
- const fpHe = path.join(hdHe, file);
19207
- if (exists(fpHe)) {
19208
- const entries = _parseArchiveBlocks(read(fpHe));
19209
- a[key] = entries.length;
19210
- a.total += entries.length;
19211
- }
19212
- }
19213
- } catch {}
19214
- return a;
19215
- })(),
19216
- summary: `T${tasksInProgress}/D${decisionsCount}/R${rulesActive}/P${milestones}/L${lessonsCount}`,
19217
- };
19218
- } catch { out.memorySurface = { error: 'memorySurface 점검 실패' }; }
19219
- // 1.9.143: health --json featureGraph 통합 (handoff/session close 와 동일 패턴 — JSON 4종 완성)
19220
- try {
19221
- const { nodes: fNodesHe } = _readFeatureGraph(root);
19222
- const edgeCount = fNodesHe.reduce((s, n) => s + (n.dependsOn?.length || 0) + (n.affects?.length || 0) + (n.coChangesWith?.length || 0), 0);
19223
- const linkedSet = new Set();
19224
- for (const n of fNodesHe) {
19225
- for (const x of [...(n.dependsOn||[]), ...(n.affects||[]), ...(n.coChangesWith||[])]) { linkedSet.add(n.id); linkedSet.add(x); }
19226
- }
19227
- const isolated = fNodesHe.length ? (fNodesHe.length - linkedSet.size) : 0;
19228
- out.featureGraph = {
19229
- total: fNodesHe.length,
19230
- edges: edgeCount,
19231
- isolated: Math.max(0, isolated),
19232
- summary: `F${fNodesHe.length}/E${edgeCount}${isolated > 0 ? `/iso${isolated}` : ''}`
19233
- };
19234
- } catch { out.featureGraph = { error: 'featureGraph 점검 실패' }; }
19235
- // 1.9.228: health --json roundHistory 통합 (handoff/session close 와 동일 — JSON 3 명령 일관성 + 6 통합 필드 완성)
19236
- try {
19237
- const rh = _computeRoundHistory(root);
19238
- out.roundHistory = {
19239
- roundCount: rh.roundCount,
19240
- baselineVersion: rh.baselineVersion,
19241
- nextMilestone: rh.nextMilestone,
19242
- roundsToNextMilestone: rh.roundsToNextMilestone,
19243
- daysActive: rh.daysActive,
19244
- avgRoundsPerDay: rh.avgRoundsPerDay
19245
- };
19246
- } catch { out.roundHistory = { error: 'roundHistory 점검 실패' }; }
19247
- // 1.9.230: health --json milestones 통합 (handoff/session close/health 3 명령 일관성 유지)
19248
- try {
19249
- const ms = _computeMilestones(root);
19250
- out.milestones = {
19251
- reachedCount: ms.reached.length,
19252
- reached: ms.reached.map(m => ({ milestone: m.milestone, version: m.version, reachedAt: m.reachedAt })),
19253
- next: ms.next,
19254
- avgRoundsPerDay: ms.avgRoundsPerDay
19255
- };
19256
- } catch { out.milestones = { error: 'milestones 점검 실패' }; }
19257
- // 1.9.234: health --json recentChanges 통합 (3 명령 8 필드 일관성)
19258
- try {
19259
- out.recentChanges = _computeRecentChanges(root, 5);
19260
- } catch { out.recentChanges = { error: 'recentChanges 점검 실패' }; }
19261
- // 1.9.240: health --json pyFiles 통합 (3 명령 9 필드 — UR-0013 2단계)
19262
- try {
19263
- const pyFiles = _collectPyFiles(root, 200);
19264
- const analyses = pyFiles.slice(0, 200).map(f => _analyzePyFile(f)).filter(Boolean);
19265
- out.pyFiles = {
19266
- total: pyFiles.length,
19267
- analyzed: analyses.length,
19268
- totalLOC: analyses.reduce((s, a) => s + a.loc, 0),
19269
- totalImports: analyses.reduce((s, a) => s + a.imports, 0),
19270
- totalFuncs: analyses.reduce((s, a) => s + a.funcs, 0),
19271
- totalClasses: analyses.reduce((s, a) => s + a.classes, 0)
19272
- };
19273
- } catch { out.pyFiles = { error: 'pyFiles 점검 실패' }; }
19274
- // 1.9.242: health --json envInfo 통합 (3 명령 10 필드 — UR-0014 2단계)
19275
- try {
19276
- const runtimeEnv = _collectRuntimeEnv();
19277
- const encScan = _scanShellScriptsEncoding(root);
19278
- out.envInfo = {
19279
- os: runtimeEnv.os.platform,
19280
- isKoreanWindows: runtimeEnv.locale.isKoreanWindows || false,
19281
- codepage: runtimeEnv.locale.codepage || null,
19282
- nodeVersion: runtimeEnv.node.version,
19283
- shellScriptsScanned: encScan.scanned,
19284
- encodingRiskCount: encScan.atRisk.length,
19285
- encodingRiskFiles: encScan.atRisk.slice(0, 5).map(r => r.file),
19286
- // 1.9.249 (UR-0018): 터미널 출력 인코딩 안전 여부 + 자동 회복 결과
19287
- terminalEncodingOk: runtimeEnv.locale.codepage === 65001 || !runtimeEnv.locale.isKoreanWindows,
19288
- autoChcpApplied: process.env._LEERNESS_AUTOCHCP_APPLIED || null,
19289
- // 1.9.250 (UR-0018 2단계): POSIX (Linux/macOS/WSL) terminal encoding 점검
19290
- posixEncodingOk: runtimeEnv.locale.posixEncodingOk,
19291
- isWSL: runtimeEnv.locale.isWSL || false
19292
- };
19293
- } catch { out.envInfo = { error: 'envInfo 점검 실패' }; }
19294
- // 1.9.245: health --json apiSkills 통합 (3 명령 11 필드 — UR-0015)
19295
- try {
19296
- const allSkills = _listAPISkills(root);
19297
- let currentTaskText = '';
19298
- try {
19299
- const rows = readProgressRows(root);
19300
- const ip = rows.find(r => r.status === 'in-progress');
19301
- if (ip) currentTaskText = (ip.title || '') + ' ' + (ip.notes || '');
19302
- } catch {}
19303
- const matched = currentTaskText ? _matchAPISkills(root, currentTaskText) : [];
19304
- out.apiSkills = {
19305
- total: allSkills.length,
19306
- matched: matched.length,
19307
- matchedIds: matched.slice(0, 5).map(s => s.id),
19308
- ids: allSkills.slice(0, 10).map(s => s.id)
19309
- };
19310
- } catch { out.apiSkills = { error: 'apiSkills 점검 실패' }; }
19311
- // 1.9.264: shellGuard 통합 (health JSON 12번째 통합 필드 — handoff/session close 와 JSON 3 명령 일관성) — UR-0020
19312
- try {
19313
- const sf = _loadShellFailures(root);
19314
- const drift = _shellEnvDrift(root);
19315
- out.shellGuard = {
19316
- failureCount: sf.failures.length,
19317
- recent: sf.failures.slice(-3).map(f => ({ cmd: (f.cmd || '').slice(0, 50), exitCode: f.exitCode, shell: f.shell, rules: f.issues || [] })),
19318
- envDriftChanges: drift && drift.changes ? drift.changes.length : 0,
19319
- envDrift: drift ? drift.changes : null
19320
- };
19321
- } catch { out.shellGuard = { error: 'shellGuard 점검 실패' }; }
19322
- // 1.9.163: 5능력 매트릭스 자동 평가 (1.9.155 sub-agent 점검 → 코드 기반 자동화)
19323
- // 각 능력을 코드 grep 으로 검출 → 0~100 점수. 사용자가 매 health 호출 시 leerness 자기 평가 확인.
19324
- try {
19325
- const harnessSrc = read(__filename);
19326
- const cap = {};
19327
- // (1) 웹 자동화 — 1.9.165 playwright bridge 통합 + 실제 playwright 설치 detect
19328
- const hasWebBridge = /function webCmd\(root, sub/.test(harnessSrc);
19329
- // 사용자가 playwright 설치했는지 실시간 detect (require try)
19330
- let playwrightInstalled = false;
19331
- try { require('playwright'); playwrightInstalled = true; }
19332
- catch { try { require('playwright-core'); playwrightInstalled = true; } catch {} }
19333
- if (hasWebBridge && playwrightInstalled) {
19334
- cap.webAutomation = { score: 90, status: '✓', evidence: 'playwright 설치 + leerness web bridge (1.9.165)' };
19335
- } else if (hasWebBridge) {
19336
- cap.webAutomation = { score: 50, status: '⚠', evidence: 'leerness web bridge 있음, playwright 미설치 (npm i -g playwright)' };
19337
- } else {
19338
- cap.webAutomation = { score: 5, status: '❌', evidence: 'permissions.browser=toggle만 (실 코드 미구현)' };
19339
- }
19340
- // (2) PC 조작 — 1.9.166 robotjs/nut-tree bridge + 실제 설치 detect
19341
- const hasPCBridge = /function pcCmd\(root, sub/.test(harnessSrc);
19342
- let pcInstalled = false;
19343
- try { require('robotjs'); pcInstalled = true; }
19344
- catch { try { require('@nut-tree/nut-js'); pcInstalled = true; } catch {} }
19345
- if (hasPCBridge && pcInstalled) {
19346
- cap.pcAutomation = { score: 90, status: '✓', evidence: 'robotjs/nut-tree 설치 + leerness pc bridge (1.9.166)' };
19347
- } else if (hasPCBridge) {
19348
- cap.pcAutomation = { score: 50, status: '⚠', evidence: 'leerness pc bridge 있음, robotjs 미설치 (npm i -g robotjs)' };
19349
- } else {
19350
- cap.pcAutomation = { score: 5, status: '❌', evidence: 'permissions.mouse/keyboard=필드만 (실 사용처 0)' };
19351
- }
19352
- // (3) 멀티 에이전트 오케스트레이션 — agents multi --execute + consensus 로직?
19353
- const hasExecute = /const execute = has\('--execute'\)/.test(harnessSrc);
19354
- const hasConsensus = /multi-signal consensus/.test(harnessSrc);
19355
- cap.multiAgentOrchestration = (hasExecute && hasConsensus)
19356
- ? { score: 90, status: '✓', evidence: '실 spawn + multi-signal consensus (1.9.156+1.9.155)' }
19357
- : { score: 50, status: '⚠', evidence: '명령 출력만 (1.9.152 기본 모드)' };
19358
- // (4) REPL multi-provider — _agentRepl + _cliChat 5종?
19359
- const hasRepl = /async function _agentRepl/.test(harnessSrc);
19360
- const hasCliChat = /async function _cliChat/.test(harnessSrc);
19361
- cap.replMultiProvider = (hasRepl && hasCliChat)
19362
- ? { score: 90, status: '✓', evidence: 'ollama/claude/codex/agy/copilot 5종 (1.9.149+1.9.153)' }
19363
- : { score: 30, status: '⚠', evidence: 'REPL 미완성' };
19364
- // (5) MCP 도구 — tools array 카운트 (1.9.288: 정확한 도구 정의 패턴 — 자기-매칭 오탐 제거, Codex #5)
19365
- const toolCount = _mcpToolCount();
19366
- cap.mcpTools = toolCount >= 50
19367
- ? { score: 100, status: '✓', evidence: `${toolCount}/50+ 도구 (1.9.159 CRUD 완성)` }
19368
- : { score: Math.round((toolCount / 50) * 100), status: toolCount > 30 ? '✓' : '⚠', evidence: `${toolCount} 도구` };
19369
- // (6) 코드 인텔리전스 — 1.9.167 LSP 어댑터 + typescript 설치 detect
19370
- const hasLspBridge = /function lspCmd\(root, sub/.test(harnessSrc);
19371
- let tsInstalled = false;
19372
- try { require('typescript'); tsInstalled = true; } catch {}
19373
- if (hasLspBridge && tsInstalled) {
19374
- cap.codeIntel = { score: 90, status: '✓', evidence: 'typescript 설치 + leerness lsp bridge (1.9.167, Compiler API)' };
19375
- } else if (hasLspBridge) {
19376
- cap.codeIntel = { score: 50, status: '⚠', evidence: 'leerness lsp bridge 있음, typescript 미설치 (regex fallback 동작, npm i -g typescript)' };
19377
- } else {
19378
- cap.codeIntel = { score: 5, status: '❌', evidence: 'LSP 어댑터 미구현 (코드 인텔리전스 없음)' };
19379
- }
19380
- const avgScore = Math.round((cap.webAutomation.score + cap.pcAutomation.score + cap.multiAgentOrchestration.score + cap.replMultiProvider.score + cap.mcpTools.score + cap.codeIntel.score) / 6);
19381
- out.capabilityMatrix = {
19382
- capabilities: cap,
19383
- overallScore: avgScore,
19384
- summary: `웹${cap.webAutomation.score}/PC${cap.pcAutomation.score}/멀티${cap.multiAgentOrchestration.score}/REPL${cap.replMultiProvider.score}/MCP${cap.mcpTools.score}/LSP${cap.codeIntel.score} · 종합 ${avgScore}%`,
19385
- assessment: avgScore >= 70 ? 'production-ready' : avgScore >= 50 ? 'beta-ready' : 'mvp'
19386
- };
19387
- } catch { out.capabilityMatrix = { error: '5능력 매트릭스 평가 실패' }; }
19388
- // 6) issues 요약 (사용자 글로벌 룰 가시화)
19389
- const issues = [];
19390
- if (out.checks.drift?.level && !/healthy/.test(out.checks.drift.level)) issues.push(`drift ${out.checks.drift.level}`);
19391
- if (out.checks.security?.committedSecrets > 0) issues.push(`🚨 커밋 대상 하드코딩 시크릿 ${out.checks.security.committedSecrets}건 (보안 CRITICAL)`); // 1.9.418 (9th 외부평가 Codex P2)
19392
- if (out.checks.security?.hasDotEnv && out.checks.security?.envInGitignore === false) issues.push('🚨 .env가 .gitignore에 누락 (보안 CRITICAL)');
19393
- if (out.checks.security?.envExampleMissing?.length) issues.push(`.env→.env.example 누락 ${out.checks.security.envExampleMissing.length}건`);
19394
- if (out.checks.security?.gitignoreMissingSecrets?.length) issues.push(`.gitignore 시크릿 누락 ${out.checks.security.gitignoreMissingSecrets.length}건`);
19395
- out.issues = issues;
19396
- out.healthy = issues.length === 0;
19397
-
19398
- // --strict: issue 있으면 exit 1
19399
- if (has('--strict') && !out.healthy) process.exitCode = 1;
19400
-
19401
- if (has('--json')) { log(JSON.stringify(out, null, 2)); return; }
19402
- log(`# leerness health (1.9.85)`);
19403
- log(`Date: ${out.generatedAt}`);
19404
- log(`Status: ${out.healthy ? '✅ healthy' : `⚠ ${issues.length} issues`}`);
19405
- log('');
19406
- log(`## drift`);
19407
- log(` level: ${out.checks.drift?.level || 'n/a'} (score ${out.checks.drift?.score || 0}, fired ${out.checks.drift?.firedCount || 0})`);
19408
- log('');
19409
- log(`## 보안`);
19410
- if (out.checks.security?.hasDotEnv) {
19411
- log(` .env 존재 · .gitignore에 .env 포함: ${out.checks.security.envInGitignore ? '✓' : '✗ CRITICAL'}`);
19412
- log(` .env.example 누락 키: ${out.checks.security.envExampleMissing?.length || 0}건`);
19413
- log(` .gitignore 시크릿 패턴 누락: ${out.checks.security.gitignoreMissingSecrets?.length || 0}건`);
19414
- } else {
19415
- log(` .env 없음 (검증 불필요)`);
19416
- }
19417
- log('');
19418
- log(`## skills`);
19419
- log(` 설치: ${out.checks.skills?.installed || 0}개 · skill query 누적: ${out.checks.skills?.queryHistoryCount || 0}회`);
19420
- log('');
19421
- log(`## usage`);
19422
- log(` 명령 호출: ${out.checks.usage?.commandTotal || 0}회 / ${out.checks.usage?.commandKinds || 0}종`);
19423
- log(` MCP 호출: ${out.checks.usage?.mcpTotal || 0}회 / ${out.checks.usage?.mcpToolKinds || 0}종 도구`);
19424
- log(` since: ${out.checks.usage?.since || 'unknown'}`);
19425
- log('');
19426
- log(`## tasks`);
19427
- const tb = out.checks.tasks?.byStatus || {};
19428
- log(` 총 ${out.checks.tasks?.total || 0}건: ${Object.entries(tb).map(([s, n]) => `${s}=${n}`).join(', ') || '없음'}`);
19429
- // 1.9.163: 5능력 매트릭스 — 1.9.155 sub-agent 점검의 코드 기반 자동 평가
19430
- if (out.capabilityMatrix && !out.capabilityMatrix.error) {
19431
- log('');
19432
- log(`## 🧪 6능력 매트릭스 (1.9.167 자동 평가)`);
19433
- const cm = out.capabilityMatrix;
19434
- log(` 종합: ${cm.overallScore}% (${cm.assessment})`);
19435
- log(` (1) 웹 자동화 ${cm.capabilities.webAutomation.status} ${cm.capabilities.webAutomation.score}% · ${cm.capabilities.webAutomation.evidence}`);
19436
- log(` (2) PC 조작 ${cm.capabilities.pcAutomation.status} ${cm.capabilities.pcAutomation.score}% · ${cm.capabilities.pcAutomation.evidence}`);
19437
- log(` (3) 멀티 오케스트레이션 ${cm.capabilities.multiAgentOrchestration.status} ${cm.capabilities.multiAgentOrchestration.score}% · ${cm.capabilities.multiAgentOrchestration.evidence}`);
19438
- log(` (4) REPL multi-provider ${cm.capabilities.replMultiProvider.status} ${cm.capabilities.replMultiProvider.score}% · ${cm.capabilities.replMultiProvider.evidence}`);
19439
- log(` (5) MCP 도구 ${cm.capabilities.mcpTools.status} ${cm.capabilities.mcpTools.score}% · ${cm.capabilities.mcpTools.evidence}`);
19440
- log(` (6) 코드 인텔리전스 ${cm.capabilities.codeIntel.status} ${cm.capabilities.codeIntel.score}% · ${cm.capabilities.codeIntel.evidence}`);
19441
- }
19442
- if (issues.length) {
19443
- log('');
19444
- log(`## ⚠ Issues (${issues.length})`);
19445
- for (const i of issues) log(` - ${i}`);
19446
- log('');
19447
- log(`💡 자동 회복: leerness drift check --auto-fix · leerness audit --fix`);
19448
- }
19449
- }
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 }); }
19450
18840
 
19451
18841
  function usageStatsCmd(root) {
19452
18842
  root = absRoot(root || process.cwd());