leerness 1.9.267 → 1.9.269

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,44 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.269 — 2026-06-02 — UR-0022: init 시 OS 시스템 언어 감지 → 설치 가이드 자동 언어 선택
4
+
5
+ **🌐 `npx leerness init` 등 설치 시 OS 시스템 언어를 감지해 설치 가이드/생성 문서를 알맞은 언어로 표시 (사용자 명시 UR-0022).**
6
+
7
+ ### 배경
8
+ 기존 `detectLanguageValue(root, 'auto')`는 프로젝트 파일(README/guideline 등)의 **한글 포함 여부만** 판별했음. 그 결과 빈 디렉토리에서 신규 `npx leerness init` 시 콘텐츠가 없어 한국어 OS 에서도 항상 영어(en)로 폴백됐음. 사용자 요청은 OS locale 기반으로 설치 언어를 자동 결정하는 것.
9
+
10
+ ### 구현
11
+ 1. **`_detectSystemLang(env)` 순수 함수 신설** — OS locale 감지. 우선순위: POSIX 환경변수(`LC_ALL`>`LC_CTYPE`>`LANG`>`LANGUAGE`) > Node ICU `Intl.DateTimeFormat().resolvedOptions().locale`(Windows 등 LANG 미설정 시 OS 언어 회수) > `null`. `C`/`POSIX` 로케일은 스킵. env 주입 가능(테스트).
12
+ 2. **`detectLanguageValue` auto 우선순위 개선** — ① 프로젝트 콘텐츠 한글 → ② **콘텐츠가 비어있을 때만**(신규/빈 디렉토리 init) OS 시스템 언어 → ③ en 폴백. **콘텐츠가 있는 영어 프로젝트는 en 유지**(기존 프로젝트 회귀 방지).
13
+ 3. **비대화형 설치 투명성 안내** — `npx leerness init --yes`(auto) 시 `🌐 시스템 언어 감지: KO (→ KO)` 한 줄 노출. `--language` 명시 시 미발화.
14
+ 4. **언어 선택 UI 설명 갱신** — `install.lang.auto.desc` 에 "시스템(OS) 언어 자동 판별" 반영.
15
+ 5. **selftest 20 → 22** — `_detectSystemLang` POSIX 파싱(ko_KR/en_US) + LC_ALL 우선/LANGUAGE 폴백 2종.
16
+ 6. **e2e +1 (217 → 218)** — 빈 디렉토리 init auto + `LANG=en_US`/`ko_KR` → `.harness/LANGUAGE` 가 각각 en/ko 로 결정되는지 통합 검증.
17
+
18
+ ### 검증
19
+ - **selftest 22/22 PASS** · **E2E 218/218 PASS** (회귀 0).
20
+ - 기존 init e2e 는 모두 `--language ko` 명시 → auto 변경 영향 없음(회귀 0 확인).
21
+
22
+ ## 1.9.268 — 2026-06-02 — grok 정식 EXTERNAL_AGENTS provider 승격 (1.9.266 후속 task)
23
+
24
+ **🤖 grok 을 슬래시 레지스트리 전용에서 정식 provider 로 승격 — provider cycle / setup-agents / dispatch / `--help` probe 가 grok 도 자동 처리.**
25
+
26
+ ### 배경
27
+ 1.9.265~267(UR-0021)에서 grok 은 `AGENT_SLASH_COMMANDS` 슬래시 레지스트리에만 포함됐을 뿐 `EXTERNAL_AGENTS` 정식 provider 는 아니었음(1.9.266 CHANGELOG 명시 후속 task). 그 결과 `slash-commands --refresh` probe 대상에서 제외되고, install 흐름 6선택지·provider cycle·setup-agents 에서도 빠졌음. 1.9.267 의 `--help` probe(3단계)가 완성되면서 grok 을 정식 편입할 자연스러운 시점.
28
+
29
+ ### 구현
30
+ 1. **`EXTERNAL_AGENTS` 에 grok 추가** — `{ id:'grok', bin:'grok', envFlag:'LEERNESS_ENABLE_GROK', versionArgs:['--version'], installCmd:'npm i -g @vibe-kit/grok-cli' }`. 빌트인 5종 → **6종** (claude/codex/agy/**grok**/copilot/ollama).
31
+ 2. **자동 전파** — EXTERNAL_AGENTS 를 순회하는 모든 소비처가 grok 흡수: `_checkAgent`(ready 감지)·`provider list`(6 builtin)·`slash-commands --refresh`(probe 대상)·`setup-agents`·`agents dispatch`·`agents list/quota`.
32
+ 3. **install 흐름 6선택지 확장** — 대화형 `_selectMany` + 비대화형 `1~6` 맵에 grok 추가(`all` 배열 포함). `4) grok (xAI)` 신설로 5→6 선택지.
33
+ 4. **비시크릿 키 + config 템플릿** — `_LEERNESS_NONSECRET_KEYS` 및 `.harness/leerness-config.json` 템플릿에 `LEERNESS_ENABLE_GROK` 추가(AI 가시성 유지, 시크릿 아님).
34
+ 5. **슬래시 레지스트리 note 갱신** — grok `asOf: 1.9.268` + "정식 provider 승격 — --refresh 자동 probe 가능".
35
+ 6. **selftest 19 → 20** — grok EXTERNAL_AGENTS 편입(bin/envFlag/length===6) 검증.
36
+
37
+ ### 검증
38
+ - **selftest 20/20 PASS** · **E2E 217/217 PASS** (회귀 0).
39
+ - `provider list` → 총 6개(빌트인 6), grok=builtin 확인.
40
+ - `slash-commands grok --refresh --dry-run` → 이전 "probe 대상 아님" → 이제 probe 시도(미설치 시 ENOENT graceful fallback) — 3단계 probe 통합 확인.
41
+
3
42
  ## 1.9.267 — 2026-06-02 — UR-0021 3단계: CLI `--help` probe 슬래시 레지스트리 자동 refresh
