leerness 1.9.68 → 1.9.70

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,60 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.70 — 2026-05-19
4
+
5
+ **MCP server `tools/call` 자동 사용 통계** (1.9.65 usage-stats 확장).
6
+
7
+ ### Added — MCP 도구별 호출 카운트
8
+ - MCP server가 `tools/call` 요청을 받을 때마다 도구 이름별 카운트를 `.harness/cache/usage-stats.json`의 `mcp.tools` 섹션에 기록.
9
+ - 별도 mtime 캐시 invalidation (1.9.65 _USAGE_CACHE 재활용).
10
+ - `leerness usage stats` 출력에 MCP 섹션 자동 노출:
11
+ ```
12
+ ## 🔌 MCP tools/call 통계 (1.9.70) — last: <ISO>
13
+ | MCP 도구 | 호출 수 |
14
+ | leerness_handoff | 8 |
15
+ | ... |
16
+ 💡 드물게 호출된 도구 (≤N): leerness_xxx, ...
17
+ ```
18
+ - 드물게 호출되는 도구 (전체 5% 미만)를 자동 식별 — AI 에이전트가 안 쓰는 도구가 있다는 가시화.
19
+
20
+ ### Internal
21
+ - 새 헬퍼: `_bumpMcpUsage(root, toolName)` — atomic write + 캐시 invalidation.
22
+ - usage-stats.json 스키마 확장:
23
+ ```json
24
+ {
25
+ "commands": {...},
26
+ "drift": {...},
27
+ "mcp": { "tools": {...}, "lastTool": "...", "lastAt": "..." }
28
+ }
29
+ ```
30
+
31
+ ### Verified
32
+ - stress-v16 — MCP 카운트 정합성 + 13 도구 종합 호출 + 1.9.43~69 누적 회귀 + 성능.
33
+ - e2e 회귀: 219/219 PASS 유지.
34
+
35
+ ---
36
+
37
+ ## 1.9.69 — 2026-05-19
38
+
39
+ **handoff에 skill-suggestions.md rolling history hit 노출 (1.9.67 + 1.9.68 결합)**.
40
+
41
+ ### Added — handoff history hit
42
+ - 매 `leerness handoff`마다 현재 task 키워드와 매칭되는 **이전 세션의 `skill match` 결과**를 함께 노출.
43
+ - 매칭: 현재 키워드 (≥4자, 7할 길이)의 fuzzy regex로 `skill-suggestions.md`의 query 헤더 검색.
44
+ - 표시: 최근 2건 + 각 블록의 top 2 매치 라인.
45
+ - AI 에이전트는 **이전 세션과 같은 결정을 일관되게 유지** 가능.
46
+ - 끄기: 같은 `--no-skill-suggest` / `LEERNESS_NO_SKILL_SUGGEST=1`.
47
+
48
+ ### Internal
49
+ - `_loadSkillHistory(root)` + `_SKILL_HISTORY_CACHE` — mtime 기반 메모리 캐시 (1.9.65/66/67 캐시 패밀리 연속).
50
+ - 같은 프로세스에서 `_lidx` / `_SKILLS_LIST_CACHE` / `_USAGE_CACHE`와 함께 lifetime 공유.
51
+
52
+ ### Verified
53
+ - stress-v15 — history hit 노출 + 비매칭 시 출력 안 함 + mtime invalidation + 누적 회귀.
54
+ - e2e 회귀: 219/219 PASS 유지.
55
+
56
+ ---
57
+
3
58
  ## 1.9.68 — 2026-05-19
4
59
 
