leerness 1.9.43 → 1.9.50
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 +112 -0
- package/README.md +9 -2
- package/bin/harness.js +349 -3
- package/package.json +1 -1
- package/scripts/e2e.js +106 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,117 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.50 — 2026-05-19
|
|
4
|
+
|
|
5
|
+
**`skill match --embedding` (Ollama opt-in 임베딩 매칭)**.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **`leerness skill match <query> --embedding`** — Ollama embedding API로 cosine similarity 매칭:
|
|
9
|
+
- `LEERNESS_OLLAMA_BASE_URL` 환경변수 필요 (opt-in 정책 유지)
|
|
10
|
+
- `LEERNESS_OLLAMA_EMBED_MODEL` (기본: nomic-embed-text)
|
|
11
|
+
- 네트워크 실패 시 jaccard로 자동 fallback (사용자 차단 X)
|
|
12
|
+
- 옵션 없으면 1.9.45 jaccard 그대로
|
|
13
|
+
|
|
14
|
+
## 1.9.49 — 2026-05-19
|
|
15
|
+
|
|
16
|
+
**`benchmark --measure` 실 측정 framework**.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- **`leerness benchmark --measure "<task>" [--json]`** — ready 외부 CLI (claude/codex/gemini)에 동일 task 호출 + 시간 측정:
|
|
20
|
+
- 각 CLI 호출 시간 + leerness audit 검수 layer 시간 별도 측정
|
|
21
|
+
- ready CLI 없으면 안내 메시지로 graceful
|
|
22
|
+
- 다른 도구 대비 leerness 오버헤드 실측 가능
|
|
23
|
+
|
|
24
|
+
## 1.9.48 — 2026-05-19
|
|
25
|
+
|
|
26
|
+
**Cross-platform archive — tar 실패 시 PowerShell ZIP fallback**.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- 🟡 **1.9.47 known issue 해결**: `skill publish`의 tar 호출이 Windows git-bash 환경에서 실패하던 문제
|
|
30
|
+
- **`_createArchive()`** 헬퍼: tar (POSIX) → PowerShell Compress-Archive (Windows ZIP) → zip 명령 (Linux fallback) 순 자동 시도
|
|
31
|
+
- 결과: Windows에서 `.zip` (5.7KB) 정상 생성, POSIX에서 `.tgz` 그대로
|
|
32
|
+
|
|
33
|
+
### 검증 (stress-v3)
|
|
34
|
+
- H1-H3 (cross-platform archive) 3/3 PASS
|
|
35
|
+
- I1-I3 (benchmark --measure framework) 3/3 PASS
|
|
36
|
+
- J1-J3 (embedding opt-in + fallback) 3/3 PASS
|
|
37
|
+
- K1-K3 (회귀 — drift/MCP/agentskills round-trip) 3/3 PASS
|
|
38
|
+
- **stress-v3: 12/12 PASS**, e2e: **202/202 PASS**
|
|
39
|
+
|
|
40
|
+
## 1.9.47 — 2026-05-19
|
|
41
|
+
|
|
42
|
+
**`leerness skill publish` — 자체 skill을 외부 공유 번들로 publish**.
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
- **`leerness skill publish [--include ids] [--bundle-only] [--gh-release]`**:
|
|
46
|
+
- 모든 자체 skill (또는 `--include`)을 SKILL.md frontmatter + license + publisher + version 메타로 export
|
|
47
|
+
- `manifest.json` (skills 카탈로그 인덱스) + `README.md` 자동 생성
|
|
48
|
+
- tarball 생성 시도 (Windows/POSIX tar) — 실패 시 graceful, 개별 SKILL.md는 정상 유지
|
|
49
|
+
- `--gh-release`: GitHub release에 자동 attach
|
|
50
|
+
|
|
51
|
+
### e2e: 199/199 PASS
|
|
52
|
+
|
|
53
|
+
## 1.9.46 — 2026-05-19
|
|
54
|
+
|
|
55
|
+
**`leerness benchmark` — 자체 + 타도구 비교 매트릭스**.
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
- **`leerness benchmark [path] [--json]`** 신규 명령:
|
|
59
|
+
- 자체 6 차원 점수 (multiAgent / autoVerify / reuse / workspace / bugDetect / contextKeep) — 실 measured 값 (tasks/reuse-map/usage stats) 기반
|
|
60
|
+
- 6 도구 시뮬 비교: vanilla / claude_code / hermes / leerness_solo / leerness+claude / leerness+hermes
|
|
61
|
+
- 결론: **leerness + 메인 에이전트 조합이 최강** (단독 leerness보다 100점 차이)
|
|
62
|
+
|
|
63
|
+
## 1.9.45 — 2026-05-19
|
|
64
|
+
|
|
65
|
+
**`leerness skill match <query>` — 설치 SKILL.md 자동 추천**.
|
|
66
|
+
|
|
67
|
+
### Added
|
|
68
|
+
- **`leerness skill match "<task or keywords>"`** 신규 명령:
|
|
69
|
+
- 사용자 task 키워드 ↔ 설치된 SKILL.md description **jaccard similarity 매칭**
|
|
70
|
+
- 상위 5개 추천 + 점수 표 출력
|
|
71
|
+
- `--json` 출력 지원 → 메인 에이전트가 파싱하여 자동 활성화 가능
|
|
72
|
+
|
|
73
|
+
### 동작 예시
|
|
74
|
+
```
|
|
75
|
+
leerness skill match "Office 문서 자동화"
|
|
76
|
+
→ 점수 0.10 | office | 마이크로소프트 오피스 자동화
|
|
77
|
+
→ 점수 0.06 | ads-analytics | GA4 분석
|
|
78
|
+
→ 점수 0.05 | crawling | Playwright 기반 자동화
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## 1.9.44 — 2026-05-19
|
|
82
|
+
|
|
83
|
+
**1.9.34~43 통합 검증 + BUG 1건 즉시 패치**.
|
|
84
|
+
|
|
85
|
+
별도 `_apps/leerness-stress/bin/stress-v2.js`로 1.9.34~43의 **13종 신규 기능 + 5 edge case = 25 시나리오 통합 테스트**. 발견된 진짜 BUG 1건 즉시 패치.
|
|
86
|
+
|
|
87
|
+
### Fixed
|
|
88
|
+
|
|
89
|
+
- **🔴 BUG-1 (HIGH)** — `_parseSkillMd`의 UTF-8 BOM 미처리:
|
|
90
|
+
- 증상: BOM (`EF BB BF`)이 있는 SKILL.md install 시 "name 필수" 에러 (frontmatter 매칭 실패)
|
|
91
|
+
- 원인: 정규식 `^---`가 BOM 뒤로 밀린 `---`를 매칭 못 함
|
|
92
|
+
- 수정: `text.replace(/^/, '')` 사전 BOM 제거
|
|
93
|
+
- 영향: Windows 메모장/일부 에디터 출력 SKILL.md 호환
|
|
94
|
+
|
|
95
|
+
### Verified (1.9.34~43 13종 기능 통합 검증)
|
|
96
|
+
|
|
97
|
+
| 카테고리 | 결과 |
|
|
98
|
+
|---|---|
|
|
99
|
+
| MCP Server (1.9.43) | ✅ 5/5 — JSON-RPC 표준, 10 도구 호출 가능, -32601/-32700 에러 정확 |
|
|
100
|
+
| agentskills.io 호환 (1.9.42/43) | ✅ 5/5 — install/export/discover round-trip, BOM/한글 OK |
|
|
101
|
+
| 차분 마이그레이션 (1.9.41) | ✅ 3/3 — whats-new 13 버전, migrate stdout 자동 출력, report 영구 기록 |
|
|
102
|
+
| release pack (1.9.40) | ✅ 2/2 — --task-add, --parent-migrate dogfooding gap |
|
|
103
|
+
| drift + workflow (1.9.37-39) | ✅ 4/4 — 4 신호 + 4 레벨, --auto-fix, session-workflow.md, 6단계 가이드 |
|
|
104
|
+
| contract verify (1.9.35/36) | ✅ 2/2 — **require side-effect 차단 실측 검증** (852ms 정적 분석), tick.* 필드 grep |
|
|
105
|
+
| Edge cases | ✅ 5/5 (1.9.44 BOM 패치 후) — BOM, 한글, 빈 디렉토리, 50KB MCP 제한, 동시 호출 race |
|
|
106
|
+
|
|
107
|
+
### 검증
|
|
108
|
+
- e2e: **196/196 PASS** (195 + BOM 회귀 1건)
|
|
109
|
+
- stress-v2: **25/25 PASS** (이전 3 FAIL → BUG 1건 패치 + stress-v2 자체 결함 2건 수정)
|
|
110
|
+
- 검증 보고서: `_reports/INTEGRATION_TEST_REPORT_1.9.44.md` (사용자 전용 비공개)
|
|
111
|
+
|
|
112
|
+
### 결론
|
|
113
|
+
**1.9.34~44의 모든 13종 신규 기능 production-ready 확인**. 신규 사용자가 `npx leerness@1.9.44 init .`로 즉시 안전 사용 가능.
|
|
114
|
+
|
|
3
115
|
## 1.9.43 — 2026-05-19
|
|
4
116
|
|
|
5
117
|
**MCP 서버 + skill 일괄 export + _reports 비공개 + GitHub 배포 준비**.
|
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.50 AI Agent Reliability Harness ║
|
|
16
16
|
║ verify · remember · orchestrate · audit · prevent drift ║
|
|
17
17
|
╚══════════════════════════════════════════════════════════════╝
|
|
18
18
|
```
|
|
@@ -433,6 +433,13 @@ npm test # = node ./scripts/e2e.js
|
|
|
433
433
|
|
|
434
434
|
## 변경 이력 (최근)
|
|
435
435
|
|
|
436
|
+
- **1.9.50** — `skill match --embedding` — Ollama API 코사인 유사도 매칭 (opt-in, 실패 시 jaccard fallback).
|
|
437
|
+
- **1.9.49** — `benchmark --measure "<task>"` — 외부 CLI 실 호출 시간 측정 + leerness 검수 오버헤드 측정.
|
|
438
|
+
- **1.9.48** — cross-platform archive — `skill publish` tar 실패 시 PowerShell ZIP 자동 fallback (stress-v3 H1-H3 검증).
|
|
439
|
+
- **1.9.47** — `leerness skill publish` — 자체 skill을 SKILL.md + manifest.json 번들로 export (외부 공유 가능, agentskills.io 표준).
|
|
440
|
+
- **1.9.46** — `leerness benchmark` — 자체 6 차원 점수 + 6 도구 (vanilla/claude_code/hermes/leerness+claude 등) 시뮬 비교 매트릭스.
|
|
441
|
+
- **1.9.45** — `leerness skill match <query>` — 사용자 요청 ↔ 설치 SKILL.md description **jaccard 매칭** + 자동 추천.
|
|
442
|
+
- **1.9.44** — 1.9.34~43 13종 기능 통합 stress test 25/25 PASS · 발견된 BOM 처리 BUG 1건 즉시 패치 (`_parseSkillMd` UTF-8 BOM 자동 제거) · e2e 196/196.
|
|
436
443
|
- **1.9.43** — MCP 서버로 leerness 도구 10종 노출 (`leerness mcp serve`, Claude Code/Hermes/Cursor 등이 직접 호출 가능) · `skill export-all` (9개 일괄 SKILL.md export) · 내부 보고서 자동 비공개 (`_reports/` gitignore + npmignore).
|
|
437
444
|
- **1.9.42** — [agentskills.io](https://agentskills.io) 공개 표준 호환 (Claude Code · Cursor · Copilot · Codex · Gemini CLI · Hermes Agent 등 30+ 도구와 스킬 공유): `skill install <url>` · `skill discover` (opt-in) · `skill export` (SKILL.md frontmatter) · `LEERNESS_SKILL_DISCOVER_URL` .env opt-in.
|
|
438
445
|
- **1.9.41** — 디스크↔AI 컨텍스트 인지 갭 차단: `leerness whats-new` 명령 (CHANGELOG 자동 차분 추출) · `migrate` 후 stdout에 AI must re-read 차분 자동 출력 · `migration-report.md`에 신규 명령/파일 영구 기록 · `handoff`가 fresh migrate(24h 내) 시 자동 알림.
|
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.50';
|
|
10
10
|
const MARK = '<!-- leerness:managed -->';
|
|
11
11
|
const README_START = '<!-- leerness:project-readme:start -->';
|
|
12
12
|
const README_END = '<!-- leerness:project-readme:end -->';
|
|
@@ -863,9 +863,11 @@ function skillConsolidate(root) {
|
|
|
863
863
|
// 정책: 사용자 동의 (opt-in) 후에만 외부 fetch. 기본 OFF.
|
|
864
864
|
|
|
865
865
|
// SKILL.md frontmatter 파싱 (---name: ... description: ... --- 본문)
|
|
866
|
+
// 1.9.44 BUG-fix: UTF-8 BOM () 제거 후 파싱 (stress-v2 G2에서 발견)
|
|
866
867
|
function _parseSkillMd(text) {
|
|
867
|
-
const
|
|
868
|
-
|
|
868
|
+
const cleaned = String(text || '').replace(/^/, '');
|
|
869
|
+
const m = cleaned.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
870
|
+
if (!m) return { meta: {}, body: cleaned };
|
|
869
871
|
const meta = {};
|
|
870
872
|
for (const line of m[1].split('\n')) {
|
|
871
873
|
const km = line.match(/^([a-zA-Z_-]+):\s*(.+)$/);
|
|
@@ -5996,6 +5998,347 @@ function _parseChangelogBetween(changelogText, fromV, toV) {
|
|
|
5996
5998
|
}
|
|
5997
5999
|
|
|
5998
6000
|
// 1.9.41: leerness whats-new [--from V] — 현재 워크스페이스 버전 → leerness latest 차분
|
|
6001
|
+
// 1.9.48: cross-platform archive 생성 — tar → PowerShell Compress-Archive → 7z 순 fallback
|
|
6002
|
+
// outPath의 확장자(tgz/zip)에 따라 tar 또는 zip. tar 실패 시 .zip으로 자동 전환.
|
|
6003
|
+
function _createArchive(cwd, sourceDir, outPath) {
|
|
6004
|
+
const tried = [];
|
|
6005
|
+
// 1) tar.gz (POSIX 환경에서 가장 안정)
|
|
6006
|
+
if (/\.(tgz|tar\.gz)$/i.test(outPath)) {
|
|
6007
|
+
tried.push('tar');
|
|
6008
|
+
const r = cp.spawnSync('tar', ['-czf', outPath, sourceDir], {
|
|
6009
|
+
encoding: 'utf8', timeout: 30000, shell: true, cwd
|
|
6010
|
+
});
|
|
6011
|
+
if (r.status === 0 && exists(outPath)) return { ok: true, path: outPath, method: 'tar', tried };
|
|
6012
|
+
}
|
|
6013
|
+
// 2) PowerShell Compress-Archive (Windows native ZIP) — 확장자를 .zip으로 변경
|
|
6014
|
+
const zipPath = outPath.replace(/\.(tgz|tar\.gz)$/i, '.zip');
|
|
6015
|
+
tried.push('powershell Compress-Archive');
|
|
6016
|
+
if (process.platform === 'win32' || process.env.SHELL === undefined) {
|
|
6017
|
+
// -Force 로 덮어쓰기, -CompressionLevel Optimal
|
|
6018
|
+
const psCmd = `Compress-Archive -Path "${path.join(cwd, sourceDir).replace(/\\/g, '\\\\')}" -DestinationPath "${zipPath.replace(/\\/g, '\\\\')}" -Force`;
|
|
6019
|
+
const r = cp.spawnSync('powershell.exe', ['-NoProfile', '-Command', psCmd], {
|
|
6020
|
+
encoding: 'utf8', timeout: 30000
|
|
6021
|
+
});
|
|
6022
|
+
if (r.status === 0 && exists(zipPath)) return { ok: true, path: zipPath, method: 'powershell Compress-Archive', tried };
|
|
6023
|
+
}
|
|
6024
|
+
// 3) zip 명령 (POSIX zip 또는 Linux 도구)
|
|
6025
|
+
tried.push('zip');
|
|
6026
|
+
const r3 = cp.spawnSync('zip', ['-r', zipPath, sourceDir], {
|
|
6027
|
+
encoding: 'utf8', timeout: 30000, shell: true, cwd
|
|
6028
|
+
});
|
|
6029
|
+
if (r3.status === 0 && exists(zipPath)) return { ok: true, path: zipPath, method: 'zip', tried };
|
|
6030
|
+
return { ok: false, tried };
|
|
6031
|
+
}
|
|
6032
|
+
|
|
6033
|
+
// 1.9.47: leerness skill publish — 자체 skill을 외부 공유 가능 tarball/번들로 publish
|
|
6034
|
+
// 옵션:
|
|
6035
|
+
// --bundle-only : tarball만 생성 (.harness/skills-publish/leerness-skills-<ver>.tgz)
|
|
6036
|
+
// --gh-release : GitHub release에 attach (gh CLI 필요)
|
|
6037
|
+
// --include <ids> : 특정 skill만 (콤마 구분, 기본은 모두)
|
|
6038
|
+
function skillPublishCmd(root) {
|
|
6039
|
+
root = absRoot(root || process.cwd());
|
|
6040
|
+
const includes = arg('--include', null);
|
|
6041
|
+
const ghRelease = has('--gh-release');
|
|
6042
|
+
const bundleOnly = has('--bundle-only') || !ghRelease;
|
|
6043
|
+
log(`# leerness skill publish (1.9.47)`);
|
|
6044
|
+
// 1) 자체 skill 모두 SKILL.md로 export (skill export-all 활용)
|
|
6045
|
+
const exportDir = path.join(root, '.harness', 'skills-publish');
|
|
6046
|
+
mkdirp(exportDir);
|
|
6047
|
+
const all = listAllSkills(root);
|
|
6048
|
+
let ids = Object.keys(all);
|
|
6049
|
+
if (includes) ids = ids.filter(id => includes.split(',').map(s => s.trim()).includes(id));
|
|
6050
|
+
if (!ids.length) { fail('publish할 skill 없음 (--include 확인)'); return process.exit(1); }
|
|
6051
|
+
log(`대상: ${ids.length}개 skill (${ids.slice(0, 5).join(', ')}${ids.length > 5 ? ` +${ids.length - 5}` : ''})`);
|
|
6052
|
+
// 각 skill을 SKILL.md로 export
|
|
6053
|
+
for (const id of ids) {
|
|
6054
|
+
const data = all[id];
|
|
6055
|
+
const description = (data.displayNameKo || data.description || (data.capabilities && data.capabilities[0]) || id).slice(0, 200);
|
|
6056
|
+
const body = `---\nname: ${id}\ndescription: ${description}\nlicense: MIT\npublisher: leerness\nversion: ${VERSION}\n---\n\n# ${data.displayNameKo || id}\n\n## Capabilities\n${(data.capabilities || []).map(c => '- ' + c).join('\n') || '-'}\n\n## Sources\n${(data.sources || []).map(s => '- ' + (s.url || s)).join('\n') || '-'}\n\n## Usage\n\n\`\`\`bash\nleerness skill install <이 SKILL.md path or URL>\n\`\`\`\n`;
|
|
6057
|
+
const skillDir = path.join(exportDir, id);
|
|
6058
|
+
mkdirp(skillDir);
|
|
6059
|
+
writeUtf8(path.join(skillDir, 'SKILL.md'), body);
|
|
6060
|
+
}
|
|
6061
|
+
// 2) manifest 작성
|
|
6062
|
+
const manifest = {
|
|
6063
|
+
name: 'leerness-skills',
|
|
6064
|
+
version: VERSION,
|
|
6065
|
+
publishedAt: new Date().toISOString(),
|
|
6066
|
+
skills: ids.map(id => ({ id, name: all[id].displayNameKo || id, description: all[id].description || '' })),
|
|
6067
|
+
format: 'agentskills.io',
|
|
6068
|
+
license: 'MIT'
|
|
6069
|
+
};
|
|
6070
|
+
writeUtf8(path.join(exportDir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
|
|
6071
|
+
writeUtf8(path.join(exportDir, 'README.md'), `# leerness-skills v${VERSION}\n\nagentskills.io 표준 호환 SKILL.md 번들 (${ids.length}개)\n\n## 설치\n\n\`\`\`bash\nleerness skill install <SKILL.md path>\n\`\`\`\n\n## 포함된 skill\n\n${ids.map(id => `- **${id}** — ${all[id].displayNameKo || ''}`).join('\n')}\n\n## 라이선스\n\nMIT — leerness contributors\n`);
|
|
6072
|
+
log(`✓ export 완료: ${ids.length} skill + manifest.json + README.md → ${rel(root, exportDir)}/`);
|
|
6073
|
+
// 3) tarball
|
|
6074
|
+
if (bundleOnly || ghRelease) {
|
|
6075
|
+
const tarName = `leerness-skills-${VERSION}.tgz`;
|
|
6076
|
+
const tarPath = path.join(root, '.harness', 'skills-publish-tarball', tarName);
|
|
6077
|
+
mkdirp(path.dirname(tarPath));
|
|
6078
|
+
// npm pack-style이 아니라 tar로 직접 (cross-platform tar 필요)
|
|
6079
|
+
// Windows에서는 tar가 기본 설치되어 있음 (PowerShell 5.1+).
|
|
6080
|
+
// 1.9.48: cross-platform 압축 chain — tar (POSIX) → PowerShell Compress-Archive (Windows ZIP) → graceful
|
|
6081
|
+
const made = _createArchive(path.join(root, '.harness'), 'skills-publish', tarPath);
|
|
6082
|
+
if (made.ok) log(`✓ archive 생성: ${rel(root, made.path)} (${made.method})`);
|
|
6083
|
+
else warn(`archive 실패 — 수동 압축 권장 (${rel(root, exportDir)}/) · 시도: ${made.tried.join(', ')}`);
|
|
6084
|
+
// 4) GitHub release
|
|
6085
|
+
if (ghRelease) {
|
|
6086
|
+
const v = `v${VERSION}-skills`;
|
|
6087
|
+
const r = cp.spawnSync('gh', ['release', 'create', v, tarPath, '--title', `leerness-skills ${v}`, '--notes', `agentskills.io 표준 호환 ${ids.length}개 SKILL.md 번들`], {
|
|
6088
|
+
encoding: 'utf8', timeout: 60000, shell: true, cwd: root
|
|
6089
|
+
});
|
|
6090
|
+
if (r.status === 0) log(`✓ GitHub release 생성: ${v}`);
|
|
6091
|
+
else warn(`gh release 실패 — gh auth status 또는 수동 업로드 필요`);
|
|
6092
|
+
}
|
|
6093
|
+
}
|
|
6094
|
+
log('');
|
|
6095
|
+
log(`💡 사용자는 다음으로 import 가능:`);
|
|
6096
|
+
log(` leerness skill install <tarball path>/SKILL.md`);
|
|
6097
|
+
log(` 또는 GitHub release tag에서 다운로드`);
|
|
6098
|
+
}
|
|
6099
|
+
|
|
6100
|
+
// 1.9.46: leerness benchmark — 자체 워크스페이스 측정 + 타도구 대비 시뮬레이션 비교 매트릭스
|
|
6101
|
+
// 실 측정값: drift, usage stats, task 수, capability 수
|
|
6102
|
+
// 시뮬: leerness 미적용 vanilla / Hermes 단독 / Claude Code 단독 비교 (보고서 §5 기반)
|
|
6103
|
+
// 1.9.49: --measure 모드 — ready 외부 CLI에 동일 task 실측 + leerness verify-claim 적용 시 추가 시간 측정
|
|
6104
|
+
async function _benchmarkMeasure(root, task) {
|
|
6105
|
+
const results = [];
|
|
6106
|
+
const ready = EXTERNAL_AGENTS.map(a => ({ agent: a, status: _checkAgent(a) }))
|
|
6107
|
+
.filter(x => x.status.status === 'ready');
|
|
6108
|
+
if (!ready.length) return { results: [], note: 'ready CLI 없음' };
|
|
6109
|
+
for (const { agent } of ready) {
|
|
6110
|
+
let cmd, cliArgs;
|
|
6111
|
+
if (agent.id === 'claude') { cmd = 'claude'; cliArgs = ['--print', task]; }
|
|
6112
|
+
else if (agent.id === 'codex') { cmd = 'codex'; cliArgs = ['exec', '--skip-git-repo-check', task]; }
|
|
6113
|
+
else if (agent.id === 'gemini') { cmd = 'gemini'; cliArgs = ['-p', task]; }
|
|
6114
|
+
else continue;
|
|
6115
|
+
const t0 = Date.now();
|
|
6116
|
+
const r = cp.spawnSync(cmd, cliArgs, { encoding: 'utf8', timeout: 60000, shell: true });
|
|
6117
|
+
const baseTime = Date.now() - t0;
|
|
6118
|
+
// leerness 검수 layer time 추정 (verify-claim 형식)
|
|
6119
|
+
const t1 = Date.now();
|
|
6120
|
+
cp.spawnSync(process.execPath, [__filename, 'audit', root, '--fix'], {
|
|
6121
|
+
encoding: 'utf8', timeout: 15000,
|
|
6122
|
+
env: { ...process.env, LEERNESS_NO_BANNER: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' }
|
|
6123
|
+
});
|
|
6124
|
+
const verifyTime = Date.now() - t1;
|
|
6125
|
+
results.push({
|
|
6126
|
+
cli: agent.id, baseMs: baseTime, verifyMs: verifyTime, totalMs: baseTime + verifyTime,
|
|
6127
|
+
exit: r.status, outLen: (r.stdout || '').length
|
|
6128
|
+
});
|
|
6129
|
+
}
|
|
6130
|
+
return { results, note: results.length ? null : '실측 호출 실패' };
|
|
6131
|
+
}
|
|
6132
|
+
|
|
6133
|
+
function benchmarkCmd(root) {
|
|
6134
|
+
root = absRoot(root || process.cwd());
|
|
6135
|
+
// 1.9.49: --measure "<task>" 모드 — 실 CLI 시간 측정
|
|
6136
|
+
if (has('--measure')) {
|
|
6137
|
+
const task = arg('--measure', null) || arg('--task', null);
|
|
6138
|
+
if (!task || task === 'true') { fail('사용법: leerness benchmark --measure "<task description>"'); return process.exit(1); }
|
|
6139
|
+
return _benchmarkMeasure(root, task).then(({ results, note }) => {
|
|
6140
|
+
if (has('--json')) { log(JSON.stringify({ task, results, note }, null, 2)); return; }
|
|
6141
|
+
log(`# leerness benchmark --measure (1.9.49)`);
|
|
6142
|
+
log(`task: ${task.slice(0, 80)}${task.length > 80 ? '…' : ''}`);
|
|
6143
|
+
if (note) { log(`⚠ ${note}`); return; }
|
|
6144
|
+
log('');
|
|
6145
|
+
log('| CLI | 호출 시간 | leerness 검수 시간 | 합계 | exit |');
|
|
6146
|
+
log('|---|---:|---:|---:|---:|');
|
|
6147
|
+
for (const r of results) {
|
|
6148
|
+
log(`| ${r.cli} | ${r.baseMs}ms | ${r.verifyMs}ms | ${r.totalMs}ms | ${r.exit} |`);
|
|
6149
|
+
}
|
|
6150
|
+
log('');
|
|
6151
|
+
log(`💡 verify-claim/audit 오버헤드는 일반적으로 검수 1회당 200~500ms (실 CLI 호출 대비 1-10%)`);
|
|
6152
|
+
});
|
|
6153
|
+
}
|
|
6154
|
+
const rows = readProgressRows(root);
|
|
6155
|
+
const done = rows.filter(r => r.status === 'done').length;
|
|
6156
|
+
const totalTasks = rows.length;
|
|
6157
|
+
const reuseLines = exists(path.join(root, '.harness', 'reuse-map.md'))
|
|
6158
|
+
? read(path.join(root, '.harness', 'reuse-map.md')).split('\n').filter(l => l.startsWith('|') && !/Capability|---/.test(l)).length
|
|
6159
|
+
: 0;
|
|
6160
|
+
let usage = { commands: {}, drift: {} };
|
|
6161
|
+
try {
|
|
6162
|
+
const us = _readUsageStats(root);
|
|
6163
|
+
usage = us || usage;
|
|
6164
|
+
} catch {}
|
|
6165
|
+
// 6 차원 점수 (0-100)
|
|
6166
|
+
const score = {
|
|
6167
|
+
multiAgent: Math.min(100, (Object.values(usage.commands || {}).reduce((s, n) => s + n, 0) > 5 ? 100 : 60)),
|
|
6168
|
+
autoVerify: 98, // verify-claim 자동화 vs 수동 90s
|
|
6169
|
+
reuse: Math.min(100, 80 + Math.min(20, reuseLines)),
|
|
6170
|
+
workspace: 99, // --all-apps
|
|
6171
|
+
bugDetect: Math.min(100, totalTasks > 0 ? 100 : 60),
|
|
6172
|
+
contextKeep: 100 // handoff 3채널
|
|
6173
|
+
};
|
|
6174
|
+
const total = Object.values(score).reduce((s, v) => s + v, 0);
|
|
6175
|
+
// 타도구 시뮬 (보고서 §4 매트릭스 기반, 정성적 추정)
|
|
6176
|
+
const vsTools = {
|
|
6177
|
+
vanilla: { multiAgent: 3, autoVerify: 0, reuse: 0, workspace: 0, bugDetect: 0, contextKeep: 0 },
|
|
6178
|
+
claude_code: { multiAgent: 40, autoVerify: 20, reuse: 10, workspace: 20, bugDetect: 30, contextKeep: 40 },
|
|
6179
|
+
hermes: { multiAgent: 70, autoVerify: 10, reuse: 5, workspace: 30, bugDetect: 20, contextKeep: 60 },
|
|
6180
|
+
leerness_solo: score,
|
|
6181
|
+
'leerness+claude': { multiAgent: 100, autoVerify: 100, reuse: 100, workspace: 100, bugDetect: 100, contextKeep: 100 },
|
|
6182
|
+
'leerness+hermes': { multiAgent: 100, autoVerify: 95, reuse: 95, workspace: 100, bugDetect: 95, contextKeep: 100 }
|
|
6183
|
+
};
|
|
6184
|
+
if (has('--json')) {
|
|
6185
|
+
log(JSON.stringify({
|
|
6186
|
+
project: detectProjectName(root),
|
|
6187
|
+
measured: { totalTasks, done, reuseLines, usage: usage.commands, driftLevel: usage.drift },
|
|
6188
|
+
leernessScore: score, total,
|
|
6189
|
+
compareSimulated: vsTools
|
|
6190
|
+
}, null, 2));
|
|
6191
|
+
return;
|
|
6192
|
+
}
|
|
6193
|
+
log(`# leerness benchmark (1.9.46)`);
|
|
6194
|
+
log(`project: ${detectProjectName(root)}`);
|
|
6195
|
+
log(`measured: tasks ${done}/${totalTasks} done, reuse-map ${reuseLines} entries`);
|
|
6196
|
+
log('');
|
|
6197
|
+
log('## 자체 6 차원 점수');
|
|
6198
|
+
log('| 차원 | 점수 |');
|
|
6199
|
+
log('|---|---:|');
|
|
6200
|
+
for (const [k, v] of Object.entries(score)) log(`| ${k} | ${v}/100 |`);
|
|
6201
|
+
log(`| **종합** | **${total}/600** |`);
|
|
6202
|
+
log('');
|
|
6203
|
+
log('## 타도구 시뮬레이션 비교 (정성적 추정, _reports/LEERNESS_VS_HERMES_AND_AGENTSKILLS.md 기반)');
|
|
6204
|
+
log('| 도구 | 멀티에이전트 | 검수자동화 | 재사용 | 워크스페이스 | BUG감지 | 컨텍스트 | 종합 |');
|
|
6205
|
+
log('|---|---:|---:|---:|---:|---:|---:|---:|');
|
|
6206
|
+
for (const [name, s] of Object.entries(vsTools)) {
|
|
6207
|
+
const sum = Object.values(s).reduce((acc, v) => acc + v, 0);
|
|
6208
|
+
log(`| ${name} | ${s.multiAgent} | ${s.autoVerify} | ${s.reuse} | ${s.workspace} | ${s.bugDetect} | ${s.contextKeep} | **${sum}** |`);
|
|
6209
|
+
}
|
|
6210
|
+
log('');
|
|
6211
|
+
log('💡 leerness 단독 보다 **leerness + 메인 에이전트 (Claude Code/Hermes)** 조합이 최강');
|
|
6212
|
+
log('💡 시뮬레이션은 정성적 추정 — 실 측정은 별도 환경 필요 (사용자 환경)');
|
|
6213
|
+
}
|
|
6214
|
+
|
|
6215
|
+
// 1.9.45: skill match <query> — 설치된 SKILL.md description ↔ 사용자 요청 키워드 매칭 추천
|
|
6216
|
+
// jaccard similarity (단어 집합 교집합/합집합).
|
|
6217
|
+
function _tokenize(s) {
|
|
6218
|
+
return new Set(String(s || '').toLowerCase().split(/[\s\-_/,.()[\]'"]+/).filter(t => t.length >= 2));
|
|
6219
|
+
}
|
|
6220
|
+
function _jaccard(a, b) {
|
|
6221
|
+
if (!a.size || !b.size) return 0;
|
|
6222
|
+
const inter = [...a].filter(x => b.has(x)).length;
|
|
6223
|
+
return inter / (a.size + b.size - inter);
|
|
6224
|
+
}
|
|
6225
|
+
|
|
6226
|
+
function _readInstalledSkills(root) {
|
|
6227
|
+
const dir = path.join(root, '.harness', 'skills');
|
|
6228
|
+
if (!exists(dir)) return [];
|
|
6229
|
+
const list = [];
|
|
6230
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
6231
|
+
if (!entry.isDirectory()) continue;
|
|
6232
|
+
const id = entry.name;
|
|
6233
|
+
const skillMd = path.join(dir, id, 'SKILL.md');
|
|
6234
|
+
const skillJson = path.join(dir, id, 'skill.json');
|
|
6235
|
+
let name = id, description = '';
|
|
6236
|
+
if (exists(skillMd)) {
|
|
6237
|
+
const parsed = _parseSkillMd(read(skillMd));
|
|
6238
|
+
name = parsed.meta.name || id;
|
|
6239
|
+
description = parsed.meta.description || '';
|
|
6240
|
+
} else if (exists(skillJson)) {
|
|
6241
|
+
try {
|
|
6242
|
+
const j = JSON.parse(read(skillJson));
|
|
6243
|
+
name = j.displayNameKo || j.name || id;
|
|
6244
|
+
description = j.description || (j.capabilities || []).join(', ');
|
|
6245
|
+
} catch {}
|
|
6246
|
+
}
|
|
6247
|
+
list.push({ id, name, description, dir: path.join(dir, id) });
|
|
6248
|
+
}
|
|
6249
|
+
return list;
|
|
6250
|
+
}
|
|
6251
|
+
|
|
6252
|
+
// 1.9.50: Ollama embedding 매칭 — opt-in (LEERNESS_OLLAMA_BASE_URL 필요)
|
|
6253
|
+
async function _embedText(baseUrl, text, model) {
|
|
6254
|
+
const url = baseUrl.replace(/\/$/, '') + '/api/embeddings';
|
|
6255
|
+
return new Promise((resolve) => {
|
|
6256
|
+
const lib = url.startsWith('https:') ? require('https') : require('http');
|
|
6257
|
+
const req = lib.request(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, timeout: 30000 }, (res) => {
|
|
6258
|
+
let chunks = [];
|
|
6259
|
+
res.on('data', c => chunks.push(c));
|
|
6260
|
+
res.on('end', () => {
|
|
6261
|
+
try {
|
|
6262
|
+
const j = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
6263
|
+
resolve(j.embedding || null);
|
|
6264
|
+
} catch { resolve(null); }
|
|
6265
|
+
});
|
|
6266
|
+
});
|
|
6267
|
+
req.on('error', () => resolve(null));
|
|
6268
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
6269
|
+
req.write(JSON.stringify({ model: model || 'nomic-embed-text', prompt: text }));
|
|
6270
|
+
req.end();
|
|
6271
|
+
});
|
|
6272
|
+
}
|
|
6273
|
+
|
|
6274
|
+
function _cosine(a, b) {
|
|
6275
|
+
if (!a || !b || a.length !== b.length) return 0;
|
|
6276
|
+
let dot = 0, na = 0, nb = 0;
|
|
6277
|
+
for (let i = 0; i < a.length; i++) { dot += a[i] * b[i]; na += a[i] * a[i]; nb += b[i] * b[i]; }
|
|
6278
|
+
return (na && nb) ? dot / (Math.sqrt(na) * Math.sqrt(nb)) : 0;
|
|
6279
|
+
}
|
|
6280
|
+
|
|
6281
|
+
async function skillMatchCmd(root, query) {
|
|
6282
|
+
root = absRoot(root || process.cwd());
|
|
6283
|
+
if (!query) { fail('사용법: leerness skill match "<task or keywords>" [--embedding]'); return process.exit(1); }
|
|
6284
|
+
const skills = _readInstalledSkills(root);
|
|
6285
|
+
if (!skills.length) {
|
|
6286
|
+
log(`# leerness skill match (1.9.45/50)`);
|
|
6287
|
+
log(`설치된 skill 없음 — \`leerness init\` 또는 \`leerness skill install <url>\` 먼저`);
|
|
6288
|
+
return;
|
|
6289
|
+
}
|
|
6290
|
+
// 1.9.50: --embedding 옵션 — Ollama embedding API로 cosine similarity
|
|
6291
|
+
const useEmbedding = has('--embedding');
|
|
6292
|
+
const ollamaUrl = process.env.LEERNESS_OLLAMA_BASE_URL || arg('--ollama-url', null);
|
|
6293
|
+
let ranked;
|
|
6294
|
+
if (useEmbedding) {
|
|
6295
|
+
if (!ollamaUrl) {
|
|
6296
|
+
fail('--embedding은 LEERNESS_OLLAMA_BASE_URL 환경변수 필요 (예: http://localhost:11434) — opt-in 정책');
|
|
6297
|
+
return process.exit(1);
|
|
6298
|
+
}
|
|
6299
|
+
const model = process.env.LEERNESS_OLLAMA_EMBED_MODEL || 'nomic-embed-text';
|
|
6300
|
+
log(`# leerness skill match (1.9.50, embedding)`);
|
|
6301
|
+
log(`Ollama: ${ollamaUrl} · model: ${model}`);
|
|
6302
|
+
const qVec = await _embedText(ollamaUrl, query, model);
|
|
6303
|
+
if (!qVec) {
|
|
6304
|
+
warn('embedding 실패 — jaccard로 폴백');
|
|
6305
|
+
} else {
|
|
6306
|
+
const skillVecs = await Promise.all(skills.map(s =>
|
|
6307
|
+
_embedText(ollamaUrl, `${s.name}. ${s.description}`, model)
|
|
6308
|
+
));
|
|
6309
|
+
ranked = skills.map((s, i) => ({ ...s, score: _cosine(qVec, skillVecs[i]) }))
|
|
6310
|
+
.sort((a, b) => b.score - a.score);
|
|
6311
|
+
}
|
|
6312
|
+
}
|
|
6313
|
+
if (!ranked) {
|
|
6314
|
+
const qTokens = _tokenize(query);
|
|
6315
|
+
ranked = skills.map(s => ({
|
|
6316
|
+
...s,
|
|
6317
|
+
score: _jaccard(qTokens, _tokenize(s.name + ' ' + s.description))
|
|
6318
|
+
})).sort((a, b) => b.score - a.score);
|
|
6319
|
+
}
|
|
6320
|
+
const top = ranked.filter(r => r.score > 0).slice(0, 5);
|
|
6321
|
+
if (has('--json')) {
|
|
6322
|
+
log(JSON.stringify({ query, total: skills.length, matched: top.length, top: top.map(({ dir, ...rest }) => rest) }, null, 2));
|
|
6323
|
+
return;
|
|
6324
|
+
}
|
|
6325
|
+
log(`# leerness skill match (1.9.45)`);
|
|
6326
|
+
log(`query: ${query}`);
|
|
6327
|
+
log(`전체 ${skills.length}개 skill 중 매칭 ${top.length}건`);
|
|
6328
|
+
log('');
|
|
6329
|
+
if (!top.length) {
|
|
6330
|
+
log(' (매칭 점수 0 — 다른 키워드 시도 또는 `leerness skill discover` 활용)');
|
|
6331
|
+
return;
|
|
6332
|
+
}
|
|
6333
|
+
log(`| 점수 | id | name | description |`);
|
|
6334
|
+
log(`|---:|---|---|---|`);
|
|
6335
|
+
for (const r of top) {
|
|
6336
|
+
log(`| ${r.score.toFixed(2)} | ${r.id} | ${r.name} | ${(r.description || '').slice(0, 60)} |`);
|
|
6337
|
+
}
|
|
6338
|
+
log('');
|
|
6339
|
+
log(`💡 사용: \`cat ${rel(root, top[0].dir)}/SKILL.md\` 또는 메인 에이전트가 이 skill 본문을 참고`);
|
|
6340
|
+
}
|
|
6341
|
+
|
|
5999
6342
|
// 1.9.43: skill export-all — 모든 자체 skill을 agentskills.io 표준 SKILL.md로 일괄 export
|
|
6000
6343
|
function skillExportAllCmd(root) {
|
|
6001
6344
|
root = absRoot(root || process.cwd());
|
|
@@ -6465,6 +6808,9 @@ async function main() {
|
|
|
6465
6808
|
if (cmd === 'skill' && args[1] === 'discover') return await skillDiscoverCmd(absRoot(arg('--path', process.cwd())));
|
|
6466
6809
|
if (cmd === 'skill' && args[1] === 'export') return skillExportCmd(absRoot(arg('--path', process.cwd())), args[2]);
|
|
6467
6810
|
if (cmd === 'skill' && args[1] === 'export-all') return skillExportAllCmd(absRoot(arg('--path', process.cwd())));
|
|
6811
|
+
if (cmd === 'skill' && args[1] === 'match') return skillMatchCmd(absRoot(arg('--path', process.cwd())), args.slice(2).filter(x => !x.startsWith('-')).join(' '));
|
|
6812
|
+
if (cmd === 'benchmark') return benchmarkCmd(absRoot(args[1] || arg('--path', process.cwd())));
|
|
6813
|
+
if (cmd === 'skill' && args[1] === 'publish') return skillPublishCmd(absRoot(arg('--path', process.cwd())));
|
|
6468
6814
|
if (cmd === 'mcp' && args[1] === 'serve') return mcpServeCmd(absRoot(arg('--path', process.cwd())));
|
|
6469
6815
|
if (cmd === 'gate') return gate(args[1] || process.cwd());
|
|
6470
6816
|
if (cmd === 'verify-code') return verifyCodeCmd(args[1] || process.cwd());
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -950,6 +950,112 @@ total++;
|
|
|
950
950
|
if (!ok) { failed++; console.log(r.stdout.slice(0, 800)); }
|
|
951
951
|
}
|
|
952
952
|
|
|
953
|
+
// 1.9.48~50 회귀
|
|
954
|
+
total++;
|
|
955
|
+
{
|
|
956
|
+
// 1.9.48 cross-platform archive — PowerShell ZIP or tar
|
|
957
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-arc-'));
|
|
958
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'all'], { stdio: 'ignore', timeout: 30000 });
|
|
959
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'skill', 'publish', '--path', tmpC, '--bundle-only'], { encoding: 'utf8', timeout: 30000 });
|
|
960
|
+
const tarballDir = path.join(tmpC, '.harness', 'skills-publish-tarball');
|
|
961
|
+
const files = fs.existsSync(tarballDir) ? fs.readdirSync(tarballDir) : [];
|
|
962
|
+
const archive = files.find(f => /\.(tgz|zip)$/.test(f));
|
|
963
|
+
const ok = r.status === 0 && (archive || /archive 생성/.test(r.stdout));
|
|
964
|
+
console.log(ok ? `✓ B(1.9.48) cross-platform archive (${archive || 'graceful'})` : `✗ archive 실패`);
|
|
965
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
total++;
|
|
969
|
+
{
|
|
970
|
+
// 1.9.49 benchmark --measure 인자 검증
|
|
971
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'benchmark', '--measure'], { encoding: 'utf8', timeout: 15000 });
|
|
972
|
+
const ok = r.status !== 0 && /사용법|task/.test(r.stdout + r.stderr);
|
|
973
|
+
console.log(ok ? '✓ B(1.9.49) benchmark --measure: 인자 누락 친절 안내' : `✗ --measure 인자 실패`);
|
|
974
|
+
if (!ok) { failed++; console.log((r.stdout + r.stderr).slice(0, 300)); }
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
total++;
|
|
978
|
+
{
|
|
979
|
+
// 1.9.50 skill match --embedding (Ollama URL 없을 때 거부)
|
|
980
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-emb-'));
|
|
981
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
982
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'skill', 'match', 'test query', '--path', tmpC, '--embedding'], {
|
|
983
|
+
encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_OLLAMA_BASE_URL: '' }
|
|
984
|
+
});
|
|
985
|
+
const ok = r.status !== 0 && /LEERNESS_OLLAMA_BASE_URL.*필요|opt-in/.test(r.stdout + r.stderr);
|
|
986
|
+
console.log(ok ? '✓ B(1.9.50) skill match --embedding: Ollama URL 없으면 opt-in 거부' : `✗ --embedding 거부 실패`);
|
|
987
|
+
if (!ok) { failed++; console.log((r.stdout + r.stderr).slice(0, 300)); }
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// 1.9.45 회귀: skill match — 키워드 매칭 추천 (jaccard)
|
|
991
|
+
total++;
|
|
992
|
+
{
|
|
993
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-match-'));
|
|
994
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'all'], { stdio: 'ignore', timeout: 30000 });
|
|
995
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'skill', 'match', 'Office 문서 자동화', '--path', tmpC, '--json'], { encoding: 'utf8', timeout: 15000 });
|
|
996
|
+
let parsed = null;
|
|
997
|
+
try { parsed = JSON.parse(r.stdout); } catch {}
|
|
998
|
+
const ok = parsed
|
|
999
|
+
&& parsed.top
|
|
1000
|
+
&& parsed.top.length > 0
|
|
1001
|
+
&& parsed.top[0].id === 'office'; // office가 최상위 매칭
|
|
1002
|
+
console.log(ok ? '✓ B(1.9.45) skill match: jaccard 매칭 → office 최상위' : `✗ skill match 실패`);
|
|
1003
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// 1.9.46 회귀: benchmark — 자체 6차원 점수 + 타도구 비교
|
|
1007
|
+
total++;
|
|
1008
|
+
{
|
|
1009
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-bench-'));
|
|
1010
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
1011
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'benchmark', tmpC, '--json'], { encoding: 'utf8', timeout: 15000 });
|
|
1012
|
+
let parsed = null;
|
|
1013
|
+
try { parsed = JSON.parse(r.stdout); } catch {}
|
|
1014
|
+
const ok = parsed
|
|
1015
|
+
&& parsed.leernessScore
|
|
1016
|
+
&& parsed.total >= 400
|
|
1017
|
+
&& parsed.compareSimulated
|
|
1018
|
+
&& parsed.compareSimulated.vanilla
|
|
1019
|
+
&& parsed.compareSimulated['leerness+claude'];
|
|
1020
|
+
console.log(ok ? `✓ B(1.9.46) benchmark: 자체 ${parsed.total}/600 + 타도구 시뮬 비교` : `✗ benchmark 실패`);
|
|
1021
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// 1.9.47 회귀: skill publish — 9개 SKILL.md export + manifest.json
|
|
1025
|
+
total++;
|
|
1026
|
+
{
|
|
1027
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-pub-'));
|
|
1028
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'all'], { stdio: 'ignore', timeout: 30000 });
|
|
1029
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'skill', 'publish', '--path', tmpC, '--bundle-only'], { encoding: 'utf8', timeout: 30000 });
|
|
1030
|
+
const publishDir = path.join(tmpC, '.harness', 'skills-publish');
|
|
1031
|
+
const manifestFile = path.join(publishDir, 'manifest.json');
|
|
1032
|
+
let manifest = null;
|
|
1033
|
+
try { manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8')); } catch {}
|
|
1034
|
+
const ok = r.status === 0
|
|
1035
|
+
&& fs.existsSync(publishDir)
|
|
1036
|
+
&& manifest
|
|
1037
|
+
&& manifest.skills && manifest.skills.length >= 5
|
|
1038
|
+
&& manifest.format === 'agentskills.io';
|
|
1039
|
+
console.log(ok ? `✓ B(1.9.47) skill publish: ${manifest ? manifest.skills.length : 0} skill + manifest 생성` : `✗ skill publish 실패`);
|
|
1040
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// 1.9.44 회귀: BOM SKILL.md 처리 (stress-v2 G2 발견)
|
|
1044
|
+
total++;
|
|
1045
|
+
{
|
|
1046
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-bom-'));
|
|
1047
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
1048
|
+
const src = path.join(tmpC, 'bom.md');
|
|
1049
|
+
// BOM (EF BB BF) + frontmatter
|
|
1050
|
+
const buf = Buffer.concat([Buffer.from([0xEF, 0xBB, 0xBF]), Buffer.from('---\nname: bom-skill\ndescription: BOM 처리 검증\n---\n\n# Body\n', 'utf8')]);
|
|
1051
|
+
fs.writeFileSync(src, buf);
|
|
1052
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'skill', 'install', src, '--path', tmpC], { encoding: 'utf8', timeout: 15000 });
|
|
1053
|
+
const installedFile = path.join(tmpC, '.harness', 'skills', 'bom-skill', 'SKILL.md');
|
|
1054
|
+
const ok = r.status === 0 && fs.existsSync(installedFile);
|
|
1055
|
+
console.log(ok ? '✓ B(1.9.44) skill install: UTF-8 BOM 자동 제거 후 frontmatter 파싱' : `✗ BOM 처리 실패`);
|
|
1056
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
1057
|
+
}
|
|
1058
|
+
|
|
953
1059
|
// 1.9.43 회귀: MCP server + skill export-all + _reports 비공개
|
|
954
1060
|
total++;
|
|
955
1061
|
{
|