leerness 1.9.243 → 1.9.245

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,99 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.245 — 2026-05-26 — UR-0015 API skill cache
4
+
5
+ **📚 사용자 명시 (UR-0015): API 문서/관련링크 자동 정리 + AI 자동 참조 시스템.**
6
+
7
+ ### 사용자 명시 (UR-0015)
8
+ > *"API 문서/기능 요청 시 공식 문서·AI 탐색 내용을 스킬처럼 .harness/api-skills/ 에 정리하고 방향 지시도 함께 기록. 이후 같은 API 관련 수정/구현 요청 시 AI 가 정리해둔 파일이 자동 참조. URL 제공 시 본 URL + 관련 링크의 기능까지 참조."*
9
+
10
+ 검증 예시: 쿠팡 상품 생성 API (https://developers.coupangcorp.com/hc/ko/articles/360033877853) — 실 fetch 성공 (title "상품 생성 - Open APIs" + 7 관련 링크 자동 수집).
11
+
12
+ ### 1. `leerness api-skill` CLI 5종
13
+ - `add <url> [--direction "방향"] [--name "..."] [--no-crawl] [--skeleton]` — fetch + same-domain 관련 링크 1단계 crawl (max 10) → `.harness/api-skills/<id>.md`
14
+ - `list [--json]` — 저장된 skill 목록
15
+ - `show <id>` — 특정 skill 본문 출력 (AI 컨텍스트 적재용)
16
+ - `match <query> [--json]` — task 키워드 매칭 (CJK 한글 2자+ / ASCII 3자+ 모두 지원)
17
+ - `drop <id>` — 삭제
18
+
19
+ ### 2. URL Fetch 방식 — 의존성 0 (Node built-in https)
20
+ - Mozilla 호환 User-Agent (Cloudflare/WAF 차단 회피)
21
+ - timeout 10s · max body 1MB · max 5 redirects
22
+ - HTML→text 변환 (script/style 제거, entity decode)
23
+ - same-domain 관련 링크 추출 (max 10, depth=1)
24
+ - 차단 시 (403/401/429) `--skeleton` fallback → 빈 .md 골격 생성
25
+
26
+ ### 3. 자동 참조 — handoff body 자동 노출
27
+ - 현재 in-progress task description 키워드 + skill 매칭 → `## 📚 관련 API skill N건 발견 (참조 권장)`
28
+ - 본 URL + 방향 + 도메인 표시 (top 3)
29
+ - 매칭 0 시: 저장된 skill 수만 hint 표시
30
+
31
+ ### 4. JSON 11번째 통합 필드 `apiSkills` (handoff/session close/health)
32
+ - `{ total, matched, matchedIds, ids }` — 외부 AI 가 단일 호출에서 API skill 컨텍스트 회수
33
+ - JSON 통합 매트릭스 10 → **11 필드** (3 명령 × 11 = 33 통합 포인트)
34
+
35
+ ### 5. MCP **70번째 도구** `leerness_api_skill` 🎉
36
+ - sub: list / show / match / add / drop (외부 AI 가 MCP 호출로 직접 사용)
37
+
38
+ ### 6. 누적 회귀 (1.9.207~244) — 모두 유지
39
+ - REPL HOTFIX (1.9.244) + CJK 분류 (1.9.243) + env encoding --apply (1.9.242) + 모두 유지
40
+
41
+ ### 7. stress-v190 — **26/26 PASS · 100%**
42
+ - 1.9.245 신규 (15): VERSION + helper 함수 + CLI 5종 + 실 fetch + JSON 11 필드 × 3명령 + body 매칭 + skeleton fallback + MCP 70
43
+ - 성능 (1): cold start avg 331ms
44
+ - 누적 회귀 (10): 1.9.207~244
45
+
46
+ ### 8. 자동 release (107 라운드 main-push streak · 68 라운드 npm publish streak)
47
+
48
+ 📚 **API 지식 자동 누적·재사용** — 사용자가 한 번 정리하면 AI가 영구 기억. R201 진입.
49
+
50
+ ---
51
+
52
+ ## 1.9.244 — 2026-05-26 🎉 R200 + 🚨 HOTFIX
53
+
54
+ **🚨 HOTFIX: REPL agent ReferenceError `_lastCycleLines` (1.9.189 회귀 버그) + 🎉 R200 마일스톤 도달.**
55
+
56
+ ### 🚨 사용자 보고 버그 (실 환경 1.9.243 npx 설치)
57
+
58
+ ```
59
+ agent[claude · sonnet-4-7/actor/▶]> 웹페이지 제작해줘
60
+ .../leerness/bin/harness.js:16839
61
+ _lastCycleLines = 0; // 1.9.189: 사용자 입력 시 cycle overwrite 추적 reset
62
+ ^
63
+ ReferenceError: _lastCycleLines is not defined
64
+ at Interface.<anonymous> (.../harness.js:16839:23)
65
+ ```
66
+
67
+ **원인:** `let _lastCycleLines = 0;` 변수가 `if (isTty) { try { ... } }` 블록 스코프 내부에 선언되어 있었음 (line 16418). `rl.on('line')` 핸들러는 외부 스코프 (line 16836+) 라서 ReferenceError.
68
+
69
+ **Fix (1.9.244):**
70
+ - `let _lastCycleLines = 0;` 를 outer function 스코프 (line 16399) 로 hoist
71
+ - 두 곳 (cycleProvider/cycleModel + rl.on('line') 핸들러) 동일 closure 공유
72
+ - 검증: stress-v189 A5 — REPL agent 진입 + stdin input → no ReferenceError 확인
73
+
74
+ ### 🎉 R200 마일스톤 도달
75
+
76
+ - v1.9.6 baseline ~ v1.9.244 → **200 누적 라운드** (round-history JSON: `roundCount=200`)
77
+ - **106 main-push streak** (1.9.140~) · **🎉 67 npm publish streak** (1.9.178~)
78
+ - handoff/session close/health JSON **10 필드** × 3 = 30 통합 포인트
79
+ - MCP **69 도구** · CLI **56 명령** · 9 카테고리
80
+ - 6 능력 매트릭스 + 5축 매트릭스 100/100
81
+
82
+ ### 누적 회귀 (1.9.207~243) — 모두 유지
83
+ - CJK 분류 (1.9.243) · env encoding --apply (1.9.242) · env summary (1.9.241) · py-check (1.9.239)
84
+ - 비정상종료 + delivered + release cleanup + round-history + milestones + recentChanges + 모든 1.9.207~218 사용자 명시 신규 7종
85
+
86
+ ### stress-v189 — **20/20 PASS · 100%**
87
+ - 1.9.244 HOTFIX (6): VERSION + _lastCycleLines 단일 선언 + 15 refs + hotfix 주석 + agent input no-crash + agent --help no-crash
88
+ - R200 마일스톤 (1): roundCount=200 확인
89
+ - 성능 (1): cold start avg 373ms
90
+ - 누적 회귀 (12): 1.9.170~243
91
+
92
+ 🚨 **사용자 보고 버그 즉시 패치** — 실 환경 1.9.243 사용 중 발견된 critical REPL 크래시 회복.
93
+ 🎉 **R200 자율 모드 마일스톤** — 200 라운드 동안 단 한 번도 자율 흐름 끊김 없이 진행.
94
+
95
+ ---
96
+
3
97
  ## 1.9.243 — 2026-05-26
4
98
 
5
99
  **🌏 UR-0014 3단계: CJK 다국어 분류 (한국어/일본어/중국어) + session close --auto-fix-encoding + handoff body --apply 진입점.**
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.243-green)]() [![tests](https://img.shields.io/badge/e2e-217%2F217-success)]() [![stress](https://img.shields.io/badge/stress--v188-24%2F24-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-69-brightgreen)]() [![cli](https://img.shields.io/badge/CLI_commands-56-brightgreen)]() [![json-fields](https://img.shields.io/badge/JSON_통합-10_필드-blueviolet)]() [![rounds](https://img.shields.io/badge/autonomous--rounds-199-blueviolet)]() [![main-push](https://img.shields.io/badge/🎉_main--push_streak-105_rounds-success)]() [![npm-streak](https://img.shields.io/badge/🎉_npm--publish-66_streak-success)]() [![cjk](https://img.shields.io/badge/🌏_CJK_분류-Ko_Ja_Zh-blueviolet)]() [![perf](https://img.shields.io/badge/cold_start-333ms-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.245-green)]() [![tests](https://img.shields.io/badge/e2e-217%2F217-success)]() [![stress](https://img.shields.io/badge/stress--v190-26%2F26-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-70-brightgreen)]() [![cli](https://img.shields.io/badge/CLI_commands-57-brightgreen)]() [![json-fields](https://img.shields.io/badge/JSON_통합-11_필드-blueviolet)]() [![rounds](https://img.shields.io/badge/autonomous_R-201-blueviolet)]() [![main-push](https://img.shields.io/badge/🎉_main--push_streak-107_rounds-success)]() [![npm-streak](https://img.shields.io/badge/🎉_npm--publish-68_streak-success)]() [![api-skill](https://img.shields.io/badge/📚_API_skill_cache-UR--0015-blueviolet)]() [![perf](https://img.shields.io/badge/cold_start-331ms-success)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
6
6
 
7
7
  ```
8
8
  ╔══════════════════════════════════════════════════════════════╗
@@ -12,9 +12,9 @@
12
12
  ║ ██║ ██╔══╝ ██╔══╝ ██╔══██╗██║╚██╗██║██╔══╝ ╚════██║ ║
13
13
  ║ ███████╗███████╗███████╗██║ ██║██║ ╚████║███████╗███████║ ║
14
14
  ║ ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝ ║
15
- ║ v1.9.243 AI Agent Reliability Harness + Sandbox ║
15
+ ║ v1.9.245 AI Agent Reliability Harness + Sandbox ║
16
16
  ║ verify · remember · orchestrate · audit · sandbox · drift ║
17
- 🌏 CJK 분류 + session close --auto-fix-encoding (UR-0014 3) ║
17
+ 📚 API skill cache (UR-0015) — 공식 문서 자동 정리 + AI 참조
18
18
  ╚══════════════════════════════════════════════════════════════╝
19
19
  ```
20
20
 
package/bin/harness.js CHANGED
@@ -7,7 +7,7 @@ const cp = require('child_process');
7
7
  const os = require('os'); // 1.9.178: _publishToNpm 에서 os.tmpdir() 사용 (전역 import)
8
8
  const readline = require('readline');
9
9
 
10
- const VERSION = '1.9.243';
10
+ const VERSION = '1.9.245';
11
11
 
12
12
  // 1.9.184: DEP0190 (child_process shell: true) deprecation warning 억제 (사용자 명시).
13
13
  // leerness 는 cross-platform PATH resolution 을 위해 shell: true 를 의도적으로 사용 (claude.cmd / ollama.cmd 등 Windows .cmd 처리).
@@ -2588,6 +2588,338 @@ function envCmd(root, sub) {
2588
2588
  log(dm(` → JSON 출력: leerness env --json`));
2589
2589
  }
2590
2590
 
2591
+ // 1.9.245: API skill cache — 공식 문서 + 관련 링크 자동 정리 (사용자 명시 UR-0015)
2592
+ // .harness/api-skills/<id>.md 에 frontmatter + 본문 저장. handoff 자동 매칭.
2593
+ // Node built-in https (의존성 0), depth=1 same-domain crawl, max 10 links.
2594
+ function _apiSkillsDir(root) {
2595
+ return path.join(absRoot(root), '.harness', 'api-skills');
2596
+ }
2597
+ function _apiSkillId(url, name) {
2598
+ // 도메인 + path slug 로 id 생성. 예: developers.coupangcorp.com/articles/360033877853 → coupang-articles-360033877853
2599
+ try {
2600
+ const u = new URL(url);
2601
+ const host = u.hostname.replace(/^www\./, '').split('.').slice(0, -1).join('-') || u.hostname;
2602
+ const pathSlug = u.pathname.replace(/^\/+|\/+$/g, '').replace(/[^a-z0-9가-힣\-_]+/gi, '-').slice(-50);
2603
+ const base = `${host}-${pathSlug}`.replace(/-+/g, '-').replace(/^-|-$/g, '');
2604
+ return (name ? base + '-' + name.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 30) : base).replace(/-+/g, '-');
2605
+ } catch { return 'api-' + Date.now(); }
2606
+ }
2607
+ function _fetchUrl(url, opts = {}) {
2608
+ // Node https.get with timeout, follow redirects (max 5), max body 1MB
2609
+ const https = require('https');
2610
+ const http = require('http');
2611
+ const maxRedirects = opts.maxRedirects != null ? opts.maxRedirects : 5;
2612
+ const timeout = opts.timeout || 10000;
2613
+ return new Promise((resolve, reject) => {
2614
+ function go(u, redirects) {
2615
+ let parsed;
2616
+ try { parsed = new URL(u); } catch { return reject(new Error('invalid URL: ' + u)); }
2617
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') return reject(new Error('unsupported protocol: ' + parsed.protocol));
2618
+ const lib = parsed.protocol === 'https:' ? https : http;
2619
+ const req = lib.get({
2620
+ hostname: parsed.hostname, port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
2621
+ path: parsed.pathname + parsed.search, timeout,
2622
+ // 1.9.245: 일부 사이트 (Cloudflare/AWS WAF) 가 비-브라우저 UA 차단 → Mozilla 호환 UA 로 호환성 ↑
2623
+ headers: {
2624
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 leerness/1.9.245',
2625
+ 'Accept': 'text/html,application/xhtml+xml,application/json;q=0.9,*/*;q=0.8',
2626
+ 'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
2627
+ 'Accept-Encoding': 'identity'
2628
+ }
2629
+ }, res => {
2630
+ // 3xx → follow
2631
+ if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location && redirects < maxRedirects) {
2632
+ const next = new URL(res.headers.location, u).toString();
2633
+ res.resume(); return go(next, redirects + 1);
2634
+ }
2635
+ let body = ''; let size = 0; const MAX = 1024 * 1024;
2636
+ res.on('data', chunk => { size += chunk.length; if (size > MAX) { res.destroy(); return reject(new Error('body > 1MB')); } body += chunk; });
2637
+ res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body, finalUrl: u }));
2638
+ });
2639
+ req.on('error', reject);
2640
+ req.on('timeout', () => { req.destroy(); reject(new Error('timeout ' + timeout + 'ms')); });
2641
+ }
2642
+ go(url, 0);
2643
+ });
2644
+ }
2645
+ function _htmlToText(html) {
2646
+ if (!html) return '';
2647
+ return html
2648
+ .replace(/<script[\s\S]*?<\/script>/gi, '')
2649
+ .replace(/<style[\s\S]*?<\/style>/gi, '')
2650
+ .replace(/<!--[\s\S]*?-->/g, '')
2651
+ .replace(/<br\s*\/?>/gi, '\n')
2652
+ .replace(/<\/?(p|div|li|h[1-6]|tr|td|pre)[^>]*>/gi, '\n')
2653
+ .replace(/<[^>]+>/g, ' ')
2654
+ .replace(/&nbsp;/g, ' ').replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#39;/g, "'")
2655
+ .replace(/&#(\d+);/g, (_, n) => String.fromCharCode(parseInt(n, 10)))
2656
+ .replace(/[ \t]+/g, ' ').replace(/\n\s*\n\s*\n+/g, '\n\n').trim();
2657
+ }
2658
+ function _extractTitle(html) {
2659
+ const m = (html || '').match(/<title[^>]*>([\s\S]*?)<\/title>/i);
2660
+ if (!m) return '';
2661
+ return _htmlToText(m[1]).slice(0, 200);
2662
+ }
2663
+ function _extractLinks(html, baseUrl, maxLinks) {
2664
+ if (!html) return [];
2665
+ const base = new URL(baseUrl);
2666
+ const found = new Map();
2667
+ const re = /<a\s+[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi;
2668
+ let m;
2669
+ while ((m = re.exec(html)) !== null) {
2670
+ let href = m[1];
2671
+ if (!href || href.startsWith('#') || href.startsWith('javascript:') || href.startsWith('mailto:')) continue;
2672
+ let abs;
2673
+ try { abs = new URL(href, baseUrl).toString(); } catch { continue; }
2674
+ const u = new URL(abs);
2675
+ if (u.hostname !== base.hostname) continue; // same-domain only
2676
+ if (abs === baseUrl) continue;
2677
+ if (found.has(abs)) continue;
2678
+ const text = _htmlToText(m[2]).slice(0, 120);
2679
+ found.set(abs, { url: abs, text });
2680
+ if (found.size >= (maxLinks || 10)) break;
2681
+ }
2682
+ return Array.from(found.values());
2683
+ }
2684
+ async function _collectAPIDoc(url, opts = {}) {
2685
+ const direction = opts.direction || '';
2686
+ const noCrawl = opts.noCrawl;
2687
+ const r0 = await _fetchUrl(url);
2688
+ if (r0.status >= 400) throw new Error('fetch ' + url + ' → status ' + r0.status);
2689
+ const html = r0.body;
2690
+ const title = _extractTitle(html);
2691
+ const text = _htmlToText(html);
2692
+ const result = { url, title, text: text.slice(0, 8000), related_links: [] };
2693
+ if (!noCrawl) {
2694
+ const links = _extractLinks(html, r0.finalUrl || url, 10);
2695
+ for (const lk of links) {
2696
+ try {
2697
+ const rr = await _fetchUrl(lk.url, { timeout: 8000 });
2698
+ if (rr.status < 400) {
2699
+ const t = _extractTitle(rr.body) || lk.text;
2700
+ const excerpt = _htmlToText(rr.body).slice(0, 400);
2701
+ result.related_links.push({ url: lk.url, title: t.slice(0, 200), excerpt });
2702
+ }
2703
+ } catch {}
2704
+ }
2705
+ }
2706
+ result.direction = direction;
2707
+ result.captured_at = new Date().toISOString();
2708
+ return result;
2709
+ }
2710
+ function _serializeAPISkill(id, name, urls, direction, doc) {
2711
+ const lines = [];
2712
+ lines.push('---');
2713
+ lines.push(`id: ${id}`);
2714
+ lines.push(`name: ${name}`);
2715
+ lines.push(`urls:`);
2716
+ urls.forEach(u => lines.push(` - ${u}`));
2717
+ lines.push(`direction: ${JSON.stringify(direction || '')}`);
2718
+ lines.push(`captured_at: ${doc.captured_at}`);
2719
+ if (doc.related_links && doc.related_links.length) {
2720
+ lines.push(`related_links:`);
2721
+ doc.related_links.forEach(rl => lines.push(` - { url: ${JSON.stringify(rl.url)}, title: ${JSON.stringify(rl.title || '')} }`));
2722
+ }
2723
+ lines.push('---');
2724
+ lines.push('');
2725
+ lines.push(`# ${name}`);
2726
+ lines.push('');
2727
+ if (direction) { lines.push('## 🎯 방향 지시 (사용자 또는 AI)'); lines.push(''); lines.push(direction); lines.push(''); }
2728
+ lines.push('## 📄 본 URL 내용 (정리)');
2729
+ lines.push('');
2730
+ if (doc.title) { lines.push(`**Title:** ${doc.title}`); lines.push(''); }
2731
+ lines.push('```');
2732
+ lines.push(doc.text.slice(0, 6000));
2733
+ lines.push('```');
2734
+ lines.push('');
2735
+ if (doc.related_links && doc.related_links.length) {
2736
+ lines.push('## 🔗 관련 링크 (depth=1, same-domain, max 10)');
2737
+ lines.push('');
2738
+ doc.related_links.forEach(rl => {
2739
+ lines.push(`### ${rl.title || rl.url}`);
2740
+ lines.push(`URL: ${rl.url}`);
2741
+ lines.push('');
2742
+ if (rl.excerpt) { lines.push(rl.excerpt); lines.push(''); }
2743
+ });
2744
+ }
2745
+ return lines.join('\n');
2746
+ }
2747
+ function _loadAPISkill(root, id) {
2748
+ const fp = path.join(_apiSkillsDir(root), id + '.md');
2749
+ if (!fs.existsSync(fp)) return null;
2750
+ const content = fs.readFileSync(fp, 'utf8');
2751
+ const fm = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
2752
+ if (!fm) return { id, content, urls: [], name: id };
2753
+ const meta = {};
2754
+ fm[1].split('\n').forEach(l => {
2755
+ const m = l.match(/^(\w+):\s*(.*)$/);
2756
+ if (m) meta[m[1]] = m[2];
2757
+ });
2758
+ const urls = [];
2759
+ const urlMatch = fm[1].match(/urls:\n((?:\s+-\s+.+\n?)+)/);
2760
+ if (urlMatch) urlMatch[1].split('\n').forEach(l => { const u = l.match(/^\s+-\s+(.+)$/); if (u) urls.push(u[1].trim()); });
2761
+ return { id: meta.id || id, name: meta.name || id, urls, direction: (meta.direction || '').replace(/^"|"$/g, ''), captured_at: meta.captured_at, body: fm[2] };
2762
+ }
2763
+ function _listAPISkills(root) {
2764
+ const dir = _apiSkillsDir(root);
2765
+ if (!fs.existsSync(dir)) return [];
2766
+ return fs.readdirSync(dir).filter(f => f.endsWith('.md')).map(f => f.slice(0, -3))
2767
+ .map(id => _loadAPISkill(root, id)).filter(Boolean);
2768
+ }
2769
+ function _matchAPISkills(root, query) {
2770
+ if (!query) return [];
2771
+ const q = String(query).toLowerCase();
2772
+ // 1.9.245: CJK 한글/한자 토큰 (2자+) 허용, ASCII 토큰 (3자+)
2773
+ const tokens = q.split(/[\s,.;:/\(\)\[\]\{\}"'`]+/).filter(t => {
2774
+ if (!t) return false;
2775
+ const isCJK = /[ -鿿가-힯]/.test(t);
2776
+ return isCJK ? t.length >= 2 : t.length >= 3;
2777
+ });
2778
+ const skills = _listAPISkills(root);
2779
+ const scored = skills.map(s => {
2780
+ let score = 0;
2781
+ const haystack = (s.name + ' ' + (s.direction || '') + ' ' + s.urls.join(' ') + ' ' + s.body.slice(0, 3000)).toLowerCase();
2782
+ tokens.forEach(t => { if (haystack.includes(t)) score += 1; });
2783
+ return { skill: s, score };
2784
+ }).filter(x => x.score > 0).sort((a, b) => b.score - a.score);
2785
+ return scored.map(x => x.skill);
2786
+ }
2787
+ async function apiSkillCmd(root, sub) {
2788
+ root = absRoot(root);
2789
+ const isTty = process.stdout && process.stdout.isTTY;
2790
+ const cy = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
2791
+ const gr = s => isTty ? `\x1b[32m${s}\x1b[0m` : s;
2792
+ const yl = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
2793
+ const rd = s => isTty ? `\x1b[31m${s}\x1b[0m` : s;
2794
+ const dm = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
2795
+ const dir = _apiSkillsDir(root);
2796
+ if (sub === 'list') {
2797
+ if (!fs.existsSync(dir)) {
2798
+ if (has('--json')) { log(JSON.stringify({ skills: [] })); return; }
2799
+ log(dm(`(아직 API skill 없음 — leerness api-skill add <url> 으로 생성)`));
2800
+ return;
2801
+ }
2802
+ const skills = _listAPISkills(root);
2803
+ if (has('--json')) {
2804
+ log(JSON.stringify({ skills: skills.map(s => ({ id: s.id, name: s.name, urls: s.urls, captured_at: s.captured_at })) }, null, 2));
2805
+ return;
2806
+ }
2807
+ log(cy(`# leerness api-skill list (1.9.245, UR-0015)`));
2808
+ log('');
2809
+ if (skills.length === 0) { log(dm(`(없음)`)); return; }
2810
+ skills.forEach(s => {
2811
+ log(` • ${gr(s.id)} — ${s.name}`);
2812
+ if (s.urls && s.urls.length) log(dm(` ${s.urls[0]}${s.urls.length > 1 ? ` (+${s.urls.length - 1})` : ''}`));
2813
+ if (s.direction) log(dm(` 방향: ${s.direction.slice(0, 80)}`));
2814
+ });
2815
+ return;
2816
+ }
2817
+ // 1.9.245: 위치 인자 robust 추출 — argv 에서 cmd/sub 뒤 첫 non-flag (flag value 도 skip)
2818
+ const _positionalAfterSub = () => {
2819
+ const argv = process.argv.slice(2); // ['api-skill', sub, ...]
2820
+ let i = 2; // skip cmd + sub
2821
+ while (i < argv.length) {
2822
+ const a = argv[i];
2823
+ if (a.startsWith('--')) {
2824
+ // --key value 패턴이면 value 도 skip
2825
+ if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) i += 2;
2826
+ else i += 1;
2827
+ continue;
2828
+ }
2829
+ return a;
2830
+ }
2831
+ return null;
2832
+ };
2833
+ if (sub === 'show') {
2834
+ const id = arg('--id') || _positionalAfterSub();
2835
+ if (!id) { log(rd('id 필요: leerness api-skill show <id>')); return; }
2836
+ const s = _loadAPISkill(root, id);
2837
+ if (!s) { log(rd(`api-skill 없음: ${id}`)); return; }
2838
+ if (has('--json')) { log(JSON.stringify(s, null, 2)); return; }
2839
+ log(fs.readFileSync(path.join(dir, id + '.md'), 'utf8'));
2840
+ return;
2841
+ }
2842
+ if (sub === 'match') {
2843
+ const query = arg('--query') || _positionalAfterSub() || '';
2844
+ const matches = _matchAPISkills(root, query);
2845
+ if (has('--json')) {
2846
+ log(JSON.stringify({ query, matches: matches.map(s => ({ id: s.id, name: s.name })) }, null, 2));
2847
+ return;
2848
+ }
2849
+ log(cy(`# leerness api-skill match "${query}" — ${matches.length}건`));
2850
+ matches.forEach(s => log(` • ${gr(s.id)} — ${s.name}`));
2851
+ if (matches.length === 0) log(dm(`(매칭 없음)`));
2852
+ return;
2853
+ }
2854
+ if (sub === 'drop') {
2855
+ const id = arg('--id') || _positionalAfterSub();
2856
+ if (!id) { log(rd('id 필요: leerness api-skill drop <id>')); return; }
2857
+ const fp = path.join(dir, id + '.md');
2858
+ if (!fs.existsSync(fp)) { log(rd(`없음: ${id}`)); return; }
2859
+ fs.unlinkSync(fp);
2860
+ log(gr(`✓ 삭제: ${id}`));
2861
+ return;
2862
+ }
2863
+ if (sub === 'add') {
2864
+ const url = arg('--url') || _positionalAfterSub();
2865
+ if (!url) { log(rd('URL 필요: leerness api-skill add <url> [--direction "..."]')); return; }
2866
+ const direction = arg('--direction', '');
2867
+ const name = arg('--name', '');
2868
+ const noCrawl = has('--no-crawl');
2869
+ log(cy(`# leerness api-skill add (1.9.245, UR-0015)`));
2870
+ log(` URL: ${url}`);
2871
+ if (direction) log(` 방향: ${direction}`);
2872
+ log(dm(` Fetch + crawl (depth=1, same-domain, max 10)...`));
2873
+ try {
2874
+ const doc = await _collectAPIDoc(url, { direction, noCrawl });
2875
+ const skillName = name || doc.title || new URL(url).hostname;
2876
+ const id = _apiSkillId(url, name);
2877
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
2878
+ const md = _serializeAPISkill(id, skillName, [url], direction, doc);
2879
+ fs.writeFileSync(path.join(dir, id + '.md'), md, 'utf8');
2880
+ log(gr(`✓ 저장: .harness/api-skills/${id}.md`));
2881
+ log(dm(` title: ${doc.title || '(none)'}`));
2882
+ log(dm(` related links: ${(doc.related_links || []).length}`));
2883
+ log(dm(` text size: ${doc.text.length} chars`));
2884
+ if (has('--json')) log(JSON.stringify({ id, name: skillName, url, related_count: (doc.related_links || []).length }, null, 2));
2885
+ } catch (e) {
2886
+ log(rd(`✗ Fetch 실패: ${e.message}`));
2887
+ // 1.9.245: --skeleton fallback — Cloudflare/WAF 차단된 URL 도 빈 .md 골격 생성 → 사용자가 수동 채움
2888
+ if (has('--skeleton') || /403|401|429/.test(e.message)) {
2889
+ const skillName = name || new URL(url).hostname;
2890
+ const id = _apiSkillId(url, name);
2891
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
2892
+ const stub = {
2893
+ url, title: skillName, text: `(fetch 실패 — 사용자가 직접 ${url} 에서 내용을 복사해 채워주세요)`,
2894
+ related_links: [], captured_at: new Date().toISOString(), direction
2895
+ };
2896
+ const md = _serializeAPISkill(id, skillName, [url], direction, stub);
2897
+ fs.writeFileSync(path.join(dir, id + '.md'), md, 'utf8');
2898
+ log(yl(`⚠ 빈 골격 생성 (--skeleton fallback): .harness/api-skills/${id}.md`));
2899
+ log(dm(` → 직접 ${url} 내용을 복사해서 .md 파일에 채워넣어주세요`));
2900
+ log(dm(` → 이후 leerness handoff 가 매칭 시 참조됩니다`));
2901
+ if (has('--json')) log(JSON.stringify({ id, skeleton: true, url, name: skillName }, null, 2));
2902
+ } else {
2903
+ process.exitCode = 1;
2904
+ }
2905
+ }
2906
+ return;
2907
+ }
2908
+ // help
2909
+ log(cy(`# leerness api-skill (1.9.245, UR-0015) — API 문서·관련링크 자동 정리`));
2910
+ log('');
2911
+ log(` add <url> [--direction "..."] [--name "..."] [--no-crawl]`);
2912
+ log(` URL 페치 + same-domain 관련 링크 1단계 crawl (max 10) → .harness/api-skills/<id>.md`);
2913
+ log(` list [--json] → 저장된 api-skill 목록`);
2914
+ log(` show <id> → 특정 skill 본문 출력`);
2915
+ log(` match <query> [--json] → task 키워드 매칭`);
2916
+ log(` drop <id> → 삭제`);
2917
+ log('');
2918
+ log(dm(` 저장: .harness/api-skills/<id>.md (frontmatter: id/name/urls/direction/captured_at/related_links)`));
2919
+ log(dm(` fetch: Node built-in https (의존성 0), max 1MB body, 10s timeout, 5 redirect`));
2920
+ log(dm(` 자동 참조: handoff body 에 매칭 N건 자동 노출 (현재 task 키워드)`));
2921
+ }
2922
+
2591
2923
  // 1.9.231: leerness pulse — 한 줄 종합 요약 (10 핵심 지표)
2592
2924
  // handoff 헤드라인의 축약 버전 — 사용자가 빠르게 상태 파악
2593
2925
  // 응답 (--json): { version, roundCount, mcpTools, memorySurface, security, health, driftScore, nextMilestone, etaDays, abnormal }
@@ -6629,6 +6961,25 @@ function handoff(root) {
6629
6961
  encodingRiskFiles: encScan.atRisk.slice(0, 5).map(r => r.file)
6630
6962
  };
6631
6963
  } catch {}
