leerness 1.9.13 → 1.9.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.13",
3
+ "version": "1.9.18",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -237,6 +237,334 @@ total++;
237
237
  if (!(strongOK && weakHint)) failed++;
238
238
  }
239
239
 
240
+ // 1.9.16 회귀: brainstorm --all-apps / --json / session close 워크스페이스 안내
241
+ total++;
242
+ {
243
+ // brainstorm --all-apps
244
+ const tmpA = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-bsa-'));
245
+ const tmpB = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-bsb-'));
246
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpA, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
247
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpB, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
248
+ fs.appendFileSync(path.join(tmpA, '.harness/decisions.md'), '\n### 2026-05-13 — 캐시 정책\n- Reason: rate limit\n');
249
+ fs.appendFileSync(path.join(tmpB, '.harness/decisions.md'), '\n### 2026-05-13 — 캐시 분산\n- Reason: 확장성\n');
250
+ const r = cp.spawnSync(process.execPath, [CLI, 'brainstorm', '캐시', '--include', `${tmpA},${tmpB}`], { encoding: 'utf8', timeout: 15000, cwd: os.tmpdir() });
251
+ const ok = r.status === 0 && /Cross-project Brainstorm — "캐시" — 2개/.test(r.stdout) && /워크스페이스 총합: 2건/.test(r.stdout);
252
+ console.log(ok ? '✓ B(1.9.16) brainstorm --include 통합' : '✗ brainstorm --include 실패');
253
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 600)); }
254
+ }
255
+
256
+ total++;
257
+ {
258
+ // --json 단일 brainstorm
259
+ const tmpJ = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-json-'));
260
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpJ, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
261
+ fs.appendFileSync(path.join(tmpJ, '.harness/decisions.md'), '\n### 2026-05-13 — JSON 결정\n- ...\n');
262
+ const r = cp.spawnSync(process.execPath, [CLI, 'brainstorm', 'JSON', '--json', '--path', tmpJ], { encoding: 'utf8', timeout: 15000 });
263
+ let parsed = null;
264
+ try { parsed = JSON.parse(r.stdout); } catch {}
265
+ const ok = r.status === 0 && parsed && parsed.topic === 'JSON' && parsed.total >= 1;
266
+ console.log(ok ? '✓ B(1.9.16) brainstorm --json 단일' : `✗ brainstorm --json 실패\n${r.stdout.slice(0, 300)}`);
267
+ if (!ok) failed++;
268
+ }
269
+
270
+ total++;
271
+ {
272
+ // retro --json
273
+ const tmpR = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-rj-'));
274
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpR, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
275
+ const r = cp.spawnSync(process.execPath, [CLI, 'retro', tmpR, '--json'], { encoding: 'utf8', timeout: 15000 });
276
+ let parsed = null;
277
+ try { parsed = JSON.parse(r.stdout); } catch {}
278
+ const ok = r.status === 0 && parsed && parsed.summary && parsed.data;
279
+ console.log(ok ? '✓ B(1.9.16) retro --json' : '✗ retro --json 실패');
280
+ if (!ok) failed++;
281
+ }
282
+
283
+ total++;
284
+ {
285
+ // insights --json (workspace)
286
+ const tmpA = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-iwsa-'));
287
+ const tmpB = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-iwsb-'));
288
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpA, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
289
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpB, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
290
+ const r = cp.spawnSync(process.execPath, [CLI, 'insights', '--include', `${tmpA},${tmpB}`, '--json'], { encoding: 'utf8', timeout: 15000, cwd: os.tmpdir() });
291
+ let parsed = null;
292
+ try { parsed = JSON.parse(r.stdout); } catch {}
293
+ const ok = r.status === 0 && parsed && parsed.projectCount === 2 && Array.isArray(parsed.projects);
294
+ console.log(ok ? '✓ B(1.9.16) insights --include --json' : '✗ insights --json 실패');
295
+ if (!ok) failed++;
296
+ }
297
+
298
+ total++;
299
+ {
300
+ // session close 끝에 워크스페이스 안내 (다른 leerness 프로젝트 시뮬)
301
+ const wsRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-ws-'));
302
+ fs.mkdirSync(path.join(wsRoot, '_apps'), { recursive: true });
303
+ const proj = path.join(wsRoot, 'main');
304
+ const other = path.join(wsRoot, '_apps', 'other');
305
+ cp.spawnSync(process.execPath, [CLI, 'init', proj, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
306
+ cp.spawnSync(process.execPath, [CLI, 'init', other, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
307
+ // _apps가 proj와 같은 부모 디렉토리에 있어야 감지
308
+ // 우리 케이스는 wsRoot/main과 wsRoot/_apps/other → main에서 ../_apps 검색하면 발견
309
+ const r = cp.spawnSync(process.execPath, [CLI, 'session', 'close', proj], { encoding: 'utf8', timeout: 15000 });
310
+ const ok = /워크스페이스에 \d+개 다른 leerness 프로젝트/.test(r.stdout);
311
+ console.log(ok ? '✓ B(1.9.16) session close 워크스페이스 안내' : '✗ session close 안내 실패');
312
+ if (!ok) { failed++; console.log(r.stdout.slice(-500)); }
313
+ }
314
+
315
+ // 1.9.17 회귀: handoff --all-apps / reuse-map --all-apps (워크스페이스 오케스트레이션)
316
+ total++;
317
+ {
318
+ // handoff --include
319
+ const tmpA = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-ha-'));
320
+ const tmpB = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-hb-'));
321
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpA, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
322
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpB, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
323
+ const r = cp.spawnSync(process.execPath, [CLI, 'handoff', '--include', `${tmpA},${tmpB}`], { encoding: 'utf8', timeout: 15000, cwd: os.tmpdir() });
324
+ const ok = r.status === 0 && /Workspace Handoff — 2개 프로젝트 \(1\.9\.1[78]\)/.test(r.stdout) && /워크스페이스 총합/.test(r.stdout) && /오케스트레이션 권장/.test(r.stdout);
325
+ console.log(ok ? '✓ B(1.9.17) handoff --include 통합 워크스페이스 뷰' : '✗ handoff --include 실패');
326
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 600)); }
327
+ }
328
+
329
+ total++;
330
+ {
331
+ // handoff --include --json
332
+ const tmpA = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-haj-'));
333
+ const tmpB = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-hbj-'));
334
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpA, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
335
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpB, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
336
+ const r = cp.spawnSync(process.execPath, [CLI, 'handoff', '--include', `${tmpA},${tmpB}`, '--json'], { encoding: 'utf8', timeout: 15000, cwd: os.tmpdir() });
337
+ let parsed = null;
338
+ try { parsed = JSON.parse(r.stdout); } catch {}
339
+ const ok = r.status === 0 && parsed && Array.isArray(parsed.projects) && parsed.projects.length === 2 && parsed.totals;
340
+ console.log(ok ? '✓ B(1.9.17) handoff --include --json' : '✗ handoff --json 실패');
341
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
342
+ }
343
+
344
+ total++;
345
+ {
346
+ // reuse-map 단일 + 워크스페이스 모드 + 중복 감지
347
+ const tmpA = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-rma-'));
348
+ const tmpB = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-rmb-'));
349
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpA, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
350
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpB, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
351
+ // 양쪽에 같은 capability "Cache" 추가 → 중복 감지 기대
352
+ const rowA = '| Cache | src/cache.js | util | LRU |\n';
353
+ const rowB = '| Cache | src/foo.js | util | Memoize |\n';
354
+ fs.appendFileSync(path.join(tmpA, '.harness/reuse-map.md'), rowA);
355
+ fs.appendFileSync(path.join(tmpB, '.harness/reuse-map.md'), rowB);
356
+ // 단일
357
+ const rs = cp.spawnSync(process.execPath, [CLI, 'reuse-map', tmpA], { encoding: 'utf8', timeout: 15000 });
358
+ const okSingle = rs.status === 0 && /Reuse Map/.test(rs.stdout) && /Cache/.test(rs.stdout);
359
+ // 워크스페이스
360
+ const rw = cp.spawnSync(process.execPath, [CLI, 'reuse-map', '--include', `${tmpA},${tmpB}`], { encoding: 'utf8', timeout: 15000, cwd: os.tmpdir() });
361
+ const okMulti = rw.status === 0 && /Workspace Reuse Map — 2개 프로젝트/.test(rw.stdout) && /중복 capability/.test(rw.stdout) && /"Cache"/.test(rw.stdout);
362
+ // JSON
363
+ const rj = cp.spawnSync(process.execPath, [CLI, 'reuse-map', '--include', `${tmpA},${tmpB}`, '--json'], { encoding: 'utf8', timeout: 15000, cwd: os.tmpdir() });
364
+ let parsed = null;
365
+ try { parsed = JSON.parse(rj.stdout); } catch {}
366
+ const okJson = rj.status === 0 && parsed && Array.isArray(parsed.duplicates) && parsed.duplicates.length >= 1;
367
+ const ok = okSingle && okMulti && okJson;
368
+ console.log(ok ? '✓ B(1.9.17) reuse-map 단일/워크스페이스/JSON + 중복 감지' : `✗ reuse-map 실패 (단일=${okSingle} 멀티=${okMulti} JSON=${okJson})`);
369
+ if (!ok) { failed++; console.log(rw.stdout.slice(0, 400)); }
370
+ }
371
+
372
+ // 1.9.18 회귀: handoff --since / reuse-map --strict-elements + depends-on / verify-claim
373
+ total++;
374
+ {
375
+ // handoff --since: 최근 변경 강조
376
+ const tmpA = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-sincea-'));
377
+ const tmpB = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-sinceb-'));
378
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpA, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
379
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpB, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
380
+ // 오늘 날짜로 T-row 추가
381
+ const today = new Date().toISOString().slice(0,10);
382
+ fs.appendFileSync(path.join(tmpA, '.harness/progress-tracker.md'), `| T-9999 | done | 신규 기능 | src/x.js | M-NEW | ${today} |\n`);
383
+ const r = cp.spawnSync(process.execPath, [CLI, 'handoff', '--include', `${tmpA},${tmpB}`, '--since', '1d'], { encoding: 'utf8', timeout: 15000, cwd: os.tmpdir() });
384
+ const ok = r.status === 0 && /1\.9\.18/.test(r.stdout) && /Filter: since 1d/.test(r.stdout) && /🆕/.test(r.stdout) && /최근 변경/.test(r.stdout);
385
+ console.log(ok ? '✓ B(1.9.18) handoff --since: 최근 변경 강조' : '✗ handoff --since 실패');
386
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
387
+ }
388
+
389
+ total++;
390
+ {
391
+ // handoff --since 형식 오류 → fail
392
+ const tmpE = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-sincee-'));
393
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpE, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
394
+ const r = cp.spawnSync(process.execPath, [CLI, 'handoff', '--include', tmpE, '--since', 'banana'], { encoding: 'utf8', timeout: 15000, cwd: os.tmpdir() });
395
+ const ok = r.status !== 0 && /형식 오류/.test(r.stdout + r.stderr);
396
+ console.log(ok ? '✓ B(1.9.18) handoff --since 형식 오류 → exit≠0' : '✗ handoff --since 오류 검증 실패');
397
+ if (!ok) failed++;
398
+ }
399
+
400
+ total++;
401
+ {
402
+ // reuse-map --strict-elements: 같은 함수명 다른 capability 감지
403
+ const tmpA = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-strict-a-'));
404
+ const tmpB = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-strict-b-'));
405
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpA, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
406
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpB, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
407
+ // 같은 함수 escapeHtml을 다른 capability 이름으로 등록
408
+ fs.appendFileSync(path.join(tmpA, '.harness/reuse-map.md'), '| HtmlEscape | src/util.js (escapeHtml) | util | XSS 방지 |\n');
409
+ fs.appendFileSync(path.join(tmpB, '.harness/reuse-map.md'), '| EscapeHtml | src/build.js (escapeHtml) | util | 마크업 이스케이프 |\n');
410
+ // 기본 모드 → 정확 중복 0
411
+ const r1 = cp.spawnSync(process.execPath, [CLI, 'reuse-map', '--include', `${tmpA},${tmpB}`], { encoding: 'utf8', timeout: 15000, cwd: os.tmpdir() });
412
+ const okDefault = r1.status === 0 && /정확 중복 capability/.test(r1.stdout) && /\(없음\)/.test(r1.stdout);
413
+ // --strict-elements → 잠재 중복 1건
414
+ const r2 = cp.spawnSync(process.execPath, [CLI, 'reuse-map', '--include', `${tmpA},${tmpB}`, '--strict-elements'], { encoding: 'utf8', timeout: 15000, cwd: os.tmpdir() });
415
+ const okStrict = r2.status === 0 && /잠재 중복/.test(r2.stdout) && /escapeHtml/.test(r2.stdout) && /HtmlEscape/.test(r2.stdout) && /EscapeHtml/.test(r2.stdout);
416
+ // JSON
417
+ const r3 = cp.spawnSync(process.execPath, [CLI, 'reuse-map', '--include', `${tmpA},${tmpB}`, '--strict-elements', '--json'], { encoding: 'utf8', timeout: 15000, cwd: os.tmpdir() });
418
+ let parsed = null;
419
+ try { parsed = JSON.parse(r3.stdout); } catch {}
420
+ const okJson = r3.status === 0 && parsed && Array.isArray(parsed.fuzzyDuplicates) && parsed.fuzzyDuplicates.length === 1 && parsed.fuzzyDuplicates[0].functionName === 'escapehtml';
421
+ const ok = okDefault && okStrict && okJson;
422
+ console.log(ok ? '✓ B(1.9.18) reuse-map --strict-elements 잠재 중복 감지' : `✗ strict-elements 실패 (default=${okDefault} strict=${okStrict} json=${okJson})`);
423
+ if (!ok) { failed++; console.log(r2.stdout.slice(0, 500)); }
424
+ }
425
+
426
+ total++;
427
+ {
428
+ // reuse-map depends-on: notes 컬럼에서 의존 추출
429
+ const tmpA = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-deps-'));
430
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpA, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
431
+ fs.appendFileSync(path.join(tmpA, '.harness/reuse-map.md'),
432
+ '| EscapeHtml | src/build.js (escapeHtml) | util | XSS 방지 |\n' +
433
+ '| RssFeed | src/build.js (buildFeed) | util | RSS 2.0 (depends-on: EscapeHtml) |\n');
434
+ const r = cp.spawnSync(process.execPath, [CLI, 'reuse-map', '--include', tmpA], { encoding: 'utf8', timeout: 15000, cwd: os.tmpdir() });
435
+ const ok = r.status === 0 && /의존 관계 \(depends-on, 1개 엣지\)/.test(r.stdout) && /RssFeed.*─→.*EscapeHtml/.test(r.stdout);
436
+ console.log(ok ? '✓ B(1.9.18) reuse-map depends-on 엣지 추출' : '✗ depends-on 실패');
437
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
438
+ }
439
+
440
+ total++;
441
+ {
442
+ // verify-claim: 파일 존재 + 테스트 카운트 검증
443
+ const tmpV = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-vc-'));
444
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpV, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
445
+ // 실제 src 파일 + 테스트 파일 생성 (5개 check)
446
+ fs.mkdirSync(path.join(tmpV, 'src'), { recursive: true });
447
+ fs.mkdirSync(path.join(tmpV, 'tests'), { recursive: true });
448
+ fs.writeFileSync(path.join(tmpV, 'src/myMod.js'), 'module.exports = {};\n');
449
+ fs.writeFileSync(path.join(tmpV, 'tests/test.js'), 'check(1); check(2); check(3); check(4); check(5);\n');
450
+ // T-row를 evidence와 함께 추가
451
+ fs.appendFileSync(path.join(tmpV, '.harness/progress-tracker.md'),
452
+ '| T-0099 | done | 신모듈 | src/myMod.js + tests/test.js (5/5 통과) | next | 2026-05-14 |\n');
453
+ // 정상: 파일 존재 + 테스트 5개
454
+ const r = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0099', '--path', tmpV], { encoding: 'utf8', timeout: 15000 });
455
+ const okPass = r.status === 0 && /✓ src\/myMod\.js/.test(r.stdout) && /✓ tests\/test\.js/.test(r.stdout) && /pass \(실측 ≥ 주장\)/.test(r.stdout);
456
+ // 파일 없는 케이스 → exit ≠ 0
457
+ fs.unlinkSync(path.join(tmpV, 'src/myMod.js'));
458
+ const r2 = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0099', '--path', tmpV], { encoding: 'utf8', timeout: 15000 });
459
+ const okFail = r2.status !== 0 && /✗ src\/myMod\.js/.test(r2.stdout) && /FAIL/.test(r2.stdout);
460
+ // JSON
461
+ const r3 = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0099', '--path', tmpV, '--json'], { encoding: 'utf8', timeout: 15000 });
462
+ let parsed = null;
463
+ try { parsed = JSON.parse(r3.stdout); } catch {}
464
+ const okJson = parsed && parsed.taskId === 'T-0099' && parsed.verdict && parsed.verdict.filesAllExist === false;
465
+ const ok = okPass && okFail && okJson;
466
+ console.log(ok ? '✓ B(1.9.18) verify-claim 파일/테스트 검증 + exit code + JSON' : `✗ verify-claim 실패 (pass=${okPass} fail=${okFail} json=${okJson})`);
467
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
468
+ }
469
+
470
+ // 1.9.15 회귀: brainstorm 라인번호 / --all-apps / --include
471
+ total++;
472
+ {
473
+ const tmpL = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-line-'));
474
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpL, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
475
+ fs.appendFileSync(path.join(tmpL, '.harness/decisions.md'), '\n### 2026-05-13 — 캐시 정책 결정\n- Reason: rate limit 회피\n');
476
+ cp.spawnSync(process.execPath, [CLI, 'plan', 'add', '캐시 helper 구현', '--path', tmpL], { stdio: 'ignore', timeout: 10000 });
477
+ const r = cp.spawnSync(process.execPath, [CLI, 'brainstorm', '캐시', '--path', tmpL], { encoding: 'utf8', timeout: 15000 });
478
+ const ok = /\.harness\/decisions\.md:\d+/.test(r.stdout) && /\.harness\/progress-tracker\.md:\d+/.test(r.stdout) && /matched: request/.test(r.stdout);
479
+ console.log(ok ? '✓ B(1.9.15) brainstorm: 파일:라인 + 매치 필드 표시' : `✗ 1.9.15 brainstorm 위치 실패\n${r.stdout.slice(0,500)}`);
480
+ if (!ok) failed++;
481
+ }
482
+ total++;
483
+ {
484
+ // --include 다중 경로 통합
485
+ const tmpA = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-wsA-'));
486
+ const tmpB = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-wsB-'));
487
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpA, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
488
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpB, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
489
+ cp.spawnSync(process.execPath, [CLI, 'plan', 'add', 'A 작업', '--status', 'done', '--path', tmpA], { stdio: 'ignore', timeout: 10000 });
490
+ cp.spawnSync(process.execPath, [CLI, 'plan', 'add', 'B 작업', '--status', 'planned', '--path', tmpB], { stdio: 'ignore', timeout: 10000 });
491
+ const r = cp.spawnSync(process.execPath, [CLI, 'retro', '--include', `${tmpA},${tmpB}`], { encoding: 'utf8', timeout: 15000, cwd: os.tmpdir() });
492
+ const ok = r.status === 0 && /Cross-project retro — 2개 프로젝트/.test(r.stdout) && /워크스페이스 총합/.test(r.stdout);
493
+ console.log(ok ? '✓ B(1.9.15) retro --include: 2개 통합' : '✗ 1.9.15 retro --include 실패');
494
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 600)); }
495
+ }
496
+ total++;
497
+ {
498
+ // insights --include
499
+ const tmpA = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-iA-'));
500
+ const tmpB = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-iB-'));
501
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpA, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
502
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpB, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
503
+ const r = cp.spawnSync(process.execPath, [CLI, 'insights', '--include', `${tmpA},${tmpB}`], { encoding: 'utf8', timeout: 15000, cwd: os.tmpdir() });
504
+ const ok = r.status === 0 && /Workspace Insights — 2개/.test(r.stdout) && /TOTAL/.test(r.stdout);
505
+ console.log(ok ? '✓ B(1.9.15) insights --include: 표 형식 통합' : '✗ 1.9.15 insights --include 실패');
506
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 600)); }
507
+ }
508
+ total++;
509
+ {
510
+ // 잘못된 --include 경로 — warn 출력 + .harness 있는 것만 처리
511
+ const tmpA = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-bad-'));
512
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpA, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
513
+ const bad = '/tmp/nonexistent-leerness-' + Date.now();
514
+ const r = cp.spawnSync(process.execPath, [CLI, 'retro', '--include', `${tmpA},${bad}`], { encoding: 'utf8', timeout: 15000, cwd: os.tmpdir() });
515
+ const ok = r.status === 0 && /--include 무시/.test(r.stdout) && /Cross-project retro — 1개/.test(r.stdout);
516
+ console.log(ok ? '✓ B(1.9.15) --include 잘못된 경로 graceful skip' : '✗ 1.9.15 bad path 처리 실패');
517
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
518
+ }
519
+
520
+ // 1.9.14 회귀: A(Template 제외) / B(word boundary) / C(planned 포함) / D(코드블록 템플릿)
521
+ total++;
522
+ {
523
+ // A: init 직후 decisions.md의 Template이 결정으로 카운트되지 않아야 함
524
+ const tmpA = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-A-'));
525
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpA, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
526
+ const r = cp.spawnSync(process.execPath, [CLI, 'insights', tmpA], { encoding: 'utf8', timeout: 15000 });
527
+ const ok = r.status === 0 && /누적 결정 \(decisions\.md\): 0건/.test(r.stdout);
528
+ console.log(ok ? '✓ B(1.9.14-A) Template 제외: 누적 결정 0건' : `✗ A 실패\n${r.stdout.slice(0, 500)}`);
529
+ if (!ok) failed++;
530
+ }
531
+ total++;
532
+ {
533
+ // B: brainstorm 토큰 매칭 — "API"는 매치, "AP"는 부분 매치라 안 잡힘
534
+ const tmpB = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-B-'));
535
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpB, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
536
+ fs.appendFileSync(path.join(tmpB, '.harness/decisions.md'), '\n### 2026-05-13 — API rate limit 정책\n- Reason: ...\n');
537
+ // "limit" 매치
538
+ const r1 = cp.spawnSync(process.execPath, [CLI, 'brainstorm', 'limit', '--path', tmpB], { encoding: 'utf8', timeout: 15000 });
539
+ // "lim" 부분 매치 — 매치되면 안 됨
540
+ const r2 = cp.spawnSync(process.execPath, [CLI, 'brainstorm', 'lim', '--path', tmpB], { encoding: 'utf8', timeout: 15000 });
541
+ const ok = /총 1건/.test(r1.stdout) && /총 0건/.test(r2.stdout);
542
+ console.log(ok ? '✓ B(1.9.14-B) brainstorm word boundary: limit 매치 / lim 부분매치 안 잡힘' : `✗ B 실패\n${r1.stdout.slice(0, 200)}\n${r2.stdout.slice(0, 200)}`);
543
+ if (!ok) failed++;
544
+ }
545
+ total++;
546
+ {
547
+ // C: retro 다음 우선 작업에 planned 포함
548
+ const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-C-'));
549
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
550
+ // 모든 task 제거 후 planned만 추가
551
+ fs.writeFileSync(path.join(tmpC, '.harness/progress-tracker.md'), `# Progress Tracker\nStatus values: requested, planned, in-progress, waiting, on-hold, blocked, incomplete, done, dropped\n\n| ID | Status | Request | Evidence | Next Action | Updated |\n|---|---|---|---|---|---|\n| T-0001 | planned | 미래 작업 | plan:M-0001 | 시작 예정 | 2026-05-13 |\n`);
552
+ const r = cp.spawnSync(process.execPath, [CLI, 'retro', tmpC], { encoding: 'utf8', timeout: 15000 });
553
+ const ok = r.status === 0 && /T-0001 \[planned\]/.test(r.stdout) && !/없음 — 새 plan add 권장/.test(r.stdout);
554
+ console.log(ok ? '✓ B(1.9.14-C) retro 다음 우선 작업에 planned 포함' : '✗ C 실패');
555
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 600)); }
556
+ }
557
+ total++;
558
+ {
559
+ // D: init decisions.md가 ```md 코드블록으로 감싸짐
560
+ const tmpD = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-D-'));
561
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpD, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
562
+ const dec = fs.readFileSync(path.join(tmpD, '.harness/decisions.md'), 'utf8');
563
+ const ok = /```md\n### \d{4}-\d{2}-\d{2} — Decision/.test(dec) && /^## Template/m.test(dec);
564
+ console.log(ok ? '✓ B(1.9.14-D) decisions.md template 코드블록 감싸짐' : '✗ D 실패');
565
+ if (!ok) { failed++; console.log(dec.slice(0, 400)); }
566
+ }
567
+
240
568
  // 1.9.13: retro / insights / brainstorm
241
569
  total++;
242
570
  {