leerness 1.12.1 → 1.14.0

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,124 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.14.0 — 2026-06-09 — 🛡️ [안정화/Stable] 블라인드 리뷰 수정 + Karpathy 정렬 안정 minor
4
+
5
+ **🛡️ 안정화(Stable) minor.** 블라인드 3-모델 리뷰(codex/Sonnet/Opus) 수정 + Karpathy 가이드라인 정렬(1.13.1~1.13.2)을 검증·통합해 npm 공개. R-0011 정책의 5번째 minor. (이 릴리스의 소개 영상부터 **HyperFrames 파이프라인**으로 자동 제작됩니다.)
6
+
7
+ ### 이번 minor 통합 (1.13.1~1.13.2)
8
+ - **🔴 memory search 5종 표면 복구** (블라인드 Sonnet P1): `memory search` 가 lessons.md/rules.md 를 누락해 저장한 교훈·룰을 못 찾던 문제 → 추가.
9
+ - **🔴 옵션-only 무명령의 묵시적 init 쓰기 차단** (블라인드 codex P1): `leerness --json` 처럼 명령 없이 옵션만 주면 cwd 에 `.harness` 가 생성되던 부작용 → help 처리(명시 init 만 쓰기).
10
+ - **📖 README 블라인드 재구성**: 3모델이 코드/행위만으로 파악한 정체성 반영(거짓완료 차단 전면화·다언어·경로규칙·한국어 우선).
11
+ - **🔬 verify-claim scope-creep 표면화** (Karpathy 원칙3 "외과적 변경"): git 에 변경됐으나 evidence/주장에 없는 파일(요청 범위 밖 변경)을 advisory 로 노출(`--json scopeCreep`).
12
+ - doctor 문구 + selftest README exists 가드 (블라인드 P3).
13
+
14
+ ### 검증 (회귀 0)
15
+ - **selftest 211 PASS** · **E2E 365/365 PASS** · npm gate=minor_bump. scope-creep 행위(claim A + change A,B→[B]), memory search 5종 검색, --json-무명령 비쓰기 재현.
16
+
17
+ ### 안정화 표시 (R-0006)
18
+ CHANGELOG [안정화/Stable] · git tag annotation (Stable) · GitHub release (Stable) · npm dist-tag `stable` 시도.
19
+
20
+ ## 1.13.2 — 2026-06-09 — Karpathy 가이드라인 정렬: verify-claim scope-creep 표면화 (외과적 변경)
21
+
22
+ **🔬 외부 에이전트(Sonnet/Opus) 가 leerness 를 Andrej Karpathy 4대 코딩 가이드라인 대비 검토.** 두 리뷰가 수렴: leerness 는 원칙4(목표주도/검증루프)는 최강이나, **원칙3(외과적 변경)이 최대 갭** — 변경 파일이 요청 범위 내인지 검사가 없었음. 최고가치·최저노력 항목을 즉시 구현(신규 명령 0 — leerness 자신의 원칙2 위반 악화 방지).
23
+
24
+ ### 변경 (UR-0030, Karpathy 원칙3)
25
+ - **verify-claim 역방향 git 교차검증**: 기존엔 "주장한 파일이 git 변경에 있는가"(한 방향)만 봤음. 이제 **"git 에 변경됐으나 evidence/주장에 없는 파일"(scope-creep / 요청 범위 밖 변경 신호)** 도 표면화 — `--json scopeCreep` + 사람용 advisory(`🔬 외과적 변경 점검`). 하네스 자체 기록(.harness/.git/node_modules 등) 제외, 오탐 방지 위해 advisory(기본 FAIL 아님). 이미 계산하던 `gitChanged` Set 재사용 — 최소 변경.
26
+
27
+ ### 검증 (회귀 0)
28
+ - **selftest 210→211**, 행위 재현: claim a.js + change a.js,b.js → `scopeCreep={count:1,files:["b.js"]}` (.harness 정확 제외).
29
+ - 백로그(기존 명령 확장 권장): review-request scope/단순성 신호(원칙1+2, UR-0031) · plan `--done-when` 성공기준(원칙4, UR-0032) · leerness 자체 단순화 검토(원칙2, UR-0033).
30
+ - patch(1.13.2) — R-0011 정책상 npm 미배포(GitHub).
31
+
32
+ ## 1.13.1 — 2026-06-09 — 블라인드 3-모델 리뷰 수정 + README 재구성 (codex gpt-5.5 · Sonnet 4.8 · Opus 4.8)
33
+
34
+ **🔍 블라인드 멀티모델 리뷰**: 3개 모델(codex gpt-5.5 외부 CLI + Claude Sonnet/Opus 4.8 서브에이전트)이 **leerness 소개 없이, README 를 물리적으로 제거(숨김)한 클린룸**에서 npm 최신본(1.13.0)을 코드·CLI·행위만으로 분석. 발견을 맹신 X로 직접 재현·검증해 수정하고, 그 리뷰 내용으로 README 를 재구성.
35
+
36
+ ### 버그 수정 (전부 재현 검증)
37
+ - **🔴 memory search 가 lessons/rules 누락** (Sonnet P1): `memory search` 가 5종 메모리 표면을 표방하나 `lessons.md`/`rules.md` 를 검색 못 해(저장한 교훈·룰이 'no matches') 모순감지 핵심 용도가 훼손됐음 → 두 파일 추가. (검증: lesson/rule/decision 3종 모두 검색됨)
38
+ - **🔴 옵션-only 무명령의 묵시적 init 쓰기** (codex P1): `leerness --json` 처럼 명령 없이 옵션만 주면 cwd 에 `.harness` 가 의도치 않게 생성되던 쓰기 부작용 → 무명령+옵션만은 help 로 처리(명시 `init` 만 쓰기). bare `leerness` 온보딩은 유지.
39
+ - **doctor 문구 + selftest README 의존** (Sonnet/codex/Opus P3): doctor 가 pass 수에 '실패' 를 붙여 "209/210 실패"(=209건 실패로 오독)되던 것 → "통과 N/M (K건 실패)". selftest 의 README 배너 케이스에 `exists` 가드 추가 → 문서가 없는(pruned) 설치에서도 코어 무결성 진단 통과.
40
+
41
+ ### README 재구성 (블라인드 리뷰 기반)
42
+ 3모델이 코드/행위만으로 파악한 정체성("AI 코딩 에이전트 운영/신뢰성 하네스, 코드를 대신 쓰지 않음")을 반영 — **거짓 완료 차단(핵심 차별점) 섹션 신설**(verify-claim 다언어 인식 포함), selftest 카운트 정정(→210), 경로 규칙(메모리 명령은 `--path`/`./dir`)·한국어-우선 출력 명시.
43
+
44
+ ### 검증 / 잔여
45
+ - **selftest 210 PASS** · E2E 365/365. 백로그: bare relative dir cwd-fallback(codex P2, README 경로규칙으로 문서화 + pure 함수 계약 변경은 후속).
46
+ - patch(1.13.1, 같은 minor) — R-0011 정책상 npm 미배포(GitHub). Opus 가 "버그처럼 보였으나 자기 테스트 데이터 오류"로 정직하게 철회한 항목은 수정 안 함(과수정 회피).
47
+
48
+ ## 1.13.0 — 2026-06-09 — 🛡️ [안정화/Stable] verify-claim 다언어 + 정직성·자원·보안 안정화
49
+
50
+ **🛡️ 안정화(Stable) minor. 헤드라인 = verify-claim 다언어 지원(비-JS 개발자 핵심 회귀 수정).** 15번째 멀티에이전트 버그헌트 성과(1.12.2~1.12.5)를 검증·통합해 npm 공개. R-0011 정책의 4번째 minor.
51
+
52
+ ### ⚠️ 동작 변경 (중요)
53
+ - **verify-claim 기본 게이트가 Python·Ruby·Go·C#·Java·PHP·Rust 등 비-JS 구현을 인식**합니다(1.12.0 의 게이트는 JS 패턴만 알아 비-JS 정상 완료를 오차단했음). 정직하게 구현한 완료는 언어 무관 통과, 가짜 완료는 여전히 차단.
54
+
55
+ ### 이번 minor 통합 (1.12.2~1.12.5)
56
+ - **🌐 verify-claim 다언어 지원** (UR-0014): `OPTIMISM_PATTERNS.codeRe` 에 교차언어 idiom 추가 — 비-JS 정상 done-claim 오차단(exit 1) 제거. 1.12.0 핵심가치의 비-JS 회귀 수정.
57
+ - **🔒 MCP 정직성** (UR-0181): unknown tool 호출의 임의경로 쓰기 차단(사용통계 기록을 도구 검증 후로). path-타게팅은 generic 서버 설계로 확인(맹신 X — 과수정 회피).
58
+ - **🔍 탐지 정직성** (UR-0182/0183): lazy detect TODO 파일별 추적(무관 task 의 'TODO' 글자 전역 억제 제거) + session close 완료 정직성 advisory(증거 없는 done 노출).
59
+ - **🧹 견고성/자원/정확성** (UR-0015~0021): glossary 표 파이프 escape · MCP _chunkSize 클램프(무한루프 데이터손실) · api-skill CRLF/BOM · shell-guard 공백없는 `&&` · 대형파일 stat-before-read(메모리 2배 스파이크) · 중첩 skip-dir 시크릿 오탐 · requirements pip 디렉티브.
60
+
61
+ ### 검증 (회귀 0)
62
+ - **selftest 210 PASS** · **E2E 365/365 PASS** · npm gate=minor_bump. Python API done-claim→exit 0, 중첩 node_modules 시크릿 제외, MCP unknown tool 쓰기 차단 등 행위 재현.
63
+
64
+ ### 안정화 표시 (R-0006)
65
+ CHANGELOG [안정화/Stable] · git tag annotation (Stable) · GitHub release (Stable) · npm dist-tag `stable` 시도.
66
+
67
+ ## 1.12.5 — 2026-06-09 — 15th 버그헌트 잔여 5종: CRLF·shell-guard·메모리·skip-dir·requirements
68
+
69
+ **🧹 15번째 버그헌트 잔여 클러스터(UR-0017~0021) 일괄 처리 — 견고성/자원/정확성.**
70
+
71
+ ### 변경
72
+ - **api-skill CRLF/BOM 복구** (UR-0017, P2): `_loadAPISkill` 가 raw readFileSync 라 CRLF 파일에서 frontmatter 전부 유실 + `api-skill match` 크래시(body undefined). `read()`(BOM strip) + `\r\n`/`\r` 정규화 + fallback 에 `body` 추가(1.9.408 SKILL.md 수정 누락분).
73
+ - **shell-guard 공백없는 `&&`/`||` 탐지** (UR-0018, P2): `/\s&&\s/` 가 양쪽 공백을 요구해 `npm run build&&npm test`(PS5.1 에서 실패하는 흔한 형태) 미탐 → 공백 무관 토큰 매칭.
74
+ - **대형 파일 stat-before-read** (UR-0019, P2): `_scanCodeForPatterns`·`scan secrets`·`encoding check` 가 size-cap 을 read() **후** 검사해 대형 파일 1개가 메모리 2배 스파이크(200MB→RSS 464MB) → 읽기 **전** stat 으로 초과 파일 건너뜀. verify-claim/gate 메모리 안정화.
75
+ - **중첩 skip-dir 제외** (UR-0020, P3): `isSkippedRel` 가 root-anchored 만 매칭해 중첩 `node_modules`/`.git`/`dist` 가 스캔돼 오탐 → 경로 세그먼트 매칭(SCAN_SKIP_DIRS Set).
76
+ - **requirements.txt 디렉티브 skip** (UR-0021, P3): `-e`/`-r`/`--hash` pip 디렉티브를 패키지로 파싱하던 것 → `-` 라인 skip + 영숫자 시작 요구.
77
+
78
+ ### 검증 (회귀 0)
79
+ - **selftest 209→210**, 행위 재현: api-skill CRLF→"Real Name"+match exit 0, scan secrets 중첩 node_modules/dist/.git 제외(root.js 만 탐지), requirements `["requests","flask"]`.
80
+ - 개발 중 발견·수정: `SCAN_SKIP_DIRS` 는 Set(.has) — `.includes` 오용을 selftest 가 배포 전 차단(handoff 보안 스캔 회귀 포함).
81
+ - patch(1.12.5, 같은 minor) — R-0011 정책상 npm 미배포. **15번째 버그헌트(UR-0014~0021) 전부 처리 완료.**
82
+
83
+ ## 1.12.4 — 2026-06-09 — 🌐 verify-claim 다언어 지원 + glossary 표/MCP 페이지네이션 (15th 버그헌트 P1/P2)
84
+
85
+ **🔴 핵심 회귀 수정: verify-claim 기본 게이트가 非JS 정상 완료를 오차단하던 문제.** 15번째 멀티에이전트 버그헌트(크로스플랫폼·최신기능·성능 3관점) 결과 중 즉시-수정 가능한 고가치 3건.
86
+
87
+ ### 변경
88
+ - **🔴 verify-claim 다언어 지원** (UR-0014, P1): 1.12.0 의 optimism 기본 게이트가 `OPTIMISM_PATTERNS.codeRe` 를 **JS 전용**으로 두고도 코드 스캐너는 13개 언어를 읽어, Python(`requests`)·Ruby(`Net::HTTP`)·Go(`http.Get`)·C#(`HttpClient`)·Java·PHP·Rust 로 정직하게 구현한 done-claim 을 "호출 흔적 없음"으로 **오차단(exit 1)** 했음. 각 codeRe 에 교차언어 idiom 추가 → 非JS 정상 완료 통과. (검출 관대화 = 정직한 작업 오차단 제거; 과탐보다 안전.)
89
+ - **glossary.md 표 파이프 escape** (UR-0015, P2): 표 셀에 `_lineSafe`(개행만 제거) 대신 `_cellSafe`(파이프 escape) 적용 — 의존성 description 의 `|` 가 표 칼럼을 깨뜨리던 문제(node_modules description fallback 벡터).
90
+ - **MCP 페이지네이션 _chunkSize 클램프** (UR-0016, P2): 음수/소수 `_chunkSize` → 빈 출력 무한 루프(데이터 손실)였음 → 양의 정수로 클램프.
91
+
92
+ ### 검증 (회귀 0)
93
+ - **selftest 207→209**, 행위 재현: Python API done-claim 기본 verify-claim **exit 0**(false-fail 제거), glossary `|`→`\|`, _chunkSize 음수 클램프.
94
+ - patch(1.12.4, 같은 minor) — R-0011 정책상 npm 미배포. 15th 잔여(UR-0017~0021): _loadAPISkill CRLF, shell-guard 공백없는 &&, 대형파일 stat-before-read, 중첩 skip-dir, requirements `-` — 후속.
95
+
96
+ ## 1.12.3 — 2026-06-09 — 정직성 마무리: lazy TODO 파일별 추적 + session close 완료 정직성 (14th 버그헌트, UR-0182/0183)
97
+
98
+ **🔍 14번째 버그헌트 잔여 2건 — 정직성 탐지 정밀화.**
99
+
100
+ ### 변경
101
+ - **lazy detect TODO 파일별 추적** (UR-0182): `todo_untracked` 가 아무 task 의 request/evidence 에 'TODO' 글자만 있어도 모든 코드 TODO 경보를 **전역 억제**(무관한 task 1개가 전부 묵음)하던 문제 → 해당 TODO 의 **파일을 참조하는 task 가 없는 것만** 미추적으로 경보. `--auto-track` 도 미추적 TODO 만 등록.
102
+ - **session close 완료 정직성 advisory** (UR-0183): 마감 시 done 인데 evidence 가 비었거나 placeholder 인 task 를 노출(`completionHonesty` JSON 필드 + `⚠ 완료 정직성` 라인). 차단하지 않는 advisory(session close 의 advisory 철학) — verify-claim 권장 환기.
103
+
104
+ ### 검증 (회귀 0)
105
+ - **selftest 206→208**, 행위 재현: 무관 task 의 'TODO' 글자에도 코드 TODO 경보 유지(untracked=1), session close `completionHonesty.doneWithoutEvidence` 노출.
106
+ - patch(1.12.3, 같은 minor) — R-0011 정책상 npm 미배포. **14번째 버그헌트 백로그(UR-0175~0183) 전부 처리 완료.**
107
+
108
+ ## 1.12.2 — 2026-06-09 — MCP unknown-tool 임의경로 쓰기 차단 (14th 버그헌트, UR-0181)
109
+
110
+ **🧹 MCP 사용통계 기록을 도구 검증 후로 — unknown tool 의 임의 경로 쓰기 차단.**
111
+
112
+ ### 조사 정정 (맹신 X)
113
+ 외부 에이전트가 "MCP `path` 인자가 서버 루트를 탈출(traversal/정책 우회)"을 P2 보안으로 보고했으나, **직접 조사 + e2e 로 확인 결과 MCP 서버는 generic 설계**임: `mcp serve` 는 cwd 에서 실행되고 각 `tools/call` 의 `path` 로 대상 프로젝트를 지정(의도된 기능 — e2e 가 temp 경로 타게팅을 검증). policy 도 대상-프로젝트별로 path 에서 로드(올바름). 즉 **path-타게팅 자체는 취약점이 아니라 설계**(신뢰된 로컬 MCP 모델). 처음 적용한 "루트 가둠"은 이 설계를 깨 e2e 5건 실패 → **revert**.
114
+
115
+ ### 진짜 버그 수정
116
+ - `_bumpMcpUsage`(사용 통계 쓰기)가 **unknown-tool 검증 전**에 실행돼, 미등록 도구 호출로도 임의 경로에 `.harness/cache` 가 생성됐음 → **검증 후(알려진 도구만)로 이동**.
117
+
118
+ ### 검증 (회귀 0)
119
+ - **selftest 206→207** (bump 위치 와이어), e2e 365/365. 프로젝트-scoped 가둠은 opt-in 플래그로 별도 검토(backlog).
120
+ - patch(1.12.2, 같은 minor) — R-0011 정책상 npm 미배포. 14th 잔여: UR-0182(lazy TODO)·UR-0183(session close 정직성).
121
+
3
122
  ## 1.12.1 — 2026-06-08 — 🩹 [Stable hotfix] 클린룸 selftest false-alarm 수정 (UR-0008)
