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/CHANGELOG.md +96 -0
- package/README.md +4 -4
- package/bin/leerness.js +100 -23
- package/lib/health.js +355 -354
- package/lib/session-close.js +2 -2
- package/package.json +1 -1
- package/scripts/e2e.js +35 -0
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
s.
|
|
42
|
-
s.
|
|
43
|
-
s.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
for (const
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
a
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if (out.checks.
|
|
291
|
-
if (out.checks.security?.
|
|
292
|
-
if (out.checks.security?.
|
|
293
|
-
if (out.checks.security?.
|
|
294
|
-
out.issues
|
|
295
|
-
out.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
//
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
log(
|
|
307
|
-
log(`
|
|
308
|
-
log('');
|
|
309
|
-
log(
|
|
310
|
-
log(
|
|
311
|
-
log('');
|
|
312
|
-
log(
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
log(` .env.
|
|
316
|
-
log(` .
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
log(
|
|
322
|
-
log(
|
|
323
|
-
log(
|
|
324
|
-
log(
|
|
325
|
-
log(
|
|
326
|
-
log(`
|
|
327
|
-
log(`
|
|
328
|
-
log('');
|
|
329
|
-
log(
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
log(
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
log(`
|
|
339
|
-
log(` (
|
|
340
|
-
log(` (
|
|
341
|
-
log(` (
|
|
342
|
-
log(` (
|
|
343
|
-
log(` (
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
log(
|
|
348
|
-
|
|
349
|
-
log(
|
|
350
|
-
log(
|
|
351
|
-
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
|
|
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 };
|