5
60
  **`skill match` rolling history 자동 누적 + 종합 회귀**.
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.68-green)]() [![tests](https://img.shields.io/badge/e2e-219%2F219-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.70-green)]() [![tests](https://img.shields.io/badge/e2e-219%2F219-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.68 AI Agent Reliability Harness ║
15
+ ║ v1.9.70 AI Agent Reliability Harness ║
16
16
  ║ verify · remember · orchestrate · audit · prevent drift ║
17
17
  ╚══════════════════════════════════════════════════════════════╝
18
18
  ```
@@ -433,6 +433,8 @@ npm test # = node ./scripts/e2e.js
433
433
 
434
434
  ## 변경 이력 (최근)
435
435
 
436
+ - **1.9.70** — **MCP server `tools/call` 자동 사용 통계** — 도구별 호출 카운트 (`.harness/cache/usage-stats.json#mcp.tools`) + `leerness usage stats` 출력에 MCP 섹션 + 드물게 호출되는 도구 식별.
437
+ - **1.9.69** — **handoff에 skill-suggestions.md history hit 노출** (fuzzy 매칭, 최근 2건 + top 2 매치). AI가 이전 세션 결정을 일관 유지. mtime 기반 캐시 (1.9.65/66/67 캐시 패밀리 연속).
436
438
  - **1.9.68** — **`skill match` 결과 → `.harness/skill-suggestions.md` rolling history 자동 누적** (AI가 다음 세션에 이전 추천 참조 가능). `--no-save` / `LEERNESS_NO_SKILL_HISTORY=1`로 끄기.
437
439
  - **1.9.67** — **handoff 자동 skill 추천 default ON** (jaccard 매칭) + lessons 인덱스에 task-log.md 실패 라인 통합 (회수 범위 확장).
438
440
  - **1.9.66** — **성능 최적화 2차 + MCP 13번째 도구**. `listAllSkills` 메모리 캐시 (skill list/info/match/discover/suggest 공유) + MCP `leerness_task_export` 추가 (TodoWrite 양방향 sync 외부 노출).
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.68';
9
+ const VERSION = '1.9.70';
10
10
  const MARK = '<!-- leerness:managed -->';
11
11
  const README_START = '<!-- leerness:project-readme:start -->';
12
12
  const README_END = '<!-- leerness:project-readme:end -->';
@@ -1844,6 +1844,26 @@ function handoff(root) {
1844
1844
  log(dim(` → 전체: leerness skill match "${String(latestRow.request).slice(0, 60)}"`));
1845
1845
  log('');
1846
1846
  }
1847
+ // 1.9.69: skill-suggestions.md rolling history hit — 이전 세션 매칭 결과 노출
1848
+ const hist = _loadSkillHistory(root);
1849
+ if (hist.blocks.length) {
1850
+ const histRe = new RegExp(escapeRegex(keyword.slice(0, Math.max(4, Math.floor(keyword.length * 0.7)))), 'i');
1851
+ const hits = hist.blocks.filter(b => histRe.test(b.query)).slice(0, 2);
1852
+ if (hits.length) {
1853
+ const isTty = process.stdout && process.stdout.isTTY;
1854
+ const blu = s => isTty ? `\x1b[34m${s}\x1b[0m` : s;
1855
+ const dim = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
1856
+ log(blu(`## 📒 이전 skill match 이력 (1.9.69) — 키워드 "${keyword}" 관련`));
1857
+ for (const h of hits) {
1858
+ // 블록에서 첫 1~2개 match 줄만 추출
1859
+ const matchLines = (h.block.match(/^\s*-\s*\[[\d.]+\][^\n]+/gm) || []).slice(0, 2);
1860
+ log(dim(` [${h.at}] query "${h.query}"`));
1861
+ for (const ml of matchLines) log(dim(` ${ml.trim()}`));
1862
+ }
1863
+ log(dim(` → 전체 이력: cat .harness/skill-suggestions.md`));
1864
+ log('');
1865
+ }
1866
+ }
1847
1867
  }
1848
1868
  } catch {}
1849
1869
  }
