leerness 1.9.84 → 1.9.85
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 +26 -0
- package/README.md +3 -2
- package/bin/harness.js +117 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.85 — 2026-05-20
|
|
4
|
+
|
|
5
|
+
**`leerness health` 새 명령** — 종합 헬스 체크 (drift + 보안 + skill + MCP + 누적 통계).
|
|
6
|
+
|
|
7
|
+
### Added — `leerness health [<path>]`
|
|
8
|
+
- 종합 진단 한 번에:
|
|
9
|
+
- `drift`: level + score + fired 개수
|
|
10
|
+
- `보안`: .env 존재 / .gitignore에 .env 포함 / .env.example 누락 키 / .gitignore 시크릿 패턴 누락
|
|
11
|
+
- `skills`: 설치 수 / skill query 누적 (rolling history)
|
|
12
|
+
- `usage`: 명령 호출 총수 + 종류 / MCP 호출 총수 + 종류 / since
|
|
13
|
+
- `tasks`: progress-tracker 총수 + 상태별 카운트
|
|
14
|
+
- **`issues`** 배열에 발견된 모든 문제 자동 집계.
|
|
15
|
+
- `--json`: 구조화된 JSON 출력 (CI 친화).
|
|
16
|
+
- `--strict`: issue ≥ 1 시 exit 1.
|
|
17
|
+
|
|
18
|
+
### Use Case
|
|
19
|
+
- 사용자: `leerness health .` 한 줄로 워크스페이스 전체 상태 즉시 확인.
|
|
20
|
+
- CI 통합: `leerness health . --strict` 로 보안/drift 문제 자동 감지.
|
|
21
|
+
- 1.9.78 drift + 1.9.75/76/80 보안 + 1.9.70 MCP 통계 + 1.9.79 skill suggest의 모든 신호를 한 곳에 집계.
|
|
22
|
+
|
|
23
|
+
### Verified
|
|
24
|
+
- stress-v31 — health 출력 / --json / --strict / 누적 회귀.
|
|
25
|
+
- e2e 219/219 PASS 유지.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
3
29
|
## 1.9.84 — 2026-05-20
|
|
4
30
|
|
|
5
31
|
**MCP server 17번째 도구 `leerness_skill_list` 추가** (외부 AI에 skill 목록 조회 노출).
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **AI 코딩 에이전트의 거짓 완료·중복·망각·충돌을 막아주는 검수·기억·협업 CLI 하네스.**
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/leerness) [](https://www.npmjs.com/package/leerness) []() []() []()
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
╔══════════════════════════════════════════════════════════════╗
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
║ ██║ ██╔══╝ ██╔══╝ ██╔══██╗██║╚██╗██║██╔══╝ ╚════██║ ║
|
|
13
13
|
║ ███████╗███████╗███████╗██║ ██║██║ ╚████║███████╗███████║ ║
|
|
14
14
|
║ ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝ ║
|
|
15
|
-
║ v1.9.
|
|
15
|
+
║ v1.9.85 AI Agent Reliability Harness ║
|
|
16
16
|
║ verify · remember · orchestrate · audit · prevent drift ║
|
|
17
17
|
╚══════════════════════════════════════════════════════════════╝
|
|
18
18
|
```
|
|
@@ -433,6 +433,7 @@ npm test # = node ./scripts/e2e.js
|
|
|
433
433
|
|
|
434
434
|
## 변경 이력 (최근)
|
|
435
435
|
|
|
436
|
+
- **1.9.85** — **`leerness health` 새 명령** — drift + 보안 + skill + MCP + 누적 통계 종합 헬스 체크. `--json` / `--strict` 옵션.
|
|
436
437
|
- **1.9.84** — **MCP server 17번째 도구 `leerness_skill_list`** — 외부 AI에 skill 카탈로그 (id/이름/출처/능력/사용 횟수) 조회 노출. `skill list --json` 옵션도 함께 추가.
|
|
437
438
|
- **1.9.83** — **MCP server 16번째 도구 `leerness_skill_match`** — 1.9.45/50/68 skill match를 외부 AI에 노출. rolling history 자동 누적.
|
|
438
439
|
- **1.9.82** — **`drift check --auto-fix` 보안 회복 통합** — 1.9.78 보안 신호 발견 시 `audit --fix` 자동 실행 → `.gitignore` + `.env.example` 동기화 후 재검사. 보안 + 세션 마감 한 번에 자동 회복.
|
package/bin/harness.js
CHANGED
|
@@ -6,7 +6,7 @@ const path = require('path');
|
|
|
6
6
|
const cp = require('child_process');
|
|
7
7
|
const readline = require('readline');
|
|
8
8
|
|
|
9
|
-
const VERSION = '1.9.
|
|
9
|
+
const VERSION = '1.9.85';
|
|
10
10
|
const MARK = '<!-- leerness:managed -->';
|
|
11
11
|
const README_START = '<!-- leerness:project-readme:start -->';
|
|
12
12
|
const README_END = '<!-- leerness:project-readme:end -->';
|
|
@@ -3398,12 +3398,13 @@ function _banner(opts = {}) {
|
|
|
3398
3398
|
lines.push('');
|
|
3399
3399
|
for (const ln of lines) log(ln);
|
|
3400
3400
|
if (opts.quickStart) {
|
|
3401
|
-
log(C.bold(C.cyan(' ✨ 빠른 시작 (1.9.
|
|
3401
|
+
log(C.bold(C.cyan(' ✨ 빠른 시작 (1.9.85+ 워크플로)')));
|
|
3402
3402
|
log(' ' + C.green('npx leerness@latest init .') + C.dim(' # 신규 프로젝트 + 외부 AI CLI 설정'));
|
|
3403
3403
|
log(' ' + C.green('npx leerness handoff .') + C.dim(' # 컨텍스트 + lessons + 매칭 skill + 이전 history hit (1.9.69)'));
|
|
3404
3404
|
log(' ' + C.green('npx leerness skill match "<query>"') + C.dim(' # 매칭 skill + rolling history 자동 누적 (1.9.68)'));
|
|
3405
3405
|
log(' ' + C.green('npx leerness verify-claim T-0001 --run-tests') + C.dim(' # AI 거짓 완료 자동 검증'));
|
|
3406
3406
|
log(' ' + C.green('npx leerness env check .') + C.dim(' # .env ↔ .env.example 동기화 검사 (1.9.71)'));
|
|
3407
|
+
log(' ' + C.green('npx leerness health .') + C.dim(' # 종합 헬스 체크 — drift + 보안 + skill + MCP (1.9.85)'));
|
|
3407
3408
|
log(' ' + C.green('npx leerness session close .') + C.dim(' # 마감 + 다음 라운드 추천 (default)'));
|
|
3408
3409
|
log('');
|
|
3409
3410
|
log(C.bold(C.cyan(' 🤖 메인 에이전트 (Claude/Cursor/Copilot)용')));
|
|
@@ -7581,6 +7582,118 @@ function envSyncCmd(root) {
|
|
|
7581
7582
|
for (const k of d.inEnvOnly) log(` + ${k}=`);
|
|
7582
7583
|
}
|
|
7583
7584
|
|
|
7585
|
+
// 1.9.85: leerness health — 종합 헬스 체크 (drift + 보안 + skill + MCP + 누적)
|
|
7586
|
+
function healthCmd(root) {
|
|
7587
|
+
root = absRoot(root || process.cwd());
|
|
7588
|
+
const out = { root, generatedAt: new Date().toISOString(), checks: {} };
|
|
7589
|
+
// 1) drift level
|
|
7590
|
+
try {
|
|
7591
|
+
const r = cp.spawnSync(process.execPath, [__filename, 'drift', 'check', root, '--json'],
|
|
7592
|
+
{ encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '0' } });
|
|
7593
|
+
const j = JSON.parse(r.stdout.trim());
|
|
7594
|
+
out.checks.drift = { level: j.level, score: j.score, firedCount: (j.fired || []).length };
|
|
7595
|
+
} catch { out.checks.drift = { error: 'drift check 실패' }; }
|
|
7596
|
+
// 2) 보안 상태 (env + .gitignore)
|
|
7597
|
+
try {
|
|
7598
|
+
const envPath = path.join(root, '.env');
|
|
7599
|
+
if (exists(envPath)) {
|
|
7600
|
+
const d = envDiff(root);
|
|
7601
|
+
const giText = exists(path.join(root, '.gitignore')) ? read(path.join(root, '.gitignore')) : '';
|
|
7602
|
+
const giLines = giText.split('\n').map(l => l.trim());
|
|
7603
|
+
const envInGi = giLines.includes('.env') || giLines.includes('/.env');
|
|
7604
|
+
const SECRET_PATTERNS = ['.env', '.env.local', '.env.production', '.env.*.local', '*.pem', 'credentials.json'];
|
|
7605
|
+
const missingSecrets = SECRET_PATTERNS.filter(p => !giLines.some(l => l === p || l === '/' + p));
|
|
7606
|
+
out.checks.security = {
|
|
7607
|
+
hasDotEnv: true,
|
|
7608
|
+
envInGitignore: envInGi,
|
|
7609
|
+
envExampleMissing: d.inEnvOnly,
|
|
7610
|
+
gitignoreMissingSecrets: missingSecrets,
|
|
7611
|
+
critical: !envInGi
|
|
7612
|
+
};
|
|
7613
|
+
} else {
|
|
7614
|
+
out.checks.security = { hasDotEnv: false, ok: true };
|
|
7615
|
+
}
|
|
7616
|
+
} catch { out.checks.security = { error: '보안 점검 실패' }; }
|
|
7617
|
+
// 3) skill 수 + skill query 누적
|
|
7618
|
+
try {
|
|
7619
|
+
const all = listAllSkills(root);
|
|
7620
|
+
const skillCount = Object.keys(all).length;
|
|
7621
|
+
let queryCount = 0;
|
|
7622
|
+
const histPath = path.join(root, '.harness', 'skill-suggestions.md');
|
|
7623
|
+
if (exists(histPath)) {
|
|
7624
|
+
queryCount = (read(histPath).match(/^## [\d-]+ [\d:]+ — query/gm) || []).length;
|
|
7625
|
+
}
|
|
7626
|
+
out.checks.skills = { installed: skillCount, queryHistoryCount: queryCount };
|
|
7627
|
+
} catch { out.checks.skills = { error: 'skill 점검 실패' }; }
|
|
7628
|
+
// 4) MCP + 명령 호출 누적
|
|
7629
|
+
try {
|
|
7630
|
+
const stats = _readUsageStats(root);
|
|
7631
|
+
const cmdTotal = Object.values(stats.commands || {}).reduce((s, n) => s + n, 0);
|
|
7632
|
+
const mcpTotal = stats.mcp?.tools ? Object.values(stats.mcp.tools).reduce((s, n) => s + n, 0) : 0;
|
|
7633
|
+
out.checks.usage = {
|
|
7634
|
+
commandTotal: cmdTotal,
|
|
7635
|
+
commandKinds: Object.keys(stats.commands || {}).length,
|
|
7636
|
+
mcpTotal,
|
|
7637
|
+
mcpToolKinds: stats.mcp?.tools ? Object.keys(stats.mcp.tools).length : 0,
|
|
7638
|
+
since: stats.since || null
|
|
7639
|
+
};
|
|
7640
|
+
} catch { out.checks.usage = { error: 'usage 점검 실패' }; }
|
|
7641
|
+
// 5) tasks (progress-tracker)
|
|
7642
|
+
try {
|
|
7643
|
+
const rows = readProgressRows(root);
|
|
7644
|
+
const byStatus = {};
|
|
7645
|
+
for (const r of rows) byStatus[r.status] = (byStatus[r.status] || 0) + 1;
|
|
7646
|
+
out.checks.tasks = { total: rows.length, byStatus };
|
|
7647
|
+
} catch { out.checks.tasks = { error: 'tasks 점검 실패' }; }
|
|
7648
|
+
// 6) issues 요약 (사용자 글로벌 룰 가시화)
|
|
7649
|
+
const issues = [];
|
|
7650
|
+
if (out.checks.drift?.level && !/healthy/.test(out.checks.drift.level)) issues.push(`drift ${out.checks.drift.level}`);
|
|
7651
|
+
if (out.checks.security?.critical) issues.push('🚨 .env가 .gitignore에 누락 (보안 CRITICAL)');
|
|
7652
|
+
if (out.checks.security?.envExampleMissing?.length) issues.push(`.env→.env.example 누락 ${out.checks.security.envExampleMissing.length}건`);
|
|
7653
|
+
if (out.checks.security?.gitignoreMissingSecrets?.length) issues.push(`.gitignore 시크릿 누락 ${out.checks.security.gitignoreMissingSecrets.length}건`);
|
|
7654
|
+
out.issues = issues;
|
|
7655
|
+
out.healthy = issues.length === 0;
|
|
7656
|
+
|
|
7657
|
+
// --strict: issue 있으면 exit 1
|
|
7658
|
+
if (has('--strict') && !out.healthy) process.exitCode = 1;
|
|
7659
|
+
|
|
7660
|
+
if (has('--json')) { log(JSON.stringify(out, null, 2)); return; }
|
|
7661
|
+
log(`# leerness health (1.9.85)`);
|
|
7662
|
+
log(`Date: ${out.generatedAt}`);
|
|
7663
|
+
log(`Status: ${out.healthy ? '✅ healthy' : `⚠ ${issues.length} issues`}`);
|
|
7664
|
+
log('');
|
|
7665
|
+
log(`## drift`);
|
|
7666
|
+
log(` level: ${out.checks.drift?.level || 'n/a'} (score ${out.checks.drift?.score || 0}, fired ${out.checks.drift?.firedCount || 0})`);
|
|
7667
|
+
log('');
|
|
7668
|
+
log(`## 보안`);
|
|
7669
|
+
if (out.checks.security?.hasDotEnv) {
|
|
7670
|
+
log(` .env 존재 · .gitignore에 .env 포함: ${out.checks.security.envInGitignore ? '✓' : '✗ CRITICAL'}`);
|
|
7671
|
+
log(` .env.example 누락 키: ${out.checks.security.envExampleMissing?.length || 0}건`);
|
|
7672
|
+
log(` .gitignore 시크릿 패턴 누락: ${out.checks.security.gitignoreMissingSecrets?.length || 0}건`);
|
|
7673
|
+
} else {
|
|
7674
|
+
log(` .env 없음 (검증 불필요)`);
|
|
7675
|
+
}
|
|
7676
|
+
log('');
|
|
7677
|
+
log(`## skills`);
|
|
7678
|
+
log(` 설치: ${out.checks.skills?.installed || 0}개 · skill query 누적: ${out.checks.skills?.queryHistoryCount || 0}회`);
|
|
7679
|
+
log('');
|
|
7680
|
+
log(`## usage`);
|
|
7681
|
+
log(` 명령 호출: ${out.checks.usage?.commandTotal || 0}회 / ${out.checks.usage?.commandKinds || 0}종`);
|
|
7682
|
+
log(` MCP 호출: ${out.checks.usage?.mcpTotal || 0}회 / ${out.checks.usage?.mcpToolKinds || 0}종 도구`);
|
|
7683
|
+
log(` since: ${out.checks.usage?.since || 'unknown'}`);
|
|
7684
|
+
log('');
|
|
7685
|
+
log(`## tasks`);
|
|
7686
|
+
const tb = out.checks.tasks?.byStatus || {};
|
|
7687
|
+
log(` 총 ${out.checks.tasks?.total || 0}건: ${Object.entries(tb).map(([s, n]) => `${s}=${n}`).join(', ') || '없음'}`);
|
|
7688
|
+
if (issues.length) {
|
|
7689
|
+
log('');
|
|
7690
|
+
log(`## ⚠ Issues (${issues.length})`);
|
|
7691
|
+
for (const i of issues) log(` - ${i}`);
|
|
7692
|
+
log('');
|
|
7693
|
+
log(`💡 자동 회복: leerness drift check --auto-fix · leerness audit --fix`);
|
|
7694
|
+
}
|
|
7695
|
+
}
|
|
7696
|
+
|
|
7584
7697
|
function usageStatsCmd(root) {
|
|
7585
7698
|
root = absRoot(root || process.cwd());
|
|
7586
7699
|
const stats = _readUsageStats(root);
|
|
@@ -7912,6 +8025,8 @@ async function main() {
|
|
|
7912
8025
|
// 1.9.71: leerness env check / sync — .env vs .env.example 자동 동기화
|
|
7913
8026
|
if (cmd === 'env' && args[1] === 'check') return envCheckCmd(args[2] || arg('--path', process.cwd()));
|
|
7914
8027
|
if (cmd === 'env' && args[1] === 'sync') return envSyncCmd(args[2] || arg('--path', process.cwd()));
|
|
8028
|
+
// 1.9.85: leerness health — 종합 헬스 체크
|
|
8029
|
+
if (cmd === 'health') return healthCmd(args[1] || arg('--path', process.cwd()));
|
|
7915
8030
|
if (cmd === 'whats-new') return whatsNewCmd(args[1] || arg('--path', process.cwd()));
|
|
7916
8031
|
if (cmd === 'reuse' && args[1] === 'autodetect') return reuseAutodetectCmd(args[2] || arg('--path', process.cwd()));
|
|
7917
8032
|
if (cmd === 'setup-agents' || cmd === 'setup' && args[1] === 'agents') return await setupAgentsCmd(args[1] && args[1] !== 'agents' ? args[1] : (args[2] || process.cwd()));
|