6964
+ // 1.9.245: apiSkills 통합 (handoff JSON 11번째 통합 필드) — UR-0015 API 문서 캐시
6965
+ // 현재 task 키워드 기반 매칭 + 전체 카운트
6966
+ try {
6967
+ const allSkills = _listAPISkills(root);
6968
+ // 현재 task description 추출 (간단 휴리스틱: progress-tracker in-progress 첫 row)
6969
+ let currentTaskText = '';
6970
+ try {
6971
+ const rows = readProgressRows(root);
6972
+ const ip = rows.find(r => r.status === 'in-progress');
6973
+ if (ip) currentTaskText = (ip.title || '') + ' ' + (ip.notes || '');
6974
+ } catch {}
6975
+ const matched = currentTaskText ? _matchAPISkills(root, currentTaskText) : [];
6976
+ result.apiSkills = {
6977
+ total: allSkills.length,
6978
+ matched: matched.length,
6979
+ matchedIds: matched.slice(0, 5).map(s => s.id),
6980
+ ids: allSkills.slice(0, 10).map(s => s.id)
6981
+ };
6982
+ } catch {}
6632
6983
  } catch {}
6633
6984
  try {
6634
6985
  const pwState = _loadPreWakeReport(root);
@@ -6995,6 +7346,40 @@ function handoff(root) {
6995
7346
  }
6996
7347
  } catch {}
