leerness 1.9.268 → 1.9.270
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 +44 -0
- package/bin/harness.js +284 -32
- package/package.json +1 -1
- package/scripts/e2e.js +44 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.270 — 2026-06-02 — agent roles: 모델별 역할 부여 (코딩/검수/지휘/디자인/디버그/설계/분배)
|
|
4
|
+
|
|
5
|
+
**🎭 여러 AI 에이전트 활성 시 역할을 provider+model 에 매핑하고 `dispatch --role` 로 라우팅 — 사용자가 선택적으로 역할 룰 설정 (사용자 명시).**
|
|
6
|
+
|
|
7
|
+
### 배경 + 방향성 판단 (사용자 요청)
|
|
8
|
+
사용자 요청: Codex gpt-5.5=코딩, Claude Opus=지휘/검수, Gemini=디자인 등 **모델별 역할 부여**가 leerness 프로젝트의 성능/정확도를 올리는지 판단하고 올바른 방향으로 구현.
|
|
9
|
+
|
|
10
|
+
**판단**: 역할 특화는 다음 조건에서 품질·정확도를 **유의미하게 향상**시킨다 —
|
|
11
|
+
1. **모델 강점이 분명할 때** — 코드 특화 모델(코딩)·강추론 모델(설계/검수)·멀티모달(디자인)을 적재적소 배치.
|
|
12
|
+
2. **독립 검수 분리** — 구현자와 다른 강추론 모델이 검수하면 **자기승인 편향(self-approval bias)** 차단 → 버그 적발률↑ (기존 `_AGENT_ROLE_PROMPTS` planner/reviewer/actor 분리 원칙의 모델 레벨 확장).
|
|
13
|
+
3. **작업 분해 가능 + 조율 오버헤드 < 이득**일 때.
|
|
14
|
+
→ 따라서 **opt-in(기본 미설정) + `roles verify`(비활성 provider 배정 경고) + `roles suggest`(활성 에이전트 기반 자동 배치 + 근거 제시)** 로 오설정을 막고 안전하게 도입. 단일 에이전트만 활성이면 모든 역할이 그 에이전트로 수렴(무해).
|
|
15
|
+
|
|
16
|
+
### 구현
|
|
17
|
+
1. **`ROLE_CATALOG` 7종** — commander(지휘)/reviewer(검수)/coder(코딩)/architect(설계)/designer(디자인)/debugger(디버그)/dispatcher(분배). 각 역할: 선호 provider 우선순위 + 모델 등급(top/code/fast) + **근거(why)**.
|
|
18
|
+
2. **`.harness/agent-roles.json`** 사용자 설정 — `{ roles: { <role>: { provider, model, persona } } }`.
|
|
19
|
+
3. **`leerness roles <list|set|unset|catalog|suggest|verify>`** CLI — 한국어 별칭(코딩→coder, 검수자→reviewer 등) · `set` 시 모델 미지정이면 등급 기반 자동 선택(`_pickModel`) · `suggest [--apply]` 활성 에이전트 기반 최적 배치 + 근거 · `verify` 비활성 provider 배정 적발(exit 1).
|
|
20
|
+
4. **`agents dispatch --role <role>`** — 역할 → provider+model 라우팅, `_dispatchCommand` 가 provider 별 모델 플래그 주입(claude `--model`, codex `-m`, agy/grok `--model`). `--model` 직접 override 도 지원.
|
|
21
|
+
5. **grok 통합 보완** (1.9.268 후속) — `_PROVIDER_MODEL_CATALOG`/`_PROVIDER_CYCLE_ORDER`/`_dispatchCommand` 에 grok 추가.
|
|
22
|
+
6. **MCP `leerness_roles`** (외부 AI 가 역할 배치/조회) + selftest 22 → 24 + e2e 218 → 219.
|
|
23
|
+
|
|
24
|
+
### 검증
|
|
25
|
+
- **selftest 24/24 PASS** · **E2E 219/219 PASS** (회귀 0).
|
|
26
|
+
- 실측: `roles set 코딩 --provider codex --model gpt-5.5` → coder/codex/gpt-5.5 · `dispatch --role reviewer` → `claude --print --model claude-opus-4-7 "..."` 모델 주입 확인 · `verify` 비활성 provider 🔴 적발 · `suggest` 활성 기반 배치 + 근거.
|
|
27
|
+
|
|
28
|
+
## 1.9.269 — 2026-06-02 — UR-0022: init 시 OS 시스템 언어 감지 → 설치 가이드 자동 언어 선택
|
|
29
|
+
|
|
30
|
+
**🌐 `npx leerness init` 등 설치 시 OS 시스템 언어를 감지해 설치 가이드/생성 문서를 알맞은 언어로 표시 (사용자 명시 UR-0022).**
|
|
31
|
+
|
|
32
|
+
### 배경
|
|
33
|
+
기존 `detectLanguageValue(root, 'auto')`는 프로젝트 파일(README/guideline 등)의 **한글 포함 여부만** 판별했음. 그 결과 빈 디렉토리에서 신규 `npx leerness init` 시 콘텐츠가 없어 한국어 OS 에서도 항상 영어(en)로 폴백됐음. 사용자 요청은 OS locale 기반으로 설치 언어를 자동 결정하는 것.
|
|
34
|
+
|
|
35
|
+
### 구현
|
|
36
|
+
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 주입 가능(테스트).
|
|
37
|
+
2. **`detectLanguageValue` auto 우선순위 개선** — ① 프로젝트 콘텐츠 한글 → ② **콘텐츠가 비어있을 때만**(신규/빈 디렉토리 init) OS 시스템 언어 → ③ en 폴백. **콘텐츠가 있는 영어 프로젝트는 en 유지**(기존 프로젝트 회귀 방지).
|
|
38
|
+
3. **비대화형 설치 투명성 안내** — `npx leerness init --yes`(auto) 시 `🌐 시스템 언어 감지: KO (→ KO)` 한 줄 노출. `--language` 명시 시 미발화.
|
|
39
|
+
4. **언어 선택 UI 설명 갱신** — `install.lang.auto.desc` 에 "시스템(OS) 언어 자동 판별" 반영.
|
|
40
|
+
5. **selftest 20 → 22** — `_detectSystemLang` POSIX 파싱(ko_KR/en_US) + LC_ALL 우선/LANGUAGE 폴백 2종.
|
|
41
|
+
6. **e2e +1 (217 → 218)** — 빈 디렉토리 init auto + `LANG=en_US`/`ko_KR` → `.harness/LANGUAGE` 가 각각 en/ko 로 결정되는지 통합 검증.
|
|
42
|
+
|
|
43
|
+
### 검증
|
|
44
|
+
- **selftest 22/22 PASS** · **E2E 218/218 PASS** (회귀 0).
|
|
45
|
+
- 기존 init e2e 는 모두 `--language ko` 명시 → auto 변경 영향 없음(회귀 0 확인).
|
|
46
|
+
|
|
3
47
|
## 1.9.268 — 2026-06-02 — grok 정식 EXTERNAL_AGENTS provider 승격 (1.9.266 후속 task)
|
|
4
48
|
|
|
5
49
|
**🤖 grok 을 슬래시 레지스트리 전용에서 정식 provider 로 승격 — provider cycle / setup-agents / dispatch / `--help` probe 가 grok 도 자동 처리.**
|
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.270';
|
|
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';
|
|
@@ -1852,7 +1885,8 @@ const STRINGS = {
|
|
|
1852
1885
|
// 설치 가이드 prompt
|
|
1853
1886
|
'install.lang.title': { ko: '설치 언어를 선택하세요', en: 'Select install language' },
|
|
1854
1887
|
'install.lang.auto': { ko: '자동 감지', en: 'Auto detect' },
|
|
1855
|
-
'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' },
|
|
1856
1890
|
'install.lang.ko': { ko: '한국어', en: 'Korean' },
|
|
1857
1891
|
'install.lang.ko.desc': { ko: '모든 인스트럭션을 한국어로 생성', en: 'All instructions in Korean' },
|
|
1858
1892
|
'install.lang.en': { ko: 'English', en: 'English' },
|
|
@@ -2912,6 +2946,10 @@ function _selfTestCases() {
|
|
|
2912
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)); } },
|
|
2913
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'); } },
|
|
2914
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' },
|
|
2951
|
+
{ name: 'ROLE_CATALOG + _normalizeRole: 7종 + 한국어 별칭 (1.9.270)', run: () => { const keys = Object.keys(ROLE_CATALOG); return keys.length === 7 && keys.includes('coder') && keys.includes('reviewer') && _normalizeRole('코딩') === 'coder' && _normalizeRole('검수자') === 'reviewer' && _normalizeRole('지휘관') === 'commander'; } },
|
|
2952
|
+
{ name: '_pickModel: top/code/fast 등급 선택 (1.9.270)', run: () => { return _pickModel('codex', 'code') === 'gpt-5-codex' && _pickModel('claude', 'top') === 'claude-opus-4-7' && /haiku/.test(_pickModel('claude', 'fast')); } },
|
|
2915
2953
|
{ name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
|
|
2916
2954
|
];
|
|
2917
2955
|
}
|
|
@@ -3566,7 +3604,8 @@ function commandsCmd(root) {
|
|
|
3566
3604
|
{ cmd: 'session close [path] [--json] [--auto-apply-delivered]', desc: '세션 마감 + 9 카테고리 + 자동 통합' },
|
|
3567
3605
|
{ cmd: 'resume [path]', desc: 'auto-resume-plan 적용 (1.9.203)' },
|
|
3568
3606
|
{ cmd: 'route <task-type>', desc: '작업 유형 분류 (11종)' },
|
|
3569
|
-
{ cmd: 'agents list|check|quota|dispatch|multi', desc: '외부 AI CLI 오케스트레이션' },
|
|
3607
|
+
{ cmd: 'agents list|check|quota|dispatch|multi', desc: '외부 AI CLI 오케스트레이션 (dispatch --role 모델 라우팅 1.9.270)' },
|
|
3608
|
+
{ cmd: 'roles list|set|unset|catalog|suggest|verify', desc: '모델별 역할 부여 (코딩/검수/지휘/디자인/디버그/설계/분배) — 1.9.270' },
|
|
3570
3609
|
{ cmd: 'slash-commands [agent] [--json --record --detect --refresh]', desc: 'CLI 에이전트 슬래시 명령 레지스트리 + --help probe 자동 갱신 (1.9.265~267, UR-0021)' },
|
|
3571
3610
|
{ cmd: 'review-request "<request>"', desc: '사용자 요청 사전 검토 (1.9.176)' },
|
|
3572
3611
|
{ cmd: 'review <file> --persona <ids>', desc: '페르소나 리뷰 (1.9.29)' },
|
|
@@ -11265,13 +11304,16 @@ async function setupAgentsCmd(root, opts = {}) {
|
|
|
11265
11304
|
}
|
|
11266
11305
|
|
|
11267
11306
|
// 1.9.152: 단일 agent dispatch 명령 빌더 — agents dispatch (단일) + agents multi (복수) 가 공유
|
|
11268
|
-
|
|
11307
|
+
// 1.9.270: model 인자 추가 — 역할(roles) 기반 dispatch 시 provider 별 모델 플래그 주입 (없으면 기존 동작 그대로).
|
|
11308
|
+
function _dispatchCommand(agentId, task, writeMode, model) {
|
|
11269
11309
|
const q = String(task || '').replace(/"/g, '\\"');
|
|
11270
|
-
|
|
11271
|
-
if (agentId === '
|
|
11272
|
-
if (agentId === '
|
|
11310
|
+
const m = model ? String(model).replace(/"/g, '') : '';
|
|
11311
|
+
if (agentId === 'claude') return `claude ${writeMode ? '--print --dangerously-skip-permissions' : '--print'}${m ? ` --model ${m}` : ''} "${q}"`;
|
|
11312
|
+
if (agentId === 'codex') return `codex ${writeMode ? 'exec --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox' : 'exec --skip-git-repo-check'}${m ? ` -m ${m}` : ''} "${q}"`;
|
|
11313
|
+
if (agentId === 'agy') return `agy ${writeMode ? '-p --yolo' : '-p'}${m ? ` --model ${m}` : ''} "${q}"`;
|
|
11314
|
+
if (agentId === 'grok') return `grok${writeMode ? ' --yolo' : ''}${m ? ` --model ${m}` : ''} "${q}"`;
|
|
11273
11315
|
if (agentId === 'copilot') return `gh copilot suggest "${q}"`;
|
|
11274
|
-
if (agentId === 'ollama') return `# ollama — leerness agent "${q}" --provider ollama 로 직접 호출 (REPL: leerness agent)`;
|
|
11316
|
+
if (agentId === 'ollama') return `# ollama — leerness agent "${q}" --provider ollama${m ? ` --model ${m}` : ''} 로 직접 호출 (REPL: leerness agent)`;
|
|
11275
11317
|
return `# ${agentId}: 명령 빌더 미정의`;
|
|
11276
11318
|
}
|
|
11277
11319
|
|
|
@@ -11488,13 +11530,26 @@ function agentsCmd(root, sub, ...args) {
|
|
|
11488
11530
|
}
|
|
11489
11531
|
if (sub === 'dispatch') {
|
|
11490
11532
|
const task = args.filter(x => !x.startsWith('-')).join(' ').trim() || arg('--task', null);
|
|
11491
|
-
|
|
11533
|
+
let target = arg('--to', null);
|
|
11492
11534
|
if (!task) { fail('dispatch "<task>" 또는 --task 필요'); return process.exit(1); }
|
|
11493
11535
|
// 1.9.152: --multi 또는 --to=all 또는 --to 없음 + 활성 ≥2 → multi 모드로 routing
|
|
11494
11536
|
if (has('--multi') || target === 'all' || target === '*') {
|
|
11495
11537
|
return agentsCmd(root, 'multi', ...args);
|
|
11496
11538
|
}
|
|
11497
|
-
|
|
11539
|
+
// 1.9.270: --role <role> — 설정된 역할 → provider+model 라우팅 (--to 없을 때)
|
|
11540
|
+
const roleArg = arg('--role', null);
|
|
11541
|
+
let roleModel = arg('--model', null);
|
|
11542
|
+
let rolePersona = '';
|
|
11543
|
+
if (roleArg && !target) {
|
|
11544
|
+
const resolved = _resolveRole(root, roleArg);
|
|
11545
|
+
if (!resolved) { fail(`역할 미설정: ${_normalizeRole(roleArg)} — leerness roles set ${_normalizeRole(roleArg)} --provider <id> 또는 roles suggest --apply`); return process.exit(1); }
|
|
11546
|
+
target = resolved.provider;
|
|
11547
|
+
if (!roleModel) roleModel = resolved.model;
|
|
11548
|
+
rolePersona = resolved.persona || '';
|
|
11549
|
+
log(`🎭 역할 ${_normalizeRole(roleArg)} → ${target}${roleModel ? ' / ' + roleModel : ''}`);
|
|
11550
|
+
if (rolePersona) log(` persona: ${rolePersona}`);
|
|
11551
|
+
}
|
|
11552
|
+
if (!target) { fail('--to <agent_id> 또는 --role <role> 필요 (claude/codex/agy/grok/copilot) — 활성 전체 일괄은 `leerness agents multi`'); return process.exit(1); }
|
|
11498
11553
|
const agentDef = EXTERNAL_AGENTS.find(a => a.id === target);
|
|
11499
11554
|
if (!agentDef) { fail(`알 수 없는 agent: ${target}`); return process.exit(1); }
|
|
11500
11555
|
// 1.9.36: 작업 유형 키워드 분석 → 최적 CLI 추천 (ready 체크 전에 출력 — 비활성이어도 추천)
|
|
@@ -11518,24 +11573,14 @@ function agentsCmd(root, sub, ...args) {
|
|
|
11518
11573
|
log(`모드: ${writeMode ? '✏ write (파일 수정 가능)' : '🔒 read-only (분석 전용, 안전)'}`);
|
|
11519
11574
|
log('');
|
|
11520
11575
|
log(`## 실행 명령 (사용자가 복사해서 실행)`);
|
|
11576
|
+
if (roleModel) log(`# 🎭 모델: ${roleModel} (역할 기반 라우팅, 1.9.270)`);
|
|
11521
11577
|
log('');
|
|
11522
|
-
|
|
11523
|
-
|
|
11524
|
-
|
|
11525
|
-
|
|
11526
|
-
|
|
11527
|
-
|
|
11528
|
-
const flags = writeMode ? 'exec --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox' : 'exec --skip-git-repo-check';
|
|
11529
|
-
log(`codex ${flags} "${q}"`);
|
|
11530
|
-
log(`# ℹ codex는 PowerShell 경유 — POSIX /tmp 경로는 C:\\tmp\\로 해석됨`);
|
|
11531
|
-
if (writeMode) log(`# ⚠ --dangerously-bypass-approvals-and-sandbox: sandbox 우회`);
|
|
11532
|
-
} else if (target === 'agy') {
|
|
11533
|
-
const flags = writeMode ? '-p --yolo' : '-p';
|
|
11534
|
-
log(`agy ${flags} "${q}"`);
|
|
11535
|
-
if (writeMode) log(`# ⚠ --yolo: 워크스페이스 파일 직접 수정 가능`);
|
|
11536
|
-
} else if (target === 'copilot') {
|
|
11537
|
-
log(`gh copilot suggest "${q}"`);
|
|
11538
|
-
}
|
|
11578
|
+
// 1.9.270: _dispatchCommand 로 통일 (roleModel 주입) — 명령 빌더 단일화
|
|
11579
|
+
log(_dispatchCommand(target, task, writeMode, roleModel));
|
|
11580
|
+
if (target === 'claude' && writeMode) log(`# ⚠ --dangerously-skip-permissions: 도구 권한 자동 승인 (파일 수정 가능)`);
|
|
11581
|
+
if (target === 'codex') { log(`# ℹ codex는 PowerShell 경유 — POSIX /tmp 경로는 C:\\tmp\\로 해석됨`); if (writeMode) log(`# ⚠ --dangerously-bypass-approvals-and-sandbox: sandbox 우회`); }
|
|
11582
|
+
if (target === 'agy' && writeMode) log(`# ⚠ --yolo: 워크스페이스 파일 직접 수정 가능`);
|
|
11583
|
+
if (target === 'grok' && writeMode) log(`# ⚠ grok --yolo: 자동 승인 (배포판에 따라 플래그 상이 가능)`);
|
|
11539
11584
|
// 1.9.266 (UR-0021 2단계): 대상 에이전트의 슬래시 명령 힌트 — sub-agent 작업 시 알맞은 슬래시 명령 참조
|
|
11540
11585
|
try {
|
|
11541
11586
|
const hint = _agentSlashHint(root, target);
|
|
@@ -16468,7 +16513,8 @@ function mcpServeCmd(root) {
|
|
|
16468
16513
|
{ name: 'leerness_api_skill', description: '1.9.245 (사용자 명시 UR-0015) — API 문서·관련링크 자동 캐시. 공식 API 문서 URL을 fetch 하고 1단계 same-domain 관련 링크까지 정리 → .harness/api-skills/<id>.md 저장. 후속 같은 API 관련 작업 시 자동 참조. 응답: list (skills 배열) / show (전체 본문) / match (task 매칭 결과). 인자: { path?, sub ("list"|"show"|"match"|"add"|"drop"), url? (add), id? (show/drop), query? (match), direction? (add: 구현 방향 텍스트) }. 외부 AI가 "이 프로젝트 어떤 API 문서가 정리되어 있나?" / "내 작업과 매칭되는 API skill 있나?" 회수.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, sub: { type: 'string', enum: ['list', 'show', 'match', 'add', 'drop'] }, url: { type: 'string' }, id: { type: 'string' }, query: { type: 'string' }, direction: { type: 'string' } }, required: ['sub'] } },
|
|
16469
16514
|
{ name: 'leerness_selftest', description: '1.9.258/259 — 설치된 leerness 바이너리의 코어 함수(보안 _isSecretKey / 버전 compareVer / 인코딩 _classifyCJK 등) 무결성 자가 검증. 응답: { version, total, pass, fail, ok, results[] }. 외부 AI/CI 가 "이 leerness 설치가 정상인가?(npx 캐시 손상·부분 설치 감지)" 를 1초 내 확인. 인자: 없음.', inputSchema: { type: 'object', properties: {} } },
|
|
16470
16515
|
{ name: 'leerness_shell_guard', description: '1.9.260/261 (사용자 명시 UR-0020) — 터미널 명령 셸 호환성 린터. 외부 AI 가 명령을 실행하기 전에 셸 호환성 문제를 사전 점검. 예: Windows PowerShell 5.1 은 && / || 미지원 → A; if ($?) { B } 제안. 6 규칙(ps5-chain/ps-devnull/ps-inline-env/ps-rm-rf/cmd-semicolon/ps-version-unknown) + 과거 실패 회수(.harness/shell-failures.json). 응답: { shell, psVersion, issues[{rule,severity,detail,suggestion}], pastSame, pastSimilar, ok }. 인자: { command (required), path? }. 현재 셸/PS 버전 자동 감지.', inputSchema: { type: 'object', properties: { command: { type: 'string' }, path: { type: 'string' } }, required: ['command'] } },
|
|
16471
|
-
{ name: 'leerness_slash_commands', description: '1.9.265/266 (사용자 명시 UR-0021) — CLI AI 에이전트별 슬래시 명령어 레지스트리. 외부 AI(메인)가 sub-agent(codex/agy/claude/grok/copilot)를 호출할 때 각 에이전트에 알맞는 슬래시 명령을 참조. 빌트인 + 사용자 .harness/agent-slash-commands.json override 병합. 응답: { agents: { <id>: { label, asOf, invoke(slash|subcommand), note, source, count, commands[{cmd,desc}] } } }. 인자: { path?, agent? (생략 시 전체), refresh? (1.9.267 — 설치된 CLI --help probe 자동 갱신), dryRun? }. agent 지정 시 단일.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, agent: { type: 'string' }, refresh: { type: 'boolean' }, dryRun: { type: 'boolean' } } } }
|
|
16516
|
+
{ name: 'leerness_slash_commands', description: '1.9.265/266 (사용자 명시 UR-0021) — CLI AI 에이전트별 슬래시 명령어 레지스트리. 외부 AI(메인)가 sub-agent(codex/agy/claude/grok/copilot)를 호출할 때 각 에이전트에 알맞는 슬래시 명령을 참조. 빌트인 + 사용자 .harness/agent-slash-commands.json override 병합. 응답: { agents: { <id>: { label, asOf, invoke(slash|subcommand), note, source, count, commands[{cmd,desc}] } } }. 인자: { path?, agent? (생략 시 전체), refresh? (1.9.267 — 설치된 CLI --help probe 자동 갱신), dryRun? }. agent 지정 시 단일.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, agent: { type: 'string' }, refresh: { type: 'boolean' }, dryRun: { type: 'boolean' } } } },
|
|
16517
|
+
{ name: 'leerness_roles', description: '1.9.270 (사용자 명시) — 모델별 역할 부여. 여러 AI 에이전트 활성 시 역할(commander/reviewer/coder/architect/designer/debugger/dispatcher)을 provider+model 에 매핑하고, agents dispatch --role 로 라우팅. sub=suggest 면 활성 에이전트 기반 최적 배치 + 근거 반환(방향성 판단). sub=set 시 role/provider/model 필요. 응답: list/suggest/verify 별 JSON. 인자: { path?, sub(list|set|unset|catalog|suggest|verify), role?, provider?, model?, apply? }.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, sub: { type: 'string' }, role: { type: 'string' }, provider: { type: 'string' }, model: { type: 'string' }, apply: { type: 'boolean' } } } }
|
|
16472
16518
|
];
|
|
16473
16519
|
|
|
16474
16520
|
function send(obj) {
|
|
@@ -16685,6 +16731,13 @@ function mcpServeCmd(root) {
|
|
|
16685
16731
|
if (args.refresh === true) cliArgs.push('--refresh');
|
|
16686
16732
|
if (args.dryRun === true) cliArgs.push('--dry-run');
|
|
16687
16733
|
break;
|
|
16734
|
+
case 'leerness_roles':
|
|
16735
|
+
// 1.9.270 (사용자 명시): 모델별 역할 부여
|
|
16736
|
+
cliArgs = ['roles', String(args.sub || 'list'), ...(args.role ? [String(args.role)] : []), '--path', targetPath, '--json'];
|
|
16737
|
+
if (args.provider) cliArgs.push('--provider', String(args.provider));
|
|
16738
|
+
if (args.model) cliArgs.push('--model', String(args.model));
|
|
16739
|
+
if (args.apply === true) cliArgs.push('--apply');
|
|
16740
|
+
break;
|
|
16688
16741
|
default:
|
|
16689
16742
|
return send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown tool: ${name}` } });
|
|
16690
16743
|
}
|
|
@@ -17627,6 +17680,11 @@ const _PROVIDER_MODEL_CATALOG = {
|
|
|
17627
17680
|
{ id: 'antigravity-flash', note: '빠른 응답' },
|
|
17628
17681
|
{ id: 'antigravity-experimental', note: '실험적 (사용 가능 시)' }
|
|
17629
17682
|
],
|
|
17683
|
+
grok: [
|
|
17684
|
+
{ id: 'grok-beta', note: 'xAI 최신 (1.9.268 provider 승격)' },
|
|
17685
|
+
{ id: 'grok-2', note: 'xAI Grok 2' },
|
|
17686
|
+
{ id: 'grok-2-mini', note: '빠른 응답' }
|
|
17687
|
+
],
|
|
17630
17688
|
copilot: [
|
|
17631
17689
|
{ id: 'default', note: 'gh copilot 기본 (모델 선택 불가)' }
|
|
17632
17690
|
],
|
|
@@ -17638,8 +17696,8 @@ const _PROVIDER_MODEL_CATALOG = {
|
|
|
17638
17696
|
]
|
|
17639
17697
|
};
|
|
17640
17698
|
|
|
17641
|
-
// 1.9.170: provider cycle 순서 (Tab) — 빌트인
|
|
17642
|
-
const _PROVIDER_CYCLE_ORDER = ['ollama', 'claude', 'codex', 'agy', 'copilot'];
|
|
17699
|
+
// 1.9.170: provider cycle 순서 (Tab) — 빌트인 6종(1.9.268 grok 추가). user provider는 동적으로 뒤에 추가.
|
|
17700
|
+
const _PROVIDER_CYCLE_ORDER = ['ollama', 'claude', 'codex', 'agy', 'grok', 'copilot'];
|
|
17643
17701
|
|
|
17644
17702
|
// 1.9.148: planner/reviewer/actor 역할 시스템 프롬프트 (Gemini 권고 — 자기-승인 편향 방지)
|
|
17645
17703
|
const _AGENT_ROLE_PROMPTS = {
|
|
@@ -17647,6 +17705,194 @@ const _AGENT_ROLE_PROMPTS = {
|
|
|
17647
17705
|
reviewer: '역할: reviewer. planner 의 계획 또는 actor 의 결과를 비판적으로 검토. 누락된 검증, 잠재 cascade, 오류 가능성 지적. 동의/수정 결론 명시.',
|
|
17648
17706
|
actor: '역할: actor. 계획에 따라 정확한 명령/코드만 실행. evidence(파일 경로 + 테스트 결과) 함께 기록. 새 계획 생성 금지.'
|
|
17649
17707
|
};
|
|
17708
|
+
// ===== 1.9.270: Agent Roles — 모델별 역할 부여 (사용자 명시) =====
|
|
17709
|
+
// 여러 AI 에이전트 활성 시 역할(코딩/검수/지휘/디자인/디버그/설계/분배)을 provider+model 에 매핑.
|
|
17710
|
+
// 사용자 설정: .harness/agent-roles.json { schemaVersion, roles: { <role>: { provider, model, persona } } }
|
|
17711
|
+
// dispatch --role <role> → 역할 기반 provider+model 라우팅. roles suggest → 활성 에이전트 기반 최적 배치 판단.
|
|
17712
|
+
// 방향성 판단(사용자 요청): 모델별 강점이 분명하고(추론/코드/멀티모달) 작업이 분해 가능할 때, 역할 특화는
|
|
17713
|
+
// 품질·정확도를 올림 — 강추론 모델의 독립 검수는 자기승인 편향을 차단, 코드 특화 모델은 구현 정확도↑.
|
|
17714
|
+
// 단 조율 오버헤드 < 이득이어야 하므로 opt-in + verify(비활성 provider 배정 경고)로 오설정을 방지.
|
|
17715
|
+
const ROLE_CATALOG = {
|
|
17716
|
+
commander: { ko: '지휘관', desc: '전체 계획 수립·작업 분해·최종 의사결정', prefer: ['claude', 'codex', 'grok'], modelKind: 'top', why: '최강 추론 모델이 전체 맥락을 쥐고 분해/결정 → 일관성↑' },
|
|
17717
|
+
reviewer: { ko: '검수자', desc: '구현 결과 비판적 검수·버그/누락 적발', prefer: ['claude', 'codex', 'grok'], modelKind: 'top', why: '독립 강추론 모델의 적대적 검수가 자기승인 편향 차단 → 정확도↑' },
|
|
17718
|
+
coder: { ko: '코딩 담당', desc: '구현·패치·리팩터', prefer: ['codex', 'agy', 'claude', 'grok'], modelKind: 'code', why: '코드 특화 모델이 구현 정확도/속도↑' },
|
|
17719
|
+
architect: { ko: '설계 담당', desc: '아키텍처·데이터 모델·인터페이스 설계', prefer: ['codex', 'claude'], modelKind: 'top', why: '깊은 코드 추론 모델이 설계 트레이드오프 분석' },
|
|
17720
|
+
designer: { ko: '디자인 담당', desc: 'UI/UX·시각 디자인·레이아웃', prefer: ['agy', 'claude', 'grok'], modelKind: 'top', why: '멀티모달/시각 역량 모델이 디자인 산출물↑' },
|
|
17721
|
+
debugger: { ko: '디버그 담당', desc: '버그 진단·근본원인·재현', prefer: ['codex', 'claude', 'grok'], modelKind: 'top', why: '깊은 추론 모델의 가설-검증이 디버깅 효율↑' },
|
|
17722
|
+
dispatcher: { ko: '하위 분배 담당', desc: '서브에이전트 업무 분배·병렬 조율', prefer: ['claude', 'grok', 'codex'], modelKind: 'fast', why: '빠른 응답 모델이 분배 오버헤드↓' }
|
|
17723
|
+
};
|
|
17724
|
+
const _ROLE_ALIASES = {
|
|
17725
|
+
'코딩': 'coder', '코더': 'coder', '코딩담당': 'coder',
|
|
17726
|
+
'검수': 'reviewer', '검수자': 'reviewer', '리뷰': 'reviewer', '리뷰어': 'reviewer',
|
|
17727
|
+
'지휘': 'commander', '지휘관': 'commander', '사령관': 'commander',
|
|
17728
|
+
'디자인': 'designer', '디자인담당': 'designer',
|
|
17729
|
+
'디버그': 'debugger', '디버거': 'debugger', '디버깅': 'debugger',
|
|
17730
|
+
'설계': 'architect', '설계담당': 'architect', '아키텍트': 'architect',
|
|
17731
|
+
'분배': 'dispatcher', '분배담당': 'dispatcher', '오케스트레이터': 'dispatcher'
|
|
17732
|
+
};
|
|
17733
|
+
function _normalizeRole(name) {
|
|
17734
|
+
const raw = String(name || '').trim();
|
|
17735
|
+
if (_ROLE_ALIASES[raw]) return _ROLE_ALIASES[raw];
|
|
17736
|
+
const low = raw.toLowerCase();
|
|
17737
|
+
return _ROLE_ALIASES[low] || low;
|
|
17738
|
+
}
|
|
17739
|
+
// 역할 modelKind 에 맞는 모델 선택 — top(최상위) / code(코드 특화) / fast(빠른). 순수 함수(selftest).
|
|
17740
|
+
function _pickModel(provider, kind) {
|
|
17741
|
+
const list = _PROVIDER_MODEL_CATALOG[provider] || [];
|
|
17742
|
+
if (!list.length) return null;
|
|
17743
|
+
if (kind === 'code') { const m = list.find(x => /codex|coder|code/i.test(x.id)); return (m || list[0]).id; }
|
|
17744
|
+
if (kind === 'fast') { const m = list.find(x => /haiku|mini|flash|oss/i.test(x.id)); return (m || list[list.length - 1]).id; }
|
|
17745
|
+
return list[0].id; // top
|
|
17746
|
+
}
|
|
17747
|
+
function _rolesFile(root) { return path.join(absRoot(root), '.harness', 'agent-roles.json'); }
|
|
17748
|
+
function _loadRoles(root) {
|
|
17749
|
+
const f = _rolesFile(root);
|
|
17750
|
+
if (!exists(f)) return {};
|
|
17751
|
+
try { const j = JSON.parse(read(f)); return (j && j.roles && typeof j.roles === 'object') ? j.roles : {}; } catch { return {}; }
|
|
17752
|
+
}
|
|
17753
|
+
function _saveRoles(root, roles) {
|
|
17754
|
+
const f = _rolesFile(root);
|
|
17755
|
+
mkdirp(path.dirname(f));
|
|
17756
|
+
writeUtf8(f, JSON.stringify({ schemaVersion: 1, updatedAt: new Date().toISOString(), roles }, null, 2) + '\n');
|
|
17757
|
+
return f;
|
|
17758
|
+
}
|
|
17759
|
+
// 역할 → {role, provider, model, persona, source} 해석 (사용자 설정 전용; 미설정 시 null).
|
|
17760
|
+
function _resolveRole(root, name) {
|
|
17761
|
+
const role = _normalizeRole(name);
|
|
17762
|
+
const roles = _loadRoles(root);
|
|
17763
|
+
if (roles[role] && roles[role].provider) return { role, provider: roles[role].provider, model: roles[role].model || null, persona: roles[role].persona || '', source: 'user' };
|
|
17764
|
+
return null;
|
|
17765
|
+
}
|
|
17766
|
+
// 활성(ready) 에이전트 기반 최적 역할 배치 제안 — "올바른 방향성 판단" (사용자 요청).
|
|
17767
|
+
function _suggestRoles(root) {
|
|
17768
|
+
const ready = EXTERNAL_AGENTS.map(a => ({ id: a.id, status: _checkAgent(a) })).filter(x => x.status.status === 'ready').map(x => x.id);
|
|
17769
|
+
const readySet = new Set(ready);
|
|
17770
|
+
const suggestions = [];
|
|
17771
|
+
for (const [role, def] of Object.entries(ROLE_CATALOG)) {
|
|
17772
|
+
const provider = def.prefer.find(p => readySet.has(p)) || null;
|
|
17773
|
+
const model = provider ? _pickModel(provider, def.modelKind) : null;
|
|
17774
|
+
suggestions.push({ role, ko: def.ko, desc: def.desc, provider, model, ready: !!provider, why: def.why, prefer: def.prefer });
|
|
17775
|
+
}
|
|
17776
|
+
return { ready, suggestions };
|
|
17777
|
+
}
|
|
17778
|
+
// leerness roles <list|set|unset|catalog|suggest|verify>
|
|
17779
|
+
function rolesCmd(root, sub, ...args) {
|
|
17780
|
+
root = absRoot(root || process.cwd());
|
|
17781
|
+
try { _loadEnvFile(root); _loadEnvFile(path.join(root, '..')); } catch {}
|
|
17782
|
+
const json = has('--json');
|
|
17783
|
+
sub = sub || 'list';
|
|
17784
|
+
|
|
17785
|
+
if (sub === 'catalog') {
|
|
17786
|
+
if (json) { log(JSON.stringify({ roles: ROLE_CATALOG }, null, 2)); return; }
|
|
17787
|
+
log(`# leerness roles catalog (1.9.270) — 알려진 역할 ${Object.keys(ROLE_CATALOG).length}종`);
|
|
17788
|
+
for (const [role, d] of Object.entries(ROLE_CATALOG)) {
|
|
17789
|
+
log(` ${role.padEnd(11)} ${d.ko.padEnd(8)} — ${d.desc}`);
|
|
17790
|
+
log(` 선호 provider: ${d.prefer.join(' > ')} · 모델 등급: ${d.modelKind} · 근거: ${d.why}`);
|
|
17791
|
+
}
|
|
17792
|
+
log(`\n 설정: leerness roles set <role> --provider <id> [--model <m>] [--persona "..."]`);
|
|
17793
|
+
log(` 자동 제안: leerness roles suggest [--apply]`);
|
|
17794
|
+
return;
|
|
17795
|
+
}
|
|
17796
|
+
|
|
17797
|
+
if (sub === 'set') {
|
|
17798
|
+
const roleRaw = args.find(a => a && !a.startsWith('-'));
|
|
17799
|
+
const role = _normalizeRole(roleRaw);
|
|
17800
|
+
if (!role) return fail('roles set <role> 필요 (예: coder, reviewer, commander)');
|
|
17801
|
+
if (!ROLE_CATALOG[role] && !has('--force')) return fail(`알 수 없는 역할: ${role} (catalog: ${Object.keys(ROLE_CATALOG).join(', ')}) — 커스텀 역할은 --force`);
|
|
17802
|
+
const provider = arg('--provider', null) || arg('--to', null);
|
|
17803
|
+
if (!provider) return fail('--provider <id> 필요 (claude/codex/agy/grok/copilot/ollama)');
|
|
17804
|
+
if (!_allProviders(root).some(p => p.id === provider) && !has('--force')) return fail(`알 수 없는 provider: ${provider} — 커스텀은 --force 또는 provider add`);
|
|
17805
|
+
let model = arg('--model', null);
|
|
17806
|
+
if (!model) model = _pickModel(provider, (ROLE_CATALOG[role] || {}).modelKind || 'top');
|
|
17807
|
+
const persona = arg('--persona', null) || (ROLE_CATALOG[role] ? ROLE_CATALOG[role].desc : '');
|
|
17808
|
+
const roles = _loadRoles(root);
|
|
17809
|
+
roles[role] = { provider, model: model || null, persona };
|
|
17810
|
+
const f = _saveRoles(root, roles);
|
|
17811
|
+
// 비활성 provider 경고 (verify 사전 노출)
|
|
17812
|
+
let warnMsg = '';
|
|
17813
|
+
try { const st = _checkAgent(EXTERNAL_AGENTS.find(a => a.id === provider) || { id: provider, bin: provider, versionArgs: ['--version'], envFlag: `LEERNESS_ENABLE_${provider.toUpperCase()}` }); if (st.status !== 'ready') warnMsg = `⚠ ${provider} 현재 비활성(${st.status}) — 활성화: LEERNESS_ENABLE_${provider.toUpperCase()}=1 + CLI 설치`; } catch {}
|
|
17814
|
+
if (json) { log(JSON.stringify({ set: role, provider, model: model || null, persona, file: f, warning: warnMsg || null }, null, 2)); return; }
|
|
17815
|
+
ok(`역할 설정: ${role} (${ROLE_CATALOG[role] ? ROLE_CATALOG[role].ko : 'custom'}) → ${provider}${model ? ' / ' + model : ''}`);
|
|
17816
|
+
if (persona) log(` persona: ${persona}`);
|
|
17817
|
+
if (warnMsg) warn(warnMsg);
|
|
17818
|
+
log(` 파일: ${f} · 사용: leerness agents dispatch "<task>" --role ${role}`);
|
|
17819
|
+
return;
|
|
17820
|
+
}
|
|
17821
|
+
|
|
17822
|
+
if (sub === 'unset' || sub === 'remove' || sub === 'rm') {
|
|
17823
|
+
const role = _normalizeRole(args.find(a => a && !a.startsWith('-')));
|
|
17824
|
+
const roles = _loadRoles(root);
|
|
17825
|
+
if (!roles[role]) return fail(`설정되지 않은 역할: ${role}`);
|
|
17826
|
+
delete roles[role];
|
|
17827
|
+
const f = _saveRoles(root, roles);
|
|
17828
|
+
if (json) { log(JSON.stringify({ removed: role, file: f }, null, 2)); return; }
|
|
17829
|
+
ok(`역할 제거: ${role}`);
|
|
17830
|
+
return;
|
|
17831
|
+
}
|
|
17832
|
+
|
|
17833
|
+
if (sub === 'suggest') {
|
|
17834
|
+
const { ready, suggestions } = _suggestRoles(root);
|
|
17835
|
+
if (has('--apply')) {
|
|
17836
|
+
const roles = _loadRoles(root);
|
|
17837
|
+
let applied = 0;
|
|
17838
|
+
for (const s of suggestions) { if (s.ready) { roles[s.role] = { provider: s.provider, model: s.model, persona: ROLE_CATALOG[s.role].desc }; applied++; } }
|
|
17839
|
+
const f = _saveRoles(root, roles);
|
|
17840
|
+
if (json) { log(JSON.stringify({ applied, ready, file: f, suggestions }, null, 2)); return; }
|
|
17841
|
+
ok(`제안 적용: ${applied}개 역할 → ${f}`);
|
|
17842
|
+
for (const s of suggestions.filter(x => x.ready)) log(` ${s.role.padEnd(11)} → ${s.provider} / ${s.model}`);
|
|
17843
|
+
const unready = suggestions.filter(x => !x.ready);
|
|
17844
|
+
if (unready.length) log(` (미배정 ${unready.length}: ${unready.map(x => x.role).join(', ')} — 활성 provider 없음)`);
|
|
17845
|
+
return;
|
|
17846
|
+
}
|
|
17847
|
+
if (json) { log(JSON.stringify({ ready, suggestions }, null, 2)); return; }
|
|
17848
|
+
log(`# leerness roles suggest (1.9.270) — 활성 에이전트 기반 최적 역할 배치`);
|
|
17849
|
+
log(`활성(ready): ${ready.join(', ') || '(없음 — LEERNESS_ENABLE_* + CLI 설치 필요)'}`);
|
|
17850
|
+
log('');
|
|
17851
|
+
for (const s of suggestions) {
|
|
17852
|
+
const mark = s.ready ? '✓' : '·';
|
|
17853
|
+
log(` ${mark} ${s.role.padEnd(11)} ${s.ko.padEnd(8)} → ${s.ready ? `${s.provider} / ${s.model}` : '(미배정 — 선호 ' + s.prefer.join('>') + ' 비활성)'}`);
|
|
17854
|
+
log(` 근거: ${s.why}`);
|
|
17855
|
+
}
|
|
17856
|
+
log(`\n 적용: leerness roles suggest --apply · 개별: leerness roles set <role> --provider <id>`);
|
|
17857
|
+
return;
|
|
17858
|
+
}
|
|
17859
|
+
|
|
17860
|
+
if (sub === 'verify') {
|
|
17861
|
+
const roles = _loadRoles(root);
|
|
17862
|
+
const entries = Object.entries(roles);
|
|
17863
|
+
const results = entries.map(([role, def]) => {
|
|
17864
|
+
let status = 'unknown';
|
|
17865
|
+
try { const a = EXTERNAL_AGENTS.find(x => x.id === def.provider); status = a ? _checkAgent(a).status : 'unknown-provider'; } catch {}
|
|
17866
|
+
return { role, provider: def.provider, model: def.model || null, status, ok: status === 'ready' };
|
|
17867
|
+
});
|
|
17868
|
+
const bad = results.filter(r => !r.ok);
|
|
17869
|
+
if (json) { log(JSON.stringify({ total: entries.length, ready: results.filter(r => r.ok).length, issues: bad, results }, null, 2)); return; }
|
|
17870
|
+
log(`# leerness roles verify (1.9.270)`);
|
|
17871
|
+
if (!entries.length) { log(' (설정된 역할 없음 — leerness roles suggest --apply 또는 roles set)'); return; }
|
|
17872
|
+
for (const r of results) log(` ${r.ok ? '🟢' : '🔴'} ${r.role.padEnd(11)} → ${r.provider}${r.model ? ' / ' + r.model : ''} [${r.status}]`);
|
|
17873
|
+
if (bad.length) { log(`\n ⚠ ${bad.length}개 역할이 비활성 provider 배정 — 활성화 또는 roles set 으로 재배정`); process.exitCode = 1; }
|
|
17874
|
+
else log(`\n ✓ 모든 역할이 활성 provider 에 배정됨`);
|
|
17875
|
+
return;
|
|
17876
|
+
}
|
|
17877
|
+
|
|
17878
|
+
// default: list
|
|
17879
|
+
const roles = _loadRoles(root);
|
|
17880
|
+
const entries = Object.entries(roles);
|
|
17881
|
+
if (json) { log(JSON.stringify({ roles, count: entries.length }, null, 2)); return; }
|
|
17882
|
+
log(`# leerness roles (1.9.270) — 설정된 역할 ${entries.length}개`);
|
|
17883
|
+
if (!entries.length) {
|
|
17884
|
+
log(` (없음) — leerness roles suggest 로 활성 에이전트 기반 제안 확인 → roles suggest --apply`);
|
|
17885
|
+
log(` 또는: leerness roles set coder --provider codex --model gpt-5.5`);
|
|
17886
|
+
return;
|
|
17887
|
+
}
|
|
17888
|
+
for (const [role, def] of entries) {
|
|
17889
|
+
const ko = ROLE_CATALOG[role] ? ROLE_CATALOG[role].ko : 'custom';
|
|
17890
|
+
log(` ${role.padEnd(11)} ${ko.padEnd(8)} → ${def.provider}${def.model ? ' / ' + def.model : ''}`);
|
|
17891
|
+
if (def.persona) log(` ${def.persona}`);
|
|
17892
|
+
}
|
|
17893
|
+
log(`\n 사용: leerness agents dispatch "<task>" --role <role> · 검증: leerness roles verify`);
|
|
17894
|
+
}
|
|
17895
|
+
|
|
17650
17896
|
// 1.9.149+1.9.153: REPL 모드 — leerness 자율 AI 에이전트 (multi-provider 세션)
|
|
17651
17897
|
async function _agentRepl(root, opts) {
|
|
17652
17898
|
// 1.9.153: .env 자동 로드 (REPL 진입 직전) — install 직후 LEERNESS_ENABLE_* 즉시 반영
|
|
@@ -20568,6 +20814,8 @@ async function main() {
|
|
|
20568
20814
|
const agentArg = args[1] && !args[1].startsWith('-') ? args[1] : null;
|
|
20569
20815
|
return slashCommandsCmd(arg('--path', process.cwd()), agentArg, { json: has('--json'), record: has('--record'), force: has('--force'), detect: has('--detect'), refresh: has('--refresh'), dryRun: has('--dry-run') });
|
|
20570
20816
|
}
|
|
20817
|
+
// 1.9.270: leerness roles <list|set|unset|catalog|suggest|verify> — 모델별 역할 부여 (사용자 명시)
|
|
20818
|
+
if (cmd === 'roles' || cmd === 'role') return rolesCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
|
|
20571
20819
|
// 1.9.245: API skill cache — 공식 문서·관련링크 자동 정리 (사용자 명시 UR-0015)
|
|
20572
20820
|
if (cmd === 'api-skill') return apiSkillCmd(arg('--path', process.cwd()), args[1] || 'help');
|
|
20573
20821
|
// 1.9.208: leerness constraints <list|check|add> — 플랫폼/API 제약 사전 체크 (사용자 명시)
|
|
@@ -20736,5 +20984,9 @@ module.exports = {
|
|
|
20736
20984
|
_shellFailuresPath, _loadShellFailures, _recordShellFailure, _shellEnvDrift,
|
|
20737
20985
|
// 1.9.265: CLI 에이전트 슬래시 명령어 레지스트리 (UR-0021 1단계) — 단위 테스트
|
|
20738
20986
|
AGENT_SLASH_COMMANDS, _agentSlashFile, _loadAgentSlashCommands, _recordAgentSlashCommands, _agentSlashHint, slashCommandsCmd,
|
|
20739
|
-
_parseSlashFromHelp, _probeAgentSlash, _refreshAgentSlashCommands
|
|
20987
|
+
_parseSlashFromHelp, _probeAgentSlash, _refreshAgentSlashCommands,
|
|
20988
|
+
// 1.9.269: 시스템(OS) 언어 감지 (UR-0022) + 언어 판별 — 단위 테스트
|
|
20989
|
+
_detectSystemLang, detectLanguageValue,
|
|
20990
|
+
// 1.9.270: agent roles — 모델별 역할 부여 (사용자 명시) — 단위 테스트
|
|
20991
|
+
ROLE_CATALOG, _normalizeRole, _pickModel, _rolesFile, _loadRoles, _saveRoles, _resolveRole, _suggestRoles, rolesCmd, _dispatchCommand
|
|
20740
20992
|
};
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -2704,5 +2704,49 @@ run('consistency check', ['consistency', 'check', tmp]);
|
|
|
2704
2704
|
run('--version', ['--version']);
|
|
2705
2705
|
run('--help', ['--help']);
|
|
2706
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
|
+
|
|
2725
|
+
// 1.9.270 회귀: agent roles — 모델별 역할 부여 (사용자 명시)
|
|
2726
|
+
total++;
|
|
2727
|
+
{
|
|
2728
|
+
const tmpRole = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-roles-'));
|
|
2729
|
+
// set (한국어 별칭) + model
|
|
2730
|
+
const rSet = cp.spawnSync(process.execPath, [CLI, 'roles', 'set', '코딩', '--provider', 'codex', '--model', 'gpt-5.5', '--path', tmpRole, '--json'], { encoding: 'utf8', timeout: 15000 });
|
|
2731
|
+
let setOk = false;
|
|
2732
|
+
try { const j = JSON.parse(rSet.stdout); setOk = j.set === 'coder' && j.provider === 'codex' && j.model === 'gpt-5.5'; } catch {}
|
|
2733
|
+
// list JSON
|
|
2734
|
+
const rList = cp.spawnSync(process.execPath, [CLI, 'roles', 'list', '--path', tmpRole, '--json'], { encoding: 'utf8', timeout: 15000 });
|
|
2735
|
+
let listOk = false;
|
|
2736
|
+
try { const j = JSON.parse(rList.stdout); listOk = j.count === 1 && j.roles.coder && j.roles.coder.provider === 'codex'; } catch {}
|
|
2737
|
+
// dispatch --role → 모델 라우팅 (claude 활성; claude 미설치 환경이면 비활성 메시지 허용)
|
|
2738
|
+
cp.spawnSync(process.execPath, [CLI, 'roles', 'set', 'reviewer', '--provider', 'claude', '--model', 'claude-opus-4-7', '--path', tmpRole], { encoding: 'utf8', timeout: 15000 });
|
|
2739
|
+
const envC = { ...process.env, LEERNESS_ENABLE_CLAUDE: '1' };
|
|
2740
|
+
const rRoute = cp.spawnSync(process.execPath, [CLI, 'agents', 'dispatch', '검수', '--role', 'reviewer', '--path', tmpRole], { encoding: 'utf8', timeout: 15000, env: envC });
|
|
2741
|
+
const routeOk = /역할 reviewer → claude/.test(rRoute.stdout) && (/--model claude-opus-4-7/.test(rRoute.stdout) || /claude 비활성/.test(rRoute.stdout));
|
|
2742
|
+
// catalog 7종
|
|
2743
|
+
const rCat = cp.spawnSync(process.execPath, [CLI, 'roles', 'catalog', '--path', tmpRole, '--json'], { encoding: 'utf8', timeout: 15000 });
|
|
2744
|
+
let catOk = false;
|
|
2745
|
+
try { const j = JSON.parse(rCat.stdout); catOk = Object.keys(j.roles).length === 7 && j.roles.coder && j.roles.commander; } catch {}
|
|
2746
|
+
const ok = setOk && listOk && routeOk && catOk;
|
|
2747
|
+
console.log(ok ? '✓ B(1.9.270) agent roles: set(별칭)/list/dispatch --role 라우팅/catalog 7종' : `✗ roles 실패 (set=${setOk} list=${listOk} route=${routeOk} cat=${catOk})`);
|
|
2748
|
+
if (!ok) { failed++; console.log((rRoute.stdout || '').slice(0, 400)); }
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2707
2751
|
console.log(`\nE2E result: ${total - failed}/${total} passed`);
|
|
2708
2752
|
if (failed > 0) process.exit(1);
|