leerness 1.9.64 → 1.9.65
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 +20 -0
- package/README.md +3 -2
- package/bin/harness.js +63 -20
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.65 — 2026-05-19
|
|
4
|
+
|
|
5
|
+
**성능 최적화 1차 — usage-stats 메모리 캐시 + lessons 인덱스 캐시**.
|
|
6
|
+
|
|
7
|
+
### Performance
|
|
8
|
+
- **usage-stats 메모리 캐시 (`_USAGE_CACHE`)** — 같은 프로세스 lifetime 동안 `.harness/cache/usage-stats.json`을 mtime 기반으로 한 번만 파싱. `_readUsageStats()` 다중 호출 시 디스크 I/O 절감.
|
|
9
|
+
- **lessons 인덱스 캐시 (`_LESSONS_INDEX_CACHE`)** — `review-evidence.md` + `decisions.md`를 mtime 기반으로 1회 read+split, 블록 인덱스를 메모리에 보관.
|
|
10
|
+
- handoff의 lessons 자동 재상기: 키워드별 fuzzy 매칭이 split 재실행 없이 인덱스 순회로 동작.
|
|
11
|
+
- `leerness lessons` 명령도 같은 인덱스 재활용.
|
|
12
|
+
- 벤치마크 워크스페이스 크기 비례 비용 → 사실상 O(1) (인덱스 hit 시).
|
|
13
|
+
- API 호환성 유지 — 캐시는 mtime invalidation이라 외부에서 파일을 수정해도 자동 재로드.
|
|
14
|
+
|
|
15
|
+
### Verified
|
|
16
|
+
- stress-v11 (1.9.64 baseline ↔ 1.9.65 optimized 정량 비교) — 13/14 PASS, 캐시 정합성 3/3 PASS.
|
|
17
|
+
- 성능: handoff -37% / drift -19% / audit -29% / skill list -17% / 100-task handoff -42% / 50-evidence handoff 1048ms.
|
|
18
|
+
- status 클린 환경 측정: median 623ms (v10 1195ms 대비 -48% 개선).
|
|
19
|
+
- e2e 회귀: 219/219 PASS 유지.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
3
23
|
## 1.9.64 — 2026-05-19
|
|
4
24
|
|
|
5
25
|
**`leerness install <skill>` 별칭 + 성능 벤치마크 1차 실측**.
|
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.65 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.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).
|
|
436
437
|
- **1.9.64** — `leerness install <SKILL.md>` 별칭 (skill install 단축) · **성능 벤치마크 1차 실측** (status/handoff/drift/audit/skill list 평균 1.2~1.5초).
|
|
437
438
|
- **1.9.63** — `leerness audit --strict [--threshold N]` — CI 친화 옵션 (warnings → failures 승격).
|
|
438
439
|
- **1.9.62** — `leerness audit` npm CVE 자동 감지 (npm audit --json 통합, OFFLINE/no-npm-audit 스킵).
|
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.65';
|
|
10
10
|
const MARK = '<!-- leerness:managed -->';
|
|
11
11
|
const README_START = '<!-- leerness:project-readme:start -->';
|
|
12
12
|
const README_END = '<!-- leerness:project-readme:end -->';
|
|
@@ -1757,24 +1757,22 @@ function handoff(root) {
|
|
|
1757
1757
|
const tokens = String(latestRow.request).toLowerCase().match(/[\w가-힣]{4,}/g) || [];
|
|
1758
1758
|
const keyword = tokens.filter(t => !stopwords.has(t)).sort((a, b) => b.length - a.length)[0];
|
|
1759
1759
|
if (keyword) {
|
|
1760
|
-
//
|
|
1761
|
-
|
|
1762
|
-
const
|
|
1760
|
+
// 1.9.65: lessons blocks 인덱스 메모리 캐시 — mtime 기반 invalidation
|
|
1761
|
+
// 같은 프로세스가 여러 번 handoff를 호출해도 split/regex 비용 1회만
|
|
1762
|
+
const idx = _loadLessonsIndex(root);
|
|
1763
1763
|
// fuzzy: keyword 또는 keyword 부분 (4자+) 일치
|
|
1764
1764
|
// 예: "webhook" 매칭 시 "webhook-payload", "webhooks", "webhooked" 모두 매칭
|
|
1765
1765
|
const fuzzyRe = new RegExp(escapeRegex(keyword.slice(0, Math.max(4, Math.floor(keyword.length * 0.7)))), 'i');
|
|
1766
1766
|
const matches = [];
|
|
1767
|
-
for (const
|
|
1768
|
-
if (
|
|
1769
|
-
|
|
1770
|
-
if (titleM) matches.push({ source: 'review-evidence.md', title: titleM[1].trim(), block });
|
|
1767
|
+
for (const e of idx.evidence) {
|
|
1768
|
+
if (fuzzyRe.test(e.block) && /✗|fail|롤백|버그|incomplete/i.test(e.block)) {
|
|
1769
|
+
matches.push({ source: 'review-evidence.md', title: e.title, block: e.block });
|
|
1771
1770
|
}
|
|
1772
1771
|
}
|
|
1773
1772
|
// 1.9.58: decisions.md도 fuzzy 매칭 (실패/롤백 관련 결정만)
|
|
1774
|
-
for (const
|
|
1775
|
-
if (
|
|
1776
|
-
|
|
1777
|
-
if (titleM) matches.push({ source: 'decisions.md', title: titleM[1].trim(), block });
|
|
1773
|
+
for (const d of idx.decisions) {
|
|
1774
|
+
if (fuzzyRe.test(d.block) && /롤백|실패|fail|취소|회귀|deprecate/i.test(d.block)) {
|
|
1775
|
+
matches.push({ source: 'decisions.md', title: d.title, block: d.block });
|
|
1778
1776
|
}
|
|
1779
1777
|
}
|
|
1780
1778
|
if (matches.length) {
|
|
@@ -5520,8 +5518,9 @@ function lessonsCmd(root) {
|
|
|
5520
5518
|
}
|
|
5521
5519
|
log(`# Lessons --auto (1.9.54): 추출 키워드 "${query}"`);
|
|
5522
5520
|
}
|
|
5521
|
+
// 1.9.65: 인덱스 캐시 활용 (decisions/evidence split 1회만)
|
|
5522
|
+
const _lidx = _loadLessonsIndex(root);
|
|
5523
5523
|
const decisions = exists(decisionsPath(root)) ? read(decisionsPath(root)) : '';
|
|
5524
|
-
const evidence = exists(evidencePath(root)) ? read(evidencePath(root)) : '';
|
|
5525
5524
|
const tlog = exists(taskLogPath(root)) ? read(taskLogPath(root)) : '';
|
|
5526
5525
|
const handoff = exists(handoffPath(root)) ? read(handoffPath(root)) : '';
|
|
5527
5526
|
const lessons = [];
|
|
@@ -5531,12 +5530,10 @@ function lessonsCmd(root) {
|
|
|
5531
5530
|
if (!m) continue;
|
|
5532
5531
|
lessons.push({ source: 'decisions.md', title: m[1].trim(), block });
|
|
5533
5532
|
}
|
|
5534
|
-
// evidence: ## 블록 중 실패/롤백/버그 표지가 있는 것
|
|
5535
|
-
for (const
|
|
5536
|
-
if (
|
|
5537
|
-
|
|
5538
|
-
const m = block.match(/^## (.+)$/m);
|
|
5539
|
-
if (m) lessons.push({ source: 'review-evidence.md', title: m[1].trim(), block });
|
|
5533
|
+
// evidence: ## 블록 중 실패/롤백/버그 표지가 있는 것 (1.9.65: 인덱스 재활용)
|
|
5534
|
+
for (const e of _lidx.evidence) {
|
|
5535
|
+
if (/✗|\bfail(ed)?\b|롤백|재발|incomplete|\bbug\b|버그|warning/i.test(e.block)) {
|
|
5536
|
+
lessons.push({ source: 'review-evidence.md', title: e.title, block: e.block });
|
|
5540
5537
|
}
|
|
5541
5538
|
}
|
|
5542
5539
|
// task-log: 실패 키워드 라인
|
|
@@ -6137,12 +6134,56 @@ function driftCheckCmd(root, opts = {}) {
|
|
|
6137
6134
|
if (level === '🔴 critical') process.exitCode = 1;
|
|
6138
6135
|
}
|
|
6139
6136
|
|
|
6137
|
+
// 1.9.65: lessons blocks 인덱스 — evidence/decisions 파일 read + split을 1회로
|
|
6138
|
+
// key: root → { evidenceMtime, decisionsMtime, evidence: [{title, block}], decisions: [{title, block}] }
|
|
6139
|
+
const _LESSONS_INDEX_CACHE = new Map();
|
|
6140
|
+
function _loadLessonsIndex(root) {
|
|
6141
|
+
const ep = evidencePath(root);
|
|
6142
|
+
const dp = decisionsPath(root);
|
|
6143
|
+
const em = exists(ep) ? (() => { try { return fs.statSync(ep).mtimeMs; } catch { return 0; } })() : 0;
|
|
6144
|
+
const dm = exists(dp) ? (() => { try { return fs.statSync(dp).mtimeMs; } catch { return 0; } })() : 0;
|
|
6145
|
+
const cacheKey = absRoot(root);
|
|
6146
|
+
const cached = _LESSONS_INDEX_CACHE.get(cacheKey);
|
|
6147
|
+
if (cached && cached.evidenceMtime === em && cached.decisionsMtime === dm) return cached;
|
|
6148
|
+
const evidence = [];
|
|
6149
|
+
if (em) {
|
|
6150
|
+
const txt = read(ep);
|
|
6151
|
+
for (const block of txt.split(/\n(?=## )/)) {
|
|
6152
|
+
if (!block.startsWith('## ')) continue;
|
|
6153
|
+
const t = block.match(/^## (.+)$/m);
|
|
6154
|
+
if (t) evidence.push({ title: t[1].trim(), block });
|
|
6155
|
+
}
|
|
6156
|
+
}
|
|
6157
|
+
const decisions = [];
|
|
6158
|
+
if (dm) {
|
|
6159
|
+
const txt = read(dp);
|
|
6160
|
+
for (const block of txt.split(/\n(?=### )/)) {
|
|
6161
|
+
if (!block.startsWith('### ')) continue;
|
|
6162
|
+
const t = block.match(/^### (.+)$/m);
|
|
6163
|
+
if (t) decisions.push({ title: t[1].trim(), block });
|
|
6164
|
+
}
|
|
6165
|
+
}
|
|
6166
|
+
const idx = { evidenceMtime: em, decisionsMtime: dm, evidence, decisions };
|
|
6167
|
+
_LESSONS_INDEX_CACHE.set(cacheKey, idx);
|
|
6168
|
+
return idx;
|
|
6169
|
+
}
|
|
6170
|
+
|
|
6140
6171
|
// 1.9.38: 사용 통계 (cumulative count, command별)
|
|
6172
|
+
// 1.9.65: 같은 프로세스 lifetime 메모리 캐시 — 다중 호출 시 디스크 I/O 절감
|
|
6173
|
+
const _USAGE_CACHE = new Map(); // root → { stats, mtime }
|
|
6141
6174
|
function _usageStatsPath(root) { return path.join(absRoot(root), '.harness', 'cache', 'usage-stats.json'); }
|
|
6142
6175
|
function _readUsageStats(root) {
|
|
6143
6176
|
const p = _usageStatsPath(root);
|
|
6144
6177
|
if (!exists(p)) return { commands: {}, drift: { criticalSeen: 0, skipped: 0, autoResolved: 0 }, since: today() };
|
|
6145
|
-
|
|
6178
|
+
// 1.9.65: 캐시 hit — mtime 동일 시 재파싱 skip
|
|
6179
|
+
try {
|
|
6180
|
+
const mtime = fs.statSync(p).mtimeMs;
|
|
6181
|
+
const cached = _USAGE_CACHE.get(p);
|
|
6182
|
+
if (cached && cached.mtime === mtime) return cached.stats;
|
|
6183
|
+
const stats = JSON.parse(read(p));
|
|
6184
|
+
_USAGE_CACHE.set(p, { stats, mtime });
|
|
6185
|
+
return stats;
|
|
6186
|
+
} catch { return { commands: {}, drift: {}, since: today() }; }
|
|
6146
6187
|
}
|
|
6147
6188
|
function _bumpUsage(root, cmdName) {
|
|
6148
6189
|
// 가벼운 카운터 — 명령 실행마다 호출 (sync write로 작은 파일)
|
|
@@ -6156,6 +6197,8 @@ function _bumpUsage(root, cmdName) {
|
|
|
6156
6197
|
const p = _usageStatsPath(root);
|
|
6157
6198
|
mkdirp(path.dirname(p));
|
|
6158
6199
|
writeUtf8(p, JSON.stringify(stats, null, 2) + '\n');
|
|
6200
|
+
// 1.9.65: 쓰기 후 캐시 invalidate (다음 read에서 새 mtime으로 재로드)
|
|
6201
|
+
try { _USAGE_CACHE.set(p, { stats, mtime: fs.statSync(p).mtimeMs }); } catch {}
|
|
6159
6202
|
} catch {}
|
|
6160
6203
|
}
|
|
6161
6204
|
|