leerness 1.36.69 → 1.36.71

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,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.36.71 — 2026-07-24 — 성장 한계 클래스 3: retro/insights --json rows 상한 + 워크스페이스 --days 기존 버그 — R-0001 검수 14회전
4
+
5
+ - **retro/insights --json 의 data.rows 전량 임베드**(무한 성장, 72 task 에 22KB)를 기본 최신 30행으로 상한 — `rowsTotal/rowsShown/rowsTruncated` 정직 표기, `--all/--limit N` 옵트인, **집계 수치(totalTasks 등)는 전량 기준 그대로**(수치 왜곡 없음). 4개 JSON 배출 지점(단일/워크스페이스 × retro/insights) 일괄. 실측 33KB → 14KB.
6
+ - (검수 High) e2e 가 init 기본 T-0001 을 몰라 하드코딩 35 로 확정 실패할 케이스 — 기준선 유도 방식 + init/add exit 단언으로 교정(--force 가 init 실패를 가리던 문제도 제거).
7
+ - (검수 Medium) MCP `leerness_retro` 에 `limit`/`all` 노출+전달 (whats-new 1.36.69 와 동일 계약 규율). insights 는 MCP 도구 자체가 없음(현행 유지).
8
+ - (검수 Medium, **기존 버그**) 워크스페이스 retro(`--include/--all-apps`)가 `--days` 를 조용히 무시 — cutoff 를 `_retroWorkspace` 로 전달해 단일 모드와 일관(실측: days 0→fix 0 / 36500→fix 3 분리 확인).
9
+ - (검수 Low) help 에 `[--limit N|--all]` + rowsTotal 의미(수명 전체, --days 창 아님) 명시.
10
+ - 검증: selftest 334, e2e +1. codex 확인: 텍스트 모드 바이트 동일, data.rows 하위 소비자 없음, focusNext/집계 무변형.
11
+
12
+ ## 1.36.70 — 2026-07-24 — 성장 한계 클래스 2: migrate 보고 상한 (88KB → 13.5KB) — R-0001 검수 13회전
13
+
14
+ 1.36.69 whats-new 상한과 동일 규율을 migrate 보고서의 "AI must re-read" 섹션에 적용. 큰 버전 점프(602 릴리스)에서 보고서가 ~88KB 로 폭주해 AI 컨텍스트를 태우던 것.
15
+
16
+ - **헤드라인 최신 30 + 명령/플래그/파일 목록 40 캡**, 각 헤더에 실제 총수 표기 + 정직한 "… 외 N개" 오버플로 라인 + `whats-new --from X --all/--limit N` 탈출구. 작은 점프에선 상한 비가시(전량 출력).
17
+ - (검수 #1 Medium) **광고된 `--limit N` 이 텍스트 모드에서 조용히 무시되던 것 수정** — 명시적 `--limit` 존중 + 오버플로 라인(기본 텍스트 출력은 전체 유지, 계약 무변경). 종전엔 보고서가 권하는 명령이 602줄을 그대로 뱉었다.
18
+ - (검수 #2) e2e 픽스처 `--no-stale-check`(빈 캐시 npm 조회 8초 지연 차단). (검수 #3) e2e 단언을 헤딩 파싱 기반 정확 일치(shown===30·행수·오버플로 수치)로 강화.
19
+ - 검수 실측(codex): 30/602 헤드라인 + 외 572 · 명령 40/170+130 · 플래그 40/133+93 · 파일 40/43+3 — 전 수치 독립 파싱과 일치. 소점프 무영향, 마크다운 정합, fromV 엣지(동일/쓰레기/미래) 전부 통과.
20
+ - 검증: selftest 334, e2e +1.
21
+
3
22
  ## 1.36.69 — 2026-07-22 — 성장 한계 클래스: whats-new --json 응답 상한 (804KB → 30KB) — R-0001 검수 12회전
4
23
 
5
24
  직전 게이트가 우연히 드러낸 누적 성장 문제를 픽스처 우회가 아니라 제품에서 해결. CHANGELOG 1MB 돌파로 `--json` 이 804KB 를 뱉어 AI 소비자 컨텍스트를 폭파시키고 기본 stdout 버퍼에서 절단됐다.
package/README.md CHANGED
@@ -122,7 +122,7 @@ MIT
122
122
  <!-- leerness:project-readme:start -->
123
123
  ## Leerness Project Harness
124
124
 
125
- 이 프로젝트는 Leerness v1.36.69 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
125
+ 이 프로젝트는 Leerness v1.36.71 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
126
126
 
127
127
  ### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
128
128
 
@@ -176,7 +176,7 @@ leerness memory restore decision <date|title>
176
176
 
177
177
  ### MCP server (외부 AI 통합)
178
178
 
179
- Leerness v1.36.69는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
179
+ Leerness v1.36.71는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
180
180
 
181
181
  ```jsonc
182
182
  // 카테고리별
@@ -197,7 +197,7 @@ Leerness v1.36.69는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
197
197
  `<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
198
198
  1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) 회귀 테스트 갱신 → 4) 전체 e2e 스위트 통과 → 5) npm publish + git tag → 6) main push → 7) session close → 8) 다음 라운드 예약.
