leerness 1.9.40 → 1.9.43

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 CHANGED
@@ -1,5 +1,117 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.43 — 2026-05-19
4
+
5
+ **MCP 서버 + skill 일괄 export + _reports 비공개 + GitHub 배포 준비**.
6
+
7
+ [agentskills.io 분석](https://agentskills.io)에서 도출한 발전 로드맵의 Phase 1 즉시 후보 3건을 통합. leerness 도구를 **MCP 서버로 노출**하여 Claude Code · Hermes · Cursor 등 30+ 도구가 직접 호출 가능.
8
+
9
+ ### Added — MCP Server (sub-agent로서 leerness)
10
+
11
+ - **`leerness mcp serve`** 신규 명령 — stdio JSON-RPC로 leerness 도구 10종 노출:
12
+ - `leerness_handoff` · `leerness_drift_check` · `leerness_audit` (--fix 지원)
13
+ - `leerness_verify_claim` (--run-tests, --strict-claims)
14
+ - `leerness_contract_verify` (사양 ↔ 구현)
15
+ - `leerness_agents_list` · `leerness_reuse_map` · `leerness_whats_new`
16
+ - `leerness_usage_stats` · `leerness_session_close`
17
+ - 표준 MCP 프로토콜 (2024-11-05) — initialize / tools/list / tools/call
18
+ - 이제 Claude Code · Hermes · Cursor 등이 `.mcp.json`에 leerness를 등록하면 메인 에이전트가 leerness 검수를 sub-tool로 호출 가능
19
+
20
+ ### Added — skill 표준 export·discover
21
+
22
+ - **`leerness skill export-all [--out <dir>]`** — 모든 자체 skill(9개)을 agentskills.io 표준 `SKILL.md`로 일괄 export. 다른 도구가 `skill install <path>`로 즉시 import.
23
+
24
+ ### Added — 내부 보고서 비공개
25
+
26
+ - **`_reports/` 디렉토리 자동 비공개**:
27
+ - root `.gitignore`에 `_reports/`, `**/_reports/`, `*.private.md`, `*.private.json` 추가
28
+ - `leerness-pkg/.gitignore`에 동일 추가
29
+ - 신규 `leerness-pkg/.npmignore` — npm publish 시 명시적 제외
30
+ - `package.json#files` 화이트리스트와 이중 안전
31
+ - 내부 검수 보고서 (`LEERNESS_VS_HERMES_AND_AGENTSKILLS.md`, `SESSION_LEERNESS_USAGE_AUDIT.md` 등)는 사용자 확인 전용이며 npm/GitHub 배포에 포함되지 않음
32
+
33
+ ### Verified
34
+ - e2e: **195/195 PASS** (1.9.42 190 + 신규 5)
35
+ - MCP server initialize/tools/list 정상 JSON-RPC 응답
36
+ - skill export-all → 9개 SKILL.md 일괄 생성
37
+ - .gitignore/.npmignore에 _reports/ 차단 확인
38
+
39
+ ### 정책
40
+ - ✅ MCP server는 명시 호출 (`leerness mcp serve`) 시에만 작동 — 자동 시작 안 함
41
+ - ✅ MCP 도구 호출 시 LEERNESS_NO_BANNER/NO_PROMPT/NO_DRIFT_CHECK 자동 설정 (호스트 환경 깔끔)
42
+ - ✅ _reports 비공개 — 다중 채널 (gitignore + npmignore + files 화이트리스트)
43
+
44
+ ## 1.9.42 — 2026-05-19
45
+
46
+ **agentskills.io 공개 표준 호환 — 30+ AI 도구와 스킬 즉시 공유**.
47
+
48
+ [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 가능.
49
+
50
+ ### Added
51
+
52
+ - **`leerness skill install <url-or-path>`** 신규 명령 — `SKILL.md` 다운로드/import:
53
+ - URL (https://...) 또는 로컬 파일/디렉토리 모두 지원
54
+ - frontmatter (`name`, `description`) 파싱 → `.harness/skills/<id>/SKILL.md` 자동 배치
55
+ - 자체 `skill.json` 도 함께 생성 (자체 catalog 호환, `_source: 'agentskills.io'` 추적)
56
+ - **`leerness skill discover [--query <q>] [--source <url>]`** 신규 명령 — 공개 스킬 카탈로그에서 매칭 추천:
57
+ - **opt-in**: `LEERNESS_SKILL_DISCOVER_URL` 환경변수 또는 `--source` 명시 필요 (자동 외부 fetch 금지 정책 유지)
58
+ - `--query` 키워드 매칭 + 마크다운 링크/SKILL.md URL 자동 추출
59
+ - `--json` 출력 지원
60
+ - **`leerness skill export <id> [--out <dir>]`** 신규 명령 — 기존 자체 skill을 agentskills.io 표준 `SKILL.md` 포맷으로 export → 다른 도구가 `skill install`로 import 가능
61
+ - **`.env.example`에 2개 신규 환경변수** (opt-in, 기본 OFF):
62
+ - `LEERNESS_SKILL_DISCOVER_URL=` — 공개 카탈로그 URL
63
+ - `LEERNESS_SKILL_AUTO_DISCOVER=0` — 사용자 요청 분석 시 자동 매칭 추천
64
+ - **`_httpFetch()` 내장 HTTPS 호출자** — Node 18+ globalThis.fetch, fallback https module. 사용자 동의 명령에서만 호출.
65
+
66
+ ### Reports
67
+ - `_reports/LEERNESS_VS_HERMES_AND_AGENTSKILLS.md` 작성 — 10 섹션 상세 분석:
68
+ - agentskills.io 표준 + Progressive Disclosure 메커니즘
69
+ - Hermes Agent (NousResearch, 157k ⭐, MIT) 분석
70
+ - leerness 4 고유 우위 (거짓 완료 검증, drift 자동 감지, 워크스페이스 가시성, 마이그레이션 인지 갭)
71
+ - 1.9.42 → 2.0 발전 로드맵 3 Phase
72
+
73
+ ### 정책
74
+ - ❌ leerness는 외부 URL 자동 fetch 절대 금지 — opt-in (env 또는 `--source` 명시) 필수
75
+ - ✅ `_httpFetch`는 사용자 명령 (`skill install URL` / `skill discover`)에서만 호출
76
+ - ✅ 기존 자체 skillCatalog와 양립 — `_source: 'agentskills.io'`로 출처 추적
77
+
78
+ ### e2e: 190/190 PASS (1.9.41 186 + 신규 4)
79
+
80
+ ## 1.9.41 — 2026-05-19
81
+
82
+ **디스크 마이그레이션 ↔ AI 컨텍스트 인지 갭 차단 — 맞춤형 차분 마이그레이션**.
83
+
84
+ 사용자 통찰: 같은 채팅 세션에서 leerness를 latest로 migrate해도, AI 에이전트는 이전 청크의 마인드셋으로 계속 작업하여 신규 도구(release pack, drift check 등)를 자동으로 호출하지 않는 패턴 발견. migrate는 파일만 업데이트, AI에겐 "새 도구가 들어왔다"는 신호 전달 부재.
85
+
86
+ ### Added
87
+
88
+ - **`leerness whats-new [--from V] [--to V] [--json]`** 신규 명령 — CHANGELOG.md를 자동 파싱하여 두 버전 사이의 차분 추출:
89
+ - 신규 명령 (`leerness X` 패턴), 신규 플래그 (`--xxx`), 신규 파일 (`.harness/*.md`) 자동 분류
90
+ - 각 버전의 헤드라인 (`**...**` 또는 첫 라인) 추출
91
+ - AI 가독 권장 행동 자동 출력
92
+ - **`migrate` 후 stdout에 AI must re-read 차분 자동 출력** — migrate 직전 이전 버전을 캡처 (`_previousVersion`) → CHANGELOG 차분 추출 → 신규 명령/파일을 stdout에 즉시 표시:
93
+ - "이전 청크의 기억 무효 — 새 도구 우선 시도" 명시
94
+ - 같은 세션 내 AI 인-컨텍스트에 신규 도구 인지 주입
95
+ - **`migration-report.md`에 "🤖 AI must re-read" 섹션 영구 기록** — 신규 명령/플래그/파일 + 버전별 헤드라인 + 권장 행동
96
+ - **`handoff`가 fresh migration-report (24h 내) 시 자동 알림** — "🆕 최근 N시간 전 migrate 차분" 블록 자동 표시. 같은 세션 내 매 handoff 호출이 AI에게 신규 도구 재안내.
97
+
98
+ ### 발견된 시스템 결함 (이번 라운드 해결)
99
+ - ❌ **before 1.9.41**: migrate가 파일만 업데이트, AI 마인드셋 stale 유지 → 신규 도구 자동 호출 X
100
+ - ✅ **1.9.41 이후**: migrate 직후 stdout + migration-report.md + handoff 모두 신규 도구를 AI 가독 포맷으로 노출 → "잊을 수 없는" 차분 안내
101
+
102
+ ### 자기 검증
103
+ - 의도적으로 root를 1.9.37로 되돌림 → `leerness migrate .` 호출 → **AI must re-read 차분 자동 stdout 출력**:
104
+ - `📌 신규 명령: leerness release pack`
105
+ - 1.9.38/1.9.39/1.9.40 버전별 헤드라인 자동 추출
106
+ - 권장 행동 4단계 (--help, 신규 파일 재독, 인스트럭션 재독, whats-new --json)
107
+
108
+ ### e2e: 186/186 PASS (1.9.40 182 + 신규 4)
109
+
110
+ ### 정책
111
+ - ✅ 차분 안내는 **AI 가독 포맷** (`**📌**`, `` `leerness X` `` 등 마크다운)
112
+ - ✅ 같은 세션 내 다양한 채널 (stdout + report + handoff)로 *반복 노출* → 청크 stale 방지
113
+ - ✅ 추출은 CHANGELOG.md 파싱 — 새 라운드 마다 자동 갱신
114
+
3
115
  ## 1.9.40 — 2026-05-19
4
116
 
5
117
  **dogfooding gap 차단 — `leerness release pack` 통합 명령 + audit README mismatch 자동 감지**.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **AI 코딩 에이전트의 거짓 완료·중복·망각·충돌을 막아주는 검수·기억·협업 CLI 하네스.**
4
4
 
5
- [![npm](https://img.shields.io/badge/npm-leerness-blue)](https://www.npmjs.com/package/leerness) [![version](https://img.shields.io/badge/version-1.9.40-green)]() [![tests](https://img.shields.io/badge/e2e-138%2F138-success)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
5
+ [![npm](https://img.shields.io/badge/npm-leerness-blue)](https://www.npmjs.com/package/leerness) [![version](https://img.shields.io/badge/version-1.9.43-green)]() [![tests](https://img.shields.io/badge/e2e-195%2F195-success)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
6
6
 
7
7
  ```
8
8
  ╔══════════════════════════════════════════════════════════════╗
@@ -12,7 +12,7 @@
12
12
  ║ ██║ ██╔══╝ ██╔══╝ ██╔══██╗██║╚██╗██║██╔══╝ ╚════██║ ║
13
13
  ║ ███████╗███████╗███████╗██║ ██║██║ ╚████║███████╗███████║ ║
14
14
  ║ ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝ ║
15
- ║ v1.9.40 AI Agent Reliability Harness ║
15
+ ║ v1.9.43 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,9 @@ npm test # = node ./scripts/e2e.js
394
433
 
395
434
  ## 변경 이력 (최근)
396
435
 
436
+ - **1.9.43** — MCP 서버로 leerness 도구 10종 노출 (`leerness mcp serve`, Claude Code/Hermes/Cursor 등이 직접 호출 가능) · `skill export-all` (9개 일괄 SKILL.md export) · 내부 보고서 자동 비공개 (`_reports/` gitignore + npmignore).
437
+ - **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
+ - **1.9.41** — 디스크↔AI 컨텍스트 인지 갭 차단: `leerness whats-new` 명령 (CHANGELOG 자동 차분 추출) · `migrate` 후 stdout에 AI must re-read 차분 자동 출력 · `migration-report.md`에 신규 명령/파일 영구 기록 · `handoff`가 fresh migrate(24h 내) 시 자동 알림.
397
439
  - **1.9.40** — dogfooding gap 차단: `leerness release pack` 통합 명령 (라운드 마감 자동화 — npm pack + parent migrate + task add + close + readme sync) · `audit`에 README ↔ package.json version mismatch 자동 감지 + `--fix`로 자동 갱신.
398
440
  - **1.9.39** — AI 하네스 엔지니어링 6단계 워크플로 자동 유도 (`session-workflow.md` + handoff 끝 가이드 + AGENTS/CLAUDE 인스트럭션 통합) · `drift check --auto-fix` · `handoff --auto-recover` (critical 시 session close 자동 실행).
399
441
  - **1.9.38** — drift 자동 reminder (`agent-reminders.md`) · `usage stats` 명령 · `task sync --from <todo.json>` · drift 임계 학습 (skip ≥5 → 임계 완화).
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.40';
9
+ const VERSION = '1.9.43';
10
10
  const MARK = '<!-- leerness:managed -->';
11
11
  const README_START = '<!-- leerness:project-readme:start -->';
12
12
  const README_END = '<!-- leerness:project-readme:end -->';
@@ -411,10 +411,43 @@ function mergeLinesFile(p, lines) {
411
411
  writeUtf8(p, next);
412
412
  }
413
413
 
414
- function writeMigrationReport(root, backup, actions) {
414
+ function writeMigrationReport(root, backup, actions, opts = {}) {
415
415
  const p = path.join(root, '.harness/migration-report.md');
416
416
  const rows = actions.map(a => `| ${a.file} | ${a.action} |`).join('\n');
417
- writeUtf8(p, `# Leerness Migration Report\n\nVersion: ${VERSION}\nDate: ${now()}\nBackup: ${rel(root, backup.archiveDir)}\n\n## Policy\n\n- Existing harness, skill, and instruction files are backed up before migration.\n- Project memory files are preserved by default.\n- Managed instruction files are merged with previous content instead of being blindly overwritten.\n- .env.example/.gitignore are line-merged only.\n\n## Backed Up Candidates\n\n${backup.candidates.map(x => '- ' + x).join('\n')}\n\n## File Actions\n\n| File | Action |\n|---|---|\n${rows}\n`);
417
+ // 1.9.41: AI must re-read 섹션 migrate가 추가/변경한 파일을 AI 가독 포맷으로 추출
418
+ // fromV가 명시되면 CHANGELOG 차분 포함
419
+ let aiReadBlock = '';
420
+ try {
421
+ const fromV = opts.fromV || (backup && backup.previousVersion) || null;
422
+ if (fromV && fromV !== VERSION) {
423
+ const changelogPath = path.join(__dirname, '..', 'CHANGELOG.md');
424
+ const cl = exists(changelogPath) ? read(changelogPath) : (exists(path.join(root, 'CHANGELOG.md')) ? read(path.join(root, 'CHANGELOG.md')) : '');
425
+ if (cl) {
426
+ const diff = _parseChangelogBetween(cl, fromV, VERSION);
427
+ const allCommands = new Set(), allFlags = new Set(), allFiles = new Set();
428
+ for (const v of diff) {
429
+ v.newCommands.forEach(c => allCommands.add(c));
430
+ v.newFlags.forEach(f => allFlags.add(f));
431
+ v.newFiles.forEach(f => allFiles.add(f));
432
+ }
433
+ if (diff.length) {
434
+ aiReadBlock = `\n## 🤖 AI must re-read (1.9.41 차분 안내)\n\n`;
435
+ aiReadBlock += `이 migrate는 ${fromV} → ${VERSION} 점프입니다. 메인 AI 에이전트는 다음을 인지하고 우선 활용:\n\n`;
436
+ if (allCommands.size) aiReadBlock += `**📌 신규 명령** (이전엔 없던 것):\n${[...allCommands].map(c => `- \`leerness ${c}\``).join('\n')}\n\n`;
437
+ if (allFlags.size) aiReadBlock += `**🚩 신규 플래그**:\n${[...allFlags].map(f => `- \`${f}\``).join('\n')}\n\n`;
438
+ if (allFiles.size) aiReadBlock += `**📄 신규/변경 파일** (반드시 재독):\n${[...allFiles].map(f => `- \`${f}\``).join('\n')}\n\n`;
439
+ aiReadBlock += `**버전별 헤드라인**:\n`;
440
+ for (const v of diff) {
441
+ const firstLine = (v.body.match(/^\*\*([^*]+)\*\*/) || [])[1]
442
+ || (v.body.split('\n').find(l => l.trim() && !l.startsWith('##')) || '').trim().slice(0, 120);
443
+ aiReadBlock += `- ${v.version} — ${firstLine || '(no headline)'}\n`;
444
+ }
445
+ aiReadBlock += `\n**권장 행동**:\n1. 위 신규 명령을 \`--help\`로 확인\n2. \`AGENTS.md\` / \`CLAUDE.md\` / \`.harness/session-workflow.md\` 재독 (다음 \`leerness handoff\` 호출 시 자동 안내)\n3. 이전 청크의 기억 무효 — 새 도구 우선 시도\n4. 상세: \`leerness whats-new --from ${fromV}\`\n`;
446
+ }
447
+ }
448
+ }
449
+ } catch {}
450
+ writeUtf8(p, `# Leerness Migration Report\n\nVersion: ${VERSION}\nDate: ${now()}\nBackup: ${rel(root, backup.archiveDir)}\n${opts.fromV ? `Previous: ${opts.fromV}\n` : ''}${aiReadBlock}\n## Policy\n\n- Existing harness, skill, and instruction files are backed up before migration.\n- Project memory files are preserved by default.\n- Managed instruction files are merged with previous content instead of being blindly overwritten.\n- .env.example/.gitignore are line-merged only.\n\n## Backed Up Candidates\n\n${backup.candidates.map(x => '- ' + x).join('\n')}\n\n## File Actions\n\n| File | Action |\n|---|---|\n${rows}\n`);
418
451
  }
419
452
 
420
453
  function syncReadme(root) {
@@ -517,6 +550,14 @@ async function resolveInstallOptions(root, opts = {}) {
517
550
 
518
551
  async function install(root, opts = {}) {
519
552
  root = absRoot(root); mkdirp(root);
553
+ // 1.9.41: migrate 직전 이전 버전 캡처 — 차분 안내에 사용
554
+ try {
555
+ const hv = path.join(root, '.harness', 'HARNESS_VERSION');
556
+ if (exists(hv) && !opts._previousVersion) {
557
+ const parsed = parseHarnessVersion(read(hv));
558
+ opts._previousVersion = parsed.base || parsed.plus || null;
559
+ }
560
+ } catch {}
520
561
  // 1.9.32: init 시 ASCII 배너 + 빠른 시작 가이드 (migrate는 quiet)
521
562
  if (!opts.migration && !has('--no-banner')) _banner({ quickStart: !opts.dry });
522
563
  // 1.9.33: npx 캐시로 옛 버전이 실행될 때 경고 (migrate/--no-stale-check 시 스킵)
@@ -581,14 +622,35 @@ async function install(root, opts = {}) {
581
622
  'LEERNESS_ENABLE_CLAUDE=1',
582
623
  'LEERNESS_ENABLE_CODEX=0',
583
624
  'LEERNESS_ENABLE_GEMINI=0',
584
- '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'
585
631
  ]);
586
632
  mergeLinesFile(path.join(root, '.gitattributes'), [
587
633
  '* text=auto eol=lf','*.bat text eol=crlf','*.ps1 text eol=crlf'
588
634
  ]);
589
635
  syncReadme(root);
590
636
  installSkills(root, skills);
591
- writeMigrationReport(root, backup, actions);
637
+ // 1.9.41: migrate 시 이전 버전을 미리 캡처해 차분 안내에 사용
638
+ writeMigrationReport(root, backup, actions, { fromV: opts._previousVersion || null });
639
+ // 1.9.41: migrate 후 (= 점프인 경우) 차분 안내를 stdout에 즉시 출력 — AI 컨텍스트에 새 도구 주입
640
+ if (opts.migration && opts._previousVersion && opts._previousVersion !== VERSION) {
641
+ try {
642
+ const reportPath = path.join(root, '.harness', 'migration-report.md');
643
+ if (exists(reportPath)) {
644
+ const rep = read(reportPath);
645
+ const aiBlock = rep.match(/## 🤖 AI must re-read[\s\S]*?(?=\n## )/);
646
+ if (aiBlock) {
647
+ log('');
648
+ log(aiBlock[0].trim());
649
+ log('');
650
+ }
651
+ }
652
+ } catch {}
653
+ }
592
654
  // 1.9.1 P7: 디폴트 M-0001이 plan에 있고 progress에 row가 없으면 자동 추가
593
655
  try {
594
656
  const planText = exists(planPath(root)) ? read(planPath(root)) : '';
@@ -797,6 +859,177 @@ function skillConsolidate(root) {
797
859
  for (const c of candidates) log(`| ${c.a} | ${c.b} | ${c.score.toFixed(2)} | \`leerness skill learn <new> --capability ...\` 후 \`leerness skill remove <old>\` |`);
798
860
  }
799
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
+ function _parseSkillMd(text) {
867
+ const m = text.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
868
+ if (!m) return { meta: {}, body: text };
869
+ const meta = {};
870
+ for (const line of m[1].split('\n')) {
871
+ const km = line.match(/^([a-zA-Z_-]+):\s*(.+)$/);
872
+ if (km) meta[km[1].trim()] = km[2].trim().replace(/^["']|["']$/g, '');
873
+ }
874
+ return { meta, body: m[2] };
875
+ }
876
+
877
+ // HTTPS fetch — Node 18+ globalThis.fetch 사용. 미지원 시 https module.
878
+ async function _httpFetch(urlStr, opts = {}) {
879
+ const timeout = opts.timeout || 15000;
880
+ if (typeof fetch === 'function') {
881
+ const controller = new AbortController();
882
+ const timer = setTimeout(() => controller.abort(), timeout);
883
+ try {
884
+ const r = await fetch(urlStr, { signal: controller.signal });
885
+ clearTimeout(timer);
886
+ return { status: r.status, body: await r.text() };
887
+ } catch (e) {
888
+ clearTimeout(timer);
889
+ return { status: 0, body: '', error: e.message };
890
+ }
891
+ }
892
+ // fallback: https module
893
+ return new Promise((resolve) => {
894
+ const u = new URL(urlStr);
895
+ const lib = u.protocol === 'http:' ? require('http') : require('https');
896
+ const req = lib.get(urlStr, (res) => {
897
+ // redirect handling
898
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
899
+ return _httpFetch(res.headers.location, opts).then(resolve);
900
+ }
901
+ let chunks = [];
902
+ res.on('data', c => chunks.push(c));
903
+ res.on('end', () => resolve({ status: res.statusCode, body: Buffer.concat(chunks).toString('utf8') }));
904
+ });
905
+ req.on('error', (e) => resolve({ status: 0, body: '', error: e.message }));
906
+ req.setTimeout(timeout, () => { req.destroy(); resolve({ status: 0, body: '', error: 'timeout' }); });
907
+ });
908
+ }
909
+
910
+ // skill install <url-or-path> — SKILL.md 다운로드 + .harness/skills/<id>/에 설치
911
+ async function skillInstallCmd(root, source) {
912
+ if (!source) { fail('사용법: leerness skill install <SKILL.md URL 또는 로컬 디렉토리>'); return process.exit(1); }
913
+ let body = '';
914
+ if (/^https?:\/\//.test(source)) {
915
+ log(`# leerness skill install (1.9.42)`);
916
+ log(`다운로드 중: ${source}`);
917
+ const r = await _httpFetch(source);
918
+ if (r.status !== 200) {
919
+ fail(`다운로드 실패 (HTTP ${r.status}${r.error ? `, ${r.error}` : ''})`);
920
+ return process.exit(1);
921
+ }
922
+ body = r.body;
923
+ } else if (exists(source)) {
924
+ const localPath = exists(path.join(source, 'SKILL.md')) ? path.join(source, 'SKILL.md') : source;
925
+ body = read(localPath);
926
+ log(`# leerness skill install (1.9.42)`);
927
+ log(`로컬 로드: ${localPath}`);
928
+ } else {
929
+ fail(`source 없음 (URL 또는 디렉토리 경로): ${source}`);
930
+ return process.exit(1);
931
+ }
932
+ const parsed = _parseSkillMd(body);
933
+ const name = parsed.meta.name || parsed.meta.id;
934
+ const description = parsed.meta.description || '';
935
+ if (!name) { fail('SKILL.md frontmatter에 `name` 필수'); return process.exit(1); }
936
+ // .harness/skills/<id>/SKILL.md 저장
937
+ const skillId = String(name).toLowerCase().replace(/[^a-z0-9._-]+/g, '-');
938
+ const dir = path.join(root, '.harness', 'skills', skillId);
939
+ mkdirp(dir);
940
+ writeUtf8(path.join(dir, 'SKILL.md'), body);
941
+ // skill.json도 함께 (자체 catalog 호환)
942
+ writeUtf8(path.join(dir, 'skill.json'), JSON.stringify({
943
+ name: skillId, displayNameKo: name, description,
944
+ capabilities: [], _source: 'agentskills.io',
945
+ verification: { status: 'unverified', method: 'agentskills.io-import' }
946
+ }, null, 2) + '\n');
947
+ log(`✓ skill installed: ${skillId}`);
948
+ log(` name: ${name}`);
949
+ log(` description: ${description.slice(0, 100)}`);
950
+ log(` saved: ${rel(root, dir)}/`);
951
+ log('');
952
+ log(`💡 다음: leerness skill info ${skillId}`);
953
+ }
954
+
955
+ // skill discover — agentskills.io 또는 사용자 지정 URL의 카탈로그 인덱스에서 매칭 추천
956
+ async function skillDiscoverCmd(root) {
957
+ const url = arg('--source', null) || process.env.LEERNESS_SKILL_DISCOVER_URL || null;
958
+ const query = arg('--query', null);
959
+ if (!url) {
960
+ fail([
961
+ 'LEERNESS_SKILL_DISCOVER_URL 환경변수 또는 --source URL 필요.',
962
+ '예: leerness skill discover --source https://agentskills.io/llms.txt',
963
+ '또는 .env에 LEERNESS_SKILL_DISCOVER_URL=...',
964
+ '',
965
+ '(정책: leerness는 사용자 동의 없이 외부 URL을 fetch하지 않음 — 1.9.42 opt-in)'
966
+ ].join('\n'));
967
+ return process.exit(1);
968
+ }
969
+ log(`# leerness skill discover (1.9.42)`);
970
+ log(`source: ${url}`);
971
+ if (query) log(`query: ${query}`);
972
+ log(`fetching...`);
973
+ const r = await _httpFetch(url);
974
+ if (r.status !== 200) {
975
+ fail(`fetch 실패 (HTTP ${r.status}${r.error ? `, ${r.error}` : ''})`);
976
+ return process.exit(1);
977
+ }
978
+ // 카탈로그 인덱스 파싱 — agentskills.io는 llms.txt 형식 또는 raw 마크다운
979
+ const body = r.body;
980
+ // 간이 추출: SKILL.md 링크 + name + description 패턴
981
+ // - URL: https://.../SKILL.md
982
+ // - 마크다운 링크: [name](url) — description
983
+ const entries = [];
984
+ for (const m of body.matchAll(/^\s*-\s*\[([^\]]+)\]\(([^)]+)\)\s*[-—:]\s*(.+)$/gm)) {
985
+ entries.push({ name: m[1], url: m[2], description: m[3].trim() });
986
+ }
987
+ // URL only (개별 SKILL.md 파일)
988
+ if (!entries.length) {
989
+ for (const m of body.matchAll(/(https?:\/\/[^\s)]+SKILL\.md)/g)) {
990
+ entries.push({ name: m[1].split('/').slice(-2)[0], url: m[1], description: '' });
991
+ }
992
+ }
993
+ if (has('--json')) { log(JSON.stringify({ source: url, query, entries }, null, 2)); return; }
994
+ if (!entries.length) {
995
+ log(' (스킬 항목을 찾지 못함 — URL 형식 확인)');
996
+ return;
997
+ }
998
+ // 쿼리 매칭 (description 단순 포함)
999
+ let matched = entries;
1000
+ if (query) {
1001
+ const q = query.toLowerCase();
1002
+ matched = entries.filter(e => e.name.toLowerCase().includes(q) || (e.description || '').toLowerCase().includes(q));
1003
+ log(`매칭 ${matched.length}/${entries.length}건`);
1004
+ } else {
1005
+ log(`전체 ${entries.length}건 (매칭 없음 — --query로 필터링)`);
1006
+ }
1007
+ log('');
1008
+ log('| name | description | url |');
1009
+ log('|---|---|---|');
1010
+ for (const e of matched.slice(0, 30)) {
1011
+ log(`| ${e.name} | ${e.description.slice(0, 60)} | ${e.url} |`);
1012
+ }
1013
+ log('');
1014
+ log(`💡 설치: leerness skill install <url>`);
1015
+ }
1016
+
1017
+ // skill export <id> — 기존 자체 skill을 agentskills.io 표준 SKILL.md로 export
1018
+ function skillExportCmd(root, id) {
1019
+ if (!id) { fail('사용법: leerness skill export <id>'); return process.exit(1); }
1020
+ const data = loadUserSkill(root, id) || (skillCatalog[id] ? { ...skillCatalog[id], name: id } : null);
1021
+ if (!data) { fail(`skill 없음: ${id}`); return process.exit(1); }
1022
+ const description = data.displayNameKo || data.description || (data.capabilities && data.capabilities[0]) || id;
1023
+ 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`;
1024
+ const outDir = arg('--out', path.join(root, '.harness', 'skills-export', id));
1025
+ mkdirp(outDir);
1026
+ const outPath = path.join(outDir, 'SKILL.md');
1027
+ writeUtf8(outPath, body);
1028
+ log(`✓ exported to ${rel(root, outPath)}`);
1029
+ log('');
1030
+ log(`💡 공유 가능 — 다른 도구가 \`leerness skill install ${outPath}\` 또는 URL로 import`);
1031
+ }
1032
+
800
1033
  const planPath = root => path.join(root, '.harness/plan.md');
801
1034
  const progressPath = root => path.join(root, '.harness/progress-tracker.md');
802
1035
  const taskLogPath = root => path.join(root, '.harness/task-log.md');
@@ -1425,6 +1658,29 @@ function handoff(root) {
1425
1658
  const cs = read(currentStatePath(root)).replace(/Updated: \d{4}-\d{2}-\d{2}/, `Updated: ${today()}`);
1426
1659
  writeUtf8(currentStatePath(root), cs);
1427
1660
  }
1661
+ // 1.9.41: 최근 migrate 차분 알림 — migration-report.md가 24h 내면 "AI must re-read" 블록 자동 표시
1662
+ // 같은 채팅 세션의 AI 청크가 이전 버전 마인드셋이어도 새 도구를 즉시 인지하도록.
1663
+ if (!has('--no-workflow-guide') && !has('--compact')) {
1664
+ try {
1665
+ const reportPath = path.join(root, '.harness', 'migration-report.md');
1666
+ if (exists(reportPath)) {
1667
+ const stat = fs.statSync(reportPath);
1668
+ const ageHr = (Date.now() - stat.mtimeMs) / 3600000;
1669
+ if (ageHr < 24) {
1670
+ const rep = read(reportPath);
1671
+ const aiBlock = rep.match(/## 🤖 AI must re-read[\s\S]*?(?=\n## )/);
1672
+ if (aiBlock) {
1673
+ const isTty = process.stdout && process.stdout.isTTY;
1674
+ const yel = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
1675
+ log('');
1676
+ log(yel(`## 🆕 최근 ${ageHr.toFixed(1)}시간 전 migrate 차분 — AI 에이전트는 신규 도구 우선 시도`));
1677
+ log(aiBlock[0].trim());
1678
+ log('');
1679
+ }
1680
+ }
1681
+ }
1682
+ } catch {}
1683
+ }
1428
1684
  // 1.9.39: handoff 출력 끝에 6단계 워크플로 가이드 자동 표시 (메인 에이전트가 매 세션 인지)
1429
1685
  if (!has('--no-workflow-guide') && !has('--compact') && process.env.LEERNESS_NO_WORKFLOW_GUIDE !== '1') {
1430
1686
  const isTty = process.stdout && process.stdout.isTTY;
@@ -5698,6 +5954,228 @@ function _bumpUsage(root, cmdName) {
5698
5954
  } catch {}
5699
5955
  }
5700
5956
 
5957
+ // 1.9.41: CHANGELOG.md를 파싱하여 from → to 사이 버전 차분 추출
5958
+ // 반환: [{ version, date, body, newCommands, newFlags, newFiles }]
5959
+ function _parseChangelogBetween(changelogText, fromV, toV) {
5960
+ // ## 1.9.X — YYYY-MM-DD 헤더 사이의 텍스트 추출
5961
+ const sections = [];
5962
+ const re = /^## (\d+\.\d+\.\d+)(?:\s+—\s+(\d{4}-\d{2}-\d{2}))?\s*\n([\s\S]*?)(?=^## \d+\.\d+\.\d+|$)/gm;
5963
+ let m;
5964
+ while ((m = re.exec(changelogText)) !== null) {
5965
+ sections.push({ version: m[1], date: m[2] || null, body: m[3].trim() });
5966
+ }
5967
+ // from < V <= to 만 (fromV 자체는 이미 적용된 버전이므로 제외)
5968
+ const ranged = sections.filter(s => {
5969
+ const cmp = (v1, v2) => {
5970
+ const a = v1.split('.').map(Number), b = v2.split('.').map(Number);
5971
+ for (let i = 0; i < 3; i++) { if (a[i] !== b[i]) return a[i] - b[i]; }
5972
+ return 0;
5973
+ };
5974
+ return cmp(s.version, fromV) > 0 && cmp(s.version, toV) <= 0;
5975
+ });
5976
+ // 각 섹션에서 신규 명령/플래그/파일 추출
5977
+ for (const s of ranged) {
5978
+ s.newCommands = [];
5979
+ s.newFlags = [];
5980
+ s.newFiles = [];
5981
+ // `leerness X [...]` 또는 backtick에 싸인 leerness 명령
5982
+ for (const cm of s.body.matchAll(/`leerness\s+([a-z][\w-]*(?:\s+[a-z][\w-]*)?)/g)) {
5983
+ const cmd = cm[1].trim();
5984
+ if (!s.newCommands.includes(cmd)) s.newCommands.push(cmd);
5985
+ }
5986
+ // `--xxx` 플래그
5987
+ for (const fm of s.body.matchAll(/`(--[a-z][\w-]*)`/g)) {
5988
+ if (!s.newFlags.includes(fm[1])) s.newFlags.push(fm[1]);
5989
+ }
5990
+ // .harness/X.md 같은 신규 파일
5991
+ for (const ff of s.body.matchAll(/`(\.harness\/[\w./-]+\.(?:md|json|jsonl))`/g)) {
5992
+ if (!s.newFiles.includes(ff[1])) s.newFiles.push(ff[1]);
5993
+ }
5994
+ }
5995
+ return ranged;
5996
+ }
5997
+
5998
+ // 1.9.41: leerness whats-new [--from V] — 현재 워크스페이스 버전 → leerness latest 차분
5999
+ // 1.9.43: skill export-all — 모든 자체 skill을 agentskills.io 표준 SKILL.md로 일괄 export
6000
+ function skillExportAllCmd(root) {
6001
+ root = absRoot(root || process.cwd());
6002
+ const all = listAllSkills(root);
6003
+ const ids = Object.keys(all);
6004
+ const outDir = arg('--out', path.join(root, '.harness', 'skills-export'));
6005
+ mkdirp(outDir);
6006
+ let exported = 0;
6007
+ log(`# leerness skill export-all (1.9.43)`);
6008
+ log(`총 ${ids.length}개 skill → ${rel(root, outDir)}/`);
6009
+ log('');
6010
+ for (const id of ids) {
6011
+ const data = all[id];
6012
+ const description = (data.displayNameKo || data.description || (data.capabilities && data.capabilities[0]) || id).slice(0, 200);
6013
+ 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`;
6014
+ const skillDir = path.join(outDir, id);
6015
+ mkdirp(skillDir);
6016
+ writeUtf8(path.join(skillDir, 'SKILL.md'), body);
6017
+ log(` ✓ ${id} → ${rel(root, path.join(skillDir, 'SKILL.md'))}`);
6018
+ exported++;
6019
+ }
6020
+ log('');
6021
+ log(`✅ ${exported}개 skill 일괄 export 완료`);
6022
+ log(`💡 다른 도구에서: leerness skill install <SKILL.md path>`);
6023
+ }
6024
+
6025
+ // 1.9.43: MCP server — stdio JSON-RPC로 leerness 도구 노출 (Claude Code/Hermes 등이 호출)
6026
+ // 프로토콜: MCP 표준 (JSON-RPC 2.0). 메서드: initialize, tools/list, tools/call
6027
+ function mcpServeCmd(root) {
6028
+ root = absRoot(root || process.cwd());
6029
+ // 노출할 leerness 도구 목록
6030
+ const TOOLS = [
6031
+ { name: 'leerness_handoff', description: '워크스페이스 컨텍스트(plan/progress/decisions) 적재', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
6032
+ { name: 'leerness_drift_check', description: 'AI 에이전트 leerness 미사용 drift 자동 감지 (4 신호 + 4단계 레벨)', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
6033
+ { name: 'leerness_audit', description: '워크스페이스 일관성 감사 (verify + scan + encoding + lazy 통합)', inputSchema: { type: 'object', properties: { path: { type: 'string' }, fix: { type: 'boolean' } } } },
6034
+ { 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'] } },
6035
+ { name: 'leerness_contract_verify', description: '명세 ↔ 구현 함수/필드 일치 자동 검사', inputSchema: { type: 'object', properties: { spec: { type: 'string' }, impl: { type: 'string' } }, required: ['spec', 'impl'] } },
6036
+ { name: 'leerness_agents_list', description: '외부 AI CLI 가용성 표 (claude/codex/gemini/copilot 상태 + 환경변수 활성화 여부)', inputSchema: { type: 'object', properties: {} } },
6037
+ { name: 'leerness_reuse_map', description: '워크스페이스 중복 함수/capability 자동 감지 (--all-apps + fuzzy 매칭)', inputSchema: { type: 'object', properties: { path: { type: 'string' }, allApps: { type: 'boolean' }, strictElements: { type: 'boolean' } } } },
6038
+ { name: 'leerness_whats_new', description: 'CHANGELOG 차분 자동 추출 (from → to 사이 신규 명령/플래그/파일)', inputSchema: { type: 'object', properties: { from: { type: 'string' }, to: { type: 'string' } } } },
6039
+ { name: 'leerness_usage_stats', description: 'leerness 명령별 누적 호출 통계 + drift 통계', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
6040
+ { name: 'leerness_session_close', description: '세션 마감 — handoff/current-state/task-log 자동 갱신', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } }
6041
+ ];
6042
+
6043
+ function send(obj) {
6044
+ process.stdout.write(JSON.stringify(obj) + '\n');
6045
+ }
6046
+ function callLeerness(cliArgs) {
6047
+ const r = cp.spawnSync(process.execPath, [__filename, ...cliArgs], {
6048
+ encoding: 'utf8',
6049
+ timeout: 60000,
6050
+ 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' }
6051
+ });
6052
+ return { ok: r.status === 0, exit: r.status, stdout: r.stdout || '', stderr: r.stderr || '' };
6053
+ }
6054
+ function handleRequest(req) {
6055
+ const id = req.id;
6056
+ if (req.method === 'initialize') {
6057
+ send({ jsonrpc: '2.0', id, result: {
6058
+ protocolVersion: '2024-11-05',
6059
+ capabilities: { tools: {} },
6060
+ serverInfo: { name: 'leerness', version: VERSION }
6061
+ } });
6062
+ } else if (req.method === 'tools/list') {
6063
+ send({ jsonrpc: '2.0', id, result: { tools: TOOLS } });
6064
+ } else if (req.method === 'tools/call') {
6065
+ const { name, arguments: args = {} } = req.params || {};
6066
+ const targetPath = args.path || root;
6067
+ let cliArgs;
6068
+ try {
6069
+ switch (name) {
6070
+ case 'leerness_handoff': cliArgs = ['handoff', targetPath, '--compact', '--no-drift-check']; break;
6071
+ case 'leerness_drift_check': cliArgs = ['drift', 'check', targetPath]; break;
6072
+ case 'leerness_audit': cliArgs = ['audit', targetPath, ...(args.fix ? ['--fix'] : [])]; break;
6073
+ case 'leerness_verify_claim': cliArgs = ['verify-claim', args.taskId, '--path', targetPath, ...(args.runTests ? ['--run-tests'] : []), ...(args.strictClaims ? ['--strict-claims'] : [])]; break;
6074
+ case 'leerness_contract_verify': cliArgs = ['contract', 'verify', args.spec, args.impl]; break;
6075
+ case 'leerness_agents_list': cliArgs = ['agents', 'list', '--json']; break;
6076
+ case 'leerness_reuse_map': cliArgs = ['reuse-map', targetPath, ...(args.allApps ? ['--all-apps'] : []), ...(args.strictElements ? ['--strict-elements'] : []), '--json']; break;
6077
+ case 'leerness_whats_new': cliArgs = ['whats-new', '--path', targetPath, ...(args.from ? ['--from', args.from] : []), ...(args.to ? ['--to', args.to] : []), '--json']; break;
6078
+ case 'leerness_usage_stats': cliArgs = ['usage', 'stats', targetPath, '--json']; break;
6079
+ case 'leerness_session_close': cliArgs = ['session', 'close', targetPath]; break;
6080
+ default:
6081
+ return send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown tool: ${name}` } });
6082
+ }
6083
+ const r = callLeerness(cliArgs);
6084
+ send({ jsonrpc: '2.0', id, result: {
6085
+ content: [{ type: 'text', text: (r.stdout || r.stderr || '(no output)').slice(0, 50000) }],
6086
+ isError: !r.ok
6087
+ } });
6088
+ } catch (e) {
6089
+ send({ jsonrpc: '2.0', id, error: { code: -32603, message: 'Internal error: ' + e.message } });
6090
+ }
6091
+ } else {
6092
+ send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown method: ${req.method}` } });
6093
+ }
6094
+ }
6095
+
6096
+ // stdin JSON-RPC 한 줄 단위
6097
+ let buf = '';
6098
+ process.stdin.setEncoding('utf8');
6099
+ process.stdin.on('data', chunk => {
6100
+ buf += chunk;
6101
+ let nl;
6102
+ while ((nl = buf.indexOf('\n')) !== -1) {
6103
+ const line = buf.slice(0, nl).trim();
6104
+ buf = buf.slice(nl + 1);
6105
+ if (!line) continue;
6106
+ try {
6107
+ const req = JSON.parse(line);
6108
+ handleRequest(req);
6109
+ } catch (e) {
6110
+ send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error: ' + e.message } });
6111
+ }
6112
+ }
6113
+ });
6114
+ process.stdin.on('end', () => process.exit(0));
6115
+ // 인터럽트 처리
6116
+ process.on('SIGINT', () => process.exit(0));
6117
+ process.on('SIGTERM', () => process.exit(0));
6118
+ }
6119
+
6120
+ function whatsNewCmd(root) {
6121
+ root = absRoot(root || process.cwd());
6122
+ const fromV = arg('--from', null) || (function () {
6123
+ const hv = path.join(root, '.harness', 'HARNESS_VERSION');
6124
+ if (exists(hv)) { try { return parseHarnessVersion(read(hv)).base || parseHarnessVersion(read(hv)).plus; } catch { return null; } }
6125
+ return null;
6126
+ })();
6127
+ const toV = arg('--to', null) || VERSION;
6128
+ if (!fromV) {
6129
+ fail('현재 버전을 파악할 수 없습니다. --from <version> 명시');
6130
+ return process.exit(1);
6131
+ }
6132
+ // CHANGELOG.md — 우선 root, 없으면 leerness-pkg 자체
6133
+ let changelogPath = path.join(root, 'CHANGELOG.md');
6134
+ if (!exists(changelogPath)) changelogPath = path.join(__dirname, '..', 'CHANGELOG.md');
6135
+ if (!exists(changelogPath)) {
6136
+ fail('CHANGELOG.md 없음');
6137
+ return process.exit(1);
6138
+ }
6139
+ const diff = _parseChangelogBetween(read(changelogPath), fromV, toV);
6140
+ if (has('--json')) { log(JSON.stringify({ from: fromV, to: toV, versions: diff }, null, 2)); return; }
6141
+ if (!diff.length) {
6142
+ log(`# leerness whats-new (1.9.41)`);
6143
+ log(`현재 ${fromV} ↔ 대상 ${toV}: 새 항목 없음 (또는 CHANGELOG에 기록 안 됨)`);
6144
+ return;
6145
+ }
6146
+ log(`# leerness whats-new (1.9.41)`);
6147
+ log(`현재 워크스페이스 버전: ${fromV} → 대상: ${toV}`);
6148
+ log(`범위: ${diff.length}개 버전 (${diff[0].version} → ${diff[diff.length - 1].version})`);
6149
+ log('');
6150
+ // AI 가독 요약 — 각 버전당 한 줄 + 신규 명령/플래그/파일
6151
+ log(`## 🆕 신규 명령·플래그·파일 (AI 에이전트는 다음 명령을 우선 시도)`);
6152
+ const allCommands = new Set();
6153
+ const allFlags = new Set();
6154
+ const allFiles = new Set();
6155
+ for (const v of diff) {
6156
+ v.newCommands.forEach(c => allCommands.add(c));
6157
+ v.newFlags.forEach(f => allFlags.add(f));
6158
+ v.newFiles.forEach(f => allFiles.add(f));
6159
+ }
6160
+ if (allCommands.size) log(` 📌 신규 명령: ${[...allCommands].join(', ')}`);
6161
+ if (allFlags.size) log(` 🚩 신규 플래그: ${[...allFlags].join(', ')}`);
6162
+ if (allFiles.size) log(` 📄 신규 파일: ${[...allFiles].join(', ')}`);
6163
+ log('');
6164
+ log(`## 📜 버전별 헤드라인`);
6165
+ for (const v of diff) {
6166
+ // body 첫 줄(또는 strong header) 추출
6167
+ const firstLine = (v.body.match(/^\*\*([^*]+)\*\*/) || [])[1]
6168
+ || (v.body.split('\n').find(l => l.trim() && !l.startsWith('##')) || '').trim().slice(0, 120);
6169
+ log(` • ${v.version}${v.date ? ` (${v.date})` : ''} — ${firstLine || '(no headline)'}`);
6170
+ }
6171
+ log('');
6172
+ log(`## 💡 권장 행동`);
6173
+ log(` 1. 위 신규 명령들을 시도해 보세요 (예: \`leerness <명령> --help\`)`);
6174
+ log(` 2. 신규 파일들을 읽어 보세요 (예: \`cat .harness/session-workflow.md\`)`);
6175
+ log(` 3. AGENTS.md/CLAUDE.md 재독 — migrate가 인스트럭션을 업데이트했을 수 있음`);
6176
+ log(` 4. 상세: \`cat CHANGELOG.md\` 또는 \`leerness whats-new --json\``);
6177
+ }
6178
+
5701
6179
  function usageStatsCmd(root) {
5702
6180
  root = absRoot(root || process.cwd());
5703
6181
  const stats = _readUsageStats(root);
@@ -5963,6 +6441,7 @@ async function main() {
5963
6441
  if (cmd === 'contract' && args[1] === 'verify') return contractVerifyCmd(args[2], args[3]);
5964
6442
  if (cmd === 'drift' && (args[1] === 'check' || !args[1])) return driftCheckCmd(args[2] || arg('--path', process.cwd()));
5965
6443
  if (cmd === 'usage' && (args[1] === 'stats' || !args[1])) return usageStatsCmd(args[2] || arg('--path', process.cwd()));
6444
+ if (cmd === 'whats-new') return whatsNewCmd(args[1] || arg('--path', process.cwd()));
5966
6445
  if (cmd === 'reuse' && args[1] === 'autodetect') return reuseAutodetectCmd(args[2] || arg('--path', process.cwd()));
5967
6446
  if (cmd === 'setup-agents' || cmd === 'setup' && args[1] === 'agents') return await setupAgentsCmd(args[1] && args[1] !== 'agents' ? args[1] : (args[2] || process.cwd()));
5968
6447
  if (cmd === 'session' && args[1] === 'close') { const r = sessionClose(args[2] || process.cwd()); viewworkEmit(args[2] || process.cwd(), { action: 'task', tool: 'session-close', note: 'session close' }); return r; }
@@ -5982,6 +6461,11 @@ async function main() {
5982
6461
  if (cmd === 'skill' && args[1] === 'optimize') return skillOptimize(absRoot(arg('--path', process.cwd())), args[2]);
5983
6462
  if (cmd === 'skill' && args[1] === 'remove') return skillRemove(absRoot(arg('--path', process.cwd())), args[2]);
5984
6463
  if (cmd === 'skill' && args[1] === 'consolidate') return skillConsolidate(absRoot(arg('--path', process.cwd())));
6464
+ if (cmd === 'skill' && args[1] === 'install') return await skillInstallCmd(absRoot(arg('--path', process.cwd())), args[2]);
6465
+ if (cmd === 'skill' && args[1] === 'discover') return await skillDiscoverCmd(absRoot(arg('--path', process.cwd())));
6466
+ if (cmd === 'skill' && args[1] === 'export') return skillExportCmd(absRoot(arg('--path', process.cwd())), args[2]);
6467
+ if (cmd === 'skill' && args[1] === 'export-all') return skillExportAllCmd(absRoot(arg('--path', process.cwd())));
6468
+ if (cmd === 'mcp' && args[1] === 'serve') return mcpServeCmd(absRoot(arg('--path', process.cwd())));
5985
6469
  if (cmd === 'gate') return gate(args[1] || process.cwd());
5986
6470
  if (cmd === 'verify-code') return verifyCodeCmd(args[1] || process.cwd());
5987
6471
  if (cmd === 'lessons') return lessonsCmd(arg('--path', process.cwd()));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.40",
3
+ "version": "1.9.43",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -950,6 +950,202 @@ total++;
950
950
  if (!ok) { failed++; console.log(r.stdout.slice(0, 800)); }
951
951
  }
952
952
 
953
+ // 1.9.43 회귀: MCP server + skill export-all + _reports 비공개
954
+ total++;
955
+ {
956
+ // skill export-all: 모든 내장 skill을 SKILL.md로 일괄 export
957
+ const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-exall-'));
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', 'export-all', '--path', tmpC], { encoding: 'utf8', timeout: 30000 });
960
+ const exportDir = path.join(tmpC, '.harness', 'skills-export');
961
+ const exists2 = fs.existsSync(exportDir);
962
+ const count = exists2 ? fs.readdirSync(exportDir).length : 0;
963
+ const ok = r.status === 0 && exists2 && count >= 5;
964
+ console.log(ok ? `✓ B(1.9.43) skill export-all: ${count}개 skill 일괄 SKILL.md 생성` : `✗ export-all 실패 (count=${count})`);
965
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
966
+ }
967
+
968
+ total++;
969
+ {
970
+ // MCP server initialize: stdio JSON-RPC 정상 응답
971
+ const r = cp.spawnSync(process.execPath, [CLI, 'mcp', 'serve'], {
972
+ encoding: 'utf8', timeout: 10000,
973
+ input: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize' }) + '\n'
974
+ });
975
+ let parsed = null;
976
+ try { parsed = JSON.parse(r.stdout.split('\n').filter(Boolean)[0]); } catch {}
977
+ const ok = parsed
978
+ && parsed.jsonrpc === '2.0'
979
+ && parsed.id === 1
980
+ && parsed.result
981
+ && parsed.result.serverInfo
982
+ && parsed.result.serverInfo.name === 'leerness';
983
+ console.log(ok ? '✓ B(1.9.43) MCP server initialize: JSON-RPC 표준 응답 + serverInfo' : `✗ MCP initialize 실패`);
984
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
985
+ }
986
+
987
+ total++;
988
+ {
989
+ // MCP server tools/list: 10개 도구 노출
990
+ const r = cp.spawnSync(process.execPath, [CLI, 'mcp', 'serve'], {
991
+ encoding: 'utf8', timeout: 10000,
992
+ input: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list' }) + '\n'
993
+ });
994
+ let parsed = null;
995
+ try { parsed = JSON.parse(r.stdout.split('\n').filter(Boolean)[0]); } catch {}
996
+ const ok = parsed
997
+ && Array.isArray(parsed.result && parsed.result.tools)
998
+ && parsed.result.tools.length >= 8
999
+ && parsed.result.tools.some(t => t.name === 'leerness_verify_claim')
1000
+ && parsed.result.tools.some(t => t.name === 'leerness_drift_check');
1001
+ console.log(ok ? `✓ B(1.9.43) MCP tools/list: ${parsed.result.tools.length}개 leerness 도구 노출` : `✗ MCP tools/list 실패`);
1002
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
1003
+ }
1004
+
1005
+ total++;
1006
+ {
1007
+ // .gitignore에 _reports/ 포함 — leerness-pkg
1008
+ const giPath = path.join(__dirname, '..', '.gitignore');
1009
+ const body = fs.existsSync(giPath) ? fs.readFileSync(giPath, 'utf8') : '';
1010
+ const ok = /_reports\//.test(body) && /\*\.private\.md/.test(body);
1011
+ console.log(ok ? '✓ B(1.9.43) leerness-pkg/.gitignore: _reports/ + *.private.md 차단' : `✗ .gitignore 실패`);
1012
+ if (!ok) { failed++; console.log(body.slice(0, 300)); }
1013
+ }
1014
+
1015
+ total++;
1016
+ {
1017
+ // .npmignore에 _reports/ + 보고서 차단 명시
1018
+ const niPath = path.join(__dirname, '..', '.npmignore');
1019
+ const body = fs.existsSync(niPath) ? fs.readFileSync(niPath, 'utf8') : '';
1020
+ const ok = /_reports\//.test(body) && /\*\.private/.test(body);
1021
+ console.log(ok ? '✓ B(1.9.43) leerness-pkg/.npmignore: _reports/ 차단' : `✗ .npmignore 실패`);
1022
+ if (!ok) { failed++; console.log(body.slice(0, 300)); }
1023
+ }
1024
+
1025
+ // 1.9.42 회귀: agentskills.io 표준 호환 (skill install/discover/export + .env opt-in)
1026
+ total++;
1027
+ {
1028
+ // skill discover: env 없으면 opt-in 안내로 거부
1029
+ const env = { ...process.env };
1030
+ delete env.LEERNESS_SKILL_DISCOVER_URL;
1031
+ const r = cp.spawnSync(process.execPath, [CLI, 'skill', 'discover'], { encoding: 'utf8', timeout: 10000, env });
1032
+ const ok = r.status !== 0 && /LEERNESS_SKILL_DISCOVER_URL.*필요|opt-in/.test(r.stdout + r.stderr);
1033
+ console.log(ok ? '✓ B(1.9.42) skill discover: env 없으면 opt-in 안내 거부' : `✗ discover opt-in 실패`);
1034
+ if (!ok) { failed++; console.log((r.stdout + r.stderr).slice(0, 400)); }
1035
+ }
1036
+
1037
+ total++;
1038
+ {
1039
+ // skill export → SKILL.md frontmatter 정확 생성
1040
+ const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-skex-'));
1041
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
1042
+ const r = cp.spawnSync(process.execPath, [CLI, 'skill', 'export', 'office', '--path', tmpC], { encoding: 'utf8', timeout: 15000 });
1043
+ const skillFile = path.join(tmpC, '.harness', 'skills-export', 'office', 'SKILL.md');
1044
+ const exists2 = fs.existsSync(skillFile);
1045
+ const body = exists2 ? fs.readFileSync(skillFile, 'utf8') : '';
1046
+ const ok = r.status === 0
1047
+ && exists2
1048
+ && /^---\nname: office\ndescription:/.test(body)
1049
+ && /\n---\n/.test(body);
1050
+ console.log(ok ? '✓ B(1.9.42) skill export: agentskills.io 표준 SKILL.md frontmatter 생성' : `✗ export 실패`);
1051
+ if (!ok) { failed++; console.log(body.slice(0, 300) || r.stdout.slice(0, 300)); }
1052
+ }
1053
+
1054
+ total++;
1055
+ {
1056
+ // skill install: 로컬 SKILL.md import → .harness/skills/<id>/SKILL.md 자동 배치
1057
+ const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-skin-'));
1058
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
1059
+ // 로컬 SKILL.md 직접 작성
1060
+ const skillSrc = path.join(tmpC, 'test-skill.md');
1061
+ fs.writeFileSync(skillSrc, '---\nname: my-test-skill\ndescription: agentskills.io 표준 호환 e2e 검증\n---\n\n# Test Skill\n\n본문 내용.\n', 'utf8');
1062
+ const r = cp.spawnSync(process.execPath, [CLI, 'skill', 'install', skillSrc, '--path', tmpC], { encoding: 'utf8', timeout: 15000 });
1063
+ const installedFile = path.join(tmpC, '.harness', 'skills', 'my-test-skill', 'SKILL.md');
1064
+ const ok = r.status === 0
1065
+ && fs.existsSync(installedFile)
1066
+ && /my-test-skill/.test(fs.readFileSync(installedFile, 'utf8'));
1067
+ console.log(ok ? '✓ B(1.9.42) skill install: 로컬 SKILL.md → .harness/skills/<id>/ 배치' : `✗ install 실패`);
1068
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
1069
+ }
1070
+
1071
+ total++;
1072
+ {
1073
+ // skill install이 skill.json도 자동 작성 (자체 catalog 호환)
1074
+ const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-skin2-'));
1075
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
1076
+ const skillSrc = path.join(tmpC, 'test-skill2.md');
1077
+ fs.writeFileSync(skillSrc, '---\nname: dual-format\ndescription: skill.json + SKILL.md 양쪽 자동 생성\n---\n\n# Dual\n', 'utf8');
1078
+ cp.spawnSync(process.execPath, [CLI, 'skill', 'install', skillSrc, '--path', tmpC], { stdio: 'ignore', timeout: 15000 });
1079
+ const jsonFile = path.join(tmpC, '.harness', 'skills', 'dual-format', 'skill.json');
1080
+ const json = fs.existsSync(jsonFile) ? JSON.parse(fs.readFileSync(jsonFile, 'utf8')) : null;
1081
+ const ok = json
1082
+ && json.name === 'dual-format'
1083
+ && json._source === 'agentskills.io'
1084
+ && json.verification && json.verification.method === 'agentskills.io-import';
1085
+ console.log(ok ? '✓ B(1.9.42) skill install: skill.json 자동 생성 + _source 추적' : `✗ skill.json 실패`);
1086
+ if (!ok) { failed++; console.log(JSON.stringify(json || {})); }
1087
+ }
1088
+
1089
+ // 1.9.41 회귀: whats-new 명령 + migrate 차분 AI must re-read + handoff fresh 알림
1090
+ total++;
1091
+ {
1092
+ // whats-new --from 큰 점프 → 신규 명령 추출
1093
+ const r = cp.spawnSync(process.execPath, [CLI, 'whats-new', '--from', '1.9.33', '--json'], { encoding: 'utf8', timeout: 15000 });
1094
+ let parsed = null;
1095
+ try { parsed = JSON.parse(r.stdout); } catch {}
1096
+ const ok = parsed
1097
+ && parsed.from === '1.9.33'
1098
+ && Array.isArray(parsed.versions)
1099
+ && parsed.versions.length >= 5
1100
+ && parsed.versions.some(v => v.newCommands && v.newCommands.length > 0);
1101
+ console.log(ok ? '✓ B(1.9.41) whats-new --from 1.9.33: 5+ 버전 차분 + 신규 명령 추출' : `✗ whats-new 실패`);
1102
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
1103
+ }
1104
+
1105
+ total++;
1106
+ {
1107
+ // migrate가 fromV가 있는 경우 AI must re-read 블록을 stdout에 출력
1108
+ const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-mig-'));
1109
+ // 1.9.30 표시로 init한 척 (HARNESS_VERSION 직접 작성)
1110
+ fs.mkdirSync(path.join(tmpC, '.harness'), { recursive: true });
1111
+ fs.writeFileSync(path.join(tmpC, '.harness', 'HARNESS_VERSION'), 'leerness@1.9.36\n', 'utf8');
1112
+ const r = cp.spawnSync(process.execPath, [CLI, 'migrate', tmpC, '--yes', '--no-banner', '--no-stale-check'], { encoding: 'utf8', timeout: 60000 });
1113
+ const ok = r.status === 0
1114
+ && /AI must re-read/.test(r.stdout)
1115
+ && /1\.9\.36 → 1\.9\.\d+/.test(r.stdout)
1116
+ && /신규 명령/.test(r.stdout);
1117
+ console.log(ok ? '✓ B(1.9.41) migrate stdout: AI must re-read 차분 자동 출력' : `✗ migrate 차분 출력 실패`);
1118
+ if (!ok) { failed++; console.log(r.stdout.slice(-800)); }
1119
+ }
1120
+
1121
+ total++;
1122
+ {
1123
+ // migration-report.md에 AI must re-read 섹션 영구 기록
1124
+ const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-mig2-'));
1125
+ fs.mkdirSync(path.join(tmpC, '.harness'), { recursive: true });
1126
+ fs.writeFileSync(path.join(tmpC, '.harness', 'HARNESS_VERSION'), 'leerness@1.9.30\n', 'utf8');
1127
+ cp.spawnSync(process.execPath, [CLI, 'migrate', tmpC, '--yes', '--no-banner', '--no-stale-check'], { stdio: 'ignore', timeout: 60000 });
1128
+ const reportPath = path.join(tmpC, '.harness', 'migration-report.md');
1129
+ const body = fs.existsSync(reportPath) ? fs.readFileSync(reportPath, 'utf8') : '';
1130
+ const ok = /## 🤖 AI must re-read/.test(body) && /Previous: 1\.9\.30/.test(body);
1131
+ console.log(ok ? '✓ B(1.9.41) migration-report.md: AI must re-read 섹션 + Previous 버전 기록' : `✗ report 기록 실패`);
1132
+ if (!ok) { failed++; console.log(body.slice(0, 600)); }
1133
+ }
1134
+
1135
+ total++;
1136
+ {
1137
+ // handoff가 fresh migration-report (24h 내) 시 자동 알림
1138
+ const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-fresh-'));
1139
+ fs.mkdirSync(path.join(tmpC, '.harness'), { recursive: true });
1140
+ fs.writeFileSync(path.join(tmpC, '.harness', 'HARNESS_VERSION'), 'leerness@1.9.30\n', 'utf8');
1141
+ cp.spawnSync(process.execPath, [CLI, 'migrate', tmpC, '--yes', '--no-banner', '--no-stale-check'], { stdio: 'ignore', timeout: 60000 });
1142
+ const r = cp.spawnSync(process.execPath, [CLI, 'handoff', tmpC, '--no-drift-check'], { encoding: 'utf8', timeout: 15000 });
1143
+ const ok = r.status === 0
1144
+ && /최근.*시간 전 migrate 차분|AI must re-read/.test(r.stdout);
1145
+ console.log(ok ? '✓ B(1.9.41) handoff: 최근 migrate 차분 자동 표시 (24h 내)' : `✗ handoff 차분 알림 실패`);
1146
+ if (!ok) { failed++; console.log(r.stdout.slice(-500)); }
1147
+ }
1148
+
953
1149
  // 1.9.40 회귀: release pack 통합 명령 + audit README mismatch 감지
954
1150
  total++;
955
1151
  {