4
123
 
5
124
  **1.12.0 직후 발견된 사용자-노출 결함 핫픽스 — npm 배포(예외).**
package/README.md CHANGED
@@ -67,6 +67,22 @@ CLAUDE.md / AGENTS.md 에 `세션 시작 시 leerness handoff .`, `종료 전 le
67
67
 
68
68
  ---
69
69
 
70
+ ## 거짓 완료 차단 — 핵심 차별점
71
+
72
+ 세 모델의 블라인드 리뷰가 공통으로 꼽은 leerness의 핵심 가치입니다. AI 에이전트가 가장 자주 하는 거짓말 "다 했어요"를, leerness는 **완료 주장을 실제 코드와 대조**해 검증합니다.
73
+
74
+ ```bash
75
+ # 에이전트가 "결제 API 연동 완료"라고 주장하지만 코드엔 호출 흔적이 없으면:
76
+ leerness verify-claim T-0001 --require-evidence
77
+ # ⚠ FAIL (낙관 1) · [Payment] 결제: evidence에 주장 있으나 코드에 호출 흔적 없음 → exit 1
78
+ ```
79
+
80
+ - `optimism-check` · `verify-claim` — evidence의 도메인 주장(API·DB·결제·이메일·큐·캐시·알림·스토리지 등 10종)을 실제 소스 호출과 대조. **JavaScript뿐 아니라 Python·Ruby·Go·C#·Java·PHP·Rust 구현도 인식**합니다(1.13).
81
+ - `lazy detect` — 증거 없는 done, 빈 handoff, 테스트 미실행, 미추적 TODO를 탐지.
82
+ - 정직한 완료는 통과하고 가짜 완료만 exit 1로 차단 — `gate` 또는 CI에 그대로 연결됩니다.
83
+
84
+ ---
85
+
70
86
  ## 핵심 개념 — 5계층
71
87
 
72
88
  - **기억(Memory)** — `task`/`plan`/`decision`/`lesson`/`rule`/`feature`(그래프). canonical JSON 을 단일 진실소스로 저장하고 마크다운은 projection. archive/restore 지원.
@@ -90,6 +106,8 @@ CLAUDE.md / AGENTS.md 에 `세션 시작 시 leerness handoff .`, `종료 전 le
90
106
 
