leerness 1.29.0 → 1.30.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,97 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.30.0 — 2026-06-16 — 🛡️ [안정화/Stable] handoff 본문 i18n 4종 안정 minor
4
+
5
+ **🛡️ 안정화(Stable) minor — handoff 본문 4블록 영어화 + i18n-coupling 감사 + 블록-스코프 t 회귀수정을 npm 공개.** 직전 minor(1.29.0) 이후 누적된 패치 4건(1.29.1~1.29.4)을 검증·통합해 배포. R-0011 정책의 21번째 stable minor. 한국어 우선 기본은 그대로.
6
+
7
+ ### 이번 minor 통합 (1.29.1~1.29.4)
8
+ - **🔒🌐 보안 요약 섹션 영어화 + i18n-coupling 감사 (1.29.1)**: 영어 사용자가 커밋된 시크릿을 가질 때 정확히 노출되는 handoff 보안 요약 섹션(`## 🔒 보안 요약` / issues / `🚨 CRITICAL` / 자동회복 / `💡 자동 실행 옵션`)을 영어화. lib/ 전수로 1.28.1 `hasSecurityFired` 류 라벨-결합 버그 추가 탐색 → 0건. **블록-스코프 t 회귀 자체수정**: headline `t()` 스코프 밖이라 `ReferenceError → try 삼킴 → 섹션 증발` 회귀를 심었다가 **행위 검증**으로 적발(소스가드는 통과)·수정.
9
+ - **🌐 env-detect 블록 영어화 (1.29.2)**: `🖥 실행 환경` PATH 누락/변동 감지 + `→ 상세` 안내 영어화.
10
+ - **🌐 shell-guard 블록 영어화 (1.29.3)**: `🐚 터미널 셸 가드` 헤더 + 환경 버전 변동 재검토 + 최근 셸 실패 + 명령 실행 전 점검 영어화.
11
+ - **🌐 CLI 에이전트 슬래시 블록 영어화 (1.29.4)**: `🤖 CLI 에이전트 슬래시 명령` 헤더 + 활성 에이전트 + (하위명령) + 전체/기록/최신화 영어화.
12
+ - **공통**: 4블록 모두 headline `t()` 스코프 밖 → 블록 로컬 `t()`/`_uiLang(root)` 정의(1.29.1 교훈 적용). 한국어 verbatim 보존.
13
+
14
+ ### 핵심 교훈 (defense-in-depth)
15
+ - **소스가드(문자열 존재) ≠ 동작**: 번역 헬퍼를 다른 섹션의 블록 스코프에 두면 `ReferenceError` 가 `try/catch` 에 삼켜져 표면 전체가 (양 언어 모두) 사라진다. selftest 소스가드는 통과하지만 런타임 출력이 0. **행위 검증 + e2e 가드**로만 잡힌다.
16
+
17
+ ### 검증 (회귀 0)
18
+ - **selftest 250→254** (4블록 영어/한국어 보존 소스가드, split-literal 로 self-reference 회피).
19
+ - **E2E 368/368** — i18n 행위가드 **⑧~⑪ 신규**: 보안요약(.env+미흡 gitignore) / env-detect(스냅샷 변동) / shell-guard(셸실패+스냅샷변동) / agent-slash(env flag 활성) 4 시나리오를 실제 발동시켜 en 영어(블록 한글 0, Node 탐지) + ko 보존 검증.
20
+ - minor(1.30.0) — npm 배포(R-0011 stable) + annotated tag(Stable) + GitHub release(latest) + 게시본 클린룸 재실증.
21
+
22
+ ### 잔여 (UR-0010 백로그)
23
+ - capabilities/commands/constraints/install-safety 영어화 · init en seed 템플릿 i18n · handoff 본문 잔여 표면(팀 스케줄/요청 자동완료/비정상종료) 점진 영어화.
24
+
25
+ ## 1.29.4 — 2026-06-16 — handoff CLI 에이전트 슬래시 명령 블록 영어화 (UR-0010)
26
+
27
+ **🌐 handoff CLI 에이전트 슬래시 명령 블록을 영어화.** 1.29.3 잔여 백로그. 외부 CLI 에이전트(codex/claude/agy/copilot/ollama)가 env flag 로 활성일 때 노출되는 sub-agent 슬래시 명령 요약을 `--language en` 에서 영어로. 한국어 기본 보존.
28
+
29
+ ### 변경 (UR-0010)
30
+ - **🌐 agent-slash 블록 4줄 영어화**: `## 🤖 CLI agent slash commands (UR-0021)` 헤더 + `N active agent(s) — use each one's slash commands when invoking a sub-agent:` + ` (subcommand)` 접미사 + `→ full list / record / refresh: leerness slash-commands [agent] [--record]`. 에이전트별 명령 라인(`id cmd…`)은 데이터라 무변경. ko verbatim 보존.
31
+ - **블록-스코프 t 함정 회피(1.29.1 교훈)**: 이 블록도 headline `t()` 스코프 밖 → 블록 로컬 `t()`/`_uiLang(root)` 정의.
32
+
33
+ ### 검증 (회귀 0)
34
+ - **selftest 253→254** (agent-slash 영어/한국어 보존 소스가드, split-literal).
35
+ - **행위 검증(1.29.1 교훈)**: `LEERNESS_ENABLE_CODEX=1`+`LEERNESS_ENABLE_CLAUDE=1` 로 블록 발동 → `handoff --language en` 영어 렌더(블록 라인 한글 0, Node 탐지) / `handoff` ko 보존.
36
+ - **E2E i18n 행위가드 ⑪ 추가**: agent-slash en/ko 를 env flag 활성 시나리오로 e2e 에 못박음.
37
+ - patch(1.29.4) — npm 미배포(R-0011, GitHub/CHANGELOG 누적).
38
+
39
+ ### 잔여 (UR-0010 백로그)
40
+ - capabilities/commands/constraints/install-safety 영어화 · init en seed 템플릿 i18n · handoff 본문 잔여 표면(팀 스케줄/요청 자동완료/비정상종료 등) 점진 영어화.
41
+
42
+ ## 1.29.3 — 2026-06-16 — handoff shell-guard 블록 영어화 (UR-0010)
43
+
44
+ **🌐 handoff 터미널 셸 가드 블록을 영어화.** 1.29.2 probe 에서 관측한 인접 en-leak. 과거 셸 실패(예: PowerShell 5.1 `&&` 미지원) + 환경 버전 변동 알림을 `--language en` 에서 영어로. 한국어 기본 보존.
45
+
46
+ ### 변경 (UR-0010)
47
+ - **🌐 shell-guard 블록 4줄 영어화**: `## 🐚 Terminal shell guard (UR-0020)` 헤더 + `⚠ Environment version changed — review past shell failures:` + `N recent shell failure(s) (showing up to 3):` + `→ check before running a command: leerness shell-guard "<command>"`. data 라인(`what: from → to`, `cmd (exit=…, shell)`)은 이미 영어라 무변경. ko verbatim 보존.
48
+ - **블록-스코프 t 함정 회피(1.29.1 교훈)**: 이 블록도 headline `t()` 스코프 밖 → 블록 로컬 `t()`/`_uiLang(root)` 정의하고 영어화(회귀 사전 차단).
49
+
50
+ ### 검증 (회귀 0)
51
+ - **selftest 252→253** (shell-guard 영어/한국어 보존 소스가드, split-literal).
52
+ - **행위 검증(1.29.1 교훈)**: `.harness/shell-failures.json` 시드 + 스냅샷 변동으로 hasFailures/hasDrift 양쪽 발동 → `handoff --language en` 4줄 영어 렌더(블록 라인 한글 0, Node 탐지) / `handoff` ko 보존.
53
+ - **E2E i18n 행위가드 ⑩ 추가**: shell-guard en/ko 를 셸실패+스냅샷변동 시나리오로 e2e 에 못박음.
54
+ - patch(1.29.3) — npm 미배포(R-0011, GitHub/CHANGELOG 누적).
55
+
56
+ ### 잔여 (UR-0010 백로그)
57
+ - handoff CLI 에이전트 슬래시 명령 블록(`## 🤖 CLI 에이전트 슬래시 명령`/`활성 에이전트 N개`) en-leak(외부 에이전트 활성 시 노출, 다음 라운드) · capabilities/commands/constraints/install-safety 영어화 · init en seed 템플릿 i18n.
58
+
59
+ ## 1.29.2 — 2026-06-16 — handoff env-detect 블록 영어화 (UR-0010)
60
+
61
+ **🌐 handoff 실행 환경 변동/PATH 누락 알림을 영어화.** 1.29.1 감사 중 발견한 인접 en-leak. `🖥 실행 환경` 헤더 2종(PATH 누락 / 변동 감지) + `→ 상세` 안내를 `--language en` 에서 영어로. 변동 설명(`os.platform`/`node`/`tool added` 등)은 이미 영어라 source 무변경. 한국어 기본 보존.
62
+
63
+ ### 변경 (UR-0010)
64
+ - **🌐 env-detect 블록 3줄 영어화**: `🖥 Runtime environment: ⚠ N PATH tool(s) missing — npm run may fail` / `🖥 Runtime environment: N change(s) detected` / `→ details: leerness env detect . --json`. ko verbatim 보존.
65
+ - **블록-스코프 t 함정 회피(1.29.1 교훈 적용)**: env-detect 블록도 headline 의 `t()` 스코프 밖이라, 처음부터 블록 로컬 `t()`/`_uiLang(root)` 를 정의하고 영어화 — `ReferenceError → try 삼킴 → 블록 증발` 회귀를 사전 차단.
66
+
67
+ ### 검증 (회귀 0)
68
+ - **selftest 251→252** (env-detect 영어/한국어 보존 소스가드, split-literal).
69
+ - **행위 검증(1.29.1 교훈)**: 스냅샷 `node.version` 인위 변경으로 블록 강제 발동 → `handoff --language en` 이 영어 렌더(블록 라인 한글 0, Node 탐지) / `handoff` ko 보존 확인.
70
+ - **E2E i18n 행위가드 ⑨ 추가**: env-detect en/ko 를 스냅샷 변동 시나리오로 e2e 에 못박음.
71
+ - patch(1.29.2) — npm 미배포(R-0011, GitHub/CHANGELOG 누적).
72
+
73
+ ### 잔여 (UR-0010 백로그)
74
+ - handoff shell-failures 재검토 알림(`⚠ 환경 버전 변동 — 과거 셸 실패 기록 재검토 권장`) en-leak(env 변동 시 동반 노출, 다음 라운드) · capabilities/commands/constraints/install-safety 영어화 · init en seed 템플릿 i18n.
75
+
76
+ ## 1.29.1 — 2026-06-16 — i18n-coupling 감사(clean) + handoff 보안 요약 섹션 영어화 (UR-0010)
77
+
78
+ **🔒🌐 영어 사용자가 커밋된 시크릿을 가질 때 정확히 노출되는 handoff 보안 요약 섹션을 영어화.** 1.28.1 의 `hasSecurityFired` 라벨-결합 버그 클래스를 lib/ 전수 감사(추가 결합 버그 0)한 뒤, 감사 중 발견한 마지막 en-leak(보안 요약 본문)을 영어화. 한국어 우선 기본 보존.
79
+
80
+ ### 감사 (i18n-coupling, clean)
81
+ - **lib/ 전수**: 번역된 라벨에 내부 로직이 string-match 로 결합된 곳(1.28.1 류) 추가 탐색 → **0 건**. drift `level === '🔴 critical'` 은 언어-안정(미번역), analyzer 조건은 bilingual-by-design, audit/pure-utils 한국어 조건은 미번역 템플릿 매칭, agents 조건은 안정 status 필드. 교차명령(handoff en headline 이 내부 ko-spawn `--json` 을 안정 필드로 파싱)도 견고.
82
+
83
+ ### 변경 (UR-0010)
84
+ - **🌐 handoff 보안 요약 섹션 영어화**: `## 🔒 보안 요약` 헤더 + 2 issue(`.env→.env.example` / `.gitignore` 시크릿 누락) + `자동 수정` 안내 + `🚨 CRITICAL` + 자동 회복 3줄 + `💡 자동 실행 옵션` 을 `t(ko,en)` 으로 영어 opt-in. 한국어 verbatim 보존.
85
+ - **🐛 회귀 자체수정(같은 라운드)**: 영어화 첫 시도에서 이 블록이 headline 의 `t()` 스코프 밖이라 `t` 가 미정의 → `ReferenceError` 가 블록의 `try`/`catch` 에 **삼켜져 보안 요약 섹션 전체가 (양 언어 모두) 사라지는** 회귀를 심었다. 소스가드(문자열 존재)는 통과했지만 **행위 검증**에서 적발(맹신 X) → 블록 로컬 `t()` 정의로 수정. 같은 함정의 [[lesson-selftest-self-reference-trap]] / [[lesson-reverify-published-artifact]] 계열.
86
+
87
+ ### 검증 (회귀 0)
88
+ - **selftest 250→251** (보안 요약 영어/한국어 보존 소스가드, split-literal 로 self-reference 회피) · **행위**: `.env`+미흡 `.gitignore` 시나리오에서 `handoff --language en` 보안 요약 영어 렌더(섹션 라인 한글 0, Node 탐지) / `handoff` ko 보존.
89
+ - **E2E i18n 행위가드 ⑧ 추가**: handoff 보안 요약 en/ko 를 e2e 로 못박음 — 소스가드가 못 잡는 "블록-스코프 t 누락 → 섹션 증발" 회귀를 행위로 차단(defense-in-depth).
90
+ - patch(1.29.1) — npm 미배포(R-0011, GitHub/CHANGELOG 누적).
91
+
92
+ ### 잔여 (UR-0010 백로그)
93
+ - handoff env-detect 블록(`🖥 실행 환경`/`→ 상세: leerness env detect`) en-leak(다음 라운드) · capabilities/commands/constraints/install-safety 영어화 · init en seed 템플릿 i18n.
94
+
3
95
  ## 1.29.0 — 2026-06-16 — 🛡️ [안정화/Stable] drift auto-fix·진단 명령 영어화 안정 minor
