leerness 1.9.40 → 1.9.41

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 CHANGED
@@ -1,5 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.41 — 2026-05-19
4
+
5
+ **디스크 마이그레이션 ↔ AI 컨텍스트 인지 갭 차단 — 맞춤형 차분 마이그레이션**.
6
+
7
+ 사용자 통찰: 같은 채팅 세션에서 leerness를 latest로 migrate해도, AI 에이전트는 이전 청크의 마인드셋으로 계속 작업하여 신규 도구(release pack, drift check 등)를 자동으로 호출하지 않는 패턴 발견. migrate는 파일만 업데이트, AI에겐 "새 도구가 들어왔다"는 신호 전달 부재.
8
+
9
+ ### Added
10
+
11
+ - **`leerness whats-new [--from V] [--to V] [--json]`** 신규 명령 — CHANGELOG.md를 자동 파싱하여 두 버전 사이의 차분 추출:
12
+ - 신규 명령 (`leerness X` 패턴), 신규 플래그 (`--xxx`), 신규 파일 (`.harness/*.md`) 자동 분류
13
+ - 각 버전의 헤드라인 (`**...**` 또는 첫 라인) 추출
14
+ - AI 가독 권장 행동 자동 출력
15
+ - **`migrate` 후 stdout에 AI must re-read 차분 자동 출력** — migrate 직전 이전 버전을 캡처 (`_previousVersion`) → CHANGELOG 차분 추출 → 신규 명령/파일을 stdout에 즉시 표시:
16
+ - "이전 청크의 기억 무효 — 새 도구 우선 시도" 명시
17
+ - 같은 세션 내 AI 인-컨텍스트에 신규 도구 인지 주입
18
+ - **`migration-report.md`에 "🤖 AI must re-read" 섹션 영구 기록** — 신규 명령/플래그/파일 + 버전별 헤드라인 + 권장 행동
19
+ - **`handoff`가 fresh migration-report (24h 내) 시 자동 알림** — "🆕 최근 N시간 전 migrate 차분" 블록 자동 표시. 같은 세션 내 매 handoff 호출이 AI에게 신규 도구 재안내.
20
+
21
+ ### 발견된 시스템 결함 (이번 라운드 해결)
22
+ - ❌ **before 1.9.41**: migrate가 파일만 업데이트, AI 마인드셋 stale 유지 → 신규 도구 자동 호출 X
23
+ - ✅ **1.9.41 이후**: migrate 직후 stdout + migration-report.md + handoff 모두 신규 도구를 AI 가독 포맷으로 노출 → "잊을 수 없는" 차분 안내
24
+
25
+ ### 자기 검증
26
+ - 의도적으로 root를 1.9.37로 되돌림 → `leerness migrate .` 호출 → **AI must re-read 차분 자동 stdout 출력**:
27
+ - `📌 신규 명령: leerness release pack`
28
+ - 1.9.38/1.9.39/1.9.40 버전별 헤드라인 자동 추출
29
+ - 권장 행동 4단계 (--help, 신규 파일 재독, 인스트럭션 재독, whats-new --json)
30
+
31
+ ### e2e: 186/186 PASS (1.9.40 182 + 신규 4)
32
+
33
+ ### 정책
34
+ - ✅ 차분 안내는 **AI 가독 포맷** (`**📌**`, `` `leerness X` `` 등 마크다운)
35
+ - ✅ 같은 세션 내 다양한 채널 (stdout + report + handoff)로 *반복 노출* → 청크 stale 방지
36
+ - ✅ 추출은 CHANGELOG.md 파싱 — 새 라운드 마다 자동 갱신
37
+
3
38
  ## 1.9.40 — 2026-05-19
4
39
 
5
40
  **dogfooding gap 차단 — `leerness release pack` 통합 명령 + audit README mismatch 자동 감지**.
package/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
  ║ ██║ ██╔══╝ ██╔══╝ ██╔══██╗██║╚██╗██║██╔══╝ ╚════██║ ║
13
13
  ║ ███████╗███████╗███████╗██║ ██║██║ ╚████║███████╗███████║ ║
14
14
  ║ ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝ ║
