leerness 1.20.0 → 1.22.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,107 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.22.0 — 2026-06-15 — 🛡️ [안정화/Stable] 확장된 영어 지원(헤드라인 + verify-claim) 안정 minor
4
+
5
+ **🛡️ 안정화(Stable) minor — 영어 CLI 커버리지를 핵심 검증 흐름까지 확장해 npm 공개.** 직전 minor(1.21.0) 이후 누적된 패치 2건(1.21.1~1.21.2)을 검증·통합해 배포. R-0011 정책의 13번째 stable minor. 영어 opt-in 표면이 첫 화면(1.21.0)에서 **handoff 헤드라인 + verify-claim 플래그십**까지 확장 — 한국어 우선 기본은 그대로.
6
+
7
+ ### 이번 minor 통합 (1.21.1~1.21.2)
8
+ - **🌐 handoff 헤드라인 항목 라벨 영어화 (Phase 2)**: 매 세션 가장 많이 보는 출력 — `--language en` 시 `📊 Headline: drift healthy · security OK · N skills · health · mem …` 완전 영어. 보안/시크릿/미답요청/비정상종료/플랫폼제약 등 전 항목.
9
+ - **🌐 verify-claim 출력 영어화 (Phase 3, 플래그십)**: 완료 검증 시 보는 핵심 출력 ~30 문자열 영어화 — File check / Test count / Summary / implementation substance / test-impl link / git diff cross-check / optimism+honesty / final verdict.
10
+ - **한국어 우선 기본 보존**: 영어는 명시 opt-in(`--language en`/`LEERNESS_LANG=en`/en init 프로젝트). 플래그 없으면 한국어 — e2e 무회귀로 검증.
11
+
12
+ ### 잔여 (UR-0010 Phase 4+, 백로그)
13
+ - session close 마감 보고(~117 문자열) + help + status — 단계적 확대.
14
+
15
+ ### 검증 (회귀 0)
16
+ - **selftest 236→238** · **E2E 365/365** · en/ko 헤드라인·verify-claim 행위 + 게시본 재실증.
17
+
18
+ ### 안정화 표시 (R-0006)
19
+ CHANGELOG [안정화/Stable] · git tag (Stable) · GitHub release (`--latest`) · npm dist-tag `stable` 시도.
20
+
21
+ ## 1.21.2 — 2026-06-15 — CLI 영어화 Phase 3: verify-claim(플래그십) 출력 영어화 (UR-0010)
22
+
23
+ **🌐 플래그십 명령 출력을 영어로.** 사용자가 "완료됐나?"를 확인할 때 보는 `verify-claim` 의 human 출력 전체를 영어 opt-in 으로 — 온보딩 다음으로 영향 큰 표면. 한국어 기본 유지(영어 명시 opt-in).
24
+
25
+ ### 변경 (UR-0010 Phase 3)
26
+ - **verify-claim 출력 영어화**: human 렌더(–json 제외) 시작에서 `_uiLang(root)` 1회 해석 후 전 출력을 `t(ko,en)` 분기 — `📂 파일 검증`→`File check`, `🧪 테스트 카운트`→`Test count`, `🚦 실행`→`run`, `종합`→`Summary`, `파일 모두 존재`→`all files exist`, `구현 실체`→`implementation substance`, `테스트-구현 연결`→`test-impl link`, `git diff 교차검증`→`git diff cross-check`, `낙관적 표시+정직성`→`optimism+honesty`, evidence 완전성/외과적 변경/한계/최종 판정까지 ~30 문자열. 품질 렌즈 advisory 헤더도 영어.
27
+ - **한국어 기본 무회귀**: e2e 는 `--language` 없이 실행되며 init 이 이 환경에서 manifest.language=`ko` 로 저장(검증함) → verify-claim 기본 출력 한국어 유지. ko 원문은 `t()` 의 ko 인자로 소스에 보존.
28
+
29
+ ### 잔여 (UR-0010 Phase 4+, 백로그)
30
+ - session close 마감 보고 + help + status + 그 외 명령 본문 영어화 — 단계적 확대.
31
+
32
+ ### 검증 (회귀 0)
33
+ - **selftest 237→238** (verify-claim t() 경유 + 한국어 보존 소스가드) · 행위(en: `## Summary / all files exist / evidence claim matches`, ko 기본: `## 종합 / 파일 모두 존재`) · **E2E 365/365**.
34
+ - patch(1.21.2) — npm 미배포(R-0011, GitHub/CHANGELOG 누적).
35
+
36
+ ## 1.21.1 — 2026-06-15 — CLI 영어화 Phase 2: handoff 헤드라인 항목 라벨 영어화 (UR-0010)
37
+
38
+ **🌐 매 세션 가장 많이 보는 출력(handoff 헤드라인)을 영어로 완성.** Phase 1(배너 + 헤드라인 프리픽스)에 이어, 헤드라인의 **항목 라벨 전체**를 영어 opt-in 으로. 비한국어 사용자가 세션마다 보던 한국어 토막들(보안 OK·미답 요청·비정상종료·플랫폼 제약 등)이 사라짐.
39
+
40
+ ### 변경 (UR-0010 Phase 2)
41
+ - **handoff 헤드라인 항목 라벨 영어화**: 블록 1회 `_uiLang(root)` 해석 후 각 항목을 `t(ko,en)` 분기 — `🔒 보안 OK`→`security OK`, `🚨 시크릿 N건`→`N secret(s)`, `🚨 .env 미무시`→`.env not ignored`, `🔌 MCP N회`→`MCP Nx`, `📒 skill query N회`, `🪄 slash 24h N회`, `📥 미답 요청 N건`→`N unanswered request(s)`, `📥 요청 N (tracked)`, `📥 자동완료가능 N건`→`N auto-completable`, `🔌 비정상종료`→`abnormal-exit`, `🚦 N 플랫폼 제약`→`N platform constraint(s)`, `R N남음`→`N left`. **한국어 기본 유지**(영어 opt-in).
42
+ - 행위 확인: en → `📊 Headline: drift healthy · 🔒 security OK · 📚 N skills · ⚕ health: ✓ · 🧠 mem …` 완전 영어 / 플래그 없음 → `📊 헤드라인`(한국어).
43
+
44
+ ### 잔여 (UR-0010 Phase 3+, 백로그)
45
+ - session close 마감 보고 + verify-claim 출력 + help + status 등 명령 본문 영어화 — 단계적 확대.
46
+
47
+ ### 검증 (회귀 0)
48
+ - **selftest 236→237** (헤드라인 t() 경유 소스가드) · 행위(en 헤드라인 완전 영어 + ko 기본) · **E2E 365/365**.
49
+ - patch(1.21.1) — npm 미배포(R-0011, GitHub/CHANGELOG 누적).
50
+
51
+ ## 1.21.0 — 2026-06-15 — 🛡️ [안정화/Stable] 영어 온보딩 첫걸음 + 검증 강화 안정 minor
52
+
53
+ **🛡️ 안정화(Stable) minor — 비한국어 사용자에게 영어 온보딩을 실제로 전달.** 직전 minor(1.20.0) 이후 누적된 패치 2건(1.20.1~1.20.2)을 검증·통합해 npm 공개. R-0011 정책의 12번째 stable minor. 핵심: **영어 첫 화면(opt-in)** 이 npm 사용자에게 닿고, 검증 플래그십의 정적 우회가 닫힘.
54
+
55
+ ### 이번 minor 통합 (1.20.1~1.20.2)
56
+ - **🌐 CLI 영어화 Phase 1 (UR-0010, 사용자 지정 방향)**: 영어 README 정문 ↔ 한국어 CLI 불일치(외부평가 #1 병목) 해소 시작. `--language en` / `LEERNESS_LANG=en` / en 으로 init 된 프로젝트에서 **init 시작하기 배너 전체 + handoff 헤드라인 라벨**이 영어로. 한국어 우선 정체성은 **기본값 유지**(영어는 명시 opt-in, system locale 미사용). 순수 `_uiLang`/`_tx`.
57
+ - **🛡️ 장식 no-op 정적 우회 차단 (검증)**: `"use strict"; module.exports={}` · `{}; //c` · `void 0;` · `;;;;` · `0;` 같은 "실로직 0" 파일이 정적 verify-claim 을 통과하던 우회를 차감식 residue 검사로 폐쇄(FP 0).
58
+ - **📄 README 플래그십 데모 정합**: task-id(T-0002) + "실파일 쓰면 같은 명령 통과" 정정 — 신규 유저 복붙이 실제로 동작. lens 헤더 동적 버전.
59
+ - **🔬 신뢰 투명성/렌즈 완전판** (1.19.x 누적 — 1.20.0 에 포함됨, 본 minor 는 1.20.x 패치 묶음).
60
+
61
+ ### 잔여 (UR-0010 Phase 2+, 백로그)
62
+ - handoff 헤드라인 **항목 라벨**(drift·보안·skills·health·mem 등 ~20종) + session close·verify-claim·help·status 출력 영어화 — 단계적 확대.
63
+
64
+ ### 검증 (회귀 0)
65
+ - **selftest 235→236** · **E2E 365/365** · 영어 배너/헤드라인 행위 + no-op 우회 차단 + 게시본 재실증.
66
+
67
+ ### 안정화 표시 (R-0006)
68
+ CHANGELOG [안정화/Stable] · git tag (Stable) · GitHub release (`--latest`) · npm dist-tag `stable` 시도.
69
+
70
+ ## 1.20.2 — 2026-06-15 — CLI 영어화 Phase 1: 첫 화면(init 배너·handoff 헤드라인) 영어 opt-in (UR-0010)
71
+
72
+ **🌐 "어떤 언어, 어떤 AI 에이전트로 작업하든"의 실질 첫걸음.** 외부평가가 꼽은 단일 최대 병목 — 영어 README 정문 ↔ 한국어 CLI 불일치 — 를 단계적으로 해소 시작. 한국어 우선 정체성은 기본값으로 유지하고, **영어는 명시 opt-in**.
73
+
74
+ ### 변경 (UR-0010 Phase 1)
75
+ - **UI 언어 해석 `_uiLang(root)`**: 우선순위 `--language` 플래그 > `LEERNESS_LANG` env > `.harness/manifest.json` 의 language(init 선택) > **`ko`(기본)**. system locale 은 의도적으로 미사용(영어 OS 한국 사용자 놀람 방지) — 영어는 항상 명시 opt-in.
76
+ - **init 시작하기 배너 영어화**: `--language en`(또는 LEERNESS_LANG=en, 또는 en 으로 init 된 프로젝트)에서 "Get started (3 steps)" + 메모리/인과/보안/MCP/release 섹션 전체 영어로. 비한국어 신규 유저의 첫 화면이 더 이상 한국어 벽이 아님.
77
+ - **handoff 헤드라인 라벨 영어화**: `📊 헤드라인 (버전태그…)` → 영어 시 `📊 Headline`(버전태그 노이즈 제거).
78
+ - 순수 함수 `_uiLang`/`_tx` 분리(단위 테스트). **한국어 기본 회귀 0**(플래그/env/manifest 없으면 그대로 한국어).
79
+
80
+ ### 잔여 (UR-0010 Phase 2+, 백로그)
81
+ - handoff 헤드라인 **항목 라벨**(보안 OK·skills·health…) 영어화, 그 외 명령(session close·verify-claim 출력·help·status 등) 영어화 — 단계적 확대. Phase 1 은 첫인상 표면(배너+헤드라인 프리픽스)에 한정.
82
+
83
+ ### 검증 (회귀 0)
84
+ - **selftest 235→236** (`_uiLang` 해석 4종 + `_tx` + 첫화면 분기 소스가드) · 행위: `--language en` → 영어 배너/헤드라인, 플래그 없음 → 한국어 유지 · **E2E 365/365**.
85
+ - patch(1.20.2) — npm 미배포(R-0011, GitHub/CHANGELOG 누적).
86
+
87
+ ## 1.20.1 — 2026-06-15 — 1.20.0 외부평가 채택: 장식 no-op 우회 차단 + README 데모 정합
88
+
89
+ **🔎 게시본 1.20.0 신규 멀티모델 클린룸 평가(5축) 채택 — 맹신 X로 재현된 것만.** 검증(8)·정직성/보안(9)·품질렌즈(8)는 GPT-5.5 대비 뚜렷이 상승했고, 평가가 찾은 재현 가능한 P2 두 가지(검증 정적모드 우회 + README 데모 부정확)를 닫음.
90
+
91
+ ### 변경 (1.20.0 외부평가 재현 채택)
92
+ - **🛡️ 장식된 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 토큰화 필요(백로그).
93
+ - **📄 README 플래그십 데모 정합 (온보딩, P2)**: ① task-id 수정 — init 이 T-0001(계획 task)을 차지하므로 `task add` 는 T-0002 생성, 데모를 실제 흐름(출력된 id 사용)으로. ② "실파일+실테스트 쓰면 같은 명령 통과" 정정 — evidence 의 테스트 개수 주장은 실측과 대조되므로(거짓이면 거부) 정직한 evidence + `--run-tests --test-cmd` 안내로 교체. 신규 유저 복붙이 실제로 동작.
94
+ - **🔖 lens 헤더 버전 정합 (P3)**: 하드코딩 `(1.18.3)` → 동적 `(v${VERSION})`.
95
+
96
+ ### 백로그 등록 (평가 추천 우선순위, 이번 라운드 미포함)
97
+ - **UR-0010 (최우선)**: CLI UX 영어화 — `--language en` 이 템플릿 파일만 바꾸고 CLI 자체 출력(init 배너·handoff 헤드라인·help)은 한국어 유지. 영어 README 정문 ↔ 한국어 CLI 불일치가 비한국어 시장의 단일 최대 병목(거대 작업, UR-0042 잔여 통합).
98
+ - **UR-0011**: 표면 core-vs-extended 티어링 — 84커맨드+85툴 평면 노출, banner/about/commands/help 의 "start here" 4종 불일치 → 핵심8 표준화 + `--help` 정리.
99
+ - **UR-0012**: secret scan AWS AKIA 키 ID + 추가 prefix 커버리지.
100
+
101
+ ### 검증 (회귀 0)
102
+ - **selftest 235** (no-op 우회 6종 차단 + FP 가드 3종 행위) · 5종 우회 재현 차단·FP 0 확인 · README 데모 재현(task-id·count 정합) · **E2E 365/365**.
103
+ - patch(1.20.1) — npm 미배포(R-0011, GitHub/CHANGELOG 누적).
104
+
3
105
  ## 1.20.0 — 2026-06-15 — 🛡️ [안정화/Stable] 품질 렌즈 완전판 + 신뢰 투명성 안정 minor
4
106
 
5
107
  **🛡️ 안정화(Stable) minor — "AI가 스스로 질문하고 행동하도록" 완성.** 직전 minor(1.19.0) 이후 누적된 패치 3건(1.19.1~1.19.3)을 검증·통합해 npm 공개. R-0011 정책의 11번째 stable minor. 핵심 테마: **사용자 자기질문 품질 렌즈를 정적 명령에서 완전한 워크플로 기능으로** + 신뢰 투명성(클린룸 평가 공개).
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
 
@@ -102,7 +104,7 @@ MIT
102
104
  <!-- leerness:project-readme:start -->
103
105
  ## Leerness Project Harness
104
106
 
105
- 이 프로젝트는 Leerness v1.20.0 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
107
+ 이 프로젝트는 Leerness v1.22.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.20.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
161
+ Leerness v1.22.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.20.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.20.0)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
182
+ 현재 누적: **70 라운드 (1.9.40 → 1.22.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.20.0: 2026-06-15
220
+ Last synced by Leerness v1.22.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.20.0';
35
+ const VERSION = '1.22.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
  }