4
43
 
5
44
  **🔄 UR-0021 백로그 완전 소진 — 설치된 CLI 의 `--help` 출력을 probe 해 슬래시 명령 레지스트리를 자동 갱신 (best-effort, offline-first).**
package/bin/harness.js CHANGED
@@ -7,7 +7,7 @@ const cp = require('child_process');
7
7
  const os = require('os'); // 1.9.178: _publishToNpm 에서 os.tmpdir() 사용 (전역 import)
8
8
  const readline = require('readline');
9
9
 
10
- const VERSION = '1.9.267';
10
+ const VERSION = '1.9.269';
11
11
 
12
12
  // 1.9.184: DEP0190 (child_process shell: true) deprecation warning 억제 (사용자 명시).
13
13
  // leerness 는 cross-platform PATH resolution 을 위해 shell: true 를 의도적으로 사용 (claude.cmd / ollama.cmd 등 Windows .cmd 처리).
@@ -225,13 +225,41 @@ function argAll(name) {
225
225
  return out;
226
226
  }
227
227
  function detectProjectName(root) { try { const pkg = JSON.parse(read(path.join(root, 'package.json'))); if (pkg.name) return pkg.name; } catch {} return path.basename(root); }
228
+ // 1.9.269 (UR-0022): OS 시스템 언어 감지 — 신규 init(빈 디렉토리, 프로젝트 콘텐츠 없음)에서 OS locale 로 설치 언어 판별.
229
+ // 우선순위: POSIX 환경변수(LC_ALL>LC_CTYPE>LANG>LANGUAGE) > Intl ICU resolvedOptions().locale(Windows 등 LANG 미설정 시) > null.
230
+ // 순수 함수(부작용 0) — env 주입 가능(selftest). 판별 불가 시 null 반환 → 호출부가 en 폴백.
231
+ function _detectSystemLang(env) {
232
+ env = env || process.env;
233
+ const raw = String(env.LC_ALL || env.LC_CTYPE || env.LANG || env.LANGUAGE || '').toLowerCase();
234
+ if (raw && raw !== 'c' && raw !== 'posix') {
235
+ if (/(^|[^a-z])ko([_\-.]|$)|korean|[_-]kr([_\-.]|$)/.test(raw)) return 'ko';
236
+ if (/(^|[^a-z])en([_\-.]|$)|english|[_-](us|gb)([_\-.]|$)/.test(raw)) return 'en';
237
+ }
238
+ // POSIX env 없거나 미매칭 → Node ICU locale (Windows 는 LANG 보통 미설정 → 여기서 OS 언어 회수)
239
+ try {
240
+ const loc = (Intl.DateTimeFormat().resolvedOptions().locale || '').toLowerCase();
241
+ const primary = loc.split('-')[0];
242
+ if (primary === 'ko') return 'ko';
243
+ if (primary === 'en') return 'en';
244
+ } catch {}
245
+ return null;
246
+ }
228
247
  function detectLanguageValue(root, value = 'auto') {
229
248
  const v = String(value || 'auto').toLowerCase();
230
249
  if (v === 'ko' || v === 'en') return v;
250
+ // ① 프로젝트 콘텐츠 한글 우선 — 콘텐츠가 있으면 그 의도를 존중 (기존 동작 보존).
231
251
  const candidates = ['README.md', 'docs/guideline.md', '.harness/project-brief.md', '.harness/plan.md'];
232
252
  let text = '';
233
253
  for (const c of candidates) { const p = path.join(root, c); if (exists(p)) text += read(p).slice(0, 3000); }
234
- return /[가-힣]/.test(text) ? 'ko' : 'en';
254
+ if (/[가-힣]/.test(text)) return 'ko';
255
+ // 1.9.269 (UR-0022): ② 콘텐츠가 비어있으면(신규/빈 디렉토리 init) OS 시스템 언어로 판별.
256
+ // 콘텐츠가 있는데 한글이 아니면(영어 프로젝트) en 유지 — 기존 프로젝트 회귀 방지.
257
+ if (text.trim().length === 0) {
258
+ const sys = _detectSystemLang();
259
+ if (sys) return sys;
260
+ }
261
+ // ③ en 폴백
262
+ return 'en';
235
263
  }