@@ -3196,15 +3216,15 @@ function _banner(opts = {}) {
3196
3216
  lines.push('');
3197
3217
  for (const ln of lines) log(ln);
3198
3218
  if (opts.quickStart) {
3199
- log(C.bold(C.cyan(' ✨ 빠른 시작 (1.9.68+ 워크플로)')));
3219
+ log(C.bold(C.cyan(' ✨ 빠른 시작 (1.9.70+ 워크플로)')));
3200
3220
  log(' ' + C.green('npx leerness@latest init .') + C.dim(' # 신규 프로젝트 + 외부 AI CLI 설정'));
3201
- log(' ' + C.green('npx leerness handoff .') + C.dim(' # 컨텍스트 + lessons 재상기 + 매칭 skill 자동 추천'));
3221
+ log(' ' + C.green('npx leerness handoff .') + C.dim(' # 컨텍스트 + lessons + 매칭 skill + 이전 history hit (1.9.69)'));
3202
3222
  log(' ' + C.green('npx leerness skill match "<query>"') + C.dim(' # 매칭 skill + rolling history 자동 누적 (1.9.68)'));
3203
3223
  log(' ' + C.green('npx leerness verify-claim T-0001 --run-tests') + C.dim(' # AI 거짓 완료 자동 검증'));
3204
3224
  log(' ' + C.green('npx leerness session close .') + C.dim(' # 마감 + 다음 라운드 추천 (default)'));
3205
3225
  log('');
3206
3226
  log(C.bold(C.cyan(' 🤖 메인 에이전트 (Claude/Cursor/Copilot)용')));
3207
- log(' ' + C.green('npx leerness mcp serve') + C.dim(' # MCP 서버 — 13 도구 노출 (task_export 포함)'));
3227
+ log(' ' + C.green('npx leerness mcp serve') + C.dim(' # MCP 서버 — 13 도구 + tools/call 자동 통계 (1.9.70)'));
3208
3228
  log(' ' + C.green('npx leerness agents bench "<task>"') + C.dim(' # 3 CLI 동시 비교'));
3209
3229
  log('');
3210
3230
  }
@@ -6191,6 +6211,33 @@ function driftCheckCmd(root, opts = {}) {
6191
6211
  if (level === '🔴 critical') process.exitCode = 1;
6192
6212
  }
6193
6213
 
6214
+ // 1.9.69: skill-suggestions.md rolling history 인덱스 — mtime 기반 캐시
6215
+ // handoff에서 같은 키워드 과거 추천 결과를 즉시 노출 (재매칭 불필요)
6216
+ const _SKILL_HISTORY_CACHE = new Map();
6217
+ function _loadSkillHistory(root) {
6218
+ const p = path.join(absRoot(root), '.harness', 'skill-suggestions.md');
6219
+ if (!exists(p)) return { mtime: 0, blocks: [] };
6220
+ let mtime = 0;
6221
+ try { mtime = fs.statSync(p).mtimeMs; } catch {}
6222
+ const key = absRoot(root);
6223
+ const cached = _SKILL_HISTORY_CACHE.get(key);
6224
+ if (cached && cached.mtime === mtime) return cached;
6225
+ const txt = read(p);
6226
+ const blocks = [];
6227
+ for (const block of txt.split(/\n(?=## )/)) {
6228
+ if (!block.startsWith('## ')) continue;
6229
+ // 헤더에서 timestamp + query 추출
6230
+ const h = block.match(/^## ([\d-]+ [\d:]+) — query "([^"]+)"/m);
6231
+ if (!h) continue;
6232
+ blocks.push({ at: h[1], query: h[2], block });
6233
+ }
6234
+ // 최신순 (마지막에 append되므로 reverse)
6235
+ blocks.reverse();
6236
+ const idx = { mtime, blocks };
6237
+ _SKILL_HISTORY_CACHE.set(key, idx);
6238
+ return idx;
6239
+ }
6240
+
6194
6241
  // 1.9.65: lessons blocks 인덱스 — evidence/decisions 파일 read + split을 1회로
6195
6242
  // 1.9.67: task-log.md 실패 라인도 인덱스에 포함 (mtime 기반 invalidation)
6196
6243
  // key: root → { evidenceMtime, decisionsMtime, taskLogMtime, evidence/decisions/taskLogFails: [{title, block}] }
@@ -6272,6 +6319,23 @@ function _bumpUsage(root, cmdName) {
6272
6319
  } catch {}
6273
6320
  }
6274
6321
 
6322
+ // 1.9.70: MCP tools/call 자동 사용 통계 — 도구별 호출 카운트
6323
+ function _bumpMcpUsage(root, toolName) {
6324
+ try {
6325
+ const stats = _readUsageStats(root);
6326
+ if (!stats.mcp) stats.mcp = { tools: {} };
6327
+ if (!stats.mcp.tools) stats.mcp.tools = {};
6328
+ stats.mcp.tools[toolName] = (stats.mcp.tools[toolName] || 0) + 1;
6329
+ stats.mcp.lastTool = toolName;
6330
+ stats.mcp.lastAt = new Date().toISOString();
6331
+ if (!stats.since) stats.since = today();
6332
+ const p = _usageStatsPath(root);
6333
+ mkdirp(path.dirname(p));
6334
+ writeUtf8(p, JSON.stringify(stats, null, 2) + '\n');
6335
+ try { _USAGE_CACHE.set(p, { stats, mtime: fs.statSync(p).mtimeMs }); } catch {}
6336
+ } catch {}
6337
+ }
6338
+
6275
6339
  // 1.9.41: CHANGELOG.md를 파싱하여 from → to 사이 버전 차분 추출
6276
6340
  // 반환: [{ version, date, body, newCommands, newFlags, newFiles }]
6277
6341
  function _parseChangelogBetween(changelogText, fromV, toV) {
@@ -6946,6 +7010,8 @@ function mcpServeCmd(root) {
6946
7010
  } else if (req.method === 'tools/call') {
6947
7011
  const { name, arguments: args = {} } = req.params || {};
6948
7012
  const targetPath = args.path || root;
7013
+ // 1.9.70: MCP tools/call 자동 사용 통계 — 어떤 도구가 자주/드물게 호출되는지 가시화
7014
+ try { _bumpMcpUsage(targetPath, name); } catch {}
6949
7015
  let cliArgs;
6950
7016
  try {
6951
7017
  switch (name) {
@@ -7097,6 +7163,22 @@ function usageStatsCmd(root) {
7097
7163
  log(`💡 drift 경고 ${stats.drift.skipped}회 스킵 → 1.9.38 학습: 임계 자동 완화 (--no-drift-check 빈도 ≥5)`);
7098
7164
  }
7099
7165
  }
7166
+ // 1.9.70: MCP tools/call 자동 사용 통계 — 어떤 도구가 자주/드물게 호출되는지
7167
+ if (stats.mcp && stats.mcp.tools && Object.keys(stats.mcp.tools).length) {
7168
+ const mcpEntries = Object.entries(stats.mcp.tools).sort((a, b) => b[1] - a[1]);
7169
+ const mcpTotal = mcpEntries.reduce((s, [, n]) => s + n, 0);
7170
+ log('');
7171
+ log(`## 🔌 MCP tools/call 통계 (1.9.70) — last: ${stats.mcp.lastAt || '(none)'}`);
7172
+ log(`| MCP 도구 | 호출 수 |`);
7173
+ log(`|---|---:|`);
7174
+ for (const [tool, n] of mcpEntries) log(`| ${tool} | ${n} |`);
7175
+ log('');
7176
+ log(`총 ${mcpTotal} 회 MCP 호출 · 도구 ${mcpEntries.length} 가지 사용`);
7177
+ // 드물게 호출되는 도구 식별 (전체의 5% 미만 호출)
7178
+ const threshold = Math.max(1, Math.floor(mcpTotal * 0.05));
7179
+ const rare = mcpEntries.filter(([, n]) => n <= threshold).map(([t]) => t);
7180
+ if (rare.length) log(`💡 드물게 호출된 도구 (≤${threshold}): ${rare.slice(0, 6).join(', ')}`);
7181
+ }
7100
7182
  }
7101
7183
 
7102
7184
  // 1.9.38: task sync — TodoWrite/외부 JSON에서 leerness task로 mirror
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.68",
3
+ "version": "1.9.70",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",