4
96
 
5
97
  **🛡️ 안정화(Stable) minor — drift 완전 영어화(버그수정 포함) + 진단 3종 영어화를 npm 공개.** 직전 minor(1.28.0) 이후 누적된 패치 2건(1.28.1 + 1.28.2)을 검증·통합해 배포. R-0011 정책의 20번째 stable minor. 한국어 우선 기본은 그대로.
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.29.0 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
107
+ 이 프로젝트는 Leerness v1.30.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.29.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
161
+ Leerness v1.30.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.29.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.29.0)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
182
+ 현재 누적: **70 라운드 (1.9.40 → 1.30.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.29.0: 2026-06-16
220
+ Last synced by Leerness v1.30.0: 2026-06-16
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.29.0';
35
+ const VERSION = '1.30.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') 시 호스트 프로세스 오염.
@@ -3851,6 +3851,46 @@ function _selfTestCases() {
3851
3851
  const koPreserved = dg.includes('설치/환경 진단') && dg.includes('## npm 환경') && dg.includes('버전별 헤드라인'); // ko 인자 보존(내부호출/e2e)
3852
3852
  return injected && en && koPreserved;
3853
3853
  } },
3854
+ { name: 'Phase 10d (1.29.1): handoff 보안 요약 섹션 영어/한국어 보존 (소스 가드)', run: () => {
3855
+ const bin = read(__filename);
3856
+ // split-literals so this guard's own strings are not contiguous matches (self-reference trap)
3857
+ const en = bin.includes('Securit' + 'y summary —')
3858
+ && bin.includes('auto-recover' + 'ed (LEERNESS_AUTO_SECURITY_FIX')
3859
+ && bin.includes('auto-fix optio' + 'n: LEERNESS_AUTO_SECURITY_FIX');
3860
+ const koPreserved = bin.includes('보안 ' + '요약 (1.9.76)')
3861
+ && bin.includes('자동 실행 ' + '옵션: LEERNESS_AUTO_SECURITY_FIX');
3862
+ return en && koPreserved;
3863
+ } },
3864
+ { name: 'Phase 10e (1.29.2): handoff env-detect 블록 영어/한국어 보존 (소스 가드)', run: () => {
3865
+ const bin = read(__filename);
3866
+ // split-literals (self-reference trap 회피)
3867
+ const en = bin.includes('Runtime environ' + 'ment: ⚠')
3868
+ && bin.includes('change(s) detec' + 'ted')
3869
+ && bin.includes('→ detail' + 's: leerness env detect');
3870
+ const koPreserved = bin.includes('실행 환경 (1.9.145): ⚠ PATH 누' + '락')
3871
+ && bin.includes('→ 상' + '세: leerness env detect');
3872
+ return en && koPreserved;
3873
+ } },
3874
+ { name: 'Phase 10f (1.29.3): handoff shell-guard 블록 영어/한국어 보존 (소스 가드)', run: () => {
3875
+ const bin = read(__filename);
3876
+ // split-literals (self-reference trap 회피)
3877
+ const en = bin.includes('Terminal shell gua' + 'rd (UR-0020)')
3878
+ && bin.includes('review past shell fail' + 'ures')
3879
+ && bin.includes('check before running a comm' + 'and: leerness shell-guard');
3880
+ const koPreserved = bin.includes('터미널 셸 가' + '드 (1.9.263, UR-0020)')
3881
+ && bin.includes('과거 셸 실패 기록 재검' + '토 권장');
3882
+ return en && koPreserved;
3883
+ } },
3884
+ { name: 'Phase 10g (1.29.4): handoff CLI 에이전트 슬래시 블록 영어/한국어 보존 (소스 가드)', run: () => {
3885
+ const bin = read(__filename);
3886
+ // split-literals (self-reference trap 회피)
3887
+ const en = bin.includes('CLI agent slash comma' + 'nds (UR-0021)')
3888
+ && bin.includes("active agent(s) — use each one'" + 's slash commands')
3889
+ && bin.includes('full list / record / refr' + 'esh: leerness slash-commands');
3890
+ const koPreserved = bin.includes('CLI 에이전트 슬래시 명' + '령 (1.9.265~266, UR-0021)')
3891
+ && bin.includes('전체/기록/최신' + '화: leerness slash-commands');
3892
+ return en && koPreserved;
3893
+ } },
3854
3894
  { name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
3855
3895
  ];
