leerness 1.9.269 → 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 +25 -0
- package/bin/harness.js +244 -30
- package/package.json +1 -1
- package/scripts/e2e.js +26 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
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
|
+
|
|
3
28
|
## 1.9.269 — 2026-06-02 — UR-0022: init 시 OS 시스템 언어 감지 → 설치 가이드 자동 언어 선택
|
|
4
29
|
|
|
5
30
|
**🌐 `npx leerness init` 등 설치 시 OS 시스템 언어를 감지해 설치 가이드/생성 문서를 알맞은 언어로 표시 (사용자 명시 UR-0022).**
|
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 처리).
|
|
@@ -2948,6 +2948,8 @@ function _selfTestCases() {
|
|
|
2948
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
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
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')); } },
|
|
2951
2953
|
{ name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
|
|
2952
2954
|
];
|
|
2953
2955
|
}
|
|
@@ -3602,7 +3604,8 @@ function commandsCmd(root) {
|
|
|
3602
3604
|
{ cmd: 'session close [path] [--json] [--auto-apply-delivered]', desc: '세션 마감 + 9 카테고리 + 자동 통합' },
|
|
3603
3605
|
{ cmd: 'resume [path]', desc: 'auto-resume-plan 적용 (1.9.203)' },
|
|
3604
3606
|
{ cmd: 'route <task-type>', desc: '작업 유형 분류 (11종)' },
|
|
3605
|
-
{ 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' },
|
|
3606
3609
|
{ cmd: 'slash-commands [agent] [--json --record --detect --refresh]', desc: 'CLI 에이전트 슬래시 명령 레지스트리 + --help probe 자동 갱신 (1.9.265~267, UR-0021)' },
|
|
3607
3610
|
{ cmd: 'review-request "<request>"', desc: '사용자 요청 사전 검토 (1.9.176)' },
|
|
3608
3611
|
{ cmd: 'review <file> --persona <ids>', desc: '페르소나 리뷰 (1.9.29)' },
|
|
@@ -11301,13 +11304,16 @@ async function setupAgentsCmd(root, opts = {}) {
|
|
|
11301
11304
|
}
|
|
11302
11305
|
|
|
11303
11306
|
// 1.9.152: 단일 agent dispatch 명령 빌더 — agents dispatch (단일) + agents multi (복수) 가 공유
|
|
11304
|
-
|
|
11307
|
+
// 1.9.270: model 인자 추가 — 역할(roles) 기반 dispatch 시 provider 별 모델 플래그 주입 (없으면 기존 동작 그대로).
|
|
11308
|
+
function _dispatchCommand(agentId, task, writeMode, model) {
|
|
11305
11309
|
const q = String(task || '').replace(/"/g, '\\"');
|
|
11306
|
-
|
|
11307
|
-
if (agentId === '
|
|
11308
|
-
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}"`;
|
|
11309
11315
|
if (agentId === 'copilot') return `gh copilot suggest "${q}"`;
|
|
11310
|
-
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)`;
|
|
11311
11317
|
return `# ${agentId}: 명령 빌더 미정의`;
|
|
11312
11318
|
}
|
|
11313
11319
|
|
|
@@ -11524,13 +11530,26 @@ function agentsCmd(root, sub, ...args) {
|
|
|
11524
11530
|
}
|
|
11525
11531
|
if (sub === 'dispatch') {
|
|
11526
11532
|
const task = args.filter(x => !x.startsWith('-')).join(' ').trim() || arg('--task', null);
|
|
11527
|
-
|
|
11533
|
+
let target = arg('--to', null);
|
|
11528
11534
|
if (!task) { fail('dispatch "<task>" 또는 --task 필요'); return process.exit(1); }
|
|
11529
11535
|
// 1.9.152: --multi 또는 --to=all 또는 --to 없음 + 활성 ≥2 → multi 모드로 routing
|
|
11530
11536
|
if (has('--multi') || target === 'all' || target === '*') {
|
|
11531
11537
|
return agentsCmd(root, 'multi', ...args);
|
|
11532
11538
|
}
|
|
11533
|
-
|
|
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); }
|
|
11534
11553
|
const agentDef = EXTERNAL_AGENTS.find(a => a.id === target);
|
|
11535
11554
|
if (!agentDef) { fail(`알 수 없는 agent: ${target}`); return process.exit(1); }
|
|
11536
11555
|
// 1.9.36: 작업 유형 키워드 분석 → 최적 CLI 추천 (ready 체크 전에 출력 — 비활성이어도 추천)
|
|
@@ -11554,24 +11573,14 @@ function agentsCmd(root, sub, ...args) {
|
|
|
11554
11573
|
log(`모드: ${writeMode ? '✏ write (파일 수정 가능)' : '🔒 read-only (분석 전용, 안전)'}`);
|
|
11555
11574
|
log('');
|
|
11556
11575
|
log(`## 실행 명령 (사용자가 복사해서 실행)`);
|
|
11576
|
+
if (roleModel) log(`# 🎭 모델: ${roleModel} (역할 기반 라우팅, 1.9.270)`);
|
|
11557
11577
|
log('');
|
|
11558
|
-
|
|
11559
|
-
|
|
11560
|
-
|
|
11561
|
-
|
|
11562
|
-
|
|
11563
|
-
|
|
11564
|
-
const flags = writeMode ? 'exec --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox' : 'exec --skip-git-repo-check';
|
|
11565
|
-
log(`codex ${flags} "${q}"`);
|
|
11566
|
-
log(`# ℹ codex는 PowerShell 경유 — POSIX /tmp 경로는 C:\\tmp\\로 해석됨`);
|
|
11567
|
-
if (writeMode) log(`# ⚠ --dangerously-bypass-approvals-and-sandbox: sandbox 우회`);
|
|
11568
|
-
} else if (target === 'agy') {
|
|
11569
|
-
const flags = writeMode ? '-p --yolo' : '-p';
|
|
11570
|
-
log(`agy ${flags} "${q}"`);
|
|
11571
|
-
if (writeMode) log(`# ⚠ --yolo: 워크스페이스 파일 직접 수정 가능`);
|
|
11572
|
-
} else if (target === 'copilot') {
|
|
11573
|
-
log(`gh copilot suggest "${q}"`);
|
|
11574
|
-
}
|
|
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: 자동 승인 (배포판에 따라 플래그 상이 가능)`);
|
|
11575
11584
|
// 1.9.266 (UR-0021 2단계): 대상 에이전트의 슬래시 명령 힌트 — sub-agent 작업 시 알맞은 슬래시 명령 참조
|
|
11576
11585
|
try {
|
|
11577
11586
|
const hint = _agentSlashHint(root, target);
|
|
@@ -16504,7 +16513,8 @@ function mcpServeCmd(root) {
|
|
|
16504
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'] } },
|
|
16505
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: {} } },
|
|
16506
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'] } },
|
|
16507
|
-
{ 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' } } } }
|
|
16508
16518
|
];
|
|
16509
16519
|
|
|
16510
16520
|
function send(obj) {
|
|
@@ -16721,6 +16731,13 @@ function mcpServeCmd(root) {
|
|
|
16721
16731
|
if (args.refresh === true) cliArgs.push('--refresh');
|
|
16722
16732
|
if (args.dryRun === true) cliArgs.push('--dry-run');
|
|
16723
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;
|
|
16724
16741
|
default:
|
|
16725
16742
|
return send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown tool: ${name}` } });
|
|
16726
16743
|
}
|
|
@@ -17663,6 +17680,11 @@ const _PROVIDER_MODEL_CATALOG = {
|
|
|
17663
17680
|
{ id: 'antigravity-flash', note: '빠른 응답' },
|
|
17664
17681
|
{ id: 'antigravity-experimental', note: '실험적 (사용 가능 시)' }
|
|
17665
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
|
+
],
|
|
17666
17688
|
copilot: [
|
|
17667
17689
|
{ id: 'default', note: 'gh copilot 기본 (모델 선택 불가)' }
|
|
17668
17690
|
],
|
|
@@ -17674,8 +17696,8 @@ const _PROVIDER_MODEL_CATALOG = {
|
|
|
17674
17696
|
]
|
|
17675
17697
|
};
|
|
17676
17698
|
|
|
17677
|
-
// 1.9.170: provider cycle 순서 (Tab) — 빌트인
|
|
17678
|
-
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'];
|
|
17679
17701
|
|
|
17680
17702
|
// 1.9.148: planner/reviewer/actor 역할 시스템 프롬프트 (Gemini 권고 — 자기-승인 편향 방지)
|
|
17681
17703
|
const _AGENT_ROLE_PROMPTS = {
|
|
@@ -17683,6 +17705,194 @@ const _AGENT_ROLE_PROMPTS = {
|
|
|
17683
17705
|
reviewer: '역할: reviewer. planner 의 계획 또는 actor 의 결과를 비판적으로 검토. 누락된 검증, 잠재 cascade, 오류 가능성 지적. 동의/수정 결론 명시.',
|
|
17684
17706
|
actor: '역할: actor. 계획에 따라 정확한 명령/코드만 실행. evidence(파일 경로 + 테스트 결과) 함께 기록. 새 계획 생성 금지.'
|
|
17685
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
|
+
|
|
17686
17896
|
// 1.9.149+1.9.153: REPL 모드 — leerness 자율 AI 에이전트 (multi-provider 세션)
|
|
17687
17897
|
async function _agentRepl(root, opts) {
|
|
17688
17898
|
// 1.9.153: .env 자동 로드 (REPL 진입 직전) — install 직후 LEERNESS_ENABLE_* 즉시 반영
|
|
@@ -20604,6 +20814,8 @@ async function main() {
|
|
|
20604
20814
|
const agentArg = args[1] && !args[1].startsWith('-') ? args[1] : null;
|
|
20605
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') });
|
|
20606
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));
|
|
20607
20819
|
// 1.9.245: API skill cache — 공식 문서·관련링크 자동 정리 (사용자 명시 UR-0015)
|
|
20608
20820
|
if (cmd === 'api-skill') return apiSkillCmd(arg('--path', process.cwd()), args[1] || 'help');
|
|
20609
20821
|
// 1.9.208: leerness constraints <list|check|add> — 플랫폼/API 제약 사전 체크 (사용자 명시)
|
|
@@ -20774,5 +20986,7 @@ module.exports = {
|
|
|
20774
20986
|
AGENT_SLASH_COMMANDS, _agentSlashFile, _loadAgentSlashCommands, _recordAgentSlashCommands, _agentSlashHint, slashCommandsCmd,
|
|
20775
20987
|
_parseSlashFromHelp, _probeAgentSlash, _refreshAgentSlashCommands,
|
|
20776
20988
|
// 1.9.269: 시스템(OS) 언어 감지 (UR-0022) + 언어 판별 — 단위 테스트
|
|
20777
|
-
_detectSystemLang, detectLanguageValue
|
|
20989
|
+
_detectSystemLang, detectLanguageValue,
|
|
20990
|
+
// 1.9.270: agent roles — 모델별 역할 부여 (사용자 명시) — 단위 테스트
|
|
20991
|
+
ROLE_CATALOG, _normalizeRole, _pickModel, _rolesFile, _loadRoles, _saveRoles, _resolveRole, _suggestRoles, rolesCmd, _dispatchCommand
|
|
20778
20992
|
};
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -2722,5 +2722,31 @@ total++;
|
|
|
2722
2722
|
if (!ok) failed++;
|
|
2723
2723
|
}
|
|
2724
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
|
+
|
|
2725
2751
|
console.log(`\nE2E result: ${total - failed}/${total} passed`);
|
|
2726
2752
|
if (failed > 0) process.exit(1);
|