leerness 1.9.35 → 1.9.36
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 +28 -0
- package/README.md +6 -4
- package/bin/harness.js +165 -36
- package/package.json +1 -1
- package/scripts/e2e.js +55 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.36 — 2026-05-18
|
|
4
|
+
|
|
5
|
+
**외부 AI CLI 오케스트레이션 강화: dispatch 안전 모드 + agents bench + 작업 유형 추천 + stress test에서 발견한 2 BUG 즉시 수정**.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`leerness agents bench "<task>" [--write] [--timeout N]`** — 활성/설치된 모든 ready CLI에 같은 task를 동시 호출. 결과: 시간/exit/응답길이/마지막 라인 비교 매트릭스 + 🏆 가장 빠른 CLI 자동 표시. `--json` 출력 지원.
|
|
10
|
+
- **`agents dispatch`에 `--write` 모드 추가** — 기본은 read-only (안전). `--write` 명시 시 각 CLI에 위험 플래그 자동 첨부:
|
|
11
|
+
- claude → `--print --dangerously-skip-permissions`
|
|
12
|
+
- codex → `exec --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox`
|
|
13
|
+
- gemini → `-p --yolo`
|
|
14
|
+
- **`_recommendAgent()` 작업 유형 기반 CLI 추천** — task 키워드 분석:
|
|
15
|
+
- 번역/요약/분석/review → **claude** (1.7× 빠름)
|
|
16
|
+
- 아키텍처/리팩터/복잡 → **codex** (가장 상세)
|
|
17
|
+
- 생성/작성/수정/구현 → **gemini --yolo** (직접 수정 정확)
|
|
18
|
+
- ready 체크 전에 출력 → 비활성이어도 추천 안내
|
|
19
|
+
- **`dispatch` 출력에 CLI별 안내 추가** — codex의 POSIX path 변환 차이, gemini의 yolo 위험성 등.
|
|
20
|
+
|
|
21
|
+
### Fixed (stress test에서 발견된 진짜 BUG)
|
|
22
|
+
|
|
23
|
+
- 🔴 **`contract verify` require() side-effect 제거** — `require(implFile)`가 스크립트 본문 실행 → 18초 소요 + 임의 코드 실행 위험. **정적 소스 분석** (`module.exports = {...}` / `exports.foo =` 패턴 grep)으로 교체. 18,245ms → **705ms (25.9× 빠름)** + 보안 위험 제거.
|
|
24
|
+
- 🟡 **`reuse autodetect` 디렉토리 제한 해제** — `src/`만 스캔 → **src/, bin/, lib/, app/ 4개 디렉토리** 스캔. require → 정적 분석.
|
|
25
|
+
|
|
26
|
+
### Verified
|
|
27
|
+
- 신규 프로젝트 `_apps/leerness-stress` 생성 + 31개 leerness 명령 자동 호출 stress test
|
|
28
|
+
- 결과: 28 PASS / 3 의도된 BUG 감지 (false positive 0건)
|
|
29
|
+
- e2e: 166/166 PASS (1.9.35 161 + 신규 5)
|
|
30
|
+
|
|
3
31
|
## 1.9.35 — 2026-05-17
|
|
4
32
|
|
|
5
33
|
**파이프라인 메타-감사에서 도출된 5개 개선 사항 통합**.
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **AI 에이전트 검수·다중 협업 CLI 하네스** — 거짓 완료 차단, 멀티 에이전트 오케스트레이션, 사양 ↔ 구현 일치 검증, 워크스페이스 통합 가시성. **한국어 우선**.
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/leerness) [](https://www.npmjs.com/package/leerness) []() []() []()
|
|
6
6
|
|
|
7
7
|
> Claude Code · Cursor · Copilot · Codex · Gemini CLI — 어떤 AI 에이전트로 코드를 짜든, leerness는 **"진짜로 했나? 중복 아닌가? 합의된 사양인가?"** 를 자동으로 검수합니다.
|
|
8
8
|
|
|
@@ -275,9 +275,10 @@ leerness deps <capability> --run-tests # 영향 추적 + 자동 회귀
|
|
|
275
275
|
leerness setup-agents . # 인터랙티브 활성화 + 자동 설치
|
|
276
276
|
leerness agents list # 4 CLI 상태표
|
|
277
277
|
leerness agents quota # 사용량 추정
|
|
278
|
-
leerness agents dispatch "<task>" --to gemini #
|
|
279
|
-
leerness
|
|
280
|
-
leerness
|
|
278
|
+
leerness agents dispatch "<task>" --to gemini --write # 1.9.36 --write: --yolo 자동 첨부
|
|
279
|
+
leerness agents bench "<task>" [--timeout N] # 1.9.36 ready CLI 모두 병렬 호출 + 비교표
|
|
280
|
+
leerness contract verify <spec.md> <impl.js> # 1.9.35/36 명세 ↔ 구현 일치 검사 (정적 분석)
|
|
281
|
+
leerness reuse autodetect [path] [--apply] # 1.9.35/36 capability 자동 등록 (src/bin/lib/app)
|
|
281
282
|
leerness audit . --fix # 1.9.35 누락 메타 자동 갱신
|
|
282
283
|
```
|
|
283
284
|
|
|
@@ -470,6 +471,7 @@ npm test # = node ./scripts/e2e.js
|
|
|
470
471
|
|
|
471
472
|
## 📜 변경 이력 (최근)
|
|
472
473
|
|
|
474
|
+
- **1.9.36** — 외부 AI CLI 오케스트레이션 강화: `agents bench` (3 CLI 동시 비교) + `dispatch --write` 자동 권장 플래그 + 작업 유형 키워드 추천. stress test에서 발견한 `contract verify` require() side-effect (보안 위험 + 25× 속도 회복) 즉시 수정.
|
|
473
475
|
- **1.9.35** — 파이프라인 메타-감사에서 도출된 5개 개선 통합: `contract verify` · `reuse autodetect` · `audit --fix` · `handoff` init 부재 경고 · `agents dispatch` 안전 규칙 안내.
|
|
474
476
|
- **1.9.34** — 방향키/스페이스 인터랙티브 multi-select (`_selectOne`/`_selectMany`) + 256색 그라데이션 배너 + 3단계 sub-agent 오케스트레이션 검증 (2.2× 효율 실측).
|
|
475
477
|
- **1.9.33** — `_warnIfStale()` — `npx leerness init` 시 옛 버전 자동 경고 + 해결 안내.
|
package/bin/harness.js
CHANGED
|
@@ -6,7 +6,7 @@ const path = require('path');
|
|
|
6
6
|
const cp = require('child_process');
|
|
7
7
|
const readline = require('readline');
|
|
8
8
|
|
|
9
|
-
const VERSION = '1.9.
|
|
9
|
+
const VERSION = '1.9.36';
|
|
10
10
|
const MARK = '<!-- leerness:managed -->';
|
|
11
11
|
const README_START = '<!-- leerness:project-readme:start -->';
|
|
12
12
|
const README_END = '<!-- leerness:project-readme:end -->';
|
|
@@ -2369,6 +2369,30 @@ const EXTERNAL_AGENTS = [
|
|
|
2369
2369
|
installCmd: 'gh extension install github/gh-copilot', installHint: 'https://github.com/github/gh-copilot (gh CLI 선행 설치 필요)' }
|
|
2370
2370
|
];
|
|
2371
2371
|
|
|
2372
|
+
// 1.9.36: 작업 키워드 분석으로 최적 CLI 추천
|
|
2373
|
+
// \b는 ASCII word boundary만 인식 → 한글 키워드는 단순 substring 검사 사용.
|
|
2374
|
+
function _recommendAgent(task) {
|
|
2375
|
+
if (!task || typeof task !== 'string') return { target: null, reason: '' };
|
|
2376
|
+
const t = task.toLowerCase();
|
|
2377
|
+
const hasAny = (keywords) => keywords.some(k => t.includes(k));
|
|
2378
|
+
// 텍스트 분석/번역 → claude (가장 빠름, 1.7×)
|
|
2379
|
+
if (hasAny(['translate', 'summary', 'explain', 'describe', 'analyze', 'review',
|
|
2380
|
+
'번역', '요약', '설명', '분석', '리뷰'])) {
|
|
2381
|
+
return { target: 'claude', reason: '텍스트 분석·요약·번역은 claude가 1.7× 빠름' };
|
|
2382
|
+
}
|
|
2383
|
+
// 깊은 코드 추론
|
|
2384
|
+
if (hasAny(['architecture', 'design pattern', 'refactor', 'trace', 'complex', 'critical path',
|
|
2385
|
+
'아키텍처', '리팩터', '복잡'])) {
|
|
2386
|
+
return { target: 'codex', reason: '깊은 코드 추론은 codex가 가장 상세' };
|
|
2387
|
+
}
|
|
2388
|
+
// 파일 작성·수정·생성
|
|
2389
|
+
if (hasAny(['create', 'write', 'generate', 'patch', 'fix', 'implement', 'edit',
|
|
2390
|
+
'구현', '생성', '작성', '수정', '추가'])) {
|
|
2391
|
+
return { target: 'gemini', reason: '워크스페이스 직접 수정은 gemini --yolo가 정확' };
|
|
2392
|
+
}
|
|
2393
|
+
return { target: null, reason: '' };
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2372
2396
|
function _checkAgent(agent, opts = {}) {
|
|
2373
2397
|
const enabled = process.env[agent.envFlag] === '1';
|
|
2374
2398
|
// PATH 존재 확인 (which / where)
|
|
@@ -2806,32 +2830,51 @@ function agentsCmd(root, sub, ...args) {
|
|
|
2806
2830
|
if (!target) { fail('--to <agent_id> 필요 (claude/codex/gemini/copilot)'); return process.exit(1); }
|
|
2807
2831
|
const agentDef = EXTERNAL_AGENTS.find(a => a.id === target);
|
|
2808
2832
|
if (!agentDef) { fail(`알 수 없는 agent: ${target}`); return process.exit(1); }
|
|
2833
|
+
// 1.9.36: 작업 유형 키워드 분석 → 최적 CLI 추천 (ready 체크 전에 출력 — 비활성이어도 추천)
|
|
2834
|
+
const recommendation = _recommendAgent(task);
|
|
2835
|
+
const recommended = recommendation.target;
|
|
2836
|
+
if (recommended && recommended !== target) {
|
|
2837
|
+
log(`💡 추천: 이 작업은 ${recommended}가 더 적합 (${recommendation.reason})`);
|
|
2838
|
+
}
|
|
2809
2839
|
const status = _checkAgent(agentDef);
|
|
2810
2840
|
if (status.status !== 'ready') {
|
|
2811
2841
|
fail(`${target} 비활성 (${status.status}). 환경변수 ${agentDef.envFlag}=1 + CLI 설치 필요.`);
|
|
2812
2842
|
return process.exit(1);
|
|
2813
2843
|
}
|
|
2844
|
+
// 1.9.36: --write 시 파일 수정 가능 권장 플래그 자동 첨부, 미명시 시 read-only 안전 모드
|
|
2845
|
+
const writeMode = has('--write');
|
|
2846
|
+
const readOnly = has('--readonly') || !writeMode;
|
|
2814
2847
|
// 실제 호출은 안 함 — 프롬프트만 생성 (사용자가 명시적으로 실행)
|
|
2815
|
-
log(`# leerness agents dispatch (1.9.
|
|
2848
|
+
log(`# leerness agents dispatch (1.9.36)`);
|
|
2816
2849
|
log(`대상: ${target} (${agentDef.bin})`);
|
|
2817
2850
|
log(`상태: 🟢 ready, 버전 ${status.version || '?'}`);
|
|
2851
|
+
log(`모드: ${writeMode ? '✏ write (파일 수정 가능)' : '🔒 read-only (분석 전용, 안전)'}`);
|
|
2818
2852
|
log('');
|
|
2819
2853
|
log(`## 실행 명령 (사용자가 복사해서 실행)`);
|
|
2820
2854
|
log('');
|
|
2855
|
+
const q = task.replace(/"/g, '\\"');
|
|
2821
2856
|
if (target === 'claude') {
|
|
2822
|
-
|
|
2857
|
+
const flags = writeMode ? '--print --dangerously-skip-permissions' : '--print';
|
|
2858
|
+
log(`claude ${flags} "${q}"`);
|
|
2859
|
+
if (writeMode) log(`# ⚠ --dangerously-skip-permissions: 도구 권한 자동 승인 (파일 수정 가능)`);
|
|
2823
2860
|
} else if (target === 'codex') {
|
|
2824
|
-
|
|
2861
|
+
const flags = writeMode ? 'exec --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox' : 'exec --skip-git-repo-check';
|
|
2862
|
+
log(`codex ${flags} "${q}"`);
|
|
2863
|
+
log(`# ℹ codex는 PowerShell 경유 — POSIX /tmp 경로는 C:\\tmp\\로 해석됨`);
|
|
2864
|
+
if (writeMode) log(`# ⚠ --dangerously-bypass-approvals-and-sandbox: sandbox 우회`);
|
|
2825
2865
|
} else if (target === 'gemini') {
|
|
2826
|
-
|
|
2866
|
+
const flags = writeMode ? '-p --yolo' : '-p';
|
|
2867
|
+
log(`gemini ${flags} "${q}"`);
|
|
2868
|
+
if (writeMode) log(`# ⚠ --yolo: 워크스페이스 파일 직접 수정 가능`);
|
|
2827
2869
|
} else if (target === 'copilot') {
|
|
2828
|
-
log(`gh copilot suggest "${
|
|
2870
|
+
log(`gh copilot suggest "${q}"`);
|
|
2829
2871
|
}
|
|
2830
2872
|
log('');
|
|
2831
|
-
log(`## 정책 (1.9.
|
|
2873
|
+
log(`## 정책 (1.9.36)`);
|
|
2832
2874
|
log(` - leerness는 외부 CLI를 자동 호출하지 않음 (사용자 명시적 실행)`);
|
|
2833
2875
|
log(` - 메인 에이전트(Claude)가 위 명령을 보고 sub-agent로 spawn 가능`);
|
|
2834
2876
|
log(` - quota 체크: \`leerness agents quota\` (1.9.31+)`);
|
|
2877
|
+
log(` - 동시 호출 시: \`leerness agents bench "<task>"\` (1.9.36)`);
|
|
2835
2878
|
log('');
|
|
2836
2879
|
log(`## 분배 시 안전 규칙 (1.9.35)`);
|
|
2837
2880
|
log(` - sub-agent 프롬프트에 "당신만 수정할 파일 경로"를 명시 (파일 경로 격리)`);
|
|
@@ -2841,6 +2884,85 @@ function agentsCmd(root, sub, ...args) {
|
|
|
2841
2884
|
return;
|
|
2842
2885
|
}
|
|
2843
2886
|
|
|
2887
|
+
if (sub === 'bench') {
|
|
2888
|
+
// 1.9.36: 같은 prompt를 ready CLI 모두에 동시 호출 + 시간/응답 길이/exit code 비교
|
|
2889
|
+
const task = args.filter(x => !x.startsWith('-')).join(' ').trim() || arg('--task', null);
|
|
2890
|
+
if (!task) { fail('bench "<task>" 필요'); return process.exit(1); }
|
|
2891
|
+
const timeoutS = parseInt(arg('--timeout', '60'), 10);
|
|
2892
|
+
const writeMode = has('--write');
|
|
2893
|
+
const ready = EXTERNAL_AGENTS.map(a => ({ agent: a, status: _checkAgent(a) }))
|
|
2894
|
+
.filter(x => x.status.status === 'ready');
|
|
2895
|
+
if (!ready.length) {
|
|
2896
|
+
fail('ready CLI 없음 — leerness setup-agents 또는 .env에 LEERNESS_ENABLE_X=1 설정 필요');
|
|
2897
|
+
return process.exit(1);
|
|
2898
|
+
}
|
|
2899
|
+
log(`# leerness agents bench (1.9.36)`);
|
|
2900
|
+
log(`task: ${task.slice(0, 80)}${task.length > 80 ? '…' : ''}`);
|
|
2901
|
+
log(`참여 CLI: ${ready.map(r => r.agent.id).join(', ')} (${ready.length}개)`);
|
|
2902
|
+
log(`타임아웃: ${timeoutS}s/CLI · 모드: ${writeMode ? 'write' : 'read-only'}`);
|
|
2903
|
+
log('');
|
|
2904
|
+
log('병렬 호출 중... (병렬 fork 후 wait)');
|
|
2905
|
+
log('');
|
|
2906
|
+
const results = [];
|
|
2907
|
+
const promises = ready.map(({ agent, status }) => new Promise((resolve) => {
|
|
2908
|
+
const t0 = Date.now();
|
|
2909
|
+
let cmd, cmdArgs;
|
|
2910
|
+
if (agent.id === 'claude') {
|
|
2911
|
+
cmdArgs = writeMode ? ['--print', '--dangerously-skip-permissions', task] : ['--print', task];
|
|
2912
|
+
cmd = 'claude';
|
|
2913
|
+
} else if (agent.id === 'codex') {
|
|
2914
|
+
cmdArgs = writeMode
|
|
2915
|
+
? ['exec', '--skip-git-repo-check', '--dangerously-bypass-approvals-and-sandbox', task]
|
|
2916
|
+
: ['exec', '--skip-git-repo-check', task];
|
|
2917
|
+
cmd = 'codex';
|
|
2918
|
+
} else if (agent.id === 'gemini') {
|
|
2919
|
+
cmdArgs = writeMode ? ['-p', task, '--yolo'] : ['-p', task];
|
|
2920
|
+
cmd = 'gemini';
|
|
2921
|
+
} else if (agent.id === 'copilot') {
|
|
2922
|
+
cmdArgs = ['copilot', 'suggest', task];
|
|
2923
|
+
cmd = 'gh';
|
|
2924
|
+
}
|
|
2925
|
+
const r = cp.spawn(cmd, cmdArgs, { shell: true });
|
|
2926
|
+
let stdout = '', stderr = '';
|
|
2927
|
+
r.stdout.on('data', d => { stdout += d; });
|
|
2928
|
+
r.stderr.on('data', d => { stderr += d; });
|
|
2929
|
+
const timer = setTimeout(() => { r.kill(); }, timeoutS * 1000);
|
|
2930
|
+
r.on('close', (code) => {
|
|
2931
|
+
clearTimeout(timer);
|
|
2932
|
+
const elapsed = Date.now() - t0;
|
|
2933
|
+
results.push({
|
|
2934
|
+
id: agent.id, exit: code, elapsed,
|
|
2935
|
+
stdout: stdout.trim().split('\n').slice(-3).join('\n'),
|
|
2936
|
+
stderrLen: stderr.length,
|
|
2937
|
+
ok: code === 0 && stdout.trim().length > 0
|
|
2938
|
+
});
|
|
2939
|
+
resolve();
|
|
2940
|
+
});
|
|
2941
|
+
r.on('error', (err) => {
|
|
2942
|
+
clearTimeout(timer);
|
|
2943
|
+
results.push({ id: agent.id, exit: -1, elapsed: Date.now() - t0, stdout: '', stderrLen: 0, error: err.message, ok: false });
|
|
2944
|
+
resolve();
|
|
2945
|
+
});
|
|
2946
|
+
}));
|
|
2947
|
+
return Promise.all(promises).then(() => {
|
|
2948
|
+
if (has('--json')) { log(JSON.stringify({ task, results }, null, 2)); return; }
|
|
2949
|
+
log(`| CLI | 시간 | exit | 응답 길이 | 마지막 라인 |`);
|
|
2950
|
+
log(`|---|---:|---:|---:|---|`);
|
|
2951
|
+
// sort by elapsed
|
|
2952
|
+
results.sort((a, b) => a.elapsed - b.elapsed);
|
|
2953
|
+
for (const r of results) {
|
|
2954
|
+
const respLen = (r.stdout || '').length;
|
|
2955
|
+
const last = (r.stdout || '').split('\n').pop().slice(0, 50);
|
|
2956
|
+
log(`| ${r.id} | ${r.elapsed}ms | ${r.exit} | ${respLen} | ${last.replace(/\|/g, '\\|')} |`);
|
|
2957
|
+
}
|
|
2958
|
+
log('');
|
|
2959
|
+
const okCount = results.filter(r => r.ok).length;
|
|
2960
|
+
log(`결과: ${okCount}/${results.length} 성공`);
|
|
2961
|
+
const fastest = results.filter(r => r.ok).sort((a, b) => a.elapsed - b.elapsed)[0];
|
|
2962
|
+
if (fastest) log(`🏆 가장 빠름: ${fastest.id} (${fastest.elapsed}ms)`);
|
|
2963
|
+
});
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2844
2966
|
if (sub === 'quota') {
|
|
2845
2967
|
// 1.9.31: 각 CLI 사용량/쿼터 추정 + provider 대시보드 링크
|
|
2846
2968
|
const results = [];
|
|
@@ -2903,7 +3025,7 @@ function agentsCmd(root, sub, ...args) {
|
|
|
2903
3025
|
return;
|
|
2904
3026
|
}
|
|
2905
3027
|
|
|
2906
|
-
fail('사용법: leerness agents list|check|quota|dispatch "<task>" --to <id>');
|
|
3028
|
+
fail('사용법: leerness agents list|check|quota|dispatch|bench [--write] "<task>" [--to <id>]');
|
|
2907
3029
|
return process.exit(1);
|
|
2908
3030
|
}
|
|
2909
3031
|
|
|
@@ -5119,28 +5241,27 @@ function contractVerifyCmd(specPath, implPath) {
|
|
|
5119
5241
|
for (const m of specText.matchAll(/`([A-Za-z_$][\w$]*)\s*\(/g)) fnSpec.add(m[1]);
|
|
5120
5242
|
// 필드: tick.<name>
|
|
5121
5243
|
for (const m of specText.matchAll(/tick\.([A-Za-z_$][\w$]*)/g)) fieldSpec.add(m[1]);
|
|
5122
|
-
//
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
// 캐시 우회: delete from cache
|
|
5126
|
-
delete require.cache[implFile];
|
|
5127
|
-
exports = require(implFile);
|
|
5128
|
-
} catch (e) { fail(`impl 로드 실패: ${e.message}`); return process.exit(1); }
|
|
5244
|
+
// 1.9.36 BUG-fix: require()는 side-effect 실행 위험 (CLI 스크립트는 require로 실행됨).
|
|
5245
|
+
// 대신 정적 소스 분석 — module.exports = { foo, bar } / exports.foo = ... / module.exports.foo = ... 패턴 grep.
|
|
5246
|
+
const implSrc = read(implFile);
|
|
5129
5247
|
const implExports = new Set();
|
|
5130
|
-
|
|
5131
|
-
|
|
5248
|
+
// pattern 1: module.exports = { foo, bar, baz }
|
|
5249
|
+
for (const m of implSrc.matchAll(/module\.exports\s*=\s*\{([^}]+)\}/g)) {
|
|
5250
|
+
for (const k of m[1].split(',')) {
|
|
5251
|
+
const name = k.replace(/:.*/, '').trim();
|
|
5252
|
+
if (/^[A-Za-z_$][\w$]*$/.test(name)) implExports.add(name);
|
|
5253
|
+
}
|
|
5132
5254
|
}
|
|
5133
|
-
//
|
|
5255
|
+
// pattern 2: exports.foo = / module.exports.foo =
|
|
5256
|
+
for (const m of implSrc.matchAll(/(?:module\.)?exports\.([A-Za-z_$][\w$]*)\s*=/g)) implExports.add(m[1]);
|
|
5257
|
+
// pattern 3: function foo + module.exports에 포함되었는지는 위에서 처리됨
|
|
5258
|
+
// 검사: spec에 명시된 함수 중 impl exports에 없는 것
|
|
5134
5259
|
const missing = [];
|
|
5135
5260
|
for (const fn of fnSpec) {
|
|
5136
|
-
|
|
5137
|
-
if (typeof exports[fn] === 'function') continue;
|
|
5261
|
+
if (implExports.has(fn)) continue;
|
|
5138
5262
|
// spec에 'function fnName('이 있지만 impl exports에 없으면 미구현
|
|
5139
|
-
// 단, 헬퍼 함수(internal _)는 spec에 없을 수 있으니 단방향만
|
|
5140
5263
|
if (specText.includes(`function ${fn}`) && !implExports.has(fn)) missing.push(fn);
|
|
5141
5264
|
}
|
|
5142
|
-
// 필드 검증: impl 소스 파일에 spec 필드명이 있는지 grep
|
|
5143
|
-
const implSrc = read(implFile);
|
|
5144
5265
|
const fieldMissing = [];
|
|
5145
5266
|
for (const f of fieldSpec) {
|
|
5146
5267
|
if (!new RegExp(`\\b${f}\\b`).test(implSrc)) fieldMissing.push(f);
|
|
@@ -5182,20 +5303,28 @@ function contractVerifyCmd(specPath, implPath) {
|
|
|
5182
5303
|
// src/*.js의 module.exports를 스캔해서 reuse-map.md에 capability 후보 등록.
|
|
5183
5304
|
function reuseAutodetectCmd(root) {
|
|
5184
5305
|
root = absRoot(root || process.cwd());
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5306
|
+
// 1.9.36 BUG-fix: src/만이 아니라 bin/, lib/, app/도 스캔. require() 대신 정적 분석 (side-effect 차단).
|
|
5307
|
+
const candidateDirs = ['src', 'bin', 'lib', 'app'].filter(d => exists(path.join(root, d)));
|
|
5308
|
+
if (!candidateDirs.length) { fail(`스캔할 디렉토리 없음 (src/, bin/, lib/, app/ 중 하나 필요): ${root}`); return process.exit(1); }
|
|
5188
5309
|
const found = [];
|
|
5189
|
-
for (const
|
|
5190
|
-
const
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5310
|
+
for (const dir of candidateDirs) {
|
|
5311
|
+
const files = fs.readdirSync(path.join(root, dir)).filter(f => f.endsWith('.js'));
|
|
5312
|
+
for (const f of files) {
|
|
5313
|
+
const full = path.join(root, dir, f);
|
|
5314
|
+
const src = read(full);
|
|
5315
|
+
// 정적 분석: module.exports = { foo, bar } / exports.foo = / module.exports.foo =
|
|
5316
|
+
const names = new Set();
|
|
5317
|
+
for (const m of src.matchAll(/module\.exports\s*=\s*\{([^}]+)\}/g)) {
|
|
5318
|
+
for (const k of m[1].split(',')) {
|
|
5319
|
+
const name = k.replace(/:.*/, '').trim();
|
|
5320
|
+
if (/^[A-Za-z_$][\w$]*$/.test(name)) names.add(name);
|
|
5321
|
+
}
|
|
5322
|
+
}
|
|
5323
|
+
for (const m of src.matchAll(/(?:module\.)?exports\.([A-Za-z_$][\w$]*)\s*=/g)) names.add(m[1]);
|
|
5324
|
+
for (const name of names) {
|
|
5325
|
+
if (name.startsWith('_')) continue; // internal helpers 제외
|
|
5326
|
+
found.push({ file: `${dir}/${f}`, name });
|
|
5327
|
+
}
|
|
5199
5328
|
}
|
|
5200
5329
|
}
|
|
5201
5330
|
if (has('--json')) {
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -950,6 +950,61 @@ total++;
|
|
|
950
950
|
if (!ok) { failed++; console.log(r.stdout.slice(0, 800)); }
|
|
951
951
|
}
|
|
952
952
|
|
|
953
|
+
// 1.9.36 회귀: dispatch 권장 플래그 + bench + 작업 유형 추천
|
|
954
|
+
total++;
|
|
955
|
+
{
|
|
956
|
+
// dispatch --write 시 gemini --yolo 자동 추가
|
|
957
|
+
const env = { ...process.env, LEERNESS_ENABLE_GEMINI: '1' };
|
|
958
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'agents', 'dispatch', '코드 분석해서 요약', '--to', 'gemini', '--write'], { encoding: 'utf8', timeout: 15000, env });
|
|
959
|
+
// gemini가 ready면 명령 출력에 --yolo 포함, 비-ready면 거부 — 둘 다 OK
|
|
960
|
+
const ok = (r.status === 0 && /--yolo/.test(r.stdout) && /write \(파일 수정 가능\)/.test(r.stdout))
|
|
961
|
+
|| (r.status !== 0 && /비활성|disabled|not-installed/.test(r.stdout));
|
|
962
|
+
console.log(ok ? '✓ B(1.9.36) dispatch --write: gemini --yolo 자동 첨부 또는 비활성 거부' : `✗ dispatch --write 실패`);
|
|
963
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
total++;
|
|
967
|
+
{
|
|
968
|
+
// dispatch read-only (기본) — --yolo/--dangerously 같은 위험 플래그 없음
|
|
969
|
+
const env = { ...process.env, LEERNESS_ENABLE_CLAUDE: '1' };
|
|
970
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'agents', 'dispatch', '번역해줘', '--to', 'claude'], { encoding: 'utf8', timeout: 15000, env });
|
|
971
|
+
// claude가 ready면 read-only 표시 + dangerously 플래그 없음
|
|
972
|
+
const ok = (r.status === 0 && /read-only/.test(r.stdout) && !/--dangerously-skip-permissions/.test(r.stdout))
|
|
973
|
+
|| (r.status !== 0 && /비활성|disabled|not-installed/.test(r.stdout));
|
|
974
|
+
console.log(ok ? '✓ B(1.9.36) dispatch read-only 기본: 위험 플래그 미첨부 또는 비활성 거부' : `✗ dispatch read-only 실패`);
|
|
975
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
total++;
|
|
979
|
+
{
|
|
980
|
+
// 작업 유형 추천 — 비활성 CLI에도 추천 메시지 우선 출력
|
|
981
|
+
const env = { ...process.env, LEERNESS_ENABLE_GEMINI: '0', LEERNESS_ENABLE_CLAUDE: '0' };
|
|
982
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'agents', 'dispatch', '번역해줘 한국어를 영어로', '--to', 'gemini'], { encoding: 'utf8', timeout: 15000, env });
|
|
983
|
+
// 번역 → claude 추천. ready 체크 전에 추천 출력 → stdout에 "추천...claude" 포함
|
|
984
|
+
const ok = /추천.*claude/.test(r.stdout);
|
|
985
|
+
console.log(ok ? '✓ B(1.9.36) 작업 유형 추천: 번역→claude 추천 (비활성이어도 출력)' : `✗ 추천 실패`);
|
|
986
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
total++;
|
|
990
|
+
{
|
|
991
|
+
// bench 명령: ready CLI 없을 때 거부
|
|
992
|
+
const env = { ...process.env, LEERNESS_ENABLE_CLAUDE: '0', LEERNESS_ENABLE_CODEX: '0', LEERNESS_ENABLE_GEMINI: '0', LEERNESS_ENABLE_COPILOT: '0' };
|
|
993
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'agents', 'bench', 'test'], { encoding: 'utf8', timeout: 15000, env });
|
|
994
|
+
const ok = r.status !== 0 && /ready CLI 없음/.test(r.stdout);
|
|
995
|
+
console.log(ok ? '✓ B(1.9.36) agents bench: ready 없을 때 거부' : `✗ bench 거부 실패`);
|
|
996
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 300)); }
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
total++;
|
|
1000
|
+
{
|
|
1001
|
+
// 사용법 메시지에 bench 포함
|
|
1002
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'agents', 'unknown'], { encoding: 'utf8', timeout: 10000 });
|
|
1003
|
+
const ok = r.status !== 0 && /bench/.test(r.stdout + r.stderr);
|
|
1004
|
+
console.log(ok ? '✓ B(1.9.36) agents 사용법에 bench 명시' : `✗ usage bench 실패`);
|
|
1005
|
+
if (!ok) { failed++; console.log((r.stdout + r.stderr).slice(0, 300)); }
|
|
1006
|
+
}
|
|
1007
|
+
|
|
953
1008
|
// 1.9.35 회귀: 5개 신규 기능
|
|
954
1009
|
total++;
|
|
955
1010
|
{
|