3856
3896
  }
@@ -8806,23 +8846,26 @@ function handoff(root) {
8806
8846
  const hasFailures = sf.failures && sf.failures.length > 0;
8807
8847
  const hasDrift = drift && drift.changes && drift.changes.length > 0;
8808
8848
  if (hasFailures || hasDrift) {
8849
+ // 1.29.3: headline t() 스코프 밖 — 로컬 t() (1.29.1 교훈: 없으면 ReferenceError 가 try 에 삼켜져 블록 증발)
8850
+ const _Lsh = _uiLang(root);
8851
+ const t = (ko, en) => (_Lsh === 'en' ? en : ko);
8809
8852
  const isTty8 = process.stdout && process.stdout.isTTY;
8810
8853
  const yl8 = s => isTty8 ? `\x1b[33m${s}\x1b[0m` : s;
8811
8854
  const dm8 = s => isTty8 ? `\x1b[2m${s}\x1b[0m` : s;
8812
8855
  const cy8 = s => isTty8 ? `\x1b[36m${s}\x1b[0m` : s;
8813
8856
  log('');
8814
- log(cy8(`## 🐚 터미널 셸 가드 (1.9.263, UR-0020)`));
8857
+ log(cy8(t(`## 🐚 터미널 셸 가드 (1.9.263, UR-0020)`, `## 🐚 Terminal shell guard (UR-0020)`)));
8815
8858
  if (hasDrift) {
8816
- log(yl8(` ⚠ 환경 버전 변동 — 과거 셸 실패 기록 재검토 권장:`));
8859
+ log(yl8(t(` ⚠ 환경 버전 변동 — 과거 셸 실패 기록 재검토 권장:`, ` ⚠ Environment version changed — review past shell failures:`)));
8817
8860
  drift.changes.forEach(ch => log(dm8(` ${ch.what}: ${ch.from} → ${ch.to}`)));
8818
8861
  }
8819
8862
  if (hasFailures) {
8820
- log(dm8(` 최근 셸 실패 ${sf.failures.length}건 (최대 3 표시):`));
8863
+ log(dm8(t(` 최근 셸 실패 ${sf.failures.length}건 (최대 3 표시):`, ` ${sf.failures.length} recent shell failure(s) (showing up to 3):`)));
8821
8864
  sf.failures.slice(-3).reverse().forEach(f => {
8822
8865
  const rules = (f.issues && f.issues.length) ? ` [${f.issues.join(',')}]` : '';
8823
8866
  log(dm8(` • ${(f.cmd || '').slice(0, 50)} (exit=${f.exitCode}, ${f.shell})${rules}`));
8824
8867
  });
8825
- log(dm8(` → 명령 실행 전 점검: leerness shell-guard "<command>"`));
8868
+ log(dm8(t(` → 명령 실행 전 점검: leerness shell-guard "<command>"`, ` → check before running a command: leerness shell-guard "<command>"`)));
8826
8869
  }
8827
8870
  }
8828
8871
  } catch {}