199
199
 
200
- 현재 누적: **v1.9.x → 1.36.69 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
200
+ 현재 누적: **v1.9.x → 1.36.71 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
201
201
 
202
202
  ### 성능 가이드
203
203
 
@@ -235,5 +235,5 @@ leerness release pack --close --auto-main-push
235
235
  - `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
236
236
  - `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
237
237
 
238
- Last synced by Leerness v1.36.69: 2026-07-23
238
+ Last synced by Leerness v1.36.71: 2026-07-24
239
239
  <!-- leerness:project-readme:end -->
package/bin/leerness.js CHANGED
@@ -34,7 +34,7 @@ const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE
34
34
  const { tokenizeForRank: _tokenizeForRank, expandQuery: _expandQuery, scoreHits: _scoreHits, suggestTerms: _suggestTerms } = require('../lib/search-core'); // 1.36.23: memory search 랭킹 코어(순수·0-deps)
35
35
  const { findCorruptedStateJson: _findCorruptedStateJson } = require('../lib/state-integrity'); // 1.36.1 (클린룸 리뷰 FN): .harness/*.json 상태 무결성 (audit/health/check 공유)
36
36
 
37
- const VERSION = '1.36.69';
37
+ const VERSION = '1.36.71';
38
38
 
39
39
  // 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
40
40
  // 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
@@ -929,17 +929,22 @@ function writeMigrationReport(root, backup, actions, opts = {}) {
929
929
  v.newFiles.forEach(f => allFiles.add(f));
930
930
  }