91
107
  전체 명령은 `leerness commands` 또는 `leerness --help` 로 확인하세요.
92
108
 
109
+ > **경로 규칙** — 대부분 명령은 `[path]` 위치 인자를 받습니다. `task`/`decision`/`lesson` 등 메모리 명령에 다른 폴더를 지정할 땐 `--path <dir>` 또는 `./dir` 형태를 쓰세요(맨 폴더 이름만 주면 현재 폴더로 처리됩니다). 출력은 한국어가 기본이며 `--language en` 으로 영어를 늘릴 수 있습니다(일부 메시지는 한국어 유지).
110
+
93
111
  ---
94
112
 
95
113
  ## 대표 워크플로
@@ -130,7 +148,7 @@ leerness mcp serve # JSON-RPC over stdio, 85 도구
130
148
  - **원자적 UTF-8 쓰기** — temp + rename 으로 부분쓰기 손상 방지, BOM 자동 strip.
131
149
  - **shell 미경유 MCP** — `mcp serve` 의 도구 호출은 셸을 거치지 않고 인자를 직접 전달(명령 주입 차단).
132
150
  - **순수 `--json` / 일관 exit code** — 성공·실패·미존재 경로 모두 파싱 가능한 JSON(`{ok,error,code}`) + 실패 시 exit 1. 자동화·CI 친화.