15
- ║ v1.9.40 AI Agent Reliability Harness ║
15
+ ║ v1.9.41 AI Agent Reliability Harness ║
16
16
  ║ verify · remember · orchestrate · audit · prevent drift ║
17
17
  ╚══════════════════════════════════════════════════════════════╝
18
18
  ```
@@ -394,6 +394,7 @@ npm test # = node ./scripts/e2e.js
394
394
 
395
395
  ## 변경 이력 (최근)
396
396
 
397
+ - **1.9.41** — 디스크↔AI 컨텍스트 인지 갭 차단: `leerness whats-new` 명령 (CHANGELOG 자동 차분 추출) · `migrate` 후 stdout에 AI must re-read 차분 자동 출력 · `migration-report.md`에 신규 명령/파일 영구 기록 · `handoff`가 fresh migrate(24h 내) 시 자동 알림.
397
398
  - **1.9.40** — dogfooding gap 차단: `leerness release pack` 통합 명령 (라운드 마감 자동화 — npm pack + parent migrate + task add + close + readme sync) · `audit`에 README ↔ package.json version mismatch 자동 감지 + `--fix`로 자동 갱신.
398
399
  - **1.9.39** — AI 하네스 엔지니어링 6단계 워크플로 자동 유도 (`session-workflow.md` + handoff 끝 가이드 + AGENTS/CLAUDE 인스트럭션 통합) · `drift check --auto-fix` · `handoff --auto-recover` (critical 시 session close 자동 실행).
399
400
  - **1.9.38** — drift 자동 reminder (`agent-reminders.md`) · `usage stats` 명령 · `task sync --from <todo.json>` · drift 임계 학습 (skip ≥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.40';
9
+ const VERSION = '1.9.41';
10
10
  const MARK = '<!-- leerness:managed -->';
11
11
  const README_START = '<!-- leerness:project-readme:start -->';
12
12
  const README_END = '<!-- leerness:project-readme:end -->';
@@ -411,10 +411,43 @@ function mergeLinesFile(p, lines) {
411
411
  writeUtf8(p, next);
412
412
  }
413
413
 
414
- function writeMigrationReport(root, backup, actions) {
414
+ function writeMigrationReport(root, backup, actions, opts = {}) {
415
415
  const p = path.join(root, '.harness/migration-report.md');
416
416
  const rows = actions.map(a => `| ${a.file} | ${a.action} |`).join('\n');
417
- writeUtf8(p, `# Leerness Migration Report\n\nVersion: ${VERSION}\nDate: ${now()}\nBackup: ${rel(root, backup.archiveDir)}\n\n## Policy\n\n- Existing harness, skill, and instruction files are backed up before migration.\n- Project memory files are preserved by default.\n- Managed instruction files are merged with previous content instead of being blindly overwritten.\n- .env.example/.gitignore are line-merged only.\n\n## Backed Up Candidates\n\n${backup.candidates.map(x => '- ' + x).join('\n')}\n\n## File Actions\n\n| File | Action |\n|---|---|\n${rows}\n`);
417
+ // 1.9.41: AI must re-read 섹션 migrate가 추가/변경한 파일을 AI 가독 포맷으로 추출
418
+ // fromV가 명시되면 CHANGELOG 차분 포함
419
+ let aiReadBlock = '';
420
+ try {
421
+ const fromV = opts.fromV || (backup && backup.previousVersion) || null;
422
+ if (fromV && fromV !== VERSION) {
423
+ const changelogPath = path.join(__dirname, '..', 'CHANGELOG.md');
424
+ const cl = exists(changelogPath) ? read(changelogPath) : (exists(path.join(root, 'CHANGELOG.md')) ? read(path.join(root, 'CHANGELOG.md')) : '');
425
+ if (cl) {
426
+ const diff = _parseChangelogBetween(cl, fromV, VERSION);
427
+ const allCommands = new Set(), allFlags = new Set(), allFiles = new Set();
428
+ for (const v of diff) {
429
+ v.newCommands.forEach(c => allCommands.add(c));
430
+ v.newFlags.forEach(f => allFlags.add(f));
431
+ v.newFiles.forEach(f => allFiles.add(f));
432
+ }
433
+ if (diff.length) {
434
+ aiReadBlock = `\n## 🤖 AI must re-read (1.9.41 차분 안내)\n\n`;
435
+ aiReadBlock += `이 migrate는 ${fromV} → ${VERSION} 점프입니다. 메인 AI 에이전트는 다음을 인지하고 우선 활용:\n\n`;
436
+ if (allCommands.size) aiReadBlock += `**📌 신규 명령** (이전엔 없던 것):\n${[...allCommands].map(c => `- \`leerness ${c}\``).join('\n')}\n\n`;
437
+ if (allFlags.size) aiReadBlock += `**🚩 신규 플래그**:\n${[...allFlags].map(f => `- \`${f}\``).join('\n')}\n\n`;
438
+ if (allFiles.size) aiReadBlock += `**📄 신규/변경 파일** (반드시 재독):\n${[...allFiles].map(f => `- \`${f}\``).join('\n')}\n\n`;
439
+ aiReadBlock += `**버전별 헤드라인**:\n`;
440
+ for (const v of diff) {
441
+ const firstLine = (v.body.match(/^\*\*([^*]+)\*\*/) || [])[1]
442
+ || (v.body.split('\n').find(l => l.trim() && !l.startsWith('##')) || '').trim().slice(0, 120);
443
+ aiReadBlock += `- ${v.version} — ${firstLine || '(no headline)'}\n`;
444
+ }
445
+ aiReadBlock += `\n**권장 행동**:\n1. 위 신규 명령을 \`--help\`로 확인\n2. \`AGENTS.md\` / \`CLAUDE.md\` / \`.harness/session-workflow.md\` 재독 (다음 \`leerness handoff\` 호출 시 자동 안내)\n3. 이전 청크의 기억 무효 — 새 도구 우선 시도\n4. 상세: \`leerness whats-new --from ${fromV}\`\n`;
446
+ }
447
+ }
448
+ }
449
+ } catch {}
450
+ writeUtf8(p, `# Leerness Migration Report\n\nVersion: ${VERSION}\nDate: ${now()}\nBackup: ${rel(root, backup.archiveDir)}\n${opts.fromV ? `Previous: ${opts.fromV}\n` : ''}${aiReadBlock}\n## Policy\n\n- Existing harness, skill, and instruction files are backed up before migration.\n- Project memory files are preserved by default.\n- Managed instruction files are merged with previous content instead of being blindly overwritten.\n- .env.example/.gitignore are line-merged only.\n\n## Backed Up Candidates\n\n${backup.candidates.map(x => '- ' + x).join('\n')}\n\n## File Actions\n\n| File | Action |\n|---|---|\n${rows}\n`);
418
451
  }