@@ -8835,20 +8878,23 @@ function handoff(root) {
8835
8878
  return v && v !== '0' && String(v).toLowerCase() !== 'false';
8836
8879
  });
8837
8880
  if (enabledAgents.length > 0) {
8881
+ // 1.29.4: headline t() 스코프 밖 — 로컬 t() (1.29.1 교훈)
8882
+ const _Lag = _uiLang(root);
8883
+ const t = (ko, en) => (_Lag === 'en' ? en : ko);
8838
8884
  const isTtyS = process.stdout && process.stdout.isTTY;
8839
8885
  const cyS = s => isTtyS ? `\x1b[36m${s}\x1b[0m` : s;
8840
8886
  const dmS = s => isTtyS ? `\x1b[2m${s}\x1b[0m` : s;
8841
8887
  log('');
8842
- log(cyS(`## 🤖 CLI 에이전트 슬래시 명령 (1.9.265~266, UR-0021)`));
8843
- log(dmS(` 활성 에이전트 ${enabledAgents.length}개 — sub-agent 호출 시 각자 슬래시 명령 사용:`));
8888
+ log(cyS(t(`## 🤖 CLI 에이전트 슬래시 명령 (1.9.265~266, UR-0021)`, `## 🤖 CLI agent slash commands (UR-0021)`)));
8889
+ log(dmS(t(` 활성 에이전트 ${enabledAgents.length}개 — sub-agent 호출 시 각자 슬래시 명령 사용:`, ` ${enabledAgents.length} active agent(s) — use each one's slash commands when invoking a sub-agent:`)));
8844
8890
  for (const a of enabledAgents) {
8845
8891
  const hint = _agentSlashHint(root, a.id);
8846
8892
  if (hint && hint.commands.length) {
8847
8893
  const top = hint.commands.slice(0, 8).map(c => c.cmd).join(' ');
8848
- log(dmS(` ${a.id.padEnd(8)} ${top}${hint.invoke === 'subcommand' ? ' (하위명령)' : ''}`));
8894
+ log(dmS(` ${a.id.padEnd(8)} ${top}${hint.invoke === 'subcommand' ? t(' (하위명령)', ' (subcommand)') : ''}`));
8849
8895
  }
8850
8896
  }
8851
- log(dmS(` → 전체/기록/최신화: leerness slash-commands [agent] [--record]`));
8897
+ log(dmS(t(` → 전체/기록/최신화: leerness slash-commands [agent] [--record]`, ` → full list / record / refresh: leerness slash-commands [agent] [--record]`)));
8852
8898
  }
