leerness 1.28.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 +137 -0
- package/README.md +4 -4
- package/bin/leerness.js +91 -24
- package/lib/diagnostics.js +44 -41
- package/lib/drift.js +23 -22
- package/package.json +1 -1
- package/scripts/e2e.js +65 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,142 @@
|
|
|
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
|
+
|
|
95
|
+
## 1.29.0 — 2026-06-16 — 🛡️ [안정화/Stable] drift auto-fix·진단 명령 영어화 안정 minor
|
|
96
|
+
|
|
97
|
+
**🛡️ 안정화(Stable) minor — drift 완전 영어화(버그수정 포함) + 진단 3종 영어화를 npm 공개.** 직전 minor(1.28.0) 이후 누적된 패치 2건(1.28.1 + 1.28.2)을 검증·통합해 배포. R-0011 정책의 20번째 stable minor. 한국어 우선 기본은 그대로.
|
|
98
|
+
|
|
99
|
+
### 이번 minor 통합 (1.28.1~1.28.2)
|
|
100
|
+
- **🐛+🌐 drift --auto-fix 영어화 + 보안신호 판정 버그수정 (1.28.1)**: 1.27.2 가 보안 신호 라벨을 언어화하며 내부 `hasSecurityFired` 가 한국어 라벨 정규식에 결합돼 `--language en` 에서 보안 auto-fix 가 미발동하던 잠복 버그를 언어-안정 필드(`f.file`)로 수정. drift `--auto-fix` 진행 로그 ~21줄 영어화 → `drift check` 완전 영어(출력 + auto-fix).
|
|
101
|
+
- **🌐 진단 모듈 영어화 (1.28.2)**: `doctor`(설치/환경)·`which`(버전 충돌·npx 캐시)·`whats-new`(CHANGELOG 차분) 3종을 영어 opt-in. 기존 위임 가드 보존.
|
|
102
|
+
- **한국어 우선 기본 보존**: 영어는 명시 opt-in. 한국어 출력/내부 호출 무영향(e2e 무회귀).
|
|
103
|
+
|
|
104
|
+
### 잔여 (UR-0010 백로그)
|
|
105
|
+
- capabilities/commands/constraints/install-safety 영어화 · init en seed 템플릿 i18n.
|
|
106
|
+
|
|
107
|
+
### 검증 (회귀 0)
|
|
108
|
+
- **selftest 250/250** · **E2E 368/368** (i18n 행위가드 lens/health/drift/doctor en/ko) · 게시본 클린룸 재실증.
|
|
109
|
+
- minor(1.29.0) — npm 배포(R-0011 stable) + annotated tag(Stable) + GitHub release(latest).
|
|
110
|
+
|
|
111
|
+
## 1.28.2 — 2026-06-16 — CLI 영어화 Phase 10c: 진단 모듈(doctor/which/whats-new) 완전 영어화 (UR-0010)
|
|
112
|
+
|
|
113
|
+
**🌐 설치/버전 진단 3종을 영어로.** 문제 해결 시 자주 쓰는 진단 명령 `doctor`(설치/환경)·`which`(버전 충돌·npx 캐시)·`whats-new`(버전 변경 요약)를 한 모듈(lib/diagnostics.js)에서 완전 영어화.
|
|
114
|
+
|
|
115
|
+
### 변경 (UR-0010 Phase 10c)
|
|
116
|
+
- **lib/diagnostics.js 3 명령 영어화 (DI uiLang)**: doctor(설치/환경 진단·설치 경로·MCP 도구·selftest 통과/실패·셸·정상/문제), which(현재 실행·버전·npm 환경·글로벌 설치·PATH 후보·진단·강제 최신 방법), whats-new(버전 파악 실패/CHANGELOG 없음 에러·신규 명령·플래그·파일·버전별 헤드라인·권장 행동). `t(ko,en)`, ko 인자 verbatim.
|
|
117
|
+
- **3 DI 호출에 uiLang 주입**: doctorCmd/whichCmd(`_uiLang(arg('--path', cwd))`) + whatsNewCmd(`_uiLang(root)`). 기존 1.9.392/1.9.394 위임 가드 문자열 보존(selftest 무회귀).
|
|
118
|
+
- **한국어 기본 유지**: 영어는 명시 opt-in. ko/내부 호출 무영향.
|
|
119
|
+
|
|
120
|
+
### 잔여 (UR-0010 백로그)
|
|
121
|
+
- capabilities/commands/constraints/install-safety 영어화 · init en seed 템플릿 i18n.
|
|
122
|
+
|
|
123
|
+
### 검증 (회귀 0)
|
|
124
|
+
- **selftest 249→250** (진단 영어/한국어 보존 + uiLang 주입 소스가드) · 행위(doctor/which/whats-new `--language en` 한글 0 [Node 탐지] / ko 보존) · **E2E 368/368** (i18n 행위가드에 doctor en/ko 추가).
|
|
125
|
+
- patch(1.28.2) — npm 미배포(R-0011, GitHub/CHANGELOG 누적).
|
|
126
|
+
|
|
127
|
+
## 1.28.1 — 2026-06-16 — Phase 10b: drift --auto-fix 로그 영어화 + 보안신호 판정 버그 수정 (UR-0010)
|
|
128
|
+
|
|
129
|
+
**🐛+🌐 drift 를 완전 영어로 + 직전 라운드가 심은 버그 수정.** `drift check --auto-fix` 진행 로그를 영어화하면서, 1.27.2 가 보안 신호 라벨을 언어화하며 함께 심은 **내부 판정 버그**를 발견·수정.
|
|
130
|
+
|
|
131
|
+
### 변경
|
|
132
|
+
- **🐛 hasSecurityFired 언어-결합 버그 수정**: 1.27.2 에서 보안 신호 `label` 을 `t(ko,en)` 로 언어화했는데, 내부 `hasSecurityFired` 가 한국어 라벨 정규식(`/보안 위험 \(1\.9\.78\)/`)으로 매칭하고 있어 **`drift check --auto-fix --language en` 에서 보안 auto-fix 가 발동하지 않던** 잠복 버그. 언어-안정 필드(`f.file === '.env / .gitignore'`)로 매칭 변경 → en/ko 모두 정상 발동. (번역된 라벨에 내부 로직이 결합되면 안 된다는 교훈 — [[lesson-adversarial-harden-heuristic]] 의 'valueGroup' 류와 동일.)
|
|
133
|
+
- **🌐 drift --auto-fix 로그 영어화 (~21줄)**: 보안/인코딩/delivered/idempotency/release-cleanup/session-close 자동회복 진행 로그를 `t(ko,en)`. 이로써 `drift check` 가 **완전히** 영어 지원(출력 + auto-fix).
|
|
134
|
+
- **한국어 기본 유지**: 영어는 명시 opt-in. ko 로그/내부 호출 무영향.
|
|
135
|
+
|
|
136
|
+
### 검증 (회귀 0)
|
|
137
|
+
- **selftest 248→249** (afLog 영어/한국어 + 버그수정 소스가드) · 행위(보안 auto-fix en/ko 모두 발동 + en 로그 한글 0 [Node 탐지] + ko 보존) · **E2E 368/368**.
|
|
138
|
+
- patch(1.28.1) — npm 미배포(R-0011, GitHub/CHANGELOG 누적).
|
|
139
|
+
|
|
3
140
|
## 1.28.0 — 2026-06-15 — 🛡️ [안정화/Stable] 정직성 후속 + drift 영어화 안정 minor
|
|
4
141
|
|
|
5
142
|
**🛡️ 안정화(Stable) minor — 13번째 외부리뷰 정직성 수정 + drift 진단 영어화를 npm 공개.** 직전 minor(1.27.0) 이후 누적된 패치 2건(1.27.1 + 1.27.2)을 검증·통합해 배포. R-0011 정책의 19번째 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.
|
|
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.
|
|
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.28.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.
|
|
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.
|
|
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.
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -3836,6 +3836,61 @@ function _selfTestCases() {
|
|
|
3836
3836
|
const koPreserved = dr.includes('| 신호 | age | 임계 | 가중치 | 발화 |') && dr.includes('session close 누락') && dr.includes('권장 조치'); // ko 인자 보존(e2e ko/내부호출)
|
|
3837
3837
|
return injected && en && koPreserved;
|
|
3838
3838
|
} },
|
|
3839
|
+
{ name: 'Phase 10b (1.28.1): drift --auto-fix 로그 영어화 + hasSecurityFired 언어안정 매칭 (소스 가드)', run: () => {
|
|
3840
|
+
const dr = read(path.join(path.dirname(__filename), '..', 'lib', 'drift.js'));
|
|
3841
|
+
const bugFix = dr.includes("f.file === '.env / .gitignore'"); // 보안 신호 판정을 언어-안정 file 필드로(라벨 regex 결합 제거)
|
|
3842
|
+
const afLogEn = dr.includes('recovering security signal: running audit --fix') && dr.includes('re-checking...') && dr.includes('auto-fix error:');
|
|
3843
|
+
const afLogKo = dr.includes('보안 신호 회복: audit --fix 자동 실행 중') && dr.includes('재검사 중...'); // ko 인자 보존
|
|
3844
|
+
return bugFix && afLogEn && afLogKo;
|
|
3845
|
+
} },
|
|
3846
|
+
{ name: 'Phase 10c (1.28.2): 진단 모듈(doctor/which/whats-new) 영어/한국어 보존 + uiLang 주입 (소스 가드)', run: () => {
|
|
3847
|
+
const bin = read(__filename);
|
|
3848
|
+
const dg = read(path.join(path.dirname(__filename), '..', 'lib', 'diagnostics.js'));
|
|
3849
|
+
const injected = bin.includes("uiLang: _uiLang(arg('--path', process.cwd())), _selfTestCases") && bin.includes('_diag.whatsNewCmd(root, { VERSION, uiLang: _uiLang(root)');
|
|
3850
|
+
const en = dg.includes('install/environment diagnosis') && dg.includes('## npm environment') && dg.includes('per-version headlines') && dg.includes('how to force the latest');
|
|
3851
|
+
const koPreserved = dg.includes('설치/환경 진단') && dg.includes('## npm 환경') && dg.includes('버전별 헤드라인'); // ko 인자 보존(내부호출/e2e)
|
|
3852
|
+
return injected && en && koPreserved;
|
|
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
|
+
} },
|
|
3839
3894
|
{ name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
|
|
3840
3895
|
];
|
|
3841
3896
|
}
|
|
@@ -8791,23 +8846,26 @@ function handoff(root) {
|
|
|
8791
8846
|
const hasFailures = sf.failures && sf.failures.length > 0;
|
|
8792
8847
|
const hasDrift = drift && drift.changes && drift.changes.length > 0;
|
|
8793
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);
|
|
8794
8852
|
const isTty8 = process.stdout && process.stdout.isTTY;
|
|
8795
8853
|
const yl8 = s => isTty8 ? `\x1b[33m${s}\x1b[0m` : s;
|
|
8796
8854
|
const dm8 = s => isTty8 ? `\x1b[2m${s}\x1b[0m` : s;
|
|
8797
8855
|
const cy8 = s => isTty8 ? `\x1b[36m${s}\x1b[0m` : s;
|
|
8798
8856
|
log('');
|
|
8799
|
-
log(cy8(`## 🐚 터미널 셸 가드 (1.9.263, UR-0020)`));
|
|
8857
|
+
log(cy8(t(`## 🐚 터미널 셸 가드 (1.9.263, UR-0020)`, `## 🐚 Terminal shell guard (UR-0020)`)));
|
|
8800
8858
|
if (hasDrift) {
|
|
8801
|
-
log(yl8(` ⚠ 환경 버전 변동 — 과거 셸 실패 기록 재검토
|
|
8859
|
+
log(yl8(t(` ⚠ 환경 버전 변동 — 과거 셸 실패 기록 재검토 권장:`, ` ⚠ Environment version changed — review past shell failures:`)));
|
|
8802
8860
|
drift.changes.forEach(ch => log(dm8(` ${ch.what}: ${ch.from} → ${ch.to}`)));
|
|
8803
8861
|
}
|
|
8804
8862
|
if (hasFailures) {
|
|
8805
|
-
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):`)));
|
|
8806
8864
|
sf.failures.slice(-3).reverse().forEach(f => {
|
|
8807
8865
|
const rules = (f.issues && f.issues.length) ? ` [${f.issues.join(',')}]` : '';
|
|
8808
8866
|
log(dm8(` • ${(f.cmd || '').slice(0, 50)} (exit=${f.exitCode}, ${f.shell})${rules}`));
|
|
8809
8867
|
});
|
|
8810
|
-
log(dm8(` → 명령 실행 전 점검: leerness shell-guard "<command>"`));
|
|
8868
|
+
log(dm8(t(` → 명령 실행 전 점검: leerness shell-guard "<command>"`, ` → check before running a command: leerness shell-guard "<command>"`)));
|
|
8811
8869
|
}
|
|
8812
8870
|
}
|
|
8813
8871
|
} catch {}
|
|
@@ -8820,20 +8878,23 @@ function handoff(root) {
|
|
|
8820
8878
|
return v && v !== '0' && String(v).toLowerCase() !== 'false';
|
|
8821
8879
|
});
|
|
8822
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);
|
|
8823
8884
|
const isTtyS = process.stdout && process.stdout.isTTY;
|
|
8824
8885
|
const cyS = s => isTtyS ? `\x1b[36m${s}\x1b[0m` : s;
|
|
8825
8886
|
const dmS = s => isTtyS ? `\x1b[2m${s}\x1b[0m` : s;
|
|
8826
8887
|
log('');
|
|
8827
|
-
log(cyS(`## 🤖 CLI 에이전트 슬래시 명령 (1.9.265~266, UR-0021)`));
|
|
8828
|
-
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:`)));
|
|
8829
8890
|
for (const a of enabledAgents) {
|
|
8830
8891
|
const hint = _agentSlashHint(root, a.id);
|
|
8831
8892
|
if (hint && hint.commands.length) {
|
|
8832
8893
|
const top = hint.commands.slice(0, 8).map(c => c.cmd).join(' ');
|
|
8833
|
-
log(dmS(` ${a.id.padEnd(8)} ${top}${hint.invoke === 'subcommand' ? ' (하위명령)' : ''}`));
|
|
8894
|
+
log(dmS(` ${a.id.padEnd(8)} ${top}${hint.invoke === 'subcommand' ? t(' (하위명령)', ' (subcommand)') : ''}`));
|
|
8834
8895
|
}
|
|
8835
8896
|
}
|
|
8836
|
-
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]`)));
|
|
8837
8898
|
}
|
|
8838
8899
|
} catch {}
|
|
8839
8900
|
|
|
@@ -9474,6 +9535,9 @@ function handoff(root) {
|
|
|
9474
9535
|
// 첫 실행에선 자동 캡처 (silent), 이후엔 변동/누락 시에만 노출.
|
|
9475
9536
|
if (!has('--no-env-detect') && !has('--compact') && !has('--quiet') && process.env.LEERNESS_NO_ENV_DETECT !== '1') {
|
|
9476
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);
|
|
9477
9541
|
const isTtyEd = process.stdout && process.stdout.isTTY;
|
|
9478
9542
|
const edCy = s => isTtyEd ? `\x1b[35m${s}\x1b[0m` : s; // magenta
|
|
9479
9543
|
const edDim = s => isTtyEd ? `\x1b[2m${s}\x1b[0m` : s;
|
|
@@ -9487,14 +9551,14 @@ function handoff(root) {
|
|
|
9487
9551
|
} else if (diff.changes.length || (diff.missing && diff.missing.length)) {
|
|
9488
9552
|
// 변동/누락 알림
|
|
9489
9553
|
if (diff.missing && diff.missing.length) {
|
|
9490
|
-
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`)));
|
|
9491
9555
|
for (const m of diff.missing.slice(0, 3)) log(edDim(` • ${m.command} (used by: npm run ${m.usedBy})`));
|
|
9492
9556
|
}
|
|
9493
9557
|
if (diff.changes.length) {
|
|
9494
|
-
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`)));
|
|
9495
9559
|
for (const c of diff.changes.slice(0, 3)) log(edDim(` • ${c}`));
|
|
9496
9560
|
}
|
|
9497
|
-
log(edDim(` → 상세: leerness env detect . --json`));
|
|
9561
|
+
log(edDim(t(` → 상세: leerness env detect . --json`, ` → details: leerness env detect . --json`)));
|
|
9498
9562
|
log('');
|
|
9499
9563
|
// 갱신 (다음 비교 baseline)
|
|
9500
9564
|
try { writeUtf8(snapPath, JSON.stringify(curr, null, 2) + '\n'); } catch {}
|
|
@@ -9505,13 +9569,16 @@ function handoff(root) {
|
|
|
9505
9569
|
// 매 세션 시작 시 AI가 보안 위험을 즉시 인지. --no-security-summary 또는 --compact로 끄기
|
|
9506
9570
|
if (!has('--no-security-summary') && !has('--compact') && !has('--quiet')) {
|
|
9507
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);
|
|
9508
9575
|
const envExists = exists(path.join(root, '.env'));
|
|
9509
9576
|
if (envExists) {
|
|
9510
9577
|
const issues = [];
|
|
9511
9578
|
// 1) env diff
|
|
9512
9579
|
try {
|
|
9513
9580
|
const d = envDiff(root);
|
|
9514
|
-
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}`));
|
|
9515
9582
|
} catch {}
|
|
9516
9583
|
// 2) gitignore 시크릿 패턴
|
|
9517
9584
|
try {
|
|
@@ -9520,7 +9587,7 @@ function handoff(root) {
|
|
|
9520
9587
|
const giLines = giText.split('\n').map(l => l.trim());
|
|
9521
9588
|
const SECRET_PATTERNS = ['.env', '.env.local', '.env.production', '.env.*.local', '*.pem', 'credentials.json'];
|
|
9522
9589
|
const missing = SECRET_PATTERNS.filter(p => !giLines.some(l => l === p || l === '/' + p));
|
|
9523
|
-
if (missing.length) issues.push(`.gitignore 시크릿 누락 ${missing.length}
|
|
9590
|
+
if (missing.length) issues.push(t(`.gitignore 시크릿 누락 ${missing.length}건`, `.gitignore missing secret patterns ${missing.length}`));
|
|
9524
9591
|
} catch {}
|
|
9525
9592
|
if (issues.length) {
|
|
9526
9593
|
const isTty = process.stdout && process.stdout.isTTY;
|
|
@@ -9528,31 +9595,31 @@ function handoff(root) {
|
|
|
9528
9595
|
const dim = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
9529
9596
|
const yel = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
|
|
9530
9597
|
log('');
|
|
9531
|
-
log(red(`## 🔒 보안 요약 (1.9.76) — ${issues.length}건
|
|
9598
|
+
log(red(t(`## 🔒 보안 요약 (1.9.76) — ${issues.length}건 주의`, `## 🔒 Security summary — ${issues.length} to review`)));
|
|
9532
9599
|
for (const i of issues) log(dim(` ⚠ ${i}`));
|
|
9533
|
-
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`)));
|
|
9534
9601
|
// 1.9.80: critical 수준 (.gitignore에 .env 자체 누락) 시 자동 회복 옵션
|
|
9535
9602
|
const giText = exists(path.join(root, '.gitignore')) ? read(path.join(root, '.gitignore')) : '';
|
|
9536
9603
|
const giLines = giText.split('\n').map(l => l.trim());
|
|
9537
9604
|
const envInGitignore = giLines.includes('.env') || giLines.includes('/.env');
|
|
9538
9605
|
if (!envInGitignore) {
|
|
9539
9606
|
// .env 자체 누락 → 최우선 위험
|
|
9540
|
-
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.`)));
|
|
9541
9608
|
// LEERNESS_AUTO_SECURITY_FIX=1 자동 실행 옵션
|
|
9542
9609
|
if (process.env.LEERNESS_AUTO_SECURITY_FIX === '1') {
|
|
9543
9610
|
try {
|
|
9544
9611
|
const r = cp.spawnSync(process.execPath, [__filename, 'audit', root, '--fix'],
|
|
9545
9612
|
{ encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' } });
|
|
9546
9613
|
if (r.status === 0) {
|
|
9547
|
-
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`)));
|
|
9548
9615
|
} else {
|
|
9549
|
-
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`)));
|
|
9550
9617
|
}
|
|
9551
9618
|
} catch (e) {
|
|
9552
|
-
log(dim(` ⚠ 자동 회복 예외: ${e.message}`));
|
|
9619
|
+
log(dim(t(` ⚠ 자동 회복 예외: ${e.message}`, ` ⚠ auto-recovery error: ${e.message}`)));
|
|
9553
9620
|
}
|
|
9554
9621
|
} else {
|
|
9555
|
-
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 .`)));
|
|
9556
9623
|
}
|
|
9557
9624
|
}
|
|
9558
9625
|
log('');
|
|
@@ -16115,7 +16182,7 @@ function mcpServeCmd(root) {
|
|
|
16115
16182
|
}
|
|
16116
16183
|
|
|
16117
16184
|
// 1.9.394 (UR-0025): whatsNewCmd 를 lib/diagnostics.js 로 분리 (whats-new 서브시스템 완결 — 파서는 1.9.393 에 pure-utils 로). deps 위임.
|
|
16118
|
-
function whatsNewCmd(root) { return _diag.whatsNewCmd(root, { VERSION, arg, has }); }
|
|
16185
|
+
function whatsNewCmd(root) { return _diag.whatsNewCmd(root, { VERSION, uiLang: _uiLang(root), arg, has }); }
|
|
16119
16186
|
|
|
16120
16187
|
// 1.9.71: .env / .env.example 자동 동기화 — 누락 키 감지 + (옵션) 자동 추가
|
|
16121
16188
|
// 보안 정책: .env의 실제 값은 절대 옮기지 않음. .env.example엔 키만 (빈 값).
|
|
@@ -19734,8 +19801,8 @@ function reviewRequestCmd(root, request) { return _reviewRequest.reviewRequestCm
|
|
|
19734
19801
|
// 1.9.392 (UR-0025 큰 핸들러 모듈화 4번째): doctor/which 진단 핸들러를 lib/diagnostics.js 로 분리.
|
|
19735
19802
|
// harness 는 deps(VERSION · _selfTestCases · _detectShellCtx · _mcpToolCount · has · harnessPath)를 구성해 위임(thin wrapper). 호출부/동작 무변경.
|
|
19736
19803
|
const _diag = require('../lib/diagnostics');
|
|
19737
|
-
function doctorCmd(opts = {}) { return _diag.doctorCmd(opts, { VERSION, _selfTestCases, _detectShellCtx, _mcpToolCount, has, harnessPath: __filename }); }
|
|
19738
|
-
function whichCmd() { return _diag.whichCmd({ VERSION, has, harnessPath: __filename }); }
|
|
19804
|
+
function doctorCmd(opts = {}) { return _diag.doctorCmd(opts, { VERSION, uiLang: _uiLang(arg('--path', process.cwd())), _selfTestCases, _detectShellCtx, _mcpToolCount, has, harnessPath: __filename }); }
|
|
19805
|
+
function whichCmd() { return _diag.whichCmd({ VERSION, uiLang: _uiLang(arg('--path', process.cwd())), has, harnessPath: __filename }); }
|
|
19739
19806
|
|
|
19740
19807
|
// 1.23.1 (UR-0010 Phase 6): 영어 큐레이트 도움말 — 한국어 help 의 줄별 번역이 아니라, 카테고리별로 정리한 별도 영어판.
|
|
19741
19808
|
// 레거시 버전태그(1.9.x) 군더더기를 빼고 영어 사용자가 읽기 쉽게. 전체 전수 목록은 `leerness commands`.
|
package/lib/diagnostics.js
CHANGED
|
@@ -10,7 +10,8 @@ const { log, fail, absRoot, exists, read } = require('./io');
|
|
|
10
10
|
const { parseHarnessVersion, _parseChangelogBetween } = require('./pure-utils');
|
|
11
11
|
|
|
12
12
|
function doctorCmd(opts = {}, deps = {}) {
|
|
13
|
-
const { VERSION, _selfTestCases, _detectShellCtx, _mcpToolCount, has, harnessPath } = deps;
|
|
13
|
+
const { VERSION, uiLang, _selfTestCases, _detectShellCtx, _mcpToolCount, has, harnessPath } = deps;
|
|
14
|
+
const t = (ko, en) => (uiLang === 'en' ? en : ko); // 1.28.2 (UR-0010 Phase 10c)
|
|
14
15
|
const json = opts.json || has('--json');
|
|
15
16
|
// 1) 코어 무결성: selftest 케이스 인라인 실행
|
|
16
17
|
let pass = 0; const failNames = [];
|
|
@@ -35,23 +36,24 @@ function doctorCmd(opts = {}, deps = {}) {
|
|
|
35
36
|
if (json) { process.stdout.write(JSON.stringify(report, null, 2) + '\n'); if (!report.healthy) process.exitCode = 1; return report; }
|
|
36
37
|
const isTty = process.stdout && process.stdout.isTTY;
|
|
37
38
|
const gr = s => isTty ? `\x1b[32m${s}\x1b[0m` : s, rd = s => isTty ? `\x1b[31m${s}\x1b[0m` : s, cy = s => isTty ? `\x1b[36m${s}\x1b[0m` : s, dm = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
38
|
-
log(cy(`# leerness doctor — 설치/환경
|
|
39
|
+
log(cy(t(`# leerness doctor — 설치/환경 진단`, `# leerness doctor — install/environment diagnosis`)));
|
|
39
40
|
log('');
|
|
40
41
|
log(` ${gr('✓')} version ${VERSION} · node ${process.version} · ${process.platform}/${process.arch}`);
|
|
41
|
-
log(` ${gr('✓')} 설치
|
|
42
|
-
log(` ${gr('✓')} MCP
|
|
42
|
+
log(` ${gr('✓')} ${t('설치 경로', 'install path')}: ${dm(harnessPath)}`);
|
|
43
|
+
log(` ${gr('✓')} ${t('MCP 도구', 'MCP tools')}: ${mcpCount}`);
|
|
43
44
|
// 1.13.1 (15th 블라인드 리뷰 P3, Sonnet): pass 수에 '실패' 가 붙어 "209/210 실패"(=209건 실패로 오독)되던 문구 → "통과 N/M (K건 실패)" 로 명확화.
|
|
44
|
-
log(` ${report.selftest.ok ? gr('✓') : rd('✗')} selftest: ${pass}/${total}
|
|
45
|
+
log(` ${report.selftest.ok ? gr('✓') : rd('✗')} selftest: ${pass}/${total} ${t('통과', 'passed')}${report.selftest.ok ? '' : t(` (${total - pass}건 실패)`, ` (${total - pass} failed)`)}`);
|
|
45
46
|
if (!report.selftest.ok) report.selftest.failed.slice(0, 5).forEach(n => log(rd(` ✗ ${n}`)));
|
|
46
|
-
log(` ${gr('✓')}
|
|
47
|
+
log(` ${gr('✓')} ${t('셸', 'shell')}: ${shell || 'unknown'}${psVersion && shell === 'powershell' ? ` (PowerShell ${psVersion})` : ''}`);
|
|
47
48
|
log('');
|
|
48
|
-
log(report.healthy ? gr(' ✓ leerness 설치 정상') : rd(' ✗ 문제 감지 — 재설치: npm i -g leerness@latest · 진단: leerness which'));
|
|
49
|
+
log(report.healthy ? gr(t(' ✓ leerness 설치 정상', ' ✓ leerness install OK')) : rd(t(' ✗ 문제 감지 — 재설치: npm i -g leerness@latest · 진단: leerness which', ' ✗ problem detected — reinstall: npm i -g leerness@latest · diagnose: leerness which')));
|
|
49
50
|
if (!report.healthy) process.exitCode = 1;
|
|
50
51
|
return report;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
function whichCmd(deps = {}) {
|
|
54
|
-
const { VERSION, has, harnessPath } = deps;
|
|
55
|
+
const { VERSION, uiLang, has, harnessPath } = deps;
|
|
56
|
+
const t = (ko, en) => (uiLang === 'en' ? en : ko); // 1.28.2 (UR-0010 Phase 10c)
|
|
55
57
|
const out = {
|
|
56
58
|
version: VERSION,
|
|
57
59
|
runningFrom: harnessPath,
|
|
@@ -91,48 +93,49 @@ function whichCmd(deps = {}) {
|
|
|
91
93
|
// 진단: 글로벌 설치된 leerness 와 현재 실행 버전이 다르면 경고
|
|
92
94
|
out.diagnostics = [];
|
|
93
95
|
if (out.npm.globalInstalled && out.npm.globalInstalled !== VERSION) {
|
|
94
|
-
out.diagnostics.push(`⚠ 글로벌 설치 ${out.npm.globalInstalled} ≠ 현재 실행 ${VERSION} — npx 캐시 또는 PATH 충돌
|
|
95
|
-
out.diagnostics.push(` → 강제 최신: npm i -g leerness@latest / 또는 npx --yes leerness@latest <command>`);
|
|
96
|
+
out.diagnostics.push(t(`⚠ 글로벌 설치 ${out.npm.globalInstalled} ≠ 현재 실행 ${VERSION} — npx 캐시 또는 PATH 충돌 의심`, `⚠ global install ${out.npm.globalInstalled} ≠ running ${VERSION} — suspect npx cache or PATH conflict`));
|
|
97
|
+
out.diagnostics.push(t(` → 강제 최신: npm i -g leerness@latest / 또는 npx --yes leerness@latest <command>`, ` → force latest: npm i -g leerness@latest / or npx --yes leerness@latest <command>`));
|
|
96
98
|
}
|
|
97
99
|
if (out.pathCandidates.length > 1) {
|
|
98
|
-
out.diagnostics.push(`⚠ PATH 에 leerness 가 ${out.pathCandidates.length}개 — 우선순위 충돌
|
|
99
|
-
out.diagnostics.push(` → 명시적 경로 사용: ${out.runningFrom}`);
|
|
100
|
+
out.diagnostics.push(t(`⚠ PATH 에 leerness 가 ${out.pathCandidates.length}개 — 우선순위 충돌 가능`, `⚠ leerness appears ${out.pathCandidates.length}× on PATH — possible precedence conflict`));
|
|
101
|
+
out.diagnostics.push(t(` → 명시적 경로 사용: ${out.runningFrom}`, ` → use the explicit path: ${out.runningFrom}`));
|
|
100
102
|
}
|
|
101
103
|
if (has('--json')) { log(JSON.stringify(out, null, 2)); return; }
|
|
102
104
|
log(`# leerness which (1.9.164)`);
|
|
103
|
-
log(
|
|
104
|
-
log(
|
|
105
|
+
log(`${t('현재 실행', 'running from')}: ${out.runningFrom}`);
|
|
106
|
+
log(`${t('버전', 'version')}: v${out.version}`);
|
|
105
107
|
log(`Node: ${out.nodeVersion} (${out.platform}/${out.arch})`);
|
|
106
108
|
log('');
|
|
107
|
-
log(`## npm
|
|
109
|
+
log(t(`## npm 환경`, `## npm environment`));
|
|
108
110
|
if (out.npm.globalRoot) log(` npm root -g: ${out.npm.globalRoot}`);
|
|
109
|
-
if (out.npm.cacheDir) log(` npm cache: ${out.npm.cacheDir} (npx 옛 버전이 여기 캐싱 — 의심 시 \`npm cache clean --force\`)`);
|
|
110
|
-
if (out.npm.globalInstalled) log(` 글로벌 설치: leerness@${out.npm.globalInstalled}`);
|
|
111
|
-
else log(` 글로벌 설치: (없음 — npx 또는 로컬 경로만 사용 중)`);
|
|
111
|
+
if (out.npm.cacheDir) log(t(` npm cache: ${out.npm.cacheDir} (npx 옛 버전이 여기 캐싱 — 의심 시 \`npm cache clean --force\`)`, ` npm cache: ${out.npm.cacheDir} (npx caches old versions here — if suspect: \`npm cache clean --force\`)`));
|
|
112
|
+
if (out.npm.globalInstalled) log(t(` 글로벌 설치: leerness@${out.npm.globalInstalled}`, ` global install: leerness@${out.npm.globalInstalled}`));
|
|
113
|
+
else log(t(` 글로벌 설치: (없음 — npx 또는 로컬 경로만 사용 중)`, ` global install: (none — using npx or a local path only)`));
|
|
112
114
|
if (out.pathCandidates.length) {
|
|
113
115
|
log('');
|
|
114
|
-
log(`## PATH 후보 (${out.pathCandidates.length}개)`);
|
|
116
|
+
log(t(`## PATH 후보 (${out.pathCandidates.length}개)`, `## PATH candidates (${out.pathCandidates.length})`));
|
|
115
117
|
for (const p of out.pathCandidates) log(` - ${p}`);
|
|
116
118
|
}
|
|
117
119
|
if (out.diagnostics.length) {
|
|
118
120
|
log('');
|
|
119
|
-
log(`## ⚠
|
|
121
|
+
log(t(`## ⚠ 진단`, `## ⚠ diagnostics`));
|
|
120
122
|
for (const d of out.diagnostics) log(` ${d}`);
|
|
121
123
|
} else {
|
|
122
124
|
log('');
|
|
123
|
-
log(`✓ 충돌 없음 (현재 실행 버전 = 글로벌 설치 버전)`);
|
|
125
|
+
log(t(`✓ 충돌 없음 (현재 실행 버전 = 글로벌 설치 버전)`, `✓ no conflict (running version = global install version)`));
|
|
124
126
|
}
|
|
125
127
|
log('');
|
|
126
|
-
log(`💡 강제 최신 실행
|
|
127
|
-
log(` 1) npx --yes leerness@latest <command> # npx 캐시 무시하고 최신
|
|
128
|
-
log(` 2) npm i -g leerness@latest # 글로벌 설치
|
|
129
|
-
log(` 3) npm cache clean --force # npx 캐시 강제 비우기 (의심 시)`);
|
|
128
|
+
log(t(`💡 강제 최신 실행 방법:`, `💡 how to force the latest:`));
|
|
129
|
+
log(t(` 1) npx --yes leerness@latest <command> # npx 캐시 무시하고 최신 다운로드`, ` 1) npx --yes leerness@latest <command> # bypass npx cache, download latest`));
|
|
130
|
+
log(t(` 2) npm i -g leerness@latest # 글로벌 설치 갱신`, ` 2) npm i -g leerness@latest # update the global install`));
|
|
131
|
+
log(t(` 3) npm cache clean --force # npx 캐시 강제 비우기 (의심 시)`, ` 3) npm cache clean --force # force-clear the npx cache (if suspect)`));
|
|
130
132
|
}
|
|
131
133
|
|
|
132
134
|
// 1.9.394 (UR-0025): whats-new — 현재 워크스페이스 버전 → 도구 버전 CHANGELOG 차분(신규 명령/플래그/파일 요약). introspection 핸들러.
|
|
133
135
|
// 순수 파서 _parseChangelogBetween(pure-utils) 사용. deps: VERSION/arg/has. CHANGELOG 경로는 root 우선, 없으면 pkg 자체(lib/../).
|
|
134
136
|
function whatsNewCmd(root, deps = {}) {
|
|
135
|
-
const { VERSION, arg, has } = deps;
|
|
137
|
+
const { VERSION, uiLang, arg, has } = deps;
|
|
138
|
+
const t = (ko, en) => (uiLang === 'en' ? en : ko); // 1.28.2 (UR-0010 Phase 10c)
|
|
136
139
|
root = absRoot(root || process.cwd());
|
|
137
140
|
const fromV = arg('--from', null) || (function () {
|
|
138
141
|
const hv = path.join(root, '.harness', 'HARNESS_VERSION');
|
|
@@ -141,29 +144,29 @@ function whatsNewCmd(root, deps = {}) {
|
|
|
141
144
|
})();
|
|
142
145
|
const toV = arg('--to', null) || VERSION;
|
|
143
146
|
if (!fromV) {
|
|
144
|
-
fail('현재 버전을 파악할 수 없습니다. --from <version> 명시');
|
|
147
|
+
fail(t('현재 버전을 파악할 수 없습니다. --from <version> 명시', 'cannot determine current version. specify --from <version>'));
|
|
145
148
|
return process.exit(1);
|
|
146
149
|
}
|
|
147
150
|
// CHANGELOG.md — 우선 root, 없으면 leerness-pkg 자체 (lib/../CHANGELOG.md = pkg 루트)
|
|
148
151
|
let changelogPath = path.join(root, 'CHANGELOG.md');
|
|
149
152
|
if (!exists(changelogPath)) changelogPath = path.join(__dirname, '..', 'CHANGELOG.md');
|
|
150
153
|
if (!exists(changelogPath)) {
|
|
151
|
-
fail('CHANGELOG.md 없음');
|
|
154
|
+
fail(t('CHANGELOG.md 없음', 'CHANGELOG.md not found'));
|
|
152
155
|
return process.exit(1);
|
|
153
156
|
}
|
|
154
157
|
const diff = _parseChangelogBetween(read(changelogPath), fromV, toV);
|
|
155
158
|
if (has('--json')) { log(JSON.stringify({ from: fromV, to: toV, versions: diff }, null, 2)); return; }
|
|
156
159
|
if (!diff.length) {
|
|
157
160
|
log(`# leerness whats-new (1.9.41)`);
|
|
158
|
-
log(`현재 ${fromV} ↔ 대상 ${toV}: 새 항목 없음 (또는 CHANGELOG에 기록 안 됨)`);
|
|
161
|
+
log(t(`현재 ${fromV} ↔ 대상 ${toV}: 새 항목 없음 (또는 CHANGELOG에 기록 안 됨)`, `current ${fromV} ↔ target ${toV}: no new entries (or not recorded in CHANGELOG)`));
|
|
159
162
|
return;
|
|
160
163
|
}
|
|
161
164
|
log(`# leerness whats-new (1.9.41)`);
|
|
162
|
-
log(`현재 워크스페이스 버전: ${fromV} → 대상: ${toV}`);
|
|
163
|
-
log(`범위: ${diff.length}개 버전 (${diff[0].version} → ${diff[diff.length - 1].version})`);
|
|
165
|
+
log(t(`현재 워크스페이스 버전: ${fromV} → 대상: ${toV}`, `current workspace version: ${fromV} → target: ${toV}`));
|
|
166
|
+
log(t(`범위: ${diff.length}개 버전 (${diff[0].version} → ${diff[diff.length - 1].version})`, `range: ${diff.length} version(s) (${diff[0].version} → ${diff[diff.length - 1].version})`));
|
|
164
167
|
log('');
|
|
165
168
|
// AI 가독 요약 — 각 버전당 한 줄 + 신규 명령/플래그/파일
|
|
166
|
-
log(`## 🆕 신규 명령·플래그·파일 (AI 에이전트는 다음 명령을 우선 시도)`);
|
|
169
|
+
log(t(`## 🆕 신규 명령·플래그·파일 (AI 에이전트는 다음 명령을 우선 시도)`, `## 🆕 new commands·flags·files (AI agents: try these first)`));
|
|
167
170
|
const allCommands = new Set();
|
|
168
171
|
const allFlags = new Set();
|
|
169
172
|
const allFiles = new Set();
|
|
@@ -172,11 +175,11 @@ function whatsNewCmd(root, deps = {}) {
|
|
|
172
175
|
v.newFlags.forEach(f => allFlags.add(f));
|
|
173
176
|
v.newFiles.forEach(f => allFiles.add(f));
|
|
174
177
|
}
|
|
175
|
-
if (allCommands.size) log(` 📌 신규 명령: ${[...allCommands].join(', ')}`);
|
|
176
|
-
if (allFlags.size) log(` 🚩 신규 플래그: ${[...allFlags].join(', ')}`);
|
|
177
|
-
if (allFiles.size) log(` 📄 신규 파일: ${[...allFiles].join(', ')}`);
|
|
178
|
+
if (allCommands.size) log(t(` 📌 신규 명령: ${[...allCommands].join(', ')}`, ` 📌 new commands: ${[...allCommands].join(', ')}`));
|
|
179
|
+
if (allFlags.size) log(t(` 🚩 신규 플래그: ${[...allFlags].join(', ')}`, ` 🚩 new flags: ${[...allFlags].join(', ')}`));
|
|
180
|
+
if (allFiles.size) log(t(` 📄 신규 파일: ${[...allFiles].join(', ')}`, ` 📄 new files: ${[...allFiles].join(', ')}`));
|
|
178
181
|
log('');
|
|
179
|
-
log(`## 📜 버전별
|
|
182
|
+
log(t(`## 📜 버전별 헤드라인`, `## 📜 per-version headlines`));
|
|
180
183
|
for (const v of diff) {
|
|
181
184
|
// body 첫 줄(또는 strong header) 추출
|
|
182
185
|
const firstLine = (v.body.match(/^\*\*([^*]+)\*\*/) || [])[1]
|
|
@@ -184,11 +187,11 @@ function whatsNewCmd(root, deps = {}) {
|
|
|
184
187
|
log(` • ${v.version}${v.date ? ` (${v.date})` : ''} — ${firstLine || '(no headline)'}`);
|
|
185
188
|
}
|
|
186
189
|
log('');
|
|
187
|
-
log(`## 💡 권장
|
|
188
|
-
log(` 1. 위 신규 명령들을 시도해 보세요 (예: \`leerness <명령> --help\`)`);
|
|
189
|
-
log(` 2. 신규 파일들을 읽어 보세요 (예: \`cat .harness/session-workflow.md\`)`);
|
|
190
|
-
log(` 3. AGENTS.md/CLAUDE.md 재독 — migrate가 인스트럭션을 업데이트했을 수
|
|
191
|
-
log(` 4. 상세: \`cat CHANGELOG.md\` 또는 \`leerness whats-new --json\``);
|
|
190
|
+
log(t(`## 💡 권장 행동`, `## 💡 recommended actions`));
|
|
191
|
+
log(t(` 1. 위 신규 명령들을 시도해 보세요 (예: \`leerness <명령> --help\`)`, ` 1. Try the new commands above (e.g. \`leerness <command> --help\`)`));
|
|
192
|
+
log(t(` 2. 신규 파일들을 읽어 보세요 (예: \`cat .harness/session-workflow.md\`)`, ` 2. Read the new files (e.g. \`cat .harness/session-workflow.md\`)`));
|
|
193
|
+
log(t(` 3. AGENTS.md/CLAUDE.md 재독 — migrate가 인스트럭션을 업데이트했을 수 있음`, ` 3. Re-read AGENTS.md/CLAUDE.md — migrate may have updated the instructions`));
|
|
194
|
+
log(t(` 4. 상세: \`cat CHANGELOG.md\` 또는 \`leerness whats-new --json\``, ` 4. Details: \`cat CHANGELOG.md\` or \`leerness whats-new --json\``));
|
|
192
195
|
}
|
|
193
196
|
|
|
194
197
|
module.exports = { doctorCmd, whichCmd, whatsNewCmd };
|
package/lib/drift.js
CHANGED
|
@@ -168,24 +168,25 @@ function driftCheckCmd(root, opts = {}, deps = {}) {
|
|
|
168
168
|
// 재귀(_noAutoFix)는 auto-fix 블록을 건너뛰고 마지막 JSON(아래 has('--json') 블록)만 출력 → afLog 로 첫 패스 진행로그만 무음화.
|
|
169
169
|
const afLog = has('--json') ? () => {} : log;
|
|
170
170
|
// 1.9.82: 보안 신호가 fired에 있으면 우선 audit --fix 호출
|
|
171
|
-
|
|
171
|
+
// 1.28.1 (Phase 10b): 보안 신호 판정을 *언어-안정* 한 file 필드로 — 1.27.2 에서 label 을 언어화하면서 한국어 라벨 regex 매칭이 --language en 에서 깨지던 버그 수정(보안 auto-fix 미발동 방지).
|
|
172
|
+
const hasSecurityFired = fired.some(f => f.file === '.env / .gitignore');
|
|
172
173
|
if (autoFix && hasSecurityFired) {
|
|
173
174
|
afLog('');
|
|
174
|
-
afLog(`🔒 --auto-fix 활성 (1.9.82) — 보안 신호 회복: audit --fix 자동 실행
|
|
175
|
+
afLog(t(`🔒 --auto-fix 활성 (1.9.82) — 보안 신호 회복: audit --fix 자동 실행 중...`, `🔒 --auto-fix on — recovering security signal: running audit --fix...`));
|
|
175
176
|
try {
|
|
176
177
|
const r = cp.spawnSync(process.execPath, [harnessPath, 'audit', root, '--fix'],
|
|
177
178
|
{ encoding: 'utf8', timeout: 30000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' } });
|
|
178
179
|
if (r.status === 0) {
|
|
179
|
-
afLog(`✓ audit --fix 완료 — .gitignore + .env.example
|
|
180
|
+
afLog(t(`✓ audit --fix 완료 — .gitignore + .env.example 동기화`, `✓ audit --fix done — .gitignore + .env.example synced`));
|
|
180
181
|
// 재검사 (보안 신호 회복 확인)
|
|
181
182
|
afLog('');
|
|
182
|
-
afLog(`재검사
|
|
183
|
+
afLog(t(`재검사 중...`, `re-checking...`));
|
|
183
184
|
return driftCheckCmd(root, { ...opts, _noAutoFix: true }, deps); // 재귀 1회 (auto-fix 없이, 1.9.432 depth 가드)
|
|
184
185
|
} else {
|
|
185
|
-
afLog(`⚠ audit --fix 실패 (exit ${r.status}) — 수동 \`leerness audit --fix\`
|
|
186
|
+
afLog(t(`⚠ audit --fix 실패 (exit ${r.status}) — 수동 \`leerness audit --fix\` 권장`, `⚠ audit --fix failed (exit ${r.status}) — run \`leerness audit --fix\` manually`));
|
|
186
187
|
}
|
|
187
188
|
} catch (e) {
|
|
188
|
-
afLog(`⚠ auto-fix 보안 회복 오류: ${e.message}`);
|
|
189
|
+
afLog(t(`⚠ auto-fix 보안 회복 오류: ${e.message}`, `⚠ auto-fix security recovery error: ${e.message}`));
|
|
189
190
|
}
|
|
190
191
|
}
|
|
191
192
|
// 1.9.242: drift check --auto-fix 에 env encoding BOM 자동 추가 통합 (사용자 명시 UR-0014 2단계)
|
|
@@ -195,7 +196,7 @@ function driftCheckCmd(root, opts = {}, deps = {}) {
|
|
|
195
196
|
const encScan = _scanShellScriptsEncoding(root);
|
|
196
197
|
if (encScan.atRisk && encScan.atRisk.length > 0) {
|
|
197
198
|
afLog('');
|
|
198
|
-
afLog(`🌐 --auto-fix 활성 (1.9.242) — 셸 스크립트 인코딩 위험 ${encScan.atRisk.length}건 BOM 자동 추가
|
|
199
|
+
afLog(t(`🌐 --auto-fix 활성 (1.9.242) — 셸 스크립트 인코딩 위험 ${encScan.atRisk.length}건 BOM 자동 추가 중...`, `🌐 --auto-fix on — adding UTF-8 BOM to ${encScan.atRisk.length} at-risk shell script(s)...`));
|
|
199
200
|
let ok = 0;
|
|
200
201
|
for (const r of encScan.atRisk) {
|
|
201
202
|
try {
|
|
@@ -207,10 +208,10 @@ function driftCheckCmd(root, opts = {}, deps = {}) {
|
|
|
207
208
|
ok++;
|
|
208
209
|
} catch {}
|
|
209
210
|
}
|
|
210
|
-
afLog(`✓ UTF-8 BOM 추가 ${ok}/${encScan.atRisk.length}건 (1.9.242 UR-0014)`);
|
|
211
|
+
afLog(t(`✓ UTF-8 BOM 추가 ${ok}/${encScan.atRisk.length}건 (1.9.242 UR-0014)`, `✓ added UTF-8 BOM ${ok}/${encScan.atRisk.length}`));
|
|
211
212
|
}
|
|
212
213
|
} catch (e) {
|
|
213
|
-
afLog(`⚠ env encoding auto-fix 오류 (1.9.242): ${e.message}`);
|
|
214
|
+
afLog(t(`⚠ env encoding auto-fix 오류 (1.9.242): ${e.message}`, `⚠ env encoding auto-fix error: ${e.message}`));
|
|
214
215
|
}
|
|
215
216
|
}
|
|
216
217
|
// 1.9.225: drift check --auto-fix 에 delivered 패턴 자동 적용 통합 (1.9.223/224 시스템 회수)
|
|
@@ -221,16 +222,16 @@ function driftCheckCmd(root, opts = {}, deps = {}) {
|
|
|
221
222
|
const delivered = _detectDeliveredRequests(root);
|
|
222
223
|
if (delivered.candidates && delivered.candidates.length > 0) {
|
|
223
224
|
afLog('');
|
|
224
|
-
afLog(`📥 --auto-fix 활성 (1.9.225) — delivered 패턴 ${delivered.candidates.length}건 자동 완료
|
|
225
|
+
afLog(t(`📥 --auto-fix 활성 (1.9.225) — delivered 패턴 ${delivered.candidates.length}건 자동 완료 중...`, `📥 --auto-fix on — auto-completing ${delivered.candidates.length} delivered pattern(s)...`));
|
|
225
226
|
let ok = 0;
|
|
226
227
|
for (const c of delivered.candidates) {
|
|
227
228
|
const u = _updateUserRequest(root, c.id, { status: 'completed', autoCompletedAt: new Date().toISOString(), autoCompleteReason: 'drift-auto-fix-1.9.225' });
|
|
228
229
|
if (u) ok++;
|
|
229
230
|
}
|
|
230
|
-
afLog(`✓ delivered 자동 완료 ${ok}/${delivered.candidates.length}
|
|
231
|
+
afLog(t(`✓ delivered 자동 완료 ${ok}/${delivered.candidates.length}건`, `✓ delivered auto-completed ${ok}/${delivered.candidates.length}`));
|
|
231
232
|
}
|
|
232
233
|
} catch (e) {
|
|
233
|
-
afLog(`⚠ delivered auto-apply 오류 (1.9.225): ${e.message}`);
|
|
234
|
+
afLog(t(`⚠ delivered auto-apply 오류 (1.9.225): ${e.message}`, `⚠ delivered auto-apply error: ${e.message}`));
|
|
234
235
|
}
|
|
235
236
|
}
|
|
236
237
|
// 1.9.293: drift check --auto-fix 에 idempotency task/user-request 중복 자동 정리 통합
|
|
@@ -241,10 +242,10 @@ function driftCheckCmd(root, opts = {}, deps = {}) {
|
|
|
241
242
|
const totalFixed = idemFixes.reduce((n, f) => n + (f.removedExact || 0) + (f.droppedSameText || 0) + (f.count || 0), 0);
|
|
242
243
|
if (totalFixed > 0) {
|
|
243
244
|
afLog('');
|
|
244
|
-
afLog(`🔁 --auto-fix 활성 (1.9.293) — idempotency 중복 ${totalFixed}건 자동 정리 (task/user-request dedup)`);
|
|
245
|
+
afLog(t(`🔁 --auto-fix 활성 (1.9.293) — idempotency 중복 ${totalFixed}건 자동 정리 (task/user-request dedup)`, `🔁 --auto-fix on — deduped ${totalFixed} idempotency duplicate(s) (task/user-request)`));
|
|
245
246
|
}
|
|
246
247
|
} catch (e) {
|
|
247
|
-
afLog(`⚠ idempotency auto-fix 오류 (1.9.293): ${e.message}`);
|
|
248
|
+
afLog(t(`⚠ idempotency auto-fix 오류 (1.9.293): ${e.message}`, `⚠ idempotency auto-fix error: ${e.message}`));
|
|
248
249
|
}
|
|
249
250
|
}
|
|
250
251
|
// 1.9.236: drift check --auto-fix 에 release cleanup 통합 (1.9.235 회수)
|
|
@@ -260,7 +261,7 @@ function driftCheckCmd(root, opts = {}, deps = {}) {
|
|
|
260
261
|
.filter(l => l && /^release\/\d+\.\d+\.\d+$/.test(l));
|
|
261
262
|
if (merged.length > 50) {
|
|
262
263
|
afLog('');
|
|
263
|
-
afLog(`🗑 --auto-fix 활성 (1.9.236) — release/* merged ${merged.length}개 (50+) 자동 정리 (keep 10)...`);
|
|
264
|
+
afLog(t(`🗑 --auto-fix 활성 (1.9.236) — release/* merged ${merged.length}개 (50+) 자동 정리 (keep 10)...`, `🗑 --auto-fix on — cleaning up ${merged.length} merged release/* branches (50+, keep 10)...`));
|
|
264
265
|
// 정렬 (semver desc)
|
|
265
266
|
merged.sort((a, b) => {
|
|
266
267
|
const va = a.replace('release/', '').split('.').map(n => parseInt(n, 10) || 0);
|
|
@@ -276,20 +277,20 @@ function driftCheckCmd(root, opts = {}, deps = {}) {
|
|
|
276
277
|
const r = cp.spawnSync('git', ['branch', '-d', b], { cwd: root, encoding: 'utf8' });
|
|
277
278
|
if (r.status === 0) ok++;
|
|
278
279
|
}
|
|
279
|
-
afLog(`✓ release cleanup 자동 완료 ${ok}/${toDelete.length}건 (keep 10)`);
|
|
280
|
+
afLog(t(`✓ release cleanup 자동 완료 ${ok}/${toDelete.length}건 (keep 10)`, `✓ release cleanup done ${ok}/${toDelete.length} (keep 10)`));
|
|
280
281
|
}
|
|
281
282
|
}
|
|
282
283
|
} catch (e) {
|
|
283
|
-
afLog(`⚠ release cleanup auto-fix 오류 (1.9.236): ${e.message}`);
|
|
284
|
+
afLog(t(`⚠ release cleanup auto-fix 오류 (1.9.236): ${e.message}`, `⚠ release cleanup auto-fix error: ${e.message}`));
|
|
284
285
|
}
|
|
285
286
|
}
|
|
286
287
|
if (autoFix && level === '🔴 critical' && !hasSecurityFired) {
|
|
287
288
|
afLog('');
|
|
288
|
-
afLog(`🔧 --auto-fix 활성 — session close 자동 실행
|
|
289
|
+
afLog(t(`🔧 --auto-fix 활성 — session close 자동 실행 중...`, `🔧 --auto-fix on — running session close...`));
|
|
289
290
|
try {
|
|
290
291
|
const r = cp.spawnSync(process.execPath, [harnessPath, 'session', 'close', root], { encoding: 'utf8', timeout: 60000, env: { ...process.env, LEERNESS_INTERNAL: '1' } });
|
|
291
292
|
if (r.status === 0) {
|
|
292
|
-
afLog(`✓ session close 자동
|
|
293
|
+
afLog(t(`✓ session close 자동 완료`, `✓ session close done`));
|
|
293
294
|
// autoResolved 카운트
|
|
294
295
|
const stats = _readUsageStats(root);
|
|
295
296
|
stats.drift = stats.drift || {};
|
|
@@ -299,13 +300,13 @@ function driftCheckCmd(root, opts = {}, deps = {}) {
|
|
|
299
300
|
writeUtf8(p, JSON.stringify(stats, null, 2) + '\n');
|
|
300
301
|
// 재검사
|
|
301
302
|
afLog('');
|
|
302
|
-
afLog(`재검사
|
|
303
|
+
afLog(t(`재검사 중...`, `re-checking...`));
|
|
303
304
|
return driftCheckCmd(root, { ...opts, _noAutoFix: true }, deps); // 재귀 1회 (auto-fix 없이, 1.9.432 depth 가드)
|
|
304
305
|
} else {
|
|
305
|
-
afLog(`⚠ session close 실패 (exit ${r.status}) — 수동 실행
|
|
306
|
+
afLog(t(`⚠ session close 실패 (exit ${r.status}) — 수동 실행 필요`, `⚠ session close failed (exit ${r.status}) — run manually`));
|
|
306
307
|
}
|
|
307
308
|
} catch (e) {
|
|
308
|
-
afLog(`⚠ auto-fix 오류: ${e.message}`);
|
|
309
|
+
afLog(t(`⚠ auto-fix 오류: ${e.message}`, `⚠ auto-fix error: ${e.message}`));
|
|
309
310
|
}
|
|
310
311
|
}
|
|
311
312
|
if (has('--json')) {
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -6234,10 +6234,72 @@ total++;
|
|
|
6234
6234
|
const drEn = out(cp.spawnSync(process.execPath, [CLI, 'drift', 'check', d, '--language', 'en'], { encoding: 'utf8', timeout: 20000 }));
|
|
6235
6235
|
const drKo = out(cp.spawnSync(process.execPath, [CLI, 'drift', 'check', d], { encoding: 'utf8', timeout: 20000 }));
|
|
6236
6236
|
const driftOk = /signal \| age \| threshold/.test(drEn) && !H.test(drEn) && /신호 \| age \| 임계/.test(drKo);
|
|
6237
|
+
// ⑦ (1.28.2 Phase 10c) doctor: en 영어(한글 0) + ko 기본 한글 보존
|
|
6238
|
+
const docEn = out(cp.spawnSync(process.execPath, [CLI, 'doctor', '--language', 'en'], { encoding: 'utf8', timeout: 20000, cwd: d }));
|
|
6239
|
+
const docKo = out(cp.spawnSync(process.execPath, [CLI, 'doctor'], { encoding: 'utf8', timeout: 20000, cwd: d }));
|
|
6240
|
+
const doctorOk = /install\/environment diagnosis/.test(docEn) && !H.test(docEn) && /설치\/환경 진단/.test(docKo);
|
|
6237
6241
|
fs.rmSync(d, { recursive: true, force: true });
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
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 행위 회귀 가드 실패');
|
|
6241
6303
|
if (!ok) failed++;
|
|
6242
6304
|
}
|
|
6243
6305
|
|