leerness 1.21.0 → 1.23.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,103 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.23.0 — 2026-06-15 — 🛡️ [안정화/Stable] session close 영어 완전화 안정 minor
4
+
5
+ **🛡️ 안정화(Stable) minor — 세션 마감 화면 전체를 영어로 공개.** 직전 minor(1.22.0) 이후 누적된 패치 2건(1.22.1 Phase 4 + 1.22.2 Phase 5)을 검증·통합해 npm 배포. R-0011 정책의 14번째 stable minor. 이로써 **핵심 세션 라이프사이클 — 설치(init) → 시작(handoff) → 검증(verify-claim) → 마감(session close) — 전체가 영어 opt-in 으로 일관**됩니다. 한국어 우선 기본은 그대로.
6
+
7
+ ### 이번 minor 통합 (1.22.1~1.22.2)
8
+ - **🌐 session close 마감 보고 영어화 (Phase 4)**: 항상-표시되는 마감 라인 — 완료 정직성/마감 보안 advisory, `활성 룰 없음`→`no active rules`, Required final response sections, `🔚 자동 통합 보고`→`integrated report`(사용자 요청·pre-wake·멱등성·셸 실패 포함). DI 로 `uiLang` 주입.
9
+ - **🌐 session close --suggest/진행요약/cleanup 영어화 (Phase 5)**: 다음 라운드 추천(skill 후보·`drift 상태`→`drift status`·`가장 많이 쓴 명령`→`Most-used commands`·MCP/skill-query 통계), `진행 요약`→`Progress summary`(archive·마일스톤·회고·워크스페이스), 운영 자동정리(release cleanup·인코딩 BOM).
10
+ - **가벼움 정리**: MCP 통계 줄의 `t` 헬퍼 셰도잉(`.map(([t,n])…)` → `[tool,n]`) 제거.
11
+ - **한국어 우선 기본 보존**: 영어는 명시 opt-in(`--language en`/`LEERNESS_LANG=en`/en init 프로젝트). 플래그 없으면 한국어 — e2e 무회귀로 검증.
12
+
13
+ ### 잔여 (UR-0010 Phase 6+, 백로그)
14
+ - session-handoff.md / current-state.md 본문(파일에 기록되는 한국어 메모리 아티팩트) + help + status + 그 외 명령 본문 — 단계적 확대.
15
+
16
+ ### 검증 (회귀 0)
17
+ - **selftest 240/240** (Phase 4·5 영어/한국어 보존 소스가드 포함) · 행위(en: `integrated report / Next-round suggestions / Progress summary`, ko 기본: `자동 통합 보고 / 다음 라운드 추천 / 진행 요약`) · **E2E 365/365**.
18
+ - minor(1.23.0) — npm 배포(R-0011 stable) + annotated tag(Stable) + GitHub release(latest) + 게시본 클린룸 재실증.
19
+
20
+ ## 1.22.2 — 2026-06-15 — CLI 영어화 Phase 5: session close --suggest/진행요약/cleanup 영어화 (UR-0010)
21
+
22
+ **🌐 마감 보고의 나머지 절반까지 영어로.** Phase 4 가 "항상 표시되는" 마감 라인을 영어화했다면, Phase 5 는 **다음 라운드 추천(`--suggest`)·진행 요약·운영 자동정리** 출력을 영어 opt-in 으로 — 마감 화면 전체가 언어 선택을 따른다.
23
+
24
+ ### 변경 (UR-0010 Phase 5)
25
+ - **`--suggest` 다음 라운드 추천 영어화**: `다음 라운드 추천`→`Next-round suggestions`, 신규 skill 후보, `drift 상태`→`drift status`, `가장 많이 쓴 명령`→`Most-used commands`, MCP 호출/드문 호출, skill match query 누적 — 전부 `t(ko,en)` 분기.
26
+ - **진행 요약 영어화**: `진행 요약`→`Progress summary`, archive 누적, N세션 마일스톤/자동 회고, 워크스페이스 안내, retro 요약 실패 메시지.
27
+ - **운영 자동정리 영어화**: release/* 정리(`--auto-cleanup-branches`), 셸 스크립트 인코딩 위험 자동 BOM(`--auto-fix-encoding`) 안내.
28
+ - **가벼움 정리**: MCP 통계 줄에서 `t` 헬퍼와 충돌하던 `.map(([t, n]) => …)` 지역 변수를 `tool` 로 rename — 셰도잉 제거(가독성).
29
+ - **한국어 기본 유지**: 영어는 `--language en`/`LEERNESS_LANG=en`/en init 프로젝트에서만. 플래그 없으면 한국어(ko 원문은 `t()` ko 인자로 보존).
30
+
31
+ ### 잔여 (UR-0010 Phase 6+, 백로그)
32
+ - session-handoff.md / current-state.md 본문(파일에 기록되는 한국어 메모리 아티팩트) + help + status + 그 외 명령 본문 — 단계적 확대.
33
+
34
+ ### 검증 (회귀 0)
35
+ - **selftest 239→240** (Phase 5 영어/한국어 보존 + 셰도잉 제거 소스가드) · 행위(en: `Next-round suggestions / drift status / Most-used commands / Progress summary`, ko 기본: `다음 라운드 추천 / drift 상태 / 가장 많이 쓴 명령 / 진행 요약`) · **E2E 365/365**.
36
+ - patch(1.22.2) — npm 미배포(R-0011, GitHub/CHANGELOG 누적).
37
+
38
+ ## 1.22.1 — 2026-06-15 — CLI 영어화 Phase 4: session close 마감 보고 영어화 (UR-0010)
39
+
40
+ **🌐 세션을 마칠 때 보는 마지막 출력을 영어로.** init 배너(P1)→handoff 헤드라인(P2)→verify-claim(P3)에 이어 **session close 의 항상-표시 마감 보고**를 영어 opt-in 으로 — 핵심 세션 라이프사이클(시작→작업→검증→마감)이 영어로 일관.
41
+
42
+ ### 변경 (UR-0010 Phase 4)
43
+ - **session close 마감 보고 영어화 (DI uiLang 주입)**: `_uiLang(root)` 를 session-close DI deps 로 주입 후 항상-표시 라인 `t(ko,en)` 분기 — 완료 정직성/마감 보안 advisory, `활성 룰 없음`→`no active rules`, Required final response sections 목록, `🔚 자동 통합 보고`→`integrated report`(사용자 요청/pre-wake/멱등성/셸 실패 결과 포함). 한국어 기본 유지(영어 명시 opt-in).
44
+ - **한국어 무회귀**: e2e 는 `--language` 없이 ko 로 실행 → 마감 보고 한국어 유지(검증). ko 원문은 `t()` ko 인자로 보존.
45
+
46
+ ### 잔여 (UR-0010 Phase 5+, 백로그)
47
+ - session close `--suggest` 상세(skill 후보·drift·MCP/skill-query 통계·진행 요약) + help + status + 그 외 명령 본문 — 단계적 확대.
48
+
49
+ ### 검증 (회귀 0)
50
+ - **selftest 238→239** (session close uiLang 주입 + 영어/한국어 보존 소스가드) · 행위(en: `integrated report / no active rules / no user requests`, ko 기본: `자동 통합 보고 / 활성 룰 없음`) · **E2E 365/365**.
51
+ - patch(1.22.1) — npm 미배포(R-0011, GitHub/CHANGELOG 누적).
52
+
53
+ ## 1.22.0 — 2026-06-15 — 🛡️ [안정화/Stable] 확장된 영어 지원(헤드라인 + verify-claim) 안정 minor
54
+
55
+ **🛡️ 안정화(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 플래그십**까지 확장 — 한국어 우선 기본은 그대로.
56
+
57
+ ### 이번 minor 통합 (1.21.1~1.21.2)
58
+ - **🌐 handoff 헤드라인 항목 라벨 영어화 (Phase 2)**: 매 세션 가장 많이 보는 출력 — `--language en` 시 `📊 Headline: drift healthy · security OK · N skills · health · mem …` 완전 영어. 보안/시크릿/미답요청/비정상종료/플랫폼제약 등 전 항목.
59
+ - **🌐 verify-claim 출력 영어화 (Phase 3, 플래그십)**: 완료 검증 시 보는 핵심 출력 ~30 문자열 영어화 — File check / Test count / Summary / implementation substance / test-impl link / git diff cross-check / optimism+honesty / final verdict.
60
+ - **한국어 우선 기본 보존**: 영어는 명시 opt-in(`--language en`/`LEERNESS_LANG=en`/en init 프로젝트). 플래그 없으면 한국어 — e2e 무회귀로 검증.
61
+
62
+ ### 잔여 (UR-0010 Phase 4+, 백로그)
63
+ - session close 마감 보고(~117 문자열) + help + status — 단계적 확대.
64
+
65
+ ### 검증 (회귀 0)
66
+ - **selftest 236→238** · **E2E 365/365** · en/ko 헤드라인·verify-claim 행위 + 게시본 재실증.
67
+
68
+ ### 안정화 표시 (R-0006)
69
+ CHANGELOG [안정화/Stable] · git tag (Stable) · GitHub release (`--latest`) · npm dist-tag `stable` 시도.
70
+
71
+ ## 1.21.2 — 2026-06-15 — CLI 영어화 Phase 3: verify-claim(플래그십) 출력 영어화 (UR-0010)
72
+
73
+ **🌐 플래그십 명령 출력을 영어로.** 사용자가 "완료됐나?"를 확인할 때 보는 `verify-claim` 의 human 출력 전체를 영어 opt-in 으로 — 온보딩 다음으로 영향 큰 표면. 한국어 기본 유지(영어 명시 opt-in).
74
+
75
+ ### 변경 (UR-0010 Phase 3)
76
+ - **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 헤더도 영어.
77
+ - **한국어 기본 무회귀**: e2e 는 `--language` 없이 실행되며 init 이 이 환경에서 manifest.language=`ko` 로 저장(검증함) → verify-claim 기본 출력 한국어 유지. ko 원문은 `t()` 의 ko 인자로 소스에 보존.
78
+
79
+ ### 잔여 (UR-0010 Phase 4+, 백로그)
80
+ - session close 마감 보고 + help + status + 그 외 명령 본문 영어화 — 단계적 확대.
81
+
82
+ ### 검증 (회귀 0)
83
+ - **selftest 237→238** (verify-claim t() 경유 + 한국어 보존 소스가드) · 행위(en: `## Summary / all files exist / evidence claim matches`, ko 기본: `## 종합 / 파일 모두 존재`) · **E2E 365/365**.
84
+ - patch(1.21.2) — npm 미배포(R-0011, GitHub/CHANGELOG 누적).
85
+
86
+ ## 1.21.1 — 2026-06-15 — CLI 영어화 Phase 2: handoff 헤드라인 항목 라벨 영어화 (UR-0010)
87
+
88
+ **🌐 매 세션 가장 많이 보는 출력(handoff 헤드라인)을 영어로 완성.** Phase 1(배너 + 헤드라인 프리픽스)에 이어, 헤드라인의 **항목 라벨 전체**를 영어 opt-in 으로. 비한국어 사용자가 세션마다 보던 한국어 토막들(보안 OK·미답 요청·비정상종료·플랫폼 제약 등)이 사라짐.
89
+
90
+ ### 변경 (UR-0010 Phase 2)
91
+ - **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).
92
+ - 행위 확인: en → `📊 Headline: drift healthy · 🔒 security OK · 📚 N skills · ⚕ health: ✓ · 🧠 mem …` 완전 영어 / 플래그 없음 → `📊 헤드라인`(한국어).
93
+
94
+ ### 잔여 (UR-0010 Phase 3+, 백로그)
95
+ - session close 마감 보고 + verify-claim 출력 + help + status 등 명령 본문 영어화 — 단계적 확대.
96
+
97
+ ### 검증 (회귀 0)
98
+ - **selftest 236→237** (헤드라인 t() 경유 소스가드) · 행위(en 헤드라인 완전 영어 + ko 기본) · **E2E 365/365**.
99
+ - patch(1.21.1) — npm 미배포(R-0011, GitHub/CHANGELOG 누적).
100
+
3
101
  ## 1.21.0 — 2026-06-15 — 🛡️ [안정화/Stable] 영어 온보딩 첫걸음 + 검증 강화 안정 minor
4
102
 
5
103
  **🛡️ 안정화(Stable) minor — 비한국어 사용자에게 영어 온보딩을 실제로 전달.** 직전 minor(1.20.0) 이후 누적된 패치 2건(1.20.1~1.20.2)을 검증·통합해 npm 공개. R-0011 정책의 12번째 stable minor. 핵심: **영어 첫 화면(opt-in)** 이 npm 사용자에게 닿고, 검증 플래그십의 정적 우회가 닫힘.
package/README.md CHANGED
@@ -104,7 +104,7 @@ MIT
104
104
  <!-- leerness:project-readme:start -->
105
105
  ## Leerness Project Harness
106
106
 
107
- 이 프로젝트는 Leerness v1.21.0 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
107
+ 이 프로젝트는 Leerness v1.23.0 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
108
108
 
109
109
  ### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
110
110
 
@@ -158,7 +158,7 @@ leerness memory restore decision <date|title>
158
158
 
159
159
  ### MCP server (외부 AI 통합)
160
160
 
161
- Leerness v1.21.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
161
+ Leerness v1.23.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
162
162
 
163
163
  ```jsonc
164
164
  // 카테고리별
@@ -179,7 +179,7 @@ Leerness v1.21.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code
179
179
  `<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
180
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) 다음 라운드 예약.
181
181
 
182
- 현재 누적: **70 라운드 (1.9.40 → 1.21.0)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
182
+ 현재 누적: **70 라운드 (1.9.40 → 1.23.0)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
183
183
 
184
184
  ### 성능 가이드 (1.9.140 측정)
185
185
 
@@ -217,6 +217,6 @@ leerness release pack --close --auto-main-push
217
217
  - `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
218
218
  - `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
219
219
 
220
- Last synced by Leerness v1.21.0: 2026-06-15
220
+ Last synced by Leerness v1.23.0: 2026-06-15
221
221
  <!-- leerness:project-readme:end -->
222
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.21.0';
35
+ const VERSION = '1.23.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') 시 호스트 프로세스 오염.
@@ -3610,7 +3610,7 @@ function _selfTestCases() {
3610
3610
  const src = read(__filename);
3611
3611
  const authPass = src.includes('userAuthorized: true, timeout: 5 * 60 * 1000, kind: ' + "'verify_claim_test'");
3612
3612
  const skipOnBlock = src.includes('if (r.blocked) {') && src.includes('테스트 명령 차단') && src.includes('불일치 판정 아님');
3613
- 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()
3614
3614
  return authPass && skipOnBlock && label;
3615
3615
  } },
3616
3616
  { name: '재실증 P2 (1.18.1): task update id 뒤 non-path positional(status) 거부 + path-like 허용 (소스 가드)', run: () => {
@@ -3737,6 +3737,33 @@ function _selfTestCases() {
3737
3737
  const src = read(__filename);
3738
3738
  return src.includes("const L = _uiLang(arg('--path', process.cwd()));") && src.includes("_uiLang(root) === 'en' ? '📊 Headline'");
3739
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
+ } },
3752
+ { name: 'CLI 영어화 Phase 4 (1.22.1, UR-0010): session close uiLang 주입 + 마감보고 영어/한국어 보존 (소스 가드)', run: () => {
3753
+ const bin = read(__filename);
3754
+ const sc = read(path.join(path.dirname(__filename), '..', 'lib', 'session-close.js'));
3755
+ const injected = bin.includes('uiLang: _uiLang(root), harnessPath: __filename');
3756
+ const en = sc.includes('session close integrated report') && sc.includes('no active rules') && sc.includes('no user requests (UR backlog empty)');
3757
+ const koPreserved = sc.includes('자동 통합 보고') && sc.includes('활성 룰 없음'); // ko 원문 t() ko 인자로 보존(e2e ko 무회귀)
3758
+ return injected && en && koPreserved;
3759
+ } },
3760
+ { name: 'CLI 영어화 Phase 5 (1.22.2, UR-0010): session close --suggest/진행요약/cleanup 영어/한국어 보존 (소스 가드)', run: () => {
3761
+ const sc = read(path.join(path.dirname(__filename), '..', 'lib', 'session-close.js'));
3762
+ const en = sc.includes('Next-round suggestions') && sc.includes('Progress summary (session') && sc.includes('Most-used commands') && sc.includes('encoding risk(s) auto-fixed');
3763
+ const koPreserved = sc.includes('다음 라운드 추천') && sc.includes('진행 요약 (session') && sc.includes('가장 많이 쓴 명령'); // ko 원문 t() ko 인자로 보존(e2e ko 무회귀)
3764
+ const noShadow = !sc.includes('.map(([t, n]) =>'); // t 헬퍼 셰도잉 제거 확인
3765
+ return en && koPreserved && noShadow;
3766
+ } },
3740
3767
  { name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
3741
3768
  ];
3742
3769
  }
@@ -8278,6 +8305,8 @@ function handoff(root) {
8278
8305
  if (!has('--no-headline') && !has('--compact') && !has('--quiet')) {
8279
8306
  try {
8280
8307
  const parts = [];
8308
+ // 1.20.3 (UR-0010 Phase 2): 헤드라인 항목 라벨 UI 언어 적용 (영어 opt-in, 한국어 기본). 블록 1회 해석.
8309
+ const _L = _uiLang(root); const t = (ko, en) => (_L === 'en' ? en : ko);
8281
8310
  // 1) drift level (가벼운 check)
8282
8311
  try {
8283
8312
  const r = cp.spawnSync(process.execPath, [__filename, 'drift', 'check', root, '--json'],
@@ -8291,15 +8320,15 @@ function handoff(root) {
8291
8320
  try {
8292
8321
  const sec = _collectSecretFindings(root);
8293
8322
  if (sec.committed.length) {
8294
- parts.push(`🚨 시크릿 ${sec.committed.length}건`);
8323
+ parts.push(t(`🚨 시크릿 ${sec.committed.length}건`, `🚨 ${sec.committed.length} secret(s)`));
8295
8324
  } else {
8296
8325
  const envPath = path.join(root, '.env');
8297
8326
  if (exists(envPath)) {
8298
8327
  const giText = exists(path.join(root, '.gitignore')) ? read(path.join(root, '.gitignore')) : '';
8299
8328
  const giLines = giText.split('\n').map(l => l.trim());
8300
- parts.push((giLines.includes('.env') || giLines.includes('/.env')) ? '🔒 보안 OK' : '🚨 .env 미무시');
8329
+ parts.push((giLines.includes('.env') || giLines.includes('/.env')) ? t('🔒 보안 OK', '🔒 security OK') : t('🚨 .env 미무시', '🚨 .env not ignored'));
8301
8330
  } else {
8302
- parts.push('🔒 보안 OK');
8331
+ parts.push(t('🔒 보안 OK', '🔒 security OK'));
8303
8332
  }
8304
8333
  }
8305
8334
  } catch {}
@@ -8307,7 +8336,7 @@ function handoff(root) {
8307
8336
  try {
8308
8337
  const stats = _readUsageStats(root);
8309
8338
  const mcpTotal = stats.mcp?.tools ? Object.values(stats.mcp.tools).reduce((s, n) => s + n, 0) : 0;
8310
- if (mcpTotal > 0) parts.push(`🔌 MCP ${mcpTotal}회`);
8339
+ if (mcpTotal > 0) parts.push(`🔌 MCP ${mcpTotal}${t('회', 'x')}`);
8311
8340
  } catch {}
8312
8341
  // 4) skill match history 누적
8313
8342
  try {
@@ -8315,7 +8344,7 @@ function handoff(root) {
8315
8344
  if (exists(histPath)) {
8316
8345
  const txt = read(histPath);
8317
8346
  const cnt = (txt.match(/^## [\d-]+ [\d:]+ — query/gm) || []).length;
8318
- if (cnt > 0) parts.push(`📒 skill query ${cnt}회`);
8347
+ if (cnt > 0) parts.push(`📒 skill query ${cnt}${t('회', 'x')}`);
8319
8348
  }
8320
8349
  } catch {}
8321
8350
  // 5) 설치된 skill 수
@@ -8376,7 +8405,7 @@ function handoff(root) {
8376
8405
  }
8377
8406
  } catch {}
8378
8407
  }
8379
- if (slashCount > 0) parts.push(`🪄 slash 24h ${slashCount}회`);
8408
+ if (slashCount > 0) parts.push(`🪄 slash 24h ${slashCount}${t('회', 'x')}`);
8380
8409
  }
8381
8410
  } catch {}
8382
8411
  // 10) 1.9.192: 공식 organization skill catalog 캐시 매칭 (C축 보강 — 사용자 명시)
@@ -8404,7 +8433,7 @@ function handoff(root) {
8404
8433
  if (rh.roundCount >= 5) {
8405
8434
  let label = `🔄 R${rh.roundCount}`;
8406
8435
  if (rh.nextMilestone != null && rh.roundsToNextMilestone <= 20) {
8407
- label += ` → R${rh.nextMilestone} (${rh.roundsToNextMilestone}R 남음)`;
8436
+ label += ` → R${rh.nextMilestone} (${rh.roundsToNextMilestone}R ${t('남음', 'left')})`;
8408
8437
  }
8409
8438
  parts.push(label);
8410
8439
  // 1.9.230: 임박 마일스톤 ETA 별도 노출 (다음 마일스톤이 매우 가까울 때만)
@@ -8449,11 +8478,11 @@ function handoff(root) {
8449
8478
  let detected = { candidates: [] };
8450
8479
  try { detected = _detectDeliveredRequests(root); } catch {}
8451
8480
  if (detected.candidates && detected.candidates.length > 0) {
8452
- parts.push(`📥 자동완료가능 ${detected.candidates.length}건 (1.9.223)`);
8481
+ parts.push(t(`📥 자동완료가능 ${detected.candidates.length}건 (1.9.223)`, `📥 ${detected.candidates.length} auto-completable`));
8453
8482
  } else if (audit.missing && audit.missing.length > 0) {
8454
- parts.push(`📥 미답 요청 ${audit.missing.length}건`);
8483
+ parts.push(t(`📥 미답 요청 ${audit.missing.length}건`, `📥 ${audit.missing.length} unanswered request(s)`));
8455
8484
  } else if (audit.open > 0) {
8456
- parts.push(`📥 요청 ${audit.open} (tracked)`);
8485
+ parts.push(t(`📥 요청 ${audit.open} (tracked)`, `📥 ${audit.open} request(s) (tracked)`));
8457
8486
  }
8458
8487
  } catch {}
8459
8488
  // 14) 1.9.209: pre-wake-audit 최근 보고서 (사용자 명시) — 깨어남 직후 자동 노출
@@ -8472,7 +8501,7 @@ function handoff(root) {
8472
8501
  try {
8473
8502
  const ad = _detectAbnormalShutdown(root);
8474
8503
  if (ad.abnormalShutdown) {
8475
- parts.push(`🔌 비정상종료 ${ad.severity} (${ad.signals.length}신호)`);
8504
+ parts.push(t(`🔌 비정상종료 ${ad.severity} (${ad.signals.length}신호)`, `🔌 abnormal-exit ${ad.severity} (${ad.signals.length} signals)`));
8476
8505
  }
8477
8506
  } catch {}
8478
8507
  // 15) 1.9.215: 현재 활성 task에서 constraints/intent 자동 분석 (1.9.208/213 통합)
@@ -8492,7 +8521,7 @@ function handoff(root) {
8492
8521
  try {
8493
8522
  const cc = _checkRequestConstraints(root, req);
8494
8523
  if (cc.matched && cc.matched.length > 0) {
8495
- parts.push(`🚦 ${cc.matched.length} 플랫폼 제약`);
8524
+ parts.push(t(`🚦 ${cc.matched.length} 플랫폼 제약`, `🚦 ${cc.matched.length} platform constraint(s)`));
8496
8525
  }
8497
8526
  } catch {}
8498
8527
  // 1.9.213 intent classify
@@ -10195,39 +10224,41 @@ function verifyClaimCmd(root, taskId) {
10195
10224
  return;
10196
10225
  }
10197
10226
 
10227
+ // 1.21.2 (UR-0010 Phase 3): verify-claim 출력 UI 언어 적용 (영어 opt-in, 한국어 기본). human 렌더 한정(--json 은 위에서 return).
10228
+ const _L = _uiLang(root); const t = (ko, en) => (_L === 'en' ? en : ko);
10198
10229
  log(`# verify-claim ${taskId} (${path.basename(root)})`);
10199
10230
  log(`Request: ${row.request}`);
10200
10231
  log(`Status: ${row.status} · Updated: ${row.updated}`);
10201
10232
  log(`Evidence: ${evidence.slice(0, 200)}${evidence.length > 200 ? '…' : ''}`);
10202
10233
  log('');
10203
- log(`## 📂 파일 검증 (${files.length}건 주장)`);
10204
- if (!files.length) log(' (evidence에서 파일 경로를 추출하지 못함)');
10234
+ log(t(`## 📂 파일 검증 (${files.length}건 주장)`, `## 📂 File check (${files.length} claimed)`));
10235
+ if (!files.length) log(t(' (evidence에서 파일 경로를 추출하지 못함)', ' (no file paths extracted from evidence)'));
10205
10236
  else {
10206
- for (const c of fileChecks) log(` ${c.exists ? '✓' : '✗'} ${c.file}${c.exists ? '' : ' ← 누락'}`);
10237
+ for (const c of fileChecks) log(` ${c.exists ? '✓' : '✗'} ${c.file}${c.exists ? '' : t(' ← 누락', ' ← missing')}`);
10207
10238
  }
10208
10239
  log('');
10209
- log(`## 🧪 테스트 카운트`);
10210
- if (declaredPass) log(` 주장 (pass): ${declaredPass.num}/${declaredPass.denom}`);
10211
- if (declaredTestCount) log(` 주장 (개수): ${declaredTestCount}개`);
10212
- if (actualTestCount != null) log(` 실측: ${actualTestCount}개 테스트 호출 (${_vcTests.length ? '주장된 테스트 파일' : '관례 탐색: 루트/tests·test_*.py·*.test.*'})`);
10213
- else log(` 실측: 테스트 파일 못 찾음 — 카운트 검증 미수행 (pass 아님)`);
10240
+ log(t(`## 🧪 테스트 카운트`, `## 🧪 Test count`));
10241
+ if (declaredPass) log(t(` 주장 (pass): ${declaredPass.num}/${declaredPass.denom}`, ` claimed (pass): ${declaredPass.num}/${declaredPass.denom}`));
10242
+ if (declaredTestCount) log(t(` 주장 (개수): ${declaredTestCount}개`, ` claimed (count): ${declaredTestCount}`));
10243
+ 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.*'})`));
10244
+ else log(t(` 실측: 테스트 파일 못 찾음 — 카운트 검증 미수행 (pass 아님)`, ` measured: no test file found — count not verified (not a pass)`));
10214
10245
 
10215
10246
  // 1.9.19: --run-tests 결과
10216
10247
  let runTestsOk = true;
10217
10248
  let declaredPassMatchesActual = true;
10218
10249
  if (runResult) {
10219
10250
  log('');
10220
- log(`## 🚦 ${runResult.cmd || '테스트'} 실행 (--run-tests)`);
10251
+ log(`## 🚦 ${runResult.cmd || t('테스트', 'test')} ${t('실행', 'run')} (--run-tests)`);
10221
10252
  if (runResult.skipped) {
10222
10253
  log(` ⚠ skipped: ${runResult.reason}`);
10223
10254
  } else {
10224
10255
  log(` exit: ${runResult.exitCode}`);
10225
- if (runResult.parsed) log(` 실행 결과: ${runResult.parsed.num}/${runResult.parsed.denom} ${runResult.parsed.num === runResult.parsed.denom ? 'passed' : 'partial'}`);
10226
- else log(` (pass/fail 비율을 stdout에서 파싱 못함)`);
10256
+ 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'}`));
10257
+ else log(t(` (pass/fail 비율을 stdout에서 파싱 못함)`, ` (could not parse pass/fail ratio from stdout)`));
10227
10258
  runTestsOk = runResult.allPassed;
10228
10259
  if (declaredPass && runResult.parsed) {
10229
10260
  declaredPassMatchesActual = (runResult.parsed.num === declaredPass.num && runResult.parsed.denom === declaredPass.denom);
10230
- log(` 주장 vs 실행: ${declaredPassMatchesActual ? '✓ 일치' : `⚠ 불일치 (주장 ${declaredPass.num}/${declaredPass.denom} ≠ 실행 ${runResult.parsed.num}/${runResult.parsed.denom})`}`);
10261
+ 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})`}`));
10231
10262
  }
10232
10263
  }
10233
10264
  }
@@ -10237,76 +10268,76 @@ function verifyClaimCmd(root, taskId) {
10237
10268
  log('');
10238
10269
  const allFilesOk = filesAllExist;
10239
10270
  const testOk = declaredTestCount == null || actualTestCount == null || actualTestCount >= declaredTestCount;
10240
- log(`## 종합`);
10241
- log(` - 파일 모두 존재: ${allFilesOk ? '✓ pass' : '✗ FAIL (일부 누락)'}`);
10271
+ log(t(`## 종합`, `## Summary`));
10272
+ log(` - ${t('파일 모두 존재', 'all files exist')}: ${allFilesOk ? '✓ pass' : t('✗ FAIL (일부 누락)', '✗ FAIL (some missing)')}`);
10242
10273
  // 1.17.4 (UR-0047): 측정 불가는 '통과' 가 아니라 '검증 미수행' — 이전엔 실측 0 인데 ✓ pass(실측≥주장) 모순 표기.
10243
- log(` - 테스트 카운트: ${declaredTestCount == null ? '⊘ (주장 없음)' : !testMeasured ? `⊘ 측정 불가 — 주장 ${declaredTestCount}개 검증 미수행 (pass 아님)` : testOk ? '✓ pass (실측 ≥ 주장)' : '⚠ 주장보다 적음'}`);
10274
+ 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')}`);
10244
10275
  if (runResult && !runResult.skipped) {
10245
- log(` - ${runResult.cmd || 'npm test'} 실행: ${runTestsOk ? '✓ all passed' : '✗ FAIL'}`);
10246
- if (declaredPass) log(` - 주장과 실행 결과 일치: ${declaredPassMatchesActual ? '✓ pass' : '⚠ 다름'}`);
10276
+ log(` - ${runResult.cmd || 'npm test'} ${t('실행', 'run')}: ${runTestsOk ? '✓ all passed' : '✗ FAIL'}`);
10277
+ if (declaredPass) log(` - ${t('주장과 실행 결과 일치', 'claimed matches run')}: ${declaredPassMatchesActual ? '✓ pass' : t('⚠ 다름', '⚠ differs')}`);
10247
10278
  }
10248
10279
  // 1.11.2 (UR-0175): optimism+정직성 — done 주장은 기본 게이팅(claimsChecked). 완화: --lenient.
10249
10280
  if (claimsChecked) {
10250
- if (strictOk) log(` - 낙관적 표시 + 정직성 (done 기본): ✓ pass (의심 없음)`);
10281
+ if (strictOk) log(t(` - 낙관적 표시 + 정직성 (done 기본): ✓ pass (의심 없음)`, ` - optimism + honesty (done default): ✓ pass (no suspicion)`));
10251
10282
  else {
10252
- log(` - 낙관적 표시 + 정직성 (done 기본 — 완화: --lenient): ⚠ FAIL (낙관 ${optimismSuspects.length} · 정직성 ${honestyFindings.length})`);
10253
- for (const s of optimismSuspects) log(` · [${s.kind}] ${s.label}: evidence에 주장 있는데 코드에 호출 흔적 없음`);
10254
- for (const f of honestyFindings) log(` · [정직성:${f.dim}] ${f.label}: ${f.detail}`);
10283
+ log(t(` - 낙관적 표시 + 정직성 (done 기본 — 완화: --lenient): ⚠ FAIL (낙관 ${optimismSuspects.length} · 정직성 ${honestyFindings.length})`, ` - optimism + honesty (done default — relax: --lenient): ⚠ FAIL (optimism ${optimismSuspects.length} · honesty ${honestyFindings.length})`));
10284
+ 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`));
10285
+ for (const f of honestyFindings) log(` · [${t('정직성', 'honesty')}:${f.dim}] ${f.label}: ${f.detail}`);
10255
10286
  }
10256
10287
  }
10257
10288
  // 1.9.302 (UR-0042) + 1.11.2 (UR-0175): git diff 시맨틱 교차검증 — 주장 파일이 실제 git 변경에 있는가. gitClaimOk/gitStrongMismatch 는 상단 공유(done 기본 게이팅, --lenient 완화).
10258
10289
  if (gitChanged === null) {
10259
- log(` - git diff 교차검증: ⊘ skip (git repo 아님 — 검증 불가)`);
10290
+ log(` - ${t('git diff 교차검증', 'git diff cross-check')}: ${t('⊘ skip (git repo 아님 — 검증 불가)', '⊘ skip (not a git repo — cannot verify)')}`);
10260
10291
  } else if (!gitApplicable) {
10261
- log(` - git diff 교차검증: ⊘ skip (working tree 변경 0 또는 주장 파일 0 — 이미 커밋됐거나 해당 없음)`);
10292
+ 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)')}`);
10262
10293
  } else {
10263
- log(` - git diff 교차검증: ${gitStrongMismatch ? '⚠ 불일치' : '✓'} 주장 ${files.length}개 중 실제 변경 ${claimedInGit.length}개${claimedNotInGit.length ? ` · git 변경에 없음: ${claimedNotInGit.slice(0, 5).join(', ')}` : ''}`);
10264
- if (gitStrongMismatch) log(` · 주장한 파일이 working tree/직전커밋 변경에 전무 — 변경이 더 오래전 커밋이거나, 실제로 변경 안 됐을 수 있음(허위완료 의심)${has('--strict-claims') ? ' → FAIL' : ' (advisory — 커밋 후 검증 시 정상일 수 있음)'}`);
10294
+ 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(', ')}`) : ''}`);
10295
+ 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)')}`);
10265
10296
  // 1.13.2 (Karpathy 가이드라인 3 "외과적 변경", UR-0030): scope-creep 표면화 — git 변경됐으나 evidence/주장에 없는 파일.
10266
- if (changedNotClaimed.length) log(` · 🔬 외과적 변경 점검: git 에 변경됐으나 evidence/주장에 없는 파일 ${changedNotClaimed.length}건: ${changedNotClaimed.slice(0, 5).join(', ')}${changedNotClaimed.length > 5 ? ' …' : ''} — 요청 범위 밖 변경(scope creep)인지 확인 (advisory)`);
10297
+ 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)`)}`);
10267
10298
  }
10268
10299
  // 1.9.309 (UR-0048): done 주장 evidence 완전성 — 기본 강제(상단 pre-compute). --lenient 로 opt-out.
10269
10300
  if (mustHaveEvidence) {
10270
- log(` - evidence 완전성 (done 기본 강제): ${evidenceQualityOk ? '✓ pass (파일+테스트 근거 있음)' : `✗ FAIL (누락: ${evq.missing.join(', ')})`}`);
10271
- if (!evidenceQualityOk) log(` · done 주장은 수정 파일 경로 + 테스트명/개수 가 evidence 에 있어야 함 (테스트 통과만으로는 불충분). 완화: --lenient`);
10301
+ 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(', ')})`)}`}`);
10302
+ 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')}`);
10272
10303
  }
10273
10304
  // 1.17.3 (UR-0046): 구현 실체(스텁) + 테스트-구현 연결 — Attack C(주석뿐 구현+assert(true)) 차단.
10274
10305
  if (stubFiles.length) {
10275
- log(` - 구현 실체 (done 기본): FAIL — 주장된 구현 파일이 주석/빈껍데기뿐: ${stubFiles.slice(0, 5).join(', ')} (비주석 코드 0줄 또는 빈 export 껍데기)`);
10306
+ 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)`)}`);
10276
10307
  } else if (claimsChecked && _vcImpl.length) {
10277
- log(` - 구현 실체 (done 기본): ✓ pass (주장 구현 파일에 실코드 존재)`);
10308
+ log(` - ${t('구현 실체 (done 기본)', 'implementation substance (done default)')}: ${t('✓ pass (주장 구현 파일에 실코드 존재)', '✓ pass (real code present in claimed impl files)')}`);
10278
10309
  }
10279
10310
  if (testLinkOk === false) {
10280
- log(` - 테스트-구현 연결: 주장된 테스트(${_vcTests.slice(0, 3).join(', ')})가 구현 파일을 참조하지 않음 — 빈 테스트(assert(true)) 의심${has('--strict-claims') ? ' → FAIL' : ' (advisory — --strict-claims 시 FAIL)'}`);
10311
+ 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)')}`);
10281
10312
  } else if (testLinkOk === true && claimsChecked) {
10282
- log(` - 테스트-구현 연결: ✓ pass (테스트가 구현을 참조)`);
10313
+ log(` - ${t('테스트-구현 연결', 'test-impl link')}: ${t('✓ pass (테스트가 구현을 참조)', '✓ pass (test references the impl)')}`);
10283
10314
  }
10284
10315
  const overallFail = !allFilesOk || !testOk || (runResult && !runResult.skipped && !runTestsOk) || (claimsChecked && !strictOk) || !evidenceQualityOk || !gitClaimOk || (claimsChecked && stubFiles.length > 0) || (has('--strict-claims') && testLinkOk === false);
10285
10316
  // 1.9.287: 정직한 한계 고지 — 테스트 통과 ≠ 의미적 구현 정확성
10286
10317
  if (claimsChecked || mustHaveEvidence) {
10287
10318
  log('');
10288
- log(` ℹ 한계: 테스트 통과는 "의미적 구현 정확성"을 보장하지 않음 — evidence 가 해당 주장(수정 파일/테스트)을 직접 링크해야 신뢰도↑.`);
10319
+ log(t(` ℹ 한계: 테스트 통과는 "의미적 구현 정확성"을 보장하지 않음 — evidence 가 해당 주장(수정 파일/테스트)을 직접 링크해야 신뢰도↑.`, ` ℹ Limit: passing tests do not guarantee "semantic correctness" — evidence should directly link the claimed files/tests for higher confidence.`));
10289
10320
  // 1.19.2 (UR-0003 렌즈 완전판 v2): 완료-검증 순간에 분야별 자기질문 advisory — 주장 파일 확장자 기반(결정적).
10290
10321
  // 기계검증(파일/테스트/스텁)을 통과해도 "사람이 보기에 좋은가"는 별개 → AI 가 스스로 답하도록 권장(advisory, 게이트 아님).
10291
10322
  const _lensDoms = _lensDomainsForFiles(files);
10292
10323
  if (_lensDoms.length) {
10293
10324
  const _lensCat = _effectiveLensCatalog(root); // 1.19.3: 프로젝트 커스텀 질문도 포함
10294
10325
  log('');
10295
- log(` 🧭 품질 렌즈 (완료 선언 전 자문 — advisory, 게이트 아님):`);
10326
+ log(t(` 🧭 품질 렌즈 (완료 선언 전 자문 — advisory, 게이트 아님):`, ` 🧭 Quality lens (self-ask before declaring done — advisory, not a gate):`));
10296
10327
  for (const d of _lensDoms) {
10297
10328
  const l = _lensCat[d];
10298
10329
  if (l) log(` · ${d}(${l.title}): ${l.questions[0]}`);
10299
10330
  }
10300
- log(` → 전체 질문: leerness lens ${_lensDoms[0]}`);
10331
+ log(` ${t('→ 전체 질문', '→ full questions')}: leerness lens ${_lensDoms[0]}`);
10301
10332
  }
10302
10333
  }
10303
10334
  if (overallFail) {
10304
10335
  log('');
10305
- log(` ⚠ evidence 주장과 실제가 일치하지 않음 — task 상태 재검토 권장`);
10336
+ log(t(` ⚠ evidence 주장과 실제가 일치하지 않음 — task 상태 재검토 권장`, ` ⚠ evidence claim does not match reality — review the task status`));
10306
10337
  return process.exit(1);
10307
10338
  }
10308
10339
  log('');
10309
- log(` ✓ evidence 주장이 실제 파일·테스트${runResult && !runResult.skipped ? '·실행 결과' : ''}와 일치`);
10340
+ log(t(` ✓ evidence 주장이 실제 파일·테스트${runResult && !runResult.skipped ? '·실행 결과' : ''}와 일치`, ` ✓ evidence claim matches actual files·tests${runResult && !runResult.skipped ? '·run results' : ''}`));
10310
10341
  }
10311
10342
 
10312
10343
  // 1.9.22: orchestrate — Ollama 로컬 LLM으로 best-of-N 멀티 에이전트 시뮬
@@ -12169,7 +12200,7 @@ function llmBenchRecordCmd(root) {
12169
12200
 
12170
12201
  const _sessionClose = require('../lib/session-close');
12171
12202
  // 1.9.425 (UR-0025/UR-0125 큰 핸들러 모듈화 10번째): sessionClose → lib/session-close.js (DI 위임)
12172
- function sessionClose(root, opts = {}) { return _sessionClose.sessionClose(root, opts, { VERSION, STATUSES, MARK, has, arg, harnessPath: __filename, readProgressRows, evidencePath, handoffPath, currentStatePath, taskLogPath, verifyRules, _autoRoadmap, _readUsageStats, readSessionCounter, writeSessionCounter, _retroAggregate, _retroOneLine, retroCmd, _loadDecisions, readRules, planPath, _loadLessons, _readFeatureGraph, _auditUserRequests, _detectDeliveredRequests, _computeRoundHistory, _computeMilestones, _computeRecentChanges, _collectPyFiles, _analyzePyFile, _collectRuntimeEnv, _scanShellScriptsEncoding, _listAPISkills, _matchAPISkills, _loadShellFailures, _shellEnvDrift, _runPreWakeAudit, _saveAndAppendPreWakeReport, _runIdempotencyAudit, _detectAbnormalShutdown, _updateUserRequest, _detectOptimism, _scanCodeForPatterns, _collectSecretFindings }); } // 1.17.6 (UR-0049): 마감 정합 — done 낙관 재확인 + 시크릿 재확인
12203
+ function sessionClose(root, opts = {}) { return _sessionClose.sessionClose(root, opts, { VERSION, STATUSES, MARK, has, arg, uiLang: _uiLang(root), harnessPath: __filename, readProgressRows, evidencePath, handoffPath, currentStatePath, taskLogPath, verifyRules, _autoRoadmap, _readUsageStats, readSessionCounter, writeSessionCounter, _retroAggregate, _retroOneLine, retroCmd, _loadDecisions, readRules, planPath, _loadLessons, _readFeatureGraph, _auditUserRequests, _detectDeliveredRequests, _computeRoundHistory, _computeMilestones, _computeRecentChanges, _collectPyFiles, _analyzePyFile, _collectRuntimeEnv, _scanShellScriptsEncoding, _listAPISkills, _matchAPISkills, _loadShellFailures, _shellEnvDrift, _runPreWakeAudit, _saveAndAppendPreWakeReport, _runIdempotencyAudit, _detectAbnormalShutdown, _updateUserRequest, _detectOptimism, _scanCodeForPatterns, _collectSecretFindings }); } // 1.17.6 (UR-0049): 마감 정합 — done 낙관 재확인 + 시크릿 재확인
12173
12204
 
12174
12205
  function readmeCmd(root) { syncReadme(absRoot(root)); }
12175
12206
  function consistencyCheck(root) {
@@ -11,7 +11,9 @@ const { log, ok, warn, fail, failJson, today, now, absRoot, exists, read, readBu
11
11
  const { _sanitizeFences, _parseArchiveBlocks } = require('./pure-utils');
12
12
 
13
13
  function sessionClose(root, opts = {}, deps = {}) {
14
- const { VERSION, STATUSES, MARK, has, arg, harnessPath, readProgressRows, evidencePath, handoffPath, currentStatePath, taskLogPath, verifyRules, _autoRoadmap, _readUsageStats, readSessionCounter, writeSessionCounter, _retroAggregate, _retroOneLine, retroCmd, _loadDecisions, readRules, planPath, _loadLessons, _readFeatureGraph, _auditUserRequests, _detectDeliveredRequests, _computeRoundHistory, _computeMilestones, _computeRecentChanges, _collectPyFiles, _analyzePyFile, _collectRuntimeEnv, _scanShellScriptsEncoding, _listAPISkills, _matchAPISkills, _loadShellFailures, _shellEnvDrift, _runPreWakeAudit, _saveAndAppendPreWakeReport, _runIdempotencyAudit, _detectAbnormalShutdown, _updateUserRequest, _detectOptimism, _scanCodeForPatterns, _collectSecretFindings } = deps;
14
+ const { VERSION, STATUSES, MARK, has, arg, uiLang, harnessPath, readProgressRows, evidencePath, handoffPath, currentStatePath, taskLogPath, verifyRules, _autoRoadmap, _readUsageStats, readSessionCounter, writeSessionCounter, _retroAggregate, _retroOneLine, retroCmd, _loadDecisions, readRules, planPath, _loadLessons, _readFeatureGraph, _auditUserRequests, _detectDeliveredRequests, _computeRoundHistory, _computeMilestones, _computeRecentChanges, _collectPyFiles, _analyzePyFile, _collectRuntimeEnv, _scanShellScriptsEncoding, _listAPISkills, _matchAPISkills, _loadShellFailures, _shellEnvDrift, _runPreWakeAudit, _saveAndAppendPreWakeReport, _runIdempotencyAudit, _detectAbnormalShutdown, _updateUserRequest, _detectOptimism, _scanCodeForPatterns, _collectSecretFindings } = deps;
15
+ // 1.22.1 (UR-0010 Phase 4): 마감 보고 UI 언어 — 영어 opt-in, 한국어 기본(uiLang 미주입 시 ko).
16
+ const t = (ko, en) => (uiLang === 'en' ? en : ko);
15
17
  root = absRoot(root);
16
18
  // 1.10.4 (13th 버그헌트 P2, UR-0167): 경로 없음/디렉토리 아님 → 구조화 에러 + exit 1. mkdir <path>/.harness ENOTDIR 크래시 & 실패를 성공(exit 0)으로 오판하던 문제 차단.
17
19
  if (!exists(root) || !fs.statSync(root).isDirectory()) { failJson(!!opts.json || has('--json'), 'path_not_found', `경로 없음 또는 디렉토리 아님: ${root}`); return; }
@@ -33,7 +35,7 @@ function sessionClose(root, opts = {}, deps = {}) {
33
35
  // 1.12.3 (14th 버그헌트 P3, UR-0183): 마감 시 완료 정직성 advisory — done 인데 evidence 가 비었거나 placeholder 인 task 노출(차단 X, 정직성 환기). lazy detect 의 done_no_evidence 휴리스틱과 동일.
34
36
  const _doneNoEvidence = (buckets['done'] || []).filter(r => !r.evidence || /^(\s*|user-request|-)$/.test(r.evidence) || /^plan:M-\d{4}\s*$/.test(r.evidence));
35
37
  jsonResult.completionHonesty = { doneTotal: (buckets['done'] || []).length, doneWithoutEvidence: _doneNoEvidence.length, ids: _doneNoEvidence.slice(0, 5).map(r => r.id) };
36
- if (_doneNoEvidence.length) log(` ⚠ 완료 정직성: done ${_doneNoEvidence.length}건 evidence 없음/placeholder (${_doneNoEvidence.slice(0, 3).map(r => r.id).join(', ')}) — verify-claim 권장 (advisory)`);
38
+ if (_doneNoEvidence.length) log(t(` ⚠ 완료 정직성: done ${_doneNoEvidence.length}건 evidence 없음/placeholder (${_doneNoEvidence.slice(0, 3).map(r => r.id).join(', ')}) — verify-claim 권장 (advisory)`, ` ⚠ completion honesty: ${_doneNoEvidence.length} done with no/placeholder evidence (${_doneNoEvidence.slice(0, 3).map(r => r.id).join(', ')}) — verify-claim recommended (advisory)`));
37
39
  // 1.17.6 (UR-0049 마감 정합): done 의 미해소 낙관 의심 재확인 — verify-claim 을 건너뛴 거짓 주장(evidence 에 API/DB 주장 있는데 코드 흔적 없음)이
38
40
  // 평범한 'done' 으로 마감을 무사 통과하던 것(5축 실증 P2: 거짓 DB 주장이 done 으로 마감, gate 실패 중 'clean' 선언) — 마감이 마지막 관문 역할을 하도록 재확인. advisory.
39
41
  let _doneOptimism = [];
@@ -44,12 +46,12 @@ function sessionClose(root, opts = {}, deps = {}) {
44
46
  }
45
47
  } catch {}
46
48
  jsonResult.completionHonesty.optimismUnresolved = _doneOptimism.map(x => ({ id: x.id, kinds: x.suspects.map(s => s.kind) }));
47
- if (_doneOptimism.length) log(` ⚠ 완료 정직성: done ${_doneOptimism.length}건 낙관 의심 미해소 (${_doneOptimism.slice(0, 3).map(x => x.id).join(', ')}) — evidence 주장 vs 코드 흔적 불일치, 마감 전 verify-claim 재확인 권장 (advisory)`);
49
+ if (_doneOptimism.length) log(t(` ⚠ 완료 정직성: done ${_doneOptimism.length}건 낙관 의심 미해소 (${_doneOptimism.slice(0, 3).map(x => x.id).join(', ')}) — evidence 주장 vs 코드 흔적 불일치, 마감 전 verify-claim 재확인 권장 (advisory)`, ` ⚠ completion honesty: ${_doneOptimism.length} done with unresolved optimism (${_doneOptimism.slice(0, 3).map(x => x.id).join(', ')}) — evidence claim vs code trace mismatch, re-check with verify-claim before closing (advisory)`));
48
50
  // 1.17.6 (UR-0049): 마감 보안 재확인 — 커밋 대상 시크릿이 살아있으면 'clean' 으로 마감하지 않도록 표면화. advisory(차단 X).
49
51
  let _closeSecrets = 0;
50
52
  try { if (_collectSecretFindings) _closeSecrets = ((_collectSecretFindings(root) || {}).committed || []).length; } catch {}
51
53
  jsonResult.closeSecurity = { committedSecrets: _closeSecrets };
52
- if (_closeSecrets) log(` 🚨 마감 보안: 커밋 대상 시크릿 ${_closeSecrets}건 미해소 — clean 아님, leerness scan secrets 확인 후 마감 권장`);
54
+ if (_closeSecrets) log(t(` 🚨 마감 보안: 커밋 대상 시크릿 ${_closeSecrets}건 미해소 — clean 아님, leerness scan secrets 확인 후 마감 권장`, ` 🚨 close security: ${_closeSecrets} committed secret(s) unresolved — not clean, run leerness scan secrets before closing`));
53
55
 
54
56
  function rowsToList(arr) {
55
57
  if (!arr || !arr.length) return '- 없음';
@@ -119,7 +121,7 @@ function sessionClose(root, opts = {}, deps = {}) {
119
121
  const ruleResults = verifyRules(root);
120
122
  jsonResult.rules = ruleResults.map(r => ({ id: r.id, trigger: r.trigger, verified: r.verified, note: r.note }));
121
123
  log('\n## ⚡ User Rules verification');
122
- if (!ruleResults.length) log('- 활성 룰 없음');
124
+ if (!ruleResults.length) log(t('- 활성 룰 없음', '- no active rules'));
123
125
  else {
124
126
  log('| ID | Trigger | Rule | Verified | Note |');
125
127
  log('|---|---|---|---|---|');
@@ -127,7 +129,7 @@ function sessionClose(root, opts = {}, deps = {}) {
127
129
  for (const r of ruleResults) log(`| ${r.id} | ${r.trigger} | ${r.rule.slice(0, 40)} | ${ic[r.verified] || '?'} | ${r.note} |`);
128
130
  }
129
131
  log('\n## Required final response sections');
130
- log('- 완료 작업\n- 진행 중 작업\n- 미완료/예정/대기/보류/차단/드랍 작업\n- 검증 결과\n- 추천 방향\n- 다음 정확한 작업\n- ⚡ 활성 룰별 검증 결과');
132
+ log(t('- 완료 작업\n- 진행 중 작업\n- 미완료/예정/대기/보류/차단/드랍 작업\n- 검증 결과\n- 추천 방향\n- 다음 정확한 작업\n- ⚡ 활성 룰별 검증 결과', '- Completed tasks\n- In-progress tasks\n- Incomplete/planned/waiting/on-hold/blocked/dropped tasks\n- Verification results\n- Recommended direction\n- Next exact step\n- ⚡ Per-rule verification results'));
131
133
  ok(`session-handoff.md and current-state.md updated`);
132
134
  // 1.9.12: session close 끝에 roadmap.html 자동 갱신
133
135
  _autoRoadmap(root, 'session-close');
@@ -139,15 +141,15 @@ function sessionClose(root, opts = {}, deps = {}) {
139
141
  const cy = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
140
142
  const dim = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
141
143
  log('');
142
- log(cy('## 💡 다음 라운드 추천 (1.9.57 --suggest)'));
144
+ log(cy(t('## 💡 다음 라운드 추천 (1.9.57 --suggest)', '## 💡 Next-round suggestions (--suggest)')));
143
145
  // 1) skill suggest
144
146
  try {
145
147
  const r = cp.spawnSync(process.execPath, [harnessPath, 'skill', 'suggest', '--path', root, '--min', '3', '--json'],
146
148
  { encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' } });
147
149
  const j = JSON.parse(r.stdout);
148
150
  if (j.candidates && j.candidates.length) {
149
- log(dim(' 📌 신규 skill 후보 (Hermes-style 자동 학습):'));
150
- for (const c of j.candidates.slice(0, 3)) log(` • ${c.keyword} (${c.count}회 등장, 출처: ${c.source})`);
151
+ log(dim(t(' 📌 신규 skill 후보 (Hermes-style 자동 학습):', ' 📌 New skill candidates (Hermes-style auto-learning):')));
152
+ for (const c of j.candidates.slice(0, 3)) log(t(` • ${c.keyword} (${c.count}회 등장, 출처: ${c.source})`, ` • ${c.keyword} (seen ${c.count}x, source: ${c.source})`));
151
153
  jsonResult.skillCandidates = j.candidates.slice(0, 5);
152
154
  }
153
155
  } catch {}
@@ -157,8 +159,8 @@ function sessionClose(root, opts = {}, deps = {}) {
157
159
  { encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '0' } });
158
160
  const j = JSON.parse(r.stdout.trim());
159
161
  if (j.level) {
160
- log(dim(` 🩺 drift 상태: ${j.level} ${j.score}/200`));
161
- if (j.fired && j.fired.length) log(dim(` 🔥 ${j.fired.length}건 임계 초과 — \`leerness drift check\` 상세`));
162
+ log(dim(t(` 🩺 drift 상태: ${j.level} ${j.score}/200`, ` 🩺 drift status: ${j.level} ${j.score}/200`)));
163
+ if (j.fired && j.fired.length) log(dim(t(` 🔥 ${j.fired.length}건 임계 초과 — \`leerness drift check\` 상세`, ` 🔥 ${j.fired.length} over threshold — \`leerness drift check\` for details`)));
162
164
  jsonResult.drift = { level: j.level, score: j.score, fired: (j.fired || []).map(f => ({ label: f.label, weight: f.weight })) };
163
165
  }
164
166
  } catch {}
@@ -167,7 +169,7 @@ function sessionClose(root, opts = {}, deps = {}) {
167
169
  const stats = _readUsageStats(root);
168
170
  const entries = Object.entries(stats.commands || {}).sort((a, b) => b[1] - a[1]).slice(0, 3);
169
171
  if (entries.length) {
170
- log(dim(` 📊 가장 많이 쓴 명령: ${entries.map(([c, n]) => `${c}(${n})`).join(', ')}`));
172
+ log(dim(t(` 📊 가장 많이 쓴 명령: ${entries.map(([c, n]) => `${c}(${n})`).join(', ')}`, ` 📊 Most-used commands: ${entries.map(([c, n]) => `${c}(${n})`).join(', ')}`)));
171
173
  jsonResult.topCommands = entries.map(([command, count]) => ({ command, count }));
172
174
  }
173
175
  // 1.9.74: MCP tools/call 통계 + rare 도구 노출
@@ -175,10 +177,10 @@ function sessionClose(root, opts = {}, deps = {}) {
175
177
  const mcpEntries = Object.entries(stats.mcp.tools).sort((a, b) => b[1] - a[1]);
176
178
  if (mcpEntries.length) {
177
179
  const mcpTotal = mcpEntries.reduce((s, [, n]) => s + n, 0);
178
- log(dim(` 🔌 MCP 호출 (1.9.74): 총 ${mcpTotal}회, top: ${mcpEntries.slice(0, 3).map(([t, n]) => `${t}(${n})`).join(', ')}`));
180
+ log(dim(t(` 🔌 MCP 호출 (1.9.74): 총 ${mcpTotal}회, top: ${mcpEntries.slice(0, 3).map(([tool, n]) => `${tool}(${n})`).join(', ')}`, ` 🔌 MCP calls: ${mcpTotal} total, top: ${mcpEntries.slice(0, 3).map(([tool, n]) => `${tool}(${n})`).join(', ')}`)));
179
181
  const threshold = Math.max(1, Math.floor(mcpTotal * 0.05));
180
- const rare = mcpEntries.filter(([, n]) => n <= threshold).map(([t]) => t);
181
- if (rare.length && mcpTotal >= 5) log(dim(` 💡 드물게 호출된 MCP: ${rare.slice(0, 4).join(', ')}`));
182
+ const rare = mcpEntries.filter(([, n]) => n <= threshold).map(([tool]) => tool);
183
+ if (rare.length && mcpTotal >= 5) log(dim(t(` 💡 드물게 호출된 MCP: ${rare.slice(0, 4).join(', ')}`, ` 💡 Rarely-called MCP: ${rare.slice(0, 4).join(', ')}`)));
182
184
  jsonResult.mcpStats = { total: mcpTotal, top: mcpEntries.slice(0, 5).map(([tool, count]) => ({ tool, count })), rare: rare.slice(0, 10) };
183
185
  }
184
186
  }
@@ -198,8 +200,8 @@ function sessionClose(root, opts = {}, deps = {}) {
198
200
  const counts = {};
199
201
  for (const q of queries) counts[q] = (counts[q] || 0) + 1;
200
202
  const topQueries = Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 3);
201
- log(dim(` 📒 skill match query 누적 (1.9.74): 총 ${queries.length}회 / 종류 ${Object.keys(counts).length}개`));
202
- for (const [q, n] of topQueries) log(dim(` • "${q.slice(0, 50)}"${n > 1 ? ` (${n}회)` : ''}`));
203
+ log(dim(t(` 📒 skill match query 누적 (1.9.74): 총 ${queries.length}회 / 종류 ${Object.keys(counts).length}개`, ` 📒 skill match queries: ${queries.length} total / ${Object.keys(counts).length} distinct`)));
204
+ for (const [q, n] of topQueries) log(dim(` • "${q.slice(0, 50)}"${n > 1 ? t(` (${n}회)`, ` (${n}x)`) : ''}`));
203
205
  }
204
206
  }
205
207
  } catch {}
@@ -212,7 +214,7 @@ function sessionClose(root, opts = {}, deps = {}) {
212
214
  sc.lastCloseAt = now();
213
215
  writeSessionCounter(root, sc);
214
216
  const agg = _retroAggregate(root);
215
- log(`\n## 📈 진행 요약 (session #${sc.count})`);
217
+ log(t(`\n## 📈 진행 요약 (session #${sc.count})`, `\n## 📈 Progress summary (session #${sc.count})`));
216
218
  log(` ${_retroOneLine(agg)}`);
217
219
  // 1.9.132: archive 활동 1줄 요약 — 마감 시점에 DELETE 활동 가시화 (handoff 7번째 회수와 symmetric)
218
220
  try {
@@ -227,17 +229,17 @@ function sessionClose(root, opts = {}, deps = {}) {
227
229
  }
228
230
  }
229
231
  if (arc.total > 0) {
230
- log(` 🗑 archive 누적: D${arc.d}/L${arc.l}/P${arc.p} (${arc.total}건) — 복원 후보: leerness memory archive list`);
232
+ log(t(` 🗑 archive 누적: D${arc.d}/L${arc.l}/P${arc.p} (${arc.total}건) — 복원 후보: leerness memory archive list`, ` 🗑 archive total: D${arc.d}/L${arc.l}/P${arc.p} (${arc.total}) — restore via: leerness memory archive list`));
231
233
  }
232
234
  } catch {}
233
235
  if (sc.count % 5 === 0) {
234
- log(`\n## 🔄 ${sc.count}세션 마일스톤 — 자동 회고 (5세션마다)`);
236
+ log(t(`\n## 🔄 ${sc.count}세션 마일스톤 — 자동 회고 (5세션마다)`, `\n## 🔄 ${sc.count}-session milestone — auto retro (every 5 sessions)`));
235
237
  retroCmd(root);
236
238
  sc.lastDeepRetroAt = now();
237
239
  writeSessionCounter(root, sc);
238
240
  } else {
239
241
  const left = 5 - (sc.count % 5);
240
- log(` 💡 ${left}세션 후 자동 깊은 회고 — \`leerness retro\`로 즉시 실행 가능`);
242
+ log(t(` 💡 ${left}세션 후 자동 깊은 회고 — \`leerness retro\`로 즉시 실행 가능`, ` 💡 deep retro in ${left} session(s) — run \`leerness retro\` now to trigger immediately`));
241
243
  }
242
244
  // 1.9.16: 워크스페이스 안내 (다른 leerness 프로젝트가 있으면)
243
245
  try {
@@ -253,11 +255,11 @@ function sessionClose(root, opts = {}, deps = {}) {
253
255
  } catch {}
254
256
  }
255
257
  }
256
- if (wsCount > 0) log(` 🌐 워크스페이스에 ${wsCount}개 다른 leerness 프로젝트 — \`leerness retro --all-apps\`로 통합 회고`);
258
+ if (wsCount > 0) log(t(` 🌐 워크스페이스에 ${wsCount}개 다른 leerness 프로젝트 — \`leerness retro --all-apps\`로 통합 회고`, ` 🌐 ${wsCount} other leerness project(s) in workspace — \`leerness retro --all-apps\` for combined retro`));
257
259
  jsonResult.workspacePeers = wsCount;
258
260
  } catch {}
259
261
  } catch (e) {
260
- warn('retro 요약 실패: ' + (e && e.message ? e.message : e));
262
+ warn(t('retro 요약 실패: ', 'retro summary failed: ') + (e && e.message ? e.message : e));
261
263
  jsonResult.retroSummaryError = e && e.message ? e.message : String(e);
262
264
  }
263
265
  } finally {
@@ -481,7 +483,7 @@ function sessionClose(root, opts = {}, deps = {}) {
481
483
  const dim = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
482
484
 
483
485
  log('');
484
- log(`## 🔚 session close 자동 통합 보고 (1.9.217)`);
486
+ log(t(`## 🔚 session close 자동 통합 보고 (1.9.217)`, `## 🔚 session close integrated report`));
485
487
  // 1.9.207 + 1.9.223 (delivered 패턴 자동 권장) + 1.9.224 (--auto-apply-delivered 옵션)
486
488
  try {
487
489
  const reqAudit = _auditUserRequests(root);
@@ -496,17 +498,17 @@ function sessionClose(root, opts = {}, deps = {}) {
496
498
  const u = _updateUserRequest(root, c.id, { status: 'completed', autoCompletedAt: new Date().toISOString(), autoCompleteReason: 'session-close-auto-apply-1.9.224' });
497
499
  if (u) ok++;
498
500
  }
499
- log(grn(` ✓ delivered 패턴 ${ok}건 자동 완료 (--auto-apply-delivered 1.9.224)`));
501
+ log(grn(t(` ✓ delivered 패턴 ${ok}건 자동 완료 (--auto-apply-delivered 1.9.224)`, ` ✓ ${ok} delivered pattern(s) auto-completed (--auto-apply-delivered)`)));
500
502
  } else {
501
- log(yel(` 📥 delivered 패턴 ${delivered.candidates.length}건 (1.9.223) — 자동 완료 가능`));
502
- log(dim(` → leerness requests auto-complete --apply (수동) 또는 session close --auto-apply-delivered (1.9.224)`));
503
+ log(yel(t(` 📥 delivered 패턴 ${delivered.candidates.length}건 (1.9.223) — 자동 완료 가능`, ` 📥 ${delivered.candidates.length} delivered pattern(s) — auto-completable`)));
504
+ log(dim(t(` → leerness requests auto-complete --apply (수동) 또는 session close --auto-apply-delivered (1.9.224)`, ` → leerness requests auto-complete --apply (manual) or session close --auto-apply-delivered`)));
503
505
  }
504
506
  } else if (missCnt > 0) {
505
- log(red(` ⚠ 미답 사용자 요청 ${missCnt}건 (task-log/plan/decisions 매칭 안 됨)`));
507
+ log(red(t(` ⚠ 미답 사용자 요청 ${missCnt}건 (task-log/plan/decisions 매칭 안 됨)`, ` ⚠ ${missCnt} unanswered user request(s) (no task-log/plan/decisions match)`)));
506
508
  } else if (reqAudit.open > 0) {
507
- log(grn(` ✓ 사용자 요청 ${reqAudit.open}건 모두 tracked`));
509
+ log(grn(t(` ✓ 사용자 요청 ${reqAudit.open}건 모두 tracked`, ` ✓ all ${reqAudit.open} user request(s) tracked`)));
508
510
  } else {
509
- log(dim(` ℹ 사용자 요청 없음 (UR 백로그 비어있음)`));
511
+ log(dim(t(` ℹ 사용자 요청 없음 (UR 백로그 비어있음)`, ` ℹ no user requests (UR backlog empty)`)));
510
512
  }
511
513
  } catch {}
512
514
  // 1.9.209
@@ -516,11 +518,11 @@ function sessionClose(root, opts = {}, deps = {}) {
516
518
  _saveAndAppendPreWakeReport(root, audit);
517
519
  const sum = audit.summary;
518
520
  if (sum.criticalCount > 0) {
519
- log(red(` 🚨 pre-wake-audit: critical ${sum.criticalCount} (다음 깨어남 시 점검 필요)`));
521
+ log(red(t(` 🚨 pre-wake-audit: critical ${sum.criticalCount} (다음 깨어남 시 점검 필요)`, ` 🚨 pre-wake-audit: critical ${sum.criticalCount} (check before next wake)`)));
520
522
  } else if (sum.warningCount > 0) {
521
523
  log(yel(` ⚠ pre-wake-audit: warning ${sum.warningCount}`));
522
524
  } else {
523
- log(grn(` ✓ pre-wake-audit: clean (sleep 안전)`));
525
+ log(grn(t(` ✓ pre-wake-audit: clean (sleep 안전)`, ` ✓ pre-wake-audit: clean (safe to sleep)`)));
524
526
  }
525
527
  }
526
528
  } catch {}
@@ -529,10 +531,10 @@ function sessionClose(root, opts = {}, deps = {}) {
529
531
  const idemp = _runIdempotencyAudit(root);
530
532
  const v = idemp.summary.totalViolations;
531
533
  if (v > 0) {
532
- log(red(` ⚠ 멱등성 위반 ${v}건 (high: ${idemp.summary.highSeverity})`));
533
- log(dim(` → leerness idempotency audit 으로 상세 확인`));
534
+ log(red(t(` ⚠ 멱등성 위반 ${v}건 (high: ${idemp.summary.highSeverity})`, ` ⚠ ${v} idempotency violation(s) (high: ${idemp.summary.highSeverity})`)));
535
+ log(dim(t(` → leerness idempotency audit 으로 상세 확인`, ` → see details with leerness idempotency audit`)));
534
536
  } else {
535
- log(grn(` ✓ 멱등성 검사 통과 — verified ${idemp.summary.verifiedAreas} 영역`));
537
+ log(grn(t(` ✓ 멱등성 검사 통과 — verified ${idemp.summary.verifiedAreas} 영역`, ` ✓ idempotency check passed — ${idemp.summary.verifiedAreas} area(s) verified`)));
536
538
  }
537
539
  } catch {}
538
540
  // 1.9.264: 셸 실패 메모리 + 환경 변동 요약 (UR-0020) — 마감 시 이번 세션 셸 실패를 회고에 노출
@@ -541,13 +543,13 @@ function sessionClose(root, opts = {}, deps = {}) {
541
543
  const drift = _shellEnvDrift(root);
542
544
  const driftN = drift && drift.changes ? drift.changes.length : 0;
543
545
  if (sf.failures.length > 0 || driftN > 0) {
544
- if (driftN > 0) log(yel(` ⚠ 환경 버전 변동 ${driftN}건 — 다음 세션 셸 실패 기록 재검토 권장`));
546
+ if (driftN > 0) log(yel(t(` ⚠ 환경 버전 변동 ${driftN}건 — 다음 세션 셸 실패 기록 재검토 권장`, ` ⚠ ${driftN} environment version change(s) — review shell-failure log next session`)));
545
547
  if (sf.failures.length > 0) {
546
- log(yel(` 🐚 셸 실패 누적 ${sf.failures.length}건 — 다음 handoff 가 자동 노출`));
547
- log(dim(` → 명령 실행 전 점검: leerness shell-guard "<command>"`));
548
+ log(yel(t(` 🐚 셸 실패 누적 ${sf.failures.length}건 — 다음 handoff 가 자동 노출`, ` 🐚 ${sf.failures.length} accumulated shell failure(s) — surfaced by next handoff`)));
549
+ log(dim(t(` → 명령 실행 전 점검: leerness shell-guard "<command>"`, ` → check before running: leerness shell-guard "<command>"`)));
548
550
  }
549
551
  } else {
550
- log(grn(` ✓ 셸 실패 기록 없음 (터미널 호환성 양호)`));
552
+ log(grn(t(` ✓ 셸 실패 기록 없음 (터미널 호환성 양호)`, ` ✓ no shell-failure records (terminal compatibility OK)`)));
551
553
  }
552
554
  } catch {}
553
555
  // 1.9.237: session close --auto-cleanup-branches — 50+ release/* branches 시 자동 정리
@@ -575,11 +577,11 @@ function sessionClose(root, opts = {}, deps = {}) {
575
577
  const r = cp.spawnSync('git', ['branch', '-d', b], { cwd: root, encoding: 'utf8' });
576
578
  if (r.status === 0) okCnt++;
577
579
  }
578
- log(grn(` ✓ release 정리 ${okCnt}/${toDelete.length}건 (--auto-cleanup-branches 1.9.237, keep 10)`));
580
+ log(grn(t(` ✓ release 정리 ${okCnt}/${toDelete.length}건 (--auto-cleanup-branches 1.9.237, keep 10)`, ` ✓ release cleanup ${okCnt}/${toDelete.length} (--auto-cleanup-branches, keep 10)`)));
579
581
  } else {
580
- log(yel(` 🗑 release/* merged ${merged.length}개 (50+) — cleanup 가능 (1.9.235)`));
581
- log(dim(` → leerness release cleanup --apply --keep 10 (수동)`));
582
- log(dim(` → 또는 session close --auto-cleanup-branches (1.9.237 자동)`));
582
+ log(yel(t(` 🗑 release/* merged ${merged.length}개 (50+) — cleanup 가능 (1.9.235)`, ` 🗑 ${merged.length} merged release/* branches (50+) — cleanup available`)));
583
+ log(dim(t(` → leerness release cleanup --apply --keep 10 (수동)`, ` → leerness release cleanup --apply --keep 10 (manual)`)));
584
+ log(dim(t(` → 또는 session close --auto-cleanup-branches (1.9.237 자동)`, ` → or session close --auto-cleanup-branches (auto)`)));
583
585
  }
584
586
  }
585
587
  }
@@ -602,10 +604,10 @@ function sessionClose(root, opts = {}, deps = {}) {
602
604
  ok++;
603
605
  } catch {}
604
606
  }
605
- log(grn(` ✓ 인코딩 위험 ${ok}/${encScan.atRisk.length}건 UTF-8 BOM 자동 추가 (--auto-fix-encoding 1.9.243)`));
607
+ log(grn(t(` ✓ 인코딩 위험 ${ok}/${encScan.atRisk.length}건 UTF-8 BOM 자동 추가 (--auto-fix-encoding 1.9.243)`, ` ✓ ${ok}/${encScan.atRisk.length} encoding risk(s) auto-fixed with UTF-8 BOM (--auto-fix-encoding)`)));
606
608
  } else {
607
- log(yel(` ⚠ 셸 스크립트 인코딩 위험 ${encScan.atRisk.length}건 (1.9.241) — 자동 회복 가능`));
608
- log(dim(` → leerness env encoding --apply (수동) 또는 session close --auto-fix-encoding (1.9.243 자동)`));
609
+ log(yel(t(` ⚠ 셸 스크립트 인코딩 위험 ${encScan.atRisk.length}건 (1.9.241) — 자동 회복 가능`, ` ⚠ ${encScan.atRisk.length} shell-script encoding risk(s) — auto-fixable`)));
610
+ log(dim(t(` → leerness env encoding --apply (수동) 또는 session close --auto-fix-encoding (1.9.243 자동)`, ` → leerness env encoding --apply (manual) or session close --auto-fix-encoding (auto)`)));
609
611
  }
610
612
  }
611
613
  } catch {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.21.0",
3
+ "version": "1.23.0",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",