419
452
 
420
453
  function syncReadme(root) {
@@ -517,6 +550,14 @@ async function resolveInstallOptions(root, opts = {}) {
517
550
 
518
551
  async function install(root, opts = {}) {
519
552
  root = absRoot(root); mkdirp(root);
553
+ // 1.9.41: migrate 직전 이전 버전 캡처 — 차분 안내에 사용
554
+ try {
555
+ const hv = path.join(root, '.harness', 'HARNESS_VERSION');
556
+ if (exists(hv) && !opts._previousVersion) {
557
+ const parsed = parseHarnessVersion(read(hv));
558
+ opts._previousVersion = parsed.base || parsed.plus || null;
559
+ }
560
+ } catch {}
520
561
  // 1.9.32: init 시 ASCII 배너 + 빠른 시작 가이드 (migrate는 quiet)
521
562
  if (!opts.migration && !has('--no-banner')) _banner({ quickStart: !opts.dry });
522
563
  // 1.9.33: npx 캐시로 옛 버전이 실행될 때 경고 (migrate/--no-stale-check 시 스킵)
@@ -588,7 +629,23 @@ async function install(root, opts = {}) {
588
629
  ]);
589
630
  syncReadme(root);
590
631
  installSkills(root, skills);
591
- writeMigrationReport(root, backup, actions);
632
+ // 1.9.41: migrate 시 이전 버전을 미리 캡처해 차분 안내에 사용
633
+ writeMigrationReport(root, backup, actions, { fromV: opts._previousVersion || null });
634
+ // 1.9.41: migrate 후 (= 점프인 경우) 차분 안내를 stdout에 즉시 출력 — AI 컨텍스트에 새 도구 주입
635
+ if (opts.migration && opts._previousVersion && opts._previousVersion !== VERSION) {
636
+ try {
637
+ const reportPath = path.join(root, '.harness', 'migration-report.md');
638
+ if (exists(reportPath)) {
639
+ const rep = read(reportPath);
640
+ const aiBlock = rep.match(/## 🤖 AI must re-read[\s\S]*?(?=\n## )/);
641
+ if (aiBlock) {
642
+ log('');
643
+ log(aiBlock[0].trim());
644
+ log('');
645
+ }
646
+ }
647
+ } catch {}
648
+ }
592
649
  // 1.9.1 P7: 디폴트 M-0001이 plan에 있고 progress에 row가 없으면 자동 추가
593
650
  try {
594
651
  const planText = exists(planPath(root)) ? read(planPath(root)) : '';
@@ -1425,6 +1482,29 @@ function handoff(root) {
1425
1482
  const cs = read(currentStatePath(root)).replace(/Updated: \d{4}-\d{2}-\d{2}/, `Updated: ${today()}`);
1426
1483
  writeUtf8(currentStatePath(root), cs);
1427
1484
  }
1485
+ // 1.9.41: 최근 migrate 차분 알림 — migration-report.md가 24h 내면 "AI must re-read" 블록 자동 표시
1486
+ // 같은 채팅 세션의 AI 청크가 이전 버전 마인드셋이어도 새 도구를 즉시 인지하도록.
1487
+ if (!has('--no-workflow-guide') && !has('--compact')) {
1488
+ try {
1489
+ const reportPath = path.join(root, '.harness', 'migration-report.md');
1490
+ if (exists(reportPath)) {
1491
+ const stat = fs.statSync(reportPath);
1492
+ const ageHr = (Date.now() - stat.mtimeMs) / 3600000;
1493
+ if (ageHr < 24) {
1494
+ const rep = read(reportPath);
1495
+ const aiBlock = rep.match(/## 🤖 AI must re-read[\s\S]*?(?=\n## )/);
1496
+ if (aiBlock) {
1497
+ const isTty = process.stdout && process.stdout.isTTY;
1498
+ const yel = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
1499
+ log('');
1500
+ log(yel(`## 🆕 최근 ${ageHr.toFixed(1)}시간 전 migrate 차분 — AI 에이전트는 신규 도구 우선 시도`));
1501
+ log(aiBlock[0].trim());
1502
+ log('');
1503
+ }
1504
+ }
1505
+ }
1506
+ } catch {}
1507
+ }
1428
1508
  // 1.9.39: handoff 출력 끝에 6단계 워크플로 가이드 자동 표시 (메인 에이전트가 매 세션 인지)
1429
1509
  if (!has('--no-workflow-guide') && !has('--compact') && process.env.LEERNESS_NO_WORKFLOW_GUIDE !== '1') {
1430
1510
  const isTty = process.stdout && process.stdout.isTTY;
@@ -5698,6 +5778,107 @@ function _bumpUsage(root, cmdName) {
5698
5778
  } catch {}
5699
5779
  }
5700
5780
 
5781
+ // 1.9.41: CHANGELOG.md를 파싱하여 from → to 사이 버전 차분 추출
5782
+ // 반환: [{ version, date, body, newCommands, newFlags, newFiles }]
5783
+ function _parseChangelogBetween(changelogText, fromV, toV) {
5784
+ // ## 1.9.X — YYYY-MM-DD 헤더 사이의 텍스트 추출
5785
+ const sections = [];
5786
+ const re = /^## (\d+\.\d+\.\d+)(?:\s+—\s+(\d{4}-\d{2}-\d{2}))?\s*\n([\s\S]*?)(?=^## \d+\.\d+\.\d+|$)/gm;
5787
+ let m;
5788
+ while ((m = re.exec(changelogText)) !== null) {
5789
+ sections.push({ version: m[1], date: m[2] || null, body: m[3].trim() });
5790
+ }
5791
+ // from < V <= to 만 (fromV 자체는 이미 적용된 버전이므로 제외)
5792
+ const ranged = sections.filter(s => {
5793
+ const cmp = (v1, v2) => {
5794
+ const a = v1.split('.').map(Number), b = v2.split('.').map(Number);
5795
+ for (let i = 0; i < 3; i++) { if (a[i] !== b[i]) return a[i] - b[i]; }
5796
+ return 0;
5797
+ };
5798
+ return cmp(s.version, fromV) > 0 && cmp(s.version, toV) <= 0;
5799
+ });
5800
+ // 각 섹션에서 신규 명령/플래그/파일 추출
5801
+ for (const s of ranged) {
5802
+ s.newCommands = [];
5803
+ s.newFlags = [];
5804
+ s.newFiles = [];
5805
+ // `leerness X [...]` 또는 backtick에 싸인 leerness 명령
5806
+ for (const cm of s.body.matchAll(/`leerness\s+([a-z][\w-]*(?:\s+[a-z][\w-]*)?)/g)) {
5807
+ const cmd = cm[1].trim();
5808
+ if (!s.newCommands.includes(cmd)) s.newCommands.push(cmd);
5809
+ }
5810
+ // `--xxx` 플래그
5811
+ for (const fm of s.body.matchAll(/`(--[a-z][\w-]*)`/g)) {
5812
+ if (!s.newFlags.includes(fm[1])) s.newFlags.push(fm[1]);
5813
+ }
5814
+ // .harness/X.md 같은 신규 파일
5815
+ for (const ff of s.body.matchAll(/`(\.harness\/[\w./-]+\.(?:md|json|jsonl))`/g)) {
5816
+ if (!s.newFiles.includes(ff[1])) s.newFiles.push(ff[1]);
5817
+ }
5818
+ }
5819
+ return ranged;
5820
+ }
5821
+
5822
+ // 1.9.41: leerness whats-new [--from V] — 현재 워크스페이스 버전 → leerness latest 차분
5823
+ function whatsNewCmd(root) {
5824
+ root = absRoot(root || process.cwd());
5825
+ const fromV = arg('--from', null) || (function () {
5826
+ const hv = path.join(root, '.harness', 'HARNESS_VERSION');
5827
+ if (exists(hv)) { try { return parseHarnessVersion(read(hv)).base || parseHarnessVersion(read(hv)).plus; } catch { return null; } }
5828
+ return null;
5829
+ })();
5830
+ const toV = arg('--to', null) || VERSION;
5831
+ if (!fromV) {
5832
+ fail('현재 버전을 파악할 수 없습니다. --from <version> 명시');
5833
+ return process.exit(1);
5834
+ }
5835
+ // CHANGELOG.md — 우선 root, 없으면 leerness-pkg 자체
5836
+ let changelogPath = path.join(root, 'CHANGELOG.md');
5837
+ if (!exists(changelogPath)) changelogPath = path.join(__dirname, '..', 'CHANGELOG.md');
5838
+ if (!exists(changelogPath)) {
5839
+ fail('CHANGELOG.md 없음');
5840
+ return process.exit(1);
5841
+ }
5842
+ const diff = _parseChangelogBetween(read(changelogPath), fromV, toV);
5843
+ if (has('--json')) { log(JSON.stringify({ from: fromV, to: toV, versions: diff }, null, 2)); return; }
5844
+ if (!diff.length) {
5845
+ log(`# leerness whats-new (1.9.41)`);
5846
+ log(`현재 ${fromV} ↔ 대상 ${toV}: 새 항목 없음 (또는 CHANGELOG에 기록 안 됨)`);
5847
+ return;
5848
+ }
5849
+ log(`# leerness whats-new (1.9.41)`);
5850
+ log(`현재 워크스페이스 버전: ${fromV} → 대상: ${toV}`);
5851
+ log(`범위: ${diff.length}개 버전 (${diff[0].version} → ${diff[diff.length - 1].version})`);
5852
+ log('');
5853
+ // AI 가독 요약 — 각 버전당 한 줄 + 신규 명령/플래그/파일
5854
+ log(`## 🆕 신규 명령·플래그·파일 (AI 에이전트는 다음 명령을 우선 시도)`);
5855
+ const allCommands = new Set();
5856
+ const allFlags = new Set();
5857
+ const allFiles = new Set();
5858
+ for (const v of diff) {
5859
+ v.newCommands.forEach(c => allCommands.add(c));
5860
+ v.newFlags.forEach(f => allFlags.add(f));
5861
+ v.newFiles.forEach(f => allFiles.add(f));
5862
+ }
5863
+ if (allCommands.size) log(` 📌 신규 명령: ${[...allCommands].join(', ')}`);
5864
+ if (allFlags.size) log(` 🚩 신규 플래그: ${[...allFlags].join(', ')}`);
5865
+ if (allFiles.size) log(` 📄 신규 파일: ${[...allFiles].join(', ')}`);
5866
+ log('');
5867
+ log(`## 📜 버전별 헤드라인`);
5868
+ for (const v of diff) {
5869
+ // body 첫 줄(또는 strong header) 추출
5870
+ const firstLine = (v.body.match(/^\*\*([^*]+)\*\*/) || [])[1]
5871
+ || (v.body.split('\n').find(l => l.trim() && !l.startsWith('##')) || '').trim().slice(0, 120);
5872
+ log(` • ${v.version}${v.date ? ` (${v.date})` : ''} — ${firstLine || '(no headline)'}`);
5873
+ }
5874
+ log('');
5875
+ log(`## 💡 권장 행동`);
5876
+ log(` 1. 위 신규 명령들을 시도해 보세요 (예: \`leerness <명령> --help\`)`);
5877
+ log(` 2. 신규 파일들을 읽어 보세요 (예: \`cat .harness/session-workflow.md\`)`);
5878
+ log(` 3. AGENTS.md/CLAUDE.md 재독 — migrate가 인스트럭션을 업데이트했을 수 있음`);
5879
+ log(` 4. 상세: \`cat CHANGELOG.md\` 또는 \`leerness whats-new --json\``);
5880
+ }
5881
+
5701
5882
  function usageStatsCmd(root) {
5702
5883
  root = absRoot(root || process.cwd());
5703
5884
  const stats = _readUsageStats(root);
@@ -5963,6 +6144,7 @@ async function main() {
5963
6144
  if (cmd === 'contract' && args[1] === 'verify') return contractVerifyCmd(args[2], args[3]);
5964
6145
  if (cmd === 'drift' && (args[1] === 'check' || !args[1])) return driftCheckCmd(args[2] || arg('--path', process.cwd()));
5965
6146
  if (cmd === 'usage' && (args[1] === 'stats' || !args[1])) return usageStatsCmd(args[2] || arg('--path', process.cwd()));
6147
+ if (cmd === 'whats-new') return whatsNewCmd(args[1] || arg('--path', process.cwd()));
5966
6148
  if (cmd === 'reuse' && args[1] === 'autodetect') return reuseAutodetectCmd(args[2] || arg('--path', process.cwd()));
5967
6149
  if (cmd === 'setup-agents' || cmd === 'setup' && args[1] === 'agents') return await setupAgentsCmd(args[1] && args[1] !== 'agents' ? args[1] : (args[2] || process.cwd()));
5968
6150
  if (cmd === 'session' && args[1] === 'close') { const r = sessionClose(args[2] || process.cwd()); viewworkEmit(args[2] || process.cwd(), { action: 'task', tool: 'session-close', note: 'session close' }); return r; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.40",
3
+ "version": "1.9.41",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -950,6 +950,66 @@ total++;
950
950
  if (!ok) { failed++; console.log(r.stdout.slice(0, 800)); }
951
951
  }
952
952
 
953
+ // 1.9.41 회귀: whats-new 명령 + migrate 차분 AI must re-read + handoff fresh 알림
954
+ total++;
955
+ {
956
+ // whats-new --from 큰 점프 → 신규 명령 추출
957
+ const r = cp.spawnSync(process.execPath, [CLI, 'whats-new', '--from', '1.9.33', '--json'], { encoding: 'utf8', timeout: 15000 });
958
+ let parsed = null;
959
+ try { parsed = JSON.parse(r.stdout); } catch {}
960
+ const ok = parsed
961
+ && parsed.from === '1.9.33'
962
+ && Array.isArray(parsed.versions)
963
+ && parsed.versions.length >= 5
964
+ && parsed.versions.some(v => v.newCommands && v.newCommands.length > 0);
965
+ console.log(ok ? '✓ B(1.9.41) whats-new --from 1.9.33: 5+ 버전 차분 + 신규 명령 추출' : `✗ whats-new 실패`);
966
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
967
+ }
968
+
969
+ total++;
970
+ {
971
+ // migrate가 fromV가 있는 경우 AI must re-read 블록을 stdout에 출력
972
+ const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-mig-'));
973
+ // 1.9.30 표시로 init한 척 (HARNESS_VERSION 직접 작성)
974
+ fs.mkdirSync(path.join(tmpC, '.harness'), { recursive: true });
975
+ fs.writeFileSync(path.join(tmpC, '.harness', 'HARNESS_VERSION'), 'leerness@1.9.36\n', 'utf8');
976
+ const r = cp.spawnSync(process.execPath, [CLI, 'migrate', tmpC, '--yes', '--no-banner', '--no-stale-check'], { encoding: 'utf8', timeout: 60000 });
977
+ const ok = r.status === 0
978
+ && /AI must re-read/.test(r.stdout)
979
+ && /1\.9\.36 → 1\.9\.4[01]/.test(r.stdout)
980
+ && /신규 명령/.test(r.stdout);
981
+ console.log(ok ? '✓ B(1.9.41) migrate stdout: AI must re-read 차분 자동 출력' : `✗ migrate 차분 출력 실패`);
982
+ if (!ok) { failed++; console.log(r.stdout.slice(-800)); }
983
+ }
984
+
985
+ total++;
986
+ {
987
+ // migration-report.md에 AI must re-read 섹션 영구 기록
988
+ const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-mig2-'));
989
+ fs.mkdirSync(path.join(tmpC, '.harness'), { recursive: true });
990
+ fs.writeFileSync(path.join(tmpC, '.harness', 'HARNESS_VERSION'), 'leerness@1.9.30\n', 'utf8');
991
+ cp.spawnSync(process.execPath, [CLI, 'migrate', tmpC, '--yes', '--no-banner', '--no-stale-check'], { stdio: 'ignore', timeout: 60000 });
992
+ const reportPath = path.join(tmpC, '.harness', 'migration-report.md');
993
+ const body = fs.existsSync(reportPath) ? fs.readFileSync(reportPath, 'utf8') : '';
994
+ const ok = /## 🤖 AI must re-read/.test(body) && /Previous: 1\.9\.30/.test(body);
995
+ console.log(ok ? '✓ B(1.9.41) migration-report.md: AI must re-read 섹션 + Previous 버전 기록' : `✗ report 기록 실패`);
996
+ if (!ok) { failed++; console.log(body.slice(0, 600)); }
997
+ }
998
+
999
+ total++;
1000
+ {
1001
+ // handoff가 fresh migration-report (24h 내) 시 자동 알림
1002
+ const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-fresh-'));
1003
+ fs.mkdirSync(path.join(tmpC, '.harness'), { recursive: true });
1004
+ fs.writeFileSync(path.join(tmpC, '.harness', 'HARNESS_VERSION'), 'leerness@1.9.30\n', 'utf8');
1005
+ cp.spawnSync(process.execPath, [CLI, 'migrate', tmpC, '--yes', '--no-banner', '--no-stale-check'], { stdio: 'ignore', timeout: 60000 });
1006
+ const r = cp.spawnSync(process.execPath, [CLI, 'handoff', tmpC, '--no-drift-check'], { encoding: 'utf8', timeout: 15000 });
1007
+ const ok = r.status === 0
1008
+ && /최근.*시간 전 migrate 차분|AI must re-read/.test(r.stdout);
1009
+ console.log(ok ? '✓ B(1.9.41) handoff: 최근 migrate 차분 자동 표시 (24h 내)' : `✗ handoff 차분 알림 실패`);
1010
+ if (!ok) { failed++; console.log(r.stdout.slice(-500)); }
1011
+ }
1012
+
953
1013
  // 1.9.40 회귀: release pack 통합 명령 + audit README mismatch 감지
954
1014
  total++;
955
1015
  {