@@ -3594,7 +3610,7 @@ function _selfTestCases() {
3594
3610
  const src = read(__filename);
3595
3611
  const authPass = src.includes('userAuthorized: true, timeout: 5 * 60 * 1000, kind: ' + "'verify_claim_test'");
3596
3612
  const skipOnBlock = src.includes('if (r.blocked) {') && src.includes('테스트 명령 차단') && src.includes('불일치 판정 아님');
3597
- const label = src.includes('` - ${runResult.cmd ' + "|| 'npm test'} 실행:"); // P3: 하드코딩된 npm test 라벨 제거
3613
+ const label = src.includes('` - ${runResult.cmd ' + "|| 'npm test'} ${t('실행', 'run')}:"); // P3: 하드코딩 제거 + 1.21.2 영어화 t()
3598
3614
  return authPass && skipOnBlock && label;
3599
3615
  } },
3600
3616
  { name: '재실증 P2 (1.18.1): task update id 뒤 non-path positional(status) 거부 + path-like 허용 (소스 가드)', run: () => {
@@ -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);
@@ -3691,6 +3718,37 @@ function _selfTestCases() {
3691
3718
  const d = read(dp);
3692
3719
  return /AI clean-room evaluations/i.test(d) && /heuristic, not semantic/i.test(d) && /npm i leerness@/.test(d);
3693
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
+ } },
3740
+ { name: 'CLI 영어화 Phase 2 (1.21.1, UR-0010): handoff 헤드라인 항목 라벨 t() 경유 (소스 가드)', run: () => {
3741
+ const src = read(__filename);
3742
+ return src.includes('const _L = _uiLang(root); const t = (ko, en) => (_L === ' + "'en' ? en : ko);")
3743
+ && src.includes("security OK") && src.includes("unanswered request(s)") && src.includes("abnormal-exit") && src.includes("platform constraint(s)");
3744
+ } },
3745
+ { name: 'CLI 영어화 Phase 3 (1.21.2, UR-0010): verify-claim 출력 t() 경유 + 한국어 기본 보존 (소스 가드)', run: () => {
3746
+ const src = read(__filename);
3747
+ const en = src.includes('## File check') && src.includes('## Test count') && src.includes('## Summary')
3748
+ && src.includes('implementation substance (done default)') && src.includes('evidence claim matches actual files');
3749
+ const koPreserved = src.includes('## 종합') && src.includes('구현 실체 (done 기본)'); // ko 원문이 t() ko 인자로 남아 e2e(ko) 무회귀
3750
+ return en && koPreserved;
3751
+ } },
3694
3752
  { name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
3695
3753
  ];
