leerness 1.9.422 → 1.9.424
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 +36 -0
- package/README.md +5 -5
- package/bin/leerness.js +38 -778
- package/lib/agents.js +456 -0
- package/lib/health.js +348 -0
- package/package.json +1 -1
package/bin/leerness.js
CHANGED
|
@@ -31,7 +31,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
|
|
|
31
31
|
// 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
|
|
32
32
|
const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST, _DEFAULT_PLATFORM_CONSTRAINTS, _DEFAULT_DOMAIN_CATALOG, _LSP_LANG_PATTERNS, OPTIMISM_PATTERNS, BUILT_IN_PERSONAS, STRINGS, BUILTIN_CATALOG, ROADMAP_STATUS_LABEL, ROADMAP_STATUS_COLOR, SECRET_PATTERNS, MERGE_OVERWRITE_FILES, MINIMAL_SKIP_KEYS, REQUIRED_WORKSPACE_FILES, KEYWORD_STOPWORDS, SKILL_CATALOG_PRESETS } = require('../lib/catalogs'); // 1.9.344/368/369 (UR-0025): catalog 분리 (MERGE_OVERWRITE_FILES/MINIMAL_SKIP_KEYS 포함)
|
|
33
33
|
|
|
34
|
-
const VERSION = '1.9.
|
|
34
|
+
const VERSION = '1.9.424';
|
|
35
35
|
|
|
36
36
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
37
37
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -2965,7 +2965,7 @@ function _selfTestCases() {
|
|
|
2965
2965
|
{ name: 'UR-0061(외부리뷰 P1): roadmap CSS 값 살균 — :root/</style> breakout 차단', run: () => { const m = require('../lib/pure-utils'); const css = m._roadmapTokenStyles({ 'color.primary': 'red;}' + '</style><script>alert(1)</script>' }, {}); const blocked = !css.includes('<') && !css.includes('>'); const primaryLine = (css.split('\n').find(l => l.includes('--lr-primary')) || ''); const noBreakout = !primaryLine.replace(/;$/, '').includes('}'); const preserved = m._roadmapTokenStyles({ 'color.primary': '#2563eb' }, {}).includes('--lr-primary: #2563eb'); return blocked && noBreakout && preserved; } },
|
|
2966
2966
|
{ name: 'UR-0060(외부리뷰 P1): SECRET_PATTERNS 20종 (unquoted 보강) — GitLab/JWT/DB-URI/SendGrid/AWS-secret/Bearer 보강 + 오탐 가드', run: () => { const c = require('../lib/catalogs'); const hit = s => c.SECRET_PATTERNS.some(p => { p.re.lastIndex = 0; return p.re.test(s); }); const det = hit('glpat-' + 'x'.repeat(20)) && hit('eyJ' + 'x'.repeat(15) + '.eyJ' + 'y'.repeat(15) + '.' + 'z'.repeat(15)) && hit('postgres://u:p@host:5432/db') && hit('SG.' + 'x'.repeat(22) + '.' + 'y'.repeat(43)) && hit('aws_secret_access_key = "' + 'x'.repeat(40) + '"') && hit('Bearer ' + 'x'.repeat(25)); const clean = !hit('const u = "john' + '_doe_2024";') && !hit('https://example.com/path/to/page'); return c.SECRET_PATTERNS.length === 20 && det && clean; } },
|
|
2967
2967
|
{ name: 'UR-0068(외부리뷰 P2): _roadmapParseMilestones 블록 경계 — 다음 milestone status 누출 차단', run: () => { const m = require('../lib/pure-utils'); const r = m._roadmapParseMilestones('### M-0001. A\n\n### M-0002. B\nStatus: done\nProgress: 80%\n'); return r.length === 2 && r[0].status === 'planned' && r[0].progress === 0 && r[1].status === 'done' && r[1].progress === 80; } },
|
|
2968
|
-
{ name: 'UR-0066(외부리뷰 P2): shell:true 주입 가드 — agents bench task _shellQuoteArg + fetchNpmLatest cmd.exe args', run: () => { const m = require('../lib/pure-utils'); const src = read(__filename); const benchQuoted =
|
|
2968
|
+
{ name: 'UR-0066(외부리뷰 P2): shell:true 주입 가드 — agents bench task _shellQuoteArg + fetchNpmLatest cmd.exe args', run: () => { const m = require('../lib/pure-utils'); const src = read(__filename); const agentsSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'agents.js')); const benchQuoted = agentsSrc.includes('const qTask = ' + '_shellQuoteArg(task)'); const npmSafe = /'\/d', '\/s', '\/c', 'npm', 'view'/.test(src); const q = m._shellQuoteArg('a & b'); const safe = (process.platform === 'win32' ? q === '"a & b"' : q === "'a & b'"); return benchQuoted && npmSafe && safe; } },
|
|
2969
2969
|
{ name: 'UR-0072(외부리뷰 P3): compareVer pre-release + _classifyCJK 한자 kana 귀속', run: () => { const m = require('../lib/pure-utils'); const verOk = m.compareVer('1.9.0-beta', '1.9.0') === -1 && m.compareVer('1.9.0', '1.9.0-beta') === 1 && m.compareVer('1.9.5', '1.9.5') === 0 && m.compareVer('1.9.6', '1.9.5') === 1; const jp = Buffer.from([0xE3, 0x81, 0x82, 0xE6, 0x97, 0xA5, 0xE6, 0x9C, 0xAC]); const cn = Buffer.from([0xE4, 0xB8, 0xAD, 0xE5, 0x9B, 0xBD]); const rj = m._classifyCJK(jp, jp.length); const rc = m._classifyCJK(cn, cn.length); const cjkOk = rj.japanese > rj.chinese && rc.chinese > 0 && rc.japanese === 0; return verOk && cjkOk; } },
|
|
2970
2970
|
{ name: 'UR-0075 Phase A: 마이그레이션 가이드(_migrationGuideText) + migrate --guide 와이어 + init/migrate/update --path', run: () => { const m = require('../lib/pure-utils'); const g = m._migrationGuideText('1.9.355'); const guideOk = typeof g === 'string' && g.includes('마이그레이션 가이드') && g.includes('update --check --path') && g.includes('selftest') && g.includes('canonical JSON') && g.includes('롤백') && g.includes('1.9.355'); const src = read(__filename); const wired = src.includes("has('--guide') || args[1] === " + "'guide'") && src.includes('install(arg(' + "'--path', args[1] || process.cwd())") && src.includes('updateCmd(arg(' + "'--path', args[1] || process.cwd())"); return guideOk && wired; } },
|
|
2971
2971
|
{ name: 'UR-0075 Phase B: migrate audit(dry-run 스키마 drift) 명령 + 와이어', run: () => { const src = read(__filename); return typeof migrateAuditCmd === 'function' && src.includes('migrateAuditCmd(arg(' + "'--path'") && src.includes("args[1] === " + "'audit'"); } },
|
|
@@ -3073,6 +3073,36 @@ function _selfTestCases() {
|
|
|
3073
3073
|
} },
|
|
3074
3074
|
{ name: '9라운드 (UR-0119/0120): team review(메인 검수) — _composeTeamPlan reviewStep + handoff 검수필요 + team add 와이어 (1.9.414)', run: () => { const m = require('../lib/pure-utils'); const on = m._composeTeamPlan({ id: 't', members: ['a', 'b'], personas: ['security'] }, '점검'); const off = m._composeTeamPlan({ id: 't', members: ['a'], review: false }, '점검'); const planOk = on.review === true && !!on.reviewStep && on.reviewStep.suggestedCommand.includes('verify-claim') && off.review === false && !off.reviewStep; const rem = m._teamHandoffReminders([{ id: 'r', schedule: 'every-session', status: 'active', members: ['a'], review: true }]); const remOk = rem.length === 1 && rem[0].includes('검수필요'); const teamSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'team.js')); const wired = teamSrc.includes("review: !has('--no-review')") && teamSrc.includes('메인 검수 (필수)'); return planOk && remOk && wired; } },
|
|
3075
3075
|
{ name: '파일명 변경 (UR-0126): bin 파일=leerness.js + package.json bin/main 일치 (1.9.419)', run: () => { const okName = path.basename(__filename) === 'leerness.js'; let pkg; try { pkg = require('../package.json'); } catch { return false; } const okBin = pkg && pkg.bin && pkg.bin.leerness === 'bin/leerness.js' && pkg.main === 'bin/leerness.js'; return okName && okBin; } },
|
|
3076
|
+
{ name: 'UR-0025 큰핸들러 모듈화 9번째: agentsCmd → lib/agents.js + DI 위임 + rest→array (1.9.424)', run: () => {
|
|
3077
|
+
const m = require('../lib/agents');
|
|
3078
|
+
const expOk = typeof m.agentsCmd === 'function';
|
|
3079
|
+
const src = read(__filename);
|
|
3080
|
+
const delegated = src.includes("require('../lib/agents')") && src.includes('_agents.agentsCmd(root, sub, args,');
|
|
3081
|
+
const modSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'agents.js'));
|
|
3082
|
+
const bodyMarker = 'LEERNESS_NO_' + 'MULTIAGENT_LESSON'; // agents 본문 고유(split-literal 자기참조 회피)
|
|
3083
|
+
const sigTransform = modSrc.includes('function agentsCmd(root, sub, args = [], deps = {})') && modSrc.includes("agentsCmd(root, 'list', [], deps)");
|
|
3084
|
+
const movedToLib = modSrc.includes("require('./io')") && modSrc.includes("require('./agent-registry')") && sigTransform && modSrc.includes(bodyMarker) && !src.includes(bodyMarker);
|
|
3085
|
+
return expOk && delegated && movedToLib;
|
|
3086
|
+
} },
|
|
3087
|
+
{ name: 'UR-0025 큰핸들러 모듈화 8번째: healthCmd → lib/health.js + DI 위임 + 동작 (1.9.423)', run: () => {
|
|
3088
|
+
const m = require('../lib/health');
|
|
3089
|
+
const expOk = typeof m.healthCmd === 'function';
|
|
3090
|
+
const src = read(__filename);
|
|
3091
|
+
const delegated = src.includes("require('../lib/health')") && src.includes('_health.healthCmd(root,');
|
|
3092
|
+
const modSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'health.js'));
|
|
3093
|
+
const bodyMarker = 'capability' + 'Matrix'; // health 본문 고유(split-literal 자기참조 회피)
|
|
3094
|
+
const movedToLib = modSrc.includes("require('./io')") && modSrc.includes("require('./pure-utils')") && modSrc.includes(bodyMarker) && !src.includes(bodyMarker);
|
|
3095
|
+
let behavOk = false;
|
|
3096
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_health_'));
|
|
3097
|
+
const _w = process.stdout.write; let out = '';
|
|
3098
|
+
try {
|
|
3099
|
+
fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true });
|
|
3100
|
+
process.stdout.write = s => { out += s; return true; };
|
|
3101
|
+
m.healthCmd(tmp, { VERSION, has: f => f === '--json', arg: (k, d) => d, harnessPath: path.join(tmp, '__nope.js'), listAllSkills, planPath, readProgressRows, readRules, envDiff, _collectSecretFindings, _readUsageStats, _loadDecisions, _loadLessons, _loadShellFailures, _readFeatureGraph, _scanShellScriptsEncoding, _shellEnvDrift, _computeMilestones, _computeRecentChanges, _computeRoundHistory, _collectPyFiles, _analyzePyFile, _collectRuntimeEnv, _listAPISkills, _matchAPISkills, _mcpToolCount });
|
|
3102
|
+
} catch (e) { out = 'ERR:' + e.message; } finally { process.stdout.write = _w; try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
3103
|
+
try { const j = JSON.parse(out); behavOk = typeof j.healthy === 'boolean' && !!j.checks; } catch {}
|
|
3104
|
+
return expOk && delegated && movedToLib && behavOk;
|
|
3105
|
+
} },
|
|
3076
3106
|
{ name: 'UR-0025 큰핸들러 모듈화 7번째: driftCheckCmd → lib/drift.js + DI 위임 + 재귀/동작 (1.9.422)', run: () => {
|
|
3077
3107
|
const m = require('../lib/drift');
|
|
3078
3108
|
const expOk = typeof m.driftCheckCmd === 'function';
|
|
@@ -11049,448 +11079,9 @@ function _dispatchCommand(agentId, task, writeMode, model) {
|
|
|
11049
11079
|
return `# ${agentId}: 명령 빌더 미정의`;
|
|
11050
11080
|
}
|
|
11051
11081
|
|
|
11052
|
-
|
|
11053
|
-
|
|
11054
|
-
|
|
11055
|
-
_loadEnvFile(root);
|
|
11056
|
-
_loadEnvFile(path.join(root, '..'));
|
|
11057
|
-
|
|
11058
|
-
if (!sub || sub === 'list') {
|
|
11059
|
-
// 1.9.157: Provider Registry 통합 — 빌트인 5종 + 사용자 정의 provider 포함
|
|
11060
|
-
const providers = _allProviders(root);
|
|
11061
|
-
const userIds = new Set(_readUserProviders(root).map(u => u.id));
|
|
11062
|
-
const checks = providers.map(a => ({ ...(_checkAgent(a)), source: userIds.has(a.id) ? 'user' : 'builtin' }));
|
|
11063
|
-
if (has('--json')) { log(JSON.stringify({ agents: checks }, null, 2)); return; }
|
|
11064
|
-
log(`# 외부 AI CLI 오케스트레이션 (1.9.30)`);
|
|
11065
|
-
log('');
|
|
11066
|
-
log(`| Agent | source | env (${'env=1 활성'}) | 설치 | 버전 | 상태 |`);
|
|
11067
|
-
log(`|---|---|---|---|---|---|`);
|
|
11068
|
-
for (const c of checks) {
|
|
11069
|
-
const envMark = c.enabled ? '✓' : '✗';
|
|
11070
|
-
const instMark = c.installed ? '✓' : '✗';
|
|
11071
|
-
const statusEmoji = c.status === 'ready' ? '🟢 ready' : c.status === 'not-installed' ? '⚪ 미설치' : c.status === 'disabled' ? '🟡 비활성' : '❓';
|
|
11072
|
-
log(`| ${c.id} | ${c.source} | ${envMark} ${c.envFlag} | ${instMark} | ${c.version || '-'} | ${statusEmoji} |`);
|
|
11073
|
-
}
|
|
11074
|
-
const ready = checks.filter(c => c.status === 'ready');
|
|
11075
|
-
log('');
|
|
11076
|
-
log(`## 활성 (${ready.length}/${checks.length}): ${ready.map(c => c.id).join(', ') || '(없음)'}`);
|
|
11077
|
-
if (!ready.length) {
|
|
11078
|
-
log('');
|
|
11079
|
-
log(`💡 활성화 방법:`);
|
|
11080
|
-
log(` 1) CLI 설치 (예: \`npm i -g @openai/codex-cli\`, \`npm i -g @google/antigravity-cli\`)`);
|
|
11081
|
-
log(` 2) .env 또는 환경변수: LEERNESS_ENABLE_CODEX=1, LEERNESS_ENABLE_AGY=1`);
|
|
11082
|
-
log(` 3) \`leerness agents check\`로 재확인`);
|
|
11083
|
-
log(` 💡 1.9.157: 빌트인 외 CLI 추가: \`leerness provider add <id> --bin <cmd>\``);
|
|
11084
|
-
} else {
|
|
11085
|
-
log('');
|
|
11086
|
-
log(`💡 메인 에이전트가 sub-agent 분배 시 위 ${ready.length}개 CLI 활용 가능:`);
|
|
11087
|
-
log(` \`leerness agents dispatch "<task>" --to <id>\` 로 프롬프트 전달`);
|
|
11088
|
-
}
|
|
11089
|
-
return;
|
|
11090
|
-
}
|
|
11091
|
-
|
|
11092
|
-
if (sub === 'check') {
|
|
11093
|
-
// list의 alias, 단 명시적 재확인 (JSON 출력 기본)
|
|
11094
|
-
// 1.9.157: Provider Registry 통합
|
|
11095
|
-
const providers = _allProviders(root);
|
|
11096
|
-
const userIds = new Set(_readUserProviders(root).map(u => u.id));
|
|
11097
|
-
const checks = providers.map(a => ({ ...(_checkAgent(a)), source: userIds.has(a.id) ? 'user' : 'builtin' }));
|
|
11098
|
-
if (has('--json')) { log(JSON.stringify({ agents: checks, ready: checks.filter(c => c.status === 'ready').map(c => c.id) }, null, 2)); return; }
|
|
11099
|
-
return agentsCmd(root, 'list'); // 비-JSON은 list와 동일
|
|
11100
|
-
}
|
|
11101
|
-
|
|
11102
|
-
// 1.9.152: agents multi — 1.9.151 install 복수 선택된 ready 에이전트들에 일괄 dispatch 명령 생성
|
|
11103
|
-
// 단일 task → 활성 N개 에이전트 동시 dispatch 명령들. 사용자가 한 번에 복사 실행하거나 메인 에이전트가 spawn.
|
|
11104
|
-
if (sub === 'multi') {
|
|
11105
|
-
const task = args.filter(x => !x.startsWith('-')).join(' ').trim() || arg('--task', null);
|
|
11106
|
-
if (!task) { fail('multi "<task>" 또는 --task 필요'); return process.exit(1); }
|
|
11107
|
-
const onlyArg = arg('--only', null); // 'claude,codex' 처럼 콤마 구분 — 활성 중에서 추가 필터
|
|
11108
|
-
const writeMode = has('--write');
|
|
11109
|
-
const execute = has('--execute'); // 1.9.156: 명령 출력 → 실제 spawn + consensus 합의
|
|
11110
|
-
const checks = EXTERNAL_AGENTS.map(a => ({ def: a, status: _checkAgent(a) }));
|
|
11111
|
-
let ready = checks.filter(x => x.status.status === 'ready');
|
|
11112
|
-
if (onlyArg) {
|
|
11113
|
-
const wanted = new Set(onlyArg.split(/[,\s]+/).filter(Boolean));
|
|
11114
|
-
ready = ready.filter(x => wanted.has(x.def.id));
|
|
11115
|
-
}
|
|
11116
|
-
if (!ready.length) {
|
|
11117
|
-
fail('활성 (ready) 에이전트 없음 — `leerness agents list` 로 확인. 1.9.151 install 흐름에서 복수 선택 후 .env 활성화 필요.');
|
|
11118
|
-
return process.exit(1);
|
|
11119
|
-
}
|
|
11120
|
-
// 1.9.281 (UR-0034): 권한 등급 게이트 — enforce ON 시 shell-write 초과 차단 (기본 OFF, 동작 불변)
|
|
11121
|
-
if (execute) {
|
|
11122
|
-
const pol = _policyEnforce(root, 'agents multi --execute');
|
|
11123
|
-
if (!pol.allowed) { fail(pol.reason); return process.exit(1); }
|
|
11124
|
-
if (pol.advisory) warn(`정책 advisory: 'agents multi --execute' 요구 등급 ${pol.required} > 허용 ${pol.allowedTier} (enforce OFF — 진행). leerness policy 로 등급 확인`);
|
|
11125
|
-
}
|
|
11126
|
-
// 1.9.156: --execute 모드 — 실제 spawn + 결과 수집 + multi-signal consensus
|
|
11127
|
-
if (execute) {
|
|
11128
|
-
return (async () => {
|
|
11129
|
-
const timeout = parseInt(arg('--timeout', '60'), 10) * 1000;
|
|
11130
|
-
if (!has('--json')) {
|
|
11131
|
-
log(`# leerness agents multi --execute (1.9.156) — ${ready.length}개 활성 에이전트 병렬 호출`);
|
|
11132
|
-
log(`task: ${task.slice(0, 120)}${task.length > 120 ? '…' : ''}`);
|
|
11133
|
-
log(`mode: ${writeMode ? '✏ write' : '🔒 read-only'} · timeout=${timeout / 1000}s`);
|
|
11134
|
-
log(`대상: ${ready.map(x => x.def.id).join(', ')}`);
|
|
11135
|
-
log('');
|
|
11136
|
-
log('## 병렬 호출 중...');
|
|
11137
|
-
}
|
|
11138
|
-
const t0 = Date.now();
|
|
11139
|
-
// 병렬 _cliChat 호출 (sandbox 자동: runCommandSafe + env scrub + observability)
|
|
11140
|
-
const results = await Promise.all(ready.map(async ({ def }) => {
|
|
11141
|
-
const start = Date.now();
|
|
11142
|
-
const r = await _cliChat(root, def.id, task, { timeout });
|
|
11143
|
-
return {
|
|
11144
|
-
agent: def.id,
|
|
11145
|
-
elapsed: Date.now() - start,
|
|
11146
|
-
ok: r.ok,
|
|
11147
|
-
response: r.response || '',
|
|
11148
|
-
error: r.error || null,
|
|
11149
|
-
responseTokens: Math.ceil((r.response || '').length / 4) // 대략 token 추정
|
|
11150
|
-
};
|
|
11151
|
-
}));
|
|
11152
|
-
const totalElapsed = Date.now() - t0;
|
|
11153
|
-
const ok = results.filter(r => r.ok);
|
|
11154
|
-
const failures = results.filter(r => !r.ok);
|
|
11155
|
-
_recordRun(root, { kind: 'agents_multi_execute', count: ready.length, success: ok.length, durationMs: totalElapsed, task: task.slice(0, 200) });
|
|
11156
|
-
// 1.9.155 consensus 로직 재사용 — multi-signal scoring (tokens + overlap + lengthFit)
|
|
11157
|
-
let best = null, scored = [];
|
|
11158
|
-
if (ok.length) {
|
|
11159
|
-
const tokenizer = (s) => new Set(String(s || '').toLowerCase().match(/[\w가-힣]{3,}/g) || []);
|
|
11160
|
-
const wordsOf = ok.map(o => tokenizer(o.response));
|
|
11161
|
-
const maxTokens = Math.max(...ok.map(o => o.responseTokens), 1);
|
|
11162
|
-
const avgLen = ok.reduce((s, o) => s + o.response.length, 0) / ok.length;
|
|
11163
|
-
const stdLen = Math.sqrt(ok.reduce((s, o) => s + (o.response.length - avgLen) ** 2, 0) / ok.length) || 1;
|
|
11164
|
-
scored = ok.map((o, i) => {
|
|
11165
|
-
const tokensNorm = o.responseTokens / maxTokens;
|
|
11166
|
-
const myWords = wordsOf[i];
|
|
11167
|
-
let overlapSum = 0;
|
|
11168
|
-
for (let j = 0; j < wordsOf.length; j++) {
|
|
11169
|
-
if (i === j) continue;
|
|
11170
|
-
let inter = 0;
|
|
11171
|
-
for (const w of myWords) if (wordsOf[j].has(w)) inter++;
|
|
11172
|
-
overlapSum += inter / Math.max(myWords.size, 1);
|
|
11173
|
-
}
|
|
11174
|
-
const overlap = (ok.length > 1) ? overlapSum / (ok.length - 1) : 0;
|
|
11175
|
-
const z = Math.abs((o.response.length - avgLen) / stdLen);
|
|
11176
|
-
const lengthFit = z <= 1.5 ? (1 - z / 1.5) : 0;
|
|
11177
|
-
const score = 0.4 * tokensNorm + 0.4 * overlap + 0.2 * lengthFit;
|
|
11178
|
-
return { ...o, score, tokensNorm, overlap, lengthFit };
|
|
11179
|
-
}).sort((a, b) => b.score - a.score);
|
|
11180
|
-
best = scored[0];
|
|
11181
|
-
}
|
|
11182
|
-
if (has('--json')) {
|
|
11183
|
-
log(JSON.stringify({
|
|
11184
|
-
task, count: ready.length, success: ok.length, totalElapsedMs: totalElapsed,
|
|
11185
|
-
results: scored.length ? scored : results,
|
|
11186
|
-
best: best ? { agent: best.agent, score: best.score, response: best.response } : null,
|
|
11187
|
-
failures
|
|
11188
|
-
}, null, 2));
|
|
11189
|
-
return;
|
|
11190
|
-
}
|
|
11191
|
-
log(`\n## 결과: ${ok.length}/${ready.length} 성공 · 총 ${totalElapsed}ms (병렬)`);
|
|
11192
|
-
for (const r of results) {
|
|
11193
|
-
if (r.ok) log(` ✓ ${r.agent.padEnd(8)} · ${r.elapsed}ms · ${r.responseTokens} 토큰`);
|
|
11194
|
-
else log(` ✗ ${r.agent.padEnd(8)} · ${r.elapsed}ms · ${(r.error || '').slice(0, 60)}`);
|
|
11195
|
-
}
|
|
11196
|
-
if (best) {
|
|
11197
|
-
log('');
|
|
11198
|
-
log(`## 🏆 합의 선택 (multi-signal consensus, 1.9.155)`);
|
|
11199
|
-
log(` best: ${best.agent} · score=${best.score.toFixed(3)} (tokens=${best.tokensNorm.toFixed(2)} · overlap=${best.overlap.toFixed(2)} · lengthFit=${best.lengthFit.toFixed(2)})`);
|
|
11200
|
-
if (scored.length > 1) {
|
|
11201
|
-
log(` others: ${scored.slice(1, 4).map(s => `${s.agent}=${s.score.toFixed(2)}`).join(', ')}`);
|
|
11202
|
-
}
|
|
11203
|
-
log(` --- 처음 600자 ---`);
|
|
11204
|
-
log(best.response.slice(0, 600));
|
|
11205
|
-
// task-log 기록
|
|
11206
|
-
try {
|
|
11207
|
-
const tlp = taskLogPath(root);
|
|
11208
|
-
const block = `\n## ${today()} agents multi --execute (1.9.156)\n- task: ${task.slice(0, 200)}\n- agents: ${ready.map(x => x.def.id).join(', ')}\n- success: ${ok.length}/${ready.length}\n- best: ${best.agent} (score=${best.score.toFixed(3)})\n`;
|
|
11209
|
-
append(tlp, block);
|
|
11210
|
-
} catch {}
|
|
11211
|
-
// 1.9.193: B축 (멀티 Sub-Agent 오케스트라) 보강 — consensus 결과를 lessons.md 에 자동 기록
|
|
11212
|
-
// 같은 task 재시도 시 과거 best agent + score 가 handoff lessons auto-recall 에서 매칭
|
|
11213
|
-
// 끄기: LEERNESS_NO_MULTIAGENT_LESSON=1
|
|
11214
|
-
if (process.env.LEERNESS_NO_MULTIAGENT_LESSON !== '1') {
|
|
11215
|
-
try {
|
|
11216
|
-
const lp = lessonsPath(root);
|
|
11217
|
-
const lessonBlock = `\n### ${today()} multi-agent consensus — best=${best.agent} (1.9.193)\n`
|
|
11218
|
-
+ `- task: ${task.slice(0, 200)}\n`
|
|
11219
|
-
+ `- agents: ${ready.map(x => x.def.id).join(', ')} (${ok.length}/${ready.length} success)\n`
|
|
11220
|
-
+ `- best agent: ${best.agent}, score=${best.score.toFixed(3)}\n`
|
|
11221
|
-
+ (scored.length > 1 ? `- others: ${scored.slice(1, 4).map(s => `${s.agent}=${s.score.toFixed(2)}`).join(', ')}\n` : '')
|
|
11222
|
-
+ `- lesson: 같은 keyword 재발 시 ${best.agent} 우선 시도 (multi-signal consensus 입증)\n`;
|
|
11223
|
-
append(lp, lessonBlock);
|
|
11224
|
-
} catch {}
|
|
11225
|
-
}
|
|
11226
|
-
}
|
|
11227
|
-
if (failures.length && !best) {
|
|
11228
|
-
process.exitCode = 1;
|
|
11229
|
-
}
|
|
11230
|
-
})();
|
|
11231
|
-
}
|
|
11232
|
-
if (has('--json')) {
|
|
11233
|
-
log(JSON.stringify({
|
|
11234
|
-
task, count: ready.length,
|
|
11235
|
-
agents: ready.map(x => ({ id: x.def.id, version: x.status.version })),
|
|
11236
|
-
commands: ready.map(x => _dispatchCommand(x.def.id, task, writeMode)),
|
|
11237
|
-
// 1.9.266 (UR-0021 2단계): 각 에이전트 슬래시 명령 힌트 — sub-agent 가 알맞은 슬래시 사용
|
|
11238
|
-
slashCommands: ready.reduce((acc, x) => { const h = _agentSlashHint(root, x.def.id); if (h) acc[x.def.id] = { invoke: h.invoke, commands: h.commands.map(c => c.cmd) }; return acc; }, {})
|
|
11239
|
-
}, null, 2));
|
|
11240
|
-
return;
|
|
11241
|
-
}
|
|
11242
|
-
log(`# leerness agents multi (1.9.152) — ${ready.length}개 활성 에이전트 일괄 dispatch`);
|
|
11243
|
-
log(`task: ${task.slice(0, 120)}${task.length > 120 ? '…' : ''}`);
|
|
11244
|
-
log(`mode: ${writeMode ? '✏ write (파일 수정 가능)' : '🔒 read-only (분석 전용, 안전)'}`);
|
|
11245
|
-
log(`대상: ${ready.map(x => x.def.id).join(', ')}`);
|
|
11246
|
-
log('');
|
|
11247
|
-
log('## 각 에이전트 실행 명령 (사용자가 병렬 실행 또는 메인 에이전트가 spawn)');
|
|
11248
|
-
log('');
|
|
11249
|
-
for (const { def, status } of ready) {
|
|
11250
|
-
log(`### [${def.id}] (v${status.version || '?'})`);
|
|
11251
|
-
log('```sh');
|
|
11252
|
-
log(_dispatchCommand(def.id, task, writeMode));
|
|
11253
|
-
log('```');
|
|
11254
|
-
// 1.9.266 (UR-0021 2단계): 에이전트별 슬래시 명령 힌트
|
|
11255
|
-
try {
|
|
11256
|
-
const hint = _agentSlashHint(root, def.id);
|
|
11257
|
-
if (hint && hint.commands.length) log(` 🤖 슬래시: ${hint.commands.slice(0, 8).map(c => c.cmd).join(' ')}${hint.invoke === 'subcommand' ? ' (하위명령)' : ''}`);
|
|
11258
|
-
} catch {}
|
|
11259
|
-
log('');
|
|
11260
|
-
}
|
|
11261
|
-
log('## 정책 (1.9.152 / 1.9.156)');
|
|
11262
|
-
log(` - 기본 모드: 명령 문자열만 출력 (사용자/메인 에이전트가 명시적으로 실행)`);
|
|
11263
|
-
log(` - 1.9.156 신규: \`--execute\` 플래그 시 leerness가 직접 ${ready.length}개 sub-agent 병렬 spawn + multi-signal consensus 자동 합의`);
|
|
11264
|
-
log(` 예: leerness agents multi "<task>" --execute (또는 --execute --json)`);
|
|
11265
|
-
log(` - 활성 에이전트 변경: \`.env\`에서 LEERNESS_ENABLE_<CLI>=1/0 또는 \`leerness setup-agents\` 재실행`);
|
|
11266
|
-
log(` - quota 체크: \`leerness agents quota\``);
|
|
11267
|
-
return;
|
|
11268
|
-
}
|
|
11269
|
-
if (sub === 'dispatch') {
|
|
11270
|
-
const task = args.filter(x => !x.startsWith('-')).join(' ').trim() || arg('--task', null);
|
|
11271
|
-
let target = arg('--to', null);
|
|
11272
|
-
if (!task) { fail('dispatch "<task>" 또는 --task 필요'); return process.exit(1); }
|
|
11273
|
-
// 1.9.152: --multi 또는 --to=all 또는 --to 없음 + 활성 ≥2 → multi 모드로 routing
|
|
11274
|
-
if (has('--multi') || target === 'all' || target === '*') {
|
|
11275
|
-
return agentsCmd(root, 'multi', ...args);
|
|
11276
|
-
}
|
|
11277
|
-
// 1.9.270: --role <role> — 설정된 역할 → provider+model 라우팅 (--to 없을 때)
|
|
11278
|
-
const roleArg = arg('--role', null);
|
|
11279
|
-
let roleModel = arg('--model', null);
|
|
11280
|
-
let rolePersona = '';
|
|
11281
|
-
if (roleArg && !target) {
|
|
11282
|
-
const resolved = _resolveRole(root, roleArg);
|
|
11283
|
-
if (!resolved) { fail(`역할 미설정: ${_normalizeRole(roleArg)} — leerness roles set ${_normalizeRole(roleArg)} --provider <id> 또는 roles suggest --apply`); return process.exit(1); }
|
|
11284
|
-
target = resolved.provider;
|
|
11285
|
-
if (!roleModel) roleModel = resolved.model;
|
|
11286
|
-
rolePersona = resolved.persona || '';
|
|
11287
|
-
log(`🎭 역할 ${_normalizeRole(roleArg)} → ${target}${roleModel ? ' / ' + roleModel : ''}`);
|
|
11288
|
-
if (rolePersona) log(` persona: ${rolePersona}`);
|
|
11289
|
-
}
|
|
11290
|
-
if (!target) { fail('--to <agent_id> 또는 --role <role> 필요 (claude/codex/agy/grok/copilot) — 활성 전체 일괄은 `leerness agents multi`'); return process.exit(1); }
|
|
11291
|
-
const agentDef = EXTERNAL_AGENTS.find(a => a.id === target);
|
|
11292
|
-
if (!agentDef) { fail(`알 수 없는 agent: ${target}`); return process.exit(1); }
|
|
11293
|
-
// 1.9.36: 작업 유형 키워드 분석 → 최적 CLI 추천 (ready 체크 전에 출력 — 비활성이어도 추천)
|
|
11294
|
-
const recommendation = _recommendAgent(task);
|
|
11295
|
-
const recommended = recommendation.target;
|
|
11296
|
-
if (recommended && recommended !== target) {
|
|
11297
|
-
log(`💡 추천: 이 작업은 ${recommended}가 더 적합 (${recommendation.reason})`);
|
|
11298
|
-
}
|
|
11299
|
-
const status = _checkAgent(agentDef);
|
|
11300
|
-
if (status.status !== 'ready') {
|
|
11301
|
-
fail(`${target} 비활성 (${status.status}). 환경변수 ${agentDef.envFlag}=1 + CLI 설치 필요.`);
|
|
11302
|
-
return process.exit(1);
|
|
11303
|
-
}
|
|
11304
|
-
// 1.9.36: --write 시 파일 수정 가능 권장 플래그 자동 첨부, 미명시 시 read-only 안전 모드
|
|
11305
|
-
const writeMode = has('--write');
|
|
11306
|
-
const readOnly = has('--readonly') || !writeMode;
|
|
11307
|
-
// 실제 호출은 안 함 — 프롬프트만 생성 (사용자가 명시적으로 실행)
|
|
11308
|
-
log(`# leerness agents dispatch (1.9.36)`);
|
|
11309
|
-
log(`대상: ${target} (${agentDef.bin})`);
|
|
11310
|
-
log(`상태: 🟢 ready, 버전 ${status.version || '?'}`);
|
|
11311
|
-
log(`모드: ${writeMode ? '✏ write (파일 수정 가능)' : '🔒 read-only (분석 전용, 안전)'}`);
|
|
11312
|
-
log('');
|
|
11313
|
-
log(`## 실행 명령 (사용자가 복사해서 실행)`);
|
|
11314
|
-
if (roleModel) log(`# 🎭 모델: ${roleModel} (역할 기반 라우팅, 1.9.270)`);
|
|
11315
|
-
log('');
|
|
11316
|
-
// 1.9.270: _dispatchCommand 로 통일 (roleModel 주입) — 명령 빌더 단일화
|
|
11317
|
-
log(_dispatchCommand(target, task, writeMode, roleModel));
|
|
11318
|
-
if (target === 'claude' && writeMode) log(`# ⚠ --dangerously-skip-permissions: 도구 권한 자동 승인 (파일 수정 가능)`);
|
|
11319
|
-
if (target === 'codex') { log(`# ℹ codex는 PowerShell 경유 — POSIX /tmp 경로는 C:\\tmp\\로 해석됨`); if (writeMode) log(`# ⚠ --dangerously-bypass-approvals-and-sandbox: sandbox 우회`); }
|
|
11320
|
-
if (target === 'agy' && writeMode) log(`# ⚠ --yolo: 워크스페이스 파일 직접 수정 가능`);
|
|
11321
|
-
if (target === 'grok' && writeMode) log(`# ⚠ grok --yolo: 자동 승인 (배포판에 따라 플래그 상이 가능)`);
|
|
11322
|
-
// 1.9.266 (UR-0021 2단계): 대상 에이전트의 슬래시 명령 힌트 — sub-agent 작업 시 알맞은 슬래시 명령 참조
|
|
11323
|
-
try {
|
|
11324
|
-
const hint = _agentSlashHint(root, target);
|
|
11325
|
-
if (hint && hint.commands.length) {
|
|
11326
|
-
log('');
|
|
11327
|
-
log(`## 🤖 ${target} 슬래시 명령 (1.9.265, UR-0021)`);
|
|
11328
|
-
if (hint.invoke === 'subcommand') log(` ※ 슬래시가 아닌 하위명령: ${hint.commands.map(c => c.cmd).join(' / ')}`);
|
|
11329
|
-
else log(` 세션 내 사용 가능: ${hint.commands.slice(0, 10).map(c => c.cmd).join(' ')}`);
|
|
11330
|
-
log(` → 전체/기록: leerness slash-commands ${target} [--record]`);
|
|
11331
|
-
}
|
|
11332
|
-
} catch {}
|
|
11333
|
-
log('');
|
|
11334
|
-
log(`## 정책 (1.9.36)`);
|
|
11335
|
-
log(` - leerness는 외부 CLI를 자동 호출하지 않음 (사용자 명시적 실행)`);
|
|
11336
|
-
log(` - 메인 에이전트(Claude)가 위 명령을 보고 sub-agent로 spawn 가능`);
|
|
11337
|
-
log(` - quota 체크: \`leerness agents quota\` (1.9.31+)`);
|
|
11338
|
-
log(` - 동시 호출 시: \`leerness agents bench "<task>"\` (1.9.36)`);
|
|
11339
|
-
log('');
|
|
11340
|
-
log(`## 분배 시 안전 규칙 (1.9.35)`);
|
|
11341
|
-
log(` - sub-agent 프롬프트에 "당신만 수정할 파일 경로"를 명시 (파일 경로 격리)`);
|
|
11342
|
-
log(` - sub-agent에 "보고 시 \`stat <file>\` 또는 mtime 확인 결과 첨부" 요구 (자기 격리 검증)`);
|
|
11343
|
-
log(` - 사양 사전 정의 (예: TICK_SPEC.md) → \`leerness contract verify\`로 사후 검증`);
|
|
11344
|
-
log(` - 같은 파일 동시 쓰기는 last-writer-wins 위험 (1.9.34 검증)`);
|
|
11345
|
-
return;
|
|
11346
|
-
}
|
|
11347
|
-
|
|
11348
|
-
if (sub === 'bench') {
|
|
11349
|
-
// 1.9.36: 같은 prompt를 ready CLI 모두에 동시 호출 + 시간/응답 길이/exit code 비교
|
|
11350
|
-
const task = args.filter(x => !x.startsWith('-')).join(' ').trim() || arg('--task', null);
|
|
11351
|
-
if (!task) { fail('bench "<task>" 필요'); return process.exit(1); }
|
|
11352
|
-
const timeoutS = parseInt(arg('--timeout', '60'), 10);
|
|
11353
|
-
const writeMode = has('--write');
|
|
11354
|
-
const ready = EXTERNAL_AGENTS.map(a => ({ agent: a, status: _checkAgent(a) }))
|
|
11355
|
-
.filter(x => x.status.status === 'ready');
|
|
11356
|
-
if (!ready.length) {
|
|
11357
|
-
fail('ready CLI 없음 — leerness setup-agents 또는 .env에 LEERNESS_ENABLE_X=1 설정 필요');
|
|
11358
|
-
return process.exit(1);
|
|
11359
|
-
}
|
|
11360
|
-
log(`# leerness agents bench (1.9.36)`);
|
|
11361
|
-
log(`task: ${task.slice(0, 80)}${task.length > 80 ? '…' : ''}`);
|
|
11362
|
-
log(`참여 CLI: ${ready.map(r => r.agent.id).join(', ')} (${ready.length}개)`);
|
|
11363
|
-
log(`타임아웃: ${timeoutS}s/CLI · 모드: ${writeMode ? 'write' : 'read-only'}`);
|
|
11364
|
-
log('');
|
|
11365
|
-
log('병렬 호출 중... (병렬 fork 후 wait)');
|
|
11366
|
-
log('');
|
|
11367
|
-
const results = [];
|
|
11368
|
-
const promises = ready.map(({ agent, status }) => new Promise((resolve) => {
|
|
11369
|
-
const t0 = Date.now();
|
|
11370
|
-
let cmd, cmdArgs;
|
|
11371
|
-
// 1.9.352 (UR-0066 외부리뷰): shell:true 경로에 raw task 전달 시 셸 메타문자(& | $() 백틱) 주입 위험 → _shellQuoteArg 로 단일 토큰화 (안전 경로 _cliChat 와 일관)
|
|
11372
|
-
const qTask = _shellQuoteArg(task);
|
|
11373
|
-
if (agent.id === 'claude') {
|
|
11374
|
-
cmdArgs = writeMode ? ['--print', '--dangerously-skip-permissions', qTask] : ['--print', qTask];
|
|
11375
|
-
cmd = 'claude';
|
|
11376
|
-
} else if (agent.id === 'codex') {
|
|
11377
|
-
cmdArgs = writeMode
|
|
11378
|
-
? ['exec', '--skip-git-repo-check', '--dangerously-bypass-approvals-and-sandbox', qTask]
|
|
11379
|
-
: ['exec', '--skip-git-repo-check', qTask];
|
|
11380
|
-
cmd = 'codex';
|
|
11381
|
-
} else if (agent.id === 'agy') {
|
|
11382
|
-
cmdArgs = writeMode ? ['-p', qTask, '--yolo'] : ['-p', qTask];
|
|
11383
|
-
cmd = 'agy';
|
|
11384
|
-
} else if (agent.id === 'copilot') {
|
|
11385
|
-
cmdArgs = ['copilot', 'suggest', qTask];
|
|
11386
|
-
cmd = 'gh';
|
|
11387
|
-
}
|
|
11388
|
-
const r = cp.spawn(cmd, cmdArgs, { shell: true });
|
|
11389
|
-
let stdout = '', stderr = '';
|
|
11390
|
-
r.stdout.on('data', d => { stdout += d; });
|
|
11391
|
-
r.stderr.on('data', d => { stderr += d; });
|
|
11392
|
-
const timer = setTimeout(() => { r.kill(); }, timeoutS * 1000);
|
|
11393
|
-
r.on('close', (code) => {
|
|
11394
|
-
clearTimeout(timer);
|
|
11395
|
-
const elapsed = Date.now() - t0;
|
|
11396
|
-
results.push({
|
|
11397
|
-
id: agent.id, exit: code, elapsed,
|
|
11398
|
-
stdout: stdout.trim().split('\n').slice(-3).join('\n'),
|
|
11399
|
-
stderrLen: stderr.length,
|
|
11400
|
-
ok: code === 0 && stdout.trim().length > 0
|
|
11401
|
-
});
|
|
11402
|
-
resolve();
|
|
11403
|
-
});
|
|
11404
|
-
r.on('error', (err) => {
|
|
11405
|
-
clearTimeout(timer);
|
|
11406
|
-
results.push({ id: agent.id, exit: -1, elapsed: Date.now() - t0, stdout: '', stderrLen: 0, error: err.message, ok: false });
|
|
11407
|
-
resolve();
|
|
11408
|
-
});
|
|
11409
|
-
}));
|
|
11410
|
-
return Promise.all(promises).then(() => {
|
|
11411
|
-
if (has('--json')) { log(JSON.stringify({ task, results }, null, 2)); return; }
|
|
11412
|
-
log(`| CLI | 시간 | exit | 응답 길이 | 마지막 라인 |`);
|
|
11413
|
-
log(`|---|---:|---:|---:|---|`);
|
|
11414
|
-
// sort by elapsed
|
|
11415
|
-
results.sort((a, b) => a.elapsed - b.elapsed);
|
|
11416
|
-
for (const r of results) {
|
|
11417
|
-
const respLen = (r.stdout || '').length;
|
|
11418
|
-
const last = (r.stdout || '').split('\n').pop().slice(0, 50);
|
|
11419
|
-
log(`| ${r.id} | ${r.elapsed}ms | ${r.exit} | ${respLen} | ${last.replace(/\|/g, '\\|')} |`);
|
|
11420
|
-
}
|
|
11421
|
-
log('');
|
|
11422
|
-
const okCount = results.filter(r => r.ok).length;
|
|
11423
|
-
log(`결과: ${okCount}/${results.length} 성공`);
|
|
11424
|
-
const fastest = results.filter(r => r.ok).sort((a, b) => a.elapsed - b.elapsed)[0];
|
|
11425
|
-
if (fastest) log(`🏆 가장 빠름: ${fastest.id} (${fastest.elapsed}ms)`);
|
|
11426
|
-
});
|
|
11427
|
-
}
|
|
11428
|
-
|
|
11429
|
-
if (sub === 'quota') {
|
|
11430
|
-
// 1.9.31: 각 CLI 사용량/쿼터 추정 + provider 대시보드 링크
|
|
11431
|
-
const results = [];
|
|
11432
|
-
for (const agent of EXTERNAL_AGENTS) {
|
|
11433
|
-
const base = _checkAgent(agent);
|
|
11434
|
-
const out = { id: agent.id, bin: agent.bin, status: base.status, quota: null, hint: null, raw: null };
|
|
11435
|
-
if (base.status !== 'ready') {
|
|
11436
|
-
out.hint = base.status === 'not-installed' ? `${agent.bin} CLI 미설치` : base.status === 'disabled' ? `${agent.envFlag}=1 필요` : '알 수 없음';
|
|
11437
|
-
results.push(out); continue;
|
|
11438
|
-
}
|
|
11439
|
-
// CLI별 quota 탐지 시도
|
|
11440
|
-
try {
|
|
11441
|
-
if (agent.id === 'claude') {
|
|
11442
|
-
// claude는 /status 슬래시 (대화형)만 지원. 비대화형 추정 불가.
|
|
11443
|
-
out.quota = 'unknown';
|
|
11444
|
-
out.hint = '대화 내 `/status` 슬래시 또는 https://console.anthropic.com/settings/usage 확인';
|
|
11445
|
-
} else if (agent.id === 'codex') {
|
|
11446
|
-
// codex CLI: codex --help에 usage 명령 있는지 확인
|
|
11447
|
-
const r = cp.spawnSync(agent.bin, ['--help'], { encoding: 'utf8', timeout: 4000, shell: true });
|
|
11448
|
-
const help = (r.stdout || r.stderr || '').toLowerCase();
|
|
11449
|
-
if (help.includes('usage') || help.includes('quota')) {
|
|
11450
|
-
out.quota = 'cli-supported';
|
|
11451
|
-
out.hint = '`codex usage` 또는 `codex quota` 시도 가능';
|
|
11452
|
-
} else {
|
|
11453
|
-
out.quota = 'unknown';
|
|
11454
|
-
out.hint = 'https://platform.openai.com/account/usage 확인';
|
|
11455
|
-
}
|
|
11456
|
-
out.raw = help.slice(0, 200);
|
|
11457
|
-
} else if (agent.id === 'agy') {
|
|
11458
|
-
// agy CLI (Antigravity): 무료 티어는 분당 60req 제한, CLI 자체에선 노출 안 됨
|
|
11459
|
-
out.quota = 'rate-limited';
|
|
11460
|
-
out.hint = '무료 티어: 60 req/min, 1000 req/day · Antigravity 유료 플랜은 https://antigravity.google.com';
|
|
11461
|
-
} else if (agent.id === 'copilot') {
|
|
11462
|
-
// gh copilot은 GitHub Copilot 구독 (월 단위 quota 없음, individual/business 플랜)
|
|
11463
|
-
const r = cp.spawnSync('gh', ['auth', 'status'], { encoding: 'utf8', timeout: 4000, shell: true });
|
|
11464
|
-
const authed = r.status === 0;
|
|
11465
|
-
out.quota = authed ? 'subscription' : 'not-authed';
|
|
11466
|
-
out.hint = authed ? 'Copilot 구독자 무제한 (월 플랜) · https://github.com/settings/copilot' : '`gh auth login` 필요';
|
|
11467
|
-
}
|
|
11468
|
-
} catch (e) {
|
|
11469
|
-
out.quota = 'error';
|
|
11470
|
-
out.hint = e.message;
|
|
11471
|
-
}
|
|
11472
|
-
results.push(out);
|
|
11473
|
-
}
|
|
11474
|
-
if (has('--json')) { log(JSON.stringify({ quota: results }, null, 2)); return; }
|
|
11475
|
-
log(`# 외부 AI CLI quota 추정 (1.9.31)`);
|
|
11476
|
-
log('');
|
|
11477
|
-
log(`| Agent | 상태 | quota | 안내 |`);
|
|
11478
|
-
log(`|---|---|---|---|`);
|
|
11479
|
-
for (const q of results) {
|
|
11480
|
-
const statusEmoji = q.status === 'ready' ? '🟢' : q.status === 'not-installed' ? '⚪' : q.status === 'disabled' ? '🟡' : '❓';
|
|
11481
|
-
log(`| ${q.id} | ${statusEmoji} ${q.status} | ${q.quota || '-'} | ${q.hint || '-'} |`);
|
|
11482
|
-
}
|
|
11483
|
-
log('');
|
|
11484
|
-
log(`## 주의`);
|
|
11485
|
-
log(` - leerness는 CLI 사용량을 직접 추적하지 않음 (provider 대시보드 참조)`);
|
|
11486
|
-
log(` - rate-limit/quota는 plan/티어에 따라 달라짐`);
|
|
11487
|
-
log(` - sub-agent 분배 시 quota 여유 큰 CLI 우선 활용 권장`);
|
|
11488
|
-
return;
|
|
11489
|
-
}
|
|
11490
|
-
|
|
11491
|
-
fail('사용법: leerness agents list|check|quota|dispatch|bench [--write] "<task>" [--to <id>]');
|
|
11492
|
-
return process.exit(1);
|
|
11493
|
-
}
|
|
11082
|
+
const _agents = require('../lib/agents');
|
|
11083
|
+
// 1.9.424 (UR-0025/UR-0125 큰 핸들러 모듈화 9번째): agentsCmd → lib/agents.js (DI 위임, rest→array)
|
|
11084
|
+
function agentsCmd(root, sub, ...args) { return _agents.agentsCmd(root, sub, args, { VERSION, has, arg, _agentSlashHint, _allProviders, _checkAgent, _cliChat, _dispatchCommand, _loadEnvFile, _normalizeRole, _policyEnforce, _readUserProviders, _recommendAgent, _recordRun, _resolveRole, _shellQuoteArg, lessonsPath, taskLogPath }); }
|
|
11494
11085
|
|
|
11495
11086
|
function personaCmd(root, sub, idOrName, ...rest) {
|
|
11496
11087
|
root = absRoot(root || process.cwd());
|
|
@@ -18815,340 +18406,9 @@ async function deployAutoCmd(root, service) {
|
|
|
18815
18406
|
}
|
|
18816
18407
|
|
|
18817
18408
|
// 1.9.85: leerness health — 종합 헬스 체크 (drift + 보안 + skill + MCP + 누적)
|
|
18818
|
-
|
|
18819
|
-
|
|
18820
|
-
|
|
18821
|
-
// 1) drift level
|
|
18822
|
-
try {
|
|
18823
|
-
const r = cp.spawnSync(process.execPath, [__filename, 'drift', 'check', root, '--json'],
|
|
18824
|
-
{ encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '0' } });
|
|
18825
|
-
const j = JSON.parse(r.stdout.trim());
|
|
18826
|
-
out.checks.drift = { level: j.level, score: j.score, firedCount: (j.fired || []).length };
|
|
18827
|
-
} catch { out.checks.drift = { error: 'drift check 실패' }; }
|
|
18828
|
-
// 2) 보안 상태 (1.9.418, 9th 외부평가 Codex P2): .env/.gitignore + **실제 하드코딩 시크릿 스캔**.
|
|
18829
|
-
// 기존엔 .env 가 .gitignore 에 있으면 critical:false 라 커밋된 하드코딩 시크릿이 있어도 health 가 healthy:true 였음(false-OK).
|
|
18830
|
-
// handoff/scan secrets 와 동일하게 _collectSecretFindings 로 커밋 대상 시크릿을 반영(정직성).
|
|
18831
|
-
try {
|
|
18832
|
-
const sec = _collectSecretFindings(root);
|
|
18833
|
-
const committedSecrets = sec.committed.length;
|
|
18834
|
-
const envPath = path.join(root, '.env');
|
|
18835
|
-
const hasDotEnv = exists(envPath);
|
|
18836
|
-
const s = { hasDotEnv, committedSecrets };
|
|
18837
|
-
if (hasDotEnv) {
|
|
18838
|
-
const d = envDiff(root);
|
|
18839
|
-
const giText = exists(path.join(root, '.gitignore')) ? read(path.join(root, '.gitignore')) : '';
|
|
18840
|
-
const giLines = giText.split('\n').map(l => l.trim());
|
|
18841
|
-
const envInGi = giLines.includes('.env') || giLines.includes('/.env');
|
|
18842
|
-
const SECRET_PATTERNS = ['.env', '.env.local', '.env.production', '.env.*.local', '*.pem', 'credentials.json'];
|
|
18843
|
-
s.envInGitignore = envInGi;
|
|
18844
|
-
s.envExampleMissing = d.inEnvOnly;
|
|
18845
|
-
s.gitignoreMissingSecrets = SECRET_PATTERNS.filter(p => !giLines.some(l => l === p || l === '/' + p));
|
|
18846
|
-
s.critical = !envInGi || committedSecrets > 0;
|
|
18847
|
-
} else {
|
|
18848
|
-
s.critical = committedSecrets > 0;
|
|
18849
|
-
}
|
|
18850
|
-
out.checks.security = s;
|
|
18851
|
-
} catch { out.checks.security = { error: '보안 점검 실패' }; }
|
|
18852
|
-
// 3) skill 수 + skill query 누적
|
|
18853
|
-
try {
|
|
18854
|
-
const all = listAllSkills(root);
|
|
18855
|
-
const skillCount = Object.keys(all).length;
|
|
18856
|
-
let queryCount = 0;
|
|
18857
|
-
const histPath = path.join(root, '.harness', 'skill-suggestions.md');
|
|
18858
|
-
if (exists(histPath)) {
|
|
18859
|
-
queryCount = (read(histPath).match(/^## [\d-]+ [\d:]+ — query/gm) || []).length;
|
|
18860
|
-
}
|
|
18861
|
-
out.checks.skills = { installed: skillCount, queryHistoryCount: queryCount };
|
|
18862
|
-
} catch { out.checks.skills = { error: 'skill 점검 실패' }; }
|
|
18863
|
-
// 4) MCP + 명령 호출 누적
|
|
18864
|
-
try {
|
|
18865
|
-
const stats = _readUsageStats(root);
|
|
18866
|
-
const cmdTotal = Object.values(stats.commands || {}).reduce((s, n) => s + n, 0);
|
|
18867
|
-
const mcpTotal = stats.mcp?.tools ? Object.values(stats.mcp.tools).reduce((s, n) => s + n, 0) : 0;
|
|
18868
|
-
out.checks.usage = {
|
|
18869
|
-
commandTotal: cmdTotal,
|
|
18870
|
-
commandKinds: Object.keys(stats.commands || {}).length,
|
|
18871
|
-
mcpTotal,
|
|
18872
|
-
mcpToolKinds: stats.mcp?.tools ? Object.keys(stats.mcp.tools).length : 0,
|
|
18873
|
-
since: stats.since || null
|
|
18874
|
-
};
|
|
18875
|
-
} catch { out.checks.usage = { error: 'usage 점검 실패' }; }
|
|
18876
|
-
// 5) tasks (progress-tracker)
|
|
18877
|
-
try {
|
|
18878
|
-
const rows = readProgressRows(root);
|
|
18879
|
-
const byStatus = {};
|
|
18880
|
-
for (const r of rows) byStatus[r.status] = (byStatus[r.status] || 0) + 1;
|
|
18881
|
-
out.checks.tasks = { total: rows.length, byStatus };
|
|
18882
|
-
} catch { out.checks.tasks = { error: 'tasks 점검 실패' }; }
|
|
18883
|
-
// 1.9.123: memorySurface 통합 (handoff --json 1.9.115 / session close --json 1.9.122 와 동일 패턴)
|
|
18884
|
-
try {
|
|
18885
|
-
const rows = readProgressRows(root);
|
|
18886
|
-
const tasksByStatus = {};
|
|
18887
|
-
for (const s of STATUSES) tasksByStatus[s] = 0;
|
|
18888
|
-
for (const r of rows) tasksByStatus[r.status] = (tasksByStatus[r.status] || 0) + 1;
|
|
18889
|
-
const tasksInProgress = tasksByStatus['in-progress'] || 0;
|
|
18890
|
-
const decisionsCount = _loadDecisions(root).length; // 1.9.339 (UR-0053): canonical 단일 진실소스
|
|
18891
|
-
const rules = readRules(root);
|
|
18892
|
-
const rulesActive = rules.filter(r => r.status === 'active').length;
|
|
18893
|
-
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
18894
|
-
const milestones = (planText.match(/^### M-\d{4}\./gm) || []).length;
|
|
18895
|
-
const lessonsCount = _loadLessons(root).length;
|
|
18896
|
-
out.memorySurface = {
|
|
18897
|
-
tasks: { inProgress: tasksInProgress, total: rows.length, byStatus: tasksByStatus },
|
|
18898
|
-
decisions: { count: decisionsCount },
|
|
18899
|
-
rules: { active: rulesActive, total: rules.length },
|
|
18900
|
-
plan: { milestones },
|
|
18901
|
-
lessons: { count: lessonsCount },
|
|
18902
|
-
archive: (() => {
|
|
18903
|
-
// 1.9.130: archive 카운트 통합
|
|
18904
|
-
const a = { decisions: 0, lessons: 0, plan: 0, total: 0 };
|
|
18905
|
-
try {
|
|
18906
|
-
const hdHe = path.join(root, '.harness');
|
|
18907
|
-
for (const [key, file] of [['decisions', 'decisions.archive.md'], ['lessons', 'lessons.archive.md'], ['plan', 'plan.archive.md']]) {
|
|
18908
|
-
const fpHe = path.join(hdHe, file);
|
|
18909
|
-
if (exists(fpHe)) {
|
|
18910
|
-
const entries = _parseArchiveBlocks(read(fpHe));
|
|
18911
|
-
a[key] = entries.length;
|
|
18912
|
-
a.total += entries.length;
|
|
18913
|
-
}
|
|
18914
|
-
}
|
|
18915
|
-
} catch {}
|
|
18916
|
-
return a;
|
|
18917
|
-
})(),
|
|
18918
|
-
summary: `T${tasksInProgress}/D${decisionsCount}/R${rulesActive}/P${milestones}/L${lessonsCount}`,
|
|
18919
|
-
};
|
|
18920
|
-
} catch { out.memorySurface = { error: 'memorySurface 점검 실패' }; }
|
|
18921
|
-
// 1.9.143: health --json featureGraph 통합 (handoff/session close 와 동일 패턴 — JSON 4종 완성)
|
|
18922
|
-
try {
|
|
18923
|
-
const { nodes: fNodesHe } = _readFeatureGraph(root);
|
|
18924
|
-
const edgeCount = fNodesHe.reduce((s, n) => s + (n.dependsOn?.length || 0) + (n.affects?.length || 0) + (n.coChangesWith?.length || 0), 0);
|
|
18925
|
-
const linkedSet = new Set();
|
|
18926
|
-
for (const n of fNodesHe) {
|
|
18927
|
-
for (const x of [...(n.dependsOn||[]), ...(n.affects||[]), ...(n.coChangesWith||[])]) { linkedSet.add(n.id); linkedSet.add(x); }
|
|
18928
|
-
}
|
|
18929
|
-
const isolated = fNodesHe.length ? (fNodesHe.length - linkedSet.size) : 0;
|
|
18930
|
-
out.featureGraph = {
|
|
18931
|
-
total: fNodesHe.length,
|
|
18932
|
-
edges: edgeCount,
|
|
18933
|
-
isolated: Math.max(0, isolated),
|
|
18934
|
-
summary: `F${fNodesHe.length}/E${edgeCount}${isolated > 0 ? `/iso${isolated}` : ''}`
|
|
18935
|
-
};
|
|
18936
|
-
} catch { out.featureGraph = { error: 'featureGraph 점검 실패' }; }
|
|
18937
|
-
// 1.9.228: health --json roundHistory 통합 (handoff/session close 와 동일 — JSON 3 명령 일관성 + 6 통합 필드 완성)
|
|
18938
|
-
try {
|
|
18939
|
-
const rh = _computeRoundHistory(root);
|
|
18940
|
-
out.roundHistory = {
|
|
18941
|
-
roundCount: rh.roundCount,
|
|
18942
|
-
baselineVersion: rh.baselineVersion,
|
|
18943
|
-
nextMilestone: rh.nextMilestone,
|
|
18944
|
-
roundsToNextMilestone: rh.roundsToNextMilestone,
|
|
18945
|
-
daysActive: rh.daysActive,
|
|
18946
|
-
avgRoundsPerDay: rh.avgRoundsPerDay
|
|
18947
|
-
};
|
|
18948
|
-
} catch { out.roundHistory = { error: 'roundHistory 점검 실패' }; }
|
|
18949
|
-
// 1.9.230: health --json milestones 통합 (handoff/session close/health 3 명령 일관성 유지)
|
|
18950
|
-
try {
|
|
18951
|
-
const ms = _computeMilestones(root);
|
|
18952
|
-
out.milestones = {
|
|
18953
|
-
reachedCount: ms.reached.length,
|
|
18954
|
-
reached: ms.reached.map(m => ({ milestone: m.milestone, version: m.version, reachedAt: m.reachedAt })),
|
|
18955
|
-
next: ms.next,
|
|
18956
|
-
avgRoundsPerDay: ms.avgRoundsPerDay
|
|
18957
|
-
};
|
|
18958
|
-
} catch { out.milestones = { error: 'milestones 점검 실패' }; }
|
|
18959
|
-
// 1.9.234: health --json recentChanges 통합 (3 명령 8 필드 일관성)
|
|
18960
|
-
try {
|
|
18961
|
-
out.recentChanges = _computeRecentChanges(root, 5);
|
|
18962
|
-
} catch { out.recentChanges = { error: 'recentChanges 점검 실패' }; }
|
|
18963
|
-
// 1.9.240: health --json pyFiles 통합 (3 명령 9 필드 — UR-0013 2단계)
|
|
18964
|
-
try {
|
|
18965
|
-
const pyFiles = _collectPyFiles(root, 200);
|
|
18966
|
-
const analyses = pyFiles.slice(0, 200).map(f => _analyzePyFile(f)).filter(Boolean);
|
|
18967
|
-
out.pyFiles = {
|
|
18968
|
-
total: pyFiles.length,
|
|
18969
|
-
analyzed: analyses.length,
|
|
18970
|
-
totalLOC: analyses.reduce((s, a) => s + a.loc, 0),
|
|
18971
|
-
totalImports: analyses.reduce((s, a) => s + a.imports, 0),
|
|
18972
|
-
totalFuncs: analyses.reduce((s, a) => s + a.funcs, 0),
|
|
18973
|
-
totalClasses: analyses.reduce((s, a) => s + a.classes, 0)
|
|
18974
|
-
};
|
|
18975
|
-
} catch { out.pyFiles = { error: 'pyFiles 점검 실패' }; }
|
|
18976
|
-
// 1.9.242: health --json envInfo 통합 (3 명령 10 필드 — UR-0014 2단계)
|
|
18977
|
-
try {
|
|
18978
|
-
const runtimeEnv = _collectRuntimeEnv();
|
|
18979
|
-
const encScan = _scanShellScriptsEncoding(root);
|
|
18980
|
-
out.envInfo = {
|
|
18981
|
-
os: runtimeEnv.os.platform,
|
|
18982
|
-
isKoreanWindows: runtimeEnv.locale.isKoreanWindows || false,
|
|
18983
|
-
codepage: runtimeEnv.locale.codepage || null,
|
|
18984
|
-
nodeVersion: runtimeEnv.node.version,
|
|
18985
|
-
shellScriptsScanned: encScan.scanned,
|
|
18986
|
-
encodingRiskCount: encScan.atRisk.length,
|
|
18987
|
-
encodingRiskFiles: encScan.atRisk.slice(0, 5).map(r => r.file),
|
|
18988
|
-
// 1.9.249 (UR-0018): 터미널 출력 인코딩 안전 여부 + 자동 회복 결과
|
|
18989
|
-
terminalEncodingOk: runtimeEnv.locale.codepage === 65001 || !runtimeEnv.locale.isKoreanWindows,
|
|
18990
|
-
autoChcpApplied: process.env._LEERNESS_AUTOCHCP_APPLIED || null,
|
|
18991
|
-
// 1.9.250 (UR-0018 2단계): POSIX (Linux/macOS/WSL) terminal encoding 점검
|
|
18992
|
-
posixEncodingOk: runtimeEnv.locale.posixEncodingOk,
|
|
18993
|
-
isWSL: runtimeEnv.locale.isWSL || false
|
|
18994
|
-
};
|
|
18995
|
-
} catch { out.envInfo = { error: 'envInfo 점검 실패' }; }
|
|
18996
|
-
// 1.9.245: health --json apiSkills 통합 (3 명령 11 필드 — UR-0015)
|
|
18997
|
-
try {
|
|
18998
|
-
const allSkills = _listAPISkills(root);
|
|
18999
|
-
let currentTaskText = '';
|
|
19000
|
-
try {
|
|
19001
|
-
const rows = readProgressRows(root);
|
|
19002
|
-
const ip = rows.find(r => r.status === 'in-progress');
|
|
19003
|
-
if (ip) currentTaskText = (ip.title || '') + ' ' + (ip.notes || '');
|
|
19004
|
-
} catch {}
|
|
19005
|
-
const matched = currentTaskText ? _matchAPISkills(root, currentTaskText) : [];
|
|
19006
|
-
out.apiSkills = {
|
|
19007
|
-
total: allSkills.length,
|
|
19008
|
-
matched: matched.length,
|
|
19009
|
-
matchedIds: matched.slice(0, 5).map(s => s.id),
|
|
19010
|
-
ids: allSkills.slice(0, 10).map(s => s.id)
|
|
19011
|
-
};
|
|
19012
|
-
} catch { out.apiSkills = { error: 'apiSkills 점검 실패' }; }
|
|
19013
|
-
// 1.9.264: shellGuard 통합 (health JSON 12번째 통합 필드 — handoff/session close 와 JSON 3 명령 일관성) — UR-0020
|
|
19014
|
-
try {
|
|
19015
|
-
const sf = _loadShellFailures(root);
|
|
19016
|
-
const drift = _shellEnvDrift(root);
|
|
19017
|
-
out.shellGuard = {
|
|
19018
|
-
failureCount: sf.failures.length,
|
|
19019
|
-
recent: sf.failures.slice(-3).map(f => ({ cmd: (f.cmd || '').slice(0, 50), exitCode: f.exitCode, shell: f.shell, rules: f.issues || [] })),
|
|
19020
|
-
envDriftChanges: drift && drift.changes ? drift.changes.length : 0,
|
|
19021
|
-
envDrift: drift ? drift.changes : null
|
|
19022
|
-
};
|
|
19023
|
-
} catch { out.shellGuard = { error: 'shellGuard 점검 실패' }; }
|
|
19024
|
-
// 1.9.163: 5능력 매트릭스 자동 평가 (1.9.155 sub-agent 점검 → 코드 기반 자동화)
|
|
19025
|
-
// 각 능력을 코드 grep 으로 검출 → 0~100 점수. 사용자가 매 health 호출 시 leerness 자기 평가 확인.
|
|
19026
|
-
try {
|
|
19027
|
-
const harnessSrc = read(__filename);
|
|
19028
|
-
const cap = {};
|
|
19029
|
-
// (1) 웹 자동화 — 1.9.165 playwright bridge 통합 + 실제 playwright 설치 detect
|
|
19030
|
-
const hasWebBridge = /function webCmd\(root, sub/.test(harnessSrc);
|
|
19031
|
-
// 사용자가 playwright 설치했는지 실시간 detect (require try)
|
|
19032
|
-
let playwrightInstalled = false;
|
|
19033
|
-
try { require('playwright'); playwrightInstalled = true; }
|
|
19034
|
-
catch { try { require('playwright-core'); playwrightInstalled = true; } catch {} }
|
|
19035
|
-
if (hasWebBridge && playwrightInstalled) {
|
|
19036
|
-
cap.webAutomation = { score: 90, status: '✓', evidence: 'playwright 설치 + leerness web bridge (1.9.165)' };
|
|
19037
|
-
} else if (hasWebBridge) {
|
|
19038
|
-
cap.webAutomation = { score: 50, status: '⚠', evidence: 'leerness web bridge 있음, playwright 미설치 (npm i -g playwright)' };
|
|
19039
|
-
} else {
|
|
19040
|
-
cap.webAutomation = { score: 5, status: '❌', evidence: 'permissions.browser=toggle만 (실 코드 미구현)' };
|
|
19041
|
-
}
|
|
19042
|
-
// (2) PC 조작 — 1.9.166 robotjs/nut-tree bridge + 실제 설치 detect
|
|
19043
|
-
const hasPCBridge = /function pcCmd\(root, sub/.test(harnessSrc);
|
|
19044
|
-
let pcInstalled = false;
|
|
19045
|
-
try { require('robotjs'); pcInstalled = true; }
|
|
19046
|
-
catch { try { require('@nut-tree/nut-js'); pcInstalled = true; } catch {} }
|
|
19047
|
-
if (hasPCBridge && pcInstalled) {
|
|
19048
|
-
cap.pcAutomation = { score: 90, status: '✓', evidence: 'robotjs/nut-tree 설치 + leerness pc bridge (1.9.166)' };
|
|
19049
|
-
} else if (hasPCBridge) {
|
|
19050
|
-
cap.pcAutomation = { score: 50, status: '⚠', evidence: 'leerness pc bridge 있음, robotjs 미설치 (npm i -g robotjs)' };
|
|
19051
|
-
} else {
|
|
19052
|
-
cap.pcAutomation = { score: 5, status: '❌', evidence: 'permissions.mouse/keyboard=필드만 (실 사용처 0)' };
|
|
19053
|
-
}
|
|
19054
|
-
// (3) 멀티 에이전트 오케스트레이션 — agents multi --execute + consensus 로직?
|
|
19055
|
-
const hasExecute = /const execute = has\('--execute'\)/.test(harnessSrc);
|
|
19056
|
-
const hasConsensus = /multi-signal consensus/.test(harnessSrc);
|
|
19057
|
-
cap.multiAgentOrchestration = (hasExecute && hasConsensus)
|
|
19058
|
-
? { score: 90, status: '✓', evidence: '실 spawn + multi-signal consensus (1.9.156+1.9.155)' }
|
|
19059
|
-
: { score: 50, status: '⚠', evidence: '명령 출력만 (1.9.152 기본 모드)' };
|
|
19060
|
-
// (4) REPL multi-provider — _agentRepl + _cliChat 5종?
|
|
19061
|
-
const hasRepl = /async function _agentRepl/.test(harnessSrc);
|
|
19062
|
-
const hasCliChat = /async function _cliChat/.test(harnessSrc);
|
|
19063
|
-
cap.replMultiProvider = (hasRepl && hasCliChat)
|
|
19064
|
-
? { score: 90, status: '✓', evidence: 'ollama/claude/codex/agy/copilot 5종 (1.9.149+1.9.153)' }
|
|
19065
|
-
: { score: 30, status: '⚠', evidence: 'REPL 미완성' };
|
|
19066
|
-
// (5) MCP 도구 — tools array 카운트 (1.9.288: 정확한 도구 정의 패턴 — 자기-매칭 오탐 제거, Codex #5)
|
|
19067
|
-
const toolCount = _mcpToolCount();
|
|
19068
|
-
cap.mcpTools = toolCount >= 50
|
|
19069
|
-
? { score: 100, status: '✓', evidence: `${toolCount}/50+ 도구 (1.9.159 CRUD 완성)` }
|
|
19070
|
-
: { score: Math.round((toolCount / 50) * 100), status: toolCount > 30 ? '✓' : '⚠', evidence: `${toolCount} 도구` };
|
|
19071
|
-
// (6) 코드 인텔리전스 — 1.9.167 LSP 어댑터 + typescript 설치 detect
|
|
19072
|
-
const hasLspBridge = /function lspCmd\(root, sub/.test(harnessSrc);
|
|
19073
|
-
let tsInstalled = false;
|
|
19074
|
-
try { require('typescript'); tsInstalled = true; } catch {}
|
|
19075
|
-
if (hasLspBridge && tsInstalled) {
|
|
19076
|
-
cap.codeIntel = { score: 90, status: '✓', evidence: 'typescript 설치 + leerness lsp bridge (1.9.167, Compiler API)' };
|
|
19077
|
-
} else if (hasLspBridge) {
|
|
19078
|
-
cap.codeIntel = { score: 50, status: '⚠', evidence: 'leerness lsp bridge 있음, typescript 미설치 (regex fallback 동작, npm i -g typescript)' };
|
|
19079
|
-
} else {
|
|
19080
|
-
cap.codeIntel = { score: 5, status: '❌', evidence: 'LSP 어댑터 미구현 (코드 인텔리전스 없음)' };
|
|
19081
|
-
}
|
|
19082
|
-
const avgScore = Math.round((cap.webAutomation.score + cap.pcAutomation.score + cap.multiAgentOrchestration.score + cap.replMultiProvider.score + cap.mcpTools.score + cap.codeIntel.score) / 6);
|
|
19083
|
-
out.capabilityMatrix = {
|
|
19084
|
-
capabilities: cap,
|
|
19085
|
-
overallScore: avgScore,
|
|
19086
|
-
summary: `웹${cap.webAutomation.score}/PC${cap.pcAutomation.score}/멀티${cap.multiAgentOrchestration.score}/REPL${cap.replMultiProvider.score}/MCP${cap.mcpTools.score}/LSP${cap.codeIntel.score} · 종합 ${avgScore}%`,
|
|
19087
|
-
assessment: avgScore >= 70 ? 'production-ready' : avgScore >= 50 ? 'beta-ready' : 'mvp'
|
|
19088
|
-
};
|
|
19089
|
-
} catch { out.capabilityMatrix = { error: '5능력 매트릭스 평가 실패' }; }
|
|
19090
|
-
// 6) issues 요약 (사용자 글로벌 룰 가시화)
|
|
19091
|
-
const issues = [];
|
|
19092
|
-
if (out.checks.drift?.level && !/healthy/.test(out.checks.drift.level)) issues.push(`drift ${out.checks.drift.level}`);
|
|
19093
|
-
if (out.checks.security?.committedSecrets > 0) issues.push(`🚨 커밋 대상 하드코딩 시크릿 ${out.checks.security.committedSecrets}건 (보안 CRITICAL)`); // 1.9.418 (9th 외부평가 Codex P2)
|
|
19094
|
-
if (out.checks.security?.hasDotEnv && out.checks.security?.envInGitignore === false) issues.push('🚨 .env가 .gitignore에 누락 (보안 CRITICAL)');
|
|
19095
|
-
if (out.checks.security?.envExampleMissing?.length) issues.push(`.env→.env.example 누락 ${out.checks.security.envExampleMissing.length}건`);
|
|
19096
|
-
if (out.checks.security?.gitignoreMissingSecrets?.length) issues.push(`.gitignore 시크릿 누락 ${out.checks.security.gitignoreMissingSecrets.length}건`);
|
|
19097
|
-
out.issues = issues;
|
|
19098
|
-
out.healthy = issues.length === 0;
|
|
19099
|
-
|
|
19100
|
-
// --strict: issue 있으면 exit 1
|
|
19101
|
-
if (has('--strict') && !out.healthy) process.exitCode = 1;
|
|
19102
|
-
|
|
19103
|
-
if (has('--json')) { log(JSON.stringify(out, null, 2)); return; }
|
|
19104
|
-
log(`# leerness health (1.9.85)`);
|
|
19105
|
-
log(`Date: ${out.generatedAt}`);
|
|
19106
|
-
log(`Status: ${out.healthy ? '✅ healthy' : `⚠ ${issues.length} issues`}`);
|
|
19107
|
-
log('');
|
|
19108
|
-
log(`## drift`);
|
|
19109
|
-
log(` level: ${out.checks.drift?.level || 'n/a'} (score ${out.checks.drift?.score || 0}, fired ${out.checks.drift?.firedCount || 0})`);
|
|
19110
|
-
log('');
|
|
19111
|
-
log(`## 보안`);
|
|
19112
|
-
if (out.checks.security?.hasDotEnv) {
|
|
19113
|
-
log(` .env 존재 · .gitignore에 .env 포함: ${out.checks.security.envInGitignore ? '✓' : '✗ CRITICAL'}`);
|
|
19114
|
-
log(` .env.example 누락 키: ${out.checks.security.envExampleMissing?.length || 0}건`);
|
|
19115
|
-
log(` .gitignore 시크릿 패턴 누락: ${out.checks.security.gitignoreMissingSecrets?.length || 0}건`);
|
|
19116
|
-
} else {
|
|
19117
|
-
log(` .env 없음 (검증 불필요)`);
|
|
19118
|
-
}
|
|
19119
|
-
log('');
|
|
19120
|
-
log(`## skills`);
|
|
19121
|
-
log(` 설치: ${out.checks.skills?.installed || 0}개 · skill query 누적: ${out.checks.skills?.queryHistoryCount || 0}회`);
|
|
19122
|
-
log('');
|
|
19123
|
-
log(`## usage`);
|
|
19124
|
-
log(` 명령 호출: ${out.checks.usage?.commandTotal || 0}회 / ${out.checks.usage?.commandKinds || 0}종`);
|
|
19125
|
-
log(` MCP 호출: ${out.checks.usage?.mcpTotal || 0}회 / ${out.checks.usage?.mcpToolKinds || 0}종 도구`);
|
|
19126
|
-
log(` since: ${out.checks.usage?.since || 'unknown'}`);
|
|
19127
|
-
log('');
|
|
19128
|
-
log(`## tasks`);
|
|
19129
|
-
const tb = out.checks.tasks?.byStatus || {};
|
|
19130
|
-
log(` 총 ${out.checks.tasks?.total || 0}건: ${Object.entries(tb).map(([s, n]) => `${s}=${n}`).join(', ') || '없음'}`);
|
|
19131
|
-
// 1.9.163: 5능력 매트릭스 — 1.9.155 sub-agent 점검의 코드 기반 자동 평가
|
|
19132
|
-
if (out.capabilityMatrix && !out.capabilityMatrix.error) {
|
|
19133
|
-
log('');
|
|
19134
|
-
log(`## 🧪 6능력 매트릭스 (1.9.167 자동 평가)`);
|
|
19135
|
-
const cm = out.capabilityMatrix;
|
|
19136
|
-
log(` 종합: ${cm.overallScore}% (${cm.assessment})`);
|
|
19137
|
-
log(` (1) 웹 자동화 ${cm.capabilities.webAutomation.status} ${cm.capabilities.webAutomation.score}% · ${cm.capabilities.webAutomation.evidence}`);
|
|
19138
|
-
log(` (2) PC 조작 ${cm.capabilities.pcAutomation.status} ${cm.capabilities.pcAutomation.score}% · ${cm.capabilities.pcAutomation.evidence}`);
|
|
19139
|
-
log(` (3) 멀티 오케스트레이션 ${cm.capabilities.multiAgentOrchestration.status} ${cm.capabilities.multiAgentOrchestration.score}% · ${cm.capabilities.multiAgentOrchestration.evidence}`);
|
|
19140
|
-
log(` (4) REPL multi-provider ${cm.capabilities.replMultiProvider.status} ${cm.capabilities.replMultiProvider.score}% · ${cm.capabilities.replMultiProvider.evidence}`);
|
|
19141
|
-
log(` (5) MCP 도구 ${cm.capabilities.mcpTools.status} ${cm.capabilities.mcpTools.score}% · ${cm.capabilities.mcpTools.evidence}`);
|
|
19142
|
-
log(` (6) 코드 인텔리전스 ${cm.capabilities.codeIntel.status} ${cm.capabilities.codeIntel.score}% · ${cm.capabilities.codeIntel.evidence}`);
|
|
19143
|
-
}
|
|
19144
|
-
if (issues.length) {
|
|
19145
|
-
log('');
|
|
19146
|
-
log(`## ⚠ Issues (${issues.length})`);
|
|
19147
|
-
for (const i of issues) log(` - ${i}`);
|
|
19148
|
-
log('');
|
|
19149
|
-
log(`💡 자동 회복: leerness drift check --auto-fix · leerness audit --fix`);
|
|
19150
|
-
}
|
|
19151
|
-
}
|
|
18409
|
+
const _health = require('../lib/health');
|
|
18410
|
+
// 1.9.423 (UR-0025/UR-0125 큰 핸들러 모듈화 8번째): healthCmd → lib/health.js (DI 위임, thin wrapper)
|
|
18411
|
+
function healthCmd(root) { return _health.healthCmd(root, { VERSION, has, arg, harnessPath: __filename, listAllSkills, planPath, readProgressRows, readRules, envDiff, _collectSecretFindings, _readUsageStats, _loadDecisions, _loadLessons, _loadShellFailures, _readFeatureGraph, _scanShellScriptsEncoding, _shellEnvDrift, _computeMilestones, _computeRecentChanges, _computeRoundHistory, _collectPyFiles, _analyzePyFile, _collectRuntimeEnv, _listAPISkills, _matchAPISkills, _mcpToolCount }); }
|
|
19152
18412
|
|
|
19153
18413
|
function usageStatsCmd(root) {
|
|
19154
18414
|
root = absRoot(root || process.cwd());
|