leerness 1.25.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 +48 -0
- package/README.md +4 -4
- package/bin/leerness.js +10 -2
- package/lib/health.js +355 -354
- package/package.json +1 -1
- package/scripts/e2e.js +35 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.26.0 — 2026-06-15 — 🛡️ [안정화/Stable] i18n 행위가드 + health 진단 영어화 안정 minor
|
|
4
|
+
|
|
5
|
+
**🛡️ 안정화(Stable) minor — i18n 레이어 견고성 검증·가드 + health 진단 영어화를 npm 공개.** 직전 minor(1.25.0) 이후 누적된 패치 2건(1.25.1 + 1.25.2)을 검증·통합해 배포. R-0011 정책의 17번째 stable minor. 한국어 우선 기본은 그대로.
|
|
6
|
+
|
|
7
|
+
### 이번 minor 통합 (1.25.1~1.25.2)
|
|
8
|
+
- **🔬 22번째 버그헌트(i18n 레이어) + 행위 e2e 회귀가드 (1.25.1)**: 8 phase 영어화 레이어를 통째 적대 검증 → 런타임 버그 0(맹신 X 양방향). uiLang 크래시안전·`--language` positional 무누출·`--language=en` 문법·`--json` 유효·flag>manifest 우선순위 확인. 소스가드만 있던 공백(1.23.0 과장 통과 원인)을 **행위 e2e 가드**로 보강.
|
|
9
|
+
- **🌐 health 진단 완전 영어화 (1.25.2)**: 고빈도 진단 `health` 를 렌더 라벨 + 능력 매트릭스 evidence(16종) + issues + summary 까지 완전 영어화(반쪽 번역 회피). `--language en` opt-in.
|
|
10
|
+
- **한국어 우선 기본 보존**: 영어는 명시 opt-in. 한국어 출력/매트릭스는 한 글자도 안 바뀜(e2e 무회귀).
|
|
11
|
+
|
|
12
|
+
### 잔여 (UR-0010 Phase 10+, 백로그)
|
|
13
|
+
- capabilities/commands/drift check/install-safety/constraints/doctor + 메모리 CRUD 빈상태 + handoff 본문 — en-leak 우선순위순.
|
|
14
|
+
|
|
15
|
+
### 검증 (회귀 0)
|
|
16
|
+
- **selftest 245/245** · **E2E 366/366** (i18n 행위가드: lens/health en 한글 0 + ko 기본 보존 + positional 무누출 포함).
|
|
17
|
+
- minor(1.26.0) — npm 배포(R-0011 stable) + annotated tag(Stable) + GitHub release(latest) + 게시본 클린룸 재실증.
|
|
18
|
+
|
|
19
|
+
## 1.25.2 — 2026-06-15 — CLI 영어화 Phase 9: health 진단 완전 영어화 (UR-0010)
|
|
20
|
+
|
|
21
|
+
**🌐 health 진단을 통째로 영어로.** en-leak 스캔 우선순위에서 고빈도 진단인 `health` 를 — 렌더 라벨만이 아니라 **능력 매트릭스 evidence·issues·요약까지 완전히** — 영어화. 반쪽 번역(1.23.0 과장)의 재발을 피하려 한 모듈(lib/health.js)의 모든 한국어를 한 번에 처리.
|
|
22
|
+
|
|
23
|
+
### 변경 (UR-0010 Phase 9)
|
|
24
|
+
- **health 완전 영어화 (lib/health.js, DI uiLang 주입)**: 렌더 라벨(`## 보안`→`## Security`, skills/usage/tasks, `6능력 매트릭스`→`6-capability matrix`, `자동 회복`→`auto-recover`) + **능력 매트릭스 evidence 16종**(웹/PC/멀티/REPL/MCP/LSP 각 점수대) + summary + 보안 issues 4종 + path-not-found 에러. `t(ko,en)` 분기, ko 인자 verbatim.
|
|
25
|
+
- **bin DI**: `healthCmd` 호출에 `uiLang: _uiLang(root)` 주입(session-close/health 동일 패턴).
|
|
26
|
+
- **한국어 기본 유지**: 영어는 명시 opt-in. `health`/`health --json` 기본은 한국어 그대로(e2e 무회귀). `--json` 값도 언어 따름(en 시 영어 evidence).
|
|
27
|
+
|
|
28
|
+
### 잔여 (UR-0010 Phase 10+, 백로그)
|
|
29
|
+
- capabilities(24)/commands(90)/drift check/install-safety/constraints/doctor + 메모리 CRUD 빈상태 메시지 + handoff 본문 — en-leak 우선순위순.
|
|
30
|
+
|
|
31
|
+
### 검증 (회귀 0)
|
|
32
|
+
- **selftest 244→245** (health 영어/한국어 보존 + uiLang 주입 소스가드) · 행위(health `--language en` 한글 0 / ko 16줄 보존 / en --json 유효) · **E2E 366/366** (i18n 행위가드에 health en/ko 추가).
|
|
33
|
+
- patch(1.25.2) — npm 미배포(R-0011, GitHub/CHANGELOG 누적).
|
|
34
|
+
|
|
35
|
+
## 1.25.1 — 2026-06-15 — 22번째 버그헌트(i18n 레이어 검증) + i18n 행위 e2e 회귀가드 (UR-0010)
|
|
36
|
+
|
|
37
|
+
**🔬 8 phase 영어화 레이어를 통째로 적대 검증.** uiLang/`_tx`/`_t` 머신을 cross-cutting 레이어로 한 번에 점검 — **런타임 버그 0** (맹신 X 양방향 확인). 다만 그동안 i18n 은 *소스가드(문자열 존재)* 만 있고 *행위 e2e* 가 없었는데, 이 공백이 1.23.0 "완전 영어" 과장(런타임 누출)을 통과시킨 근본 원인이었음 → 행위 회귀가드로 보강.
|
|
38
|
+
|
|
39
|
+
### 검증 결과 (런타임 버그 없음, 레이어 견고 확인)
|
|
40
|
+
- `_uiLang` 크래시 안전(try/catch + String 가드, malformed manifest/빈 값 → ko 폴백).
|
|
41
|
+
- arg 상호작용: `--language en` 값이 positional 로 누출 안 됨(앞/뒤 위치 모두), `task add "텍스트" --language en` 보존.
|
|
42
|
+
- `--language=en` equals 문법 동작, `--json` 출력 en 에서도 유효 JSON, flag > env > manifest 우선순위(en 프로젝트에서 `--language ko` 가 ko 강제).
|
|
43
|
+
|
|
44
|
+
### 변경 (defense-in-depth)
|
|
45
|
+
- **i18n 행위 e2e 회귀가드 1건 추가** (e2e 365→366): ① ko 프로젝트 기본 lens 한글 보존 ② `--language en` 런타임 영어 렌더 + 한글 0 ③ `--language` positional 무누출 ④ status path-not-found 에러 en/ko 분기. 소스가드가 못 잡는 *런타임 누출* 을 행위로 차단.
|
|
46
|
+
|
|
47
|
+
### 검증 (회귀 0)
|
|
48
|
+
- **selftest 244/244** · **E2E 366/366** (신규 i18n 행위가드 포함, ko 기본 무회귀).
|
|
49
|
+
- patch(1.25.1) — npm 미배포(R-0011, GitHub/CHANGELOG 누적).
|
|
50
|
+
|
|
3
51
|
## 1.25.0 — 2026-06-15 — 🛡️ [안정화/Stable] 마감 본문 정직성 + lens 플래그십 영어화 안정 minor
|
|
4
52
|
|
|
5
53
|
**🛡️ 안정화(Stable) minor — 자가 검증으로 잡은 정직성 수정 + 품질 렌즈 영어화를 npm 공개.** 직전 minor(1.24.0) 이후 누적된 패치 2건(1.24.1 + 1.24.2)을 검증·통합해 배포. R-0011 정책의 16번째 stable minor. 한국어 우선 기본은 그대로.
|
package/README.md
CHANGED
|
@@ -104,7 +104,7 @@ MIT
|
|
|
104
104
|
<!-- leerness:project-readme:start -->
|
|
105
105
|
## Leerness Project Harness
|
|
106
106
|
|
|
107
|
-
이 프로젝트는 Leerness v1.
|
|
107
|
+
이 프로젝트는 Leerness v1.26.0 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
108
108
|
|
|
109
109
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
110
110
|
|
|
@@ -158,7 +158,7 @@ leerness memory restore decision <date|title>
|
|
|
158
158
|
|
|
159
159
|
### MCP server (외부 AI 통합)
|
|
160
160
|
|
|
161
|
-
Leerness v1.
|
|
161
|
+
Leerness v1.26.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
|
|
162
162
|
|
|
163
163
|
```jsonc
|
|
164
164
|
// 카테고리별
|
|
@@ -179,7 +179,7 @@ Leerness v1.25.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code
|
|
|
179
179
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
180
180
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) stress-v* 신규 작성 + 누적 회귀 → 4) e2e 219/219 → 5) npm pack + git tag + GitHub release → 6) main 자동 push (1.9.140+) → 7) session close → 8) 다음 라운드 예약.
|
|
181
181
|
|
|
182
|
-
현재 누적: **70 라운드 (1.9.40 → 1.
|
|
182
|
+
현재 누적: **70 라운드 (1.9.40 → 1.26.0)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
183
183
|
|
|
184
184
|
### 성능 가이드 (1.9.140 측정)
|
|
185
185
|
|
|
@@ -217,6 +217,6 @@ leerness release pack --close --auto-main-push
|
|
|
217
217
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
218
218
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
219
219
|
|
|
220
|
-
Last synced by Leerness v1.
|
|
220
|
+
Last synced by Leerness v1.26.0: 2026-06-15
|
|
221
221
|
<!-- leerness:project-readme:end -->
|
|
222
222
|
|
package/bin/leerness.js
CHANGED
|
@@ -32,7 +32,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
|
|
|
32
32
|
// 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
|
|
33
33
|
const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST, _DEFAULT_PLATFORM_CONSTRAINTS, _DEFAULT_DOMAIN_CATALOG, _TOOL_CATALOG, _LSP_LANG_PATTERNS, OPTIMISM_PATTERNS, BUILT_IN_PERSONAS, STRINGS, BUILTIN_CATALOG, ROADMAP_STATUS_LABEL, ROADMAP_STATUS_COLOR, SECRET_PATTERNS, MERGE_OVERWRITE_FILES, MINIMAL_SKIP_KEYS, REQUIRED_WORKSPACE_FILES, KEYWORD_STOPWORDS, SKILL_CATALOG_PRESETS } = require('../lib/catalogs'); // 1.9.344/368/369 (UR-0025): catalog 분리 · 1.11.4 (UR-0007): _TOOL_CATALOG
|
|
34
34
|
|
|
35
|
-
const VERSION = '1.
|
|
35
|
+
const VERSION = '1.26.0';
|
|
36
36
|
|
|
37
37
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
38
38
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -3791,6 +3791,14 @@ function _selfTestCases() {
|
|
|
3791
3791
|
const koPreserved = bin.includes('완료 ${done}/${total}') && bin.includes('roadmap.html 자동 갱신 (${trigger})') && sc.includes("t('- 없음', '- none')"); // ko 인자 보존
|
|
3792
3792
|
return rowsEn && retroEn && roadmapEn && koPreserved;
|
|
3793
3793
|
} },
|
|
3794
|
+
{ name: 'CLI 영어화 Phase 9 (1.25.2, UR-0010): health 진단 영어/한국어 보존 + uiLang 주입 (소스 가드)', run: () => {
|
|
3795
|
+
const bin = read(__filename);
|
|
3796
|
+
const h = read(path.join(path.dirname(__filename), '..', 'lib', 'health.js'));
|
|
3797
|
+
const injected = bin.includes('uiLang: _uiLang(root), harnessPath: __filename, listAllSkills');
|
|
3798
|
+
const en = h.includes('## Security') && h.includes('command calls:') && h.includes('6-capability matrix') && h.includes('web bridge present, playwright not installed');
|
|
3799
|
+
const koPreserved = h.includes('## 보안') && h.includes('명령 호출:') && h.includes('6능력 매트릭스') && h.includes('playwright 미설치'); // ko 인자 보존(e2e ko)
|
|
3800
|
+
return injected && en && koPreserved;
|
|
3801
|
+
} },
|
|
3794
3802
|
{ name: 'CLI 영어화 Phase 8 (1.24.2, UR-0010): lens 영어 병렬필드 + 렌더 영어/한국어 보존 (소스 가드)', run: () => {
|
|
3795
3803
|
// 카탈로그 영어 병렬필드 + ko verbatim 동시 보존 + 렌더 영어 분기
|
|
3796
3804
|
const enFields = LENS_CATALOG.code.questionsEn && LENS_CATALOG.code.questionsEn.length === LENS_CATALOG.code.questions.length
|
|
@@ -18986,7 +18994,7 @@ async function deployAutoCmd(root, service) {
|
|
|
18986
18994
|
// 1.9.85: leerness health — 종합 헬스 체크 (drift + 보안 + skill + MCP + 누적)
|
|
18987
18995
|
const _health = require('../lib/health');
|
|
18988
18996
|
// 1.9.423 (UR-0025/UR-0125 큰 핸들러 모듈화 8번째): healthCmd → lib/health.js (DI 위임, thin wrapper)
|
|
18989
|
-
function healthCmd(root) { return _health.healthCmd(root, { VERSION, STATUSES, has, arg, harnessPath: __filename, listAllSkills, planPath, readProgressRows, readRules, envDiff, _collectSecretFindings, _readUsageStats, _loadDecisions, _loadLessons, _loadShellFailures, _readFeatureGraph, _scanShellScriptsEncoding, _shellEnvDrift, _computeMilestones, _computeRecentChanges, _computeRoundHistory, _collectPyFiles, _analyzePyFile, _collectRuntimeEnv, _listAPISkills, _matchAPISkills, _mcpToolCount }); }
|
|
18997
|
+
function healthCmd(root) { return _health.healthCmd(root, { VERSION, STATUSES, has, arg, uiLang: _uiLang(root), harnessPath: __filename, listAllSkills, planPath, readProgressRows, readRules, envDiff, _collectSecretFindings, _readUsageStats, _loadDecisions, _loadLessons, _loadShellFailures, _readFeatureGraph, _scanShellScriptsEncoding, _shellEnvDrift, _computeMilestones, _computeRecentChanges, _computeRoundHistory, _collectPyFiles, _analyzePyFile, _collectRuntimeEnv, _listAPISkills, _matchAPISkills, _mcpToolCount }); }
|
|
18990
18998
|
|
|
18991
18999
|
function usageStatsCmd(root) {
|
|
18992
19000
|
root = absRoot(root || process.cwd());
|
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 };
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -6202,5 +6202,40 @@ total++;
|
|
|
6202
6202
|
if (!ok) failed++;
|
|
6203
6203
|
}
|
|
6204
6204
|
|
|
6205
|
+
// 1.25.1 (22nd 버그헌트 → i18n 행위 회귀 가드, UR-0010): --language en 런타임 렌더가 실제로 영어인지 + ko 기본 보존 + --language 값이 positional 로 누출 안 되는지 행위로 검증.
|
|
6206
|
+
// 소스가드(문자열 존재)만으로는 1.23.0 "session close 완전 영어" 과장(런타임 한글 누출)을 못 잡았던 공백을 e2e 로 보강(defense-in-depth).
|
|
6207
|
+
total++;
|
|
6208
|
+
{
|
|
6209
|
+
let ok = false;
|
|
6210
|
+
try {
|
|
6211
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-i18n-'));
|
|
6212
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
|
|
6213
|
+
const H = /[가-힣]/;
|
|
6214
|
+
const out = (r) => (r.stdout || '') + (r.stderr || '');
|
|
6215
|
+
// ① 기본(ko 프로젝트, 플래그 없음): lens 한글 보존
|
|
6216
|
+
const lensKo = out(cp.spawnSync(process.execPath, [CLI, 'lens', '--path', d], { encoding: 'utf8', timeout: 15000 }));
|
|
6217
|
+
const lensKoOk = /분야별 자기질문 품질 렌즈/.test(lensKo);
|
|
6218
|
+
// ② 영어 opt-in(ko 프로젝트라도 flag 가 manifest 를 이김): lens 영어 렌더 + 한글 0
|
|
6219
|
+
const lensEn = out(cp.spawnSync(process.execPath, [CLI, 'lens', '--language', 'en', '--path', d], { encoding: 'utf8', timeout: 15000 }));
|
|
6220
|
+
const lensEnOk = /quality self-question lenses/.test(lensEn) && !H.test(lensEn);
|
|
6221
|
+
// ③ --language en 값이 positional 로 누출 안 됨: task add 텍스트 보존, request="en" 인 task 없음
|
|
6222
|
+
cp.spawnSync(process.execPath, [CLI, 'task', 'add', 'I18N_TASK_E2E', '--language', 'en', '--path', d], { encoding: 'utf8', timeout: 15000 });
|
|
6223
|
+
const tl = out(cp.spawnSync(process.execPath, [CLI, 'task', 'list', '--json', '--path', d], { encoding: 'utf8', timeout: 15000 }));
|
|
6224
|
+
const noLeak = tl.includes('I18N_TASK_E2E') && !/"request"\s*:\s*"en"/.test(tl);
|
|
6225
|
+
// ④ status path-not-found 에러: en 영어 / ko 한글 (failJson 분기)
|
|
6226
|
+
const stEn = out(cp.spawnSync(process.execPath, [CLI, 'status', path.join(d, 'nope'), '--language', 'en', '--json'], { encoding: 'utf8', timeout: 15000 }));
|
|
6227
|
+
const stKo = out(cp.spawnSync(process.execPath, [CLI, 'status', path.join(d, 'nope'), '--json'], { encoding: 'utf8', timeout: 15000 }));
|
|
6228
|
+
const stOk = /path not found/.test(stEn) && /경로 없음/.test(stKo);
|
|
6229
|
+
// ⑤ (1.25.2 Phase 9) health: en 렌더 영어(한글 0) + ko 기본 한글 보존
|
|
6230
|
+
const hEn = out(cp.spawnSync(process.execPath, [CLI, 'health', '--language', 'en', '--path', d], { encoding: 'utf8', timeout: 20000 }));
|
|
6231
|
+
const hKo = out(cp.spawnSync(process.execPath, [CLI, 'health', '--path', d], { encoding: 'utf8', timeout: 20000 }));
|
|
6232
|
+
const healthOk = /## Security/.test(hEn) && !H.test(hEn) && /## 보안/.test(hKo);
|
|
6233
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
6234
|
+
ok = lensKoOk && lensEnOk && noLeak && stOk && healthOk;
|
|
6235
|
+
} catch {}
|
|
6236
|
+
console.log(ok ? '✓ B(1.25.1/1.25.2) i18n 행위: --language en 런타임 영어(lens/health) + ko 기본 보존 + --language positional 무누출 + status 에러 en/ko (UR-0010)' : '✗ i18n 행위 회귀 가드 실패');
|
|
6237
|
+
if (!ok) failed++;
|
|
6238
|
+
}
|
|
6239
|
+
|
|
6205
6240
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
6206
6241
|
if (failed > 0) process.exit(1);
|