leerness 1.36.183 → 1.36.185

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