leerness 1.9.59 → 1.9.64
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 +85 -0
- package/README.md +7 -2
- package/bin/harness.js +100 -5
- package/package.json +1 -1
- package/scripts/e2e.js +77 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,90 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.64 — 2026-05-19
|
|
4
|
+
|
|
5
|
+
**`leerness install <skill>` 별칭 + 성능 벤치마크 1차 실측**.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **`leerness install <SKILL.md path or URL>`** — `skill install` 별칭:
|
|
9
|
+
- 자주 쓰는 명령 단축 (agentskills.io 컨벤션 맞춤)
|
|
10
|
+
- 디렉토리만 주면 init 의도로 친절 안내 (`leerness init` 권장)
|
|
11
|
+
- 인자 없으면 사용법 안내
|
|
12
|
+
|
|
13
|
+
### 📊 성능 벤치마크 1차 (stress-v10)
|
|
14
|
+
|
|
15
|
+
10회 평균 latency (Node.js spawnSync cold start 포함):
|
|
16
|
+
|
|
17
|
+
| 명령 | median | p95 |
|
|
18
|
+
|---|---:|---:|
|
|
19
|
+
| status | 1330ms | 1426ms |
|
|
20
|
+
| handoff --compact | 1378ms | 2500ms |
|
|
21
|
+
| drift check | 1303ms | 1782ms |
|
|
22
|
+
| audit | 1159ms | 1806ms |
|
|
23
|
+
| skill list | 1526ms | 2503ms |
|
|
24
|
+
| handoff (100 task) | 1176ms | - |
|
|
25
|
+
| task export (100 task) | 2163ms | - |
|
|
26
|
+
| skill suggest (30 task) | 1075ms | - |
|
|
27
|
+
|
|
28
|
+
### 1.9.65+ 성능 최적화 후보 (벤치마크에서 도출)
|
|
29
|
+
- `.harness/cache/usage-stats.json` 파일 I/O 캐싱
|
|
30
|
+
- handoff의 lessons fuzzy 매칭 워크스페이스 크기에 비례 → 키워드 캐시
|
|
31
|
+
|
|
32
|
+
## 1.9.63 — 2026-05-19
|
|
33
|
+
|
|
34
|
+
**`leerness audit --strict` — CI 친화 옵션 (warnings → failures 승격)**.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
- **`--strict [--threshold N]`** — warnings ≥ N (기본 1) 시 failures 승격 → exit 1
|
|
38
|
+
- CI 환경에서 audit warning 무시 방지
|
|
39
|
+
|
|
40
|
+
### 검증 (stress-v10 + 누적 회귀)
|
|
41
|
+
- EE1-EE3 (audit --strict) 3/3 PASS
|
|
42
|
+
- FF1-FF3 (install 별칭) 3/3 PASS
|
|
43
|
+
- GG1-GG5 (성능 벤치마크 10회 평균) 5/5 PASS
|
|
44
|
+
- HH1-HH3 (큰 워크스페이스 100 task) 3/3 PASS
|
|
45
|
+
- II1-II3 (1.9.43~62 회귀) 3/3 PASS
|
|
46
|
+
- **stress-v10: 17/17 PASS**, e2e: **219/219 PASS**
|
|
47
|
+
|
|
48
|
+
## 1.9.62 — 2026-05-19
|
|
49
|
+
|
|
50
|
+
**`leerness audit`에 npm CVE 자동 감지 통합**.
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
- **`leerness audit`** — package.json 있으면 `npm audit --json` 자동 호출 → CVE 보고:
|
|
54
|
+
- `metadata.vulnerabilities` 파싱 → critical/high/moderate/low 카운트
|
|
55
|
+
- critical/high 발견 시 warnings +2 (가중치)
|
|
56
|
+
- 0건이면 ✓ "npm CVE: 0건"
|
|
57
|
+
- **스킵 조건** (자동): package.json 없음, `LEERNESS_OFFLINE=1`, `--no-npm-audit` 플래그
|
|
58
|
+
|
|
59
|
+
## 1.9.61 — 2026-05-19
|
|
60
|
+
|
|
61
|
+
**MCP server cursor 기반 페이지네이션**.
|
|
62
|
+
|
|
63
|
+
### Added
|
|
64
|
+
- **MCP `tools/call` 응답에 cursor 메타 추가** — 50KB 넘는 출력 자동 분할:
|
|
65
|
+
- `result.nextCursor` — 다음 청크 offset (`args._cursor`로 재호출)
|
|
66
|
+
- `result._truncated` — `{ totalLength, returned, hint }` 메타
|
|
67
|
+
- 청크 크기 override 가능: `args._chunkSize` (기본 50000)
|
|
68
|
+
- 사용 예: 100 task 워크스페이스의 handoff → 청크 1 → cursor → 청크 2 → ... 완료
|
|
69
|
+
|
|
70
|
+
## 1.9.60 — 2026-05-19
|
|
71
|
+
|
|
72
|
+
**`leerness task export` — TodoWrite ↔ leerness 양방향 sync 완성**.
|
|
73
|
+
|
|
74
|
+
### Added
|
|
75
|
+
- **`leerness task export [--to <file>] [--json]`**:
|
|
76
|
+
- progress-tracker → TodoWrite JSON 형식 변환
|
|
77
|
+
- status 매핑: `done` → `completed`, `in-progress` → `in_progress`, `planned` → `pending`, `dropped` → `cancelled`
|
|
78
|
+
- 필드: `content` / `status` / `activeForm` (TodoWrite 호환)
|
|
79
|
+
- 1.9.38 `task sync --from`(TodoWrite → leerness)과 함께 **양방향 sync 완성**
|
|
80
|
+
|
|
81
|
+
### 검증 (stress-v9)
|
|
82
|
+
- AA1-AA4 (task export + status 매핑 + round-trip) 4/4 PASS
|
|
83
|
+
- BB1-BB3 (MCP cursor 페이지네이션, chunkSize override) 3/3 PASS
|
|
84
|
+
- CC1-CC3 (audit npm CVE, OFFLINE/no-npm-audit/no-package.json 스킵) 3/3 PASS
|
|
85
|
+
- DD1-DD3 (1.9.43~59 누적 회귀) 3/3 PASS
|
|
86
|
+
- **stress-v9: 13/13 PASS** (BB2/BB3 BUG 발견·즉시 패치: chunkSize override 추가), e2e: **216/216 PASS**
|
|
87
|
+
|
|
3
88
|
## 1.9.59 — 2026-05-19
|
|
4
89
|
|
|
5
90
|
**`session close --suggest` default 활성 — 라운드 마감 잊을 단계 없음**.
|
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.64 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.64** — `leerness install <SKILL.md>` 별칭 (skill install 단축) · **성능 벤치마크 1차 실측** (status/handoff/drift/audit/skill list 평균 1.2~1.5초).
|
|
437
|
+
- **1.9.63** — `leerness audit --strict [--threshold N]` — CI 친화 옵션 (warnings → failures 승격).
|
|
438
|
+
- **1.9.62** — `leerness audit` npm CVE 자동 감지 (npm audit --json 통합, OFFLINE/no-npm-audit 스킵).
|
|
439
|
+
- **1.9.61** — MCP server cursor 기반 페이지네이션 — `nextCursor` + `_chunkSize` override.
|
|
440
|
+
- **1.9.60** — `leerness task export` — progress-tracker → TodoWrite JSON 형식. **양방향 sync 완성** (1.9.38 sync + 1.9.60 export).
|
|
436
441
|
- **1.9.59** — `session close` **--suggest default 활성** (잊을 단계 없음, `--no-suggest`로 비활성 가능) · 설치 가이드 갱신.
|
|
437
442
|
- **1.9.58** — handoff lessons **fuzzy 매칭** (어간 변형, 복합어, decisions.md 매칭 추가).
|
|
438
443
|
- **1.9.57** — `session close --suggest` (마감 시 skill suggest + drift + 명령 통계 통합 보고) · install banner quickStart + session-workflow.md 갱신 (1.9.56/57 흐름 자동 안내).
|
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.64';
|
|
10
10
|
const MARK = '<!-- leerness:managed -->';
|
|
11
11
|
const README_START = '<!-- leerness:project-readme:start -->';
|
|
12
12
|
const README_END = '<!-- leerness:project-readme:end -->';
|
|
@@ -1423,7 +1423,43 @@ function audit(root) {
|
|
|
1423
1423
|
}
|
|
1424
1424
|
}
|
|
1425
1425
|
} catch {}
|
|
1426
|
-
|
|
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
|
+
}
|
|
1454
|
+
// 1.9.63: --strict — warnings ≥ threshold 시 failures로 승격 (CI 친화)
|
|
1455
|
+
if (has('--strict')) {
|
|
1456
|
+
const threshold = parseInt(arg('--threshold', '1'), 10);
|
|
1457
|
+
if (warnings >= threshold) {
|
|
1458
|
+
failures++;
|
|
1459
|
+
warn(`--strict 활성: warnings ${warnings} ≥ threshold ${threshold} → failures 승격`);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
log(`Audit summary: warnings=${warnings} failures=${failures}${fix ? ` fixed=${fixed}` : ''}${has('--strict') ? ` strict-threshold=${arg('--threshold', '1')}` : ''}`);
|
|
1427
1463
|
if (failures) process.exitCode = 1;
|
|
1428
1464
|
}
|
|
1429
1465
|
|
|
@@ -6782,10 +6818,21 @@ function mcpServeCmd(root) {
|
|
|
6782
6818
|
return send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown tool: ${name}` } });
|
|
6783
6819
|
}
|
|
6784
6820
|
const r = callLeerness(cliArgs);
|
|
6785
|
-
|
|
6786
|
-
|
|
6821
|
+
// 1.9.61: cursor 기반 페이지네이션 — 긴 출력은 cursor offset로 다음 청크
|
|
6822
|
+
const fullText = r.stdout || r.stderr || '(no output)';
|
|
6823
|
+
const CHUNK_SIZE = (args._chunkSize && Number.isFinite(args._chunkSize)) ? args._chunkSize : 50000;
|
|
6824
|
+
const cursor = (args._cursor && /^\d+$/.test(String(args._cursor))) ? parseInt(args._cursor, 10) : 0;
|
|
6825
|
+
const chunk = fullText.slice(cursor, cursor + CHUNK_SIZE);
|
|
6826
|
+
const nextCursor = (cursor + CHUNK_SIZE) < fullText.length ? String(cursor + CHUNK_SIZE) : null;
|
|
6827
|
+
const result = {
|
|
6828
|
+
content: [{ type: 'text', text: chunk }],
|
|
6787
6829
|
isError: !r.ok
|
|
6788
|
-
}
|
|
6830
|
+
};
|
|
6831
|
+
if (nextCursor) {
|
|
6832
|
+
result.nextCursor = nextCursor;
|
|
6833
|
+
result._truncated = { totalLength: fullText.length, returned: chunk.length, hint: `args._cursor=${nextCursor} 로 다음 청크 호출 가능` };
|
|
6834
|
+
}
|
|
6835
|
+
send({ jsonrpc: '2.0', id, result });
|
|
6789
6836
|
} catch (e) {
|
|
6790
6837
|
send({ jsonrpc: '2.0', id, error: { code: -32603, message: 'Internal error: ' + e.message } });
|
|
6791
6838
|
}
|
|
@@ -6905,6 +6952,36 @@ function usageStatsCmd(root) {
|
|
|
6905
6952
|
}
|
|
6906
6953
|
|
|
6907
6954
|
// 1.9.38: task sync — TodoWrite/외부 JSON에서 leerness task로 mirror
|
|
6955
|
+
// 1.9.60: leerness task export [--to <todo.json>] [--json]
|
|
6956
|
+
// progress-tracker → TodoWrite JSON 형식 (status: completed/in_progress/pending)
|
|
6957
|
+
function taskExportCmd(root) {
|
|
6958
|
+
root = absRoot(root || process.cwd());
|
|
6959
|
+
const out = arg('--to', null);
|
|
6960
|
+
const rows = readProgressRows(root);
|
|
6961
|
+
// leerness status → TodoWrite status 매핑
|
|
6962
|
+
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' };
|
|
6963
|
+
const todos = rows.map(r => ({
|
|
6964
|
+
content: r.request,
|
|
6965
|
+
status: statusMap[r.status] || 'pending',
|
|
6966
|
+
activeForm: r.nextAction || r.request.slice(0, 40)
|
|
6967
|
+
}));
|
|
6968
|
+
if (out) {
|
|
6969
|
+
writeUtf8(path.resolve(out), JSON.stringify(todos, null, 2) + '\n');
|
|
6970
|
+
log(`# leerness task export (1.9.60)`);
|
|
6971
|
+
log(`exported: ${todos.length} task → ${path.resolve(out)}`);
|
|
6972
|
+
log(``);
|
|
6973
|
+
log(`💡 다음: 메인 에이전트가 이 JSON을 TodoWrite로 import 가능`);
|
|
6974
|
+
return;
|
|
6975
|
+
}
|
|
6976
|
+
if (has('--json')) { log(JSON.stringify(todos, null, 2)); return; }
|
|
6977
|
+
log(`# leerness task export (1.9.60)`);
|
|
6978
|
+
log(`총 ${todos.length} task (--to <file>로 저장)`);
|
|
6979
|
+
for (const t of todos.slice(0, 10)) {
|
|
6980
|
+
log(` - [${t.status}] ${t.content.slice(0, 60)}`);
|
|
6981
|
+
}
|
|
6982
|
+
if (todos.length > 10) log(` ... ${todos.length - 10}건 더`);
|
|
6983
|
+
}
|
|
6984
|
+
|
|
6908
6985
|
function taskSyncCmd(root) {
|
|
6909
6986
|
root = absRoot(root || process.cwd());
|
|
6910
6987
|
const file = arg('--from', null);
|
|
@@ -7116,6 +7193,23 @@ async function main() {
|
|
|
7116
7193
|
} catch {}
|
|
7117
7194
|
}
|
|
7118
7195
|
if (cmd === 'init') return await install(args[1] || process.cwd(), { force:false, dry:false, migration:false });
|
|
7196
|
+
// 1.9.64: install <skill-id-or-url> 별칭 (= skill install). 자주 쓰는 명령 단축형.
|
|
7197
|
+
// 단, init이 leerness install . 같은 형태로도 동작하던 옛 호환은 유지 — args[1]이 디렉토리면 init으로 라우팅.
|
|
7198
|
+
if (cmd === 'install') {
|
|
7199
|
+
const arg1 = args[1];
|
|
7200
|
+
// skill source는 .md 파일 또는 URL 또는 skill id. 디렉토리면 init으로.
|
|
7201
|
+
if (!arg1) { fail('사용법: leerness install <skill SKILL.md path or URL>'); return process.exit(1); }
|
|
7202
|
+
if (/^https?:\/\//.test(arg1) || /\.md$/.test(arg1) || exists(path.join(arg1, 'SKILL.md'))) {
|
|
7203
|
+
return await skillInstallCmd(absRoot(arg('--path', process.cwd())), arg1);
|
|
7204
|
+
}
|
|
7205
|
+
// 디렉토리면 안내
|
|
7206
|
+
if (exists(arg1) && fs.statSync(arg1).isDirectory() && !exists(path.join(arg1, 'SKILL.md'))) {
|
|
7207
|
+
fail(`디렉토리에 SKILL.md 없음: ${arg1}\n init 의도였다면: leerness init "${arg1}"`);
|
|
7208
|
+
return process.exit(1);
|
|
7209
|
+
}
|
|
7210
|
+
fail(`알 수 없는 install 대상: ${arg1}\n SKILL.md 파일/URL/SKILL.md 포함 디렉토리 필요`);
|
|
7211
|
+
return process.exit(1);
|
|
7212
|
+
}
|
|
7119
7213
|
if (cmd === 'migrate') return await install(args[1] || process.cwd(), { force:has('--force'), dry:has('--dry-run'), migration:true });
|
|
7120
7214
|
if (cmd === 'update') return await updateCmd(args[1] || process.cwd(), { checkOnly: has('--check'), yes: has('--yes'), force: has('--force') });
|
|
7121
7215
|
if (cmd === 'auto-update' && args[1] === 'install') return autoUpdateInstall(args[2] || process.cwd());
|
|
@@ -7218,6 +7312,7 @@ async function main() {
|
|
|
7218
7312
|
if (sub==='fix-evidence') return taskFixEvidence(root);
|
|
7219
7313
|
if (sub==='relink') return taskRelink(root);
|
|
7220
7314
|
if (sub==='sync') return taskSyncCmd(root);
|
|
7315
|
+
if (sub==='export') return taskExportCmd(root);
|
|
7221
7316
|
}
|
|
7222
7317
|
return help();
|
|
7223
7318
|
}
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -950,6 +950,83 @@ total++;
|
|
|
950
950
|
if (!ok) { failed++; console.log(r.stdout.slice(0, 800)); }
|
|
951
951
|
}
|
|
952
952
|
|
|
953
|
+
// 1.9.63/64 회귀
|
|
954
|
+
total++;
|
|
955
|
+
{
|
|
956
|
+
// audit --strict
|
|
957
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-st-'));
|
|
958
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
959
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'audit', tmpC, '--strict'], { encoding: 'utf8', timeout: 15000 });
|
|
960
|
+
const ok = r.status !== 0 && /strict.*승격|failures=1/.test(r.stdout);
|
|
961
|
+
console.log(ok ? '✓ B(1.9.63) audit --strict: warnings → failures 승격' : `✗ --strict 실패`);
|
|
962
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
total++;
|
|
966
|
+
{
|
|
967
|
+
// audit --strict --threshold 10 → exit 0
|
|
968
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-st2-'));
|
|
969
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
970
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'audit', tmpC, '--strict', '--threshold', '10'], { encoding: 'utf8', timeout: 15000 });
|
|
971
|
+
const ok = r.status === 0;
|
|
972
|
+
console.log(ok ? '✓ B(1.9.63) audit --strict --threshold 10: warnings 적으면 exit 0' : `✗ threshold 실패`);
|
|
973
|
+
if (!ok) { failed++; console.log(r.stdout.slice(-300)); }
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
total++;
|
|
977
|
+
{
|
|
978
|
+
// install 별칭
|
|
979
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-iv-'));
|
|
980
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
981
|
+
const src = path.join(tmpC, 'sk.md');
|
|
982
|
+
fs.writeFileSync(src, '---\nname: install-alias-test\ndescription: 별칭 검증\n---\n# Body\n', 'utf8');
|
|
983
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'install', src, '--path', tmpC], { encoding: 'utf8', timeout: 15000 });
|
|
984
|
+
const f = path.join(tmpC, '.harness', 'skills', 'install-alias-test', 'SKILL.md');
|
|
985
|
+
const ok = r.status === 0 && fs.existsSync(f);
|
|
986
|
+
console.log(ok ? '✓ B(1.9.64) install <SKILL.md>: skill install 별칭 동작' : `✗ install 별칭 실패`);
|
|
987
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 300)); }
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// 1.9.60/61/62 회귀
|
|
991
|
+
total++;
|
|
992
|
+
{
|
|
993
|
+
// task export → TodoWrite JSON 호환
|
|
994
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-tex-'));
|
|
995
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
996
|
+
cp.spawnSync(process.execPath, [CLI, 'task', 'add', 'export 검증', '--status', 'done', '--path', tmpC], { stdio: 'ignore', timeout: 10000 });
|
|
997
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'task', 'export', '--path', tmpC, '--json'], { encoding: 'utf8', timeout: 15000 });
|
|
998
|
+
let j = null;
|
|
999
|
+
try { j = JSON.parse(r.stdout); } catch {}
|
|
1000
|
+
const ok = Array.isArray(j) && j.length >= 1 && j.some(t => t.status === 'completed' && 'content' in t && 'activeForm' in t);
|
|
1001
|
+
console.log(ok ? '✓ B(1.9.60) task export: TodoWrite JSON 형식 (content/status/activeForm)' : `✗ task export 실패`);
|
|
1002
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
total++;
|
|
1006
|
+
{
|
|
1007
|
+
// MCP cursor 페이지네이션 (chunkSize override)
|
|
1008
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-mcc-'));
|
|
1009
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
1010
|
+
const req = JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'leerness_handoff', arguments: { path: tmpC, _cursor: '0', _chunkSize: 200 } } });
|
|
1011
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'mcp', 'serve'], { encoding: 'utf8', timeout: 15000, input: req + '\n' });
|
|
1012
|
+
const resp = JSON.parse(r.stdout.split('\n').filter(Boolean)[0]);
|
|
1013
|
+
const ok = resp.result && resp.result.nextCursor && resp.result._truncated;
|
|
1014
|
+
console.log(ok ? '✓ B(1.9.61) MCP cursor 페이지네이션: nextCursor + _truncated' : `✗ cursor 실패`);
|
|
1015
|
+
if (!ok) { failed++; console.log(JSON.stringify(resp).slice(0, 300)); }
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
total++;
|
|
1019
|
+
{
|
|
1020
|
+
// audit npm CVE — OFFLINE 또는 package.json 없을 때 graceful
|
|
1021
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-ac-'));
|
|
1022
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
1023
|
+
// package.json 없음 → npm audit 스킵
|
|
1024
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'audit', tmpC], { encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_OFFLINE: '1' } });
|
|
1025
|
+
const ok = r.status === 0 && !/npm CVE/.test(r.stdout) && /Audit summary/.test(r.stdout);
|
|
1026
|
+
console.log(ok ? '✓ B(1.9.62) audit: package.json 없을 때 npm audit 자동 스킵' : `✗ audit CVE 실패`);
|
|
1027
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
1028
|
+
}
|
|
1029
|
+
|
|
953
1030
|
// 1.9.58/59 회귀
|
|
954
1031
|
total++;
|
|
955
1032
|
{
|