leerness 1.9.66 → 1.9.67

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,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.67 — 2026-05-19
4
+
5
+ **handoff 자동 skill 추천 default ON + lessons 인덱스에 task-log 통합**.
6
+
7
+ ### Added — handoff 자동 skill match (default ON)
8
+ - 매 `leerness handoff` 시 **현재 in-progress task와 매칭되는 설치된 skill을 자동 추천** (점수 + skill id + description 미리보기).
9
+ - 1.9.45의 `LEERNESS_SKILL_AUTO_DISCOVER=1` opt-in 환경변수 의존성 제거 → default 활성.
10
+ - 끄기: `--no-skill-suggest` 또는 `LEERNESS_NO_SKILL_SUGGEST=1`.
11
+ - 매칭 알고리즘: `_jaccard(task.request_tokens, skill.name+description_tokens)`, top 3.
12
+ - 매칭 점수 0이면 출력 안 함 (잡음 최소화).
13
+
14
+ ### Improved — lessons 인덱스 확장
15
+ - `_loadLessonsIndex`에 **task-log.md 실패 라인** 추가 (mtime 기반 invalidation).
16
+ - `_lidx.taskLogFails: [{title, block}]` 새 필드.
17
+ - handoff lessons 자동 재상기에서 task-log fuzzy 매칭도 가능.
18
+ - `leerness lessons` 명령도 같은 인덱스 사용 (split 1회).
19
+
20
+ ### Updated
21
+ - `_banner` quickStart: "13 도구 노출 (task_export 포함)" + "매칭 skill 자동 추천" 안내.
22
+ - `.harness/session-workflow.md` 템플릿: 1.9.67 라인 추가.
23
+
24
+ ### Verified
25
+ - stress-v13 (1.9.67 검증) — handoff skill match default + --no-skill-suggest + lessons task-log fuzzy.
26
+ - e2e 회귀: 219/219 PASS 유지.
27
+
28
+ ---
29
+
3
30
  ## 1.9.66 — 2026-05-19
4
31
 
5
32
  **성능 최적화 2차 + MCP 13번째 도구**.
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.66-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.67-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.66 AI Agent Reliability Harness ║
15
+ ║ v1.9.67 AI Agent Reliability Harness ║
16
16
  ║ verify · remember · orchestrate · audit · prevent drift ║
17
17
  ╚══════════════════════════════════════════════════════════════╝