133
- - **모듈 분리(DI)** + **내장 자가검증** — `lib/` 순수 유틸/IO/카탈로그 분리, `selftest` 180+ 케이스로 설치 무결성 검증.
151
+ - **모듈 분리(DI)** + **내장 자가검증** — `lib/` 순수 유틸/IO/카탈로그 분리, `selftest` 210 케이스로 설치 무결성 검증(`doctor` 가 함께 실행). 문서(README) 부재에도 코어 무결성 진단은 통과.
134
152
 
135
153
  ---
136
154
 
@@ -168,7 +186,7 @@ MIT
168
186
  <!-- leerness:project-readme:start -->
169
187
  ## Leerness Project Harness
170
188
 
171
- 이 프로젝트는 Leerness v1.12.1 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
189
+ 이 프로젝트는 Leerness v1.14.0 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
172
190
 
173
191
  ### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
174
192
 
@@ -222,7 +240,7 @@ leerness memory restore decision <date|title>
222
240
 
223
241
  ### MCP server (외부 AI 통합)
224
242
 
225
- Leerness v1.12.1는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
243
+ Leerness v1.14.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
226
244
 
227
245
  ```jsonc
228
246
  // 카테고리별
@@ -243,7 +261,7 @@ Leerness v1.12.1는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code
243
261
  `<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
244
262
  1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) stress-v* 신규 작성 + 누적 회귀 → 4) e2e 219/219 → 5) npm pack + git tag + GitHub release → 6) main 자동 push (1.9.140+) → 7) session close → 8) 다음 라운드 예약.
245
263
 
246
- 현재 누적: **70 라운드 (1.9.40 → 1.12.1)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
264
+ 현재 누적: **70 라운드 (1.9.40 → 1.14.0)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
247
265
 
248
266
  ### 성능 가이드 (1.9.140 측정)
249
267
 
@@ -281,6 +299,6 @@ leerness release pack --close --auto-main-push
281
299
  - `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
282
300
  - `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
283
301
 
284
- Last synced by Leerness v1.12.1: 2026-06-08
302
+ Last synced by Leerness v1.14.0: 2026-06-09
285
303
  <!-- leerness:project-readme:end -->
286
304
 
package/bin/leerness.js CHANGED
@@ -32,7 +32,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
32
32
  // 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
33
33
  const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST, _DEFAULT_PLATFORM_CONSTRAINTS, _DEFAULT_DOMAIN_CATALOG, _TOOL_CATALOG, _LSP_LANG_PATTERNS, OPTIMISM_PATTERNS, BUILT_IN_PERSONAS, STRINGS, BUILTIN_CATALOG, ROADMAP_STATUS_LABEL, ROADMAP_STATUS_COLOR, SECRET_PATTERNS, MERGE_OVERWRITE_FILES, MINIMAL_SKIP_KEYS, REQUIRED_WORKSPACE_FILES, KEYWORD_STOPWORDS, SKILL_CATALOG_PRESETS } = require('../lib/catalogs'); // 1.9.344/368/369 (UR-0025): catalog 분리 · 1.11.4 (UR-0007): _TOOL_CATALOG
34
34
 
35
- const VERSION = '1.12.1';
35
+ const VERSION = '1.14.0';
36
36
 
37
37
  // 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
38
38
  // 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
@@ -3313,9 +3313,11 @@ function _selfTestCases() {
3313
3313
  return wired && behav;
3314
3314
  } },
