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 +27 -0
- package/README.md +3 -2
- package/bin/harness.js +56 -14
- package/package.json +1 -1
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
|
-
[](https://www.npmjs.com/package/leerness) [](https://www.npmjs.com/package/leerness) []() []() []()
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
╔══════════════════════════════════════════════════════════════╗
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
║ ██║ ██╔══╝ ██╔══╝ ██╔══██╗██║╚██╗██║██╔══╝ ╚════██║ ║
|
|
13
13
|
║ ███████╗███████╗███████╗██║ ██║██║ ╚████║███████╗███████║ ║
|
|
14
14
|
║ ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝ ║
|
|
15
|
-
║ v1.9.
|
|
15
|
+
║ v1.9.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.
|
|
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.
|
|
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(' # 컨텍스트
|
|
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 서버 —
|
|
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
|
|
5568
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
}
|