leerness 1.9.57 → 1.9.62
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 +72 -0
- package/README.md +7 -2
- package/bin/harness.js +94 -11
- package/package.json +1 -1
- package/scripts/e2e.js +78 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,77 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.62 — 2026-05-19
|
|
4
|
+
|
|
5
|
+
**`leerness audit`에 npm CVE 자동 감지 통합**.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **`leerness audit`** — package.json 있으면 `npm audit --json` 자동 호출 → CVE 보고:
|
|
9
|
+
- `metadata.vulnerabilities` 파싱 → critical/high/moderate/low 카운트
|
|
10
|
+
- critical/high 발견 시 warnings +2 (가중치)
|
|
11
|
+
- 0건이면 ✓ "npm CVE: 0건"
|
|
12
|
+
- **스킵 조건** (자동): package.json 없음, `LEERNESS_OFFLINE=1`, `--no-npm-audit` 플래그
|
|
13
|
+
|
|
14
|
+
## 1.9.61 — 2026-05-19
|
|
15
|
+
|
|
16
|
+
**MCP server cursor 기반 페이지네이션**.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- **MCP `tools/call` 응답에 cursor 메타 추가** — 50KB 넘는 출력 자동 분할:
|
|
20
|
+
- `result.nextCursor` — 다음 청크 offset (`args._cursor`로 재호출)
|
|
21
|
+
- `result._truncated` — `{ totalLength, returned, hint }` 메타
|
|
22
|
+
- 청크 크기 override 가능: `args._chunkSize` (기본 50000)
|
|
23
|
+
- 사용 예: 100 task 워크스페이스의 handoff → 청크 1 → cursor → 청크 2 → ... 완료
|
|
24
|
+
|
|
25
|
+
## 1.9.60 — 2026-05-19
|
|
26
|
+
|
|
27
|
+
**`leerness task export` — TodoWrite ↔ leerness 양방향 sync 완성**.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- **`leerness task export [--to <file>] [--json]`**:
|
|
31
|
+
- progress-tracker → TodoWrite JSON 형식 변환
|
|
32
|
+
- status 매핑: `done` → `completed`, `in-progress` → `in_progress`, `planned` → `pending`, `dropped` → `cancelled`
|
|
33
|
+
- 필드: `content` / `status` / `activeForm` (TodoWrite 호환)
|
|
34
|
+
- 1.9.38 `task sync --from`(TodoWrite → leerness)과 함께 **양방향 sync 완성**
|
|
35
|
+
|
|
36
|
+
### 검증 (stress-v9)
|
|
37
|
+
- AA1-AA4 (task export + status 매핑 + round-trip) 4/4 PASS
|
|
38
|
+
- BB1-BB3 (MCP cursor 페이지네이션, chunkSize override) 3/3 PASS
|
|
39
|
+
- CC1-CC3 (audit npm CVE, OFFLINE/no-npm-audit/no-package.json 스킵) 3/3 PASS
|
|
40
|
+
- DD1-DD3 (1.9.43~59 누적 회귀) 3/3 PASS
|
|
41
|
+
- **stress-v9: 13/13 PASS** (BB2/BB3 BUG 발견·즉시 패치: chunkSize override 추가), e2e: **216/216 PASS**
|
|
42
|
+
|
|
43
|
+
## 1.9.59 — 2026-05-19
|
|
44
|
+
|
|
45
|
+
**`session close --suggest` default 활성 — 라운드 마감 잊을 단계 없음**.
|
|
46
|
+
|
|
47
|
+
### Changed (호환성 보장)
|
|
48
|
+
- **`leerness session close`** — 1.9.57 `--suggest`를 **default 활성**으로 승격:
|
|
49
|
+
- 라운드 마감 시 자동으로 skill suggest + drift check + usage stats 통합 보고
|
|
50
|
+
- 사용자가 잊지 않도록 default 동작
|
|
51
|
+
- **새 옵션 `--no-suggest`** — 이전 동작으로 복귀
|
|
52
|
+
- **새 env `LEERNESS_NO_SUGGEST=1`** — CI/자동화 환경에서 suggest 강제 비활성
|
|
53
|
+
- `--suggest` 명시 호출도 그대로 동작 (호환성)
|
|
54
|
+
|
|
55
|
+
### 설치 가이드 갱신
|
|
56
|
+
- banner quickStart에서 `--suggest` 명시 제거 (이제 default라 불필요)
|
|
57
|
+
- `.harness/session-workflow.md` Step 6 갱신 — `--no-suggest`로 비활성 가능 명시
|
|
58
|
+
|
|
59
|
+
## 1.9.58 — 2026-05-19
|
|
60
|
+
|
|
61
|
+
**handoff lessons fuzzy 매칭 (어간 변형 + decisions.md 매칭)**.
|
|
62
|
+
|
|
63
|
+
### Added
|
|
64
|
+
- **fuzzy 매칭** — `escapeRegex(keyword.slice(0, max(4, len*0.7)))` 으로 부분 매칭:
|
|
65
|
+
- webhook ↔ webhooks ↔ webhook-payload ↔ webhooked 모두 매칭
|
|
66
|
+
- 한국어 어미 변화도 부분 매칭 (예: "결제" ↔ "결제처리" ↔ "결제검증")
|
|
67
|
+
- **decisions.md 매칭 추가** — 이전엔 review-evidence.md만 → 이제 decisions.md의 *실패/롤백/취소/회귀* 결정도 자동 회수
|
|
68
|
+
|
|
69
|
+
### 검증 (stress-v8)
|
|
70
|
+
- X1-X4 (fuzzy 매칭: 어간/복합어/decisions/false positive 차단) 4/4 PASS
|
|
71
|
+
- Y1-Y4 (session close default suggest + 옵션 호환) 4/4 PASS
|
|
72
|
+
- Z1-Z4 (1.9.43~57 누적 회귀) 4/4 PASS
|
|
73
|
+
- **stress-v8: 12/12 PASS**, e2e: **213/213 PASS**
|
|
74
|
+
|
|
3
75
|
## 1.9.57 — 2026-05-19
|
|
4
76
|
|
|
5
77
|
**`session close --suggest` + 설치 가이드 갱신**.
|
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.62 AI Agent Reliability Harness ║
|
|
16
16
|
║ verify · remember · orchestrate · audit · prevent drift ║
|
|
17
17
|
╚══════════════════════════════════════════════════════════════╝
|
|
18
18
|
```
|
|
@@ -433,6 +433,11 @@ npm test # = node ./scripts/e2e.js
|
|
|
433
433
|
|
|
434
434
|
## 변경 이력 (최근)
|
|
435
435
|
|
|
436
|
+
- **1.9.62** — `leerness audit` npm CVE 자동 감지 (npm audit --json 통합, OFFLINE/no-npm-audit 스킵).
|
|
437
|
+
- **1.9.61** — MCP server cursor 기반 페이지네이션 — `nextCursor` + `_chunkSize` override.
|
|
438
|
+
- **1.9.60** — `leerness task export` — progress-tracker → TodoWrite JSON 형식. **양방향 sync 완성** (1.9.38 sync + 1.9.60 export).
|
|
439
|
+
- **1.9.59** — `session close` **--suggest default 활성** (잊을 단계 없음, `--no-suggest`로 비활성 가능) · 설치 가이드 갱신.
|
|
440
|
+
- **1.9.58** — handoff lessons **fuzzy 매칭** (어간 변형, 복합어, decisions.md 매칭 추가).
|
|
436
441
|
- **1.9.57** — `session close --suggest` (마감 시 skill suggest + drift + 명령 통계 통합 보고) · install banner quickStart + session-workflow.md 갱신 (1.9.56/57 흐름 자동 안내).
|
|
437
442
|
- **1.9.56** — `handoff`에 lessons 자동 재상기 통합 (현재 in-progress task 키워드 매칭 → 과거 실패 자동 표시).
|
|
438
443
|
- **1.9.55** — MCP server에 `leerness_skill_suggest` + `leerness_lessons` 추가 (10 → 12 도구) · lessons --auto의 stopword 확장 (false positive 차단).
|
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.62';
|
|
10
10
|
const MARK = '<!-- leerness:managed -->';
|
|
11
11
|
const README_START = '<!-- leerness:project-readme:start -->';
|
|
12
12
|
const README_END = '<!-- leerness:project-readme:end -->';
|
|
@@ -278,9 +278,10 @@ leerness review <file> --persona security,performance,ux
|
|
|
278
278
|
|
|
279
279
|
## Step 6. 세션 마감 + 인계 + 다음 라운드 추천
|
|
280
280
|
\`\`\`bash
|
|
281
|
-
leerness session close .
|
|
282
|
-
#
|
|
283
|
-
|
|
281
|
+
leerness session close . # 1.9.59+ — --suggest default 활성 (마감 + 다음 라운드 자동)
|
|
282
|
+
leerness session close . --no-suggest # suggest 비활성 (이전 동작)
|
|
283
|
+
|
|
284
|
+
# 분리 호출도 가능:
|
|
284
285
|
leerness skill suggest . # 1.9.53 — 반복 패턴 → 새 skill 후보
|
|
285
286
|
leerness drift check . # 4 신호 + 4 레벨 점검
|
|
286
287
|
leerness audit . --fix # 누락 메타 자동 보강
|
|
@@ -1422,6 +1423,34 @@ function audit(root) {
|
|
|
1422
1423
|
}
|
|
1423
1424
|
}
|
|
1424
1425
|
} catch {}
|
|
1426
|
+
// 1.9.62: package.json 있으면 npm audit --json 자동 호출 → CVE 보고 (opt-out: --no-npm-audit)
|
|
1427
|
+
// 정책: leerness가 외부 호출하지만 사용자 컨텍스트에 이미 npm 설치되어 있음을 가정 (offline 시 자동 스킵)
|
|
1428
|
+
if (exists(path.join(root, 'package.json')) && !has('--no-npm-audit') && process.env.LEERNESS_OFFLINE !== '1') {
|
|
1429
|
+
try {
|
|
1430
|
+
const r = cp.spawnSync('npm', ['audit', '--json'], {
|
|
1431
|
+
cwd: root, encoding: 'utf8', shell: true, timeout: 30000
|
|
1432
|
+
});
|
|
1433
|
+
if (r.stdout) {
|
|
1434
|
+
let j = null;
|
|
1435
|
+
try { j = JSON.parse(r.stdout); } catch {}
|
|
1436
|
+
if (j && j.metadata && j.metadata.vulnerabilities) {
|
|
1437
|
+
const v = j.metadata.vulnerabilities;
|
|
1438
|
+
const total = (v.critical || 0) + (v.high || 0) + (v.moderate || 0) + (v.low || 0);
|
|
1439
|
+
if (total > 0) {
|
|
1440
|
+
warnings++;
|
|
1441
|
+
warn(`npm CVE: ${total}건 (critical=${v.critical||0}, high=${v.high||0}, moderate=${v.moderate||0}, low=${v.low||0})`);
|
|
1442
|
+
log(` → 수정: npm audit fix · 상세: npm audit`);
|
|
1443
|
+
if (v.critical || v.high) {
|
|
1444
|
+
warnings++; // critical/high는 추가 가중
|
|
1445
|
+
warn(` ⚠ critical/high CVE 즉시 대응 권장`);
|
|
1446
|
+
}
|
|
1447
|
+
} else {
|
|
1448
|
+
ok('npm CVE: 0건');
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
} catch {}
|
|
1453
|
+
}
|
|
1425
1454
|
log(`Audit summary: warnings=${warnings} failures=${failures}${fix ? ` fixed=${fixed}` : ''}`);
|
|
1426
1455
|
if (failures) process.exitCode = 1;
|
|
1427
1456
|
}
|
|
@@ -1720,16 +1749,26 @@ function handoff(root) {
|
|
|
1720
1749
|
const tokens = String(latestRow.request).toLowerCase().match(/[\w가-힣]{4,}/g) || [];
|
|
1721
1750
|
const keyword = tokens.filter(t => !stopwords.has(t)).sort((a, b) => b.length - a.length)[0];
|
|
1722
1751
|
if (keyword) {
|
|
1723
|
-
// lessons 검색
|
|
1752
|
+
// lessons 검색 — 1.9.58: fuzzy 매칭 (substring + 어간 변형)
|
|
1724
1753
|
const decisions = exists(decisionsPath(root)) ? read(decisionsPath(root)) : '';
|
|
1725
1754
|
const evidence = exists(evidencePath(root)) ? read(evidencePath(root)) : '';
|
|
1755
|
+
// fuzzy: keyword 또는 keyword 부분 (4자+) 일치
|
|
1756
|
+
// 예: "webhook" 매칭 시 "webhook-payload", "webhooks", "webhooked" 모두 매칭
|
|
1757
|
+
const fuzzyRe = new RegExp(escapeRegex(keyword.slice(0, Math.max(4, Math.floor(keyword.length * 0.7)))), 'i');
|
|
1726
1758
|
const matches = [];
|
|
1727
1759
|
for (const block of evidence.split(/\n(?=## )/)) {
|
|
1728
|
-
if (block.startsWith('## ') &&
|
|
1760
|
+
if (block.startsWith('## ') && fuzzyRe.test(block) && /✗|fail|롤백|버그|incomplete/i.test(block)) {
|
|
1729
1761
|
const titleM = block.match(/^## (.+)$/m);
|
|
1730
1762
|
if (titleM) matches.push({ source: 'review-evidence.md', title: titleM[1].trim(), block });
|
|
1731
1763
|
}
|
|
1732
1764
|
}
|
|
1765
|
+
// 1.9.58: decisions.md도 fuzzy 매칭 (실패/롤백 관련 결정만)
|
|
1766
|
+
for (const block of decisions.split(/\n(?=### )/)) {
|
|
1767
|
+
if (block.startsWith('### ') && fuzzyRe.test(block) && /롤백|실패|fail|취소|회귀|deprecate/i.test(block)) {
|
|
1768
|
+
const titleM = block.match(/^### (.+)$/m);
|
|
1769
|
+
if (titleM) matches.push({ source: 'decisions.md', title: titleM[1].trim(), block });
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1733
1772
|
if (matches.length) {
|
|
1734
1773
|
const isTty = process.stdout && process.stdout.isTTY;
|
|
1735
1774
|
const yel = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
|
|
@@ -3096,7 +3135,7 @@ function _banner(opts = {}) {
|
|
|
3096
3135
|
log(' ' + C.green('npx leerness@latest init .') + C.dim(' # 신규 프로젝트 + 외부 AI CLI 설정'));
|
|
3097
3136
|
log(' ' + C.green('npx leerness handoff .') + C.dim(' # 컨텍스트 적재 + 과거 lessons 자동 재상기'));
|
|
3098
3137
|
log(' ' + C.green('npx leerness verify-claim T-0001 --run-tests') + C.dim(' # AI 거짓 완료 자동 검증'));
|
|
3099
|
-
log(' ' + C.green('npx leerness session close .
|
|
3138
|
+
log(' ' + C.green('npx leerness session close .') + C.dim(' # 마감 + 다음 라운드 추천 (default)'));
|
|
3100
3139
|
log('');
|
|
3101
3140
|
log(C.bold(C.cyan(' 🤖 메인 에이전트 (Claude/Cursor/Copilot)용')));
|
|
3102
3141
|
log(' ' + C.green('npx leerness mcp serve') + C.dim(' # MCP 서버 — 12 도구 노출'));
|
|
@@ -3849,7 +3888,9 @@ function sessionClose(root) {
|
|
|
3849
3888
|
// 1.9.12: session close 끝에 roadmap.html 자동 갱신
|
|
3850
3889
|
_autoRoadmap(root, 'session-close');
|
|
3851
3890
|
// 1.9.57: --suggest 옵션 — 마감 시 skill suggest + drift check + lessons 통합 보고
|
|
3852
|
-
|
|
3891
|
+
// 1.9.59: default 활성 — --no-suggest로 명시 비활성 가능
|
|
3892
|
+
const suggestEnabled = (has('--suggest') || (!has('--no-suggest') && process.env.LEERNESS_NO_SUGGEST !== '1'));
|
|
3893
|
+
if (suggestEnabled) {
|
|
3853
3894
|
const isTty = process.stdout && process.stdout.isTTY;
|
|
3854
3895
|
const cy = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
|
|
3855
3896
|
const dim = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
@@ -6769,10 +6810,21 @@ function mcpServeCmd(root) {
|
|
|
6769
6810
|
return send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown tool: ${name}` } });
|
|
6770
6811
|
}
|
|
6771
6812
|
const r = callLeerness(cliArgs);
|
|
6772
|
-
|
|
6773
|
-
|
|
6813
|
+
// 1.9.61: cursor 기반 페이지네이션 — 긴 출력은 cursor offset로 다음 청크
|
|
6814
|
+
const fullText = r.stdout || r.stderr || '(no output)';
|
|
6815
|
+
const CHUNK_SIZE = (args._chunkSize && Number.isFinite(args._chunkSize)) ? args._chunkSize : 50000;
|
|
6816
|
+
const cursor = (args._cursor && /^\d+$/.test(String(args._cursor))) ? parseInt(args._cursor, 10) : 0;
|
|
6817
|
+
const chunk = fullText.slice(cursor, cursor + CHUNK_SIZE);
|
|
6818
|
+
const nextCursor = (cursor + CHUNK_SIZE) < fullText.length ? String(cursor + CHUNK_SIZE) : null;
|
|
6819
|
+
const result = {
|
|
6820
|
+
content: [{ type: 'text', text: chunk }],
|
|
6774
6821
|
isError: !r.ok
|
|
6775
|
-
}
|
|
6822
|
+
};
|
|
6823
|
+
if (nextCursor) {
|
|
6824
|
+
result.nextCursor = nextCursor;
|
|
6825
|
+
result._truncated = { totalLength: fullText.length, returned: chunk.length, hint: `args._cursor=${nextCursor} 로 다음 청크 호출 가능` };
|
|
6826
|
+
}
|
|
6827
|
+
send({ jsonrpc: '2.0', id, result });
|
|
6776
6828
|
} catch (e) {
|
|
6777
6829
|
send({ jsonrpc: '2.0', id, error: { code: -32603, message: 'Internal error: ' + e.message } });
|
|
6778
6830
|
}
|
|
@@ -6892,6 +6944,36 @@ function usageStatsCmd(root) {
|
|
|
6892
6944
|
}
|
|
6893
6945
|
|
|
6894
6946
|
// 1.9.38: task sync — TodoWrite/외부 JSON에서 leerness task로 mirror
|
|
6947
|
+
// 1.9.60: leerness task export [--to <todo.json>] [--json]
|
|
6948
|
+
// progress-tracker → TodoWrite JSON 형식 (status: completed/in_progress/pending)
|
|
6949
|
+
function taskExportCmd(root) {
|
|
6950
|
+
root = absRoot(root || process.cwd());
|
|
6951
|
+
const out = arg('--to', null);
|
|
6952
|
+
const rows = readProgressRows(root);
|
|
6953
|
+
// leerness status → TodoWrite status 매핑
|
|
6954
|
+
const statusMap = { 'done': 'completed', 'in-progress': 'in_progress', 'planned': 'pending', 'requested': 'pending', 'dropped': 'cancelled', 'in_progress': 'in_progress', 'incomplete': 'in_progress', 'blocked': 'in_progress', 'on-hold': 'pending' };
|
|
6955
|
+
const todos = rows.map(r => ({
|
|
6956
|
+
content: r.request,
|
|
6957
|
+
status: statusMap[r.status] || 'pending',
|
|
6958
|
+
activeForm: r.nextAction || r.request.slice(0, 40)
|
|
6959
|
+
}));
|
|
6960
|
+
if (out) {
|
|
6961
|
+
writeUtf8(path.resolve(out), JSON.stringify(todos, null, 2) + '\n');
|
|
6962
|
+
log(`# leerness task export (1.9.60)`);
|
|
6963
|
+
log(`exported: ${todos.length} task → ${path.resolve(out)}`);
|
|
6964
|
+
log(``);
|
|
6965
|
+
log(`💡 다음: 메인 에이전트가 이 JSON을 TodoWrite로 import 가능`);
|
|
6966
|
+
return;
|
|
6967
|
+
}
|
|
6968
|
+
if (has('--json')) { log(JSON.stringify(todos, null, 2)); return; }
|
|
6969
|
+
log(`# leerness task export (1.9.60)`);
|
|
6970
|
+
log(`총 ${todos.length} task (--to <file>로 저장)`);
|
|
6971
|
+
for (const t of todos.slice(0, 10)) {
|
|
6972
|
+
log(` - [${t.status}] ${t.content.slice(0, 60)}`);
|
|
6973
|
+
}
|
|
6974
|
+
if (todos.length > 10) log(` ... ${todos.length - 10}건 더`);
|
|
6975
|
+
}
|
|
6976
|
+
|
|
6895
6977
|
function taskSyncCmd(root) {
|
|
6896
6978
|
root = absRoot(root || process.cwd());
|
|
6897
6979
|
const file = arg('--from', null);
|
|
@@ -7205,6 +7287,7 @@ async function main() {
|
|
|
7205
7287
|
if (sub==='fix-evidence') return taskFixEvidence(root);
|
|
7206
7288
|
if (sub==='relink') return taskRelink(root);
|
|
7207
7289
|
if (sub==='sync') return taskSyncCmd(root);
|
|
7290
|
+
if (sub==='export') return taskExportCmd(root);
|
|
7208
7291
|
}
|
|
7209
7292
|
return help();
|
|
7210
7293
|
}
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -950,6 +950,84 @@ total++;
|
|
|
950
950
|
if (!ok) { failed++; console.log(r.stdout.slice(0, 800)); }
|
|
951
951
|
}
|
|
952
952
|
|
|
953
|
+
// 1.9.60/61/62 회귀
|
|
954
|
+
total++;
|
|
955
|
+
{
|
|
956
|
+
// task export → TodoWrite JSON 호환
|
|
957
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-tex-'));
|
|
958
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
959
|
+
cp.spawnSync(process.execPath, [CLI, 'task', 'add', 'export 검증', '--status', 'done', '--path', tmpC], { stdio: 'ignore', timeout: 10000 });
|
|
960
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'task', 'export', '--path', tmpC, '--json'], { encoding: 'utf8', timeout: 15000 });
|
|
961
|
+
let j = null;
|
|
962
|
+
try { j = JSON.parse(r.stdout); } catch {}
|
|
963
|
+
const ok = Array.isArray(j) && j.length >= 1 && j.some(t => t.status === 'completed' && 'content' in t && 'activeForm' in t);
|
|
964
|
+
console.log(ok ? '✓ B(1.9.60) task export: TodoWrite JSON 형식 (content/status/activeForm)' : `✗ task export 실패`);
|
|
965
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
total++;
|
|
969
|
+
{
|
|
970
|
+
// MCP cursor 페이지네이션 (chunkSize override)
|
|
971
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-mcc-'));
|
|
972
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
973
|
+
const req = JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'leerness_handoff', arguments: { path: tmpC, _cursor: '0', _chunkSize: 200 } } });
|
|
974
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'mcp', 'serve'], { encoding: 'utf8', timeout: 15000, input: req + '\n' });
|
|
975
|
+
const resp = JSON.parse(r.stdout.split('\n').filter(Boolean)[0]);
|
|
976
|
+
const ok = resp.result && resp.result.nextCursor && resp.result._truncated;
|
|
977
|
+
console.log(ok ? '✓ B(1.9.61) MCP cursor 페이지네이션: nextCursor + _truncated' : `✗ cursor 실패`);
|
|
978
|
+
if (!ok) { failed++; console.log(JSON.stringify(resp).slice(0, 300)); }
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
total++;
|
|
982
|
+
{
|
|
983
|
+
// audit npm CVE — OFFLINE 또는 package.json 없을 때 graceful
|
|
984
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-ac-'));
|
|
985
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
986
|
+
// package.json 없음 → npm audit 스킵
|
|
987
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'audit', tmpC], { encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_OFFLINE: '1' } });
|
|
988
|
+
const ok = r.status === 0 && !/npm CVE/.test(r.stdout) && /Audit summary/.test(r.stdout);
|
|
989
|
+
console.log(ok ? '✓ B(1.9.62) audit: package.json 없을 때 npm audit 자동 스킵' : `✗ audit CVE 실패`);
|
|
990
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// 1.9.58/59 회귀
|
|
994
|
+
total++;
|
|
995
|
+
{
|
|
996
|
+
// fuzzy 매칭 — 어간 변형 (webhook ↔ webhooks)
|
|
997
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-fz-'));
|
|
998
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
999
|
+
fs.writeFileSync(path.join(tmpC, '.harness', 'review-evidence.md'),
|
|
1000
|
+
'## 2026-04\nNote: ✗ webhooks payload 실패\n', 'utf8');
|
|
1001
|
+
cp.spawnSync(process.execPath, [CLI, 'task', 'add', 'webhook 작업', '--status', 'in-progress', '--path', tmpC], { stdio: 'ignore', timeout: 10000 });
|
|
1002
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'handoff', tmpC, '--no-drift-check', '--no-workflow-guide'], { encoding: 'utf8', timeout: 15000 });
|
|
1003
|
+
const ok = /lessons 자동 재상기.*webhook/.test(r.stdout);
|
|
1004
|
+
console.log(ok ? '✓ B(1.9.58) lessons fuzzy: webhook ↔ webhooks 어간 변형 매칭' : `✗ fuzzy 실패`);
|
|
1005
|
+
if (!ok) { failed++; console.log(r.stdout.slice(-500)); }
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
total++;
|
|
1009
|
+
{
|
|
1010
|
+
// session close default suggest
|
|
1011
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-sd-'));
|
|
1012
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
1013
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'session', 'close', tmpC], { encoding: 'utf8', timeout: 30000 });
|
|
1014
|
+
// 1.9.59: default activated → "다음 라운드 추천" 자동 표시
|
|
1015
|
+
const ok = /다음 라운드 추천|drift 상태/.test(r.stdout);
|
|
1016
|
+
console.log(ok ? '✓ B(1.9.59) session close: --suggest default 활성' : `✗ default suggest 실패`);
|
|
1017
|
+
if (!ok) { failed++; console.log(r.stdout.slice(-500)); }
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
total++;
|
|
1021
|
+
{
|
|
1022
|
+
// --no-suggest 비활성 (이전 동작 보존)
|
|
1023
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-ns-'));
|
|
1024
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
1025
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'session', 'close', tmpC, '--no-suggest'], { encoding: 'utf8', timeout: 30000 });
|
|
1026
|
+
const ok = r.status === 0 && !/다음 라운드 추천/.test(r.stdout) && /진행 요약/.test(r.stdout);
|
|
1027
|
+
console.log(ok ? '✓ B(1.9.59) --no-suggest: suggest 비활성 (이전 동작)' : `✗ --no-suggest 실패`);
|
|
1028
|
+
if (!ok) { failed++; console.log(r.stdout.slice(-300)); }
|
|
1029
|
+
}
|
|
1030
|
+
|
|
953
1031
|
// 1.9.56/57 회귀
|
|
954
1032
|
total++;
|
|
955
1033
|
{
|