236
264
  function fm(role, readWhen, updateWhen, body) {
237
265
  return `---\nleernessRole: ${role}\nreadWhen:\n${readWhen.map(x => ' - ' + x).join('\n')}\nupdateWhen:\n${updateWhen.map(x => ' - ' + x).join('\n')}\ndoNotStore:\n - 실제 토큰\n - 비밀번호\n - 운영 쿠키\n - 민감한 개인정보 원문\n---\n${MARK}\n${body}`;
@@ -792,6 +820,11 @@ async function resolveInstallOptions(root, opts = {}) {
792
820
  let lang = explicitLang ? detectLanguageValue(root, explicitLang) : detectLanguageValue(root, 'auto');
793
821
  let skills = explicitSkills ? parseSkillsValue(explicitSkills) : [];
794
822
  const shouldAsk = !has('--yes') && !opts.nonInteractive && process.stdin.isTTY && process.stdout.isTTY && !opts.migration;
823
+ // 1.9.269 (UR-0022): 비대화형 auto 설치(npx leerness init --yes 등)에서 OS 시스템 언어가 적용됐으면 투명성 안내.
824
+ // --language 명시 시 미발화 (explicitLang). 대화형은 이후 언어 prompt 가 처리.
825
+ if (!shouldAsk && !explicitLang) {
826
+ try { const sysLang = _detectSystemLang(); if (sysLang) log(`🌐 ${_t('install.lang.sysNotice', lang)}: ${sysLang.toUpperCase()} (→ ${lang.toUpperCase()})`); } catch {}
827
+ }
795
828
  // 1.9.34: 인터랙티브 multi-select (방향키 + Space + Enter) — 기존 숫자 선택 폴백 유지
796
829
  // --no-interactive-select 또는 LEERNESS_NO_INTERACTIVE=1 → 구식 숫자 선택
797
830
  const useInteractive = shouldAsk && !has('--no-interactive-select') && process.env.LEERNESS_NO_INTERACTIVE !== '1';
@@ -829,19 +862,20 @@ async function resolveInstallOptions(root, opts = {}) {
829
862
  { label: 'Claude (ANTHROPIC_API_KEY 또는 claude CLI)', description: '추론력 최고 — 코드 작성/리뷰 기본', id: 'claude' },
830
863
  { label: 'Codex (OpenAI codex CLI)', description: 'OpenAI 코드 모델', id: 'codex' },
831
864
  { label: 'Antigravity (agy CLI)', description: 'Google Antigravity 멀티모달 에이전트 (1.9.248~)', id: 'agy' },
865
+ { label: 'Grok (xAI grok-cli)', description: 'xAI Grok CLI — grok-beta 등 (1.9.268~)', id: 'grok' },
832
866
  { label: 'Copilot (gh extension)', description: 'GitHub Copilot CLI', id: 'copilot' },
833
867
  { label: 'Ollama (로컬 LLM — llama3/qwen 등)', description: 'http://localhost:11434 — 무료/오프라인', id: 'ollama' }
834
868
  ], { defaults: [] });
835
869
  agentsOptIn = picked.length ? picked.map(p => p.id) : 'none';
836
870
  } else {
837
871
  log('\nCLI 에이전트 활성화 (복수 선택 — 콤마로 구분, opt-in):');
838
- log(' 1) claude 2) codex 3) agy (Antigravity) 4) copilot 5) ollama (예: 1,5 또는 all 또는 none)');
872
+ log(' 1) claude 2) codex 3) agy (Antigravity) 4) grok (xAI) 5) copilot 6) ollama (예: 1,6 또는 all 또는 none)');
839
873
  const a = (await ask('선택 [none]: ')).trim().toLowerCase();
840
- if (a === 'all') agentsOptIn = ['claude', 'codex', 'agy', 'copilot', 'ollama'];
874
+ if (a === 'all') agentsOptIn = ['claude', 'codex', 'agy', 'grok', 'copilot', 'ollama'];
841
875
  else if (!a || a === 'none' || a === '0') agentsOptIn = 'none';
842
876
  else {
843
- const map = { '1': 'claude', '2': 'codex', '3': 'agy', '4': 'copilot', '5': 'ollama' };
844
- const picks = a.split(/[,\s]+/).map(t => map[t] || (['claude','codex','agy','copilot','ollama'].includes(t) ? t : null)).filter(Boolean);
877
+ const map = { '1': 'claude', '2': 'codex', '3': 'agy', '4': 'grok', '5': 'copilot', '6': 'ollama' };
878
+ const picks = a.split(/[,\s]+/).map(t => map[t] || (['claude','codex','agy','grok','copilot','ollama'].includes(t) ? t : null)).filter(Boolean);
845
879
  agentsOptIn = picks.length ? picks : 'none';
846
880
  }
847
881
  }
