leerness 1.9.269 → 1.9.271

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/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.269';
10
+ const VERSION = '1.9.271';
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 처리).
@@ -794,9 +794,9 @@ function syncReadme(root) {
794
794
  // e2e 배지: scripts/e2e.js의 출력 "E2E result: N/N passed" 추정 (직접 grep)
795
795
  const e2ePath = path.join(root, 'scripts', 'e2e.js');
796
796
  if (exists(e2ePath)) {
797
- // total++ 횟수 카운트 정확하진 않지만 추세 반영
797
+ // 1.9.271: e2e 테스트 수 = `total++;` 블록 + `run(...)` 호출 (run 헬퍼가 내부에서 total++ 수행) → 실제 결과와 일치.
798
798
  const body = read(e2ePath);
799
- const total = (body.match(/^total\+\+;/gm) || []).length;
799
+ const total = (body.match(/^total\+\+;/gm) || []).length + (body.match(/^run\(/gm) || []).length;
800
800
  if (total > 0) {
801
801
  updated = updated.replace(/badge\/e2e-(\d+)%2F(\d+)-success/g, `badge/e2e-${total}%2F${total}-success`);
802
802
  }
@@ -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
- function _dispatchCommand(agentId, task, writeMode) {
11307
+ // 1.9.270: model 인자 추가 — 역할(roles) 기반 dispatch 시 provider 별 모델 플래그 주입 (없으면 기존 동작 그대로).
11308
+ function _dispatchCommand(agentId, task, writeMode, model) {
11305
11309
  const q = String(task || '').replace(/"/g, '\\"');
11306
- if (agentId === 'claude') return `claude ${writeMode ? '--print --dangerously-skip-permissions' : '--print'} "${q}"`;
11307
- if (agentId === 'codex') return `codex ${writeMode ? 'exec --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox' : 'exec --skip-git-repo-check'} "${q}"`;
11308
- if (agentId === 'agy') return `agy ${writeMode ? '-p --yolo' : '-p'} "${q}"`;
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
- const target = arg('--to', null);
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
- if (!target) { fail('--to <agent_id> 필요 (claude/codex/agy/copilot) 활성 전체에 일괄 분배는 `leerness agents multi`'); return process.exit(1); }
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
- const q = task.replace(/"/g, '\\"');
11559
- if (target === 'claude') {
11560
- const flags = writeMode ? '--print --dangerously-skip-permissions' : '--print';
11561
- log(`claude ${flags} "${q}"`);
11562
- if (writeMode) log(`# ⚠ --dangerously-skip-permissions: 도구 권한 자동 승인 (파일 수정 가능)`);
11563
- } else if (target === 'codex') {
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) — 빌트인 5종. user provider는 동적으로 뒤에 추가.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.269",
3
+ "version": "1.9.271",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
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);