18
18
  ```
@@ -433,6 +433,7 @@ npm test # = node ./scripts/e2e.js
433
433
 
434
434
  ## 변경 이력 (최근)
435
435
 
436
+ - **1.9.67** — **handoff 자동 skill 추천 default ON** (jaccard 매칭) + lessons 인덱스에 task-log.md 실패 라인 통합 (회수 범위 확장).
436
437
  - **1.9.66** — **성능 최적화 2차 + MCP 13번째 도구**. `listAllSkills` 메모리 캐시 (skill list/info/match/discover/suggest 공유) + MCP `leerness_task_export` 추가 (TodoWrite 양방향 sync 외부 노출).
437
438
  - **1.9.65** — **성능 최적화 1차** — usage-stats 메모리 캐시 + lessons 인덱스 캐시 (mtime invalidation). handoff -37% · drift -19% · audit -29% · skill list -17% · 100-task handoff -42% · status -48% (vs 1.9.64).
438
439
  - **1.9.64** — `leerness install <SKILL.md>` 별칭 (skill install 단축) · **성능 벤치마크 1차 실측** (status/handoff/drift/audit/skill list 평균 1.2~1.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.66';
9
+ const VERSION = '1.9.67';
10
10
  const MARK = '<!-- leerness:managed -->';
11
11
  const README_START = '<!-- leerness:project-readme:start -->';
12
12
  const README_END = '<!-- leerness:project-readme:end -->';
@@ -289,6 +289,8 @@ leerness audit . --fix # 누락 메타 자동 보강
289
289
  - session close가 누락되면 다음 세션 시작 시 drift critical 발생.
290
290
  - 자동 회복 옵션: \`drift check --auto-fix\` (critical 시 session close 자동 실행).
291
291
  - 1.9.56+ handoff가 매 세션 시작 시 **과거 lessons 자동 재상기** (현재 task 키워드 기준).
292
+ - 1.9.67+ handoff가 현재 task와 매칭되는 **설치된 skill을 자동 추천** (jaccard 기반, default ON, \`--no-skill-suggest\`로 끄기).
293
+ - 1.9.67+ lessons 인덱스에 \`task-log.md\` 실패 라인까지 포함 → 회수 범위 확장.
292
294
 
293
295
  ---
294
296
 
@@ -1802,6 +1804,12 @@ function handoff(root) {
1802
1804
  matches.push({ source: 'decisions.md', title: d.title, block: d.block });
1803
1805
  }
1804
1806
  }
1807
+ // 1.9.67: task-log.md 실패 라인도 fuzzy 매칭 (회수 범위 확장)
1808
+ for (const t of (idx.taskLogFails || [])) {
1809
+ if (fuzzyRe.test(t.block)) {
1810
+ matches.push({ source: 'task-log.md', title: t.title, block: t.block });
1811
+ }
1812
+ }
1805
1813
  if (matches.length) {
1806
1814
  const isTty = process.stdout && process.stdout.isTTY;
1807
1815
  const yel = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
@@ -1815,6 +1823,30 @@ function handoff(root) {
1815
1823
  log(dim(` → 전체: leerness lessons --auto --path .`));
1816
1824
  log('');
1817
1825
  }
1826
+ // 1.9.67: 현재 task와 관련된 skill 자동 추천 (default ON, 1.9.45 opt-in → default)
1827
+ // 끄려면: --no-skill-suggest 또는 LEERNESS_NO_SKILL_SUGGEST=1
1828
+ if (!has('--no-skill-suggest') && process.env.LEERNESS_NO_SKILL_SUGGEST !== '1') {
1829
+ try {
1830
+ const installed = _readInstalledSkills(root);
1831
+ if (installed.length) {
1832
+ const qTokens = _tokenize(String(latestRow.request));
1833
+ const ranked = installed.map(s => ({
1834
+ ...s, score: _jaccard(qTokens, _tokenize(s.name + ' ' + s.description))
1835
+ })).filter(s => s.score > 0).sort((a, b) => b.score - a.score).slice(0, 3);
1836
+ if (ranked.length) {
1837
+ const isTty = process.stdout && process.stdout.isTTY;
1838
+ const grn = s => isTty ? `\x1b[32m${s}\x1b[0m` : s;
1839
+ const dim = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
1840
+ log(grn(`## 🎯 현재 task와 매칭되는 skill 자동 추천 (1.9.67) — 키워드 "${keyword}"`));
1841
+ for (const r of ranked) {
1842
+ log(dim(` • [${r.score.toFixed(2)}] ${r.id} — ${(r.description || '').slice(0, 60)}`));
1843
+ }
1844
+ log(dim(` → 전체: leerness skill match "${String(latestRow.request).slice(0, 60)}"`));
1845
+ log('');
1846
+ }
1847
+ }
1848
+ } catch {}
1849
+ }
1818
1850
  }
1819
1851
  }
1820
1852
  } catch {}
@@ -3164,14 +3196,14 @@ function _banner(opts = {}) {
3164
3196
  lines.push('');
3165
3197
  for (const ln of lines) log(ln);
3166
3198
  if (opts.quickStart) {
3167
- log(C.bold(C.cyan(' ✨ 빠른 시작 (1.9.57+ 워크플로)')));
3199
+ log(C.bold(C.cyan(' ✨ 빠른 시작 (1.9.67+ 워크플로)')));
3168
3200
  log(' ' + C.green('npx leerness@latest init .') + C.dim(' # 신규 프로젝트 + 외부 AI CLI 설정'));
3169
- log(' ' + C.green('npx leerness handoff .') + C.dim(' # 컨텍스트 적재 + 과거 lessons 자동 재상기'));
3201
+ log(' ' + C.green('npx leerness handoff .') + C.dim(' # 컨텍스트 + lessons 재상기 + 매칭 skill 자동 추천'));
3170
3202
  log(' ' + C.green('npx leerness verify-claim T-0001 --run-tests') + C.dim(' # AI 거짓 완료 자동 검증'));
3171
3203
  log(' ' + C.green('npx leerness session close .') + C.dim(' # 마감 + 다음 라운드 추천 (default)'));
3172
3204
  log('');
3173
3205
  log(C.bold(C.cyan(' 🤖 메인 에이전트 (Claude/Cursor/Copilot)용')));
3174
- log(' ' + C.green('npx leerness mcp serve') + C.dim(' # MCP 서버 — 12 도구 노출'));
3206
+ log(' ' + C.green('npx leerness mcp serve') + C.dim(' # MCP 서버 — 13 도구 노출 (task_export 포함)'));
3175
3207
  log(' ' + C.green('npx leerness agents bench "<task>"') + C.dim(' # 3 CLI 동시 비교'));
3176
3208
  log('');
3177
3209
  }
@@ -5545,10 +5577,9 @@ function lessonsCmd(root) {
5545
5577
  }
5546
5578
  log(`# Lessons --auto (1.9.54): 추출 키워드 "${query}"`);
5547
5579
  }
