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