931
931
  if (diff.length) {
932
+ // 1.36.70 (성장 한계 클래스, whats-new 1.36.69 와 동일 규율): 큰 버전 점프(601+ 릴리스)에서
933
+ // 보고서가 ~88KB 로 폭주해 AI 컨텍스트를 태우던 것 — 목록별 상한 + 정직한 "외 N개" 표기.
934
+ const _capList = (set, cap, fmt) => { const a = [...set]; return a.slice(0, cap).map(fmt).join('\n') + (a.length > cap ? `\n- … 외 ${a.length - cap}개 (전체: \`leerness whats-new --from ${fromV} --all\`)` : ''); };
932
935
  aiReadBlock = `\n## 🤖 AI must re-read (1.9.41 차분 안내)\n\n`;
933
- aiReadBlock += `이 migrate는 ${fromV} → ${VERSION} 점프입니다. 메인 AI 에이전트는 다음을 인지하고 우선 활용:\n\n`;
934
- if (allCommands.size) aiReadBlock += `**📌 신규 명령** (이전엔 없던 ):\n${[...allCommands].map(c => `- \`leerness ${c}\``).join('\n')}\n\n`;
935
- if (allFlags.size) aiReadBlock += `**🚩 신규 플래그**:\n${[...allFlags].map(f => `- \`${f}\``).join('\n')}\n\n`;
936
- if (allFiles.size) aiReadBlock += `**📄 신규/변경 파일** (반드시 재독):\n${[...allFiles].map(f => `- \`${f}\``).join('\n')}\n\n`;
937
- aiReadBlock += `**버전별 헤드라인**:\n`;
938
- for (const v of diff) {
936
+ aiReadBlock += `이 migrate는 ${fromV} → ${VERSION} 점프입니다 (${diff.length}개 릴리스). 메인 AI 에이전트는 다음을 인지하고 우선 활용:\n\n`;
937
+ if (allCommands.size) aiReadBlock += `**📌 신규 명령** (이전엔 없던 것, ${allCommands.size}개):\n${_capList(allCommands, 40, c => `- \`leerness ${c}\``)}\n\n`;
938
+ if (allFlags.size) aiReadBlock += `**🚩 신규 플래그** (${allFlags.size}개):\n${_capList(allFlags, 40, f => `- \`${f}\``)}\n\n`;
939
+ if (allFiles.size) aiReadBlock += `**📄 신규/변경 파일** (반드시 재독, ${allFiles.size}개):\n${_capList(allFiles, 40, f => `- \`${f}\``)}\n\n`;
940
+ const _hlCap = 30;
941
+ aiReadBlock += `**버전별 헤드라인** (최신 ${Math.min(_hlCap, diff.length)}/${diff.length}):\n`;
942
+ for (const v of diff.slice(0, _hlCap)) {
939
943
  const firstLine = (v.body.match(/^\*\*([^*]+)\*\*/) || [])[1]
940
944
  || (v.body.split('\n').find(l => l.trim() && !l.startsWith('##')) || '').trim().slice(0, 120);
941
945
  aiReadBlock += `- ${v.version} — ${firstLine || '(no headline)'}\n`;
942
946
  }
947
+ if (diff.length > _hlCap) aiReadBlock += `- … 외 ${diff.length - _hlCap}개 버전 — \`leerness whats-new --from ${fromV} --limit N\` 으로 조회\n`;
943
948
  aiReadBlock += `\n**권장 행동**:\n1. 위 신규 명령을 \`--help\`로 확인\n2. \`AGENTS.md\` / \`CLAUDE.md\` / \`.harness/session-workflow.md\` 재독 (다음 \`leerness handoff\` 호출 시 자동 안내)\n3. 이전 청크의 기억 무효 — 새 도구 우선 시도\n4. 상세: \`leerness whats-new --from ${fromV}\`\n`;
944
949
  }
945
950
  }
@@ -15297,21 +15302,31 @@ function _collectWorkspacePaths(rootBase) {
15297
15302
  return Array.from(set);
15298
15303
  }
15299
15304
 
