leerness 1.9.41 → 1.9.47
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 +152 -0
- package/README.md +47 -2
- package/bin/harness.js +520 -2
- package/package.json +1 -1
- package/scripts/e2e.js +206 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,157 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.47 — 2026-05-19
|
|
4
|
+
|
|
5
|
+
**`leerness skill publish` — 자체 skill을 외부 공유 번들로 publish**.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **`leerness skill publish [--include ids] [--bundle-only] [--gh-release]`**:
|
|
9
|
+
- 모든 자체 skill (또는 `--include`)을 SKILL.md frontmatter + license + publisher + version 메타로 export
|
|
10
|
+
- `manifest.json` (skills 카탈로그 인덱스) + `README.md` 자동 생성
|
|
11
|
+
- tarball 생성 시도 (Windows/POSIX tar) — 실패 시 graceful, 개별 SKILL.md는 정상 유지
|
|
12
|
+
- `--gh-release`: GitHub release에 자동 attach
|
|
13
|
+
|
|
14
|
+
### e2e: 199/199 PASS
|
|
15
|
+
|
|
16
|
+
## 1.9.46 — 2026-05-19
|
|
17
|
+
|
|
18
|
+
**`leerness benchmark` — 자체 + 타도구 비교 매트릭스**.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **`leerness benchmark [path] [--json]`** 신규 명령:
|
|
22
|
+
- 자체 6 차원 점수 (multiAgent / autoVerify / reuse / workspace / bugDetect / contextKeep) — 실 measured 값 (tasks/reuse-map/usage stats) 기반
|
|
23
|
+
- 6 도구 시뮬 비교: vanilla / claude_code / hermes / leerness_solo / leerness+claude / leerness+hermes
|
|
24
|
+
- 결론: **leerness + 메인 에이전트 조합이 최강** (단독 leerness보다 100점 차이)
|
|
25
|
+
|
|
26
|
+
## 1.9.45 — 2026-05-19
|
|
27
|
+
|
|
28
|
+
**`leerness skill match <query>` — 설치 SKILL.md 자동 추천**.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- **`leerness skill match "<task or keywords>"`** 신규 명령:
|
|
32
|
+
- 사용자 task 키워드 ↔ 설치된 SKILL.md description **jaccard similarity 매칭**
|
|
33
|
+
- 상위 5개 추천 + 점수 표 출력
|
|
34
|
+
- `--json` 출력 지원 → 메인 에이전트가 파싱하여 자동 활성화 가능
|
|
35
|
+
|
|
36
|
+
### 동작 예시
|
|
37
|
+
```
|
|
38
|
+
leerness skill match "Office 문서 자동화"
|
|
39
|
+
→ 점수 0.10 | office | 마이크로소프트 오피스 자동화
|
|
40
|
+
→ 점수 0.06 | ads-analytics | GA4 분석
|
|
41
|
+
→ 점수 0.05 | crawling | Playwright 기반 자동화
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 1.9.44 — 2026-05-19
|
|
45
|
+
|
|
46
|
+
**1.9.34~43 통합 검증 + BUG 1건 즉시 패치**.
|
|
47
|
+
|
|
48
|
+
별도 `_apps/leerness-stress/bin/stress-v2.js`로 1.9.34~43의 **13종 신규 기능 + 5 edge case = 25 시나리오 통합 테스트**. 발견된 진짜 BUG 1건 즉시 패치.
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
|
|
52
|
+
- **🔴 BUG-1 (HIGH)** — `_parseSkillMd`의 UTF-8 BOM 미처리:
|
|
53
|
+
- 증상: BOM (`EF BB BF`)이 있는 SKILL.md install 시 "name 필수" 에러 (frontmatter 매칭 실패)
|
|
54
|
+
- 원인: 정규식 `^---`가 BOM 뒤로 밀린 `---`를 매칭 못 함
|
|
55
|
+
- 수정: `text.replace(/^/, '')` 사전 BOM 제거
|
|
56
|
+
- 영향: Windows 메모장/일부 에디터 출력 SKILL.md 호환
|
|
57
|
+
|
|
58
|
+
### Verified (1.9.34~43 13종 기능 통합 검증)
|
|
59
|
+
|
|
60
|
+
| 카테고리 | 결과 |
|
|
61
|
+
|---|---|
|
|
62
|
+
| MCP Server (1.9.43) | ✅ 5/5 — JSON-RPC 표준, 10 도구 호출 가능, -32601/-32700 에러 정확 |
|
|
63
|
+
| agentskills.io 호환 (1.9.42/43) | ✅ 5/5 — install/export/discover round-trip, BOM/한글 OK |
|
|
64
|
+
| 차분 마이그레이션 (1.9.41) | ✅ 3/3 — whats-new 13 버전, migrate stdout 자동 출력, report 영구 기록 |
|
|
65
|
+
| release pack (1.9.40) | ✅ 2/2 — --task-add, --parent-migrate dogfooding gap |
|
|
66
|
+
| drift + workflow (1.9.37-39) | ✅ 4/4 — 4 신호 + 4 레벨, --auto-fix, session-workflow.md, 6단계 가이드 |
|
|
67
|
+
| contract verify (1.9.35/36) | ✅ 2/2 — **require side-effect 차단 실측 검증** (852ms 정적 분석), tick.* 필드 grep |
|
|
68
|
+
| Edge cases | ✅ 5/5 (1.9.44 BOM 패치 후) — BOM, 한글, 빈 디렉토리, 50KB MCP 제한, 동시 호출 race |
|
|
69
|
+
|
|
70
|
+
### 검증
|
|
71
|
+
- e2e: **196/196 PASS** (195 + BOM 회귀 1건)
|
|
72
|
+
- stress-v2: **25/25 PASS** (이전 3 FAIL → BUG 1건 패치 + stress-v2 자체 결함 2건 수정)
|
|
73
|
+
- 검증 보고서: `_reports/INTEGRATION_TEST_REPORT_1.9.44.md` (사용자 전용 비공개)
|
|
74
|
+
|
|
75
|
+
### 결론
|
|
76
|
+
**1.9.34~44의 모든 13종 신규 기능 production-ready 확인**. 신규 사용자가 `npx leerness@1.9.44 init .`로 즉시 안전 사용 가능.
|
|
77
|
+
|
|
78
|
+
## 1.9.43 — 2026-05-19
|
|
79
|
+
|
|
80
|
+
**MCP 서버 + skill 일괄 export + _reports 비공개 + GitHub 배포 준비**.
|
|
81
|
+
|
|
82
|
+
[agentskills.io 분석](https://agentskills.io)에서 도출한 발전 로드맵의 Phase 1 즉시 후보 3건을 통합. leerness 도구를 **MCP 서버로 노출**하여 Claude Code · Hermes · Cursor 등 30+ 도구가 직접 호출 가능.
|
|
83
|
+
|
|
84
|
+
### Added — MCP Server (sub-agent로서 leerness)
|
|
85
|
+
|
|
86
|
+
- **`leerness mcp serve`** 신규 명령 — stdio JSON-RPC로 leerness 도구 10종 노출:
|
|
87
|
+
- `leerness_handoff` · `leerness_drift_check` · `leerness_audit` (--fix 지원)
|
|
88
|
+
- `leerness_verify_claim` (--run-tests, --strict-claims)
|
|
89
|
+
- `leerness_contract_verify` (사양 ↔ 구현)
|
|
90
|
+
- `leerness_agents_list` · `leerness_reuse_map` · `leerness_whats_new`
|
|
91
|
+
- `leerness_usage_stats` · `leerness_session_close`
|
|
92
|
+
- 표준 MCP 프로토콜 (2024-11-05) — initialize / tools/list / tools/call
|
|
93
|
+
- 이제 Claude Code · Hermes · Cursor 등이 `.mcp.json`에 leerness를 등록하면 메인 에이전트가 leerness 검수를 sub-tool로 호출 가능
|
|
94
|
+
|
|
95
|
+
### Added — skill 표준 export·discover
|
|
96
|
+
|
|
97
|
+
- **`leerness skill export-all [--out <dir>]`** — 모든 자체 skill(9개)을 agentskills.io 표준 `SKILL.md`로 일괄 export. 다른 도구가 `skill install <path>`로 즉시 import.
|
|
98
|
+
|
|
99
|
+
### Added — 내부 보고서 비공개
|
|
100
|
+
|
|
101
|
+
- **`_reports/` 디렉토리 자동 비공개**:
|
|
102
|
+
- root `.gitignore`에 `_reports/`, `**/_reports/`, `*.private.md`, `*.private.json` 추가
|
|
103
|
+
- `leerness-pkg/.gitignore`에 동일 추가
|
|
104
|
+
- 신규 `leerness-pkg/.npmignore` — npm publish 시 명시적 제외
|
|
105
|
+
- `package.json#files` 화이트리스트와 이중 안전
|
|
106
|
+
- 내부 검수 보고서 (`LEERNESS_VS_HERMES_AND_AGENTSKILLS.md`, `SESSION_LEERNESS_USAGE_AUDIT.md` 등)는 사용자 확인 전용이며 npm/GitHub 배포에 포함되지 않음
|
|
107
|
+
|
|
108
|
+
### Verified
|
|
109
|
+
- e2e: **195/195 PASS** (1.9.42 190 + 신규 5)
|
|
110
|
+
- MCP server initialize/tools/list 정상 JSON-RPC 응답
|
|
111
|
+
- skill export-all → 9개 SKILL.md 일괄 생성
|
|
112
|
+
- .gitignore/.npmignore에 _reports/ 차단 확인
|
|
113
|
+
|
|
114
|
+
### 정책
|
|
115
|
+
- ✅ MCP server는 명시 호출 (`leerness mcp serve`) 시에만 작동 — 자동 시작 안 함
|
|
116
|
+
- ✅ MCP 도구 호출 시 LEERNESS_NO_BANNER/NO_PROMPT/NO_DRIFT_CHECK 자동 설정 (호스트 환경 깔끔)
|
|
117
|
+
- ✅ _reports 비공개 — 다중 채널 (gitignore + npmignore + files 화이트리스트)
|
|
118
|
+
|
|
119
|
+
## 1.9.42 — 2026-05-19
|
|
120
|
+
|
|
121
|
+
**agentskills.io 공개 표준 호환 — 30+ AI 도구와 스킬 즉시 공유**.
|
|
122
|
+
|
|
123
|
+
[agentskills.io](https://agentskills.io)는 Anthropic이 만든 Agent Skills 개방 표준으로 Claude Code · Cursor · GitHub Copilot · OpenAI Codex · Gemini CLI · Hermes Agent · OpenHands · Goose 등 30+ 도구가 채택. 1.9.42부터 leerness가 이 표준의 `SKILL.md` 포맷을 import/export 가능.
|
|
124
|
+
|
|
125
|
+
### Added
|
|
126
|
+
|
|
127
|
+
- **`leerness skill install <url-or-path>`** 신규 명령 — `SKILL.md` 다운로드/import:
|
|
128
|
+
- URL (https://...) 또는 로컬 파일/디렉토리 모두 지원
|
|
129
|
+
- frontmatter (`name`, `description`) 파싱 → `.harness/skills/<id>/SKILL.md` 자동 배치
|
|
130
|
+
- 자체 `skill.json` 도 함께 생성 (자체 catalog 호환, `_source: 'agentskills.io'` 추적)
|
|
131
|
+
- **`leerness skill discover [--query <q>] [--source <url>]`** 신규 명령 — 공개 스킬 카탈로그에서 매칭 추천:
|
|
132
|
+
- **opt-in**: `LEERNESS_SKILL_DISCOVER_URL` 환경변수 또는 `--source` 명시 필요 (자동 외부 fetch 금지 정책 유지)
|
|
133
|
+
- `--query` 키워드 매칭 + 마크다운 링크/SKILL.md URL 자동 추출
|
|
134
|
+
- `--json` 출력 지원
|
|
135
|
+
- **`leerness skill export <id> [--out <dir>]`** 신규 명령 — 기존 자체 skill을 agentskills.io 표준 `SKILL.md` 포맷으로 export → 다른 도구가 `skill install`로 import 가능
|
|
136
|
+
- **`.env.example`에 2개 신규 환경변수** (opt-in, 기본 OFF):
|
|
137
|
+
- `LEERNESS_SKILL_DISCOVER_URL=` — 공개 카탈로그 URL
|
|
138
|
+
- `LEERNESS_SKILL_AUTO_DISCOVER=0` — 사용자 요청 분석 시 자동 매칭 추천
|
|
139
|
+
- **`_httpFetch()` 내장 HTTPS 호출자** — Node 18+ globalThis.fetch, fallback https module. 사용자 동의 명령에서만 호출.
|
|
140
|
+
|
|
141
|
+
### Reports
|
|
142
|
+
- `_reports/LEERNESS_VS_HERMES_AND_AGENTSKILLS.md` 작성 — 10 섹션 상세 분석:
|
|
143
|
+
- agentskills.io 표준 + Progressive Disclosure 메커니즘
|
|
144
|
+
- Hermes Agent (NousResearch, 157k ⭐, MIT) 분석
|
|
145
|
+
- leerness 4 고유 우위 (거짓 완료 검증, drift 자동 감지, 워크스페이스 가시성, 마이그레이션 인지 갭)
|
|
146
|
+
- 1.9.42 → 2.0 발전 로드맵 3 Phase
|
|
147
|
+
|
|
148
|
+
### 정책
|
|
149
|
+
- ❌ leerness는 외부 URL 자동 fetch 절대 금지 — opt-in (env 또는 `--source` 명시) 필수
|
|
150
|
+
- ✅ `_httpFetch`는 사용자 명령 (`skill install URL` / `skill discover`)에서만 호출
|
|
151
|
+
- ✅ 기존 자체 skillCatalog와 양립 — `_source: 'agentskills.io'`로 출처 추적
|
|
152
|
+
|
|
153
|
+
### e2e: 190/190 PASS (1.9.41 186 + 신규 4)
|
|
154
|
+
|
|
3
155
|
## 1.9.41 — 2026-05-19
|
|
4
156
|
|
|
5
157
|
**디스크 마이그레이션 ↔ AI 컨텍스트 인지 갭 차단 — 맞춤형 차분 마이그레이션**.
|
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.47 AI Agent Reliability Harness ║
|
|
16
16
|
║ verify · remember · orchestrate · audit · prevent drift ║
|
|
17
17
|
╚══════════════════════════════════════════════════════════════╝
|
|
18
18
|
```
|
|
@@ -204,6 +204,45 @@ leerness persona list / show <id> / add <id>
|
|
|
204
204
|
leerness review <file> --persona security,performance,ux
|
|
205
205
|
```
|
|
206
206
|
|
|
207
|
+
### Agent Skills 표준 (1.9.42, agentskills.io 호환)
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
# Claude Code/Cursor/Copilot/Codex/Gemini CLI/Hermes Agent 등 30+ 도구와 스킬 공유
|
|
211
|
+
leerness skill install <url-or-path> # SKILL.md 다운로드/import
|
|
212
|
+
leerness skill discover --query "<task>" # 매칭 추천 (opt-in)
|
|
213
|
+
leerness skill export <id> [--out <dir>] # 자체 skill → 표준 SKILL.md 변환
|
|
214
|
+
leerness skill export-all [--out <dir>] # 1.9.43 9개 자체 skill 일괄 SKILL.md export
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### MCP Server — leerness 도구를 메인 에이전트의 sub-tool로 (1.9.43)
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
leerness mcp serve # stdio JSON-RPC로 leerness 도구 10종 노출
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Claude Code · Hermes · Cursor 등이 `.mcp.json`에 등록하면 메인 에이전트가 leerness 검수를 sub-tool로 호출 가능:
|
|
224
|
+
|
|
225
|
+
```json
|
|
226
|
+
{
|
|
227
|
+
"mcpServers": {
|
|
228
|
+
"leerness": {
|
|
229
|
+
"command": "npx",
|
|
230
|
+
"args": ["leerness", "mcp", "serve"]
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
노출 도구 10종: `leerness_handoff` · `leerness_drift_check` · `leerness_audit` · `leerness_verify_claim` · `leerness_contract_verify` · `leerness_agents_list` · `leerness_reuse_map` · `leerness_whats_new` · `leerness_usage_stats` · `leerness_session_close`
|
|
237
|
+
|
|
238
|
+
opt-in 설정 (`.env`):
|
|
239
|
+
```bash
|
|
240
|
+
LEERNESS_SKILL_DISCOVER_URL=https://agentskills.io/llms.txt # 또는 자체 카탈로그 URL
|
|
241
|
+
LEERNESS_SKILL_AUTO_DISCOVER=0 # 1=요청 자동 추천
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
> ❌ leerness는 외부 URL을 자동 fetch하지 않습니다. `LEERNESS_SKILL_DISCOVER_URL` 설정 또는 `--source` 명시 후에만 호출.
|
|
245
|
+
|
|
207
246
|
### 보안·인코딩
|
|
208
247
|
|
|
209
248
|
```bash
|
|
@@ -394,6 +433,12 @@ npm test # = node ./scripts/e2e.js
|
|
|
394
433
|
|
|
395
434
|
## 변경 이력 (최근)
|
|
396
435
|
|
|
436
|
+
- **1.9.47** — `leerness skill publish` — 자체 skill을 SKILL.md + manifest.json 번들로 export (외부 공유 가능, agentskills.io 표준).
|
|
437
|
+
- **1.9.46** — `leerness benchmark` — 자체 6 차원 점수 + 6 도구 (vanilla/claude_code/hermes/leerness+claude 등) 시뮬 비교 매트릭스.
|
|
438
|
+
- **1.9.45** — `leerness skill match <query>` — 사용자 요청 ↔ 설치 SKILL.md description **jaccard 매칭** + 자동 추천.
|
|
439
|
+
- **1.9.44** — 1.9.34~43 13종 기능 통합 stress test 25/25 PASS · 발견된 BOM 처리 BUG 1건 즉시 패치 (`_parseSkillMd` UTF-8 BOM 자동 제거) · e2e 196/196.
|
|
440
|
+
- **1.9.43** — MCP 서버로 leerness 도구 10종 노출 (`leerness mcp serve`, Claude Code/Hermes/Cursor 등이 직접 호출 가능) · `skill export-all` (9개 일괄 SKILL.md export) · 내부 보고서 자동 비공개 (`_reports/` gitignore + npmignore).
|
|
441
|
+
- **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.
|
|
397
442
|
- **1.9.41** — 디스크↔AI 컨텍스트 인지 갭 차단: `leerness whats-new` 명령 (CHANGELOG 자동 차분 추출) · `migrate` 후 stdout에 AI must re-read 차분 자동 출력 · `migration-report.md`에 신규 명령/파일 영구 기록 · `handoff`가 fresh migrate(24h 내) 시 자동 알림.
|
|
398
443
|
- **1.9.40** — dogfooding gap 차단: `leerness release pack` 통합 명령 (라운드 마감 자동화 — npm pack + parent migrate + task add + close + readme sync) · `audit`에 README ↔ package.json version mismatch 자동 감지 + `--fix`로 자동 갱신.
|
|
399
444
|
- **1.9.39** — AI 하네스 엔지니어링 6단계 워크플로 자동 유도 (`session-workflow.md` + handoff 끝 가이드 + AGENTS/CLAUDE 인스트럭션 통합) · `drift check --auto-fix` · `handoff --auto-recover` (critical 시 session close 자동 실행).
|
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.47';
|
|
10
10
|
const MARK = '<!-- leerness:managed -->';
|
|
11
11
|
const README_START = '<!-- leerness:project-readme:start -->';
|
|
12
12
|
const README_END = '<!-- leerness:project-readme:end -->';
|
|
@@ -622,7 +622,12 @@ async function install(root, opts = {}) {
|
|
|
622
622
|
'LEERNESS_ENABLE_CLAUDE=1',
|
|
623
623
|
'LEERNESS_ENABLE_CODEX=0',
|
|
624
624
|
'LEERNESS_ENABLE_GEMINI=0',
|
|
625
|
-
'LEERNESS_ENABLE_COPILOT=0'
|
|
625
|
+
'LEERNESS_ENABLE_COPILOT=0',
|
|
626
|
+
'# 1.9.42 — agentskills.io 공개 표준 스킬 자동 탐색 (opt-in). URL 설정 시 `leerness skill discover` 사용 가능.',
|
|
627
|
+
'# 예: LEERNESS_SKILL_DISCOVER_URL=https://agentskills.io/llms.txt',
|
|
628
|
+
'LEERNESS_SKILL_DISCOVER_URL=',
|
|
629
|
+
'# (선택) 사용자 요청 분석 시 자동 매칭 스킬 추천. 1=활성, 0/미설정=비활성.',
|
|
630
|
+
'LEERNESS_SKILL_AUTO_DISCOVER=0'
|
|
626
631
|
]);
|
|
627
632
|
mergeLinesFile(path.join(root, '.gitattributes'), [
|
|
628
633
|
'* text=auto eol=lf','*.bat text eol=crlf','*.ps1 text eol=crlf'
|
|
@@ -854,6 +859,179 @@ function skillConsolidate(root) {
|
|
|
854
859
|
for (const c of candidates) log(`| ${c.a} | ${c.b} | ${c.score.toFixed(2)} | \`leerness skill learn <new> --capability ...\` 후 \`leerness skill remove <old>\` |`);
|
|
855
860
|
}
|
|
856
861
|
|
|
862
|
+
// 1.9.42: agentskills.io 표준 호환 — SKILL.md (frontmatter + 본문) + scripts/ + references/ + assets/
|
|
863
|
+
// 정책: 사용자 동의 (opt-in) 후에만 외부 fetch. 기본 OFF.
|
|
864
|
+
|
|
865
|
+
// SKILL.md frontmatter 파싱 (---name: ... description: ... --- 본문)
|
|
866
|
+
// 1.9.44 BUG-fix: UTF-8 BOM () 제거 후 파싱 (stress-v2 G2에서 발견)
|
|
867
|
+
function _parseSkillMd(text) {
|
|
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 };
|
|
871
|
+
const meta = {};
|
|
872
|
+
for (const line of m[1].split('\n')) {
|
|
873
|
+
const km = line.match(/^([a-zA-Z_-]+):\s*(.+)$/);
|
|
874
|
+
if (km) meta[km[1].trim()] = km[2].trim().replace(/^["']|["']$/g, '');
|
|
875
|
+
}
|
|
876
|
+
return { meta, body: m[2] };
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// HTTPS fetch — Node 18+ globalThis.fetch 사용. 미지원 시 https module.
|
|
880
|
+
async function _httpFetch(urlStr, opts = {}) {
|
|
881
|
+
const timeout = opts.timeout || 15000;
|
|
882
|
+
if (typeof fetch === 'function') {
|
|
883
|
+
const controller = new AbortController();
|
|
884
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
885
|
+
try {
|
|
886
|
+
const r = await fetch(urlStr, { signal: controller.signal });
|
|
887
|
+
clearTimeout(timer);
|
|
888
|
+
return { status: r.status, body: await r.text() };
|
|
889
|
+
} catch (e) {
|
|
890
|
+
clearTimeout(timer);
|
|
891
|
+
return { status: 0, body: '', error: e.message };
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
// fallback: https module
|
|
895
|
+
return new Promise((resolve) => {
|
|
896
|
+
const u = new URL(urlStr);
|
|
897
|
+
const lib = u.protocol === 'http:' ? require('http') : require('https');
|
|
898
|
+
const req = lib.get(urlStr, (res) => {
|
|
899
|
+
// redirect handling
|
|
900
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
901
|
+
return _httpFetch(res.headers.location, opts).then(resolve);
|
|
902
|
+
}
|
|
903
|
+
let chunks = [];
|
|
904
|
+
res.on('data', c => chunks.push(c));
|
|
905
|
+
res.on('end', () => resolve({ status: res.statusCode, body: Buffer.concat(chunks).toString('utf8') }));
|
|
906
|
+
});
|
|
907
|
+
req.on('error', (e) => resolve({ status: 0, body: '', error: e.message }));
|
|
908
|
+
req.setTimeout(timeout, () => { req.destroy(); resolve({ status: 0, body: '', error: 'timeout' }); });
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// skill install <url-or-path> — SKILL.md 다운로드 + .harness/skills/<id>/에 설치
|
|
913
|
+
async function skillInstallCmd(root, source) {
|
|
914
|
+
if (!source) { fail('사용법: leerness skill install <SKILL.md URL 또는 로컬 디렉토리>'); return process.exit(1); }
|
|
915
|
+
let body = '';
|
|
916
|
+
if (/^https?:\/\//.test(source)) {
|
|
917
|
+
log(`# leerness skill install (1.9.42)`);
|
|
918
|
+
log(`다운로드 중: ${source}`);
|
|
919
|
+
const r = await _httpFetch(source);
|
|
920
|
+
if (r.status !== 200) {
|
|
921
|
+
fail(`다운로드 실패 (HTTP ${r.status}${r.error ? `, ${r.error}` : ''})`);
|
|
922
|
+
return process.exit(1);
|
|
923
|
+
}
|
|
924
|
+
body = r.body;
|
|
925
|
+
} else if (exists(source)) {
|
|
926
|
+
const localPath = exists(path.join(source, 'SKILL.md')) ? path.join(source, 'SKILL.md') : source;
|
|
927
|
+
body = read(localPath);
|
|
928
|
+
log(`# leerness skill install (1.9.42)`);
|
|
929
|
+
log(`로컬 로드: ${localPath}`);
|
|
930
|
+
} else {
|
|
931
|
+
fail(`source 없음 (URL 또는 디렉토리 경로): ${source}`);
|
|
932
|
+
return process.exit(1);
|
|
933
|
+
}
|
|
934
|
+
const parsed = _parseSkillMd(body);
|
|
935
|
+
const name = parsed.meta.name || parsed.meta.id;
|
|
936
|
+
const description = parsed.meta.description || '';
|
|
937
|
+
if (!name) { fail('SKILL.md frontmatter에 `name` 필수'); return process.exit(1); }
|
|
938
|
+
// .harness/skills/<id>/SKILL.md 저장
|
|
939
|
+
const skillId = String(name).toLowerCase().replace(/[^a-z0-9._-]+/g, '-');
|
|
940
|
+
const dir = path.join(root, '.harness', 'skills', skillId);
|
|
941
|
+
mkdirp(dir);
|
|
942
|
+
writeUtf8(path.join(dir, 'SKILL.md'), body);
|
|
943
|
+
// skill.json도 함께 (자체 catalog 호환)
|
|
944
|
+
writeUtf8(path.join(dir, 'skill.json'), JSON.stringify({
|
|
945
|
+
name: skillId, displayNameKo: name, description,
|
|
946
|
+
capabilities: [], _source: 'agentskills.io',
|
|
947
|
+
verification: { status: 'unverified', method: 'agentskills.io-import' }
|
|
948
|
+
}, null, 2) + '\n');
|
|
949
|
+
log(`✓ skill installed: ${skillId}`);
|
|
950
|
+
log(` name: ${name}`);
|
|
951
|
+
log(` description: ${description.slice(0, 100)}`);
|
|
952
|
+
log(` saved: ${rel(root, dir)}/`);
|
|
953
|
+
log('');
|
|
954
|
+
log(`💡 다음: leerness skill info ${skillId}`);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// skill discover — agentskills.io 또는 사용자 지정 URL의 카탈로그 인덱스에서 매칭 추천
|
|
958
|
+
async function skillDiscoverCmd(root) {
|
|
959
|
+
const url = arg('--source', null) || process.env.LEERNESS_SKILL_DISCOVER_URL || null;
|
|
960
|
+
const query = arg('--query', null);
|
|
961
|
+
if (!url) {
|
|
962
|
+
fail([
|
|
963
|
+
'LEERNESS_SKILL_DISCOVER_URL 환경변수 또는 --source URL 필요.',
|
|
964
|
+
'예: leerness skill discover --source https://agentskills.io/llms.txt',
|
|
965
|
+
'또는 .env에 LEERNESS_SKILL_DISCOVER_URL=...',
|
|
966
|
+
'',
|
|
967
|
+
'(정책: leerness는 사용자 동의 없이 외부 URL을 fetch하지 않음 — 1.9.42 opt-in)'
|
|
968
|
+
].join('\n'));
|
|
969
|
+
return process.exit(1);
|
|
970
|
+
}
|
|
971
|
+
log(`# leerness skill discover (1.9.42)`);
|
|
972
|
+
log(`source: ${url}`);
|
|
973
|
+
if (query) log(`query: ${query}`);
|
|
974
|
+
log(`fetching...`);
|
|
975
|
+
const r = await _httpFetch(url);
|
|
976
|
+
if (r.status !== 200) {
|
|
977
|
+
fail(`fetch 실패 (HTTP ${r.status}${r.error ? `, ${r.error}` : ''})`);
|
|
978
|
+
return process.exit(1);
|
|
979
|
+
}
|
|
980
|
+
// 카탈로그 인덱스 파싱 — agentskills.io는 llms.txt 형식 또는 raw 마크다운
|
|
981
|
+
const body = r.body;
|
|
982
|
+
// 간이 추출: SKILL.md 링크 + name + description 패턴
|
|
983
|
+
// - URL: https://.../SKILL.md
|
|
984
|
+
// - 마크다운 링크: [name](url) — description
|
|
985
|
+
const entries = [];
|
|
986
|
+
for (const m of body.matchAll(/^\s*-\s*\[([^\]]+)\]\(([^)]+)\)\s*[-—:]\s*(.+)$/gm)) {
|
|
987
|
+
entries.push({ name: m[1], url: m[2], description: m[3].trim() });
|
|
988
|
+
}
|
|
989
|
+
// URL only (개별 SKILL.md 파일)
|
|
990
|
+
if (!entries.length) {
|
|
991
|
+
for (const m of body.matchAll(/(https?:\/\/[^\s)]+SKILL\.md)/g)) {
|
|
992
|
+
entries.push({ name: m[1].split('/').slice(-2)[0], url: m[1], description: '' });
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
if (has('--json')) { log(JSON.stringify({ source: url, query, entries }, null, 2)); return; }
|
|
996
|
+
if (!entries.length) {
|
|
997
|
+
log(' (스킬 항목을 찾지 못함 — URL 형식 확인)');
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
// 쿼리 매칭 (description 단순 포함)
|
|
1001
|
+
let matched = entries;
|
|
1002
|
+
if (query) {
|
|
1003
|
+
const q = query.toLowerCase();
|
|
1004
|
+
matched = entries.filter(e => e.name.toLowerCase().includes(q) || (e.description || '').toLowerCase().includes(q));
|
|
1005
|
+
log(`매칭 ${matched.length}/${entries.length}건`);
|
|
1006
|
+
} else {
|
|
1007
|
+
log(`전체 ${entries.length}건 (매칭 없음 — --query로 필터링)`);
|
|
1008
|
+
}
|
|
1009
|
+
log('');
|
|
1010
|
+
log('| name | description | url |');
|
|
1011
|
+
log('|---|---|---|');
|
|
1012
|
+
for (const e of matched.slice(0, 30)) {
|
|
1013
|
+
log(`| ${e.name} | ${e.description.slice(0, 60)} | ${e.url} |`);
|
|
1014
|
+
}
|
|
1015
|
+
log('');
|
|
1016
|
+
log(`💡 설치: leerness skill install <url>`);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// skill export <id> — 기존 자체 skill을 agentskills.io 표준 SKILL.md로 export
|
|
1020
|
+
function skillExportCmd(root, id) {
|
|
1021
|
+
if (!id) { fail('사용법: leerness skill export <id>'); return process.exit(1); }
|
|
1022
|
+
const data = loadUserSkill(root, id) || (skillCatalog[id] ? { ...skillCatalog[id], name: id } : null);
|
|
1023
|
+
if (!data) { fail(`skill 없음: ${id}`); return process.exit(1); }
|
|
1024
|
+
const description = data.displayNameKo || data.description || (data.capabilities && data.capabilities[0]) || id;
|
|
1025
|
+
const body = `---\nname: ${id}\ndescription: ${description.slice(0, 200)}\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## Patterns\n${(data.patterns || []).map(p => `- \`${p.command}\` — ${p.note || ''}`).join('\n') || '-'}\n`;
|
|
1026
|
+
const outDir = arg('--out', path.join(root, '.harness', 'skills-export', id));
|
|
1027
|
+
mkdirp(outDir);
|
|
1028
|
+
const outPath = path.join(outDir, 'SKILL.md');
|
|
1029
|
+
writeUtf8(outPath, body);
|
|
1030
|
+
log(`✓ exported to ${rel(root, outPath)}`);
|
|
1031
|
+
log('');
|
|
1032
|
+
log(`💡 공유 가능 — 다른 도구가 \`leerness skill install ${outPath}\` 또는 URL로 import`);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
857
1035
|
const planPath = root => path.join(root, '.harness/plan.md');
|
|
858
1036
|
const progressPath = root => path.join(root, '.harness/progress-tracker.md');
|
|
859
1037
|
const taskLogPath = root => path.join(root, '.harness/task-log.md');
|
|
@@ -5820,6 +5998,338 @@ function _parseChangelogBetween(changelogText, fromV, toV) {
|
|
|
5820
5998
|
}
|
|
5821
5999
|
|
|
5822
6000
|
// 1.9.41: leerness whats-new [--from V] — 현재 워크스페이스 버전 → leerness latest 차분
|
|
6001
|
+
// 1.9.47: leerness skill publish — 자체 skill을 외부 공유 가능 tarball/번들로 publish
|
|
6002
|
+
// 옵션:
|
|
6003
|
+
// --bundle-only : tarball만 생성 (.harness/skills-publish/leerness-skills-<ver>.tgz)
|
|
6004
|
+
// --gh-release : GitHub release에 attach (gh CLI 필요)
|
|
6005
|
+
// --include <ids> : 특정 skill만 (콤마 구분, 기본은 모두)
|
|
6006
|
+
function skillPublishCmd(root) {
|
|
6007
|
+
root = absRoot(root || process.cwd());
|
|
6008
|
+
const includes = arg('--include', null);
|
|
6009
|
+
const ghRelease = has('--gh-release');
|
|
6010
|
+
const bundleOnly = has('--bundle-only') || !ghRelease;
|
|
6011
|
+
log(`# leerness skill publish (1.9.47)`);
|
|
6012
|
+
// 1) 자체 skill 모두 SKILL.md로 export (skill export-all 활용)
|
|
6013
|
+
const exportDir = path.join(root, '.harness', 'skills-publish');
|
|
6014
|
+
mkdirp(exportDir);
|
|
6015
|
+
const all = listAllSkills(root);
|
|
6016
|
+
let ids = Object.keys(all);
|
|
6017
|
+
if (includes) ids = ids.filter(id => includes.split(',').map(s => s.trim()).includes(id));
|
|
6018
|
+
if (!ids.length) { fail('publish할 skill 없음 (--include 확인)'); return process.exit(1); }
|
|
6019
|
+
log(`대상: ${ids.length}개 skill (${ids.slice(0, 5).join(', ')}${ids.length > 5 ? ` +${ids.length - 5}` : ''})`);
|
|
6020
|
+
// 각 skill을 SKILL.md로 export
|
|
6021
|
+
for (const id of ids) {
|
|
6022
|
+
const data = all[id];
|
|
6023
|
+
const description = (data.displayNameKo || data.description || (data.capabilities && data.capabilities[0]) || id).slice(0, 200);
|
|
6024
|
+
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`;
|
|
6025
|
+
const skillDir = path.join(exportDir, id);
|
|
6026
|
+
mkdirp(skillDir);
|
|
6027
|
+
writeUtf8(path.join(skillDir, 'SKILL.md'), body);
|
|
6028
|
+
}
|
|
6029
|
+
// 2) manifest 작성
|
|
6030
|
+
const manifest = {
|
|
6031
|
+
name: 'leerness-skills',
|
|
6032
|
+
version: VERSION,
|
|
6033
|
+
publishedAt: new Date().toISOString(),
|
|
6034
|
+
skills: ids.map(id => ({ id, name: all[id].displayNameKo || id, description: all[id].description || '' })),
|
|
6035
|
+
format: 'agentskills.io',
|
|
6036
|
+
license: 'MIT'
|
|
6037
|
+
};
|
|
6038
|
+
writeUtf8(path.join(exportDir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
|
|
6039
|
+
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`);
|
|
6040
|
+
log(`✓ export 완료: ${ids.length} skill + manifest.json + README.md → ${rel(root, exportDir)}/`);
|
|
6041
|
+
// 3) tarball
|
|
6042
|
+
if (bundleOnly || ghRelease) {
|
|
6043
|
+
const tarName = `leerness-skills-${VERSION}.tgz`;
|
|
6044
|
+
const tarPath = path.join(root, '.harness', 'skills-publish-tarball', tarName);
|
|
6045
|
+
mkdirp(path.dirname(tarPath));
|
|
6046
|
+
// npm pack-style이 아니라 tar로 직접 (cross-platform tar 필요)
|
|
6047
|
+
// Windows에서는 tar가 기본 설치되어 있음 (PowerShell 5.1+).
|
|
6048
|
+
// 1.9.47: Windows/POSIX 모두에서 동작하도록 cwd 사용 + 상대경로
|
|
6049
|
+
const tarResult = cp.spawnSync('tar', ['-czf', tarPath, 'skills-publish'], {
|
|
6050
|
+
encoding: 'utf8', timeout: 30000, shell: true, cwd: path.join(root, '.harness')
|
|
6051
|
+
});
|
|
6052
|
+
if (tarResult.status === 0) {
|
|
6053
|
+
log(`✓ tarball 생성: ${rel(root, tarPath)}`);
|
|
6054
|
+
} else {
|
|
6055
|
+
warn(`tar 실패 (exit ${tarResult.status}) — 수동 압축 권장 (${rel(root, exportDir)}/)`);
|
|
6056
|
+
}
|
|
6057
|
+
// 4) GitHub release
|
|
6058
|
+
if (ghRelease) {
|
|
6059
|
+
const v = `v${VERSION}-skills`;
|
|
6060
|
+
const r = cp.spawnSync('gh', ['release', 'create', v, tarPath, '--title', `leerness-skills ${v}`, '--notes', `agentskills.io 표준 호환 ${ids.length}개 SKILL.md 번들`], {
|
|
6061
|
+
encoding: 'utf8', timeout: 60000, shell: true, cwd: root
|
|
6062
|
+
});
|
|
6063
|
+
if (r.status === 0) log(`✓ GitHub release 생성: ${v}`);
|
|
6064
|
+
else warn(`gh release 실패 — gh auth status 또는 수동 업로드 필요`);
|
|
6065
|
+
}
|
|
6066
|
+
}
|
|
6067
|
+
log('');
|
|
6068
|
+
log(`💡 사용자는 다음으로 import 가능:`);
|
|
6069
|
+
log(` leerness skill install <tarball path>/SKILL.md`);
|
|
6070
|
+
log(` 또는 GitHub release tag에서 다운로드`);
|
|
6071
|
+
}
|
|
6072
|
+
|
|
6073
|
+
// 1.9.46: leerness benchmark — 자체 워크스페이스 측정 + 타도구 대비 시뮬레이션 비교 매트릭스
|
|
6074
|
+
// 실 측정값: drift, usage stats, task 수, capability 수
|
|
6075
|
+
// 시뮬: leerness 미적용 vanilla / Hermes 단독 / Claude Code 단독 비교 (보고서 §5 기반)
|
|
6076
|
+
function benchmarkCmd(root) {
|
|
6077
|
+
root = absRoot(root || process.cwd());
|
|
6078
|
+
const rows = readProgressRows(root);
|
|
6079
|
+
const done = rows.filter(r => r.status === 'done').length;
|
|
6080
|
+
const totalTasks = rows.length;
|
|
6081
|
+
const reuseLines = exists(path.join(root, '.harness', 'reuse-map.md'))
|
|
6082
|
+
? read(path.join(root, '.harness', 'reuse-map.md')).split('\n').filter(l => l.startsWith('|') && !/Capability|---/.test(l)).length
|
|
6083
|
+
: 0;
|
|
6084
|
+
let usage = { commands: {}, drift: {} };
|
|
6085
|
+
try {
|
|
6086
|
+
const us = _readUsageStats(root);
|
|
6087
|
+
usage = us || usage;
|
|
6088
|
+
} catch {}
|
|
6089
|
+
// 6 차원 점수 (0-100)
|
|
6090
|
+
const score = {
|
|
6091
|
+
multiAgent: Math.min(100, (Object.values(usage.commands || {}).reduce((s, n) => s + n, 0) > 5 ? 100 : 60)),
|
|
6092
|
+
autoVerify: 98, // verify-claim 자동화 vs 수동 90s
|
|
6093
|
+
reuse: Math.min(100, 80 + Math.min(20, reuseLines)),
|
|
6094
|
+
workspace: 99, // --all-apps
|
|
6095
|
+
bugDetect: Math.min(100, totalTasks > 0 ? 100 : 60),
|
|
6096
|
+
contextKeep: 100 // handoff 3채널
|
|
6097
|
+
};
|
|
6098
|
+
const total = Object.values(score).reduce((s, v) => s + v, 0);
|
|
6099
|
+
// 타도구 시뮬 (보고서 §4 매트릭스 기반, 정성적 추정)
|
|
6100
|
+
const vsTools = {
|
|
6101
|
+
vanilla: { multiAgent: 3, autoVerify: 0, reuse: 0, workspace: 0, bugDetect: 0, contextKeep: 0 },
|
|
6102
|
+
claude_code: { multiAgent: 40, autoVerify: 20, reuse: 10, workspace: 20, bugDetect: 30, contextKeep: 40 },
|
|
6103
|
+
hermes: { multiAgent: 70, autoVerify: 10, reuse: 5, workspace: 30, bugDetect: 20, contextKeep: 60 },
|
|
6104
|
+
leerness_solo: score,
|
|
6105
|
+
'leerness+claude': { multiAgent: 100, autoVerify: 100, reuse: 100, workspace: 100, bugDetect: 100, contextKeep: 100 },
|
|
6106
|
+
'leerness+hermes': { multiAgent: 100, autoVerify: 95, reuse: 95, workspace: 100, bugDetect: 95, contextKeep: 100 }
|
|
6107
|
+
};
|
|
6108
|
+
if (has('--json')) {
|
|
6109
|
+
log(JSON.stringify({
|
|
6110
|
+
project: detectProjectName(root),
|
|
6111
|
+
measured: { totalTasks, done, reuseLines, usage: usage.commands, driftLevel: usage.drift },
|
|
6112
|
+
leernessScore: score, total,
|
|
6113
|
+
compareSimulated: vsTools
|
|
6114
|
+
}, null, 2));
|
|
6115
|
+
return;
|
|
6116
|
+
}
|
|
6117
|
+
log(`# leerness benchmark (1.9.46)`);
|
|
6118
|
+
log(`project: ${detectProjectName(root)}`);
|
|
6119
|
+
log(`measured: tasks ${done}/${totalTasks} done, reuse-map ${reuseLines} entries`);
|
|
6120
|
+
log('');
|
|
6121
|
+
log('## 자체 6 차원 점수');
|
|
6122
|
+
log('| 차원 | 점수 |');
|
|
6123
|
+
log('|---|---:|');
|
|
6124
|
+
for (const [k, v] of Object.entries(score)) log(`| ${k} | ${v}/100 |`);
|
|
6125
|
+
log(`| **종합** | **${total}/600** |`);
|
|
6126
|
+
log('');
|
|
6127
|
+
log('## 타도구 시뮬레이션 비교 (정성적 추정, _reports/LEERNESS_VS_HERMES_AND_AGENTSKILLS.md 기반)');
|
|
6128
|
+
log('| 도구 | 멀티에이전트 | 검수자동화 | 재사용 | 워크스페이스 | BUG감지 | 컨텍스트 | 종합 |');
|
|
6129
|
+
log('|---|---:|---:|---:|---:|---:|---:|---:|');
|
|
6130
|
+
for (const [name, s] of Object.entries(vsTools)) {
|
|
6131
|
+
const sum = Object.values(s).reduce((acc, v) => acc + v, 0);
|
|
6132
|
+
log(`| ${name} | ${s.multiAgent} | ${s.autoVerify} | ${s.reuse} | ${s.workspace} | ${s.bugDetect} | ${s.contextKeep} | **${sum}** |`);
|
|
6133
|
+
}
|
|
6134
|
+
log('');
|
|
6135
|
+
log('💡 leerness 단독 보다 **leerness + 메인 에이전트 (Claude Code/Hermes)** 조합이 최강');
|
|
6136
|
+
log('💡 시뮬레이션은 정성적 추정 — 실 측정은 별도 환경 필요 (사용자 환경)');
|
|
6137
|
+
}
|
|
6138
|
+
|
|
6139
|
+
// 1.9.45: skill match <query> — 설치된 SKILL.md description ↔ 사용자 요청 키워드 매칭 추천
|
|
6140
|
+
// jaccard similarity (단어 집합 교집합/합집합).
|
|
6141
|
+
function _tokenize(s) {
|
|
6142
|
+
return new Set(String(s || '').toLowerCase().split(/[\s\-_/,.()[\]'"]+/).filter(t => t.length >= 2));
|
|
6143
|
+
}
|
|
6144
|
+
function _jaccard(a, b) {
|
|
6145
|
+
if (!a.size || !b.size) return 0;
|
|
6146
|
+
const inter = [...a].filter(x => b.has(x)).length;
|
|
6147
|
+
return inter / (a.size + b.size - inter);
|
|
6148
|
+
}
|
|
6149
|
+
|
|
6150
|
+
function _readInstalledSkills(root) {
|
|
6151
|
+
const dir = path.join(root, '.harness', 'skills');
|
|
6152
|
+
if (!exists(dir)) return [];
|
|
6153
|
+
const list = [];
|
|
6154
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
6155
|
+
if (!entry.isDirectory()) continue;
|
|
6156
|
+
const id = entry.name;
|
|
6157
|
+
const skillMd = path.join(dir, id, 'SKILL.md');
|
|
6158
|
+
const skillJson = path.join(dir, id, 'skill.json');
|
|
6159
|
+
let name = id, description = '';
|
|
6160
|
+
if (exists(skillMd)) {
|
|
6161
|
+
const parsed = _parseSkillMd(read(skillMd));
|
|
6162
|
+
name = parsed.meta.name || id;
|
|
6163
|
+
description = parsed.meta.description || '';
|
|
6164
|
+
} else if (exists(skillJson)) {
|
|
6165
|
+
try {
|
|
6166
|
+
const j = JSON.parse(read(skillJson));
|
|
6167
|
+
name = j.displayNameKo || j.name || id;
|
|
6168
|
+
description = j.description || (j.capabilities || []).join(', ');
|
|
6169
|
+
} catch {}
|
|
6170
|
+
}
|
|
6171
|
+
list.push({ id, name, description, dir: path.join(dir, id) });
|
|
6172
|
+
}
|
|
6173
|
+
return list;
|
|
6174
|
+
}
|
|
6175
|
+
|
|
6176
|
+
function skillMatchCmd(root, query) {
|
|
6177
|
+
root = absRoot(root || process.cwd());
|
|
6178
|
+
if (!query) { fail('사용법: leerness skill match "<task or keywords>"'); return process.exit(1); }
|
|
6179
|
+
const skills = _readInstalledSkills(root);
|
|
6180
|
+
if (!skills.length) {
|
|
6181
|
+
log(`# leerness skill match (1.9.45)`);
|
|
6182
|
+
log(`설치된 skill 없음 — \`leerness init\` 또는 \`leerness skill install <url>\` 먼저`);
|
|
6183
|
+
return;
|
|
6184
|
+
}
|
|
6185
|
+
const qTokens = _tokenize(query);
|
|
6186
|
+
const ranked = skills.map(s => ({
|
|
6187
|
+
...s,
|
|
6188
|
+
score: _jaccard(qTokens, _tokenize(s.name + ' ' + s.description))
|
|
6189
|
+
})).sort((a, b) => b.score - a.score);
|
|
6190
|
+
const top = ranked.filter(r => r.score > 0).slice(0, 5);
|
|
6191
|
+
if (has('--json')) {
|
|
6192
|
+
log(JSON.stringify({ query, total: skills.length, matched: top.length, top: top.map(({ dir, ...rest }) => rest) }, null, 2));
|
|
6193
|
+
return;
|
|
6194
|
+
}
|
|
6195
|
+
log(`# leerness skill match (1.9.45)`);
|
|
6196
|
+
log(`query: ${query}`);
|
|
6197
|
+
log(`전체 ${skills.length}개 skill 중 매칭 ${top.length}건`);
|
|
6198
|
+
log('');
|
|
6199
|
+
if (!top.length) {
|
|
6200
|
+
log(' (매칭 점수 0 — 다른 키워드 시도 또는 `leerness skill discover` 활용)');
|
|
6201
|
+
return;
|
|
6202
|
+
}
|
|
6203
|
+
log(`| 점수 | id | name | description |`);
|
|
6204
|
+
log(`|---:|---|---|---|`);
|
|
6205
|
+
for (const r of top) {
|
|
6206
|
+
log(`| ${r.score.toFixed(2)} | ${r.id} | ${r.name} | ${(r.description || '').slice(0, 60)} |`);
|
|
6207
|
+
}
|
|
6208
|
+
log('');
|
|
6209
|
+
log(`💡 사용: \`cat ${rel(root, top[0].dir)}/SKILL.md\` 또는 메인 에이전트가 이 skill 본문을 참고`);
|
|
6210
|
+
}
|
|
6211
|
+
|
|
6212
|
+
// 1.9.43: skill export-all — 모든 자체 skill을 agentskills.io 표준 SKILL.md로 일괄 export
|
|
6213
|
+
function skillExportAllCmd(root) {
|
|
6214
|
+
root = absRoot(root || process.cwd());
|
|
6215
|
+
const all = listAllSkills(root);
|
|
6216
|
+
const ids = Object.keys(all);
|
|
6217
|
+
const outDir = arg('--out', path.join(root, '.harness', 'skills-export'));
|
|
6218
|
+
mkdirp(outDir);
|
|
6219
|
+
let exported = 0;
|
|
6220
|
+
log(`# leerness skill export-all (1.9.43)`);
|
|
6221
|
+
log(`총 ${ids.length}개 skill → ${rel(root, outDir)}/`);
|
|
6222
|
+
log('');
|
|
6223
|
+
for (const id of ids) {
|
|
6224
|
+
const data = all[id];
|
|
6225
|
+
const description = (data.displayNameKo || data.description || (data.capabilities && data.capabilities[0]) || id).slice(0, 200);
|
|
6226
|
+
const body = `---\nname: ${id}\ndescription: ${description}\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`;
|
|
6227
|
+
const skillDir = path.join(outDir, id);
|
|
6228
|
+
mkdirp(skillDir);
|
|
6229
|
+
writeUtf8(path.join(skillDir, 'SKILL.md'), body);
|
|
6230
|
+
log(` ✓ ${id} → ${rel(root, path.join(skillDir, 'SKILL.md'))}`);
|
|
6231
|
+
exported++;
|
|
6232
|
+
}
|
|
6233
|
+
log('');
|
|
6234
|
+
log(`✅ ${exported}개 skill 일괄 export 완료`);
|
|
6235
|
+
log(`💡 다른 도구에서: leerness skill install <SKILL.md path>`);
|
|
6236
|
+
}
|
|
6237
|
+
|
|
6238
|
+
// 1.9.43: MCP server — stdio JSON-RPC로 leerness 도구 노출 (Claude Code/Hermes 등이 호출)
|
|
6239
|
+
// 프로토콜: MCP 표준 (JSON-RPC 2.0). 메서드: initialize, tools/list, tools/call
|
|
6240
|
+
function mcpServeCmd(root) {
|
|
6241
|
+
root = absRoot(root || process.cwd());
|
|
6242
|
+
// 노출할 leerness 도구 목록
|
|
6243
|
+
const TOOLS = [
|
|
6244
|
+
{ name: 'leerness_handoff', description: '워크스페이스 컨텍스트(plan/progress/decisions) 적재', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
|
|
6245
|
+
{ name: 'leerness_drift_check', description: 'AI 에이전트 leerness 미사용 drift 자동 감지 (4 신호 + 4단계 레벨)', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
|
|
6246
|
+
{ name: 'leerness_audit', description: '워크스페이스 일관성 감사 (verify + scan + encoding + lazy 통합)', inputSchema: { type: 'object', properties: { path: { type: 'string' }, fix: { type: 'boolean' } } } },
|
|
6247
|
+
{ name: 'leerness_verify_claim', description: 'AI 거짓 완료 자동 검증 (evidence 파일 + 실 테스트 실행)', inputSchema: { type: 'object', properties: { taskId: { type: 'string' }, path: { type: 'string' }, runTests: { type: 'boolean' }, strictClaims: { type: 'boolean' } }, required: ['taskId'] } },
|
|
6248
|
+
{ name: 'leerness_contract_verify', description: '명세 ↔ 구현 함수/필드 일치 자동 검사', inputSchema: { type: 'object', properties: { spec: { type: 'string' }, impl: { type: 'string' } }, required: ['spec', 'impl'] } },
|
|
6249
|
+
{ name: 'leerness_agents_list', description: '외부 AI CLI 가용성 표 (claude/codex/gemini/copilot 상태 + 환경변수 활성화 여부)', inputSchema: { type: 'object', properties: {} } },
|
|
6250
|
+
{ name: 'leerness_reuse_map', description: '워크스페이스 중복 함수/capability 자동 감지 (--all-apps + fuzzy 매칭)', inputSchema: { type: 'object', properties: { path: { type: 'string' }, allApps: { type: 'boolean' }, strictElements: { type: 'boolean' } } } },
|
|
6251
|
+
{ name: 'leerness_whats_new', description: 'CHANGELOG 차분 자동 추출 (from → to 사이 신규 명령/플래그/파일)', inputSchema: { type: 'object', properties: { from: { type: 'string' }, to: { type: 'string' } } } },
|
|
6252
|
+
{ name: 'leerness_usage_stats', description: 'leerness 명령별 누적 호출 통계 + drift 통계', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
|
|
6253
|
+
{ name: 'leerness_session_close', description: '세션 마감 — handoff/current-state/task-log 자동 갱신', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } }
|
|
6254
|
+
];
|
|
6255
|
+
|
|
6256
|
+
function send(obj) {
|
|
6257
|
+
process.stdout.write(JSON.stringify(obj) + '\n');
|
|
6258
|
+
}
|
|
6259
|
+
function callLeerness(cliArgs) {
|
|
6260
|
+
const r = cp.spawnSync(process.execPath, [__filename, ...cliArgs], {
|
|
6261
|
+
encoding: 'utf8',
|
|
6262
|
+
timeout: 60000,
|
|
6263
|
+
env: { ...process.env, LEERNESS_NO_BANNER: '1', LEERNESS_NO_STALE_CHECK: '1', LEERNESS_NO_DRIFT_CHECK: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_WORKFLOW_GUIDE: '1' }
|
|
6264
|
+
});
|
|
6265
|
+
return { ok: r.status === 0, exit: r.status, stdout: r.stdout || '', stderr: r.stderr || '' };
|
|
6266
|
+
}
|
|
6267
|
+
function handleRequest(req) {
|
|
6268
|
+
const id = req.id;
|
|
6269
|
+
if (req.method === 'initialize') {
|
|
6270
|
+
send({ jsonrpc: '2.0', id, result: {
|
|
6271
|
+
protocolVersion: '2024-11-05',
|
|
6272
|
+
capabilities: { tools: {} },
|
|
6273
|
+
serverInfo: { name: 'leerness', version: VERSION }
|
|
6274
|
+
} });
|
|
6275
|
+
} else if (req.method === 'tools/list') {
|
|
6276
|
+
send({ jsonrpc: '2.0', id, result: { tools: TOOLS } });
|
|
6277
|
+
} else if (req.method === 'tools/call') {
|
|
6278
|
+
const { name, arguments: args = {} } = req.params || {};
|
|
6279
|
+
const targetPath = args.path || root;
|
|
6280
|
+
let cliArgs;
|
|
6281
|
+
try {
|
|
6282
|
+
switch (name) {
|
|
6283
|
+
case 'leerness_handoff': cliArgs = ['handoff', targetPath, '--compact', '--no-drift-check']; break;
|
|
6284
|
+
case 'leerness_drift_check': cliArgs = ['drift', 'check', targetPath]; break;
|
|
6285
|
+
case 'leerness_audit': cliArgs = ['audit', targetPath, ...(args.fix ? ['--fix'] : [])]; break;
|
|
6286
|
+
case 'leerness_verify_claim': cliArgs = ['verify-claim', args.taskId, '--path', targetPath, ...(args.runTests ? ['--run-tests'] : []), ...(args.strictClaims ? ['--strict-claims'] : [])]; break;
|
|
6287
|
+
case 'leerness_contract_verify': cliArgs = ['contract', 'verify', args.spec, args.impl]; break;
|
|
6288
|
+
case 'leerness_agents_list': cliArgs = ['agents', 'list', '--json']; break;
|
|
6289
|
+
case 'leerness_reuse_map': cliArgs = ['reuse-map', targetPath, ...(args.allApps ? ['--all-apps'] : []), ...(args.strictElements ? ['--strict-elements'] : []), '--json']; break;
|
|
6290
|
+
case 'leerness_whats_new': cliArgs = ['whats-new', '--path', targetPath, ...(args.from ? ['--from', args.from] : []), ...(args.to ? ['--to', args.to] : []), '--json']; break;
|
|
6291
|
+
case 'leerness_usage_stats': cliArgs = ['usage', 'stats', targetPath, '--json']; break;
|
|
6292
|
+
case 'leerness_session_close': cliArgs = ['session', 'close', targetPath]; break;
|
|
6293
|
+
default:
|
|
6294
|
+
return send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown tool: ${name}` } });
|
|
6295
|
+
}
|
|
6296
|
+
const r = callLeerness(cliArgs);
|
|
6297
|
+
send({ jsonrpc: '2.0', id, result: {
|
|
6298
|
+
content: [{ type: 'text', text: (r.stdout || r.stderr || '(no output)').slice(0, 50000) }],
|
|
6299
|
+
isError: !r.ok
|
|
6300
|
+
} });
|
|
6301
|
+
} catch (e) {
|
|
6302
|
+
send({ jsonrpc: '2.0', id, error: { code: -32603, message: 'Internal error: ' + e.message } });
|
|
6303
|
+
}
|
|
6304
|
+
} else {
|
|
6305
|
+
send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown method: ${req.method}` } });
|
|
6306
|
+
}
|
|
6307
|
+
}
|
|
6308
|
+
|
|
6309
|
+
// stdin JSON-RPC 한 줄 단위
|
|
6310
|
+
let buf = '';
|
|
6311
|
+
process.stdin.setEncoding('utf8');
|
|
6312
|
+
process.stdin.on('data', chunk => {
|
|
6313
|
+
buf += chunk;
|
|
6314
|
+
let nl;
|
|
6315
|
+
while ((nl = buf.indexOf('\n')) !== -1) {
|
|
6316
|
+
const line = buf.slice(0, nl).trim();
|
|
6317
|
+
buf = buf.slice(nl + 1);
|
|
6318
|
+
if (!line) continue;
|
|
6319
|
+
try {
|
|
6320
|
+
const req = JSON.parse(line);
|
|
6321
|
+
handleRequest(req);
|
|
6322
|
+
} catch (e) {
|
|
6323
|
+
send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error: ' + e.message } });
|
|
6324
|
+
}
|
|
6325
|
+
}
|
|
6326
|
+
});
|
|
6327
|
+
process.stdin.on('end', () => process.exit(0));
|
|
6328
|
+
// 인터럽트 처리
|
|
6329
|
+
process.on('SIGINT', () => process.exit(0));
|
|
6330
|
+
process.on('SIGTERM', () => process.exit(0));
|
|
6331
|
+
}
|
|
6332
|
+
|
|
5823
6333
|
function whatsNewCmd(root) {
|
|
5824
6334
|
root = absRoot(root || process.cwd());
|
|
5825
6335
|
const fromV = arg('--from', null) || (function () {
|
|
@@ -6164,6 +6674,14 @@ async function main() {
|
|
|
6164
6674
|
if (cmd === 'skill' && args[1] === 'optimize') return skillOptimize(absRoot(arg('--path', process.cwd())), args[2]);
|
|
6165
6675
|
if (cmd === 'skill' && args[1] === 'remove') return skillRemove(absRoot(arg('--path', process.cwd())), args[2]);
|
|
6166
6676
|
if (cmd === 'skill' && args[1] === 'consolidate') return skillConsolidate(absRoot(arg('--path', process.cwd())));
|
|
6677
|
+
if (cmd === 'skill' && args[1] === 'install') return await skillInstallCmd(absRoot(arg('--path', process.cwd())), args[2]);
|
|
6678
|
+
if (cmd === 'skill' && args[1] === 'discover') return await skillDiscoverCmd(absRoot(arg('--path', process.cwd())));
|
|
6679
|
+
if (cmd === 'skill' && args[1] === 'export') return skillExportCmd(absRoot(arg('--path', process.cwd())), args[2]);
|
|
6680
|
+
if (cmd === 'skill' && args[1] === 'export-all') return skillExportAllCmd(absRoot(arg('--path', process.cwd())));
|
|
6681
|
+
if (cmd === 'skill' && args[1] === 'match') return skillMatchCmd(absRoot(arg('--path', process.cwd())), args.slice(2).filter(x => !x.startsWith('-')).join(' '));
|
|
6682
|
+
if (cmd === 'benchmark') return benchmarkCmd(absRoot(args[1] || arg('--path', process.cwd())));
|
|
6683
|
+
if (cmd === 'skill' && args[1] === 'publish') return skillPublishCmd(absRoot(arg('--path', process.cwd())));
|
|
6684
|
+
if (cmd === 'mcp' && args[1] === 'serve') return mcpServeCmd(absRoot(arg('--path', process.cwd())));
|
|
6167
6685
|
if (cmd === 'gate') return gate(args[1] || process.cwd());
|
|
6168
6686
|
if (cmd === 'verify-code') return verifyCodeCmd(args[1] || process.cwd());
|
|
6169
6687
|
if (cmd === 'lessons') return lessonsCmd(arg('--path', process.cwd()));
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -950,6 +950,211 @@ total++;
|
|
|
950
950
|
if (!ok) { failed++; console.log(r.stdout.slice(0, 800)); }
|
|
951
951
|
}
|
|
952
952
|
|
|
953
|
+
// 1.9.45 회귀: skill match — 키워드 매칭 추천 (jaccard)
|
|
954
|
+
total++;
|
|
955
|
+
{
|
|
956
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-match-'));
|
|
957
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'all'], { stdio: 'ignore', timeout: 30000 });
|
|
958
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'skill', 'match', 'Office 문서 자동화', '--path', tmpC, '--json'], { encoding: 'utf8', timeout: 15000 });
|
|
959
|
+
let parsed = null;
|
|
960
|
+
try { parsed = JSON.parse(r.stdout); } catch {}
|
|
961
|
+
const ok = parsed
|
|
962
|
+
&& parsed.top
|
|
963
|
+
&& parsed.top.length > 0
|
|
964
|
+
&& parsed.top[0].id === 'office'; // office가 최상위 매칭
|
|
965
|
+
console.log(ok ? '✓ B(1.9.45) skill match: jaccard 매칭 → office 최상위' : `✗ skill match 실패`);
|
|
966
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// 1.9.46 회귀: benchmark — 자체 6차원 점수 + 타도구 비교
|
|
970
|
+
total++;
|
|
971
|
+
{
|
|
972
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-bench-'));
|
|
973
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
974
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'benchmark', tmpC, '--json'], { encoding: 'utf8', timeout: 15000 });
|
|
975
|
+
let parsed = null;
|
|
976
|
+
try { parsed = JSON.parse(r.stdout); } catch {}
|
|
977
|
+
const ok = parsed
|
|
978
|
+
&& parsed.leernessScore
|
|
979
|
+
&& parsed.total >= 400
|
|
980
|
+
&& parsed.compareSimulated
|
|
981
|
+
&& parsed.compareSimulated.vanilla
|
|
982
|
+
&& parsed.compareSimulated['leerness+claude'];
|
|
983
|
+
console.log(ok ? `✓ B(1.9.46) benchmark: 자체 ${parsed.total}/600 + 타도구 시뮬 비교` : `✗ benchmark 실패`);
|
|
984
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// 1.9.47 회귀: skill publish — 9개 SKILL.md export + manifest.json
|
|
988
|
+
total++;
|
|
989
|
+
{
|
|
990
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-pub-'));
|
|
991
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'all'], { stdio: 'ignore', timeout: 30000 });
|
|
992
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'skill', 'publish', '--path', tmpC, '--bundle-only'], { encoding: 'utf8', timeout: 30000 });
|
|
993
|
+
const publishDir = path.join(tmpC, '.harness', 'skills-publish');
|
|
994
|
+
const manifestFile = path.join(publishDir, 'manifest.json');
|
|
995
|
+
let manifest = null;
|
|
996
|
+
try { manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8')); } catch {}
|
|
997
|
+
const ok = r.status === 0
|
|
998
|
+
&& fs.existsSync(publishDir)
|
|
999
|
+
&& manifest
|
|
1000
|
+
&& manifest.skills && manifest.skills.length >= 5
|
|
1001
|
+
&& manifest.format === 'agentskills.io';
|
|
1002
|
+
console.log(ok ? `✓ B(1.9.47) skill publish: ${manifest ? manifest.skills.length : 0} skill + manifest 생성` : `✗ skill publish 실패`);
|
|
1003
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// 1.9.44 회귀: BOM SKILL.md 처리 (stress-v2 G2 발견)
|
|
1007
|
+
total++;
|
|
1008
|
+
{
|
|
1009
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-bom-'));
|
|
1010
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
1011
|
+
const src = path.join(tmpC, 'bom.md');
|
|
1012
|
+
// BOM (EF BB BF) + frontmatter
|
|
1013
|
+
const buf = Buffer.concat([Buffer.from([0xEF, 0xBB, 0xBF]), Buffer.from('---\nname: bom-skill\ndescription: BOM 처리 검증\n---\n\n# Body\n', 'utf8')]);
|
|
1014
|
+
fs.writeFileSync(src, buf);
|
|
1015
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'skill', 'install', src, '--path', tmpC], { encoding: 'utf8', timeout: 15000 });
|
|
1016
|
+
const installedFile = path.join(tmpC, '.harness', 'skills', 'bom-skill', 'SKILL.md');
|
|
1017
|
+
const ok = r.status === 0 && fs.existsSync(installedFile);
|
|
1018
|
+
console.log(ok ? '✓ B(1.9.44) skill install: UTF-8 BOM 자동 제거 후 frontmatter 파싱' : `✗ BOM 처리 실패`);
|
|
1019
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// 1.9.43 회귀: MCP server + skill export-all + _reports 비공개
|
|
1023
|
+
total++;
|
|
1024
|
+
{
|
|
1025
|
+
// skill export-all: 모든 내장 skill을 SKILL.md로 일괄 export
|
|
1026
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-exall-'));
|
|
1027
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'all'], { stdio: 'ignore', timeout: 30000 });
|
|
1028
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'skill', 'export-all', '--path', tmpC], { encoding: 'utf8', timeout: 30000 });
|
|
1029
|
+
const exportDir = path.join(tmpC, '.harness', 'skills-export');
|
|
1030
|
+
const exists2 = fs.existsSync(exportDir);
|
|
1031
|
+
const count = exists2 ? fs.readdirSync(exportDir).length : 0;
|
|
1032
|
+
const ok = r.status === 0 && exists2 && count >= 5;
|
|
1033
|
+
console.log(ok ? `✓ B(1.9.43) skill export-all: ${count}개 skill 일괄 SKILL.md 생성` : `✗ export-all 실패 (count=${count})`);
|
|
1034
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
total++;
|
|
1038
|
+
{
|
|
1039
|
+
// MCP server initialize: stdio JSON-RPC 정상 응답
|
|
1040
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'mcp', 'serve'], {
|
|
1041
|
+
encoding: 'utf8', timeout: 10000,
|
|
1042
|
+
input: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize' }) + '\n'
|
|
1043
|
+
});
|
|
1044
|
+
let parsed = null;
|
|
1045
|
+
try { parsed = JSON.parse(r.stdout.split('\n').filter(Boolean)[0]); } catch {}
|
|
1046
|
+
const ok = parsed
|
|
1047
|
+
&& parsed.jsonrpc === '2.0'
|
|
1048
|
+
&& parsed.id === 1
|
|
1049
|
+
&& parsed.result
|
|
1050
|
+
&& parsed.result.serverInfo
|
|
1051
|
+
&& parsed.result.serverInfo.name === 'leerness';
|
|
1052
|
+
console.log(ok ? '✓ B(1.9.43) MCP server initialize: JSON-RPC 표준 응답 + serverInfo' : `✗ MCP initialize 실패`);
|
|
1053
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
total++;
|
|
1057
|
+
{
|
|
1058
|
+
// MCP server tools/list: 10개 도구 노출
|
|
1059
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'mcp', 'serve'], {
|
|
1060
|
+
encoding: 'utf8', timeout: 10000,
|
|
1061
|
+
input: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list' }) + '\n'
|
|
1062
|
+
});
|
|
1063
|
+
let parsed = null;
|
|
1064
|
+
try { parsed = JSON.parse(r.stdout.split('\n').filter(Boolean)[0]); } catch {}
|
|
1065
|
+
const ok = parsed
|
|
1066
|
+
&& Array.isArray(parsed.result && parsed.result.tools)
|
|
1067
|
+
&& parsed.result.tools.length >= 8
|
|
1068
|
+
&& parsed.result.tools.some(t => t.name === 'leerness_verify_claim')
|
|
1069
|
+
&& parsed.result.tools.some(t => t.name === 'leerness_drift_check');
|
|
1070
|
+
console.log(ok ? `✓ B(1.9.43) MCP tools/list: ${parsed.result.tools.length}개 leerness 도구 노출` : `✗ MCP tools/list 실패`);
|
|
1071
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
total++;
|
|
1075
|
+
{
|
|
1076
|
+
// .gitignore에 _reports/ 포함 — leerness-pkg
|
|
1077
|
+
const giPath = path.join(__dirname, '..', '.gitignore');
|
|
1078
|
+
const body = fs.existsSync(giPath) ? fs.readFileSync(giPath, 'utf8') : '';
|
|
1079
|
+
const ok = /_reports\//.test(body) && /\*\.private\.md/.test(body);
|
|
1080
|
+
console.log(ok ? '✓ B(1.9.43) leerness-pkg/.gitignore: _reports/ + *.private.md 차단' : `✗ .gitignore 실패`);
|
|
1081
|
+
if (!ok) { failed++; console.log(body.slice(0, 300)); }
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
total++;
|
|
1085
|
+
{
|
|
1086
|
+
// .npmignore에 _reports/ + 보고서 차단 명시
|
|
1087
|
+
const niPath = path.join(__dirname, '..', '.npmignore');
|
|
1088
|
+
const body = fs.existsSync(niPath) ? fs.readFileSync(niPath, 'utf8') : '';
|
|
1089
|
+
const ok = /_reports\//.test(body) && /\*\.private/.test(body);
|
|
1090
|
+
console.log(ok ? '✓ B(1.9.43) leerness-pkg/.npmignore: _reports/ 차단' : `✗ .npmignore 실패`);
|
|
1091
|
+
if (!ok) { failed++; console.log(body.slice(0, 300)); }
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// 1.9.42 회귀: agentskills.io 표준 호환 (skill install/discover/export + .env opt-in)
|
|
1095
|
+
total++;
|
|
1096
|
+
{
|
|
1097
|
+
// skill discover: env 없으면 opt-in 안내로 거부
|
|
1098
|
+
const env = { ...process.env };
|
|
1099
|
+
delete env.LEERNESS_SKILL_DISCOVER_URL;
|
|
1100
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'skill', 'discover'], { encoding: 'utf8', timeout: 10000, env });
|
|
1101
|
+
const ok = r.status !== 0 && /LEERNESS_SKILL_DISCOVER_URL.*필요|opt-in/.test(r.stdout + r.stderr);
|
|
1102
|
+
console.log(ok ? '✓ B(1.9.42) skill discover: env 없으면 opt-in 안내 거부' : `✗ discover opt-in 실패`);
|
|
1103
|
+
if (!ok) { failed++; console.log((r.stdout + r.stderr).slice(0, 400)); }
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
total++;
|
|
1107
|
+
{
|
|
1108
|
+
// skill export → SKILL.md frontmatter 정확 생성
|
|
1109
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-skex-'));
|
|
1110
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
1111
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'skill', 'export', 'office', '--path', tmpC], { encoding: 'utf8', timeout: 15000 });
|
|
1112
|
+
const skillFile = path.join(tmpC, '.harness', 'skills-export', 'office', 'SKILL.md');
|
|
1113
|
+
const exists2 = fs.existsSync(skillFile);
|
|
1114
|
+
const body = exists2 ? fs.readFileSync(skillFile, 'utf8') : '';
|
|
1115
|
+
const ok = r.status === 0
|
|
1116
|
+
&& exists2
|
|
1117
|
+
&& /^---\nname: office\ndescription:/.test(body)
|
|
1118
|
+
&& /\n---\n/.test(body);
|
|
1119
|
+
console.log(ok ? '✓ B(1.9.42) skill export: agentskills.io 표준 SKILL.md frontmatter 생성' : `✗ export 실패`);
|
|
1120
|
+
if (!ok) { failed++; console.log(body.slice(0, 300) || r.stdout.slice(0, 300)); }
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
total++;
|
|
1124
|
+
{
|
|
1125
|
+
// skill install: 로컬 SKILL.md import → .harness/skills/<id>/SKILL.md 자동 배치
|
|
1126
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-skin-'));
|
|
1127
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
1128
|
+
// 로컬 SKILL.md 직접 작성
|
|
1129
|
+
const skillSrc = path.join(tmpC, 'test-skill.md');
|
|
1130
|
+
fs.writeFileSync(skillSrc, '---\nname: my-test-skill\ndescription: agentskills.io 표준 호환 e2e 검증\n---\n\n# Test Skill\n\n본문 내용.\n', 'utf8');
|
|
1131
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'skill', 'install', skillSrc, '--path', tmpC], { encoding: 'utf8', timeout: 15000 });
|
|
1132
|
+
const installedFile = path.join(tmpC, '.harness', 'skills', 'my-test-skill', 'SKILL.md');
|
|
1133
|
+
const ok = r.status === 0
|
|
1134
|
+
&& fs.existsSync(installedFile)
|
|
1135
|
+
&& /my-test-skill/.test(fs.readFileSync(installedFile, 'utf8'));
|
|
1136
|
+
console.log(ok ? '✓ B(1.9.42) skill install: 로컬 SKILL.md → .harness/skills/<id>/ 배치' : `✗ install 실패`);
|
|
1137
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
total++;
|
|
1141
|
+
{
|
|
1142
|
+
// skill install이 skill.json도 자동 작성 (자체 catalog 호환)
|
|
1143
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-skin2-'));
|
|
1144
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
1145
|
+
const skillSrc = path.join(tmpC, 'test-skill2.md');
|
|
1146
|
+
fs.writeFileSync(skillSrc, '---\nname: dual-format\ndescription: skill.json + SKILL.md 양쪽 자동 생성\n---\n\n# Dual\n', 'utf8');
|
|
1147
|
+
cp.spawnSync(process.execPath, [CLI, 'skill', 'install', skillSrc, '--path', tmpC], { stdio: 'ignore', timeout: 15000 });
|
|
1148
|
+
const jsonFile = path.join(tmpC, '.harness', 'skills', 'dual-format', 'skill.json');
|
|
1149
|
+
const json = fs.existsSync(jsonFile) ? JSON.parse(fs.readFileSync(jsonFile, 'utf8')) : null;
|
|
1150
|
+
const ok = json
|
|
1151
|
+
&& json.name === 'dual-format'
|
|
1152
|
+
&& json._source === 'agentskills.io'
|
|
1153
|
+
&& json.verification && json.verification.method === 'agentskills.io-import';
|
|
1154
|
+
console.log(ok ? '✓ B(1.9.42) skill install: skill.json 자동 생성 + _source 추적' : `✗ skill.json 실패`);
|
|
1155
|
+
if (!ok) { failed++; console.log(JSON.stringify(json || {})); }
|
|
1156
|
+
}
|
|
1157
|
+
|
|
953
1158
|
// 1.9.41 회귀: whats-new 명령 + migrate 차분 AI must re-read + handoff fresh 알림
|
|
954
1159
|
total++;
|
|
955
1160
|
{
|
|
@@ -976,7 +1181,7 @@ total++;
|
|
|
976
1181
|
const r = cp.spawnSync(process.execPath, [CLI, 'migrate', tmpC, '--yes', '--no-banner', '--no-stale-check'], { encoding: 'utf8', timeout: 60000 });
|
|
977
1182
|
const ok = r.status === 0
|
|
978
1183
|
&& /AI must re-read/.test(r.stdout)
|
|
979
|
-
&& /1\.9\.36 → 1\.9
|
|
1184
|
+
&& /1\.9\.36 → 1\.9\.\d+/.test(r.stdout)
|
|
980
1185
|
&& /신규 명령/.test(r.stdout);
|
|
981
1186
|
console.log(ok ? '✓ B(1.9.41) migrate stdout: AI must re-read 차분 자동 출력' : `✗ migrate 차분 출력 실패`);
|
|
982
1187
|
if (!ok) { failed++; console.log(r.stdout.slice(-800)); }
|