3315
3315
  { name: 'README ASCII 배너 표시 + CLI _banner 와 동일 아트 (1.9.441)', run: () => {
3316
- const readme = read(path.join(path.dirname(__filename), '..', 'README.md'));
3316
+ // 1.13.1 (15th 블라인드 리뷰 P3, Opus): README 부재 시 read() throw 로 selftest 가 깨끗한 실패 대신 예외 → exists 가드. README 는 코어 무결성 파일이 아니므로(문서 pruning 가능) 없으면 통과 처리, 있으면 CLI _banner 와 일치 검사.
3317
+ const readmePath = path.join(path.dirname(__filename), '..', 'README.md');
3317
3318
  const bannerLine = '███████╗███████╗██╗ ██╗'.slice(0, 0) + '██║ █████╗ █████╗ ██████╔╝'; // LEERNESS 배너 고유 라인(자기참조 회피 split)
3318
- return readme.includes(bannerLine) && read(__filename).includes(bannerLine); // README CLI _banner 동일 아트
3319
+ if (!exists(readmePath)) return read(__filename).includes(bannerLine); // 문서 없으면 CLI 아트만 확인
3320
+ return read(readmePath).includes(bannerLine) && read(__filename).includes(bannerLine); // README ↔ CLI _banner 동일 아트
3319
3321
  } },
3320
3322
  { name: '12th 외부평가 Sonnet P1 (UR-0141): task 계열 positional path 지원 (cwd 오염 차단) (1.9.442)', run: () => {
3321
3323
  const m = require('../lib/pure-utils');
@@ -3522,6 +3524,48 @@ function _selfTestCases() {
3522
3524
  return md.includes(m.GLOSSARY_START) && md.includes('react') && /미정의|unknownpkgxyz/.test(md)
3523
3525
  && read(__filename).includes("if (cmd === 'glossary')");
3524
3526
  } },
3527
+ { name: '14th 버그헌트 (UR-0181): MCP _bumpMcpUsage 를 unknown-tool 검증 후로 이동(unknown tool 임의경로 쓰기 차단) (1.12.2)', run: () => {
3528
+ const src = read(__filename);
3529
+ // _bumpMcpUsage 호출이 unknown-tool 가드(cliArgs === null return) "뒤"에 위치 — generic 서버 path-타게팅은 유지(취약점 아님), unknown tool 쓰기만 차단.
3530
+ return /if \(cliArgs === null\) return send[\s\S]{0,400}?_bumpMcpUsage\(targetPath, name\)/.test(src);
3531
+ } },
3532
+ { name: '14th 버그헌트 P2/P3 (UR-0182/0183): lazy TODO 파일별 추적 + session close 완료 정직성 advisory (1.12.3)', run: () => {
3533
+ const src = read(__filename);
3534
+ const todoPerFile = src.includes('const untrackedTodos = newTodos.filter(t => !taskText.includes(t.file));') && src.includes("kind: 'todo_untracked'");
3535
+ const scSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'session-close.js'));
3536
+ const honesty = scSrc.includes('jsonResult.completionHonesty =') && scSrc.includes("doneWithoutEvidence: _doneNoEvidence.length");
3537
+ return todoPerFile && honesty;
3538
+ } },
3539
+ { name: '15th 버그헌트 P1/P2 (UR-0014/0015/0016): optimism 다언어 codeRe + glossary _cellSafe + MCP _chunkSize 클램프 (1.12.4)', run: () => {
3540
+ const cat = require('../lib/catalogs').OPTIMISM_PATTERNS;
3541
+ const api = cat.find(p => p.kind === 'API');
3542
+ // 다언어: Python requests / Ruby Net::HTTP / Go http.Get / C# HttpClient 매칭
3543
+ const multiLang = api.codeRe.test('requests.get(url)') && api.codeRe.test('Net::HTTP.get') && api.codeRe.test('http.Get(url)') && api.codeRe.test('new HttpClient()') && api.codeRe.test('fetch(');
3544
+ const m = require('../lib/pure-utils');
3545
+ // glossary 표 셀 파이프 escape
3546
+ const md = m._renderGlossaryMd([{ term: 'x', plainKo: 'a | b', plainEn: 'a | b', category: 'c', source: 'catalog' }], {});
3547
+ const pipeEsc = md.includes('a \\| b') && !/\| a \| b \|/.test(md.replace(/\\\|/g, '§'));
3548
+ const src = read(__filename);
3549
+ const clamp = src.includes('const _cs = Math.floor(Number(args._chunkSize));') && src.includes('(Number.isFinite(_cs) && _cs > 0) ? _cs : 50000');
3550
+ return multiLang && pipeEsc && clamp;
3551
+ } },
3552
+ { name: '15th 잔여 클러스터 (UR-0017~0021): api-skill CRLF + shell-guard 공백없는&& + stat-before-read + 중첩skip + requirements 디렉티브 (1.12.5)', run: () => {
3553
+ const src = read(__filename);
3554
+ const apiCrlf = src.includes("const content = read(fp).replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');") && src.includes('urls: [], name: id, body: content }');
3555
+ const statBeforeRead = src.includes('if (fs.statSync(file).size > 1024 * 1024) continue;') && src.includes('if (fs.statSync(file).size > 5 * 1024 * 1024) continue;') && src.includes('if (fs.statSync(fp2).size > budget) continue;');
3556
+ const nestedSkip = src.includes('segs.some(s => SCAN_SKIP_DIRS.has(s))');
3557
+ const an = require('../lib/analyzers');
3558
+ const sg = an._shellGuardAnalyze('npm run build&&npm test', { shell: 'powershell', psVersion: 5 });
3559
+ const shellNoSpace = (sg.issues || []).some(i => i.rule === 'ps5-chain');
3560
+ const m = require('../lib/pure-utils');
3561
+ const reqs = m._parseRequirementsTxt('-e git+https://x\n-r base.txt\n--hash=sha256:abc\nrequests==2.31\n');
3562
+ const reqDirectives = reqs.length === 1 && reqs[0] === 'requests';
3563
+ return apiCrlf && statBeforeRead && nestedSkip && shellNoSpace && reqDirectives;
3564
+ } },
3565
+ { name: 'Karpathy 가이드라인3 "외과적 변경" (UR-0030): verify-claim 역방향 git 교차검증 scope-creep 표면화 (1.13.2)', run: () => {
3566
+ const src = read(__filename);
3567
+ return src.includes('const changedNotClaimed = gitApplicable') && src.includes('files.some(f => _claimFileInGit(f, new Set([g])))') && src.includes('scopeCreep:') && src.includes('외과적 변경 점검');
3568
+ } },
3525
3569
  { name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
3526
3570
  ];