6997
7348
 
7349
+ // 1.9.245: API skill cache 자동 참조 (사용자 명시 UR-0015)
7350
+ // 현재 task 키워드 기반으로 .harness/api-skills/ 매칭 → 사용자가 정리해둔 API 문서 자동 노출
7351
+ try {
7352
+ const allSkills = _listAPISkills(root);
7353
+ if (allSkills.length > 0) {
7354
+ let currentTaskText = '';
7355
+ try {
7356
+ const rows = readProgressRows(root);
7357
+ const ip = rows.find(r => r.status === 'in-progress');
7358
+ if (ip) currentTaskText = (ip.title || '') + ' ' + (ip.notes || '');
7359
+ } catch {}
7360
+ const matched = currentTaskText ? _matchAPISkills(root, currentTaskText) : [];
7361
+ if (matched.length > 0) {
7362
+ const isTty = process.stdout && process.stdout.isTTY;
7363
+ const cy5 = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
7364
+ const gr5 = s => isTty ? `\x1b[32m${s}\x1b[0m` : s;
7365
+ const dm5 = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
7366
+ log('');
7367
+ log(cy5(`## 📚 관련 API skill ${matched.length}건 발견 (1.9.245, UR-0015) — 참조 권장`));
7368
+ matched.slice(0, 3).forEach(s => {
7369
+ log(gr5(` • ${s.id} — ${s.name}`));
7370
+ if (s.urls && s.urls.length) log(dm5(` ${s.urls[0]}${s.urls.length > 1 ? ` (+${s.urls.length - 1})` : ''}`));
7371
+ if (s.direction) log(dm5(` 방향: ${s.direction.slice(0, 100)}`));
7372
+ });
7373
+ if (matched.length > 3) log(dm5(` ... +${matched.length - 3}건 더`));
7374
+ log(dm5(` → 본문: leerness api-skill show <id> | leerness api-skill list`));
7375
+ } else {
7376
+ const isTty = process.stdout && process.stdout.isTTY;
7377
+ const dm5 = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
7378
+ log(dm5(`📚 API skill ${allSkills.length}건 저장됨 (현재 task 매칭 0 — leerness api-skill list)`));
7379
+ }
7380
+ }
7381
+ } catch {}
7382
+
6998
7383
  // 1.9.237: handoff 본문 release/* 누적 경고 — 50+ branches merged 시 자동 안내