15305
+ // 1.36.71 (성장 한계 클래스 3, whats-new 1.36.69 규율): retro/insights --json 이 data.rows 에 전체 task row 를
15306
+ // 임베드해 프로젝트와 함께 무한 성장(72 task 에 이미 22KB — AI 소비자 컨텍스트 낭비). 기본 최신 30행 +
15307
+ // rowsTotal/rowsShown/rowsTruncated 정직 표기, 전체는 --all, 개수는 --limit N. 집계 수치는 전량 기준 그대로.
15308
+ function _retroJsonData(agg) {
15309
+ const rawLim = parseInt(arg('--limit', '30'), 10);
15310
+ const lim = has('--all') ? agg.rows.length : (Number.isFinite(rawLim) && rawLim > 0 ? rawLim : 30);
15311
+ const rows = agg.rows.slice(-lim); // progress rows 는 오래된 것부터 — 최신 lim 개
15312
+ return Object.assign({}, agg, { rows, rowsTotal: agg.rows.length, rowsShown: rows.length, rowsTruncated: rows.length < agg.rows.length });
15313
+ }
15314
+
15300
15315
  function retroCmd(root) {
15301
15316
  root = absRoot(root);
15302
- // 1.9.15: --all-apps / --include 통합 모드
15303
- if (has('--all-apps') || arg('--include', null)) {
15304
- return _retroWorkspace(root);
15305
- }
15306
15317
  // 1.26.1 (13번째 외부리뷰 P2): 비숫자 --days → NaN → new Date(Invalid) throw 로 --json 소비자에 plain text 누출. 숫자 가드 + 음수/오버플로 클램프(insights/round-history 와 일관).
15307
15318
  let days = parseInt(arg('--days', '7'), 10);
15308
15319
  if (!Number.isFinite(days)) { failJson(has('--json'), 'invalid_arg', _uiLang(root) === 'en' ? '--days must be a number' : '--days 는 숫자여야 합니다'); return; }
15309
15320
  days = Math.max(0, Math.min(days, 36500));
15310
15321
  const cutoff = new Date(Date.now() - days * 86400 * 1000).toISOString().slice(0, 10);
15322
+ // 1.9.15: --all-apps / --include 통합 모드 — 1.36.71 (검수, 기존 버그): 워크스페이스 모드가 --days 를 조용히 무시하던 것, cutoff 전달
15323
+ if (has('--all-apps') || arg('--include', null)) {
15324
+ return _retroWorkspace(root, cutoff);
15325
+ }
15311
15326
  const agg = _retroAggregate(root, cutoff); // 1.36.38 (#2): 기간 필터 적용
15312
15327
  // 1.9.16: --json
15313
15328
  if (has('--json')) {
15314
- log(JSON.stringify({ project: path.basename(root), days, cutoff, summary: _retroOneLine(agg), data: agg }, null, 2));
15329
+ log(JSON.stringify({ project: path.basename(root), days, cutoff, summary: _retroOneLine(agg), data: _retroJsonData(agg) }, null, 2));
15315
15330
  return;
15316
15331
  }
15317
15332
  log(`# 회고 (retro) — 최근 ${days}일 (since ${cutoff})`);
@@ -15360,14 +15375,14 @@ function retroCmd(root) {
15360
15375
  }
15361
15376
 
15362
15377
  // 1.9.15: 워크스페이스 통합 retro (다수 프로젝트 묶음 회고)
15363
- function _retroWorkspace(rootBase) {
15378
+ function _retroWorkspace(rootBase, cutoff) {
15364
15379
  const paths = _collectWorkspacePaths(rootBase);
15365
15380
  if (!paths.length) return fail('대상 프로젝트 없음. --include <path1,path2> 또는 --all-apps 사용 필요.');
15366
15381
  // 1.9.16: --json
15367
15382
  if (has('--json')) {
15368
15383
  const projects = paths.map(p => {
15369
- const a = _retroAggregate(p);
15370
- return { project: path.basename(p), path: p, summary: _retroOneLine(a), data: a };
15384
+ const a = _retroAggregate(p, cutoff);
15385
+ return { project: path.basename(p), path: p, summary: _retroOneLine(a), data: _retroJsonData(a) };
15371
15386
  });
15372
15387
  const totals = projects.reduce((t, p) => ({
15373
15388
  tasks: t.tasks + p.data.totalTasks, done: t.done + p.data.doneCount,
@@ -15381,7 +15396,7 @@ function _retroWorkspace(rootBase) {
15381
15396
  log(`# Cross-project retro — ${paths.length}개 프로젝트`);
15382
15397
  const totals = { tasks: 0, done: 0, decisions: 0, skills: 0, totalSkillUsage: 0, totalOpts: 0, activeRules: 0, fixSig: 0, passSig: 0 };
15383
15398
  for (const p of paths) {
15384
- const agg = _retroAggregate(p);
15399
+ const agg = _retroAggregate(p, cutoff);
15385
15400
  const name = path.basename(p);
15386
15401
  log(`\n## ${name}`);
15387
15402
  log(` 📈 ${_retroOneLine(agg)}`);
@@ -15416,7 +15431,7 @@ function insightsCmd(root) {
15416
15431
  // 1.9.16: --json
15417
15432
  if (has('--json')) {
15418
15433
  const sc = readSessionCounter(root);
15419
- log(JSON.stringify({ project: path.basename(root), sessionCount: sc.count, lastCloseAt: sc.lastCloseAt, data: agg }, null, 2));
15434
+ log(JSON.stringify({ project: path.basename(root), sessionCount: sc.count, lastCloseAt: sc.lastCloseAt, data: _retroJsonData(agg) }, null, 2));
15420
15435
  return;
15421
15436
  }
15422
15437
  const sc = readSessionCounter(root);
@@ -15458,7 +15473,7 @@ function _insightsWorkspace(rootBase) {
15458
15473
  if (!paths.length) return fail('대상 프로젝트 없음. --include 또는 --all-apps 사용.');
15459
15474
  // 1.9.16: --json
15460
15475
  if (has('--json')) {
15461
- const projects = paths.map(p => ({ project: path.basename(p), path: p, data: _retroAggregate(p) }));
15476
+ const projects = paths.map(p => ({ project: path.basename(p), path: p, data: _retroJsonData(_retroAggregate(p)) }));
15462
15477
  log(JSON.stringify({ projects, projectCount: paths.length }, null, 2));
15463
15478
  return;
15464
15479
  }
@@ -18681,7 +18696,7 @@ function _mcpToCliArgs(name, args, targetPath) {
18681
18696
  case 'leerness_skill_info': cliArgs = ['skill', 'info', args.id || '', '--path', targetPath, '--json']; break;
18682
18697
  case 'leerness_benchmark': cliArgs = ['benchmark', targetPath, '--json', ...(args.scenario ? ['--scenario', args.scenario] : [])]; break;
18683
18698
  case 'leerness_lazy_detect': cliArgs = ['lazy', 'detect', targetPath, '--json']; break;
18684
- case 'leerness_retro': cliArgs = ['retro', targetPath, '--json', ...(args.days ? ['--days', String(args.days)] : []), ...(args.allApps ? ['--all-apps'] : [])]; break;
18699
+ case 'leerness_retro': cliArgs = ['retro', targetPath, '--json', ...(args.days ? ['--days', String(args.days)] : []), ...(args.allApps ? ['--all-apps'] : []), ...(args.all ? ['--all'] : []), ...(args.limit ? ['--limit', String(args.limit)] : [])]; break; // 1.36.71 (검수): rows 상한 조절 인자 전달
18685
18700
  case 'leerness_task_add': cliArgs = ['task', 'add', String(args.text || ''), '--path', targetPath, ...(args.status ? ['--status', args.status] : []), ...(args.evidence ? ['--evidence', args.evidence] : []), ...(args.nextAction ? ['--next', args.nextAction] : [])]; break;
18686
18701
  case 'leerness_task_update': cliArgs = ['task', 'update', String(args.id || ''), '--path', targetPath, ...(args.status ? ['--status', args.status] : []), ...(args.evidence ? ['--evidence', args.evidence] : []), ...(args.nextAction ? ['--next', args.nextAction] : []), ...(args.note ? ['--note', args.note] : [])]; break;
18687
18702
  case 'leerness_task_drop': cliArgs = ['task', 'drop', String(args.id || ''), '--path', targetPath, ...(args.reason ? ['--reason', args.reason] : [])]; break;
@@ -22824,7 +22839,7 @@ HANDOFF & SESSION
22824
22839
  handoff [path] [--compact] [--all-apps] [--json] Session-start context in one call
22825
22840
  session close [path] Closing report + auto handoff
22826
22841
  context [path] [--json] Agent onboarding context
22827
- retro | insights [path] [--json] Retrospective / cumulative stats
22842
+ retro | insights [path] [--json] [--limit N|--all] Retrospective / cumulative stats (JSON rows: newest 30 by default; rowsTotal = lifetime count, not the --days window)
22828
22843
 
22829
22844
  MEMORY (canonical JSON + markdown projections)
22830
22845
  task list|add|update|drop|fix-evidence|relink [args]
@@ -204,12 +204,17 @@ function whatsNewCmd(root, deps = {}) {
204
204
  if (allFiles.size) log(t(` 📄 신규 파일: ${[...allFiles].join(', ')}`, ` 📄 new files: ${[...allFiles].join(', ')}`));
205
205
  log('');
206
206
  log(t(`## 📜 버전별 헤드라인`, `## 📜 per-version headlines`));
207
- for (const v of diff) {
207
+ // 1.36.70 (검수 #1): migrate 보고서가 광고하는 `--limit N` 이 텍스트 모드에서 조용히 무시되던 것 —
208
+ // 명시적 --limit 만 존중(기본 텍스트 출력은 전체 유지, 계약 무변경).
209
+ const _txtRawLim = parseInt(arg('--limit', ''), 10);
210
+ const _txtLim = (has('--limit') && Number.isFinite(_txtRawLim) && _txtRawLim > 0) ? _txtRawLim : diff.length;
211
+ for (const v of diff.slice(0, _txtLim)) {
208
212
  // body 첫 줄(또는 strong header) 추출
209
213
  const firstLine = (v.body.match(/^\*\*([^*]+)\*\*/) || [])[1]
210
214
  || (v.body.split('\n').find(l => l.trim() && !l.startsWith('##')) || '').trim().slice(0, 120);
211
215
  log(` • ${v.version}${v.date ? ` (${v.date})` : ''} — ${firstLine || '(no headline)'}`);
212
216
  }
217
+ if (_txtLim < diff.length) log(t(` … 외 ${diff.length - _txtLim}개 버전 (전체: --all 또는 --limit 생략)`, ` … ${diff.length - _txtLim} more version(s) (drop --limit or use --all for everything)`));
213
218
  log('');
214
219
  log(t(`## 💡 권장 행동`, `## 💡 recommended actions`));
215
220
  log(t(` 1. 위 신규 명령들을 시도해 보세요 (예: \`leerness <명령> --help\`)`, ` 1. Try the new commands above (e.g. \`leerness <command> --help\`)`));
package/lib/mcp-tools.js CHANGED
@@ -27,7 +27,7 @@ module.exports = [
27
27
  { name: 'leerness_skill_info', requiredTier: 'read-only', description: '1.9.92 — 개별 skill 상세 조회 (version/capabilities/sources/patterns/usage/optimizations). 외부 AI가 skill 능력 정확히 파악', inputSchema: { type: 'object', properties: { id: { type: 'string' }, path: { type: 'string' } }, required: ['id'] } },
28
28
  { name: 'leerness_benchmark', requiredTier: 'read-only', description: '1.9.46/51/94 — 6 차원 점수 + 검수 시나리오 (--scenario) 결과 JSON. 외부 AI가 워크스페이스 leerness 활용 점수 확인', inputSchema: { type: 'object', properties: { path: { type: 'string' }, scenario: { type: 'string' } } } },
29
29
  { name: 'leerness_lazy_detect', requiredTier: 'read-only', description: '1.9.101 — 게으른 작업 자동 감지 결과 JSON (evidence 없는 done / empty handoff / no test run / TODO 미추적 / blocker no-next-action 등). 외부 AI가 거짓 완료 신호 사전 점검', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
30
- { name: 'leerness_retro', requiredTier: 'read-only', description: '1.9.104 — 4세션 누적 회고 보고서 JSON (statusCounts/focusNext/skillUsage/recentDecisions/durations/activeRules/fixSignals/passSignals/totalOptimizations). 외부 AI가 누적 패턴 자동 학습', inputSchema: { type: 'object', properties: { path: { type: 'string' }, days: { type: 'number' }, allApps: { type: 'boolean' } } } },
30
+ { name: 'leerness_retro', requiredTier: 'read-only', description: '1.9.104 — 4세션 누적 회고 보고서 JSON (statusCounts/focusNext/skillUsage/recentDecisions/durations/activeRules/fixSignals/passSignals/totalOptimizations). 외부 AI가 누적 패턴 자동 학습. 1.36.71: data.rows 는 기본 최신 30행(rowsTotal/rowsShown/rowsTruncated 로 절단 확인, rowsTotal 은 --days 창이 아니라 전체 task 수), 더 필요하면 limit(개수)/all=true(전체) 지정', inputSchema: { type: 'object', properties: { path: { type: 'string' }, days: { type: 'number' }, allApps: { type: 'boolean' }, limit: { type: 'number' }, all: { type: 'boolean' } } } },
31
31
  { name: 'leerness_task_add', requiredTier: 'safe-write', description: '1.9.105 — progress-tracker.md 에 새 task 추가 (양방향 제어 완성). 외부 AI가 사용자 요청을 task로 즉시 등록. 인자: { text (required), status?, evidence?, nextAction?, path? }', inputSchema: { type: 'object', properties: { text: { type: 'string' }, status: { type: 'string', enum: ['requested', 'planned', 'in-progress', 'waiting', 'on-hold', 'blocked', 'incomplete', 'done', 'dropped'] }, evidence: { type: 'string' }, nextAction: { type: 'string' }, path: { type: 'string' } }, required: ['text'] } },
32
32
  { name: 'leerness_task_update', requiredTier: 'safe-write', description: '1.9.106 — 기존 task 상태/evidence/nextAction 갱신. 외부 AI가 작업 진행에 따라 task를 단계적으로 업데이트 (read+add+update 3종 surface 완성). 인자: { id (required), status?, evidence?, nextAction?, note?, path? }', inputSchema: { type: 'object', properties: { id: { type: 'string' }, status: { type: 'string', enum: ['requested', 'planned', 'in-progress', 'waiting', 'on-hold', 'blocked', 'incomplete', 'done', 'dropped'] }, evidence: { type: 'string' }, nextAction: { type: 'string' }, note: { type: 'string' }, path: { type: 'string' } }, required: ['id'] } },
33
33
  { name: 'leerness_task_drop', requiredTier: 'safe-write', description: '1.9.107 — task를 dropped 상태로 폐기 (CRUD 완성: read/add/update/drop). 외부 AI가 사용자 요청으로 task 취소. 인자: { id (required), reason?, path? }', inputSchema: { type: 'object', properties: { id: { type: 'string' }, reason: { type: 'string' }, path: { type: 'string' } }, required: ['id'] } },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.36.69",
3
+ "version": "1.36.71",
4
4
  "description": "The AI-coding operations layer that makes \"done\" require evidence — persistent memory, evidence-gated completion checks, and clean handoffs for any AI agent (Claude Code, Codex, Cursor). State lives as plain files in your repo. CLI + MCP, 0 runtime dependencies.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -7207,5 +7207,68 @@ total++;
7207
7207
  if (!ok) failed++;
7208
7208
  }
7209
7209
 
7210
+ // 1.36.70 (성장 한계 클래스): migrate 보고서 상한 — 큰 버전 점프에서 ~88KB 폭주하던 AI must re-read 를 목록별 캡 + "외 N개" 정직 표기
7211
+ total++;
7212
+ {
7213
+ let ok = false;
7214
+ const _d = [];
7215
+ try {
7216
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-migcap-')); _d.push(d);
7217
+ // (검수 #2) --no-stale-check: 빈 캐시에서 npm 조회로 최대 8초 지연되던 것 차단
7218
+ cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
7219
+ fs.writeFileSync(path.join(d, '.harness', 'HARNESS_VERSION'), '1.9.33');
7220
+ cp.spawnSync(process.execPath, [CLI, 'migrate', d], { encoding: 'utf8', timeout: 60000, maxBuffer: 32 * 1024 * 1024 });
7221
+ const rep = fs.readFileSync(path.join(d, '.harness', 'migration-report.md'), 'utf8');
7222
+ const sizeOk = rep.length < 30 * 1024;
7223
+ // (검수 #3) 헤딩을 파싱해 shown/total 정합 + 헤드라인 행수 + 오버플로 수치 정확 일치로 단언 강화
7224
+ const hm = rep.match(/최신 (\d+)\/(\d+)/);
7225
+ const shown = hm ? parseInt(hm[1], 10) : 0, totalV = hm ? parseInt(hm[2], 10) : 0;
7226
+ const hlRows = (rep.match(/^- 1\.\d+\.\d+ — /gm) || []).length;
7227
+ const om = rep.match(/외 (\d+)개 버전/);
7228
+ const capOk = shown === 30 && totalV > 30 && hlRows === 30 && !!om && parseInt(om[1], 10) === totalV - 30;
7229
+ const escapeOk = rep.includes('whats-new --from 1.9.33'); // 전체가 필요한 소비자를 위한 탈출구
7230
+ // (검수 #1) 광고된 --limit 이 텍스트 모드에서도 실제 동작
7231
+ const limTxt = cp.spawnSync(process.execPath, [CLI, 'whats-new', '--from', '1.9.33', '--limit', '5'], { encoding: 'utf8', timeout: 20000, maxBuffer: 32 * 1024 * 1024 });
7232
+ const limRows = (limTxt.stdout.match(/^ {2}• 1\.\d+\.\d+/gm) || []).length;
7233
+ const limOk = limRows === 5 && /외 \d+개 버전|more version/.test(limTxt.stdout);
7234
+ ok = sizeOk && capOk && escapeOk && limOk;
7235
+ if (!ok) console.log(` [migcap 디버그] size=${sizeOk}(${Math.round(rep.length / 1024)}KB) cap=${capOk}(${shown}/${totalV},rows=${hlRows}) escape=${escapeOk} lim=${limOk}(${limRows})`);
7236
+ } catch (e) {} finally { _d.forEach(x => { try { fs.rmSync(x, { recursive: true, force: true }); } catch {} }); }
7237
+ console.log(ok ? '✓ B(1.36.70) migrate 보고 상한: 88KB→<30KB + 외N개 정직 표기 + whats-new 탈출구' : '✗ migrate 보고 상한 실패');
7238
+ if (!ok) failed++;
7239
+ }
7240
+
7241
+ // 1.36.71 (성장 한계 클래스 3): retro/insights --json data.rows 상한 — 전 task row 임베드로 무한 성장하던 것 (집계는 전량 기준 유지)
7242
+ total++;
7243
+ {
7244
+ let ok = false;
7245
+ const _d = [];
7246
+ try {
7247
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-retrocap-')); _d.push(d);
7248
+ const initR = cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
7249
+ const R = (extra) => JSON.parse(cp.spawnSync(process.execPath, [CLI, ...extra, '--path', d, '--json'], { encoding: 'utf8', timeout: 20000, maxBuffer: 32 * 1024 * 1024 }).stdout);
7250
+ // (검수 High) init 이 기본 T-0001 을 만들 수 있음 — 하드코딩 35 대신 기준선에서 유도 + exit 단언
7251
+ const baseline = R(['retro', '--all']).data.rowsTotal;
7252
+ let addFails = 0;
7253
+ for (let i = 1; i <= 35; i++) { const a = cp.spawnSync(process.execPath, [CLI, 'task', 'add', `t${i}`, '--path', d], { encoding: 'utf8', timeout: 15000 }); if (a.status !== 0) addFails++; }
7254
+ const expTotal = baseline + 35;
7255
+ const r = R(['retro']);
7256
+ const retroOk = initR.status === 0 && addFails === 0
7257
+ && r.data.rowsShown === 30 && r.data.rowsTotal === expTotal && r.data.rowsTruncated === true
7258
+ && r.data.rows.length === 30 && r.data.rows[29].request === 't35' // 최신 유지 (오래된 것부터 잘림)
7259
+ && r.data.totalTasks === expTotal; // 집계는 전량 기준
7260
+ const rAll = R(['retro', '--all']);
7261
+ const allOk = rAll.data.rowsShown === expTotal && rAll.data.rowsTruncated === false;
7262
+ const rLim = R(['retro', '--limit', '5']);
7263
+ const limOk = rLim.data.rowsShown === 5 && rLim.data.rows[4].request === 't35';
7264
+ const ins = R(['insights']);
7265
+ const insOk = ins.data.rowsShown === 30 && ins.data.rowsTotal === expTotal;
7266
+ ok = retroOk && allOk && limOk && insOk;
7267
+ if (!ok) console.log(` [retrocap 디버그] retro=${retroOk}(exp=${expTotal},got=${r.data.rowsTotal},addFail=${addFails}) all=${allOk} lim=${limOk} ins=${insOk}`);
7268
+ } catch (e) {} finally { _d.forEach(x => { try { fs.rmSync(x, { recursive: true, force: true }); } catch {} }); }
7269
+ console.log(ok ? '✓ B(1.36.71) retro/insights --json rows 상한 30 + 최신 유지 + 집계 전량 기준 + --all/--limit' : '✗ retro/insights rows 상한 실패');
7270
+ if (!ok) failed++;
7271
+ }
7272
+
7210
7273
  console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
7211
7274
  if (failed > 0) process.exit(1);