3696
3754
  }
@@ -4326,7 +4384,7 @@ function lensCmd(domain, opts = {}) {
4326
4384
  const picked = domain ? { [domain]: catalog[domain] } : catalog;
4327
4385
  if (jsonMode) { log(JSON.stringify({ ok: true, lenses: picked }, null, 2)); return; }
4328
4386
  const hasCustom = Object.values(catalog).some(l => l && (l._custom || l._customAdded));
4329
- log(`# leerness lens — 분야별 자기질문 품질 렌즈 (1.18.3)${hasCustom ? ' + 프로젝트 커스텀(.harness/quality-lenses.json)' : ''}`);
4387
+ log(`# leerness lens — 분야별 자기질문 품질 렌즈 (v${VERSION})${hasCustom ? ' + 프로젝트 커스텀(.harness/quality-lenses.json)' : ''}`);
4330
4388
  log(`완료 선언 전 해당 분야 질문에 스스로 답해보세요. "그렇다(통과)"라고 답할 수 없으면 아직 완료가 아닙니다.`);
4331
4389
  for (const [key, l] of Object.entries(picked)) {
4332
4390
  log('');
@@ -8232,6 +8290,8 @@ function handoff(root) {
8232
8290
  if (!has('--no-headline') && !has('--compact') && !has('--quiet')) {
8233
8291
  try {
8234
8292
  const parts = [];
8293
+ // 1.20.3 (UR-0010 Phase 2): 헤드라인 항목 라벨 UI 언어 적용 (영어 opt-in, 한국어 기본). 블록 1회 해석.
8294
+ const _L = _uiLang(root); const t = (ko, en) => (_L === 'en' ? en : ko);
8235
8295
  // 1) drift level (가벼운 check)
8236
8296
  try {
8237
8297
  const r = cp.spawnSync(process.execPath, [__filename, 'drift', 'check', root, '--json'],
@@ -8245,15 +8305,15 @@ function handoff(root) {
8245
8305
  try {
8246
8306
  const sec = _collectSecretFindings(root);
8247
8307
  if (sec.committed.length) {
8248
- parts.push(`🚨 시크릿 ${sec.committed.length}건`);
8308
+ parts.push(t(`🚨 시크릿 ${sec.committed.length}건`, `🚨 ${sec.committed.length} secret(s)`));
8249
8309
  } else {
8250
8310
  const envPath = path.join(root, '.env');
8251
8311
  if (exists(envPath)) {
8252
8312
  const giText = exists(path.join(root, '.gitignore')) ? read(path.join(root, '.gitignore')) : '';
8253
8313
  const giLines = giText.split('\n').map(l => l.trim());
8254
- parts.push((giLines.includes('.env') || giLines.includes('/.env')) ? '🔒 보안 OK' : '🚨 .env 미무시');
8314
+ parts.push((giLines.includes('.env') || giLines.includes('/.env')) ? t('🔒 보안 OK', '🔒 security OK') : t('🚨 .env 미무시', '🚨 .env not ignored'));
8255
8315
  } else {
8256
- parts.push('🔒 보안 OK');
8316
+ parts.push(t('🔒 보안 OK', '🔒 security OK'));
8257
8317
  }
8258
8318
  }
8259
8319
  } catch {}
@@ -8261,7 +8321,7 @@ function handoff(root) {
8261
8321
  try {
8262
8322
  const stats = _readUsageStats(root);
8263
8323
  const mcpTotal = stats.mcp?.tools ? Object.values(stats.mcp.tools).reduce((s, n) => s + n, 0) : 0;
8264
- if (mcpTotal > 0) parts.push(`🔌 MCP ${mcpTotal}회`);
8324
+ if (mcpTotal > 0) parts.push(`🔌 MCP ${mcpTotal}${t('회', 'x')}`);
8265
8325
  } catch {}
8266
8326
  // 4) skill match history 누적
8267
8327
  try {
@@ -8269,7 +8329,7 @@ function handoff(root) {
8269
8329
  if (exists(histPath)) {
8270
8330
  const txt = read(histPath);
8271
8331
  const cnt = (txt.match(/^## [\d-]+ [\d:]+ — query/gm) || []).length;
8272
- if (cnt > 0) parts.push(`📒 skill query ${cnt}회`);
8332
+ if (cnt > 0) parts.push(`📒 skill query ${cnt}${t('회', 'x')}`);
8273
8333
  }
8274
8334
  } catch {}
8275
8335
  // 5) 설치된 skill 수
@@ -8330,7 +8390,7 @@ function handoff(root) {
8330
8390
  }
8331
8391
  } catch {}
8332
8392
  }
8333
- if (slashCount > 0) parts.push(`🪄 slash 24h ${slashCount}회`);
8393
+ if (slashCount > 0) parts.push(`🪄 slash 24h ${slashCount}${t('회', 'x')}`);
8334
8394
  }
8335
8395
  } catch {}
8336
8396
  // 10) 1.9.192: 공식 organization skill catalog 캐시 매칭 (C축 보강 — 사용자 명시)
@@ -8358,7 +8418,7 @@ function handoff(root) {
8358
8418
  if (rh.roundCount >= 5) {
8359
8419
  let label = `🔄 R${rh.roundCount}`;
8360
8420
  if (rh.nextMilestone != null && rh.roundsToNextMilestone <= 20) {
8361
- label += ` → R${rh.nextMilestone} (${rh.roundsToNextMilestone}R 남음)`;
8421
+ label += ` → R${rh.nextMilestone} (${rh.roundsToNextMilestone}R ${t('남음', 'left')})`;
8362
8422
  }
8363
8423
  parts.push(label);
8364
8424
  // 1.9.230: 임박 마일스톤 ETA 별도 노출 (다음 마일스톤이 매우 가까울 때만)
@@ -8403,11 +8463,11 @@ function handoff(root) {
8403
8463
  let detected = { candidates: [] };
8404
8464
  try { detected = _detectDeliveredRequests(root); } catch {}
8405
8465
  if (detected.candidates && detected.candidates.length > 0) {
8406
- parts.push(`📥 자동완료가능 ${detected.candidates.length}건 (1.9.223)`);
8466
+ parts.push(t(`📥 자동완료가능 ${detected.candidates.length}건 (1.9.223)`, `📥 ${detected.candidates.length} auto-completable`));
8407
8467
  } else if (audit.missing && audit.missing.length > 0) {
8408
- parts.push(`📥 미답 요청 ${audit.missing.length}건`);
8468
+ parts.push(t(`📥 미답 요청 ${audit.missing.length}건`, `📥 ${audit.missing.length} unanswered request(s)`));
8409
8469
  } else if (audit.open > 0) {
8410
- parts.push(`📥 요청 ${audit.open} (tracked)`);
8470
+ parts.push(t(`📥 요청 ${audit.open} (tracked)`, `📥 ${audit.open} request(s) (tracked)`));
8411
8471
  }
8412
8472
  } catch {}
8413
8473
  // 14) 1.9.209: pre-wake-audit 최근 보고서 (사용자 명시) — 깨어남 직후 자동 노출
@@ -8426,7 +8486,7 @@ function handoff(root) {
8426
8486
  try {
8427
8487
  const ad = _detectAbnormalShutdown(root);
8428
8488
  if (ad.abnormalShutdown) {
8429
- parts.push(`🔌 비정상종료 ${ad.severity} (${ad.signals.length}신호)`);
8489
+ parts.push(t(`🔌 비정상종료 ${ad.severity} (${ad.signals.length}신호)`, `🔌 abnormal-exit ${ad.severity} (${ad.signals.length} signals)`));
8430
8490
  }
8431
8491
  } catch {}
8432
8492
  // 15) 1.9.215: 현재 활성 task에서 constraints/intent 자동 분석 (1.9.208/213 통합)
@@ -8446,7 +8506,7 @@ function handoff(root) {
8446
8506
  try {
8447
8507
  const cc = _checkRequestConstraints(root, req);
8448
8508
  if (cc.matched && cc.matched.length > 0) {
8449
- parts.push(`🚦 ${cc.matched.length} 플랫폼 제약`);
8509
+ parts.push(t(`🚦 ${cc.matched.length} 플랫폼 제약`, `🚦 ${cc.matched.length} platform constraint(s)`));
8450
8510
  }
8451
8511
  } catch {}
8452
8512
  // 1.9.213 intent classify
@@ -8467,7 +8527,9 @@ function handoff(root) {
8467
8527
  if (parts.length) {
8468
8528
  const isTty = process.stdout && process.stdout.isTTY;
8469
8529
  const cy = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
8470
- log(cy(`📊 헤드라인 (1.9.81/93/113/152/162/192/197/204/207/209/215/220/223/226): ${parts.join(' · ')}`));
8530
+ // 1.20.2 (UR-0010 Phase 1): 헤드라인 라벨 UI 언어 적용 (영어 버전태그 노이즈 제거; 항목 라벨 영어화는 Phase 2).
8531
+ const _hl = _uiLang(root) === 'en' ? '📊 Headline' : '📊 헤드라인 (1.9.81/93/113/152/162/192/197/204/207/209/215/220/223/226)';
8532
+ log(cy(`${_hl}: ${parts.join(' · ')}`));
8471
8533
  }
8472
8534
  } catch {}
8473
8535
  }
@@ -9892,7 +9954,19 @@ function _vcImplIsEmpty(body) {
9892
9954
  }).filter(Boolean);
9893
9955
  if (codeLines.length === 0) return true; // ① 코드 0줄
9894
9956
  const joined = codeLines.join(' ').replace(/\s+/g, ' ').trim();
9895
- return _VC_EMPTY_SHELL_RE.test(joined); // ② 빈 export 껍데기뿐
9957
+ if (_VC_EMPTY_SHELL_RE.test(joined)) return true; // ② 빈 export 껍데기뿐
9958
+ // ③ 1.20.1 (1.20.0 외부평가 재현): 장식된 no-op 우회 — 디렉티브 프롤로그 + 빈 export + no-op 문만 남으면 스텁.
9959
+ // "use strict"; module.exports={} · {};//c · module.exports={};void 0; · ;;;; · 0; (정적모드 우회) 폐쇄. FP 0: 실코드는 식별자/키워드가 남음.
9960
+ // (let x=1 같은 무의미 선언은 식별자가 남아 통과 — AST 토큰 카운트 필요, 백로그.)
9961
+ const residue = joined
9962
+ .replace(/^\s*(['"])use strict\1\s*;?/i, '') // 디렉티브 프롤로그
9963
+ .replace(/(?:module\.)?exports(?:\.[A-Za-z0-9_$]+)?\s*=\s*(?:\{\s*\}|\[\s*\])\s*;?/g, '') // 빈 export
9964
+ .replace(/export\s+default\s*(?:\{\s*\}|\[\s*\])\s*;?/g, '')
9965
+ .replace(/export\s*\{\s*\}\s*;?/g, '')
9966
+ .replace(/\bvoid\s+0\b/g, '') // void 0
9967
+ .replace(/\b(?:null|undefined|false|true|0)\b/g, '') // 단독 no-op 리터럴
9968
+ .replace(/[\s;{}()[\],.]/g, ''); // 공백/구두점
9969
+ return residue === ''; // 의미 토큰이 하나도 안 남으면 스텁
9896
9970
  }
9897
9971
 
9898
9972
  function verifyClaimCmd(root, taskId) {
@@ -10135,39 +10209,41 @@ function verifyClaimCmd(root, taskId) {
10135
10209
  return;
10136
10210
  }
10137
10211
 
10212
+ // 1.21.2 (UR-0010 Phase 3): verify-claim 출력 UI 언어 적용 (영어 opt-in, 한국어 기본). human 렌더 한정(--json 은 위에서 return).
10213
+ const _L = _uiLang(root); const t = (ko, en) => (_L === 'en' ? en : ko);
10138
10214
  log(`# verify-claim ${taskId} (${path.basename(root)})`);
10139
10215
  log(`Request: ${row.request}`);
10140
10216
  log(`Status: ${row.status} · Updated: ${row.updated}`);
10141
10217
  log(`Evidence: ${evidence.slice(0, 200)}${evidence.length > 200 ? '…' : ''}`);
10142
10218
  log('');
10143
- log(`## 📂 파일 검증 (${files.length}건 주장)`);
10144
- if (!files.length) log(' (evidence에서 파일 경로를 추출하지 못함)');
10219
+ log(t(`## 📂 파일 검증 (${files.length}건 주장)`, `## 📂 File check (${files.length} claimed)`));
10220
+ if (!files.length) log(t(' (evidence에서 파일 경로를 추출하지 못함)', ' (no file paths extracted from evidence)'));
10145
10221
  else {
10146
- for (const c of fileChecks) log(` ${c.exists ? '✓' : '✗'} ${c.file}${c.exists ? '' : ' ← 누락'}`);
10222
+ for (const c of fileChecks) log(` ${c.exists ? '✓' : '✗'} ${c.file}${c.exists ? '' : t(' ← 누락', ' ← missing')}`);
10147
10223
  }
10148
10224
  log('');
10149
- log(`## 🧪 테스트 카운트`);
10150
- if (declaredPass) log(` 주장 (pass): ${declaredPass.num}/${declaredPass.denom}`);
10151
- if (declaredTestCount) log(` 주장 (개수): ${declaredTestCount}개`);
10152
- if (actualTestCount != null) log(` 실측: ${actualTestCount}개 테스트 호출 (${_vcTests.length ? '주장된 테스트 파일' : '관례 탐색: 루트/tests·test_*.py·*.test.*'})`);
10153
- else log(` 실측: 테스트 파일 못 찾음 — 카운트 검증 미수행 (pass 아님)`);
10225
+ log(t(`## 🧪 테스트 카운트`, `## 🧪 Test count`));
10226
+ if (declaredPass) log(t(` 주장 (pass): ${declaredPass.num}/${declaredPass.denom}`, ` claimed (pass): ${declaredPass.num}/${declaredPass.denom}`));
10227
+ if (declaredTestCount) log(t(` 주장 (개수): ${declaredTestCount}개`, ` claimed (count): ${declaredTestCount}`));
10228
+ if (actualTestCount != null) log(t(` 실측: ${actualTestCount}개 테스트 호출 (${_vcTests.length ? '주장된 테스트 파일' : '관례 탐색: 루트/tests·test_*.py·*.test.*'})`, ` measured: ${actualTestCount} test call(s) (${_vcTests.length ? 'claimed test files' : 'convention scan: root/tests·test_*.py·*.test.*'})`));
10229
+ else log(t(` 실측: 테스트 파일 못 찾음 — 카운트 검증 미수행 (pass 아님)`, ` measured: no test file found — count not verified (not a pass)`));
10154
10230
 
10155
10231
  // 1.9.19: --run-tests 결과
10156
10232
  let runTestsOk = true;
10157
10233
  let declaredPassMatchesActual = true;
10158
10234
  if (runResult) {
10159
10235
  log('');
10160
- log(`## 🚦 ${runResult.cmd || '테스트'} 실행 (--run-tests)`);
10236
+ log(`## 🚦 ${runResult.cmd || t('테스트', 'test')} ${t('실행', 'run')} (--run-tests)`);
10161
10237
  if (runResult.skipped) {
10162
10238
  log(` ⚠ skipped: ${runResult.reason}`);
10163
10239
  } else {
10164
10240
  log(` exit: ${runResult.exitCode}`);
10165
- if (runResult.parsed) log(` 실행 결과: ${runResult.parsed.num}/${runResult.parsed.denom} ${runResult.parsed.num === runResult.parsed.denom ? 'passed' : 'partial'}`);
10166
- else log(` (pass/fail 비율을 stdout에서 파싱 못함)`);
10241
+ if (runResult.parsed) log(t(` 실행 결과: ${runResult.parsed.num}/${runResult.parsed.denom} ${runResult.parsed.num === runResult.parsed.denom ? 'passed' : 'partial'}`, ` result: ${runResult.parsed.num}/${runResult.parsed.denom} ${runResult.parsed.num === runResult.parsed.denom ? 'passed' : 'partial'}`));
10242
+ else log(t(` (pass/fail 비율을 stdout에서 파싱 못함)`, ` (could not parse pass/fail ratio from stdout)`));
10167
10243
  runTestsOk = runResult.allPassed;
10168
10244
  if (declaredPass && runResult.parsed) {
10169
10245
  declaredPassMatchesActual = (runResult.parsed.num === declaredPass.num && runResult.parsed.denom === declaredPass.denom);
10170
- log(` 주장 vs 실행: ${declaredPassMatchesActual ? '✓ 일치' : `⚠ 불일치 (주장 ${declaredPass.num}/${declaredPass.denom} ≠ 실행 ${runResult.parsed.num}/${runResult.parsed.denom})`}`);
10246
+ log(t(` 주장 vs 실행: ${declaredPassMatchesActual ? '✓ 일치' : `⚠ 불일치 (주장 ${declaredPass.num}/${declaredPass.denom} ≠ 실행 ${runResult.parsed.num}/${runResult.parsed.denom})`}`, ` claimed vs run: ${declaredPassMatchesActual ? '✓ match' : `⚠ mismatch (claimed ${declaredPass.num}/${declaredPass.denom} ≠ run ${runResult.parsed.num}/${runResult.parsed.denom})`}`));
10171
10247
  }
10172
10248
  }
10173
10249
  }
@@ -10177,76 +10253,76 @@ function verifyClaimCmd(root, taskId) {
10177
10253
  log('');
10178
10254
  const allFilesOk = filesAllExist;
10179
10255
  const testOk = declaredTestCount == null || actualTestCount == null || actualTestCount >= declaredTestCount;
10180
- log(`## 종합`);
10181
- log(` - 파일 모두 존재: ${allFilesOk ? '✓ pass' : '✗ FAIL (일부 누락)'}`);
10256
+ log(t(`## 종합`, `## Summary`));
10257
+ log(` - ${t('파일 모두 존재', 'all files exist')}: ${allFilesOk ? '✓ pass' : t('✗ FAIL (일부 누락)', '✗ FAIL (some missing)')}`);
10182
10258
  // 1.17.4 (UR-0047): 측정 불가는 '통과' 가 아니라 '검증 미수행' — 이전엔 실측 0 인데 ✓ pass(실측≥주장) 모순 표기.
10183
- log(` - 테스트 카운트: ${declaredTestCount == null ? '⊘ (주장 없음)' : !testMeasured ? `⊘ 측정 불가 — 주장 ${declaredTestCount}개 검증 미수행 (pass 아님)` : testOk ? '✓ pass (실측 ≥ 주장)' : '⚠ 주장보다 적음'}`);
10259
+ log(` - ${t('테스트 카운트', 'test count')}: ${declaredTestCount == null ? t('⊘ (주장 없음)', '⊘ (none claimed)') : !testMeasured ? t(`⊘ 측정 불가 — 주장 ${declaredTestCount}개 검증 미수행 (pass 아님)`, `⊘ not measurable — claimed ${declaredTestCount} not verified (not a pass)`) : testOk ? t('✓ pass (실측 ≥ 주장)', '✓ pass (measured ≥ claimed)') : t('⚠ 주장보다 적음', '⚠ fewer than claimed')}`);
10184
10260
  if (runResult && !runResult.skipped) {
10185
- log(` - ${runResult.cmd || 'npm test'} 실행: ${runTestsOk ? '✓ all passed' : '✗ FAIL'}`);
10186
- if (declaredPass) log(` - 주장과 실행 결과 일치: ${declaredPassMatchesActual ? '✓ pass' : '⚠ 다름'}`);
10261
+ log(` - ${runResult.cmd || 'npm test'} ${t('실행', 'run')}: ${runTestsOk ? '✓ all passed' : '✗ FAIL'}`);
10262
+ if (declaredPass) log(` - ${t('주장과 실행 결과 일치', 'claimed matches run')}: ${declaredPassMatchesActual ? '✓ pass' : t('⚠ 다름', '⚠ differs')}`);
10187
10263
  }
10188
10264
  // 1.11.2 (UR-0175): optimism+정직성 — done 주장은 기본 게이팅(claimsChecked). 완화: --lenient.
10189
10265
  if (claimsChecked) {
10190
- if (strictOk) log(` - 낙관적 표시 + 정직성 (done 기본): ✓ pass (의심 없음)`);
10266
+ if (strictOk) log(t(` - 낙관적 표시 + 정직성 (done 기본): ✓ pass (의심 없음)`, ` - optimism + honesty (done default): ✓ pass (no suspicion)`));
10191
10267
  else {
10192
- log(` - 낙관적 표시 + 정직성 (done 기본 — 완화: --lenient): ⚠ FAIL (낙관 ${optimismSuspects.length} · 정직성 ${honestyFindings.length})`);
10193
- for (const s of optimismSuspects) log(` · [${s.kind}] ${s.label}: evidence에 주장 있는데 코드에 호출 흔적 없음`);
10194
- for (const f of honestyFindings) log(` · [정직성:${f.dim}] ${f.label}: ${f.detail}`);
10268
+ log(t(` - 낙관적 표시 + 정직성 (done 기본 — 완화: --lenient): ⚠ FAIL (낙관 ${optimismSuspects.length} · 정직성 ${honestyFindings.length})`, ` - optimism + honesty (done default — relax: --lenient): ⚠ FAIL (optimism ${optimismSuspects.length} · honesty ${honestyFindings.length})`));
10269
+ for (const s of optimismSuspects) log(t(` · [${s.kind}] ${s.label}: evidence에 주장 있는데 코드에 호출 흔적 없음`, ` · [${s.kind}] ${s.label}: claimed in evidence but no call trace in code`));
10270
+ for (const f of honestyFindings) log(` · [${t('정직성', 'honesty')}:${f.dim}] ${f.label}: ${f.detail}`);
10195
10271
  }
10196
10272
  }
10197
10273
  // 1.9.302 (UR-0042) + 1.11.2 (UR-0175): git diff 시맨틱 교차검증 — 주장 파일이 실제 git 변경에 있는가. gitClaimOk/gitStrongMismatch 는 상단 공유(done 기본 게이팅, --lenient 완화).
10198
10274
  if (gitChanged === null) {
10199
- log(` - git diff 교차검증: ⊘ skip (git repo 아님 — 검증 불가)`);
10275
+ log(` - ${t('git diff 교차검증', 'git diff cross-check')}: ${t('⊘ skip (git repo 아님 — 검증 불가)', '⊘ skip (not a git repo — cannot verify)')}`);
10200
10276
  } else if (!gitApplicable) {
10201
- log(` - git diff 교차검증: ⊘ skip (working tree 변경 0 또는 주장 파일 0 — 이미 커밋됐거나 해당 없음)`);
10277
+ log(` - ${t('git diff 교차검증', 'git diff cross-check')}: ${t('⊘ skip (working tree 변경 0 또는 주장 파일 0 — 이미 커밋됐거나 해당 없음)', '⊘ skip (no working-tree changes or no claimed files — already committed or N/A)')}`);
10202
10278
  } else {
10203
- log(` - git diff 교차검증: ${gitStrongMismatch ? '⚠ 불일치' : '✓'} 주장 ${files.length}개 중 실제 변경 ${claimedInGit.length}개${claimedNotInGit.length ? ` · git 변경에 없음: ${claimedNotInGit.slice(0, 5).join(', ')}` : ''}`);
10204
- if (gitStrongMismatch) log(` · 주장한 파일이 working tree/직전커밋 변경에 전무 — 변경이 더 오래전 커밋이거나, 실제로 변경 안 됐을 수 있음(허위완료 의심)${has('--strict-claims') ? ' → FAIL' : ' (advisory — 커밋 후 검증 시 정상일 수 있음)'}`);
10279
+ log(` - ${t('git diff 교차검증', 'git diff cross-check')}: ${gitStrongMismatch ? t('⚠ 불일치', '⚠ mismatch') : '✓'} ${t(`주장 ${files.length}개 중 실제 변경 ${claimedInGit.length}개`, `${claimedInGit.length} of ${files.length} claimed actually changed`)}${claimedNotInGit.length ? t(` · git 변경에 없음: ${claimedNotInGit.slice(0, 5).join(', ')}`, ` · not in git changes: ${claimedNotInGit.slice(0, 5).join(', ')}`) : ''}`);
10280
+ if (gitStrongMismatch) log(` · ${t('주장한 파일이 working tree/직전커밋 변경에 전무 — 변경이 더 오래전 커밋이거나, 실제로 변경 안 됐을 수 있음(허위완료 의심)', 'claimed files are absent from working-tree/last-commit changes — changed in an older commit, or not actually changed (false-done suspected)')}${has('--strict-claims') ? ' → FAIL' : t(' (advisory — 커밋 후 검증 시 정상일 수 있음)', ' (advisory — may be normal when verifying after commit)')}`);
10205
10281
  // 1.13.2 (Karpathy 가이드라인 3 "외과적 변경", UR-0030): scope-creep 표면화 — git 변경됐으나 evidence/주장에 없는 파일.
10206
- if (changedNotClaimed.length) log(` · 🔬 외과적 변경 점검: git 에 변경됐으나 evidence/주장에 없는 파일 ${changedNotClaimed.length}건: ${changedNotClaimed.slice(0, 5).join(', ')}${changedNotClaimed.length > 5 ? ' …' : ''} — 요청 범위 밖 변경(scope creep)인지 확인 (advisory)`);
10282
+ if (changedNotClaimed.length) log(` · ${t(`🔬 외과적 변경 점검: git 에 변경됐으나 evidence/주장에 없는 파일 ${changedNotClaimed.length}건: ${changedNotClaimed.slice(0, 5).join(', ')}${changedNotClaimed.length > 5 ? ' …' : ''} — 요청 범위 밖 변경(scope creep)인지 확인 (advisory)`, `🔬 surgical-change check: ${changedNotClaimed.length} file(s) changed in git but not in evidence/claim: ${changedNotClaimed.slice(0, 5).join(', ')}${changedNotClaimed.length > 5 ? ' …' : ''} — check for scope creep (advisory)`)}`);
10207
10283
  }
10208
10284
  // 1.9.309 (UR-0048): done 주장 evidence 완전성 — 기본 강제(상단 pre-compute). --lenient 로 opt-out.
10209
10285
  if (mustHaveEvidence) {
10210
- log(` - evidence 완전성 (done 기본 강제): ${evidenceQualityOk ? '✓ pass (파일+테스트 근거 있음)' : `✗ FAIL (누락: ${evq.missing.join(', ')})`}`);
10211
- if (!evidenceQualityOk) log(` · done 주장은 수정 파일 경로 + 테스트명/개수 가 evidence 에 있어야 함 (테스트 통과만으로는 불충분). 완화: --lenient`);
10286
+ log(` - ${t('evidence 완전성 (done 기본 강제)', 'evidence completeness (done default)')}: ${evidenceQualityOk ? t('✓ pass (파일+테스트 근거 있음)', '✓ pass (file + test evidence present)') : `✗ FAIL ${t(`(누락: ${evq.missing.join(', ')})`, `(missing: ${evq.missing.join(', ')})`)}`}`);
10287
+ if (!evidenceQualityOk) log(` · ${t('done 주장은 수정 파일 경로 + 테스트명/개수 가 evidence 에 있어야 함 (테스트 통과만으로는 불충분). 완화: --lenient', 'a done claim needs changed-file paths + test name/count in evidence (passing tests alone is insufficient). relax: --lenient')}`);
10212
10288
  }
10213
10289
  // 1.17.3 (UR-0046): 구현 실체(스텁) + 테스트-구현 연결 — Attack C(주석뿐 구현+assert(true)) 차단.
10214
10290
  if (stubFiles.length) {
10215
- log(` - 구현 실체 (done 기본): FAIL — 주장된 구현 파일이 주석/빈껍데기뿐: ${stubFiles.slice(0, 5).join(', ')} (비주석 코드 0줄 또는 빈 export 껍데기)`);
10291
+ log(` - ${t('구현 실체 (done 기본)', 'implementation substance (done default)')}: ${t(`✗ FAIL — 주장된 구현 파일이 주석/빈껍데기뿐: ${stubFiles.slice(0, 5).join(', ')} (비주석 코드 0줄 또는 빈 export 껍데기)`, `✗ FAIL — claimed impl files are comments/empty shells only: ${stubFiles.slice(0, 5).join(', ')} (0 non-comment code lines or empty export shell)`)}`);
10216
10292
  } else if (claimsChecked && _vcImpl.length) {
10217
- log(` - 구현 실체 (done 기본): ✓ pass (주장 구현 파일에 실코드 존재)`);
10293
+ log(` - ${t('구현 실체 (done 기본)', 'implementation substance (done default)')}: ${t('✓ pass (주장 구현 파일에 실코드 존재)', '✓ pass (real code present in claimed impl files)')}`);
10218
10294
  }
10219
10295
  if (testLinkOk === false) {
10220
- log(` - 테스트-구현 연결: 주장된 테스트(${_vcTests.slice(0, 3).join(', ')})가 구현 파일을 참조하지 않음 — 빈 테스트(assert(true)) 의심${has('--strict-claims') ? ' → FAIL' : ' (advisory — --strict-claims 시 FAIL)'}`);
10296
+ log(` - ${t('테스트-구현 연결', 'test-impl link')}: ${t(`⚠ 주장된 테스트(${_vcTests.slice(0, 3).join(', ')})가 구현 파일을 참조하지 않음 — 빈 테스트(assert(true)) 의심`, `⚠ claimed test(s) (${_vcTests.slice(0, 3).join(', ')}) do not reference the impl file — empty test (assert(true)) suspected`)}${has('--strict-claims') ? ' → FAIL' : t(' (advisory — --strict-claims 시 FAIL)', ' (advisory — FAIL under --strict-claims)')}`);
10221
10297
  } else if (testLinkOk === true && claimsChecked) {
10222
- log(` - 테스트-구현 연결: ✓ pass (테스트가 구현을 참조)`);
10298
+ log(` - ${t('테스트-구현 연결', 'test-impl link')}: ${t('✓ pass (테스트가 구현을 참조)', '✓ pass (test references the impl)')}`);
10223
10299
  }
10224
10300
  const overallFail = !allFilesOk || !testOk || (runResult && !runResult.skipped && !runTestsOk) || (claimsChecked && !strictOk) || !evidenceQualityOk || !gitClaimOk || (claimsChecked && stubFiles.length > 0) || (has('--strict-claims') && testLinkOk === false);
10225
10301
  // 1.9.287: 정직한 한계 고지 — 테스트 통과 ≠ 의미적 구현 정확성
10226
10302
  if (claimsChecked || mustHaveEvidence) {
10227
10303
  log('');
10228
- log(` ℹ 한계: 테스트 통과는 "의미적 구현 정확성"을 보장하지 않음 — evidence 가 해당 주장(수정 파일/테스트)을 직접 링크해야 신뢰도↑.`);
10304
+ log(t(` ℹ 한계: 테스트 통과는 "의미적 구현 정확성"을 보장하지 않음 — evidence 가 해당 주장(수정 파일/테스트)을 직접 링크해야 신뢰도↑.`, ` ℹ Limit: passing tests do not guarantee "semantic correctness" — evidence should directly link the claimed files/tests for higher confidence.`));
10229
10305
  // 1.19.2 (UR-0003 렌즈 완전판 v2): 완료-검증 순간에 분야별 자기질문 advisory — 주장 파일 확장자 기반(결정적).
10230
10306
  // 기계검증(파일/테스트/스텁)을 통과해도 "사람이 보기에 좋은가"는 별개 → AI 가 스스로 답하도록 권장(advisory, 게이트 아님).
10231
10307
  const _lensDoms = _lensDomainsForFiles(files);
10232
10308
  if (_lensDoms.length) {
10233
10309
  const _lensCat = _effectiveLensCatalog(root); // 1.19.3: 프로젝트 커스텀 질문도 포함
10234
10310
  log('');
10235
- log(` 🧭 품질 렌즈 (완료 선언 전 자문 — advisory, 게이트 아님):`);
10311
+ log(t(` 🧭 품질 렌즈 (완료 선언 전 자문 — advisory, 게이트 아님):`, ` 🧭 Quality lens (self-ask before declaring done — advisory, not a gate):`));
10236
10312
  for (const d of _lensDoms) {
10237
10313
  const l = _lensCat[d];
10238
10314
  if (l) log(` · ${d}(${l.title}): ${l.questions[0]}`);
10239
10315
  }
10240
- log(` → 전체 질문: leerness lens ${_lensDoms[0]}`);
10316
+ log(` ${t('→ 전체 질문', '→ full questions')}: leerness lens ${_lensDoms[0]}`);
10241
10317
  }
10242
10318
  }
10243
10319
  if (overallFail) {
10244
10320
  log('');
10245
- log(` ⚠ evidence 주장과 실제가 일치하지 않음 — task 상태 재검토 권장`);
10321
+ log(t(` ⚠ evidence 주장과 실제가 일치하지 않음 — task 상태 재검토 권장`, ` ⚠ evidence claim does not match reality — review the task status`));
10246
10322
  return process.exit(1);
10247
10323
  }
10248
10324
  log('');
10249
- log(` ✓ evidence 주장이 실제 파일·테스트${runResult && !runResult.skipped ? '·실행 결과' : ''}와 일치`);
10325
+ log(t(` ✓ evidence 주장이 실제 파일·테스트${runResult && !runResult.skipped ? '·실행 결과' : ''}와 일치`, ` ✓ evidence claim matches actual files·tests${runResult && !runResult.skipped ? '·run results' : ''}`));
10250
10326
  }
10251
10327
 
10252
10328
  // 1.9.22: orchestrate — Ollama 로컬 LLM으로 best-of-N 멀티 에이전트 시뮬
@@ -11611,43 +11687,46 @@ function _banner(opts = {}) {
11611
11687
  cprint(' ' + C.green(padded) + C.dim('# ' + desc));
11612
11688
  };
11613
11689
 
11690
+ // 1.20.2 (UR-0010 Phase 1): 첫 화면 배너 — UI 언어(한국어 우선, 영어 opt-in) 적용.
11691
+ const L = _uiLang(arg('--path', process.cwd()));
11692
+ const t = (ko, en) => (L === 'en' ? en : ko);
11614
11693
  cprint('');
11615
- cprint(C.bold(C.cyan(' ✨ 시작하기 (3단계면 끝)')));
11616
- cmd('npx leerness init .', '1️⃣ 하네스 설치 + AI 도구 자동 연결');
11617
- cmd('npx leerness handoff .', '2️⃣ 세션 시작 — 컨텍스트·기억·feature impact 자동 회수');
11618
- cmd('npx leerness session close .', '3️⃣ 세션 종료 — 마감 통계 + 다음 라운드 추천');
11619
-
11620
- section('🧠 메모리 5종 CRUD (1.9.142 — cascade 방지)');
11621
- cmd('leerness task add "<제목>"', 'progress-tracker 등록');
11622
- cmd('leerness decision add "<제목>" --reason "..."', '되돌리기 어려운 결정 영구화');
11623
- cmd('leerness lesson save "<교훈>" --tag "..."', '재발견 가능한 통찰 저장');
11624
- cmd('leerness plan add "<milestone>"', '계획 단계 등록');
11625
- cmd('leerness rule add "<룰>" --trigger every-X', '자연어 영구 룰');
11626
- cmd('leerness feature add "<기능>" --files "..."', 'Feature Graph 노드 (1.9.141)');
11627
-
11628
- section('🔗 인과관계 + 영향 추적 (1.9.141~143)');
11629
- cmd('leerness feature impact <F-XXXX>', '코드 변경 전 영향받는 feature 자동 회수');
11630
- cmd('leerness feature list --json', '전체 그래프 + 엣지');
11631
- cmd('leerness audit . --json', 'orphan/cycle 무결성 검증');
11632
-
11633
- section('🛡 보안·드리프트·게으름 가드');
11634
- cmd('leerness drift check . --auto-fix', 'drift + 보안 자동 회복');
11635
- cmd('leerness lazy detect . --json', '거짓 완료/no test run 감지');
11636
- cmd('leerness env sync .', '.env ↔ .env.example 동기화');
11637
- cmd('leerness health . --json', '종합 헬스 (drift+보안+skill+feature)');
11638
-
11639
- section(`🤖 외부 AI 통합 (MCP ${_mcpToolCount()} 도구)`); // 1.9.315 (UR-0054): 하드코딩 46 → 동적
11694
+ cprint(C.bold(C.cyan(t(' ✨ 시작하기 (3단계면 끝)', ' ✨ Get started (3 steps)'))));
11695
+ cmd('npx leerness init .', t('1️⃣ 하네스 설치 + AI 도구 자동 연결', '1️⃣ install the harness + auto-wire AI tools'));
11696
+ cmd('npx leerness handoff .', t('2️⃣ 세션 시작 — 컨텍스트·기억·feature impact 자동 회수', '2️⃣ start a session — context, memory, feature impact in one call'));
11697
+ cmd('npx leerness session close .', t('3️⃣ 세션 종료 — 마감 통계 + 다음 라운드 추천', '3️⃣ end a session — closing stats + next-round suggestion'));
11698
+
11699
+ section(t('🧠 메모리 5종 CRUD (1.9.142 — cascade 방지)', '🧠 Memory (5 surfaces)'));
11700
+ cmd('leerness task add "<title>"', t('progress-tracker 등록', 'add a task to progress-tracker'));
11701
+ cmd('leerness decision add "<title>" --reason "..."', t('되돌리기 어려운 결정 영구화', 'persist a hard-to-reverse decision'));
11702
+ cmd('leerness lesson save "<lesson>" --tag "..."', t('재발견 가능한 통찰 저장', 'save a rediscoverable insight'));
11703
+ cmd('leerness plan add "<milestone>"', t('계획 단계 등록', 'add a plan milestone'));
11704
+ cmd('leerness rule add "<rule>" --trigger every-X', t('자연어 영구 룰', 'natural-language standing rule'));
11705
+ cmd('leerness feature add "<feature>" --files "..."', t('Feature Graph 노드 (1.9.141)', 'Feature Graph node'));
11706
+
11707
+ section(t('🔗 인과관계 + 영향 추적 (1.9.141~143)', '🔗 Causality + impact tracking'));
11708
+ cmd('leerness feature impact <F-XXXX>', t('코드 변경 전 영향받는 feature 자동 회수', 'which features a code change affects'));
11709
+ cmd('leerness feature list --json', t('전체 그래프 + 엣지', 'full graph + edges'));
11710
+ cmd('leerness audit . --json', t('orphan/cycle 무결성 검증', 'orphan/cycle integrity check'));
11711
+
11712
+ section(t('🛡 보안·드리프트·게으름 가드', '🛡 Security · drift · laziness guards'));
11713
+ cmd('leerness drift check . --auto-fix', t('drift + 보안 자동 회복', 'drift + security auto-heal'));
11714
+ cmd('leerness lazy detect . --json', t('거짓 완료/no test run 감지', 'detect false-done / no-test-run'));
11715
+ cmd('leerness env sync .', t('.env ↔ .env.example 동기화', 'sync .env <-> .env.example'));
11716
+ cmd('leerness health . --json', t('종합 헬스 (drift+보안+skill+feature)', 'overall health (drift+security+skill+feature)'));
11717
+
11718
+ section(t(`🤖 외부 AI 통합 (MCP ${_mcpToolCount()} 도구)`, `🤖 External AI integration (MCP, ${_mcpToolCount()} tools)`)); // 1.9.315 (UR-0054): 하드코딩 46 → 동적
11640
11719
  cmd('npx leerness mcp serve', 'stdio JSON-RPC server');
11641
- cmd('leerness memory status . --json', '5 surface + featureGraph 한 호출');
11642
- cmd('leerness memory archive list --query "kw"', 'DELETE 5종 archive 검색');
11643
- cmd('leerness memory restore <surface> <target>', 'archive → active 복원');
11720
+ cmd('leerness memory status . --json', t('5 surface + featureGraph 한 호출', '5 surfaces + featureGraph in one call'));
11721
+ cmd('leerness memory archive list --query "kw"', t('DELETE 5종 archive 검색', 'search the archive of deleted items'));
11722
+ cmd('leerness memory restore <surface> <target>', t('archive → active 복원', 'restore archive -> active'));
11644
11723
 
11645
- section('🚀 Release 자동화');
11646
- cmd('leerness release pack --close --auto-main-push', '한 줄 release (1.9.140 main push 통합)');
11647
- cmd('leerness release sync-main .', 'release branch → main 자동 fast-forward');
11724
+ section(t('🚀 Release 자동화', '🚀 Release automation'));
11725
+ cmd('leerness release pack --close --auto-main-push', t('한 줄 release (1.9.140 main push 통합)', 'one-line release (with main push)'));
11726
+ cmd('leerness release sync-main .', t('release branch → main 자동 fast-forward', 'release branch -> main fast-forward'));
11648
11727
 
11649
11728
  cprint('');
11650
- cprint(C.dim(' 📚 자세히: `leerness --help` · 자율 모드: `<<autonomous-loop-dynamic>>` 신호로 진행'));
11729
+ cprint(C.dim(t(' 📚 자세히: `leerness --help` · 자율 모드: `<<autonomous-loop-dynamic>>` 신호로 진행', ' 📚 More: `leerness --help`')));
11651
11730
  cprint('');
11652
11731
  }
11653
11732
  }
@@ -20022,7 +20101,7 @@ module.exports = {
20022
20101
  // 1.9.289: shell-safe 인용 (Codex #3) — 단위 테스트
20023
20102
  _shellQuoteArg,
20024
20103
  // 1.18.1: 명령 실행 권한 결정 (재실증 신규 P1: --test-cmd 비-JS 인터프리터 거짓차단) — 단위 테스트
20025
- _isCommandPermitted, RUN_CORE_ALLOW,
20104
+ _isCommandPermitted, RUN_CORE_ALLOW, _uiLang, _tx,
20026
20105
  // 1.18.2: verify-claim 위장 스텁(빈 export 껍데기) 판정 — 단위 테스트
20027
20106
  _vcImplIsEmpty, _VC_EMPTY_SHELL_RE,
20028
20107
  // 1.18.3 (UR-0003): 분야별 자기질문 품질 렌즈 — 단위 테스트. 1.19.2: 파일→도메인 매핑(완료-검증 advisory)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.20.0",
3
+ "version": "1.22.0",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",