5548
- // 1.9.65: 인덱스 캐시 활용 (decisions/evidence split 1회만)
5580
+ // 1.9.65/67: 인덱스 캐시 활용 (decisions/evidence/task-log split 1회만)
5549
5581
  const _lidx = _loadLessonsIndex(root);
5550
5582
  const decisions = exists(decisionsPath(root)) ? read(decisionsPath(root)) : '';
5551
- const tlog = exists(taskLogPath(root)) ? read(taskLogPath(root)) : '';
5552
5583
  const handoff = exists(handoffPath(root)) ? read(handoffPath(root)) : '';
5553
5584
  const lessons = [];
5554
5585
  // decisions: ### 블록 전체 (1.9.14: 코드블록/Template 제외)
@@ -5563,11 +5594,9 @@ function lessonsCmd(root) {
5563
5594
  lessons.push({ source: 'review-evidence.md', title: e.title, block: e.block });
5564
5595
  }
5565
5596
  }
5566
- // task-log: 실패 키워드 라인
5567
- for (const line of tlog.split('\n')) {
5568
- if (line.length > 4 && /✗|\bfail|롤백|재발|incomplete|버그/i.test(line)) {
5569
- lessons.push({ source: 'task-log.md', title: line.replace(/^[-*]\s*/, '').slice(0, 80), block: line });
5570
- }
5597
+ // task-log: 실패 키워드 라인 (1.9.67: 인덱스 재활용)
5598
+ for (const t of (_lidx.taskLogFails || [])) {
5599
+ lessons.push({ source: 'task-log.md', title: t.title, block: t.block });
5571
5600
  }
5572
5601
  // handoff: 미완료/블로커 항목
5573
5602
  if (handoff) {
@@ -6162,16 +6191,19 @@ function driftCheckCmd(root, opts = {}) {
6162
6191
  }
6163
6192
 
6164
6193
  // 1.9.65: lessons blocks 인덱스 — evidence/decisions 파일 read + split을 1회로
6165
- // key: root { evidenceMtime, decisionsMtime, evidence: [{title, block}], decisions: [{title, block}] }
6194
+ // 1.9.67: task-log.md 실패 라인도 인덱스에 포함 (mtime 기반 invalidation)
6195
+ // key: root → { evidenceMtime, decisionsMtime, taskLogMtime, evidence/decisions/taskLogFails: [{title, block}] }
6166
6196
  const _LESSONS_INDEX_CACHE = new Map();
6167
6197
  function _loadLessonsIndex(root) {
6168
6198
  const ep = evidencePath(root);
6169
6199
  const dp = decisionsPath(root);
6200
+ const tp = taskLogPath(root);
6170
6201
  const em = exists(ep) ? (() => { try { return fs.statSync(ep).mtimeMs; } catch { return 0; } })() : 0;
6171
6202
  const dm = exists(dp) ? (() => { try { return fs.statSync(dp).mtimeMs; } catch { return 0; } })() : 0;
6203
+ const tm = exists(tp) ? (() => { try { return fs.statSync(tp).mtimeMs; } catch { return 0; } })() : 0;
6172
6204
  const cacheKey = absRoot(root);
6173
6205
  const cached = _LESSONS_INDEX_CACHE.get(cacheKey);
6174
- if (cached && cached.evidenceMtime === em && cached.decisionsMtime === dm) return cached;
6206
+ if (cached && cached.evidenceMtime === em && cached.decisionsMtime === dm && cached.taskLogMtime === tm) return cached;
6175
6207
  const evidence = [];
6176
6208
  if (em) {
6177
6209
  const txt = read(ep);
@@ -6190,7 +6222,17 @@ function _loadLessonsIndex(root) {
6190
6222
  if (t) decisions.push({ title: t[1].trim(), block });
6191
6223
  }
6192
6224
  }
6193
- const idx = { evidenceMtime: em, decisionsMtime: dm, evidence, decisions };
6225
+ // 1.9.67: task-log.md 라인 실패/롤백 표지가 있는 라인만 인덱스
6226
+ const taskLogFails = [];
6227
+ if (tm) {
6228
+ const txt = read(tp);
6229
+ for (const line of txt.split('\n')) {
6230
+ if (line.length > 4 && /✗|\bfail|롤백|재발|incomplete|버그/i.test(line)) {
6231
+ taskLogFails.push({ title: line.replace(/^[-*]\s*/, '').slice(0, 100), block: line });
6232
+ }
6233
+ }
6234
+ }
6235
+ const idx = { evidenceMtime: em, decisionsMtime: dm, taskLogMtime: tm, evidence, decisions, taskLogFails };
6194
6236
  _LESSONS_INDEX_CACHE.set(cacheKey, idx);
6195
6237
  return idx;
6196
6238
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.66",
3
+ "version": "1.9.67",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",