leerness 1.24.0 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/health.js CHANGED
@@ -1,354 +1,355 @@
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, STATUSES, 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
- // 1.9.434 (11th 외부평가 Opus P2, UR-0136): 미존재 경로는 healthy 위조 금지 failJson + exit 1(audit/verify 일치, CI 안전).
16
- if (!exists(root)) { failJson(has('--json'), 'path_not_found', `경로 없음: ${root}`); return; }
17
- const out = { root, generatedAt: new Date().toISOString(), checks: {} };
18
- // 1) drift level
19
- try {
20
- const r = cp.spawnSync(process.execPath, [harnessPath, 'drift', 'check', root, '--json'],
21
- { encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '0' } });
22
- const j = JSON.parse(r.stdout.trim());
23
- out.checks.drift = { level: j.level, score: j.score, firedCount: (j.fired || []).length };
24
- } catch { out.checks.drift = { error: 'drift check 실패' }; }
25
- // 2) 보안 상태 (1.9.418, 9th 외부평가 Codex P2): .env/.gitignore + **실제 하드코딩 시크릿 스캔**.
26
- // 기존엔 .env .gitignore 있으면 critical:false 커밋된 하드코딩 시크릿이 있어도 health 가 healthy:true 였음(false-OK).
27
- // handoff/scan secrets 동일하게 _collectSecretFindings 커밋 대상 시크릿을 반영(정직성).
28
- try {
29
- const sec = _collectSecretFindings(root);
30
- const committedSecrets = sec.committed.length;
31
- const envPath = path.join(root, '.env');
32
- const hasDotEnv = exists(envPath);
33
- const s = { hasDotEnv, committedSecrets };
34
- if (hasDotEnv) {
35
- const d = envDiff(root);
36
- const giText = exists(path.join(root, '.gitignore')) ? read(path.join(root, '.gitignore')) : '';
37
- const giLines = giText.split('\n').map(l => l.trim());
38
- const envInGi = giLines.includes('.env') || giLines.includes('/.env');
39
- const SECRET_PATTERNS = ['.env', '.env.local', '.env.production', '.env.*.local', '*.pem', 'credentials.json'];
40
- s.envInGitignore = envInGi;
41
- s.envExampleMissing = d.inEnvOnly;
42
- s.gitignoreMissingSecrets = SECRET_PATTERNS.filter(p => !giLines.some(l => l === p || l === '/' + p));
43
- s.critical = !envInGi || committedSecrets > 0;
44
- } else {
45
- s.critical = committedSecrets > 0;
46
- }
47
- out.checks.security = s;
48
- } catch { out.checks.security = { error: '보안 점검 실패' }; }
49
- // 3) skill + skill query 누적
50
- try {
51
- const all = listAllSkills(root);
52
- const skillCount = Object.keys(all).length;
53
- let queryCount = 0;
54
- const histPath = path.join(root, '.harness', 'skill-suggestions.md');
55
- if (exists(histPath)) {
56
- queryCount = (read(histPath).match(/^## [\d-]+ [\d:]+ — query/gm) || []).length;
57
- }
58
- out.checks.skills = { installed: skillCount, queryHistoryCount: queryCount };
59
- } catch { out.checks.skills = { error: 'skill 점검 실패' }; }
60
- // 4) MCP + 명령 호출 누적
61
- try {
62
- const stats = _readUsageStats(root);
63
- const cmdTotal = Object.values(stats.commands || {}).reduce((s, n) => s + n, 0);
64
- const mcpTotal = stats.mcp?.tools ? Object.values(stats.mcp.tools).reduce((s, n) => s + n, 0) : 0;
65
- out.checks.usage = {
66
- commandTotal: cmdTotal,
67
- commandKinds: Object.keys(stats.commands || {}).length,
68
- mcpTotal,
69
- mcpToolKinds: stats.mcp?.tools ? Object.keys(stats.mcp.tools).length : 0,
70
- since: stats.since || null
71
- };
72
- } catch { out.checks.usage = { error: 'usage 점검 실패' }; }
73
- // 5) tasks (progress-tracker)
74
- try {
75
- const rows = readProgressRows(root);
76
- const byStatus = {};
77
- for (const r of rows) byStatus[r.status] = (byStatus[r.status] || 0) + 1;
78
- out.checks.tasks = { total: rows.length, byStatus };
79
- } catch { out.checks.tasks = { error: 'tasks 점검 실패' }; }
80
- // 1.9.123: memorySurface 통합 (handoff --json 1.9.115 / session close --json 1.9.122 동일 패턴)
81
- try {
82
- const rows = readProgressRows(root);
83
- const tasksByStatus = {};
84
- for (const s of STATUSES) tasksByStatus[s] = 0;
85
- for (const r of rows) tasksByStatus[r.status] = (tasksByStatus[r.status] || 0) + 1;
86
- const tasksInProgress = tasksByStatus['in-progress'] || 0;
87
- const decisionsCount = _loadDecisions(root).length; // 1.9.339 (UR-0053): canonical 단일 진실소스
88
- const rules = readRules(root);
89
- const rulesActive = rules.filter(r => r.status === 'active').length;
90
- const planText = exists(planPath(root)) ? read(planPath(root)) : '';
91
- const milestones = (planText.match(/^### M-\d{4}\./gm) || []).length;
92
- const lessonsCount = _loadLessons(root).length;
93
- out.memorySurface = {
94
- tasks: { inProgress: tasksInProgress, total: rows.length, byStatus: tasksByStatus },
95
- decisions: { count: decisionsCount },
96
- rules: { active: rulesActive, total: rules.length },
97
- plan: { milestones },
98
- lessons: { count: lessonsCount },
99
- archive: (() => {
100
- // 1.9.130: archive 카운트 통합
101
- const a = { decisions: 0, lessons: 0, plan: 0, total: 0 };
102
- try {
103
- const hdHe = path.join(root, '.harness');
104
- for (const [key, file] of [['decisions', 'decisions.archive.md'], ['lessons', 'lessons.archive.md'], ['plan', 'plan.archive.md']]) {
105
- const fpHe = path.join(hdHe, file);
106
- if (exists(fpHe)) {
107
- const entries = _parseArchiveBlocks(read(fpHe));
108
- a[key] = entries.length;
109
- a.total += entries.length;
110
- }
111
- }
112
- } catch {}
113
- return a;
114
- })(),
115
- summary: `T${tasksInProgress}/D${decisionsCount}/R${rulesActive}/P${milestones}/L${lessonsCount}`,
116
- };
117
- } catch { out.memorySurface = { error: 'memorySurface 점검 실패' }; }
118
- // 1.9.143: health --json featureGraph 통합 (handoff/session close 동일 패턴 — JSON 4종 완성)
119
- try {
120
- const { nodes: fNodesHe } = _readFeatureGraph(root);
121
- const edgeCount = fNodesHe.reduce((s, n) => s + (n.dependsOn?.length || 0) + (n.affects?.length || 0) + (n.coChangesWith?.length || 0), 0);
122
- const linkedSet = new Set();
123
- for (const n of fNodesHe) {
124
- for (const x of [...(n.dependsOn||[]), ...(n.affects||[]), ...(n.coChangesWith||[])]) { linkedSet.add(n.id); linkedSet.add(x); }
125
- }
126
- const isolated = fNodesHe.length ? (fNodesHe.length - linkedSet.size) : 0;
127
- out.featureGraph = {
128
- total: fNodesHe.length,
129
- edges: edgeCount,
130
- isolated: Math.max(0, isolated),
131
- summary: `F${fNodesHe.length}/E${edgeCount}${isolated > 0 ? `/iso${isolated}` : ''}`
132
- };
133
- } catch { out.featureGraph = { error: 'featureGraph 점검 실패' }; }
134
- // 1.9.228: health --json roundHistory 통합 (handoff/session close 동일 JSON 3 명령 일관성 + 6 통합 필드 완성)
135
- try {
136
- const rh = _computeRoundHistory(root);
137
- out.roundHistory = {
138
- roundCount: rh.roundCount,
139
- baselineVersion: rh.baselineVersion,
140
- nextMilestone: rh.nextMilestone,
141
- roundsToNextMilestone: rh.roundsToNextMilestone,
142
- daysActive: rh.daysActive,
143
- avgRoundsPerDay: rh.avgRoundsPerDay
144
- };
145
- } catch { out.roundHistory = { error: 'roundHistory 점검 실패' }; }
146
- // 1.9.230: health --json milestones 통합 (handoff/session close/health 3 명령 일관성 유지)
147
- try {
148
- const ms = _computeMilestones(root);
149
- out.milestones = {
150
- reachedCount: ms.reached.length,
151
- reached: ms.reached.map(m => ({ milestone: m.milestone, version: m.version, reachedAt: m.reachedAt })),
152
- next: ms.next,
153
- avgRoundsPerDay: ms.avgRoundsPerDay
154
- };
155
- } catch { out.milestones = { error: 'milestones 점검 실패' }; }
156
- // 1.9.234: health --json recentChanges 통합 (3 명령 8 필드 일관성)
157
- try {
158
- out.recentChanges = _computeRecentChanges(root, 5);
159
- } catch { out.recentChanges = { error: 'recentChanges 점검 실패' }; }
160
- // 1.9.240: health --json pyFiles 통합 (3 명령 9 필드 UR-0013 2단계)
161
- try {
162
- const pyFiles = _collectPyFiles(root, 200);
163
- const analyses = pyFiles.slice(0, 200).map(f => _analyzePyFile(f)).filter(Boolean);
164
- out.pyFiles = {
165
- total: pyFiles.length,
166
- analyzed: analyses.length,
167
- totalLOC: analyses.reduce((s, a) => s + a.loc, 0),
168
- totalImports: analyses.reduce((s, a) => s + a.imports, 0),
169
- totalFuncs: analyses.reduce((s, a) => s + a.funcs, 0),
170
- totalClasses: analyses.reduce((s, a) => s + a.classes, 0)
171
- };
172
- } catch { out.pyFiles = { error: 'pyFiles 점검 실패' }; }
173
- // 1.9.242: health --json envInfo 통합 (3 명령 10 필드 UR-0014 2단계)
174
- try {
175
- const runtimeEnv = _collectRuntimeEnv();
176
- const encScan = _scanShellScriptsEncoding(root);
177
- out.envInfo = {
178
- os: runtimeEnv.os.platform,
179
- isKoreanWindows: runtimeEnv.locale.isKoreanWindows || false,
180
- codepage: runtimeEnv.locale.codepage || null,
181
- nodeVersion: runtimeEnv.node.version,
182
- shellScriptsScanned: encScan.scanned,
183
- encodingRiskCount: encScan.atRisk.length,
184
- encodingRiskFiles: encScan.atRisk.slice(0, 5).map(r => r.file),
185
- // 1.9.249 (UR-0018): 터미널 출력 인코딩 안전 여부 + 자동 회복 결과
186
- terminalEncodingOk: runtimeEnv.locale.codepage === 65001 || !runtimeEnv.locale.isKoreanWindows,
187
- autoChcpApplied: process.env._LEERNESS_AUTOCHCP_APPLIED || null,
188
- // 1.9.250 (UR-0018 2단계): POSIX (Linux/macOS/WSL) terminal encoding 점검
189
- posixEncodingOk: runtimeEnv.locale.posixEncodingOk,
190
- isWSL: runtimeEnv.locale.isWSL || false
191
- };
192
- } catch { out.envInfo = { error: 'envInfo 점검 실패' }; }
193
- // 1.9.245: health --json apiSkills 통합 (3 명령 11 필드 UR-0015)
194
- try {
195
- const allSkills = _listAPISkills(root);
196
- let currentTaskText = '';
197
- try {
198
- const rows = readProgressRows(root);
199
- const ip = rows.find(r => r.status === 'in-progress');
200
- if (ip) currentTaskText = (ip.title || '') + ' ' + (ip.notes || '');
201
- } catch {}
202
- const matched = currentTaskText ? _matchAPISkills(root, currentTaskText) : [];
203
- out.apiSkills = {
204
- total: allSkills.length,
205
- matched: matched.length,
206
- matchedIds: matched.slice(0, 5).map(s => s.id),
207
- ids: allSkills.slice(0, 10).map(s => s.id)
208
- };
209
- } catch { out.apiSkills = { error: 'apiSkills 점검 실패' }; }
210
- // 1.9.264: shellGuard 통합 (health JSON 12번째 통합 필드 handoff/session close 와 JSON 3 명령 일관성) — UR-0020
211
- try {
212
- const sf = _loadShellFailures(root);
213
- const drift = _shellEnvDrift(root);
214
- out.shellGuard = {
215
- failureCount: sf.failures.length,
216
- recent: sf.failures.slice(-3).map(f => ({ cmd: (f.cmd || '').slice(0, 50), exitCode: f.exitCode, shell: f.shell, rules: f.issues || [] })),
217
- envDriftChanges: drift && drift.changes ? drift.changes.length : 0,
218
- envDrift: drift ? drift.changes : null
219
- };
220
- } catch { out.shellGuard = { error: 'shellGuard 점검 실패' }; }
221
- // 1.9.163: 5능력 매트릭스 자동 평가 (1.9.155 sub-agent 점검 코드 기반 자동화)
222
- // 능력을 코드 grep 으로 검출 0~100 점수. 사용자가 health 호출 시 leerness 자기 평가 확인.
223
- try {
224
- const harnessSrc = read(harnessPath);
225
- const cap = {};
226
- // (1) 자동화 — 1.9.165 playwright bridge 통합 + 실제 playwright 설치 detect
227
- const hasWebBridge = /function webCmd\(root, sub/.test(harnessSrc);
228
- // 사용자가 playwright 설치했는지 실시간 detect (require try)
229
- let playwrightInstalled = false;
230
- try { require('playwright'); playwrightInstalled = true; }
231
- catch { try { require('playwright-core'); playwrightInstalled = true; } catch {} }
232
- if (hasWebBridge && playwrightInstalled) {
233
- cap.webAutomation = { score: 90, status: '✓', evidence: 'playwright 설치 + leerness web bridge (1.9.165)' };
234
- } else if (hasWebBridge) {
235
- cap.webAutomation = { score: 50, status: '⚠', evidence: 'leerness web bridge 있음, playwright 미설치 (npm i -g playwright)' };
236
- } else {
237
- cap.webAutomation = { score: 5, status: '❌', evidence: 'permissions.browser=toggle만 (실 코드 미구현)' };
238
- }
239
- // (2) PC 조작 — 1.9.166 robotjs/nut-tree bridge + 실제 설치 detect
240
- const hasPCBridge = /function pcCmd\(root, sub/.test(harnessSrc);
241
- let pcInstalled = false;
242
- try { require('robotjs'); pcInstalled = true; }
243
- catch { try { require('@nut-tree/nut-js'); pcInstalled = true; } catch {} }
244
- if (hasPCBridge && pcInstalled) {
245
- cap.pcAutomation = { score: 90, status: '✓', evidence: 'robotjs/nut-tree 설치 + leerness pc bridge (1.9.166)' };
246
- } else if (hasPCBridge) {
247
- cap.pcAutomation = { score: 50, status: '⚠', evidence: 'leerness pc bridge 있음, robotjs 미설치 (npm i -g robotjs)' };
248
- } else {
249
- cap.pcAutomation = { score: 5, status: '❌', evidence: 'permissions.mouse/keyboard=필드만 (실 사용처 0)' };
250
- }
251
- // (3) 멀티 에이전트 오케스트레이션 — agents multi --execute + consensus 로직?
252
- const hasExecute = /const execute = has\('--execute'\)/.test(harnessSrc);
253
- const hasConsensus = /multi-signal consensus/.test(harnessSrc);
254
- cap.multiAgentOrchestration = (hasExecute && hasConsensus)
255
- ? { score: 90, status: '✓', evidence: '실 spawn + multi-signal consensus (1.9.156+1.9.155)' }
256
- : { score: 50, status: '', evidence: '명령 출력만 (1.9.152 기본 모드)' };
257
- // (4) REPL multi-provider _agentRepl + _cliChat 5종?
258
- const hasRepl = /async function _agentRepl/.test(harnessSrc);
259
- const hasCliChat = /async function _cliChat/.test(harnessSrc);
260
- cap.replMultiProvider = (hasRepl && hasCliChat)
261
- ? { score: 90, status: '✓', evidence: 'ollama/claude/codex/agy/copilot 5종 (1.9.149+1.9.153)' }
262
- : { score: 30, status: '', evidence: 'REPL 미완성' };
263
- // (5) MCP 도구 tools array 카운트 (1.9.288: 정확한 도구 정의 패턴 — 자기-매칭 오탐 제거, Codex #5)
264
- const toolCount = _mcpToolCount();
265
- cap.mcpTools = toolCount >= 50
266
- ? { score: 100, status: '✓', evidence: `${toolCount}/50+ 도구 (1.9.159 CRUD 완성)` }
267
- : { score: Math.round((toolCount / 50) * 100), status: toolCount > 30 ? '✓' : '⚠', evidence: `${toolCount} 도구` };
268
- // (6) 코드 인텔리전스 1.9.167 LSP 어댑터 + typescript 설치 detect
269
- const hasLspBridge = /function lspCmd\(root, sub/.test(harnessSrc);
270
- let tsInstalled = false;
271
- try { require('typescript'); tsInstalled = true; } catch {}
272
- if (hasLspBridge && tsInstalled) {
273
- cap.codeIntel = { score: 90, status: '✓', evidence: 'typescript 설치 + leerness lsp bridge (1.9.167, Compiler API)' };
274
- } else if (hasLspBridge) {
275
- cap.codeIntel = { score: 50, status: '⚠', evidence: 'leerness lsp bridge 있음, typescript 미설치 (regex fallback 동작, npm i -g typescript)' };
276
- } else {
277
- cap.codeIntel = { score: 5, status: '❌', evidence: 'LSP 어댑터 미구현 (코드 인텔리전스 없음)' };
278
- }
279
- const avgScore = Math.round((cap.webAutomation.score + cap.pcAutomation.score + cap.multiAgentOrchestration.score + cap.replMultiProvider.score + cap.mcpTools.score + cap.codeIntel.score) / 6);
280
- out.capabilityMatrix = {
281
- capabilities: cap,
282
- overallScore: avgScore,
283
- summary: `웹${cap.webAutomation.score}/PC${cap.pcAutomation.score}/멀티${cap.multiAgentOrchestration.score}/REPL${cap.replMultiProvider.score}/MCP${cap.mcpTools.score}/LSP${cap.codeIntel.score} · 종합 ${avgScore}%`,
284
- assessment: avgScore >= 70 ? 'production-ready' : avgScore >= 50 ? 'beta-ready' : 'mvp'
285
- };
286
- } catch { out.capabilityMatrix = { error: '5능력 매트릭스 평가 실패' }; }
287
- // 6) issues 요약 (사용자 글로벌 가시화)
288
- const issues = [];
289
- if (out.checks.drift?.level && !/healthy/.test(out.checks.drift.level)) issues.push(`drift ${out.checks.drift.level}`);
290
- if (out.checks.security?.committedSecrets > 0) issues.push(`🚨 커밋 대상 하드코딩 시크릿 ${out.checks.security.committedSecrets}건 (보안 CRITICAL)`); // 1.9.418 (9th 외부평가 Codex P2)
291
- if (out.checks.security?.hasDotEnv && out.checks.security?.envInGitignore === false) issues.push('🚨 .env가 .gitignore에 누락 (보안 CRITICAL)');
292
- if (out.checks.security?.envExampleMissing?.length) issues.push(`.env→.env.example 누락 ${out.checks.security.envExampleMissing.length}건`);
293
- if (out.checks.security?.gitignoreMissingSecrets?.length) issues.push(`.gitignore 시크릿 누락 ${out.checks.security.gitignoreMissingSecrets.length}건`);
294
- out.issues = issues;
295
- out.healthy = issues.length === 0;
296
-
297
- // 1.9.430 (10th 외부평가 UR-0130): 보안 CRITICAL(커밋 시크릿 / .env 미보호)은 --strict 없이도 exit 1.
298
- // health CI 게이트로 써도 하드코딩 시크릿을 놓치지 않음(scan secrets exit code 일치). 비-CRITICAL issue 는 종전대로 exit 0(--strict 로 게이트).
299
- const criticalSecurity = (out.checks.security?.committedSecrets > 0) || !!(out.checks.security?.hasDotEnv && out.checks.security?.envInGitignore === false);
300
- out.criticalSecurity = criticalSecurity;
301
- // --strict: 모든 issue 시 exit 1. 외엔 보안 CRITICAL 만 exit 1.
302
- if ((has('--strict') && !out.healthy) || criticalSecurity) process.exitCode = 1;
303
-
304
- if (has('--json')) { log(JSON.stringify(out, null, 2)); return; }
305
- log(`# leerness health (1.9.85)`);
306
- log(`Date: ${out.generatedAt}`);
307
- log(`Status: ${out.healthy ? '✅ healthy' : `⚠ ${issues.length} issues`}`);
308
- log('');
309
- log(`## drift`);
310
- log(` level: ${out.checks.drift?.level || 'n/a'} (score ${out.checks.drift?.score || 0}, fired ${out.checks.drift?.firedCount || 0})`);
311
- log('');
312
- log(`## 보안`);
313
- if (out.checks.security?.hasDotEnv) {
314
- log(` .env 존재 · .gitignore에 .env 포함: ${out.checks.security.envInGitignore ? '✓' : '✗ CRITICAL'}`);
315
- log(` .env.example 누락 키: ${out.checks.security.envExampleMissing?.length || 0}건`);
316
- log(` .gitignore 시크릿 패턴 누락: ${out.checks.security.gitignoreMissingSecrets?.length || 0}건`);
317
- } else {
318
- log(` .env 없음 (검증 불필요)`);
319
- }
320
- log('');
321
- log(`## skills`);
322
- log(` 설치: ${out.checks.skills?.installed || 0}개 · skill query 누적: ${out.checks.skills?.queryHistoryCount || 0}회`);
323
- log('');
324
- log(`## usage`);
325
- log(` 명령 호출: ${out.checks.usage?.commandTotal || 0}회 / ${out.checks.usage?.commandKinds || 0}종`);
326
- log(` MCP 호출: ${out.checks.usage?.mcpTotal || 0}회 / ${out.checks.usage?.mcpToolKinds || 0} 도구`);
327
- log(` since: ${out.checks.usage?.since || 'unknown'}`);
328
- log('');
329
- log(`## tasks`);
330
- const tb = out.checks.tasks?.byStatus || {};
331
- log(` 총 ${out.checks.tasks?.total || 0}건: ${Object.entries(tb).map(([s, n]) => `${s}=${n}`).join(', ') || '없음'}`);
332
- // 1.9.163: 5능력 매트릭스 1.9.155 sub-agent 점검의 코드 기반 자동 평가
333
- if (out.capabilityMatrix && !out.capabilityMatrix.error) {
334
- log('');
335
- log(`## 🧪 6능력 매트릭스 (1.9.167 자동 평가)`);
336
- const cm = out.capabilityMatrix;
337
- log(` 종합: ${cm.overallScore}% (${cm.assessment})`);
338
- log(` (1) 자동화 ${cm.capabilities.webAutomation.status} ${cm.capabilities.webAutomation.score}% · ${cm.capabilities.webAutomation.evidence}`);
339
- log(` (2) PC 조작 ${cm.capabilities.pcAutomation.status} ${cm.capabilities.pcAutomation.score}% · ${cm.capabilities.pcAutomation.evidence}`);
340
- log(` (3) 멀티 오케스트레이션 ${cm.capabilities.multiAgentOrchestration.status} ${cm.capabilities.multiAgentOrchestration.score}% · ${cm.capabilities.multiAgentOrchestration.evidence}`);
341
- log(` (4) REPL multi-provider ${cm.capabilities.replMultiProvider.status} ${cm.capabilities.replMultiProvider.score}% · ${cm.capabilities.replMultiProvider.evidence}`);
342
- log(` (5) MCP 도구 ${cm.capabilities.mcpTools.status} ${cm.capabilities.mcpTools.score}% · ${cm.capabilities.mcpTools.evidence}`);
343
- log(` (6) 코드 인텔리전스 ${cm.capabilities.codeIntel.status} ${cm.capabilities.codeIntel.score}% · ${cm.capabilities.codeIntel.evidence}`);
344
- }
345
- if (issues.length) {
346
- log('');
347
- log(`## ⚠ Issues (${issues.length})`);
348
- for (const i of issues) log(` - ${i}`);
349
- log('');
350
- log(`💡 자동 회복: leerness drift check --auto-fix · leerness audit --fix`);
351
- }
352
- }
353
-
354
- module.exports = { healthCmd };
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, STATUSES, has, arg, uiLang, 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 t = (ko, en) => (uiLang === 'en' ? en : ko); // 1.25.2 (UR-0010 Phase 9): health 영어 opt-in
16
+ // 1.9.434 (11th 외부평가 Opus P2, UR-0136): 미존재 경로는 healthy 위조 금지 — failJson + exit 1(audit/verify 일치, CI 안전).
17
+ if (!exists(root)) { failJson(has('--json'), 'path_not_found', t(`경로 없음: ${root}`, `path not found: ${root}`)); return; }
18
+ const out = { root, generatedAt: new Date().toISOString(), checks: {} };
19
+ // 1) drift level
20
+ try {
21
+ const r = cp.spawnSync(process.execPath, [harnessPath, 'drift', 'check', root, '--json'],
22
+ { encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '0' } });
23
+ const j = JSON.parse(r.stdout.trim());
24
+ out.checks.drift = { level: j.level, score: j.score, firedCount: (j.fired || []).length };
25
+ } catch { out.checks.drift = { error: 'drift check 실패' }; }
26
+ // 2) 보안 상태 (1.9.418, 9th 외부평가 Codex P2): .env/.gitignore + **실제 하드코딩 시크릿 스캔**.
27
+ // 기존엔 .env .gitignore 있으면 critical:false 커밋된 하드코딩 시크릿이 있어도 health 가 healthy:true 였음(false-OK).
28
+ // handoff/scan secrets 와 동일하게 _collectSecretFindings 로 커밋 대상 시크릿을 반영(정직성).
29
+ try {
30
+ const sec = _collectSecretFindings(root);
31
+ const committedSecrets = sec.committed.length;
32
+ const envPath = path.join(root, '.env');
33
+ const hasDotEnv = exists(envPath);
34
+ const s = { hasDotEnv, committedSecrets };
35
+ if (hasDotEnv) {
36
+ const d = envDiff(root);
37
+ const giText = exists(path.join(root, '.gitignore')) ? read(path.join(root, '.gitignore')) : '';
38
+ const giLines = giText.split('\n').map(l => l.trim());
39
+ const envInGi = giLines.includes('.env') || giLines.includes('/.env');
40
+ const SECRET_PATTERNS = ['.env', '.env.local', '.env.production', '.env.*.local', '*.pem', 'credentials.json'];
41
+ s.envInGitignore = envInGi;
42
+ s.envExampleMissing = d.inEnvOnly;
43
+ s.gitignoreMissingSecrets = SECRET_PATTERNS.filter(p => !giLines.some(l => l === p || l === '/' + p));
44
+ s.critical = !envInGi || committedSecrets > 0;
45
+ } else {
46
+ s.critical = committedSecrets > 0;
47
+ }
48
+ out.checks.security = s;
49
+ } catch { out.checks.security = { error: '보안 점검 실패' }; }
50
+ // 3) skill 수 + skill query 누적
51
+ try {
52
+ const all = listAllSkills(root);
53
+ const skillCount = Object.keys(all).length;
54
+ let queryCount = 0;
55
+ const histPath = path.join(root, '.harness', 'skill-suggestions.md');
56
+ if (exists(histPath)) {
57
+ queryCount = (read(histPath).match(/^## [\d-]+ [\d:]+ — query/gm) || []).length;
58
+ }
59
+ out.checks.skills = { installed: skillCount, queryHistoryCount: queryCount };
60
+ } catch { out.checks.skills = { error: 'skill 점검 실패' }; }
61
+ // 4) MCP + 명령 호출 누적
62
+ try {
63
+ const stats = _readUsageStats(root);
64
+ const cmdTotal = Object.values(stats.commands || {}).reduce((s, n) => s + n, 0);
65
+ const mcpTotal = stats.mcp?.tools ? Object.values(stats.mcp.tools).reduce((s, n) => s + n, 0) : 0;
66
+ out.checks.usage = {
67
+ commandTotal: cmdTotal,
68
+ commandKinds: Object.keys(stats.commands || {}).length,
69
+ mcpTotal,
70
+ mcpToolKinds: stats.mcp?.tools ? Object.keys(stats.mcp.tools).length : 0,
71
+ since: stats.since || null
72
+ };
73
+ } catch { out.checks.usage = { error: 'usage 점검 실패' }; }
74
+ // 5) tasks (progress-tracker)
75
+ try {
76
+ const rows = readProgressRows(root);
77
+ const byStatus = {};
78
+ for (const r of rows) byStatus[r.status] = (byStatus[r.status] || 0) + 1;
79
+ out.checks.tasks = { total: rows.length, byStatus };
80
+ } catch { out.checks.tasks = { error: 'tasks 점검 실패' }; }
81
+ // 1.9.123: memorySurface 통합 (handoff --json 1.9.115 / session close --json 1.9.122 와 동일 패턴)
82
+ try {
83
+ const rows = readProgressRows(root);
84
+ const tasksByStatus = {};
85
+ for (const s of STATUSES) tasksByStatus[s] = 0;
86
+ for (const r of rows) tasksByStatus[r.status] = (tasksByStatus[r.status] || 0) + 1;
87
+ const tasksInProgress = tasksByStatus['in-progress'] || 0;
88
+ const decisionsCount = _loadDecisions(root).length; // 1.9.339 (UR-0053): canonical 단일 진실소스
89
+ const rules = readRules(root);
90
+ const rulesActive = rules.filter(r => r.status === 'active').length;
91
+ const planText = exists(planPath(root)) ? read(planPath(root)) : '';
92
+ const milestones = (planText.match(/^### M-\d{4}\./gm) || []).length;
93
+ const lessonsCount = _loadLessons(root).length;
94
+ out.memorySurface = {
95
+ tasks: { inProgress: tasksInProgress, total: rows.length, byStatus: tasksByStatus },
96
+ decisions: { count: decisionsCount },
97
+ rules: { active: rulesActive, total: rules.length },
98
+ plan: { milestones },
99
+ lessons: { count: lessonsCount },
100
+ archive: (() => {
101
+ // 1.9.130: archive 카운트 통합
102
+ const a = { decisions: 0, lessons: 0, plan: 0, total: 0 };
103
+ try {
104
+ const hdHe = path.join(root, '.harness');
105
+ for (const [key, file] of [['decisions', 'decisions.archive.md'], ['lessons', 'lessons.archive.md'], ['plan', 'plan.archive.md']]) {
106
+ const fpHe = path.join(hdHe, file);
107
+ if (exists(fpHe)) {
108
+ const entries = _parseArchiveBlocks(read(fpHe));
109
+ a[key] = entries.length;
110
+ a.total += entries.length;
111
+ }
112
+ }
113
+ } catch {}
114
+ return a;
115
+ })(),
116
+ summary: `T${tasksInProgress}/D${decisionsCount}/R${rulesActive}/P${milestones}/L${lessonsCount}`,
117
+ };
118
+ } catch { out.memorySurface = { error: 'memorySurface 점검 실패' }; }
119
+ // 1.9.143: health --json featureGraph 통합 (handoff/session close 와 동일 패턴 — JSON 4종 완성)
120
+ try {
121
+ const { nodes: fNodesHe } = _readFeatureGraph(root);
122
+ const edgeCount = fNodesHe.reduce((s, n) => s + (n.dependsOn?.length || 0) + (n.affects?.length || 0) + (n.coChangesWith?.length || 0), 0);
123
+ const linkedSet = new Set();
124
+ for (const n of fNodesHe) {
125
+ for (const x of [...(n.dependsOn||[]), ...(n.affects||[]), ...(n.coChangesWith||[])]) { linkedSet.add(n.id); linkedSet.add(x); }
126
+ }
127
+ const isolated = fNodesHe.length ? (fNodesHe.length - linkedSet.size) : 0;
128
+ out.featureGraph = {
129
+ total: fNodesHe.length,
130
+ edges: edgeCount,
131
+ isolated: Math.max(0, isolated),
132
+ summary: `F${fNodesHe.length}/E${edgeCount}${isolated > 0 ? `/iso${isolated}` : ''}`
133
+ };
134
+ } catch { out.featureGraph = { error: 'featureGraph 점검 실패' }; }
135
+ // 1.9.228: health --json roundHistory 통합 (handoff/session close 와 동일 — JSON 3 명령 일관성 + 6 통합 필드 완성)
136
+ try {
137
+ const rh = _computeRoundHistory(root);
138
+ out.roundHistory = {
139
+ roundCount: rh.roundCount,
140
+ baselineVersion: rh.baselineVersion,
141
+ nextMilestone: rh.nextMilestone,
142
+ roundsToNextMilestone: rh.roundsToNextMilestone,
143
+ daysActive: rh.daysActive,
144
+ avgRoundsPerDay: rh.avgRoundsPerDay
145
+ };
146
+ } catch { out.roundHistory = { error: 'roundHistory 점검 실패' }; }
147
+ // 1.9.230: health --json milestones 통합 (handoff/session close/health 3 명령 일관성 유지)
148
+ try {
149
+ const ms = _computeMilestones(root);
150
+ out.milestones = {
151
+ reachedCount: ms.reached.length,
152
+ reached: ms.reached.map(m => ({ milestone: m.milestone, version: m.version, reachedAt: m.reachedAt })),
153
+ next: ms.next,
154
+ avgRoundsPerDay: ms.avgRoundsPerDay
155
+ };
156
+ } catch { out.milestones = { error: 'milestones 점검 실패' }; }
157
+ // 1.9.234: health --json recentChanges 통합 (3 명령 8 필드 일관성)
158
+ try {
159
+ out.recentChanges = _computeRecentChanges(root, 5);
160
+ } catch { out.recentChanges = { error: 'recentChanges 점검 실패' }; }
161
+ // 1.9.240: health --json pyFiles 통합 (3 명령 9 필드 — UR-0013 2단계)
162
+ try {
163
+ const pyFiles = _collectPyFiles(root, 200);
164
+ const analyses = pyFiles.slice(0, 200).map(f => _analyzePyFile(f)).filter(Boolean);
165
+ out.pyFiles = {
166
+ total: pyFiles.length,
167
+ analyzed: analyses.length,
168
+ totalLOC: analyses.reduce((s, a) => s + a.loc, 0),
169
+ totalImports: analyses.reduce((s, a) => s + a.imports, 0),
170
+ totalFuncs: analyses.reduce((s, a) => s + a.funcs, 0),
171
+ totalClasses: analyses.reduce((s, a) => s + a.classes, 0)
172
+ };
173
+ } catch { out.pyFiles = { error: 'pyFiles 점검 실패' }; }
174
+ // 1.9.242: health --json envInfo 통합 (3 명령 10 필드 — UR-0014 2단계)
175
+ try {
176
+ const runtimeEnv = _collectRuntimeEnv();
177
+ const encScan = _scanShellScriptsEncoding(root);
178
+ out.envInfo = {
179
+ os: runtimeEnv.os.platform,
180
+ isKoreanWindows: runtimeEnv.locale.isKoreanWindows || false,
181
+ codepage: runtimeEnv.locale.codepage || null,
182
+ nodeVersion: runtimeEnv.node.version,
183
+ shellScriptsScanned: encScan.scanned,
184
+ encodingRiskCount: encScan.atRisk.length,
185
+ encodingRiskFiles: encScan.atRisk.slice(0, 5).map(r => r.file),
186
+ // 1.9.249 (UR-0018): 터미널 출력 인코딩 안전 여부 + 자동 회복 결과
187
+ terminalEncodingOk: runtimeEnv.locale.codepage === 65001 || !runtimeEnv.locale.isKoreanWindows,
188
+ autoChcpApplied: process.env._LEERNESS_AUTOCHCP_APPLIED || null,
189
+ // 1.9.250 (UR-0018 2단계): POSIX (Linux/macOS/WSL) terminal encoding 점검
190
+ posixEncodingOk: runtimeEnv.locale.posixEncodingOk,
191
+ isWSL: runtimeEnv.locale.isWSL || false
192
+ };
193
+ } catch { out.envInfo = { error: 'envInfo 점검 실패' }; }
194
+ // 1.9.245: health --json apiSkills 통합 (3 명령 11 필드 — UR-0015)
195
+ try {
196
+ const allSkills = _listAPISkills(root);
197
+ let currentTaskText = '';
198
+ try {
199
+ const rows = readProgressRows(root);
200
+ const ip = rows.find(r => r.status === 'in-progress');
201
+ if (ip) currentTaskText = (ip.title || '') + ' ' + (ip.notes || '');
202
+ } catch {}
203
+ const matched = currentTaskText ? _matchAPISkills(root, currentTaskText) : [];
204
+ out.apiSkills = {
205
+ total: allSkills.length,
206
+ matched: matched.length,
207
+ matchedIds: matched.slice(0, 5).map(s => s.id),
208
+ ids: allSkills.slice(0, 10).map(s => s.id)
209
+ };
210
+ } catch { out.apiSkills = { error: 'apiSkills 점검 실패' }; }
211
+ // 1.9.264: shellGuard 통합 (health JSON 12번째 통합 필드 — handoff/session close 와 JSON 3 명령 일관성) — UR-0020
212
+ try {
213
+ const sf = _loadShellFailures(root);
214
+ const drift = _shellEnvDrift(root);
215
+ out.shellGuard = {
216
+ failureCount: sf.failures.length,
217
+ recent: sf.failures.slice(-3).map(f => ({ cmd: (f.cmd || '').slice(0, 50), exitCode: f.exitCode, shell: f.shell, rules: f.issues || [] })),
218
+ envDriftChanges: drift && drift.changes ? drift.changes.length : 0,
219
+ envDrift: drift ? drift.changes : null
220
+ };
221
+ } catch { out.shellGuard = { error: 'shellGuard 점검 실패' }; }
222
+ // 1.9.163: 5능력 매트릭스 자동 평가 (1.9.155 sub-agent 점검 코드 기반 자동화)
223
+ // 각 능력을 코드 grep 으로 검출 → 0~100 점수. 사용자가 매 health 호출 시 leerness 자기 평가 확인.
224
+ try {
225
+ const harnessSrc = read(harnessPath);
226
+ const cap = {};
227
+ // (1) 자동화 1.9.165 playwright bridge 통합 + 실제 playwright 설치 detect
228
+ const hasWebBridge = /function webCmd\(root, sub/.test(harnessSrc);
229
+ // 사용자가 playwright 설치했는지 실시간 detect (require try)
230
+ let playwrightInstalled = false;
231
+ try { require('playwright'); playwrightInstalled = true; }
232
+ catch { try { require('playwright-core'); playwrightInstalled = true; } catch {} }
233
+ if (hasWebBridge && playwrightInstalled) {
234
+ cap.webAutomation = { score: 90, status: '✓', evidence: t('playwright 설치 + leerness web bridge (1.9.165)', 'playwright installed + leerness web bridge') };
235
+ } else if (hasWebBridge) {
236
+ cap.webAutomation = { score: 50, status: '⚠', evidence: t('leerness web bridge 있음, playwright 미설치 (npm i -g playwright)', 'leerness web bridge present, playwright not installed (npm i -g playwright)') };
237
+ } else {
238
+ cap.webAutomation = { score: 5, status: '❌', evidence: t('permissions.browser=toggle만 (실 코드 미구현)', 'permissions.browser=toggle only (no real code)') };
239
+ }
240
+ // (2) PC 조작 — 1.9.166 robotjs/nut-tree bridge + 실제 설치 detect
241
+ const hasPCBridge = /function pcCmd\(root, sub/.test(harnessSrc);
242
+ let pcInstalled = false;
243
+ try { require('robotjs'); pcInstalled = true; }
244
+ catch { try { require('@nut-tree/nut-js'); pcInstalled = true; } catch {} }
245
+ if (hasPCBridge && pcInstalled) {
246
+ cap.pcAutomation = { score: 90, status: '✓', evidence: t('robotjs/nut-tree 설치 + leerness pc bridge (1.9.166)', 'robotjs/nut-tree installed + leerness pc bridge') };
247
+ } else if (hasPCBridge) {
248
+ cap.pcAutomation = { score: 50, status: '⚠', evidence: t('leerness pc bridge 있음, robotjs 미설치 (npm i -g robotjs)', 'leerness pc bridge present, robotjs not installed (npm i -g robotjs)') };
249
+ } else {
250
+ cap.pcAutomation = { score: 5, status: '❌', evidence: t('permissions.mouse/keyboard=필드만 (실 사용처 0)', 'permissions.mouse/keyboard=field only (no real usage)') };
251
+ }
252
+ // (3) 멀티 에이전트 오케스트레이션 agents multi --execute + consensus 로직?
253
+ const hasExecute = /const execute = has\('--execute'\)/.test(harnessSrc);
254
+ const hasConsensus = /multi-signal consensus/.test(harnessSrc);
255
+ cap.multiAgentOrchestration = (hasExecute && hasConsensus)
256
+ ? { score: 90, status: '', evidence: t(' spawn + multi-signal consensus (1.9.156+1.9.155)', 'real spawn + multi-signal consensus') }
257
+ : { score: 50, status: '⚠', evidence: t('명령 출력만 (1.9.152 기본 모드)', 'command output only (default mode)') };
258
+ // (4) REPL multi-provider _agentRepl + _cliChat 5종?
259
+ const hasRepl = /async function _agentRepl/.test(harnessSrc);
260
+ const hasCliChat = /async function _cliChat/.test(harnessSrc);
261
+ cap.replMultiProvider = (hasRepl && hasCliChat)
262
+ ? { score: 90, status: '', evidence: t('ollama/claude/codex/agy/copilot 5종 (1.9.149+1.9.153)', 'ollama/claude/codex/agy/copilot (5 providers)') }
263
+ : { score: 30, status: '⚠', evidence: t('REPL 미완성', 'REPL incomplete') };
264
+ // (5) MCP 도구 — tools array 카운트 (1.9.288: 정확한 도구 정의 패턴 — 자기-매칭 오탐 제거, Codex #5)
265
+ const toolCount = _mcpToolCount();
266
+ cap.mcpTools = toolCount >= 50
267
+ ? { score: 100, status: '✓', evidence: t(`${toolCount}/50+ 도구 (1.9.159 CRUD 완성)`, `${toolCount}/50+ tools (CRUD complete)`) }
268
+ : { score: Math.round((toolCount / 50) * 100), status: toolCount > 30 ? '✓' : '⚠', evidence: t(`${toolCount} 도구`, `${toolCount} tools`) };
269
+ // (6) 코드 인텔리전스 1.9.167 LSP 어댑터 + typescript 설치 detect
270
+ const hasLspBridge = /function lspCmd\(root, sub/.test(harnessSrc);
271
+ let tsInstalled = false;
272
+ try { require('typescript'); tsInstalled = true; } catch {}
273
+ if (hasLspBridge && tsInstalled) {
274
+ cap.codeIntel = { score: 90, status: '✓', evidence: t('typescript 설치 + leerness lsp bridge (1.9.167, Compiler API)', 'typescript installed + leerness lsp bridge (Compiler API)') };
275
+ } else if (hasLspBridge) {
276
+ cap.codeIntel = { score: 50, status: '⚠', evidence: t('leerness lsp bridge 있음, typescript 미설치 (regex fallback 동작, npm i -g typescript)', 'leerness lsp bridge present, typescript not installed (regex fallback active, npm i -g typescript)') };
277
+ } else {
278
+ cap.codeIntel = { score: 5, status: '❌', evidence: t('LSP 어댑터 미구현 (코드 인텔리전스 없음)', 'LSP adapter not implemented (no code intelligence)') };
279
+ }
280
+ const avgScore = Math.round((cap.webAutomation.score + cap.pcAutomation.score + cap.multiAgentOrchestration.score + cap.replMultiProvider.score + cap.mcpTools.score + cap.codeIntel.score) / 6);
281
+ out.capabilityMatrix = {
282
+ capabilities: cap,
283
+ overallScore: avgScore,
284
+ summary: t(`웹${cap.webAutomation.score}/PC${cap.pcAutomation.score}/멀티${cap.multiAgentOrchestration.score}/REPL${cap.replMultiProvider.score}/MCP${cap.mcpTools.score}/LSP${cap.codeIntel.score} · 종합 ${avgScore}%`, `web${cap.webAutomation.score}/PC${cap.pcAutomation.score}/multi${cap.multiAgentOrchestration.score}/REPL${cap.replMultiProvider.score}/MCP${cap.mcpTools.score}/LSP${cap.codeIntel.score} · overall ${avgScore}%`),
285
+ assessment: avgScore >= 70 ? 'production-ready' : avgScore >= 50 ? 'beta-ready' : 'mvp'
286
+ };
287
+ } catch { out.capabilityMatrix = { error: t('5능력 매트릭스 평가 실패', 'capability matrix evaluation failed') }; }
288
+ // 6) issues 요약 (사용자 글로벌 룰 가시화)
289
+ const issues = [];
290
+ if (out.checks.drift?.level && !/healthy/.test(out.checks.drift.level)) issues.push(`drift ${out.checks.drift.level}`);
291
+ if (out.checks.security?.committedSecrets > 0) issues.push(t(`🚨 커밋 대상 하드코딩 시크릿 ${out.checks.security.committedSecrets}건 (보안 CRITICAL)`, `🚨 ${out.checks.security.committedSecrets} hardcoded secret(s) staged for commit (security CRITICAL)`)); // 1.9.418 (9th 외부평가 Codex P2)
292
+ if (out.checks.security?.hasDotEnv && out.checks.security?.envInGitignore === false) issues.push(t('🚨 .env.gitignore에 누락 (보안 CRITICAL)', '🚨 .env missing from .gitignore (security CRITICAL)'));
293
+ if (out.checks.security?.envExampleMissing?.length) issues.push(t(`.env→.env.example 누락 ${out.checks.security.envExampleMissing.length}건`, `.env→.env.example missing ${out.checks.security.envExampleMissing.length}`));
294
+ if (out.checks.security?.gitignoreMissingSecrets?.length) issues.push(t(`.gitignore 시크릿 누락 ${out.checks.security.gitignoreMissingSecrets.length}건`, `.gitignore missing secret patterns ${out.checks.security.gitignoreMissingSecrets.length}`));
295
+ out.issues = issues;
296
+ out.healthy = issues.length === 0;
297
+
298
+ // 1.9.430 (10th 외부평가 UR-0130): 보안 CRITICAL(커밋 시크릿 / .env 미보호) --strict 없이도 exit 1.
299
+ // → health CI 게이트로 써도 하드코딩 시크릿을 놓치지 않음(scan secrets 와 exit code 일치). 비-CRITICAL issue 는 종전대로 exit 0(--strict 로 게이트).
300
+ const criticalSecurity = (out.checks.security?.committedSecrets > 0) || !!(out.checks.security?.hasDotEnv && out.checks.security?.envInGitignore === false);
301
+ out.criticalSecurity = criticalSecurity;
302
+ // --strict: 모든 issue exit 1. 외엔 보안 CRITICAL 만 exit 1.
303
+ if ((has('--strict') && !out.healthy) || criticalSecurity) process.exitCode = 1;
304
+
305
+ if (has('--json')) { log(JSON.stringify(out, null, 2)); return; }
306
+ log(`# leerness health (1.9.85)`);
307
+ log(`Date: ${out.generatedAt}`);
308
+ log(`Status: ${out.healthy ? '✅ healthy' : `⚠ ${issues.length} issues`}`);
309
+ log('');
310
+ log(`## drift`);
311
+ log(` level: ${out.checks.drift?.level || 'n/a'} (score ${out.checks.drift?.score || 0}, fired ${out.checks.drift?.firedCount || 0})`);
312
+ log('');
313
+ log(t(`## 보안`, `## Security`));
314
+ if (out.checks.security?.hasDotEnv) {
315
+ log(t(` .env 존재 · .gitignore에 .env 포함: ${out.checks.security.envInGitignore ? '✓' : '✗ CRITICAL'}`, ` .env present · .env in .gitignore: ${out.checks.security.envInGitignore ? '✓' : '✗ CRITICAL'}`));
316
+ log(t(` .env.example 누락 키: ${out.checks.security.envExampleMissing?.length || 0}건`, ` .env.example missing keys: ${out.checks.security.envExampleMissing?.length || 0}`));
317
+ log(t(` .gitignore 시크릿 패턴 누락: ${out.checks.security.gitignoreMissingSecrets?.length || 0}건`, ` .gitignore missing secret patterns: ${out.checks.security.gitignoreMissingSecrets?.length || 0}`));
318
+ } else {
319
+ log(t(` .env 없음 (검증 불필요)`, ` no .env (nothing to check)`));
320
+ }
321
+ log('');
322
+ log(`## skills`);
323
+ log(t(` 설치: ${out.checks.skills?.installed || 0}개 · skill query 누적: ${out.checks.skills?.queryHistoryCount || 0}회`, ` installed: ${out.checks.skills?.installed || 0} · skill queries: ${out.checks.skills?.queryHistoryCount || 0}`));
324
+ log('');
325
+ log(`## usage`);
326
+ log(t(` 명령 호출: ${out.checks.usage?.commandTotal || 0}회 / ${out.checks.usage?.commandKinds || 0}종`, ` command calls: ${out.checks.usage?.commandTotal || 0} / ${out.checks.usage?.commandKinds || 0} kinds`));
327
+ log(t(` MCP 호출: ${out.checks.usage?.mcpTotal || 0}회 / ${out.checks.usage?.mcpToolKinds || 0}종 도구`, ` MCP calls: ${out.checks.usage?.mcpTotal || 0} / ${out.checks.usage?.mcpToolKinds || 0} tools`));
328
+ log(` since: ${out.checks.usage?.since || 'unknown'}`);
329
+ log('');
330
+ log(`## tasks`);
331
+ const tb = out.checks.tasks?.byStatus || {};
332
+ log(t(` 총 ${out.checks.tasks?.total || 0}건: ${Object.entries(tb).map(([s, n]) => `${s}=${n}`).join(', ') || '없음'}`, ` total ${out.checks.tasks?.total || 0}: ${Object.entries(tb).map(([s, n]) => `${s}=${n}`).join(', ') || 'none'}`));
333
+ // 1.9.163: 5능력 매트릭스 — 1.9.155 sub-agent 점검의 코드 기반 자동 평가
334
+ if (out.capabilityMatrix && !out.capabilityMatrix.error) {
335
+ log('');
336
+ log(t(`## 🧪 6능력 매트릭스 (1.9.167 자동 평가)`, `## 🧪 6-capability matrix (auto-assessed)`));
337
+ const cm = out.capabilityMatrix;
338
+ log(t(` 종합: ${cm.overallScore}% (${cm.assessment})`, ` overall: ${cm.overallScore}% (${cm.assessment})`));
339
+ log(t(` (1) 자동화 ${cm.capabilities.webAutomation.status} ${cm.capabilities.webAutomation.score}% · ${cm.capabilities.webAutomation.evidence}`, ` (1) web automation ${cm.capabilities.webAutomation.status} ${cm.capabilities.webAutomation.score}% · ${cm.capabilities.webAutomation.evidence}`));
340
+ log(t(` (2) PC 조작 ${cm.capabilities.pcAutomation.status} ${cm.capabilities.pcAutomation.score}% · ${cm.capabilities.pcAutomation.evidence}`, ` (2) PC control ${cm.capabilities.pcAutomation.status} ${cm.capabilities.pcAutomation.score}% · ${cm.capabilities.pcAutomation.evidence}`));
341
+ log(t(` (3) 멀티 오케스트레이션 ${cm.capabilities.multiAgentOrchestration.status} ${cm.capabilities.multiAgentOrchestration.score}% · ${cm.capabilities.multiAgentOrchestration.evidence}`, ` (3) multi-agent orch. ${cm.capabilities.multiAgentOrchestration.status} ${cm.capabilities.multiAgentOrchestration.score}% · ${cm.capabilities.multiAgentOrchestration.evidence}`));
342
+ log(t(` (4) REPL multi-provider ${cm.capabilities.replMultiProvider.status} ${cm.capabilities.replMultiProvider.score}% · ${cm.capabilities.replMultiProvider.evidence}`, ` (4) REPL multi-provider ${cm.capabilities.replMultiProvider.status} ${cm.capabilities.replMultiProvider.score}% · ${cm.capabilities.replMultiProvider.evidence}`));
343
+ log(t(` (5) MCP 도구 ${cm.capabilities.mcpTools.status} ${cm.capabilities.mcpTools.score}% · ${cm.capabilities.mcpTools.evidence}`, ` (5) MCP tools ${cm.capabilities.mcpTools.status} ${cm.capabilities.mcpTools.score}% · ${cm.capabilities.mcpTools.evidence}`));
344
+ log(t(` (6) 코드 인텔리전스 ${cm.capabilities.codeIntel.status} ${cm.capabilities.codeIntel.score}% · ${cm.capabilities.codeIntel.evidence}`, ` (6) code intelligence ${cm.capabilities.codeIntel.status} ${cm.capabilities.codeIntel.score}% · ${cm.capabilities.codeIntel.evidence}`));
345
+ }
346
+ if (issues.length) {
347
+ log('');
348
+ log(`## Issues (${issues.length})`);
349
+ for (const i of issues) log(` - ${i}`);
350
+ log('');
351
+ log(t(`💡 자동 회복: leerness drift check --auto-fix · leerness audit --fix`, `💡 auto-recover: leerness drift check --auto-fix · leerness audit --fix`));
352
+ }
353
+ }
354
+
355
+ module.exports = { healthCmd };