@@ -1014,6 +1048,7 @@ async function install(root, opts = {}) {
1014
1048
  LEERNESS_ENABLE_CLAUDE: enable('claude') ? '1' : '0',
1015
1049
  LEERNESS_ENABLE_CODEX: enable('codex') ? '1' : '0',
1016
1050
  LEERNESS_ENABLE_AGY: enable('agy') ? '1' : '0',
1051
+ LEERNESS_ENABLE_GROK: enable('grok') ? '1' : '0',
1017
1052
  LEERNESS_ENABLE_COPILOT: enable('copilot') ? '1' : '0',
1018
1053
  LEERNESS_ENABLE_OLLAMA: enable('ollama') ? '1' : '0',
1019
1054
  LEERNESS_SKILL_DISCOVER_URL: '',
@@ -1850,7 +1885,8 @@ const STRINGS = {
1850
1885
  // 설치 가이드 prompt
1851
1886
  'install.lang.title': { ko: '설치 언어를 선택하세요', en: 'Select install language' },
1852
1887
  'install.lang.auto': { ko: '자동 감지', en: 'Auto detect' },
1853
- 'install.lang.auto.desc': { ko: '디렉토리/파일 분석 (한국어/영어 자동 판별)', en: 'Analyze dir/files (auto KO/EN)' },
1888
+ 'install.lang.auto.desc': { ko: '디렉토리/파일 + 시스템(OS) 언어 자동 판별 (한국어/영어)', en: 'Auto-detect from dir/files + system (OS) locale (KO/EN)' },
1889
+ 'install.lang.sysNotice': { ko: '시스템 언어 감지', en: 'System language detected' },
1854
1890
  'install.lang.ko': { ko: '한국어', en: 'Korean' },
1855
1891
  'install.lang.ko.desc': { ko: '모든 인스트럭션을 한국어로 생성', en: 'All instructions in Korean' },
1856
1892
  'install.lang.en': { ko: 'English', en: 'English' },
@@ -2909,6 +2945,9 @@ function _selfTestCases() {
2909
2945
  { name: '_agentSlashHint: grok 슬래시 요약 + copilot 하위명령 라벨', run: () => { const g = _agentSlashHint('.', 'grok'); const c = _agentSlashHint('.', 'copilot'); return !!g && g.count > 0 && /Grok/.test(g.summary) && !!c && c.invoke === 'subcommand' && /하위명령/.test(c.summary); } },
2910
2946
  { name: '_parseSlashFromHelp: 슬래시 검출 + 플래그 제외 (1.9.267)', run: () => { const r = _parseSlashFromHelp(' /help show help\n /model switch model\n --version print version\n', 'slash'); return r.length === 2 && r[0].cmd === '/help' && /show help/.test(r[0].desc) && !r.some(c => /version/.test(c.cmd)); } },
2911
2947
  { name: '_parseSlashFromHelp: subcommand 모드 들여쓰기 파싱 (1.9.267)', run: () => { const r = _parseSlashFromHelp('Commands:\n suggest 제안\n explain 설명\n', 'subcommand'); return r.length >= 2 && r.some(c => c.cmd === 'suggest') && r.some(c => c.cmd === 'explain'); } },
2948
+ { name: 'EXTERNAL_AGENTS: grok 정식 provider 승격 (1.9.268)', run: () => { const g = EXTERNAL_AGENTS.find(a => a.id === 'grok'); return !!g && g.bin === 'grok' && g.envFlag === 'LEERNESS_ENABLE_GROK' && EXTERNAL_AGENTS.length === 6; } },
2949
+ { name: '_detectSystemLang: POSIX LANG ko_KR/en_US 판별 (1.9.269)', run: () => _detectSystemLang({ LANG: 'ko_KR.UTF-8' }) === 'ko' && _detectSystemLang({ LANG: 'en_US.UTF-8' }) === 'en' },
2950
+ { name: '_detectSystemLang: LC_ALL 우선 + LANGUAGE 폴백 (1.9.269)', run: () => _detectSystemLang({ LC_ALL: 'ko_KR.UTF-8', LANG: 'en_US.UTF-8' }) === 'ko' && _detectSystemLang({ LANGUAGE: 'en_GB' }) === 'en' },
2912
2951
  { name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
2913
2952
  ];
2914
2953
  }
@@ -9417,6 +9456,7 @@ const _LEERNESS_NONSECRET_KEYS = new Set([
9417
9456
  'LEERNESS_ENABLE_CLAUDE', // 활성화 플래그
9418
9457
  'LEERNESS_ENABLE_CODEX',
9419
9458
  'LEERNESS_ENABLE_AGY',
9459
+ 'LEERNESS_ENABLE_GROK',
9420
9460
  'LEERNESS_ENABLE_COPILOT',
9421
9461
  'LEERNESS_ENABLE_OLLAMA',
9422
9462
  'LEERNESS_SKILL_DISCOVER_URL', // 공개 URL
@@ -10073,6 +10113,10 @@ const EXTERNAL_AGENTS = [
10073
10113
  // 기존 LEERNESS_ENABLE_AGY 환경변수는 LEERNESS_ENABLE_AGY 로 교체. provider id 도 'agy' 로 통일.
10074
10114
  { id: 'agy', bin: 'agy', envFlag: 'LEERNESS_ENABLE_AGY', versionArgs: ['--version'], desc: 'Google Antigravity CLI (멀티모달 에이전트, --yolo 워크스페이스 수정 가능)',
10075
10115
  installCmd: 'npm i -g @google/antigravity-cli', installHint: 'https://antigravity.google.com (Antigravity IDE/CLI)' },
10116
+ // 1.9.268: grok 정식 provider 승격 (1.9.266 후속 task) — 기존 슬래시 레지스트리(grok)만 보유하던 상태에서 EXTERNAL_AGENTS 로 편입.
10117
+ // → provider cycle / setup-agents / dispatch / slash-commands --refresh probe 가 grok 도 자동 처리.
10118
+ { id: 'grok', bin: 'grok', envFlag: 'LEERNESS_ENABLE_GROK', versionArgs: ['--version'], desc: 'xAI Grok CLI (커뮤니티 grok-cli, /model grok-beta 등)',
10119
+ installCmd: 'npm i -g @vibe-kit/grok-cli', installHint: 'https://github.com/superagent-ai/grok-cli (xAI API 키 필요 — /login 또는 GROK_API_KEY)' },
10076
10120
  { id: 'copilot', bin: 'gh', envFlag: 'LEERNESS_ENABLE_COPILOT', versionArgs: ['copilot', '--version'], desc: 'GitHub Copilot CLI (gh copilot)',
10077
10121
  installCmd: 'gh extension install github/gh-copilot', installHint: 'https://github.com/github/gh-copilot (gh CLI 선행 설치 필요)' },
10078
10122
  // 1.9.146: Ollama 추가 (사용자 명시 요청 #3) — 로컬 LLM, HTTP API 11434
@@ -10132,8 +10176,8 @@ const AGENT_SLASH_COMMANDS = {
10132
10176
  ]
10133
10177
  },
10134
10178
  grok: {
10135
- label: 'xAI Grok CLI', asOf: '1.9.265', invoke: 'slash',
10136
- note: '커뮤니티/공식 Grok CLI(예: @vibe-kit/grok-cli) 기준 큐레이션 배포판마다 차이 가능. 사용자 override 권장.',
10179
+ label: 'xAI Grok CLI', asOf: '1.9.268', invoke: 'slash',
10180
+ note: '1.9.268 정식 provider 승격 — slash-commands --refresh 로 자동 probe 가능. 커뮤니티 Grok CLI(@vibe-kit/grok-cli) 기준 큐레이션, 배포판마다 차이 가능.',
10137
10181
  commands: [
10138
10182
  { cmd: '/help', desc: '명령 목록' },
10139
10183
  { cmd: '/clear', desc: '대화 초기화' },
@@ -10281,7 +10325,7 @@ function _refreshAgentSlashCommands(root, targets, opts = {}) {
10281
10325
  for (const id of targets) {
10282
10326
  const ext = EXTERNAL_AGENTS.find(a => a.id === id);
10283
10327
  const def = merged[id] || AGENT_SLASH_COMMANDS[id];
10284
- if (!ext) { results.push({ id, ok: false, reason: 'probe 대상 아님 (EXTERNAL_AGENTS 미포함 — grok/ollama 등은 큐레이션/override )' }); continue; }
10328
+ if (!ext) { results.push({ id, ok: false, reason: 'probe 대상 아님 (EXTERNAL_AGENTS 미포함 — 사용자 override 전용 agent)' }); continue; }
10285
10329
  const pr = _probeAgentSlash(ext, { invoke: (def && def.invoke) || 'slash', timeout: opts.timeout });
10286
10330
  if (pr.ok) {
10287
10331
  agents[id] = {
@@ -10385,7 +10429,7 @@ function slashCommandsCmd(root, agentArg, opts = {}) {
10385
10429
  }
10386
10430
 
10387
10431
  // 1.9.157: Provider Registry — 사용자 정의 provider 동적 추가 (.harness/providers.json)
10388
- // 빌트인 5종 (EXTERNAL_AGENTS) + 사용자 정의를 merge. OpenRouter / Bedrock / Groq 등 새 CLI 즉시 흡수 가능.
10432
+ // 빌트인 6종 (EXTERNAL_AGENTS: claude/codex/agy/grok/copilot/ollama) + 사용자 정의를 merge. OpenRouter / Bedrock 등 새 CLI 즉시 흡수 가능.
10389
10433
  // 파일 형식: { "schemaVersion": 1, "providers": [{ id, bin, envFlag, versionArgs, desc, installHint }] }
10390
10434
  function _providersFile(root) { return path.join(absRoot(root), '.harness', 'providers.json'); }
10391
10435
  function _readUserProviders(root) {
@@ -20728,5 +20772,7 @@ module.exports = {
20728
20772
  _shellFailuresPath, _loadShellFailures, _recordShellFailure, _shellEnvDrift,
20729
20773
  // 1.9.265: CLI 에이전트 슬래시 명령어 레지스트리 (UR-0021 1단계) — 단위 테스트
20730
20774
  AGENT_SLASH_COMMANDS, _agentSlashFile, _loadAgentSlashCommands, _recordAgentSlashCommands, _agentSlashHint, slashCommandsCmd,
20731
- _parseSlashFromHelp, _probeAgentSlash, _refreshAgentSlashCommands
20775
+ _parseSlashFromHelp, _probeAgentSlash, _refreshAgentSlashCommands,
20776
+ // 1.9.269: 시스템(OS) 언어 감지 (UR-0022) + 언어 판별 — 단위 테스트
20777
+ _detectSystemLang, detectLanguageValue
20732
20778
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.267",
3
+ "version": "1.9.269",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -797,23 +797,24 @@ total++;
797
797
  total++;
798
798
  {
799
799
  // agents list — claude가 환경변수 + PATH 둘 다 충족 시 ready
800
- const env1 = { ...process.env, LEERNESS_ENABLE_CLAUDE: '1', LEERNESS_ENABLE_CODEX: '0', LEERNESS_ENABLE_AGY: '0', LEERNESS_ENABLE_COPILOT: '0' };
800
+ const env1 = { ...process.env, LEERNESS_ENABLE_CLAUDE: '1', LEERNESS_ENABLE_CODEX: '0', LEERNESS_ENABLE_AGY: '0', LEERNESS_ENABLE_GROK: '0', LEERNESS_ENABLE_COPILOT: '0' };
801
801
  const r1 = cp.spawnSync(process.execPath, [CLI, 'agents', 'list'], { encoding: 'utf8', timeout: 15000, env: env1 });
802
802
  const okList = r1.status === 0
803
803
  && /외부 AI CLI 오케스트레이션 \(1\.9\.30\)/.test(r1.stdout)
804
804
  && /\| claude \|/.test(r1.stdout)
805
805
  && /\| codex \|/.test(r1.stdout)
806
806
  && /\| agy \|/.test(r1.stdout)
807
+ && /\| grok \|/.test(r1.stdout)
807
808
  && /\| copilot \|/.test(r1.stdout);
808
809
  // env 모두 0 → 비활성
809
- // 1.9.146: Ollama 추가 → 5 CLI
810
- const env2 = { ...process.env, LEERNESS_ENABLE_CLAUDE: '0', LEERNESS_ENABLE_CODEX: '0', LEERNESS_ENABLE_AGY: '0', LEERNESS_ENABLE_COPILOT: '0', LEERNESS_ENABLE_OLLAMA: '0' };
810
+ // 1.9.146: Ollama 추가 → 5 CLI · 1.9.268: grok 정식 승격 → 6 CLI
811
+ const env2 = { ...process.env, LEERNESS_ENABLE_CLAUDE: '0', LEERNESS_ENABLE_CODEX: '0', LEERNESS_ENABLE_AGY: '0', LEERNESS_ENABLE_GROK: '0', LEERNESS_ENABLE_COPILOT: '0', LEERNESS_ENABLE_OLLAMA: '0' };
811
812
  const r2 = cp.spawnSync(process.execPath, [CLI, 'agents', 'list', '--json'], { encoding: 'utf8', timeout: 15000, env: env2 });
812
813
  let parsed = null;
813
814
  try { parsed = JSON.parse(r2.stdout); } catch {}
814
- const okJson = parsed && Array.isArray(parsed.agents) && parsed.agents.length === 5 && parsed.agents.every(a => a.status !== 'ready');
815
+ const okJson = parsed && Array.isArray(parsed.agents) && parsed.agents.length === 6 && parsed.agents.every(a => a.status !== 'ready');
815
816
  const ok = okList && okJson;
816
- console.log(ok ? '✓ B(1.9.30+1.9.146) agents list: 5 CLI 정의 (claude/codex/agy/copilot/ollama)' : `✗ agents list 실패 (list=${okList} json=${okJson})`);
817
+ console.log(ok ? '✓ B(1.9.30+1.9.268) agents list: 6 CLI 정의 (claude/codex/agy/grok/copilot/ollama)' : `✗ agents list 실패 (list=${okList} json=${okJson})`);
817
818
  if (!ok) { failed++; console.log(r1.stdout.slice(0, 500)); }
818
819
  }
819
820
 
@@ -838,24 +839,25 @@ total++;
838
839
  total++;
839
840
  {
840
841
  // agents quota — env=0 시 모두 disabled/not-installed, 안내 메시지 포함
841
- const env = { ...process.env, LEERNESS_ENABLE_CLAUDE: '0', LEERNESS_ENABLE_CODEX: '0', LEERNESS_ENABLE_AGY: '0', LEERNESS_ENABLE_COPILOT: '0' };
842
+ const env = { ...process.env, LEERNESS_ENABLE_CLAUDE: '0', LEERNESS_ENABLE_CODEX: '0', LEERNESS_ENABLE_AGY: '0', LEERNESS_ENABLE_GROK: '0', LEERNESS_ENABLE_COPILOT: '0' };
842
843
  const r = cp.spawnSync(process.execPath, [CLI, 'agents', 'quota'], { encoding: 'utf8', timeout: 15000, env });
843
844
  const okText = r.status === 0
844
845
  && /외부 AI CLI quota 추정 \(1\.9\.31\)/.test(r.stdout)
845
846
  && /\| claude \|/.test(r.stdout)
846
847
  && /\| codex \|/.test(r.stdout)
847
848
  && /\| agy \|/.test(r.stdout)
849
+ && /\| grok \|/.test(r.stdout)
848
850
  && /\| copilot \|/.test(r.stdout)
849
851
  && /provider 대시보드 참조/.test(r.stdout);
850
852
  // JSON 출력
851
853
  const r2 = cp.spawnSync(process.execPath, [CLI, 'agents', 'quota', '--json'], { encoding: 'utf8', timeout: 15000, env });
852
854
  let parsed = null;
853
855
  try { parsed = JSON.parse(r2.stdout); } catch {}
854
- // 1.9.146: Ollama 추가 → 5 CLI
855
- const okJson = parsed && Array.isArray(parsed.quota) && parsed.quota.length === 5
856
+ // 1.9.146: Ollama 추가 → 5 CLI · 1.9.268: grok 정식 승격 → 6 CLI
857
+ const okJson = parsed && Array.isArray(parsed.quota) && parsed.quota.length === 6
856
858
  && parsed.quota.every(q => typeof q.id === 'string' && typeof q.status === 'string' && (q.hint === null || typeof q.hint === 'string'));
857
859
  const ok = okText && okJson;
858
- console.log(ok ? '✓ B(1.9.31+1.9.146) agents quota: 5 CLI 사용량/안내' : `✗ quota 실패 (text=${okText} json=${okJson})`);
860
+ console.log(ok ? '✓ B(1.9.31+1.9.268) agents quota: 6 CLI 사용량/안내' : `✗ quota 실패 (text=${okText} json=${okJson})`);
859
861
  if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
860
862
  }
861
863
 
@@ -2702,5 +2704,23 @@ run('consistency check', ['consistency', 'check', tmp]);
2702
2704
  run('--version', ['--version']);
2703
2705
  run('--help', ['--help']);
2704
2706
 
2707
+ // 1.9.269 회귀 (UR-0022): 빈 디렉토리 신규 init + auto → OS 시스템 언어로 .harness/LANGUAGE 결정
2708
+ total++;
2709
+ {
2710
+ const tmpEn = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-lang-en-'));
2711
+ const tmpKo = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-lang-ko-'));
2712
+ // LANG 명시 (POSIX) — Windows 에서도 _detectSystemLang 이 LANG 우선 읽음
2713
+ const envEn = { ...process.env, LANG: 'en_US.UTF-8', LC_ALL: 'en_US.UTF-8', LANGUAGE: '' };
2714
+ const envKo = { ...process.env, LANG: 'ko_KR.UTF-8', LC_ALL: 'ko_KR.UTF-8', LANGUAGE: '' };
2715
+ // --language 미지정(auto) + 빈 디렉토리 → 시스템 언어 적용
2716
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpEn, '--yes', '--skills', 'recommended'], { encoding: 'utf8', env: envEn, timeout: 30000 });
2717
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpKo, '--yes', '--skills', 'recommended'], { encoding: 'utf8', env: envKo, timeout: 30000 });
2718
+ const langEn = fs.existsSync(path.join(tmpEn, '.harness', 'LANGUAGE')) ? fs.readFileSync(path.join(tmpEn, '.harness', 'LANGUAGE'), 'utf8').trim() : '';
2719
+ const langKo = fs.existsSync(path.join(tmpKo, '.harness', 'LANGUAGE')) ? fs.readFileSync(path.join(tmpKo, '.harness', 'LANGUAGE'), 'utf8').trim() : '';
2720
+ const ok = langEn === 'en' && langKo === 'ko';
2721
+ console.log(ok ? '✓ B(1.9.269) init auto: 빈 디렉토리 → OS 시스템 언어 (en→en, ko→ko)' : `✗ system-lang init 실패 (en=${langEn} ko=${langKo})`);
2722
+ if (!ok) failed++;
2723
+ }
2724
+
2705
2725
  console.log(`\nE2E result: ${total - failed}/${total} passed`);
2706
2726
  if (failed > 0) process.exit(1);