leerness 1.9.66 → 1.9.68
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 +58 -0
- package/README.md +4 -2
- package/bin/harness.js +90 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.68 — 2026-05-19
|
|
4
|
+
|
|
5
|
+
**`skill match` rolling history 자동 누적 + 종합 회귀**.
|
|
6
|
+
|
|
7
|
+
### Added — skill match rolling history (default ON)
|
|
8
|
+
- `leerness skill match <query>` 호출 시 결과를 `.harness/skill-suggestions.md`에 append 누적.
|
|
9
|
+
- frontmatter: `leernessRole: skill-suggestions`, `readWhen: ['skill 결정 전', '세션 시작']`.
|
|
10
|
+
- 형식:
|
|
11
|
+
```
|
|
12
|
+
## YYYY-MM-DD HH:MM:SS — query "<keyword>"
|
|
13
|
+
- Algorithm: jaccard|embedding
|
|
14
|
+
- Top N matches:
|
|
15
|
+
- [점수] skill-id — description
|
|
16
|
+
```
|
|
17
|
+
- AI 에이전트가 같은 키워드를 반복 검색하지 않고 이력 참조 가능.
|
|
18
|
+
- 끄기: `--no-save` 또는 `LEERNESS_NO_SKILL_HISTORY=1`.
|
|
19
|
+
|
|
20
|
+
### Updated
|
|
21
|
+
- `_banner` quickStart: 1.9.68 안내 라인 추가.
|
|
22
|
+
|
|
23
|
+
### Verified — 종합 회귀 + 성능 측정
|
|
24
|
+
- stress-v14 (1.9.68 + 1.9.43~67 누적 회귀 + 성능 벤치마크) — 모든 시나리오 PASS.
|
|
25
|
+
- 이전 중요 기능 12종 정상 동작 검증:
|
|
26
|
+
- MCP 13 도구 / drift check / benchmark scenario / skill suggest / lessons --auto
|
|
27
|
+
- session close --suggest default / audit --strict / install 별칭 / task export
|
|
28
|
+
- handoff 자동 skill 추천 (1.9.67) / listAllSkills 캐시 (1.9.66) / usage-stats 캐시 (1.9.65)
|
|
29
|
+
- 성능 (warm-up 적용): status / handoff / drift / audit / skill list / skill match.
|
|
30
|
+
- e2e 회귀: 219/219 PASS 유지.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 1.9.67 — 2026-05-19
|
|
35
|
+
|
|
36
|
+
**handoff 자동 skill 추천 default ON + lessons 인덱스에 task-log 통합**.
|
|
37
|
+
|
|
38
|
+
### Added — handoff 자동 skill match (default ON)
|
|
39
|
+
- 매 `leerness handoff` 시 **현재 in-progress task와 매칭되는 설치된 skill을 자동 추천** (점수 + skill id + description 미리보기).
|
|
40
|
+
- 1.9.45의 `LEERNESS_SKILL_AUTO_DISCOVER=1` opt-in 환경변수 의존성 제거 → default 활성.
|
|
41
|
+
- 끄기: `--no-skill-suggest` 또는 `LEERNESS_NO_SKILL_SUGGEST=1`.
|
|
42
|
+
- 매칭 알고리즘: `_jaccard(task.request_tokens, skill.name+description_tokens)`, top 3.
|
|
43
|
+
- 매칭 점수 0이면 출력 안 함 (잡음 최소화).
|
|
44
|
+
|
|
45
|
+
### Improved — lessons 인덱스 확장
|
|
46
|
+
- `_loadLessonsIndex`에 **task-log.md 실패 라인** 추가 (mtime 기반 invalidation).
|
|
47
|
+
- `_lidx.taskLogFails: [{title, block}]` 새 필드.
|
|
48
|
+
- handoff lessons 자동 재상기에서 task-log fuzzy 매칭도 가능.
|
|
49
|
+
- `leerness lessons` 명령도 같은 인덱스 사용 (split 1회).
|
|
50
|
+
|
|
51
|
+
### Updated
|
|
52
|
+
- `_banner` quickStart: "13 도구 노출 (task_export 포함)" + "매칭 skill 자동 추천" 안내.
|
|
53
|
+
- `.harness/session-workflow.md` 템플릿: 1.9.67 라인 추가.
|
|
54
|
+
|
|
55
|
+
### Verified
|
|
56
|
+
- stress-v13 (1.9.67 검증) — handoff skill match default + --no-skill-suggest + lessons task-log fuzzy.
|
|
57
|
+
- e2e 회귀: 219/219 PASS 유지.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
3
61
|
## 1.9.66 — 2026-05-19
|
|
4
62
|
|
|
5
63
|
**성능 최적화 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.68 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.68** — **`skill match` 결과 → `.harness/skill-suggestions.md` rolling history 자동 누적** (AI가 다음 세션에 이전 추천 참조 가능). `--no-save` / `LEERNESS_NO_SKILL_HISTORY=1`로 끄기.
|
|
437
|
+
- **1.9.67** — **handoff 자동 skill 추천 default ON** (jaccard 매칭) + lessons 인덱스에 task-log.md 실패 라인 통합 (회수 범위 확장).
|
|
436
438
|
- **1.9.66** — **성능 최적화 2차 + MCP 13번째 도구**. `listAllSkills` 메모리 캐시 (skill list/info/match/discover/suggest 공유) + MCP `leerness_task_export` 추가 (TodoWrite 양방향 sync 외부 노출).
|
|
437
439
|
- **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
440
|
- **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.68';
|
|
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,15 @@ 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.68+ 워크플로)')));
|
|
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 자동 추천'));
|
|
3202
|
+
log(' ' + C.green('npx leerness skill match "<query>"') + C.dim(' # 매칭 skill + rolling history 자동 누적 (1.9.68)'));
|
|
3170
3203
|
log(' ' + C.green('npx leerness verify-claim T-0001 --run-tests') + C.dim(' # AI 거짓 완료 자동 검증'));
|
|
3171
3204
|
log(' ' + C.green('npx leerness session close .') + C.dim(' # 마감 + 다음 라운드 추천 (default)'));
|
|
3172
3205
|
log('');
|
|
3173
3206
|
log(C.bold(C.cyan(' 🤖 메인 에이전트 (Claude/Cursor/Copilot)용')));
|
|
3174
|
-
log(' ' + C.green('npx leerness mcp serve') + C.dim(' # MCP 서버 —
|
|
3207
|
+
log(' ' + C.green('npx leerness mcp serve') + C.dim(' # MCP 서버 — 13 도구 노출 (task_export 포함)'));
|
|
3175
3208
|
log(' ' + C.green('npx leerness agents bench "<task>"') + C.dim(' # 3 CLI 동시 비교'));
|
|
3176
3209
|
log('');
|
|
3177
3210
|
}
|
|
@@ -5545,10 +5578,9 @@ function lessonsCmd(root) {
|
|
|
5545
5578
|
}
|
|
5546
5579
|
log(`# Lessons --auto (1.9.54): 추출 키워드 "${query}"`);
|
|
5547
5580
|
}
|
|
5548
|
-
// 1.9.65: 인덱스 캐시 활용 (decisions/evidence split 1회만)
|
|
5581
|
+
// 1.9.65/67: 인덱스 캐시 활용 (decisions/evidence/task-log split 1회만)
|
|
5549
5582
|
const _lidx = _loadLessonsIndex(root);
|
|
5550
5583
|
const decisions = exists(decisionsPath(root)) ? read(decisionsPath(root)) : '';
|
|
5551
|
-
const tlog = exists(taskLogPath(root)) ? read(taskLogPath(root)) : '';
|
|
5552
5584
|
const handoff = exists(handoffPath(root)) ? read(handoffPath(root)) : '';
|
|
5553
5585
|
const lessons = [];
|
|
5554
5586
|
// decisions: ### 블록 전체 (1.9.14: 코드블록/Template 제외)
|
|
@@ -5563,11 +5595,9 @@ function lessonsCmd(root) {
|
|
|
5563
5595
|
lessons.push({ source: 'review-evidence.md', title: e.title, block: e.block });
|
|
5564
5596
|
}
|
|
5565
5597
|
}
|
|
5566
|
-
// task-log: 실패 키워드 라인
|
|
5567
|
-
for (const
|
|
5568
|
-
|
|
5569
|
-
lessons.push({ source: 'task-log.md', title: line.replace(/^[-*]\s*/, '').slice(0, 80), block: line });
|
|
5570
|
-
}
|
|
5598
|
+
// task-log: 실패 키워드 라인 (1.9.67: 인덱스 재활용)
|
|
5599
|
+
for (const t of (_lidx.taskLogFails || [])) {
|
|
5600
|
+
lessons.push({ source: 'task-log.md', title: t.title, block: t.block });
|
|
5571
5601
|
}
|
|
5572
5602
|
// handoff: 미완료/블로커 항목
|
|
5573
5603
|
if (handoff) {
|
|
@@ -6162,16 +6192,19 @@ function driftCheckCmd(root, opts = {}) {
|
|
|
6162
6192
|
}
|
|
6163
6193
|
|
|
6164
6194
|
// 1.9.65: lessons blocks 인덱스 — evidence/decisions 파일 read + split을 1회로
|
|
6165
|
-
//
|
|
6195
|
+
// 1.9.67: task-log.md 실패 라인도 인덱스에 포함 (mtime 기반 invalidation)
|
|
6196
|
+
// key: root → { evidenceMtime, decisionsMtime, taskLogMtime, evidence/decisions/taskLogFails: [{title, block}] }
|
|
6166
6197
|
const _LESSONS_INDEX_CACHE = new Map();
|
|
6167
6198
|
function _loadLessonsIndex(root) {
|
|
6168
6199
|
const ep = evidencePath(root);
|
|
6169
6200
|
const dp = decisionsPath(root);
|
|
6201
|
+
const tp = taskLogPath(root);
|
|
6170
6202
|
const em = exists(ep) ? (() => { try { return fs.statSync(ep).mtimeMs; } catch { return 0; } })() : 0;
|
|
6171
6203
|
const dm = exists(dp) ? (() => { try { return fs.statSync(dp).mtimeMs; } catch { return 0; } })() : 0;
|
|
6204
|
+
const tm = exists(tp) ? (() => { try { return fs.statSync(tp).mtimeMs; } catch { return 0; } })() : 0;
|
|
6172
6205
|
const cacheKey = absRoot(root);
|
|
6173
6206
|
const cached = _LESSONS_INDEX_CACHE.get(cacheKey);
|
|
6174
|
-
if (cached && cached.evidenceMtime === em && cached.decisionsMtime === dm) return cached;
|
|
6207
|
+
if (cached && cached.evidenceMtime === em && cached.decisionsMtime === dm && cached.taskLogMtime === tm) return cached;
|
|
6175
6208
|
const evidence = [];
|
|
6176
6209
|
if (em) {
|
|
6177
6210
|
const txt = read(ep);
|
|
@@ -6190,7 +6223,17 @@ function _loadLessonsIndex(root) {
|
|
|
6190
6223
|
if (t) decisions.push({ title: t[1].trim(), block });
|
|
6191
6224
|
}
|
|
6192
6225
|
}
|
|
6193
|
-
|
|
6226
|
+
// 1.9.67: task-log.md 라인 중 실패/롤백 표지가 있는 라인만 인덱스
|
|
6227
|
+
const taskLogFails = [];
|
|
6228
|
+
if (tm) {
|
|
6229
|
+
const txt = read(tp);
|
|
6230
|
+
for (const line of txt.split('\n')) {
|
|
6231
|
+
if (line.length > 4 && /✗|\bfail|롤백|재발|incomplete|버그/i.test(line)) {
|
|
6232
|
+
taskLogFails.push({ title: line.replace(/^[-*]\s*/, '').slice(0, 100), block: line });
|
|
6233
|
+
}
|
|
6234
|
+
}
|
|
6235
|
+
}
|
|
6236
|
+
const idx = { evidenceMtime: em, decisionsMtime: dm, taskLogMtime: tm, evidence, decisions, taskLogFails };
|
|
6194
6237
|
_LESSONS_INDEX_CACHE.set(cacheKey, idx);
|
|
6195
6238
|
return idx;
|
|
6196
6239
|
}
|
|
@@ -6778,6 +6821,13 @@ async function skillMatchCmd(root, query) {
|
|
|
6778
6821
|
})).sort((a, b) => b.score - a.score);
|
|
6779
6822
|
}
|
|
6780
6823
|
const top = ranked.filter(r => r.score > 0).slice(0, 5);
|
|
6824
|
+
// 1.9.68: rolling history 자동 누적 (.harness/skill-suggestions.md) — default ON
|
|
6825
|
+
// 끄기: --no-save 또는 LEERNESS_NO_SKILL_HISTORY=1
|
|
6826
|
+
if (!has('--no-save') && process.env.LEERNESS_NO_SKILL_HISTORY !== '1') {
|
|
6827
|
+
try {
|
|
6828
|
+
_appendSkillSuggestion(root, { query, useEmbedding, top });
|
|
6829
|
+
} catch {}
|
|
6830
|
+
}
|
|
6781
6831
|
if (has('--json')) {
|
|
6782
6832
|
log(JSON.stringify({ query, total: skills.length, matched: top.length, top: top.map(({ dir, ...rest }) => rest) }, null, 2));
|
|
6783
6833
|
return;
|
|
@@ -6797,6 +6847,32 @@ async function skillMatchCmd(root, query) {
|
|
|
6797
6847
|
}
|
|
6798
6848
|
log('');
|
|
6799
6849
|
log(`💡 사용: \`cat ${rel(root, top[0].dir)}/SKILL.md\` 또는 메인 에이전트가 이 skill 본문을 참고`);
|
|
6850
|
+
log(`📒 자동 누적: .harness/skill-suggestions.md (--no-save로 끄기)`);
|
|
6851
|
+
}
|
|
6852
|
+
|
|
6853
|
+
// 1.9.68: skill match rolling history append (.harness/skill-suggestions.md)
|
|
6854
|
+
// AI가 다음 세션에 이전 추천을 참조 가능 — readWhen: '세션 시작', 'skill 결정 전'
|
|
6855
|
+
function _appendSkillSuggestion(root, { query, useEmbedding, top }) {
|
|
6856
|
+
const p = path.join(absRoot(root), '.harness', 'skill-suggestions.md');
|
|
6857
|
+
if (!exists(p)) {
|
|
6858
|
+
// 신규 파일 — frontmatter + 안내
|
|
6859
|
+
const fm = `---\nleernessRole: skill-suggestions\nreadWhen:\n - skill 결정 전\n - 세션 시작\nupdateWhen:\n - leerness skill match 호출 시 자동 누적 (1.9.68)\ndoNotStore:\n - 실제 토큰\n - 비밀번호\n - 운영 쿠키\n - 민감한 개인정보 원문\n---\n<!-- leerness:managed -->\n# Skill Suggestions (Rolling History)\n\n매 \`leerness skill match\` 호출이 여기 누적됩니다. AI 에이전트는 다음 세션에 같은 키워드를 다시 검색하지 말고 이력을 먼저 참조하세요.\n\n`;
|
|
6860
|
+
mkdirp(path.dirname(p));
|
|
6861
|
+
writeUtf8(p, fm);
|
|
6862
|
+
}
|
|
6863
|
+
const algo = useEmbedding ? 'embedding' : 'jaccard';
|
|
6864
|
+
const ts = new Date().toISOString();
|
|
6865
|
+
let block = `\n## ${ts.slice(0, 19).replace('T', ' ')} — query "${(query || '').slice(0, 80)}"\n`;
|
|
6866
|
+
block += `- Algorithm: ${algo}\n`;
|
|
6867
|
+
if (!top.length) {
|
|
6868
|
+
block += `- Matched: 0 — 다른 키워드 또는 \`leerness skill discover\` 권장\n`;
|
|
6869
|
+
} else {
|
|
6870
|
+
block += `- Top ${top.length} matches:\n`;
|
|
6871
|
+
for (const r of top) {
|
|
6872
|
+
block += ` - [${r.score.toFixed(2)}] ${r.id} — ${(r.description || '').slice(0, 80)}\n`;
|
|
6873
|
+
}
|
|
6874
|
+
}
|
|
6875
|
+
append(p, block);
|
|
6800
6876
|
}
|
|
6801
6877
|
|
|
6802
6878
|
// 1.9.43: skill export-all — 모든 자체 skill을 agentskills.io 표준 SKILL.md로 일괄 export
|