3527
3571
  }
@@ -3799,9 +3843,10 @@ function _serializeAPISkill(id, name, urls, direction, doc) {
3799
3843
  function _loadAPISkill(root, id) {
3800
3844
  const fp = path.join(_apiSkillsDir(root), id + '.md');
3801
3845
  if (!fs.existsSync(fp)) return null;
3802
- const content = fs.readFileSync(fp, 'utf8');
3846
+ // 1.12.5 (15th 버그헌트 P2, UR-0017): read()(BOM strip) + CRLF/CR 정규화 — 이전 raw readFileSync 는 CRLF 파일에서 '^---\n' 불일치로 frontmatter 전부 유실(1.9.408 SKILL.md 수정 누락분). 또 fallback 에 body 추가 → _matchAPISkills 의 s.body.slice 크래시 방지.
3847
+ const content = read(fp).replace(/\r\n/g, '\n').replace(/\r/g, '\n');
3803
3848
  const fm = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
3804
- if (!fm) return { id, content, urls: [], name: id };
3849
+ if (!fm) return { id, content, urls: [], name: id, body: content };
3805
3850
  const meta = {};
3806
3851
  fm[1].split('\n').forEach(l => {
3807
3852
  const m = l.match(/^(\w+):\s*(.*)$/);
@@ -7319,8 +7364,10 @@ function getExtraSkipDirs(root) {
7319
7364
  return read(f).split('\n').map(s => s.trim().replace(/\/+$/, '')).filter(s => s && !s.startsWith('#'));
7320
7365
  }
7321
7366
  function isSkippedRel(rel, extras = []) {
7322
- const all = [...SCAN_SKIP_DIRS, ...extras];
7323
- return all.some(d => rel === d || rel.startsWith(d + '/'));
7367
+ // 1.12.5 (15th 버그헌트 P3, UR-0020): 중첩 skip-dir 도 제외 — 이전엔 root-anchored prefix 만 매칭해 deep/node_modules, sub/.git, vendor/dist 가 스캔돼 오탐. SCAN_SKIP_DIRS 는 단일 dir 명이라 경로 세그먼트로 매칭(_scanShellScriptsEncoding 의 basename skip 과 일관).
7368
+ const segs = rel.split('/');
7369
+ if (segs.some(s => SCAN_SKIP_DIRS.has(s))) return true; // SCAN_SKIP_DIRS 는 Set
7370
+ return extras.some(d => rel === d || rel.startsWith(d + '/'));
7324
7371
  }
7325
7372
  const SCAN_TEXT_EXT = new Set(['.js','.ts','.jsx','.tsx','.mjs','.cjs','.json','.md','.txt','.env','.bash','.sh','.yml','.yaml','.toml','.ini','.cfg','.py','.rb','.go','.rs','.java','.kt','.swift','.cs','.php','.sql','.html','.css','.scss','.less','.xml','.bat','.ps1','']);
7326
7373
  function* walk(root, base = root, depth = 0, extras = null) {
@@ -7356,6 +7403,8 @@ function _collectSecretFindings(root) {
7356
7403
  const isEnvFamily = /^\.env(\.|$)/.test(path.basename(file));
7357
7404
  if (!SCAN_TEXT_EXT.has(ext) && !isEnvFamily) continue;
7358
7405
  let text;
7406
+ // 1.12.5 (15th 버그헌트 P2, UR-0019): stat-before-read — 1MB 초과 파일은 읽지 않고 건너뜀(이전엔 read 후 검사라 대형 파일 통째 로드).
7407
+ try { if (fs.statSync(file).size > 1024 * 1024) continue; } catch { continue; }
7359
7408
  try { text = read(file); } catch { continue; }
7360
7409
  if (text.length > 1024 * 1024) continue;
7361
7410
  const fileRel = (file === root) ? path.basename(file) : rel(root, file);
@@ -7414,6 +7463,8 @@ function encodingCheck(root, opts = {}) {
7414
7463
  const ext = path.extname(file).toLowerCase();
7415
7464
  if (!SCAN_TEXT_EXT.has(ext)) continue;
7416
7465
  let buf;
7466
+ // 1.12.5 (15th 버그헌트 P2, UR-0019): stat-before-read — 5MB 초과 파일은 읽지 않고 건너뜀(이전엔 readBuf 후 검사라 대형 파일 통째 로드).
7467
+ try { if (fs.statSync(file).size > 5 * 1024 * 1024) continue; } catch { continue; }
7417
7468
  try { buf = readBuf(file); } catch { continue; }
7418
7469
  if (buf.length === 0) continue;
7419
7470
  if (buf.length > 5 * 1024 * 1024) continue;
@@ -7521,14 +7572,16 @@ function lazyDetect(root, opts = {}) {
7521
7572
  }
7522
7573
  }
7523
7574
  if (todoCount > 0) {
7524
- const hasTodoTask = rows.some(r => /TODO|FIXME|XXX/.test(r.request) || /TODO|FIXME|XXX/i.test(r.evidence));
7525
- if (!hasTodoTask) {
7575
+ // 1.12.3 (14th 버그헌트 P2, UR-0182): 파일별 추적 — 기존엔 아무 task 의 request/evidence 가 'TODO' 글자만 포함해도 모든 코드 TODO 경보를 전역 억제(무관한 task 1개가 전부 묵음)했음. 이제 해당 TODO 의 파일을 참조하는 task 가 없는 것만 미추적으로 경보.
7576
+ const taskText = rows.map(r => `${r.request || ''} ${r.evidence || ''}`).join('\n');
7577
+ const untrackedTodos = newTodos.filter(t => !taskText.includes(t.file));
7578
+ if (untrackedTodos.length > 0) {
7526
7579
  issues++;
7527
- _warn(`code has ${todoCount} TODO/FIXME/XXX (new: ${newTodos.length}) but no progress-tracker entry tracks them`,
7528
- { kind: 'todo_untracked', severity: 'warn', todoCount, newCount: newTodos.length, newTodos: newTodos.slice(0, 5) });
7529
- // TODO 처음 5개 표시 (verbose 모드만)
7530
- if (!jsonMode) newTodos.slice(0, 5).forEach(t => log(` ${t.file}:${t.line} ${t.text}`));
7531
- if (has('--auto-track') && newTodos.length) {
7580
+ _warn(`code has ${todoCount} TODO/FIXME/XXX (untracked: ${untrackedTodos.length}) no progress-tracker entry references their files`,
7581
+ { kind: 'todo_untracked', severity: 'warn', todoCount, newCount: newTodos.length, untrackedCount: untrackedTodos.length, newTodos: untrackedTodos.slice(0, 5) });
7582
+ // 미추적 TODO 처음 5개 표시 (verbose 모드만)
7583
+ if (!jsonMode) untrackedTodos.slice(0, 5).forEach(t => log(` ${t.file}:${t.line} ${t.text}`));
7584
+ if (has('--auto-track') && untrackedTodos.length) {
7532
7585
  // 1.9.411 (8번째 버그헌트, UR-0115): TODO 일괄 등록을 단일 read-modify-write 로 직렬화.
7533
7586
  // 종전: TODO 마다 nextId(plan+progress 전체 스캔) + upsertProgress(전체 read+write) → O(T × tracker크기) (다수 TODO 자동등록 시 O(N²) 행걸림).
7534
7587
  // 개선: 락 1회 안에서 rows 1회 읽고, 최대 T-id 1회 계산, 전부 push, 1회 write → O(N + T).
@@ -7606,7 +7659,8 @@ function preCheck(root) {
7606
7659
  function memorySearch(root, query) {
7607
7660
  root = absRoot(root);
7608
7661
  if (!query) { fail('query required (e.g., memory search "키워드")'); return; }
7609
- const files = ['.harness/decisions.md','.harness/task-log.md','.harness/session-handoff.md','.harness/progress-tracker.md','.harness/plan.md','.harness/review-evidence.md','.harness/architecture.md'];
7662
+ // 1.13.1 (15th 블라인드 리뷰 P1, Sonnet): lessons.md + rules.md 누락 수정 — memory search 가 5종 메모리 표면을 표방하나 lesson/rule 을 검색 못 해(lesson add/rule add 로 저장한 교훈·룰이 'no matches') 모순감지 핵심 용도가 훼손됐음.
7663
+ const files = ['.harness/decisions.md','.harness/lessons.md','.harness/rules.md','.harness/task-log.md','.harness/session-handoff.md','.harness/progress-tracker.md','.harness/plan.md','.harness/review-evidence.md','.harness/architecture.md'];
7610
7664
  const re = new RegExp(query.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'), 'i');
7611
7665
  let total = 0;
7612
7666
  for (const f of files) {
@@ -9580,6 +9634,9 @@ function verifyClaimCmd(root, taskId) {
9580
9634
  const gitApplicable = !!gitChanged && gitChanged.size > 0 && files.length > 0;
9581
9635
  const claimedInGit = gitApplicable ? files.filter(f => _claimFileInGit(f, gitChanged)) : [];
9582
9636
  const claimedNotInGit = gitApplicable ? files.filter(f => !_claimFileInGit(f, gitChanged)) : [];
9637
+ // 1.13.2 (Karpathy 가이드라인 3 "외과적 변경", UR-0030): 역방향 교차검증 — git 에 변경됐으나 evidence/주장에 없는 파일(scope-creep / 요청 범위 밖 변경 신호). 하네스 자체 기록(.harness 등)은 제외. advisory(오탐 방지 — 기본 FAIL 아님, 표면화만).
9638
+ const _SCOPE_SKIP = /^(\.harness[\\/]|\.git[\\/]|node_modules[\\/]|\.claude[\\/]|dist[\\/]|build[\\/])/;
9639
+ const changedNotClaimed = gitApplicable ? [...gitChanged].filter(g => !_SCOPE_SKIP.test(g) && !files.some(f => _claimFileInGit(f, new Set([g])))) : [];
9583
9640
  // 테스트 카운트: tests/test.js의 check( 또는 it( 또는 test( 개수
9584
9641
  let actualTestCount = null;
9585
9642
  const candidateTestFiles = ['tests/test.js', 'test/test.js', 'tests/index.js'];
@@ -9681,7 +9738,8 @@ function verifyClaimCmd(root, taskId) {
9681
9738
  },
9682
9739
  evidence: { required: mustHaveEvidence, ...evq },
9683
9740
  claims: !claimsChecked ? null : { ok: strictOk, optimism: optimismSuspects.map(s => ({ kind: s.kind, label: s.label })), honesty: honestyFindings.map(f => ({ dim: f.dim, label: f.label })) },
9684
- git: gitChanged === null ? { applicable: false, reason: 'not-a-git-repo' } : (!gitApplicable ? { applicable: false, reason: 'no-working-changes-or-no-claimed-files' } : { applicable: true, claimedInGit: claimedInGit.length, claimedNotInGit, strongMismatch: gitStrongMismatch })
9741
+ git: gitChanged === null ? { applicable: false, reason: 'not-a-git-repo' } : (!gitApplicable ? { applicable: false, reason: 'no-working-changes-or-no-claimed-files' } : { applicable: true, claimedInGit: claimedInGit.length, claimedNotInGit, strongMismatch: gitStrongMismatch, changedNotClaimed }),
9742
+ scopeCreep: !gitApplicable ? null : { count: changedNotClaimed.length, files: changedNotClaimed.slice(0, 10) } // 1.13.2 (Karpathy 외과적 변경): 요청/주장 밖 변경 파일(advisory)
9685
9743
  };
9686
9744
  if (runResult) {
9687
9745
  out.run = runResult;
@@ -9764,6 +9822,8 @@ function verifyClaimCmd(root, taskId) {
9764
9822
  } else {
9765
9823
  log(` - git diff 교차검증: ${gitStrongMismatch ? '⚠ 불일치' : '✓'} 주장 ${files.length}개 중 실제 변경 ${claimedInGit.length}개${claimedNotInGit.length ? ` · git 변경에 없음: ${claimedNotInGit.slice(0, 5).join(', ')}` : ''}`);
9766
9824
  if (gitStrongMismatch) log(` · 주장한 파일이 working tree/직전커밋 변경에 전무 — 변경이 더 오래전 커밋이거나, 실제로 변경 안 됐을 수 있음(허위완료 의심)${has('--strict-claims') ? ' → FAIL' : ' (advisory — 커밋 후 검증 시 정상일 수 있음)'}`);
9825
+ // 1.13.2 (Karpathy 가이드라인 3 "외과적 변경", UR-0030): scope-creep 표면화 — git 변경됐으나 evidence/주장에 없는 파일.
9826
+ if (changedNotClaimed.length) log(` · 🔬 외과적 변경 점검: git 에 변경됐으나 evidence/주장에 없는 파일 ${changedNotClaimed.length}건: ${changedNotClaimed.slice(0, 5).join(', ')}${changedNotClaimed.length > 5 ? ' …' : ''} — 요청 범위 밖 변경(scope creep)인지 확인 (advisory)`);
9767
9827
  }
9768
9828
  // 1.9.309 (UR-0048): done 주장 evidence 완전성 — 기본 강제(상단 pre-compute). --lenient 로 opt-out.
9769
9829
  if (mustHaveEvidence) {
@@ -10267,7 +10327,8 @@ function _scanCodeForPatterns(root) {
10267
10327
  if (budget <= 0) return;
10268
10328
  if (e.isDirectory()) { if (!SKIP.test(e.name) && !e.name.startsWith('.')) walk(path.join(p, e.name), depth + 1); continue; }
10269
10329
  if (!/\.(js|ts|jsx|tsx|gd|cs|py|rb|go|rs|java|php|kt|swift)$/i.test(e.name)) continue;
10270
- try { const t = read(path.join(p, e.name)); combined += t + '\n'; budget -= t.length; } catch {}
10330
+ // 1.12.5 (15th 버그헌트 P2, UR-0019): stat-before-read 이전엔 read() budget 검사라 대형 파일 1개가 통째 메모리에 로드(200MB→RSS 464MB). 남은 budget 초과 파일은 읽지 않고 건너뜀.
10331
+ try { const fp2 = path.join(p, e.name); if (fs.statSync(fp2).size > budget) continue; const t = read(fp2); combined += t + '\n'; budget -= t.length; } catch {}
10271
10332
  }
10272
10333
  }
10273
10334
  walk(root, 0);
@@ -15344,13 +15405,14 @@ function mcpServeCmd(root) {
15344
15405
  send({ jsonrpc: '2.0', id, result: { tools: TOOLS } });
15345
15406
  } else if (req.method === 'tools/call') {
15346
15407
  const { name, arguments: args = {} } = req.params || {};
15408
+ // 1.12.2 (14th 버그헌트, UR-0181): MCP 서버는 generic 설계 — cwd 에서 실행 후 각 호출의 path 로 대상 프로젝트 지정(의도된 기능, e2e 가 검증). 즉 path-타게팅 자체는 취약점 아님(신뢰된 로컬 MCP). policy 도 대상-프로젝트별로 path 에서 로드(올바름).
15409
+ // 진짜 버그는 unknown tool 도 _bumpMcpUsage 가 임의 경로에 무조건 쓰던 것 → 사용 통계를 unknown-tool 검증 "후"로 이동. (프로젝트-scoped 가둠은 opt-in 플래그로 별도 검토 — UR backlog)
15347
15410
  const targetPath = args.path || root;
15348
- // 1.9.70: MCP tools/call 자동 사용 통계 — 어떤 도구가 자주/드물게 호출되는지 가시화
15349
- try { _bumpMcpUsage(targetPath, name); } catch {}
15350
15411
  let cliArgs;
15351
15412
  try {
15352
15413
  cliArgs = _mcpToCliArgs(name, args, targetPath);
15353
15414
  if (cliArgs === null) return send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown tool: ${name}` } });
15415
+ try { _bumpMcpUsage(targetPath, name); } catch {} // 알려진 도구만 통계 기록(unknown tool 의 임의경로 쓰기 차단)
15354
15416
  // 1.9.288 (Codex gpt-5.5 리뷰 #1 수렴): MCP 도구도 policy enforce 적용 — read-only enforce 시 write 도구 차단.
15355
15417
  // 이전: _policyEnforce 는 agents multi --execute 한 곳뿐 → MCP state_start 등이 정책 우회하고 .leerness 기록.
15356
15418
  // cliArgs(실제 실행 명령) 로 required tier 판정 → enforce ON 이고 초과 시 JSON-RPC error 반환(실행 안 함).
@@ -15365,7 +15427,9 @@ function mcpServeCmd(root) {
15365
15427
  const r = callLeerness(cliArgs);
15366
15428
  // 1.9.61: cursor 기반 페이지네이션 — 긴 출력은 cursor offset로 다음 청크
15367
15429
  const fullText = r.stdout || r.stderr || '(no output)';
15368
- const CHUNK_SIZE = (args._chunkSize && Number.isFinite(args._chunkSize)) ? args._chunkSize : 50000;
15430
+ // 1.12.4 (15th 버그헌트 P2, UR-0016): _chunkSize 양의 정수로 클램프 — 음수/소수면 slice 빈 출력 + nextCursor 음수 → 무한 빈-루프(데이터 손실)였음.
15431
+ const _cs = Math.floor(Number(args._chunkSize));
15432
+ const CHUNK_SIZE = (Number.isFinite(_cs) && _cs > 0) ? _cs : 50000;
15369
15433
  const cursor = (args._cursor && /^\d+$/.test(String(args._cursor))) ? parseInt(args._cursor, 10) : 0;
15370
15434
  const chunk = fullText.slice(cursor, cursor + CHUNK_SIZE);
15371
15435
  const nextCursor = (cursor + CHUNK_SIZE) < fullText.length ? String(cursor + CHUNK_SIZE) : null;
@@ -19056,6 +19120,8 @@ async function main() {
19056
19120
  return log(VERSION);
19057
19121
  }
19058
19122
  if (has('--help') || has('-h')) return help();
19123
+ // 1.13.1 (15th 블라인드 리뷰 P1, codex gpt-5.5): 무명령 + 옵션만(예: leerness --json) 은 묵시적 init 쓰기를 하지 않음 — cwd 에 .harness 가 의도치 않게 생성되던 부작용 차단. 명시 명령 없이 옵션만이면 help (bare leerness 온보딩 init 은 유지).
19124
+ if (!args[0] && process.argv.slice(2).some(a => a.startsWith('-'))) { help(); return; }
19059
19125
  // 1.9.38 (B): 사용 통계 카운터 — usage stats 명령 자체와 비차단 경로는 제외
19060
19126
  // 1.9.317 (UR-0051, 설치리뷰): 내부 auto-call(LEERNESS_INTERNAL=1) 은 usage 집계 제외 — 텔레메트리 오염(거짓 skill 추천) 방지.
19061
19127
  if (process.env.LEERNESS_INTERNAL !== '1' && cmd !== 'usage' && cmd !== 'init' && cmd !== 'migrate' && cmd !== '--version' && cmd !== '--help') {