leerness 1.19.0 → 1.21.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,116 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.21.0 — 2026-06-15 — 🛡️ [안정화/Stable] 영어 온보딩 첫걸음 + 검증 강화 안정 minor
4
+
5
+ **🛡️ 안정화(Stable) minor — 비한국어 사용자에게 영어 온보딩을 실제로 전달.** 직전 minor(1.20.0) 이후 누적된 패치 2건(1.20.1~1.20.2)을 검증·통합해 npm 공개. R-0011 정책의 12번째 stable minor. 핵심: **영어 첫 화면(opt-in)** 이 npm 사용자에게 닿고, 검증 플래그십의 정적 우회가 닫힘.
6
+
7
+ ### 이번 minor 통합 (1.20.1~1.20.2)
8
+ - **🌐 CLI 영어화 Phase 1 (UR-0010, 사용자 지정 방향)**: 영어 README 정문 ↔ 한국어 CLI 불일치(외부평가 #1 병목) 해소 시작. `--language en` / `LEERNESS_LANG=en` / en 으로 init 된 프로젝트에서 **init 시작하기 배너 전체 + handoff 헤드라인 라벨**이 영어로. 한국어 우선 정체성은 **기본값 유지**(영어는 명시 opt-in, system locale 미사용). 순수 `_uiLang`/`_tx`.
9
+ - **🛡️ 장식 no-op 정적 우회 차단 (검증)**: `"use strict"; module.exports={}` · `{}; //c` · `void 0;` · `;;;;` · `0;` 같은 "실로직 0" 파일이 정적 verify-claim 을 통과하던 우회를 차감식 residue 검사로 폐쇄(FP 0).
10
+ - **📄 README 플래그십 데모 정합**: task-id(T-0002) + "실파일 쓰면 같은 명령 통과" 정정 — 신규 유저 복붙이 실제로 동작. lens 헤더 동적 버전.
11
+ - **🔬 신뢰 투명성/렌즈 완전판** (1.19.x 누적 — 1.20.0 에 포함됨, 본 minor 는 1.20.x 패치 묶음).
12
+
13
+ ### 잔여 (UR-0010 Phase 2+, 백로그)
14
+ - handoff 헤드라인 **항목 라벨**(drift·보안·skills·health·mem 등 ~20종) + session close·verify-claim·help·status 출력 영어화 — 단계적 확대.
15
+
16
+ ### 검증 (회귀 0)
17
+ - **selftest 235→236** · **E2E 365/365** · 영어 배너/헤드라인 행위 + no-op 우회 차단 + 게시본 재실증.
18
+
19
+ ### 안정화 표시 (R-0006)
20
+ CHANGELOG [안정화/Stable] · git tag (Stable) · GitHub release (`--latest`) · npm dist-tag `stable` 시도.
21
+
22
+ ## 1.20.2 — 2026-06-15 — CLI 영어화 Phase 1: 첫 화면(init 배너·handoff 헤드라인) 영어 opt-in (UR-0010)
23
+
24
+ **🌐 "어떤 언어, 어떤 AI 에이전트로 작업하든"의 실질 첫걸음.** 외부평가가 꼽은 단일 최대 병목 — 영어 README 정문 ↔ 한국어 CLI 불일치 — 를 단계적으로 해소 시작. 한국어 우선 정체성은 기본값으로 유지하고, **영어는 명시 opt-in**.
25
+
26
+ ### 변경 (UR-0010 Phase 1)
27
+ - **UI 언어 해석 `_uiLang(root)`**: 우선순위 `--language` 플래그 > `LEERNESS_LANG` env > `.harness/manifest.json` 의 language(init 선택) > **`ko`(기본)**. system locale 은 의도적으로 미사용(영어 OS 한국 사용자 놀람 방지) — 영어는 항상 명시 opt-in.
28
+ - **init 시작하기 배너 영어화**: `--language en`(또는 LEERNESS_LANG=en, 또는 en 으로 init 된 프로젝트)에서 "Get started (3 steps)" + 메모리/인과/보안/MCP/release 섹션 전체 영어로. 비한국어 신규 유저의 첫 화면이 더 이상 한국어 벽이 아님.
29
+ - **handoff 헤드라인 라벨 영어화**: `📊 헤드라인 (버전태그…)` → 영어 시 `📊 Headline`(버전태그 노이즈 제거).
30
+ - 순수 함수 `_uiLang`/`_tx` 분리(단위 테스트). **한국어 기본 회귀 0**(플래그/env/manifest 없으면 그대로 한국어).
31
+
32
+ ### 잔여 (UR-0010 Phase 2+, 백로그)
33
+ - handoff 헤드라인 **항목 라벨**(보안 OK·skills·health…) 영어화, 그 외 명령(session close·verify-claim 출력·help·status 등) 영어화 — 단계적 확대. Phase 1 은 첫인상 표면(배너+헤드라인 프리픽스)에 한정.
34
+
35
+ ### 검증 (회귀 0)
36
+ - **selftest 235→236** (`_uiLang` 해석 4종 + `_tx` + 첫화면 분기 소스가드) · 행위: `--language en` → 영어 배너/헤드라인, 플래그 없음 → 한국어 유지 · **E2E 365/365**.
37
+ - patch(1.20.2) — npm 미배포(R-0011, GitHub/CHANGELOG 누적).
38
+
39
+ ## 1.20.1 — 2026-06-15 — 1.20.0 외부평가 채택: 장식 no-op 우회 차단 + README 데모 정합
40
+
41
+ **🔎 게시본 1.20.0 신규 멀티모델 클린룸 평가(5축) 채택 — 맹신 X로 재현된 것만.** 검증(8)·정직성/보안(9)·품질렌즈(8)는 GPT-5.5 대비 뚜렷이 상승했고, 평가가 찾은 재현 가능한 P2 두 가지(검증 정적모드 우회 + README 데모 부정확)를 닫음.
42
+
43
+ ### 변경 (1.20.0 외부평가 재현 채택)
44
+ - **🛡️ 장식된 no-op 정적 우회 차단 (검증, P2)**: 빈껍데기 검출이 순수 빈-export 형태만 잡던 것 — `"use strict"; module.exports={}`·`{}; // comment`·`module.exports={}; void 0;`·`;;;;`·`0;` 같은 "실로직 0" 파일이 정적 `verify-claim`을 exit 0 통과하던 우회 폐쇄. `_vcImplIsEmpty` 에 **차감식(residue) 검사** 추가 — 디렉티브 프롤로그 + 빈 export + no-op 리터럴/문을 제거하고 의미 토큰이 하나도 안 남으면 스텁. FP 0(실코드·`require` 재노출·이름붙은 export·`module.exports=0` 같은 의도적 값은 통과). `let x=1` 류 무의미 선언은 식별자가 남아 통과 — AST 토큰화 필요(백로그).
45
+ - **📄 README 플래그십 데모 정합 (온보딩, P2)**: ① task-id 수정 — init 이 T-0001(계획 task)을 차지하므로 `task add` 는 T-0002 생성, 데모를 실제 흐름(출력된 id 사용)으로. ② "실파일+실테스트 쓰면 같은 명령 통과" 정정 — evidence 의 테스트 개수 주장은 실측과 대조되므로(거짓이면 거부) 정직한 evidence + `--run-tests --test-cmd` 안내로 교체. 신규 유저 복붙이 실제로 동작.
46
+ - **🔖 lens 헤더 버전 정합 (P3)**: 하드코딩 `(1.18.3)` → 동적 `(v${VERSION})`.
47
+
48
+ ### 백로그 등록 (평가 추천 우선순위, 이번 라운드 미포함)
49
+ - **UR-0010 (최우선)**: CLI UX 영어화 — `--language en` 이 템플릿 파일만 바꾸고 CLI 자체 출력(init 배너·handoff 헤드라인·help)은 한국어 유지. 영어 README 정문 ↔ 한국어 CLI 불일치가 비한국어 시장의 단일 최대 병목(거대 작업, UR-0042 잔여 통합).
50
+ - **UR-0011**: 표면 core-vs-extended 티어링 — 84커맨드+85툴 평면 노출, banner/about/commands/help 의 "start here" 4종 불일치 → 핵심8 표준화 + `--help` 정리.
51
+ - **UR-0012**: secret scan AWS AKIA 키 ID + 추가 prefix 커버리지.
52
+
53
+ ### 검증 (회귀 0)
54
+ - **selftest 235** (no-op 우회 6종 차단 + FP 가드 3종 행위) · 5종 우회 재현 차단·FP 0 확인 · README 데모 재현(task-id·count 정합) · **E2E 365/365**.
55
+ - patch(1.20.1) — npm 미배포(R-0011, GitHub/CHANGELOG 누적).
56
+
57
+ ## 1.20.0 — 2026-06-15 — 🛡️ [안정화/Stable] 품질 렌즈 완전판 + 신뢰 투명성 안정 minor
58
+
59
+ **🛡️ 안정화(Stable) minor — "AI가 스스로 질문하고 행동하도록" 완성.** 직전 minor(1.19.0) 이후 누적된 패치 3건(1.19.1~1.19.3)을 검증·통합해 npm 공개. R-0011 정책의 11번째 stable minor. 핵심 테마: **사용자 자기질문 품질 렌즈를 정적 명령에서 완전한 워크플로 기능으로** + 신뢰 투명성(클린룸 평가 공개).
60
+
61
+ ### 이번 minor 통합 (1.19.1~1.19.3)
62
+ - **🧭 품질 렌즈 완전판 (UR-0003, 사용자 핵심 철학)**: 1.18.3 의 정적 `lens` 명령을 3단계로 완성 —
63
+ - **v2 (1.19.2)**: `verify-claim` 이 완료를 검증하는 바로 그 순간, 주장된 파일 확장자로 관련 렌즈 질문을 advisory 로 노출(결정적 매핑, 게이트 아님). 코드: "선임 개발자가 복잡하다 느끼지 않을까?" / 디자인: "이쁘고 직관적인가?".
64
+ - **v3 (1.19.3)**: 프로젝트별 커스텀 렌즈 `.harness/quality-lenses.json` — 게임은 "60fps 끊김?", 웹은 접근성을 자기 기준에 추가. 내장+커스텀 불변 병합(읽기만, 쓰기 명령 없음).
65
+ - lens 도메인 대소문자/공백 정규화(1.19.1).
66
+ - **🔬 신뢰 투명성 (GPT-5.5 #5, UR-0009, 1.19.1)**: `docs/clean-room-evaluations.md` 공개 — README "독립 클린룸 평가" 주장을 확인 가능하게(방법론·4개 평가·**정직한 한계**). 과대였던 "5개" 수치를 검증 가능한 서술로 교체.
67
+ - **🛡️ 19th 버그헌트 (1.19.1)**: 1.18.x 신규 표면(권한 술어·빈껍데기 정규식·lens·audit) 엣지 14종 + 크래시 벡터 — 견고 확인(버그 0, lens 정규화만 수정).
68
+
69
+ ### 검증 (회귀 0)
70
+ - **selftest 231→235** · **E2E 365/365** · 렌즈 v2/v3 행위 재현 + 클린룸 문서 + 게시본 재실증.
71
+
72
+ ### 안정화 표시 (R-0006)
73
+ CHANGELOG [안정화/Stable] · git tag (Stable) · GitHub release (`--latest`) · npm dist-tag `stable` 시도.
74
+
75
+ ## 1.19.3 — 2026-06-14 — 품질 렌즈 완전판 v3: 프로젝트별 커스텀 렌즈 (UR-0003)
76
+
77
+ **🧭 렌즈를 "프로젝트 인지"로.** 내장 5종(code/design/docs/test/security)에 더해, 프로젝트가 자기 품질 기준을 추가할 수 있습니다 — 게임 프로젝트는 "60fps에서 끊기지 않나?", 웹 프로젝트는 접근성("키보드만으로 모든 기능 가능?")을 자기질문에 넣습니다. 사용자의 "각 분야 고려" 철학을 프로젝트 맥락까지 확장.
78
+
79
+ ### 변경 (UR-0003 렌즈 완전판 v3)
80
+ - **프로젝트 커스텀 렌즈**: `.harness/quality-lenses.json` 을 `lens` 와 verify-claim advisory 가 내장 catalog 와 **병합**. 포맷 `{ "domains": { "code": { "questions": ["..."] }, "a11y": { "title":"접근성", "persona":"...", "questions":[...] } } }`. 기존 도메인 → 질문 추가(dedup, 최대 8), 신규 도메인 → 추가. 표시에 `[프로젝트]`/`[+프로젝트 질문]` 라벨.
81
+ - **읽기-병합만, 쓰기 명령 없음 (R-0006 보수)**: leerness 는 JSON 을 읽어 병합·표시만 함(쓰기 경로 위험 0). JSON 은 AI/사용자가 편집 — 커스텀 없으면 편집 방법을 `lens` 가 안내. 순수 함수 `_mergeLensCatalog`(불변, 내장 catalog 미오염) + `_loadProjectLenses`.
82
+ - 잔여(UR-0003 완전판): handoff 힌트, MCP 노출, 인과관계 자동 추론 — 백로그.
83
+
84
+ ### 검증 (회귀 0)
85
+ - **selftest 234→235** (병합 행위: 기존 도메인 질문 추가·신규 도메인·잘못된 입력 무시·내장 불변성) · 프로젝트 커스텀 렌즈 행위 재현(게임 fps + a11y 도메인 병합 표시) · **E2E 365/365**.
86
+ - patch(1.19.3) — npm 미배포(R-0011, GitHub/CHANGELOG 누적).
87
+
88
+ ## 1.19.2 — 2026-06-14 — 품질 렌즈 완전판 v2: verify-claim 완료-검증 순간에 분야별 자기질문 (UR-0003)
89
+
90
+ **🧭 렌즈를 "수동 참조 명령"에서 "완료 검증 흐름의 일부"로.** 1.18.3 의 `leerness lens` 는 AI 가 직접 호출해야만 보였습니다. 이제 `verify-claim` 이 **완료를 검증하는 바로 그 순간**(사용자 표현: "완료 선언 전 자기질문") 주장된 파일의 종류에 맞는 렌즈 질문을 advisory 로 노출 — AI 의 자기질문이 수동 습관이 아니라 워크플로의 일부가 됩니다.
91
+
92
+ ### 변경 (UR-0003 렌즈 완전판 v2)
93
+ - **verify-claim 렌즈 advisory**: done 주장 검증 시, 주장된 파일 확장자로 관련 렌즈 도메인을 **결정적으로** 판정(`_lensDomainsForFiles` — 퍼지 키워드 추론 아님)해 해당 분야 질문을 노출. 예: `pay.js + app.css` → `code`(선임 개발자가 복잡하다 느끼지 않을까?) + `design`(이쁘고 직관적인가?). 매핑: UI/스타일(css/scss/tsx/jsx/vue/html)→design, 문서(md/mdx/rst)→docs, 테스트파일→test, 실코드(js/ts/py/…)→code. 최대 2개(클러터 방지), 고정 순서(code 먼저).
94
+ - **advisory 전용** — 게이트가 아님(exit code 불변). 기계검증(파일/테스트/스텁)을 통과해도 "사람이 보기에 좋은가"는 별개라는 점을 완료 순간에 상기. `--json` 경로엔 영향 없음(human 출력 한정).
95
+ - 잔여(UR-0003 완전판): 프로젝트별 커스텀 렌즈(`.harness/quality-lenses.json`), handoff 힌트, MCP 노출, 인과관계 자동 추론 — 백로그.
96
+
97
+ ### 검증 (회귀 0)
98
+ - **selftest 233→234** (`_lensDomainsForFiles` 행위: 단일/혼합/문서/테스트/빈/최대2 매핑) · verify-claim advisory 행위 재현(혼합 파일 → code+design) · **E2E 365/365**.
99
+ - patch(1.19.2) — npm 미배포(R-0011, GitHub/CHANGELOG 누적).
100
+
101
+ ## 1.19.1 — 2026-06-14 — 19th 버그헌트(신규 표면 견고 확인) + 클린룸 평가 공개 (GPT-5.5 #5)
102
+
103
+ **🔬 갓 배포한 1.19.0의 신규 표면을 적대적으로 헌트 — 표면은 견고, 사소 1건만 수정 + 신뢰증거 공개.** 1.18.x 추가분(권한 술어·빈껍데기 정규식·lens·audit)을 엣지케이스로 직접 탐침: 순수 술어 14종 + CLI 크래시/주입 벡터 — **크래시·스택트레이스·오판 0**. 유일 발견은 `lens` 도메인 대소문자 미정규화.
104
+
105
+ ### 변경
106
+ - **🧭 lens 도메인 정규화 (19th 헌트)**: `leerness lens Code` / `lens CODE ` 도 인식(trim + toLowerCase). 이전엔 정확한 소문자만 허용해 사소한 오류.
107
+ - **🔬 클린룸 평가 공개 (GPT-5.5 #5, UR-0009)**: `docs/clean-room-evaluations.md` 신설 — README 의 "독립 클린룸 평가로 검증" 주장을 **확인 가능하게**. 방법론(게시본 npm 설치·행위 검증·적대 시도), 수행한 4개 평가(5축 현장조사·1.18.0/1.19.0 게시본 재실증·1.18.2 적대 스텁 워크플로), 그리고 **정직한 한계**(휴리스틱이지 의미검증 아님 / 커버리지·mutation·제3자 재현은 미해결)를 명시. README(영/한)에서 링크. 과대홍보였던 "5개" 수치를 검증 가능한 서술로 교체.
108
+ - 잔여(UR-0009): 코드 커버리지/mutation testing 은 spawn 기반 e2e 특성상 별도 작업으로 백로그.
109
+
110
+ ### 검증 (회귀 0)
111
+ - **selftest 231→233** (lens 정규화 + 클린룸 문서 행위 가드) · 순수 술어 14종 엣지 통과 · 크래시 벡터 0 · **E2E 365/365**.
112
+ - patch(1.19.1) — npm 미배포(R-0011, GitHub/CHANGELOG 누적).
113
+
3
114
  ## 1.19.0 — 2026-06-14 — 🛡️ [안정화/Stable] 검증 하네스 강화 + 정직한 프레이밍 안정 minor
4
115
 
5
116
  **🛡️ 안정화(Stable) minor — "거짓 완료를 못 하게" 만드는 검증 계층을 한층 더 단단하게.** 직전 minor(1.18.0) 이후 누적된 패치 4건(1.18.1~1.18.4)을 검증·통합해 npm 공개. R-0011 정책의 10번째 stable minor. 핵심 테마: **검증 계층 강화(비-JS 거짓FAIL 해소 + 위장 스텁 차단) + AI 자기질문 품질 렌즈 + 정직한 프레이밍(협조 vs 강제, 영문 우선)**.
package/README.ko.md CHANGED
@@ -11,7 +11,7 @@
11
11
  ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝
12
12
  ```
13
13
 
14
- > **어떤 언어, 어떤 AI 에이전트로 작업하든 — "증거 없이는 끝났다고 말할 수 없게" 만드는 AI 코딩 운영 레이어.** 코드를 대신 쓰는 도구가 아니라, AI 에이전트의 **기억·인수인계·검증·감사·보안 가드**를 프로젝트에 영속화하는 CLI + MCP 서버입니다. (이 포지셔닝은 5개 독립 클린룸 실사용 평가 — Python/Node/Rust 실개발·에이전트 교대·적대 공격 — 로 검증됐습니다.)
14
+ > **어떤 언어, 어떤 AI 에이전트로 작업하든 — "증거 없이는 끝났다고 말할 수 없게" 만드는 AI 코딩 운영 레이어.** 코드를 대신 쓰는 도구가 아니라, AI 에이전트의 **기억·인수인계·검증·감사·보안 가드**를 프로젝트에 영속화하는 CLI + MCP 서버입니다. (이 포지셔닝은 독립 클린룸 평가 — Python/Node/Rust 실개발·에이전트 교대·검증기 적대 공격 — 로 확인됐습니다. 방법론·결과·정직한 한계: [docs/clean-room-evaluations.md](./docs/clean-room-evaluations.md))
15
15
 
16
16
  [![npm](https://img.shields.io/npm/v/leerness)](https://www.npmjs.com/package/leerness) · ![MCP tools](https://img.shields.io/badge/MCP--tools-85-blue) · **런타임 의존성 0** · **install-script 0** · offline-first · Node ≥ 18 · MIT
17
17
 
package/README.md CHANGED
@@ -28,12 +28,14 @@ npx leerness handoff . # everything your AI should know right now, in on
28
28
  Your project now has agent-independent memory. To see the flagship feature — catching a false "done" claim:
29
29
 
30
30
  ```bash
31
- npx leerness task add "Implement payment API"
32
- npx leerness task update T-0001 --status done --evidence "payment.js done, 5 tests passed"
33
- npx leerness verify-claim T-0001 # exit 1 — payment.js does not exist, no tests ran. Claim rejected.
31
+ npx leerness task add "Implement payment API" # prints the new id, e.g. T-0002 — use it below
32
+ npx leerness task update T-0002 --status done --evidence "payment.js implemented + tested"
33
+ npx leerness verify-claim T-0002 # exit 1 — payment.js does not exist. Claim rejected.
34
34
  ```
35
35
 
36
- Write the real file with a real test and the same command passes. That is the whole idea: **"done" must match reality.**
36
+ Now actually write `payment.js`, then run the **same** `verify-claim T-0002` it exits 0. That is the whole idea: **"done" must match reality.**
37
+
38
+ > Tip: if your evidence claims a specific test count (e.g. "5 tests passed"), leerness measures the real count and rejects a mismatch — so claim only what's true, or add `--run-tests --test-cmd "<your test cmd>"` to verify by running them.
37
39
 
38
40
  > Want a smaller footprint? `leerness init . --minimal` installs only the core memory + verification files instead of the full set.
39
41
 
@@ -63,7 +65,7 @@ Built-in harnesses remember what the AI **said**. leerness verifies what the AI
63
65
  | Secrets · encoding · drift guards | none | `scan secrets` · `encoding check` · `drift check --auto-fix` — CI-ready |
64
66
  | Lock-in | one vendor | any agent, any language, 0 runtime dependencies |
65
67
 
66
- This positioning was verified by **5 independent clean-room evaluations** — real Python/Node/Rust development, agent swaps, and adversarial attacks against the verifier itself (fake tests, comment-only stubs, inflated test counts — all rejected).
68
+ This positioning was verified by **independent clean-room evaluations** — fresh `npm install` into temp dirs, driven by behavior only, including adversarial attacks against the verifier itself (fake tests, comment-only stubs, inflated test counts — all rejected). Methodology, results, and honest limitations: **[docs/clean-room-evaluations.md](./docs/clean-room-evaluations.md)**.
67
69
 
68
70
  ---
69
71
 
@@ -102,7 +104,7 @@ MIT
102
104
  <!-- leerness:project-readme:start -->
103
105
  ## Leerness Project Harness
104
106
 
105
- 이 프로젝트는 Leerness v1.19.0 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
107
+ 이 프로젝트는 Leerness v1.21.0 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
106
108
 
107
109
  ### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
108
110
 
@@ -156,7 +158,7 @@ leerness memory restore decision <date|title>
156
158
 
157
159
  ### MCP server (외부 AI 통합)
158
160
 
159
- Leerness v1.19.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
161
+ Leerness v1.21.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
160
162
 
161
163
  ```jsonc
162
164
  // 카테고리별
@@ -177,7 +179,7 @@ Leerness v1.19.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code
177
179
  `<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
178
180
  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) 다음 라운드 예약.
179
181
 
180
- 현재 누적: **70 라운드 (1.9.40 → 1.19.0)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
182
+ 현재 누적: **70 라운드 (1.9.40 → 1.21.0)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
181
183
 
182
184
  ### 성능 가이드 (1.9.140 측정)
183
185
 
@@ -215,6 +217,6 @@ leerness release pack --close --auto-main-push
215
217
  - `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
216
218
  - `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
217
219
 
218
- Last synced by Leerness v1.19.0: 2026-06-14
220
+ Last synced by Leerness v1.21.0: 2026-06-15
219
221
  <!-- leerness:project-readme:end -->
220
222
 
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.19.0';
35
+ const VERSION = '1.21.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') 시 호스트 프로세스 오염.
@@ -252,6 +252,22 @@ function detectLanguageValue(root, value = 'auto') {
252
252
  // ③ en 폴백
253
253
  return 'en';
254
254
  }
255
+ // 1.20.2 (UR-0010 CLI 영어화 Phase 1): UI 출력 언어 해석 — 한국어 우선 기본, 영어 opt-in.
256
+ // 우선순위: --language 플래그 > LEERNESS_LANG env > .harness/manifest.json 의 language(init 선택) > 'ko'.
257
+ // (system locale 은 의도적으로 미사용 — 영어 OS 한국 사용자 놀람 방지. 영어는 명시 opt-in.)
258
+ function _uiLang(root) {
259
+ try {
260
+ const flag = String(arg('--language', '') || '').toLowerCase();
261
+ if (flag === 'en' || flag === 'ko') return flag;
262
+ const env = String(process.env.LEERNESS_LANG || '').toLowerCase();
263
+ if (env === 'en' || env === 'ko') return env;
264
+ const mf = path.join(absRoot(root || process.cwd()), '.harness', 'manifest.json');
265
+ if (exists(mf)) { const l = String((JSON.parse(read(mf)) || {}).language || '').toLowerCase(); if (l === 'en' || l === 'ko') return l; }
266
+ } catch {}
267
+ return 'ko';
268
+ }
269
+ // ko/en 쌍에서 해석된 UI 언어로 선택 (Phase 1: 첫 화면 한정 사용).
270
+ function _tx(lang, ko, en) { return lang === 'en' ? en : ko; }
255
271
  function fm(role, readWhen, updateWhen, body) {
256
272
  return `---\nleernessRole: ${role}\nreadWhen:\n${readWhen.map(x => ' - ' + x).join('\n')}\nupdateWhen:\n${updateWhen.map(x => ' - ' + x).join('\n')}\ndoNotStore:\n - 실제 토큰\n - 비밀번호\n - 운영 쿠키\n - 민감한 개인정보 원문\n---\n${MARK}\n${body}`;
257
273
  }
@@ -3624,7 +3640,18 @@ function _selfTestCases() {
3624
3640
  && E('module.exports = Object.freeze({ a: 1 });\n') === false
3625
3641
  && E('module.exports = class { run(){ return 1; } };\n') === false
3626
3642
  && E('module.exports = (a,b) => a+b;\n') === false;
3627
- return base && bypass && real;
3643
+ // 1.20.1 (1.20.0 외부평가 재현): 장식된 no-op 우회(true 여야 함) — 디렉티브 프롤로그 + no-op 문
3644
+ const noop = E('"use strict";\nmodule.exports = {};\n') === true
3645
+ && E('{}; // c\n') === true
3646
+ && E('module.exports = {};\nvoid 0;\n') === true
3647
+ && E(';;;;\n') === true
3648
+ && E('0;\n') === true
3649
+ && E("'use strict';\nexports.default = {};\n") === true;
3650
+ // 1.20.1 FP 가드(false 여야 함): 단일 의미 토큰만 있어도 통과
3651
+ const noopFP = E('"use strict";\nfunction f(){ return 1; }\nmodule.exports = { f };\n') === false
3652
+ && E('module.exports = 0;\n') === false // 0 을 export 하는 의도적 값 — 스텁 아님
3653
+ && E('module.exports = require("./x"); void 0;\n') === false;
3654
+ return base && bypass && real && noop && noopFP;
3628
3655
  } },
3629
3656
  { name: '위장 스텁 차단 (1.18.2): stub 루프 _vcImplIsEmpty 사용 + 메시지 + FILE_EXTS java/php 정합 (소스 가드)', run: () => {
3630
3657
  const src = read(__filename);
@@ -3657,6 +3684,59 @@ function _selfTestCases() {
3657
3684
  const a = read(path.join(path.dirname(__filename), '..', 'lib', 'audit.js'));
3658
3685
  return a.includes('Last synced by Leerness v') && a.includes('readme_synced_version_stale');
3659
3686
  } },
3687
+ { name: '19th 헌트 (1.19.1): lens 도메인 대소문자/공백 정규화 (소스 가드)', run: () => {
3688
+ const src = read(__filename);
3689
+ return src.includes("if (domain != null) domain = String(domain).trim().toLowerCase();");
3690
+ } },
3691
+ { name: '렌즈 완전판 v3 (1.19.3, UR-0003): 프로젝트 커스텀 렌즈 병합 (행위)', run: () => {
3692
+ const M = _mergeLensCatalog;
3693
+ const a = M({ code: { title: '코드', persona: 'p', questions: ['Q1'], affects: [] } }, { code: { questions: ['Q1', 'Q2'] } });
3694
+ const appendOk = a.code.questions.length === 2 && a.code.questions[1] === 'Q2' && a.code._customAdded === true;
3695
+ const b = M({ code: { title: '코드', persona: 'p', questions: ['Q'], affects: [] } }, { a11y: { title: '접근성', persona: '스크린리더', questions: ['키보드로 가능?'] } });
3696
+ const newOk = b.a11y && b.a11y.title === '접근성' && b.a11y._custom === true && b.code.questions.length === 1;
3697
+ const c = M({ code: { questions: ['Q'], affects: [] } }, { bad: { title: 'x' }, nope: null });
3698
+ const ignoreOk = !c.bad && !c.nope && c.code.questions.length === 1;
3699
+ const before = LENS_CATALOG.code.questions.length;
3700
+ M(LENS_CATALOG, { code: { questions: ['임시'] } });
3701
+ const immutableOk = LENS_CATALOG.code.questions.length === before;
3702
+ return appendOk && newOk && ignoreOk && immutableOk;
3703
+ } },
3704
+ { name: '렌즈 완전판 v2 (1.19.2, UR-0003): 파일 확장자 → 렌즈 도메인 매핑 (행위)', run: () => {
3705
+ const F = _lensDomainsForFiles;
3706
+ const eq = (a, b) => JSON.stringify(a) === JSON.stringify(b);
3707
+ return eq(F(['src/api.js']), ['code'])
3708
+ && eq(F(['ui/Button.tsx', 'styles/app.css']), ['design']) // UI/스타일 → design (코드 ext 아닌 tsx 는 design 우선)
3709
+ && eq(F(['src/pay.js', 'src/Button.tsx']), ['code', 'design']) // 혼합 → code 먼저, design (최대 2)
3710
+ && eq(F(['README.md']), ['docs'])
3711
+ && eq(F(['tests/foo.test.js']), ['code', 'test']) // 테스트파일(.js) → code+test (고정 순서: code 먼저)
3712
+ && eq(F([]), [])
3713
+ && eq(F(['a.css', 'b.md', 'c.js', 'd.test.js']).length, 2); // 최대 2개 클러터 방지
3714
+ } },
3715
+ { name: 'GPT-5.5 평가 #5 (1.19.1, UR-0009): 클린룸 평가 문서 공개 + 한계 명시 (행위)', run: () => {
3716
+ const dp = path.join(path.dirname(__filename), '..', 'docs', 'clean-room-evaluations.md');
3717
+ if (!exists(dp)) return true; // 패키지에 없으면 스킵(설치본 안전)
3718
+ const d = read(dp);
3719
+ return /AI clean-room evaluations/i.test(d) && /heuristic, not semantic/i.test(d) && /npm i leerness@/.test(d);
3720
+ } },
3721
+ { name: 'CLI 영어화 Phase 1 (1.20.2, UR-0010): _uiLang 해석(flag>env>manifest>ko) + 첫화면 _t 적용 (행위+소스)', run: () => {
3722
+ const save = process.argv; const saveEnv = process.env.LEERNESS_LANG;
3723
+ try {
3724
+ // 기본 ko (한국어 우선 정체성 보존)
3725
+ process.argv = ['node', 'h', 'handoff']; delete process.env.LEERNESS_LANG;
3726
+ if (_uiLang('/no/such/dir') !== 'ko') return false;
3727
+ // --language en 플래그 → en
3728
+ process.argv = ['node', 'h', 'handoff', '--language', 'en'];
3729
+ if (_uiLang('/no/such/dir') !== 'en') return false;
3730
+ // LEERNESS_LANG env → en (플래그 없을 때)
3731
+ process.argv = ['node', 'h', 'handoff']; process.env.LEERNESS_LANG = 'en';
3732
+ if (_uiLang('/no/such/dir') !== 'en') return false;
3733
+ // _tx 선택
3734
+ if (_tx('en', '가', 'A') !== 'A' || _tx('ko', '가', 'A') !== '가') return false;
3735
+ } finally { process.argv = save; if (saveEnv === undefined) delete process.env.LEERNESS_LANG; else process.env.LEERNESS_LANG = saveEnv; }
3736
+ // 첫화면(배너/헤드라인)이 언어 분기 사용
3737
+ const src = read(__filename);
3738
+ return src.includes("const L = _uiLang(arg('--path', process.cwd()));") && src.includes("_uiLang(root) === 'en' ? '📊 Headline'");
3739
+ } },
3660
3740
  { name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
3661
3741
  ];
3662
3742
  }
@@ -4235,23 +4315,74 @@ const LENS_CATALOG = {
4235
4315
  affects: ['code', 'test'], affectsNote: '보안 가드를 넣었다면 우회/오탐 테스트가 따라와야 함'
4236
4316
  }
4237
4317
  };
4318
+ // 1.19.3 (UR-0003 렌즈 완전판 v3): 프로젝트별 커스텀 렌즈 — .harness/quality-lenses.json 읽기-병합(쓰기 명령 없음, AI/사용자가 편집).
4319
+ // 포맷: { "domains": { "code": { "questions": ["추가 질문"] }, "a11y": { "title":"접근성", "persona":"스크린리더 사용자", "questions":[...], "affects":["design"] } } }
4320
+ function _loadProjectLenses(root) {
4321
+ try {
4322
+ const p = path.join(absRoot(root || process.cwd()), '.harness', 'quality-lenses.json');
4323
+ if (!exists(p)) return {};
4324
+ const j = JSON.parse(read(p));
4325
+ return (j && typeof j === 'object' && j.domains && typeof j.domains === 'object') ? j.domains : {};
4326
+ } catch { return {}; }
4327
+ }
4328
+ // 내장 + 프로젝트 커스텀 병합. 기존 도메인 → 질문 추가(dedup, 최대 8); 신규 도메인 → 기본값으로 추가. _custom 플래그로 표시.
4329
+ function _mergeLensCatalog(builtin, custom) {
4330
+ const out = {};
4331
+ for (const [k, v] of Object.entries(builtin || {})) out[k] = Object.assign({}, v, { questions: (v.questions || []).slice(), affects: (v.affects || []).slice() });
4332
+ for (const [k, c] of Object.entries(custom || {})) {
4333
+ if (!c || typeof c !== 'object') continue;
4334
+ const cq = Array.isArray(c.questions) ? c.questions.filter(q => typeof q === 'string' && q.trim()) : [];
4335
+ if (out[k]) {
4336
+ const seen = new Set(out[k].questions);
4337
+ for (const q of cq) if (!seen.has(q)) { out[k].questions.push(q); seen.add(q); }
4338
+ out[k].questions = out[k].questions.slice(0, 8);
4339
+ out[k]._customAdded = cq.length > 0;
4340
+ } else if (cq.length) {
4341
+ out[k] = { title: (c.title || k), persona: (c.persona || '검토자'), questions: cq.slice(0, 8), affects: Array.isArray(c.affects) ? c.affects : [], affectsNote: c.affectsNote || '(프로젝트 정의)', _custom: true };
4342
+ }
4343
+ }
4344
+ return out;
4345
+ }
4346
+ function _effectiveLensCatalog(root) { return _mergeLensCatalog(LENS_CATALOG, _loadProjectLenses(root)); }
4347
+
4348
+ // 1.19.2 (UR-0003 렌즈 완전판 v2): 주장된 파일 확장자 → 관련 품질 렌즈 도메인 (결정적, 키워드 추론 아님).
4349
+ // verify-claim 이 완료-검증 순간(사용자: "완료 선언 전 자기질문")에 해당 분야 질문을 advisory 로 노출하는 데 사용.
4350
+ function _lensDomainsForFiles(files) {
4351
+ const out = [];
4352
+ const arr = Array.isArray(files) ? files : [];
4353
+ const has = (re) => arr.some(f => typeof f === 'string' && re.test(f));
4354
+ if (has(/\.(css|scss|sass|less|styl|tsx|jsx|vue|svelte|html?|astro)$/i)) out.push('design'); // UI/스타일
4355
+ if (has(/\.(md|mdx|rst|adoc|txt)$/i)) out.push('docs'); // 문서
4356
+ if (has(/(^|[\\/])(test_[^\\/]+\.[a-z]+|[^\\/]+[._-]test\.[a-z]+|[^\\/]+\.spec\.[a-z]+)$|(^|[\\/])tests?[\\/]/i)) out.push('test'); // 테스트
4357
+ if (has(/\.(js|mjs|cjs|ts|py|rb|go|rs|java|cs|php)$/i)) out.push('code'); // 실코드
4358
+ // 순서: code 먼저(가장 일반), 그다음 design/docs/test. 중복 제거 + 최대 2개(클러터 방지).
4359
+ const ordered = ['code', 'design', 'docs', 'test'].filter(d => out.includes(d));
4360
+ return ordered.slice(0, 2);
4361
+ }
4238
4362
  function lensCmd(domain, opts = {}) {
4239
4363
  const jsonMode = !!opts.json || has('--json');
4240
- if (domain && !LENS_CATALOG[domain]) {
4241
- return fail(`알 없는 렌즈: ${domain} 유효값: ${Object.keys(LENS_CATALOG).join(', ')}`);
4242
- }
4243
- const picked = domain ? { [domain]: LENS_CATALOG[domain] } : LENS_CATALOG;
4364
+ // 1.19.1 (19th 버그헌트): 도메인 인자 정규화 — `lens Code` / `lens CODE ` 도 인식(대소문자·공백 무관).
4365
+ if (domain != null) domain = String(domain).trim().toLowerCase();
4366
+ // 1.19.3: 내장 + 프로젝트 커스텀(.harness/quality-lenses.json) 병합 catalog.
4367
+ const root = opts.root || arg('--path', process.cwd());
4368
+ const catalog = _effectiveLensCatalog(root);
4369
+ if (domain && !catalog[domain]) {
4370
+ return fail(`알 수 없는 렌즈: ${domain} — 유효값: ${Object.keys(catalog).join(', ')}`);
4371
+ }
4372
+ const picked = domain ? { [domain]: catalog[domain] } : catalog;
4244
4373
  if (jsonMode) { log(JSON.stringify({ ok: true, lenses: picked }, null, 2)); return; }
4245
- log(`# leerness lens 분야별 자기질문 품질 렌즈 (1.18.3)`);
4374
+ const hasCustom = Object.values(catalog).some(l => l && (l._custom || l._customAdded));
4375
+ log(`# leerness lens — 분야별 자기질문 품질 렌즈 (v${VERSION})${hasCustom ? ' + 프로젝트 커스텀(.harness/quality-lenses.json)' : ''}`);
4246
4376
  log(`완료 선언 전 해당 분야 질문에 스스로 답해보세요. "그렇다(통과)"라고 답할 수 없으면 아직 완료가 아닙니다.`);
4247
4377
  for (const [key, l] of Object.entries(picked)) {
4248
4378
  log('');
4249
- log(`## ${key} (${l.title}) — 페르소나: ${l.persona}`);
4379
+ log(`## ${key} (${l.title}) — 페르소나: ${l.persona}${l._custom ? ' [프로젝트]' : (l._customAdded ? ' [+프로젝트 질문]' : '')}`);
4250
4380
  l.questions.forEach((q, i) => log(` ${i + 1}. ${q}`));
4251
- log(` ↔ 인과: ${key} 를 바꾸면 → ${l.affects.join(', ')} 질문도 다시 — ${l.affectsNote}`);
4381
+ log(` ↔ 인과: ${key} 를 바꾸면 → ${(l.affects || []).join(', ') || '(없음)'} 질문도 다시 — ${l.affectsNote}`);
4252
4382
  }
4253
4383
  log('');
4254
- log(`사용: leerness lens <${Object.keys(LENS_CATALOG).join('|')}> · 완료 검증과 함께: leerness verify-claim T-XXXX`);
4384
+ log(`사용: leerness lens <${Object.keys(catalog).join('|')}> · 완료 검증과 함께: leerness verify-claim T-XXXX`);
4385
+ if (!hasCustom) log(`프로젝트 커스텀 렌즈: .harness/quality-lenses.json 에 { "domains": { "code": { "questions": ["..."] } } } 추가`);
4255
4386
  }
4256
4387
 
4257
4388
  function commandsCmd(root) {
@@ -8382,7 +8513,9 @@ function handoff(root) {
8382
8513
  if (parts.length) {
8383
8514
  const isTty = process.stdout && process.stdout.isTTY;
8384
8515
  const cy = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
8385
- log(cy(`📊 헤드라인 (1.9.81/93/113/152/162/192/197/204/207/209/215/220/223/226): ${parts.join(' · ')}`));
8516
+ // 1.20.2 (UR-0010 Phase 1): 헤드라인 라벨 UI 언어 적용 (영어 버전태그 노이즈 제거; 항목 라벨 영어화는 Phase 2).
8517
+ const _hl = _uiLang(root) === 'en' ? '📊 Headline' : '📊 헤드라인 (1.9.81/93/113/152/162/192/197/204/207/209/215/220/223/226)';
8518
+ log(cy(`${_hl}: ${parts.join(' · ')}`));
8386
8519
  }
8387
8520
  } catch {}
8388
8521
  }
@@ -9807,7 +9940,19 @@ function _vcImplIsEmpty(body) {
9807
9940
  }).filter(Boolean);
9808
9941
  if (codeLines.length === 0) return true; // ① 코드 0줄
9809
9942
  const joined = codeLines.join(' ').replace(/\s+/g, ' ').trim();
9810
- return _VC_EMPTY_SHELL_RE.test(joined); // ② 빈 export 껍데기뿐
9943
+ if (_VC_EMPTY_SHELL_RE.test(joined)) return true; // ② 빈 export 껍데기뿐
9944
+ // ③ 1.20.1 (1.20.0 외부평가 재현): 장식된 no-op 우회 — 디렉티브 프롤로그 + 빈 export + no-op 문만 남으면 스텁.
9945
+ // "use strict"; module.exports={} · {};//c · module.exports={};void 0; · ;;;; · 0; (정적모드 우회) 폐쇄. FP 0: 실코드는 식별자/키워드가 남음.
9946
+ // (let x=1 같은 무의미 선언은 식별자가 남아 통과 — AST 토큰 카운트 필요, 백로그.)
9947
+ const residue = joined
9948
+ .replace(/^\s*(['"])use strict\1\s*;?/i, '') // 디렉티브 프롤로그
9949
+ .replace(/(?:module\.)?exports(?:\.[A-Za-z0-9_$]+)?\s*=\s*(?:\{\s*\}|\[\s*\])\s*;?/g, '') // 빈 export
9950
+ .replace(/export\s+default\s*(?:\{\s*\}|\[\s*\])\s*;?/g, '')
9951
+ .replace(/export\s*\{\s*\}\s*;?/g, '')
9952
+ .replace(/\bvoid\s+0\b/g, '') // void 0
9953
+ .replace(/\b(?:null|undefined|false|true|0)\b/g, '') // 단독 no-op 리터럴
9954
+ .replace(/[\s;{}()[\],.]/g, ''); // 공백/구두점
9955
+ return residue === ''; // 의미 토큰이 하나도 안 남으면 스텁
9811
9956
  }
9812
9957
 
9813
9958
  function verifyClaimCmd(root, taskId) {
@@ -10141,6 +10286,19 @@ function verifyClaimCmd(root, taskId) {
10141
10286
  if (claimsChecked || mustHaveEvidence) {
10142
10287
  log('');
10143
10288
  log(` ℹ 한계: 테스트 통과는 "의미적 구현 정확성"을 보장하지 않음 — evidence 가 해당 주장(수정 파일/테스트)을 직접 링크해야 신뢰도↑.`);
10289
+ // 1.19.2 (UR-0003 렌즈 완전판 v2): 완료-검증 순간에 분야별 자기질문 advisory — 주장 파일 확장자 기반(결정적).
10290
+ // 기계검증(파일/테스트/스텁)을 통과해도 "사람이 보기에 좋은가"는 별개 → AI 가 스스로 답하도록 권장(advisory, 게이트 아님).
10291
+ const _lensDoms = _lensDomainsForFiles(files);
10292
+ if (_lensDoms.length) {
10293
+ const _lensCat = _effectiveLensCatalog(root); // 1.19.3: 프로젝트 커스텀 질문도 포함
10294
+ log('');
10295
+ log(` 🧭 품질 렌즈 (완료 선언 전 자문 — advisory, 게이트 아님):`);
10296
+ for (const d of _lensDoms) {
10297
+ const l = _lensCat[d];
10298
+ if (l) log(` · ${d}(${l.title}): ${l.questions[0]}`);
10299
+ }
10300
+ log(` → 전체 질문: leerness lens ${_lensDoms[0]}`);
10301
+ }
10144
10302
  }
10145
10303
  if (overallFail) {
10146
10304
  log('');
@@ -11513,43 +11671,46 @@ function _banner(opts = {}) {
11513
11671
  cprint(' ' + C.green(padded) + C.dim('# ' + desc));
11514
11672
  };
11515
11673
 
11674
+ // 1.20.2 (UR-0010 Phase 1): 첫 화면 배너 — UI 언어(한국어 우선, 영어 opt-in) 적용.
11675
+ const L = _uiLang(arg('--path', process.cwd()));
11676
+ const t = (ko, en) => (L === 'en' ? en : ko);
11516
11677
  cprint('');
11517
- cprint(C.bold(C.cyan(' ✨ 시작하기 (3단계면 끝)')));
11518
- cmd('npx leerness init .', '1️⃣ 하네스 설치 + AI 도구 자동 연결');
11519
- cmd('npx leerness handoff .', '2️⃣ 세션 시작 — 컨텍스트·기억·feature impact 자동 회수');
11520
- cmd('npx leerness session close .', '3️⃣ 세션 종료 — 마감 통계 + 다음 라운드 추천');
11521
-
11522
- section('🧠 메모리 5종 CRUD (1.9.142 — cascade 방지)');
11523
- cmd('leerness task add "<제목>"', 'progress-tracker 등록');
11524
- cmd('leerness decision add "<제목>" --reason "..."', '되돌리기 어려운 결정 영구화');
11525
- cmd('leerness lesson save "<교훈>" --tag "..."', '재발견 가능한 통찰 저장');
11526
- cmd('leerness plan add "<milestone>"', '계획 단계 등록');
11527
- cmd('leerness rule add "<룰>" --trigger every-X', '자연어 영구 룰');
11528
- cmd('leerness feature add "<기능>" --files "..."', 'Feature Graph 노드 (1.9.141)');
11529
-
11530
- section('🔗 인과관계 + 영향 추적 (1.9.141~143)');
11531
- cmd('leerness feature impact <F-XXXX>', '코드 변경 전 영향받는 feature 자동 회수');
11532
- cmd('leerness feature list --json', '전체 그래프 + 엣지');
11533
- cmd('leerness audit . --json', 'orphan/cycle 무결성 검증');
11534
-
11535
- section('🛡 보안·드리프트·게으름 가드');
11536
- cmd('leerness drift check . --auto-fix', 'drift + 보안 자동 회복');
11537
- cmd('leerness lazy detect . --json', '거짓 완료/no test run 감지');
11538
- cmd('leerness env sync .', '.env ↔ .env.example 동기화');
11539
- cmd('leerness health . --json', '종합 헬스 (drift+보안+skill+feature)');
11540
-
11541
- section(`🤖 외부 AI 통합 (MCP ${_mcpToolCount()} 도구)`); // 1.9.315 (UR-0054): 하드코딩 46 → 동적
11678
+ cprint(C.bold(C.cyan(t(' ✨ 시작하기 (3단계면 끝)', ' ✨ Get started (3 steps)'))));
11679
+ cmd('npx leerness init .', t('1️⃣ 하네스 설치 + AI 도구 자동 연결', '1️⃣ install the harness + auto-wire AI tools'));
11680
+ cmd('npx leerness handoff .', t('2️⃣ 세션 시작 — 컨텍스트·기억·feature impact 자동 회수', '2️⃣ start a session — context, memory, feature impact in one call'));
11681
+ cmd('npx leerness session close .', t('3️⃣ 세션 종료 — 마감 통계 + 다음 라운드 추천', '3️⃣ end a session — closing stats + next-round suggestion'));
11682
+
11683
+ section(t('🧠 메모리 5종 CRUD (1.9.142 — cascade 방지)', '🧠 Memory (5 surfaces)'));
11684
+ cmd('leerness task add "<title>"', t('progress-tracker 등록', 'add a task to progress-tracker'));
11685
+ cmd('leerness decision add "<title>" --reason "..."', t('되돌리기 어려운 결정 영구화', 'persist a hard-to-reverse decision'));
11686
+ cmd('leerness lesson save "<lesson>" --tag "..."', t('재발견 가능한 통찰 저장', 'save a rediscoverable insight'));
11687
+ cmd('leerness plan add "<milestone>"', t('계획 단계 등록', 'add a plan milestone'));
11688
+ cmd('leerness rule add "<rule>" --trigger every-X', t('자연어 영구 룰', 'natural-language standing rule'));
11689
+ cmd('leerness feature add "<feature>" --files "..."', t('Feature Graph 노드 (1.9.141)', 'Feature Graph node'));
11690
+
11691
+ section(t('🔗 인과관계 + 영향 추적 (1.9.141~143)', '🔗 Causality + impact tracking'));
11692
+ cmd('leerness feature impact <F-XXXX>', t('코드 변경 전 영향받는 feature 자동 회수', 'which features a code change affects'));
11693
+ cmd('leerness feature list --json', t('전체 그래프 + 엣지', 'full graph + edges'));
11694
+ cmd('leerness audit . --json', t('orphan/cycle 무결성 검증', 'orphan/cycle integrity check'));
11695
+
11696
+ section(t('🛡 보안·드리프트·게으름 가드', '🛡 Security · drift · laziness guards'));
11697
+ cmd('leerness drift check . --auto-fix', t('drift + 보안 자동 회복', 'drift + security auto-heal'));
11698
+ cmd('leerness lazy detect . --json', t('거짓 완료/no test run 감지', 'detect false-done / no-test-run'));
11699
+ cmd('leerness env sync .', t('.env ↔ .env.example 동기화', 'sync .env <-> .env.example'));
11700
+ cmd('leerness health . --json', t('종합 헬스 (drift+보안+skill+feature)', 'overall health (drift+security+skill+feature)'));
11701
+
11702
+ section(t(`🤖 외부 AI 통합 (MCP ${_mcpToolCount()} 도구)`, `🤖 External AI integration (MCP, ${_mcpToolCount()} tools)`)); // 1.9.315 (UR-0054): 하드코딩 46 → 동적
11542
11703
  cmd('npx leerness mcp serve', 'stdio JSON-RPC server');
11543
- cmd('leerness memory status . --json', '5 surface + featureGraph 한 호출');
11544
- cmd('leerness memory archive list --query "kw"', 'DELETE 5종 archive 검색');
11545
- cmd('leerness memory restore <surface> <target>', 'archive → active 복원');
11704
+ cmd('leerness memory status . --json', t('5 surface + featureGraph 한 호출', '5 surfaces + featureGraph in one call'));
11705
+ cmd('leerness memory archive list --query "kw"', t('DELETE 5종 archive 검색', 'search the archive of deleted items'));
11706
+ cmd('leerness memory restore <surface> <target>', t('archive → active 복원', 'restore archive -> active'));
11546
11707
 
11547
- section('🚀 Release 자동화');
11548
- cmd('leerness release pack --close --auto-main-push', '한 줄 release (1.9.140 main push 통합)');
11549
- cmd('leerness release sync-main .', 'release branch → main 자동 fast-forward');
11708
+ section(t('🚀 Release 자동화', '🚀 Release automation'));
11709
+ cmd('leerness release pack --close --auto-main-push', t('한 줄 release (1.9.140 main push 통합)', 'one-line release (with main push)'));
11710
+ cmd('leerness release sync-main .', t('release branch → main 자동 fast-forward', 'release branch -> main fast-forward'));
11550
11711
 
11551
11712
  cprint('');
11552
- cprint(C.dim(' 📚 자세히: `leerness --help` · 자율 모드: `<<autonomous-loop-dynamic>>` 신호로 진행'));
11713
+ cprint(C.dim(t(' 📚 자세히: `leerness --help` · 자율 모드: `<<autonomous-loop-dynamic>>` 신호로 진행', ' 📚 More: `leerness --help`')));
11553
11714
  cprint('');
11554
11715
  }
11555
11716
  }
@@ -19924,9 +20085,9 @@ module.exports = {
19924
20085
  // 1.9.289: shell-safe 인용 (Codex #3) — 단위 테스트
19925
20086
  _shellQuoteArg,
19926
20087
  // 1.18.1: 명령 실행 권한 결정 (재실증 신규 P1: --test-cmd 비-JS 인터프리터 거짓차단) — 단위 테스트
19927
- _isCommandPermitted, RUN_CORE_ALLOW,
20088
+ _isCommandPermitted, RUN_CORE_ALLOW, _uiLang, _tx,
19928
20089
  // 1.18.2: verify-claim 위장 스텁(빈 export 껍데기) 판정 — 단위 테스트
19929
20090
  _vcImplIsEmpty, _VC_EMPTY_SHELL_RE,
19930
- // 1.18.3 (UR-0003): 분야별 자기질문 품질 렌즈 — 단위 테스트
19931
- LENS_CATALOG, lensCmd
20091
+ // 1.18.3 (UR-0003): 분야별 자기질문 품질 렌즈 — 단위 테스트. 1.19.2: 파일→도메인 매핑(완료-검증 advisory)
20092
+ LENS_CATALOG, lensCmd, _lensDomainsForFiles, _mergeLensCatalog, _loadProjectLenses, _effectiveLensCatalog
19932
20093
  };
@@ -0,0 +1,67 @@
1
+ # Clean-room evaluations / 클린룸 평가 기록
2
+
3
+ > **What this is, honestly.** These are **AI clean-room evaluations** — independent agents installing the
4
+ > published npm package into fresh temp directories and exercising it from behavior only (no access to the
5
+ > source tree), including adversarial attempts to defeat leerness's own verifier. They are **not**
6
+ > third-party human security audits or peer-reviewed benchmarks. We publish them so the README claim
7
+ > "verified by independent clean-room evaluations" is checkable rather than a marketing line.
8
+ >
9
+ > 정직하게 말하면: 아래는 **AI 클린룸 평가**입니다 — 독립 에이전트가 게시된 npm 패키지를 빈 임시 폴더에
10
+ > 설치해 **소스 접근 없이 행위만으로** 검증하고, leerness 검증기 자체를 무력화하려는 적대 시도까지 포함합니다.
11
+ > 제3자 인간 보안 감사나 동료심사 벤치마크가 **아닙니다**. README 의 "독립 클린룸 평가로 검증" 주장을
12
+ > 확인 가능하게 만들기 위해 공개합니다.
13
+
14
+ ## Methodology / 방법론
15
+
16
+ Every evaluation follows the same shape:
17
+
18
+ ```bash
19
+ W=$(mktemp -d); cd "$W"; npm init -y
20
+ npm i leerness@<version> # the PUBLISHED package, not the working tree
21
+ LB="node node_modules/leerness/bin/leerness.js"
22
+ # ... drive the CLI by behavior, assert on exit codes + output, clean up ...
23
+ ```
24
+
25
+ - **Published-artifact only** — installs from npm, never reads the repo source. This is what an external user gets.
26
+ - **Behavior-asserted** — checks process exit codes and stdout/stderr, not internal state.
27
+ - **Adversarial** — a portion of every pass actively tries to make a *false* "done" claim pass verification
28
+ (comment-only stubs, empty-export shells, `assert(true)` fake tests, inflated test counts, language tricks).
29
+ - **Both directions (맹신 X / "trust nothing")** — confirmed real defects AND rejected false alarms; a finding
30
+ counts only if reproduced on the published package.
31
+
32
+ ## Evaluations run / 수행한 평가
33
+
34
+ | # | Round | Scope | Outcome |
35
+ |---|---|---|---|
36
+ | 1 | Universal-harness field study (5-axis) | Real Python / Node / Rust development, agent-swap handoff, adversarial attack on the verifier | Verdict "conditionally yes"; found 5 gaps (2×P1, 3×P2) — all closed in 1.17.2–1.17.6, shipped as the 1.18.0 stable minor |
37
+ | 2 | 1.18.0 re-verification (published) | Independently re-ran the 5 gaps against the **published** 1.18.0 | 4 closed; surfaced a new P1 (Windows `--test-cmd python` blocked → false FAIL) → fixed in 1.18.1 |
38
+ | 3 | 1.18.2 adversarial stub workflow | Bypass-hunters + false-positive-hunters vs the empty-shell stub detector | 9 one-keyword bypasses found (Object.freeze, async fn, inline comment, …) and closed; **0 false positives** across ~45 legitimate patterns |
39
+ | 4 | 1.19.0 published-artifact re-verification | Installed `leerness@1.19.0` from npm; smoke-tested headline fixes | `--test-cmd python`, empty-shell rejection, `lens`, selftest 231 — all pass on the published package |
40
+
41
+ ## What these evaluations do NOT establish / 한계 (정직)
42
+
43
+ leerness's verification is **heuristic, not semantic**. These evaluations show it reliably catches *obvious
44
+ false-done claims* (missing files, empty shells, fake/unlinked tests, inflated counts) without false-failing
45
+ honest work. They do **not** prove:
46
+
47
+ - the implementation satisfies the actual requirement, or business logic is correct;
48
+ - tests are sufficient or well-designed;
49
+ - absence of security vulnerabilities or production-runtime correctness.
50
+
51
+ Known heuristic gaps tracked for an AST/token-based redesign: multi-arg call-expression empty objects
52
+ (`Object.assign({}, {})`), Python `def …: pass` / `...` / `raise NotImplementedError`, multi-language empty
53
+ bodies. **Code coverage / mutation testing and third-party reproduction remain open** (the e2e is a
54
+ spawn-based runner, which standard coverage tools do not instrument cleanly).
55
+
56
+ ## Reproduce it yourself / 직접 재현
57
+
58
+ ```bash
59
+ W=$(mktemp -d); cd "$W"; npm init -y && npm i leerness@latest
60
+ LB="node node_modules/leerness/bin/leerness.js"
61
+ $LB init "$W" --yes
62
+ # fake "done": claim an implementation that does not exist
63
+ $LB task add "Implement X"
64
+ $LB task update T-0001 --status done --evidence "x.js done, 5 tests passed"
65
+ $LB verify-claim T-0001 # exit 1 — claim rejected (x.js missing, no tests ran)
66
+ $LB selftest # core-function integrity self-check
67
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.19.0",
3
+ "version": "1.21.0",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",