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 +39 -0
- package/bin/harness.js +58 -12
- package/package.json +1 -1
- package/scripts/e2e.js +29 -9
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.
|
|
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
|
-
|
|
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)
|
|
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': '
|
|
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: '디렉토리/파일
|
|
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.
|
|
10136
|
-
note: '
|
|
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 미포함 —
|
|
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
|
-
// 빌트인
|
|
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
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 ===
|
|
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.
|
|
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 ===
|
|
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.
|
|
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);
|