8853
8899
  } catch {}
8854
8900
 
@@ -9489,6 +9535,9 @@ function handoff(root) {
9489
9535
  // 첫 실행에선 자동 캡처 (silent), 이후엔 변동/누락 시에만 노출.
9490
9536
  if (!has('--no-env-detect') && !has('--compact') && !has('--quiet') && process.env.LEERNESS_NO_ENV_DETECT !== '1') {
9491
9537
  try {
9538
+ // 1.29.2: headline t() 스코프 밖 — 로컬 t() (없으면 ReferenceError 가 try 에 삼켜져 env-detect 블록이 사라짐, 1.29.1 교훈)
9539
+ const _Lenv = _uiLang(root);
9540
+ const t = (ko, en) => (_Lenv === 'en' ? en : ko);
9492
9541
  const isTtyEd = process.stdout && process.stdout.isTTY;
9493
9542
  const edCy = s => isTtyEd ? `\x1b[35m${s}\x1b[0m` : s; // magenta
9494
9543
  const edDim = s => isTtyEd ? `\x1b[2m${s}\x1b[0m` : s;
@@ -9502,14 +9551,14 @@ function handoff(root) {
9502
9551
  } else if (diff.changes.length || (diff.missing && diff.missing.length)) {
9503
9552
  // 변동/누락 알림
9504
9553
  if (diff.missing && diff.missing.length) {
9505
- log(edCy(`🖥 실행 환경 (1.9.145): ⚠ PATH 누락 ${diff.missing.length}건 — npm run 시 실패 가능`));
9554
+ log(edCy(t(`🖥 실행 환경 (1.9.145): ⚠ PATH 누락 ${diff.missing.length}건 — npm run 시 실패 가능`, `🖥 Runtime environment: ⚠ ${diff.missing.length} PATH tool(s) missing — npm run may fail`)));
9506
9555
  for (const m of diff.missing.slice(0, 3)) log(edDim(` • ${m.command} (used by: npm run ${m.usedBy})`));
9507
9556
  }
9508
9557
  if (diff.changes.length) {
9509
- log(edCy(`🖥 실행 환경 (1.9.145): 변동 ${diff.changes.length}건 감지`));
9558
+ log(edCy(t(`🖥 실행 환경 (1.9.145): 변동 ${diff.changes.length}건 감지`, `🖥 Runtime environment: ${diff.changes.length} change(s) detected`)));
9510
9559
  for (const c of diff.changes.slice(0, 3)) log(edDim(` • ${c}`));
9511
9560
  }
9512
- log(edDim(` → 상세: leerness env detect . --json`));
9561
+ log(edDim(t(` → 상세: leerness env detect . --json`, ` → details: leerness env detect . --json`)));
9513
9562
  log('');
9514
9563
  // 갱신 (다음 비교 baseline)
9515
9564
  try { writeUtf8(snapPath, JSON.stringify(curr, null, 2) + '\n'); } catch {}
@@ -9520,13 +9569,16 @@ function handoff(root) {
9520
9569
  // 매 세션 시작 시 AI가 보안 위험을 즉시 인지. --no-security-summary 또는 --compact로 끄기
9521
9570
  if (!has('--no-security-summary') && !has('--compact') && !has('--quiet')) {
9522
9571
  try {
9572
+ // 1.29.1: 이 블록은 headline의 t() 스코프 밖 — 로컬 t() 정의 (없으면 ReferenceError가 try에 삼켜져 보안 요약 전체가 사라짐)
9573
+ const _Lsec = _uiLang(root);
9574
+ const t = (ko, en) => (_Lsec === 'en' ? en : ko);
9523
9575
  const envExists = exists(path.join(root, '.env'));
9524
9576
  if (envExists) {
9525
9577
  const issues = [];
9526
9578
  // 1) env diff
9527
9579
  try {
9528
9580
  const d = envDiff(root);
9529
- if (d.inEnvOnly.length) issues.push(`.env→.env.example 누락 ${d.inEnvOnly.length}건`);
9581
+ if (d.inEnvOnly.length) issues.push(t(`.env→.env.example 누락 ${d.inEnvOnly.length}건`, `.env→.env.example missing ${d.inEnvOnly.length}`));
9530
9582
  } catch {}
9531
9583
  // 2) gitignore 시크릿 패턴
9532
9584
  try {
@@ -9535,7 +9587,7 @@ function handoff(root) {
9535
9587
  const giLines = giText.split('\n').map(l => l.trim());
9536
9588
  const SECRET_PATTERNS = ['.env', '.env.local', '.env.production', '.env.*.local', '*.pem', 'credentials.json'];
9537
9589
  const missing = SECRET_PATTERNS.filter(p => !giLines.some(l => l === p || l === '/' + p));
9538
- if (missing.length) issues.push(`.gitignore 시크릿 누락 ${missing.length}건`);
9590
+ if (missing.length) issues.push(t(`.gitignore 시크릿 누락 ${missing.length}건`, `.gitignore missing secret patterns ${missing.length}`));
9539
9591
  } catch {}
9540
9592
  if (issues.length) {
9541
9593
  const isTty = process.stdout && process.stdout.isTTY;
@@ -9543,31 +9595,31 @@ function handoff(root) {
9543
9595
  const dim = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
9544
9596
  const yel = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
9545
9597
  log('');
9546
- log(red(`## 🔒 보안 요약 (1.9.76) — ${issues.length}건 주의`));
9598
+ log(red(t(`## 🔒 보안 요약 (1.9.76) — ${issues.length}건 주의`, `## 🔒 Security summary — ${issues.length} to review`)));
9547
9599
  for (const i of issues) log(dim(` ⚠ ${i}`));
9548
- log(dim(` → 자동 수정: leerness audit --fix · 상세: leerness env check / leerness audit`));
9600
+ log(dim(t(` → 자동 수정: leerness audit --fix · 상세: leerness env check / leerness audit`, ` → auto-fix: leerness audit --fix · details: leerness env check / leerness audit`)));
9549
9601
  // 1.9.80: critical 수준 (.gitignore에 .env 자체 누락) 시 자동 회복 옵션
9550
9602
  const giText = exists(path.join(root, '.gitignore')) ? read(path.join(root, '.gitignore')) : '';
9551
9603
  const giLines = giText.split('\n').map(l => l.trim());
9552
9604
  const envInGitignore = giLines.includes('.env') || giLines.includes('/.env');
9553
9605
  if (!envInGitignore) {
9554
9606
  // .env 자체 누락 → 최우선 위험
9555
- log(yel(` 🚨 CRITICAL (1.9.80): .env가 .gitignore에 없습니다! 시크릿 노출 위험 — 즉시 \`leerness audit --fix\` 권장.`));
9607
+ log(yel(t(` 🚨 CRITICAL (1.9.80): .env가 .gitignore에 없습니다! 시크릿 노출 위험 — 즉시 \`leerness audit --fix\` 권장.`, ` 🚨 CRITICAL: .env is not in .gitignore! secret-exposure risk — run \`leerness audit --fix\` now.`)));
9556
9608
  // LEERNESS_AUTO_SECURITY_FIX=1 자동 실행 옵션
9557
9609
  if (process.env.LEERNESS_AUTO_SECURITY_FIX === '1') {
9558
9610
  try {
9559
9611
  const r = cp.spawnSync(process.execPath, [__filename, 'audit', root, '--fix'],
9560
9612
  { encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' } });
9561
9613
  if (r.status === 0) {
9562
- log(dim(` ✓ 자동 회복 (LEERNESS_AUTO_SECURITY_FIX=1): audit --fix 완료`));
9614
+ log(dim(t(` ✓ 자동 회복 (LEERNESS_AUTO_SECURITY_FIX=1): audit --fix 완료`, ` ✓ auto-recovered (LEERNESS_AUTO_SECURITY_FIX=1): audit --fix done`)));
9563
9615
  } else {
9564
- log(dim(` ⚠ 자동 회복 실패 (exit ${r.status}) — 수동 \`leerness audit --fix\` 권장`));
9616
+ log(dim(t(` ⚠ 자동 회복 실패 (exit ${r.status}) — 수동 \`leerness audit --fix\` 권장`, ` ⚠ auto-recovery failed (exit ${r.status}) — run \`leerness audit --fix\` manually`)));
9565
9617
  }
9566
9618
  } catch (e) {
9567
- log(dim(` ⚠ 자동 회복 예외: ${e.message}`));
9619
+ log(dim(t(` ⚠ 자동 회복 예외: ${e.message}`, ` ⚠ auto-recovery error: ${e.message}`)));
9568
9620
  }
9569
9621
  } else {
9570
- log(dim(` 💡 자동 실행 옵션: LEERNESS_AUTO_SECURITY_FIX=1 leerness handoff .`));
9622
+ log(dim(t(` 💡 자동 실행 옵션: LEERNESS_AUTO_SECURITY_FIX=1 leerness handoff .`, ` 💡 auto-fix option: LEERNESS_AUTO_SECURITY_FIX=1 leerness handoff .`)));
9571
9623
  }
9572
9624
  }
9573
9625
  log('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.29.0",
3
+ "version": "1.30.0",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -6239,9 +6239,67 @@ total++;
6239
6239
  const docKo = out(cp.spawnSync(process.execPath, [CLI, 'doctor'], { encoding: 'utf8', timeout: 20000, cwd: d }));
6240
6240
  const doctorOk = /install\/environment diagnosis/.test(docEn) && !H.test(docEn) && /설치\/환경 진단/.test(docKo);
6241
6241
  fs.rmSync(d, { recursive: true, force: true });
6242
- ok = lensKoOk && lensEnOk && noLeak && stOk && healthOk && driftOk && doctorOk;
6243
- } catch {}
6244
- console.log(ok ? '✓ B(1.25.1/1.25.2/1.27.2/1.28.2) i18n 행위: --language en 런타임 영어(lens/health/drift/doctor) + ko 기본 보존 + --language positional 무누출 + status 에러 en/ko (UR-0010)' : '✗ i18n 행위 회귀 가드 실패');
6242
+ // (1.29.1) handoff 보안 요약 섹션: .env + 미흡한 .gitignore en 영어(섹션 라인 한글 0) + ko 기본 한글.
6243
+ // 소스가드만으로는 잡는 회귀를 e2e 로 보강: 보안 요약 블록은 headline 의 t() 스코프 밖이라,
6244
+ // 로컬 t() 누락 ReferenceError try 삼켜져 섹션 전체가 (양 언어 모두) 사라진다. 가드가 그걸 잡는다.
6245
+ const dh = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-i18n-ho-'));
6246
+ cp.spawnSync(process.execPath, [CLI, 'init', dh, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
6247
+ fs.writeFileSync(path.join(dh, '.env'), 'API_KEY=sk-test-abc123def456ghi789jkl012mno345\n');
6248
+ fs.writeFileSync(path.join(dh, '.gitignore'), 'node_modules/\n');
6249
+ const hoEn = out(cp.spawnSync(process.execPath, [CLI, 'handoff', dh, '--language', 'en'], { encoding: 'utf8', timeout: 25000 }));
6250
+ const hoKo = out(cp.spawnSync(process.execPath, [CLI, 'handoff', dh], { encoding: 'utf8', timeout: 25000 }));
6251
+ const enSecLines = hoEn.split('\n').filter(l => /Security summary|auto-fix:|CRITICAL|auto-fix option|recover|missing secret/i.test(l));
6252
+ const hoEnOk = /Security summary/.test(hoEn) && enSecLines.length >= 2 && !enSecLines.some(l => H.test(l));
6253
+ const hoKoOk = /보안 요약/.test(hoKo);
6254
+ fs.rmSync(dh, { recursive: true, force: true });
6255
+ // ⑨ (1.29.2) handoff env-detect 블록: 환경 스냅샷 변동 시 → en 영어(블록 라인 한글 0) + ko 기본 한글.
6256
+ // 블록은 첫 핸드오프 후 .harness/environment.json 변동이 있어야 발동 → 스냅샷의 node.version 을 인위 변경해 강제.
6257
+ // (1.29.1 과 같은 블록-스코프 t 함정 가드 — env-detect 도 headline t() 스코프 밖이라 로컬 t() 필요.)
6258
+ const de = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-i18n-env-'));
6259
+ cp.spawnSync(process.execPath, [CLI, 'init', de, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
6260
+ const snap = path.join(de, '.harness', 'environment.json');
6261
+ const forceEnvChange = () => { try { const s = JSON.parse(fs.readFileSync(snap, 'utf8')); if (s.node) s.node.version = 'v0.0.0-test'; fs.writeFileSync(snap, JSON.stringify(s, null, 2) + '\n'); } catch {} };
6262
+ cp.spawnSync(process.execPath, [CLI, 'handoff', de], { encoding: 'utf8', timeout: 25000 }); // 첫 캡처(silent)
6263
+ forceEnvChange();
6264
+ const edEn = out(cp.spawnSync(process.execPath, [CLI, 'handoff', de, '--language', 'en'], { encoding: 'utf8', timeout: 25000 }));
6265
+ forceEnvChange(); // en 실행이 스냅샷 갱신 → ko 위해 재변경
6266
+ const edKo = out(cp.spawnSync(process.execPath, [CLI, 'handoff', de], { encoding: 'utf8', timeout: 25000 }));
6267
+ const edEnLines = edEn.split('\n').filter(l => /Runtime environment|env detect|change\(s\) detected/i.test(l));
6268
+ const edEnOk = /Runtime environment/.test(edEn) && edEnLines.length >= 1 && !edEnLines.some(l => H.test(l));
6269
+ const edKoOk = /실행 환경/.test(edKo);
6270
+ fs.rmSync(de, { recursive: true, force: true });
6271
+ // ⑩ (1.29.3) handoff shell-guard 블록: 셸 실패 기록 + 환경 스냅샷 변동 → en 영어(블록 라인 한글 0) + ko 기본 한글.
6272
+ // 블록은 hasFailures(.harness/shell-failures.json) 또는 hasDrift(스냅샷 변동) 시 발동. 둘 다 인위 구성.
6273
+ const ds = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-i18n-sh-'));
6274
+ cp.spawnSync(process.execPath, [CLI, 'init', ds, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
6275
+ const ssnap = path.join(ds, '.harness', 'environment.json');
6276
+ const sfail = path.join(ds, '.harness', 'shell-failures.json');
6277
+ const seedSh = () => {
6278
+ try { const s = JSON.parse(fs.readFileSync(ssnap, 'utf8')); if (s.node) s.node.version = 'v0.0.0-test'; fs.writeFileSync(ssnap, JSON.stringify(s, null, 2) + '\n'); } catch {}
6279
+ fs.writeFileSync(sfail, JSON.stringify({ failures: [{ cmd: 'ls && pwd', exitCode: 1, shell: 'powershell-5.1', issues: ['ps5-chain'] }] }, null, 2) + '\n');
6280
+ };
6281
+ cp.spawnSync(process.execPath, [CLI, 'handoff', ds], { encoding: 'utf8', timeout: 25000 }); // 첫 캡처(silent)
6282
+ seedSh();
6283
+ const shEn = out(cp.spawnSync(process.execPath, [CLI, 'handoff', ds, '--language', 'en'], { encoding: 'utf8', timeout: 25000 }));
6284
+ seedSh(); // en 실행이 스냅샷 갱신 → ko 위해 재구성
6285
+ const shKo = out(cp.spawnSync(process.execPath, [CLI, 'handoff', ds], { encoding: 'utf8', timeout: 25000 }));
6286
+ const shEnLines = shEn.split('\n').filter(l => /shell guard|shell failure|review past shell|shell-guard|check before running/i.test(l));
6287
+ const shEnOk = /Terminal shell guard/.test(shEn) && shEnLines.length >= 2 && !shEnLines.some(l => H.test(l));
6288
+ const shKoOk = /셸 가드/.test(shKo);
6289
+ fs.rmSync(ds, { recursive: true, force: true });
6290
+ // ⑪ (1.29.4) handoff CLI 에이전트 슬래시 블록: 외부 에이전트 env flag 활성 시 → en 영어(블록 라인 한글 0) + ko 기본 한글.
6291
+ const da = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-i18n-ag-'));
6292
+ cp.spawnSync(process.execPath, [CLI, 'init', da, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
6293
+ const agEnv = { ...process.env, LEERNESS_ENABLE_CODEX: '1', LEERNESS_ENABLE_CLAUDE: '1' };
6294
+ const agEn = out(cp.spawnSync(process.execPath, [CLI, 'handoff', da, '--language', 'en'], { encoding: 'utf8', timeout: 25000, env: agEnv }));
6295
+ const agKo = out(cp.spawnSync(process.execPath, [CLI, 'handoff', da], { encoding: 'utf8', timeout: 25000, env: agEnv }));
6296
+ const agEnLines = agEn.split('\n').filter(l => /agent slash|active agent|slash-commands|full list/i.test(l));
6297
+ const agEnOk = /CLI agent slash commands/.test(agEn) && agEnLines.length >= 2 && !agEnLines.some(l => H.test(l));
6298
+ const agKoOk = /에이전트 슬래시/.test(agKo);
6299
+ fs.rmSync(da, { recursive: true, force: true });
6300
+ ok = lensKoOk && lensEnOk && noLeak && stOk && healthOk && driftOk && doctorOk && hoEnOk && hoKoOk && edEnOk && edKoOk && shEnOk && shKoOk && agEnOk && agKoOk;
6301
+ } catch {}
6302
+ console.log(ok ? '✓ B(1.25.1/1.25.2/1.27.2/1.28.2/1.29.1/1.29.2/1.29.3/1.29.4) i18n 행위: --language en 런타임 영어(lens/health/drift/doctor/handoff보안요약/env-detect/shell-guard/agent-slash) + ko 기본 보존 + --language positional 무누출 + status 에러 en/ko (UR-0010)' : '✗ i18n 행위 회귀 가드 실패');
6245
6303
  if (!ok) failed++;
6246
6304
  }
6247
6305