6999
7384
  // 1.9.220 비정상종료 release-branch-pending 신호의 본문 보강 (handoff 헤드라인은 ‘🔌 비정상종료’ 만 표시)
7000
7385
  try {
@@ -10871,6 +11256,23 @@ function sessionClose(root, opts = {}) {
10871
11256
  encodingRiskFiles: encScan.atRisk.slice(0, 5).map(r => r.file)
10872
11257
  };
10873
11258
  } catch {}
11259
+ // 1.9.245: apiSkills 통합 (session close JSON 11번째 통합 필드) — UR-0015
11260
+ try {
11261
+ const allSkills = _listAPISkills(root);
11262
+ let currentTaskText = '';
11263
+ try {
11264
+ const rows = readProgressRows(root);
11265
+ const ip = rows.find(r => r.status === 'in-progress');
11266
+ if (ip) currentTaskText = (ip.title || '') + ' ' + (ip.notes || '');
11267
+ } catch {}
11268
+ const matched = currentTaskText ? _matchAPISkills(root, currentTaskText) : [];
11269
+ jsonResult.apiSkills = {
11270
+ total: allSkills.length,
11271
+ matched: matched.length,
11272
+ matchedIds: matched.slice(0, 5).map(s => s.id),
11273
+ ids: allSkills.slice(0, 10).map(s => s.id)
11274
+ };
11275
+ } catch {}
10874
11276
  } catch {}
10875
11277
  try {
10876
11278
  // 1.9.209: pre-wake-audit 자동 실행 + 저장 (sleep 전 자동 점검)
@@ -15060,7 +15462,8 @@ function mcpServeCmd(root) {
15060
15462
  { name: 'leerness_release_cleanup', description: '1.9.236 (1.9.235 자동 회수) — local release/* branches 정리. main 에 merge된 것만 후보 (unmerged 보호, 현재 branch 보호). 응답: { apply, keep, total, merged, unmerged, deleteCount, toDelete[], recent[], unmergedSample[] }. 외부 AI가 "운영 누적 release branches 정리 가능?"을 회수. 기본 dry-run, apply: true 시에만 실 삭제. 인자: { path?, apply? (default false), keep? (default 5) }', inputSchema: { type: 'object', properties: { path: { type: 'string' }, apply: { type: 'boolean' }, keep: { type: 'number' } } } },
15061
15463
  { name: 'leerness_py_check', description: '1.9.239 (사용자 명시 UR-0013) — Python 파일 분석. 의존성 0 regex fallback. .md 외 .py 도 leerness 인지. 응답: { totalFiles, totalLOC, totalImports, totalFuncs, totalClasses, totalTodos, biggest: [{ file, loc, funcs, classes }] }. 외부 AI가 "이 프로젝트 Python 표면이 얼마나 되나"를 회수. 인자: { path? }', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
15062
15464
  { name: 'leerness_agent_mode', description: '1.9.239 (사용자 명시 UR-0013) — 자율 모드 전용 통합 명령. start: handoff + drift --auto-fix + session-resume --auto-fix (진입). tick: pulse 한 줄 (매 라운드). stop: session close --auto-apply-delivered --auto-cleanup-branches (마감). 외부 AI 가 자율 라운드 진입/매 라운드/마감을 단일 호출로 수행. 인자: { path?, sub (required: "start"|"tick"|"stop"|"help") }', inputSchema: { type: 'object', properties: { path: { type: 'string' }, sub: { type: 'string', enum: ['start', 'tick', 'stop', 'help'] } }, required: ['sub'] } },
15063
- { name: 'leerness_env_info', description: '1.9.241 (사용자 명시 UR-0014) — 환경 종합 정보 회수. OS / 언어 (LANG, 코드페이지) / 한국어 Windows / 하드웨어 / 터미널 (TTY, PowerShell 버전) / 도구 버전 (git, npm, python). 외부 AI 가 환경 호환성을 미리 인지 → 인코딩 오류 예방. 응답: { os, node, locale, hardware, terminal, tools }. 인자: { path?, encodingCheck? }. encodingCheck: true 시 셸 스크립트 (.ps1/.bat/.cmd/.sh) BOM 없는 비-ASCII 위험 감지', inputSchema: { type: 'object', properties: { path: { type: 'string' }, encodingCheck: { type: 'boolean' } } } }
15465
+ { name: 'leerness_env_info', description: '1.9.241 (사용자 명시 UR-0014) — 환경 종합 정보 회수. OS / 언어 (LANG, 코드페이지) / 한국어 Windows / 하드웨어 / 터미널 (TTY, PowerShell 버전) / 도구 버전 (git, npm, python). 외부 AI 가 환경 호환성을 미리 인지 → 인코딩 오류 예방. 응답: { os, node, locale, hardware, terminal, tools }. 인자: { path?, encodingCheck? }. encodingCheck: true 시 셸 스크립트 (.ps1/.bat/.cmd/.sh) BOM 없는 비-ASCII 위험 감지', inputSchema: { type: 'object', properties: { path: { type: 'string' }, encodingCheck: { type: 'boolean' } } } },
15466
+ { name: 'leerness_api_skill', description: '1.9.245 (사용자 명시 UR-0015) — API 문서·관련링크 자동 캐시. 공식 API 문서 URL을 fetch 하고 1단계 same-domain 관련 링크까지 정리 → .harness/api-skills/<id>.md 저장. 후속 같은 API 관련 작업 시 자동 참조. 응답: list (skills 배열) / show (전체 본문) / match (task 매칭 결과). 인자: { path?, sub ("list"|"show"|"match"|"add"|"drop"), url? (add), id? (show/drop), query? (match), direction? (add: 구현 방향 텍스트) }. 외부 AI가 "이 프로젝트 어떤 API 문서가 정리되어 있나?" / "내 작업과 매칭되는 API skill 있나?" 회수.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, sub: { type: 'string', enum: ['list', 'show', 'match', 'add', 'drop'] }, url: { type: 'string' }, id: { type: 'string' }, query: { type: 'string' }, direction: { type: 'string' } }, required: ['sub'] } }
15064
15467
  ];
15065
15468
 
15066
15469
  function send(obj) {
@@ -15249,6 +15652,14 @@ function mcpServeCmd(root) {
15249
15652
  // 1.9.241 (UR-0014): 환경 종합 + 인코딩 위험 감지
15250
15653
  cliArgs = ['env', args.encodingCheck === true ? 'encoding' : 'summary', '--path', targetPath, '--json'];
15251
15654
  break;
15655
+ case 'leerness_api_skill':
15656
+ // 1.9.245 (UR-0015): API skill cache — list/show/match/add/drop
15657
+ cliArgs = ['api-skill', args.sub || 'list', '--path', targetPath, '--json'];
15658
+ if (args.sub === 'add' && args.url) cliArgs.push('--url', args.url);
15659
+ if (args.sub === 'add' && args.direction) cliArgs.push('--direction', args.direction);
15660
+ if ((args.sub === 'show' || args.sub === 'drop') && args.id) cliArgs.push('--id', args.id);
15661
+ if (args.sub === 'match' && args.query) cliArgs.push('--query', args.query);
15662
+ break;
15252
15663
  case 'leerness_release_cleanup':
15253
15664
  // 1.9.236 (1.9.235): local release/* branches 정리
15254
15665
  cliArgs = ['release', 'cleanup', '--json'];
@@ -16393,6 +16804,11 @@ async function _agentRepl(root, opts) {
16393
16804
  };
16394
16805
  rl.setPrompt(prompt());
16395
16806
 
16807
+ // 1.9.244 HOTFIX: _lastCycleLines 를 outer 스코프로 hoist
16808
+ // 1.9.189 회귀 버그 fix — rl.on('line') 핸들러가 try 블록 외부에서 접근 시 ReferenceError 발생했음.
16809
+ // 사용자 보고: REPL agent 진입 후 채팅 입력 시 "_lastCycleLines is not defined" 크래시.
16810
+ let _lastCycleLines = 0; // 직전 cycle 출력 라인 수 (overwrite 용) — 1.9.189/244
16811
+
16396
16812
  // 1.9.170: Tab cycle — provider (Tab) / model within provider (Shift+Tab)
16397
16813
  // 사용자 명시 요청: "탭 키 등으로 provider/모델 셀렉과 선택을 간편하게"
16398
16814
  // readline의 default tab=completion 동작을 keypress 리스너로 가로채서 cycle 수행.
@@ -16415,7 +16831,7 @@ async function _agentRepl(root, opts) {
16415
16831
  // 1.9.180+1.9.189: cycleProvider/cycleModel — 한 줄 갱신 (in-place overwrite).
16416
16832
  // 1.9.180까지: 매 Tab 누름마다 새 줄 출력 → 채팅 이력으로 누적 (사용자 명시: "지져분해보여").
16417
16833
  // 1.9.189: ANSI cursor up + line clear 로 이전 cycle 라인 덮어씀 → 마지막 1건만 표시.
16418
- let _lastCycleLines = 0; // 직전 cycle 출력 라인 (overwrite 용)
16834
+ // 1.9.244: _lastCycleLines outer 스코프 (위)에서 선언 rl.on('line') 핸들러와 공유.
16419
16835
  const _clearLastCycle = () => {
16420
16836
  if (!isTty || _lastCycleLines === 0) return;
16421
16837
  // 현재 prompt 라인 + 이전 cycle 라인들 클리어
@@ -17506,6 +17922,23 @@ function healthCmd(root) {
17506
17922
  encodingRiskFiles: encScan.atRisk.slice(0, 5).map(r => r.file)
17507
17923
  };
17508
17924
  } catch { out.envInfo = { error: 'envInfo 점검 실패' }; }
17925
+ // 1.9.245: health --json apiSkills 통합 (3 명령 11 필드 — UR-0015)
17926
+ try {
17927
+ const allSkills = _listAPISkills(root);
17928
+ let currentTaskText = '';
17929
+ try {
17930
+ const rows = readProgressRows(root);
17931
+ const ip = rows.find(r => r.status === 'in-progress');
17932
+ if (ip) currentTaskText = (ip.title || '') + ' ' + (ip.notes || '');
17933
+ } catch {}
17934
+ const matched = currentTaskText ? _matchAPISkills(root, currentTaskText) : [];
17935
+ out.apiSkills = {
17936
+ total: allSkills.length,
17937
+ matched: matched.length,
17938
+ matchedIds: matched.slice(0, 5).map(s => s.id),
17939
+ ids: allSkills.slice(0, 10).map(s => s.id)
17940
+ };
17941
+ } catch { out.apiSkills = { error: 'apiSkills 점검 실패' }; }
17509
17942
  // 1.9.163: 5능력 매트릭스 자동 평가 (1.9.155 sub-agent 점검 → 코드 기반 자동화)
17510
17943
  // 각 능력을 코드 grep 으로 검출 → 0~100 점수. 사용자가 매 health 호출 시 leerness 자기 평가 확인.
17511
17944
  try {
@@ -18955,6 +19388,8 @@ async function main() {
18955
19388
  // 기존 env check/sync/detect (1.9.71) 와 충돌하지 않음 — summary/encoding 신규 서브
18956
19389
  if (cmd === 'env' && (args[1] === 'summary' || args[1] === 'encoding' || args[1] === 'encoding-check'))
18957
19390
  return envCmd(arg('--path', process.cwd()), args[1] === 'summary' ? null : 'encoding-check');
19391
+ // 1.9.245: API skill cache — 공식 문서·관련링크 자동 정리 (사용자 명시 UR-0015)
19392
+ if (cmd === 'api-skill') return apiSkillCmd(arg('--path', process.cwd()), args[1] || 'help');
18958
19393
  // 1.9.208: leerness constraints <list|check|add> — 플랫폼/API 제약 사전 체크 (사용자 명시)
18959
19394
  if (cmd === 'constraints') return constraintsCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
18960
19395
  // 1.9.209: leerness pre-wake-audit — sleep 전 sub-agent audit (사용자 명시)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.243",
3
+ "version": "1.9.245",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",