leerness 1.9.35 → 1.9.37
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 +62 -0
- package/README.md +8 -4
- package/bin/harness.js +314 -36
- package/package.json +1 -1
- package/scripts/e2e.js +127 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,67 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.37 — 2026-05-18
|
|
4
|
+
|
|
5
|
+
**메인 에이전트의 "leerness 점점 안 쓰는" drift 현상 자동 감지·경고**.
|
|
6
|
+
|
|
7
|
+
### 배경
|
|
8
|
+
실 워크스페이스 분석 결과: 라운드가 길어질수록 메인 에이전트가 `session close` / `task add` 등을 점점 잊는 패턴 발견.
|
|
9
|
+
- session-handoff.md 4.6일 stale
|
|
10
|
+
- task-log.md 4.6일 stale
|
|
11
|
+
- progress-tracker T-row 3일간 0건 업데이트
|
|
12
|
+
- 신규 sub-app 4개에 task 0건 등록
|
|
13
|
+
|
|
14
|
+
→ **drift score 100/200 (🔴 critical) 등급**. 사용자 우려 사실 확인.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`leerness drift check [path]`** 신규 명령:
|
|
19
|
+
- 4개 신호 측정: session-handoff.md, current-state.md, progress-tracker.md, task-log.md의 staleness
|
|
20
|
+
- 추가 신호: `_apps/*` 중 task 0건인 sub-project 수
|
|
21
|
+
- 가중치 합계 → 4단계 레벨 (🟢 healthy / 🟠 attention / 🟡 warning / 🔴 critical)
|
|
22
|
+
- 임계 0/20/50/100. 점수 ≥100 시 exit 1 (CI 친화)
|
|
23
|
+
- `--json` 출력 지원
|
|
24
|
+
- 권장 조치 자동 안내 (`session close` / `audit --fix` / `task add`)
|
|
25
|
+
- **`handoff` 자동 drift 경고** — handoff 호출 시 빠른 inline check (전체 `drift check` 안 호출). session-handoff/progress-tracker 중 하나라도 2일 이상 stale이면 노랑색 경고 + 권장 명령 안내.
|
|
26
|
+
- **스킵 옵션**: `--no-drift-check` 플래그 + `LEERNESS_NO_DRIFT_CHECK=1` 환경변수
|
|
27
|
+
|
|
28
|
+
### 실측 (이번 라운드)
|
|
29
|
+
- 실 워크스페이스: drift 100/200 (critical) → `session close` 1회 후 30/200 (attention)
|
|
30
|
+
- e2e: 170/170 PASS (1.9.36 166 + 신규 4)
|
|
31
|
+
|
|
32
|
+
### 정책
|
|
33
|
+
- ✅ drift 경고는 *알림만* — 자동 실행 금지 (사용자/메인이 명시적 선택)
|
|
34
|
+
- ✅ 빠른 inline check (handoff) vs 상세 보고 (`drift check`) 분리
|
|
35
|
+
- ✅ CI 친화: `--no-drift-check` 또는 env로 끄기 가능
|
|
36
|
+
|
|
37
|
+
## 1.9.36 — 2026-05-18
|
|
38
|
+
|
|
39
|
+
**외부 AI CLI 오케스트레이션 강화: dispatch 안전 모드 + agents bench + 작업 유형 추천 + stress test에서 발견한 2 BUG 즉시 수정**.
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- **`leerness agents bench "<task>" [--write] [--timeout N]`** — 활성/설치된 모든 ready CLI에 같은 task를 동시 호출. 결과: 시간/exit/응답길이/마지막 라인 비교 매트릭스 + 🏆 가장 빠른 CLI 자동 표시. `--json` 출력 지원.
|
|
44
|
+
- **`agents dispatch`에 `--write` 모드 추가** — 기본은 read-only (안전). `--write` 명시 시 각 CLI에 위험 플래그 자동 첨부:
|
|
45
|
+
- claude → `--print --dangerously-skip-permissions`
|
|
46
|
+
- codex → `exec --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox`
|
|
47
|
+
- gemini → `-p --yolo`
|
|
48
|
+
- **`_recommendAgent()` 작업 유형 기반 CLI 추천** — task 키워드 분석:
|
|
49
|
+
- 번역/요약/분석/review → **claude** (1.7× 빠름)
|
|
50
|
+
- 아키텍처/리팩터/복잡 → **codex** (가장 상세)
|
|
51
|
+
- 생성/작성/수정/구현 → **gemini --yolo** (직접 수정 정확)
|
|
52
|
+
- ready 체크 전에 출력 → 비활성이어도 추천 안내
|
|
53
|
+
- **`dispatch` 출력에 CLI별 안내 추가** — codex의 POSIX path 변환 차이, gemini의 yolo 위험성 등.
|
|
54
|
+
|
|
55
|
+
### Fixed (stress test에서 발견된 진짜 BUG)
|
|
56
|
+
|
|
57
|
+
- 🔴 **`contract verify` require() side-effect 제거** — `require(implFile)`가 스크립트 본문 실행 → 18초 소요 + 임의 코드 실행 위험. **정적 소스 분석** (`module.exports = {...}` / `exports.foo =` 패턴 grep)으로 교체. 18,245ms → **705ms (25.9× 빠름)** + 보안 위험 제거.
|
|
58
|
+
- 🟡 **`reuse autodetect` 디렉토리 제한 해제** — `src/`만 스캔 → **src/, bin/, lib/, app/ 4개 디렉토리** 스캔. require → 정적 분석.
|
|
59
|
+
|
|
60
|
+
### Verified
|
|
61
|
+
- 신규 프로젝트 `_apps/leerness-stress` 생성 + 31개 leerness 명령 자동 호출 stress test
|
|
62
|
+
- 결과: 28 PASS / 3 의도된 BUG 감지 (false positive 0건)
|
|
63
|
+
- e2e: 166/166 PASS (1.9.35 161 + 신규 5)
|
|
64
|
+
|
|
3
65
|
## 1.9.35 — 2026-05-17
|
|
4
66
|
|
|
5
67
|
**파이프라인 메타-감사에서 도출된 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
|
|
|
@@ -42,6 +42,7 @@ AI 에이전트(Claude Code, Cursor, Copilot, Codex, Gemini CLI 등)는 **빠르
|
|
|
42
42
|
| 🚨 신규 모듈의 capability가 reuse-map에 등록 안 됨 | **`leerness reuse autodetect`** — `module.exports` 스캔 + 자동 등록 (1.9.35) |
|
|
43
43
|
| 🚨 신규 디렉토리에서 sub-agent가 컨텍스트 없이 작업 시작 | `handoff` 호출 시 **.harness 부재 자동 경고** (1.9.35) |
|
|
44
44
|
| 🚨 audit warning이 쌓이지만 수동 fix가 번거로움 | **`audit --fix`** — session-handoff/current-state 자동 갱신 (1.9.35) |
|
|
45
|
+
| 🚨 **프로젝트가 길어질수록 메인 에이전트가 leerness를 점점 안 씀** (메타파일 stale, session close 누락) | **`leerness drift check`** — 4개 신호로 staleness 측정 + handoff 시 자동 경고 (1.9.37) |
|
|
45
46
|
|
|
46
47
|
---
|
|
47
48
|
|
|
@@ -275,9 +276,10 @@ leerness deps <capability> --run-tests # 영향 추적 + 자동 회귀
|
|
|
275
276
|
leerness setup-agents . # 인터랙티브 활성화 + 자동 설치
|
|
276
277
|
leerness agents list # 4 CLI 상태표
|
|
277
278
|
leerness agents quota # 사용량 추정
|
|
278
|
-
leerness agents dispatch "<task>" --to gemini #
|
|
279
|
-
leerness
|
|
280
|
-
leerness
|
|
279
|
+
leerness agents dispatch "<task>" --to gemini --write # 1.9.36 --write: --yolo 자동 첨부
|
|
280
|
+
leerness agents bench "<task>" [--timeout N] # 1.9.36 ready CLI 모두 병렬 호출 + 비교표
|
|
281
|
+
leerness contract verify <spec.md> <impl.js> # 1.9.35/36 명세 ↔ 구현 일치 검사 (정적 분석)
|
|
282
|
+
leerness reuse autodetect [path] [--apply] # 1.9.35/36 capability 자동 등록 (src/bin/lib/app)
|
|
281
283
|
leerness audit . --fix # 1.9.35 누락 메타 자동 갱신
|
|
282
284
|
```
|
|
283
285
|
|
|
@@ -470,6 +472,8 @@ npm test # = node ./scripts/e2e.js
|
|
|
470
472
|
|
|
471
473
|
## 📜 변경 이력 (최근)
|
|
472
474
|
|
|
475
|
+
- **1.9.37** — `leerness drift check` — 라운드가 길어질수록 메인 에이전트가 leerness를 점점 안 쓰는 현상 자동 감지. handoff 시 자동 경고 + 4단계 레벨 (healthy/attention/warning/critical) + 권장 조치 안내. 실 워크스페이스에서 4.6일 stale, drift 100/200 (critical) 발견 → session close 1회로 30/200 (attention) 회복 실증.
|
|
476
|
+
- **1.9.36** — 외부 AI CLI 오케스트레이션 강화: `agents bench` (3 CLI 동시 비교) + `dispatch --write` 자동 권장 플래그 + 작업 유형 키워드 추천. stress test에서 발견한 `contract verify` require() side-effect (보안 위험 + 25× 속도 회복) 즉시 수정.
|
|
473
477
|
- **1.9.35** — 파이프라인 메타-감사에서 도출된 5개 개선 통합: `contract verify` · `reuse autodetect` · `audit --fix` · `handoff` init 부재 경고 · `agents dispatch` 안전 규칙 안내.
|
|
474
478
|
- **1.9.34** — 방향키/스페이스 인터랙티브 multi-select (`_selectOne`/`_selectMany`) + 256색 그라데이션 배너 + 3단계 sub-agent 오케스트레이션 검증 (2.2× 효율 실측).
|
|
475
479
|
- **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.37';
|
|
10
10
|
const MARK = '<!-- leerness:managed -->';
|
|
11
11
|
const README_START = '<!-- leerness:project-readme:start -->';
|
|
12
12
|
const README_END = '<!-- leerness:project-readme:end -->';
|
|
@@ -1432,6 +1432,38 @@ function handoffCmd(root) {
|
|
|
1432
1432
|
if (has('--all-apps') || arg('--include', null)) {
|
|
1433
1433
|
return _handoffWorkspace(absRoot(root));
|
|
1434
1434
|
}
|
|
1435
|
+
// 1.9.37: drift 자동 경고 (메인 에이전트가 leerness를 점점 안 쓰는 현상 감지)
|
|
1436
|
+
const absR0 = absRoot(root || process.cwd());
|
|
1437
|
+
if (exists(path.join(absR0, '.harness')) && !has('--no-drift-check') && process.env.LEERNESS_NO_DRIFT_CHECK !== '1') {
|
|
1438
|
+
try {
|
|
1439
|
+
const isTty = process.stdout && process.stdout.isTTY;
|
|
1440
|
+
const yel = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
|
|
1441
|
+
const red = s => isTty ? `\x1b[31m${s}\x1b[0m` : s;
|
|
1442
|
+
const dim = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
1443
|
+
// 간이 drift 계산 (전체 driftCheckCmd 호출 안 함 — 빠른 inline check)
|
|
1444
|
+
const now = Date.now();
|
|
1445
|
+
const shPath = handoffPath(absR0);
|
|
1446
|
+
let shAge = null;
|
|
1447
|
+
if (exists(shPath)) {
|
|
1448
|
+
const m = read(shPath).match(/Last generated:\s*([\d\-T:.Z]+)/);
|
|
1449
|
+
if (m) shAge = (now - new Date(m[1]).getTime()) / 86400000;
|
|
1450
|
+
}
|
|
1451
|
+
const rows = readProgressRows(absR0);
|
|
1452
|
+
let ptAge = null;
|
|
1453
|
+
if (rows.length) {
|
|
1454
|
+
const dates = rows.map(r => (r.updated || '').match(/\d{4}-\d{2}-\d{2}/)).filter(Boolean).map(m => m[0]).sort();
|
|
1455
|
+
if (dates.length) ptAge = (now - new Date(dates[dates.length - 1]).getTime()) / 86400000;
|
|
1456
|
+
}
|
|
1457
|
+
if ((shAge !== null && shAge > 2) || (ptAge !== null && ptAge > 2)) {
|
|
1458
|
+
log('');
|
|
1459
|
+
log(yel(' ⚠ leerness drift 감지 — 메타파일이 stale합니다'));
|
|
1460
|
+
if (shAge !== null && shAge > 2) log(dim(` session-handoff.md: ${shAge.toFixed(1)}일 stale`));
|
|
1461
|
+
if (ptAge !== null && ptAge > 2) log(dim(` progress-tracker: ${ptAge.toFixed(1)}일 stale`));
|
|
1462
|
+
log(dim(` → 권장: ${red('leerness session close .')} 또는 ${red('leerness drift check .')} 로 상세 보기`));
|
|
1463
|
+
log('');
|
|
1464
|
+
}
|
|
1465
|
+
} catch {}
|
|
1466
|
+
}
|
|
1435
1467
|
// 1.9.35 개선 #1: .harness 부재 시 즉시 경고 (자동 init 권장)
|
|
1436
1468
|
// 사용자가 신규 디렉토리에서 handoff 호출 시 sub-agent 작업이 길을 잃지 않도록.
|
|
1437
1469
|
const absR = absRoot(root || process.cwd());
|
|
@@ -2369,6 +2401,30 @@ const EXTERNAL_AGENTS = [
|
|
|
2369
2401
|
installCmd: 'gh extension install github/gh-copilot', installHint: 'https://github.com/github/gh-copilot (gh CLI 선행 설치 필요)' }
|
|
2370
2402
|
];
|
|
2371
2403
|
|
|
2404
|
+
// 1.9.36: 작업 키워드 분석으로 최적 CLI 추천
|
|
2405
|
+
// \b는 ASCII word boundary만 인식 → 한글 키워드는 단순 substring 검사 사용.
|
|
2406
|
+
function _recommendAgent(task) {
|
|
2407
|
+
if (!task || typeof task !== 'string') return { target: null, reason: '' };
|
|
2408
|
+
const t = task.toLowerCase();
|
|
2409
|
+
const hasAny = (keywords) => keywords.some(k => t.includes(k));
|
|
2410
|
+
// 텍스트 분석/번역 → claude (가장 빠름, 1.7×)
|
|
2411
|
+
if (hasAny(['translate', 'summary', 'explain', 'describe', 'analyze', 'review',
|
|
2412
|
+
'번역', '요약', '설명', '분석', '리뷰'])) {
|
|
2413
|
+
return { target: 'claude', reason: '텍스트 분석·요약·번역은 claude가 1.7× 빠름' };
|
|
2414
|
+
}
|
|
2415
|
+
// 깊은 코드 추론
|
|
2416
|
+
if (hasAny(['architecture', 'design pattern', 'refactor', 'trace', 'complex', 'critical path',
|
|
2417
|
+
'아키텍처', '리팩터', '복잡'])) {
|
|
2418
|
+
return { target: 'codex', reason: '깊은 코드 추론은 codex가 가장 상세' };
|
|
2419
|
+
}
|
|
2420
|
+
// 파일 작성·수정·생성
|
|
2421
|
+
if (hasAny(['create', 'write', 'generate', 'patch', 'fix', 'implement', 'edit',
|
|
2422
|
+
'구현', '생성', '작성', '수정', '추가'])) {
|
|
2423
|
+
return { target: 'gemini', reason: '워크스페이스 직접 수정은 gemini --yolo가 정확' };
|
|
2424
|
+
}
|
|
2425
|
+
return { target: null, reason: '' };
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2372
2428
|
function _checkAgent(agent, opts = {}) {
|
|
2373
2429
|
const enabled = process.env[agent.envFlag] === '1';
|
|
2374
2430
|
// PATH 존재 확인 (which / where)
|
|
@@ -2806,32 +2862,51 @@ function agentsCmd(root, sub, ...args) {
|
|
|
2806
2862
|
if (!target) { fail('--to <agent_id> 필요 (claude/codex/gemini/copilot)'); return process.exit(1); }
|
|
2807
2863
|
const agentDef = EXTERNAL_AGENTS.find(a => a.id === target);
|
|
2808
2864
|
if (!agentDef) { fail(`알 수 없는 agent: ${target}`); return process.exit(1); }
|
|
2865
|
+
// 1.9.36: 작업 유형 키워드 분석 → 최적 CLI 추천 (ready 체크 전에 출력 — 비활성이어도 추천)
|
|
2866
|
+
const recommendation = _recommendAgent(task);
|
|
2867
|
+
const recommended = recommendation.target;
|
|
2868
|
+
if (recommended && recommended !== target) {
|
|
2869
|
+
log(`💡 추천: 이 작업은 ${recommended}가 더 적합 (${recommendation.reason})`);
|
|
2870
|
+
}
|
|
2809
2871
|
const status = _checkAgent(agentDef);
|
|
2810
2872
|
if (status.status !== 'ready') {
|
|
2811
2873
|
fail(`${target} 비활성 (${status.status}). 환경변수 ${agentDef.envFlag}=1 + CLI 설치 필요.`);
|
|
2812
2874
|
return process.exit(1);
|
|
2813
2875
|
}
|
|
2876
|
+
// 1.9.36: --write 시 파일 수정 가능 권장 플래그 자동 첨부, 미명시 시 read-only 안전 모드
|
|
2877
|
+
const writeMode = has('--write');
|
|
2878
|
+
const readOnly = has('--readonly') || !writeMode;
|
|
2814
2879
|
// 실제 호출은 안 함 — 프롬프트만 생성 (사용자가 명시적으로 실행)
|
|
2815
|
-
log(`# leerness agents dispatch (1.9.
|
|
2880
|
+
log(`# leerness agents dispatch (1.9.36)`);
|
|
2816
2881
|
log(`대상: ${target} (${agentDef.bin})`);
|
|
2817
2882
|
log(`상태: 🟢 ready, 버전 ${status.version || '?'}`);
|
|
2883
|
+
log(`모드: ${writeMode ? '✏ write (파일 수정 가능)' : '🔒 read-only (분석 전용, 안전)'}`);
|
|
2818
2884
|
log('');
|
|
2819
2885
|
log(`## 실행 명령 (사용자가 복사해서 실행)`);
|
|
2820
2886
|
log('');
|
|
2887
|
+
const q = task.replace(/"/g, '\\"');
|
|
2821
2888
|
if (target === 'claude') {
|
|
2822
|
-
|
|
2889
|
+
const flags = writeMode ? '--print --dangerously-skip-permissions' : '--print';
|
|
2890
|
+
log(`claude ${flags} "${q}"`);
|
|
2891
|
+
if (writeMode) log(`# ⚠ --dangerously-skip-permissions: 도구 권한 자동 승인 (파일 수정 가능)`);
|
|
2823
2892
|
} else if (target === 'codex') {
|
|
2824
|
-
|
|
2893
|
+
const flags = writeMode ? 'exec --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox' : 'exec --skip-git-repo-check';
|
|
2894
|
+
log(`codex ${flags} "${q}"`);
|
|
2895
|
+
log(`# ℹ codex는 PowerShell 경유 — POSIX /tmp 경로는 C:\\tmp\\로 해석됨`);
|
|
2896
|
+
if (writeMode) log(`# ⚠ --dangerously-bypass-approvals-and-sandbox: sandbox 우회`);
|
|
2825
2897
|
} else if (target === 'gemini') {
|
|
2826
|
-
|
|
2898
|
+
const flags = writeMode ? '-p --yolo' : '-p';
|
|
2899
|
+
log(`gemini ${flags} "${q}"`);
|
|
2900
|
+
if (writeMode) log(`# ⚠ --yolo: 워크스페이스 파일 직접 수정 가능`);
|
|
2827
2901
|
} else if (target === 'copilot') {
|
|
2828
|
-
log(`gh copilot suggest "${
|
|
2902
|
+
log(`gh copilot suggest "${q}"`);
|
|
2829
2903
|
}
|
|
2830
2904
|
log('');
|
|
2831
|
-
log(`## 정책 (1.9.
|
|
2905
|
+
log(`## 정책 (1.9.36)`);
|
|
2832
2906
|
log(` - leerness는 외부 CLI를 자동 호출하지 않음 (사용자 명시적 실행)`);
|
|
2833
2907
|
log(` - 메인 에이전트(Claude)가 위 명령을 보고 sub-agent로 spawn 가능`);
|
|
2834
2908
|
log(` - quota 체크: \`leerness agents quota\` (1.9.31+)`);
|
|
2909
|
+
log(` - 동시 호출 시: \`leerness agents bench "<task>"\` (1.9.36)`);
|
|
2835
2910
|
log('');
|
|
2836
2911
|
log(`## 분배 시 안전 규칙 (1.9.35)`);
|
|
2837
2912
|
log(` - sub-agent 프롬프트에 "당신만 수정할 파일 경로"를 명시 (파일 경로 격리)`);
|
|
@@ -2841,6 +2916,85 @@ function agentsCmd(root, sub, ...args) {
|
|
|
2841
2916
|
return;
|
|
2842
2917
|
}
|
|
2843
2918
|
|
|
2919
|
+
if (sub === 'bench') {
|
|
2920
|
+
// 1.9.36: 같은 prompt를 ready CLI 모두에 동시 호출 + 시간/응답 길이/exit code 비교
|
|
2921
|
+
const task = args.filter(x => !x.startsWith('-')).join(' ').trim() || arg('--task', null);
|
|
2922
|
+
if (!task) { fail('bench "<task>" 필요'); return process.exit(1); }
|
|
2923
|
+
const timeoutS = parseInt(arg('--timeout', '60'), 10);
|
|
2924
|
+
const writeMode = has('--write');
|
|
2925
|
+
const ready = EXTERNAL_AGENTS.map(a => ({ agent: a, status: _checkAgent(a) }))
|
|
2926
|
+
.filter(x => x.status.status === 'ready');
|
|
2927
|
+
if (!ready.length) {
|
|
2928
|
+
fail('ready CLI 없음 — leerness setup-agents 또는 .env에 LEERNESS_ENABLE_X=1 설정 필요');
|
|
2929
|
+
return process.exit(1);
|
|
2930
|
+
}
|
|
2931
|
+
log(`# leerness agents bench (1.9.36)`);
|
|
2932
|
+
log(`task: ${task.slice(0, 80)}${task.length > 80 ? '…' : ''}`);
|
|
2933
|
+
log(`참여 CLI: ${ready.map(r => r.agent.id).join(', ')} (${ready.length}개)`);
|
|
2934
|
+
log(`타임아웃: ${timeoutS}s/CLI · 모드: ${writeMode ? 'write' : 'read-only'}`);
|
|
2935
|
+
log('');
|
|
2936
|
+
log('병렬 호출 중... (병렬 fork 후 wait)');
|
|
2937
|
+
log('');
|
|
2938
|
+
const results = [];
|
|
2939
|
+
const promises = ready.map(({ agent, status }) => new Promise((resolve) => {
|
|
2940
|
+
const t0 = Date.now();
|
|
2941
|
+
let cmd, cmdArgs;
|
|
2942
|
+
if (agent.id === 'claude') {
|
|
2943
|
+
cmdArgs = writeMode ? ['--print', '--dangerously-skip-permissions', task] : ['--print', task];
|
|
2944
|
+
cmd = 'claude';
|
|
2945
|
+
} else if (agent.id === 'codex') {
|
|
2946
|
+
cmdArgs = writeMode
|
|
2947
|
+
? ['exec', '--skip-git-repo-check', '--dangerously-bypass-approvals-and-sandbox', task]
|
|
2948
|
+
: ['exec', '--skip-git-repo-check', task];
|
|
2949
|
+
cmd = 'codex';
|
|
2950
|
+
} else if (agent.id === 'gemini') {
|
|
2951
|
+
cmdArgs = writeMode ? ['-p', task, '--yolo'] : ['-p', task];
|
|
2952
|
+
cmd = 'gemini';
|
|
2953
|
+
} else if (agent.id === 'copilot') {
|
|
2954
|
+
cmdArgs = ['copilot', 'suggest', task];
|
|
2955
|
+
cmd = 'gh';
|
|
2956
|
+
}
|
|
2957
|
+
const r = cp.spawn(cmd, cmdArgs, { shell: true });
|
|
2958
|
+
let stdout = '', stderr = '';
|
|
2959
|
+
r.stdout.on('data', d => { stdout += d; });
|
|
2960
|
+
r.stderr.on('data', d => { stderr += d; });
|
|
2961
|
+
const timer = setTimeout(() => { r.kill(); }, timeoutS * 1000);
|
|
2962
|
+
r.on('close', (code) => {
|
|
2963
|
+
clearTimeout(timer);
|
|
2964
|
+
const elapsed = Date.now() - t0;
|
|
2965
|
+
results.push({
|
|
2966
|
+
id: agent.id, exit: code, elapsed,
|
|
2967
|
+
stdout: stdout.trim().split('\n').slice(-3).join('\n'),
|
|
2968
|
+
stderrLen: stderr.length,
|
|
2969
|
+
ok: code === 0 && stdout.trim().length > 0
|
|
2970
|
+
});
|
|
2971
|
+
resolve();
|
|
2972
|
+
});
|
|
2973
|
+
r.on('error', (err) => {
|
|
2974
|
+
clearTimeout(timer);
|
|
2975
|
+
results.push({ id: agent.id, exit: -1, elapsed: Date.now() - t0, stdout: '', stderrLen: 0, error: err.message, ok: false });
|
|
2976
|
+
resolve();
|
|
2977
|
+
});
|
|
2978
|
+
}));
|
|
2979
|
+
return Promise.all(promises).then(() => {
|
|
2980
|
+
if (has('--json')) { log(JSON.stringify({ task, results }, null, 2)); return; }
|
|
2981
|
+
log(`| CLI | 시간 | exit | 응답 길이 | 마지막 라인 |`);
|
|
2982
|
+
log(`|---|---:|---:|---:|---|`);
|
|
2983
|
+
// sort by elapsed
|
|
2984
|
+
results.sort((a, b) => a.elapsed - b.elapsed);
|
|
2985
|
+
for (const r of results) {
|
|
2986
|
+
const respLen = (r.stdout || '').length;
|
|
2987
|
+
const last = (r.stdout || '').split('\n').pop().slice(0, 50);
|
|
2988
|
+
log(`| ${r.id} | ${r.elapsed}ms | ${r.exit} | ${respLen} | ${last.replace(/\|/g, '\\|')} |`);
|
|
2989
|
+
}
|
|
2990
|
+
log('');
|
|
2991
|
+
const okCount = results.filter(r => r.ok).length;
|
|
2992
|
+
log(`결과: ${okCount}/${results.length} 성공`);
|
|
2993
|
+
const fastest = results.filter(r => r.ok).sort((a, b) => a.elapsed - b.elapsed)[0];
|
|
2994
|
+
if (fastest) log(`🏆 가장 빠름: ${fastest.id} (${fastest.elapsed}ms)`);
|
|
2995
|
+
});
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2844
2998
|
if (sub === 'quota') {
|
|
2845
2999
|
// 1.9.31: 각 CLI 사용량/쿼터 추정 + provider 대시보드 링크
|
|
2846
3000
|
const results = [];
|
|
@@ -2903,7 +3057,7 @@ function agentsCmd(root, sub, ...args) {
|
|
|
2903
3057
|
return;
|
|
2904
3058
|
}
|
|
2905
3059
|
|
|
2906
|
-
fail('사용법: leerness agents list|check|quota|dispatch "<task>" --to <id>');
|
|
3060
|
+
fail('사용법: leerness agents list|check|quota|dispatch|bench [--write] "<task>" [--to <id>]');
|
|
2907
3061
|
return process.exit(1);
|
|
2908
3062
|
}
|
|
2909
3063
|
|
|
@@ -5096,6 +5250,122 @@ function viewworkInstall(root) {
|
|
|
5096
5250
|
ok('claude .claude/settings.local.json updated (Stop hook adds a viewwork event)');
|
|
5097
5251
|
}
|
|
5098
5252
|
|
|
5253
|
+
// 1.9.37: drift detection — 메타파일 staleness 측정으로 "leerness 점점 안 쓰는" 현상 감지
|
|
5254
|
+
function driftCheckCmd(root, opts = {}) {
|
|
5255
|
+
root = absRoot(root || process.cwd());
|
|
5256
|
+
const now = Date.now();
|
|
5257
|
+
const _ageDays = (p) => {
|
|
5258
|
+
if (!exists(p)) return null;
|
|
5259
|
+
return (now - fs.statSync(p).mtimeMs) / 86400000;
|
|
5260
|
+
};
|
|
5261
|
+
// 각 메타파일의 마지막 갱신
|
|
5262
|
+
const signals = [];
|
|
5263
|
+
// 1. session-handoff.md - "Last generated" 라인 우선, 없으면 mtime
|
|
5264
|
+
const shPath = handoffPath(root);
|
|
5265
|
+
if (exists(shPath)) {
|
|
5266
|
+
const txt = read(shPath);
|
|
5267
|
+
const m = txt.match(/Last generated:\s*([\d\-T:.Z]+)/);
|
|
5268
|
+
let ageDays;
|
|
5269
|
+
if (m) {
|
|
5270
|
+
ageDays = (now - new Date(m[1]).getTime()) / 86400000;
|
|
5271
|
+
} else {
|
|
5272
|
+
ageDays = _ageDays(shPath);
|
|
5273
|
+
}
|
|
5274
|
+
signals.push({ file: 'session-handoff.md', ageDays, threshold: 1, weight: 30, label: 'session close 누락' });
|
|
5275
|
+
}
|
|
5276
|
+
// 2. current-state.md - "Updated: YYYY-MM-DD" 라인
|
|
5277
|
+
const csPath = currentStatePath(root);
|
|
5278
|
+
if (exists(csPath)) {
|
|
5279
|
+
const m = read(csPath).match(/Updated:\s*(\d{4}-\d{2}-\d{2})/);
|
|
5280
|
+
const ageDays = m ? (now - new Date(m[1]).getTime()) / 86400000 : _ageDays(csPath);
|
|
5281
|
+
signals.push({ file: 'current-state.md', ageDays, threshold: 2, weight: 20, label: 'current-state 갱신 없음' });
|
|
5282
|
+
}
|
|
5283
|
+
// 3. progress-tracker.md 마지막 row의 updated 컬럼
|
|
5284
|
+
const rows = readProgressRows(root);
|
|
5285
|
+
if (rows.length) {
|
|
5286
|
+
const dates = rows.map(r => (r.updated || '').match(/\d{4}-\d{2}-\d{2}/)).filter(Boolean).map(m => m[0]);
|
|
5287
|
+
if (dates.length) {
|
|
5288
|
+
dates.sort();
|
|
5289
|
+
const latest = dates[dates.length - 1];
|
|
5290
|
+
const ageDays = (now - new Date(latest).getTime()) / 86400000;
|
|
5291
|
+
signals.push({ file: 'progress-tracker.md', ageDays, threshold: 1, weight: 30, label: 'task update 없음' });
|
|
5292
|
+
}
|
|
5293
|
+
} else {
|
|
5294
|
+
signals.push({ file: 'progress-tracker.md', ageDays: 999, threshold: 1, weight: 25, label: 'progress-tracker 비어있음' });
|
|
5295
|
+
}
|
|
5296
|
+
// 4. task-log.md 마지막 entry "## YYYY-MM-DD"
|
|
5297
|
+
const tlPath = taskLogPath(root);
|
|
5298
|
+
if (exists(tlPath)) {
|
|
5299
|
+
const dates = Array.from(read(tlPath).matchAll(/^## (\d{4}-\d{2}-\d{2})/gm)).map(m => m[1]);
|
|
5300
|
+
if (dates.length) {
|
|
5301
|
+
dates.sort();
|
|
5302
|
+
const latest = dates[dates.length - 1];
|
|
5303
|
+
const ageDays = (now - new Date(latest).getTime()) / 86400000;
|
|
5304
|
+
signals.push({ file: 'task-log.md', ageDays, threshold: 2, weight: 20, label: 'task-log 갱신 없음' });
|
|
5305
|
+
}
|
|
5306
|
+
}
|
|
5307
|
+
// 점수 계산
|
|
5308
|
+
let totalScore = 0;
|
|
5309
|
+
const fired = [];
|
|
5310
|
+
for (const s of signals) {
|
|
5311
|
+
if (s.ageDays > s.threshold) {
|
|
5312
|
+
totalScore += s.weight;
|
|
5313
|
+
fired.push(s);
|
|
5314
|
+
}
|
|
5315
|
+
}
|
|
5316
|
+
// 신규 _apps/* 에서 task 0건도 신호로
|
|
5317
|
+
const appsDir = path.join(root, '_apps');
|
|
5318
|
+
let appsZeroTask = [];
|
|
5319
|
+
if (exists(appsDir)) {
|
|
5320
|
+
for (const d of fs.readdirSync(appsDir)) {
|
|
5321
|
+
const sub = path.join(appsDir, d);
|
|
5322
|
+
if (!exists(path.join(sub, '.harness'))) continue;
|
|
5323
|
+
const subRows = readProgressRows(sub);
|
|
5324
|
+
if (!subRows.length) appsZeroTask.push(d);
|
|
5325
|
+
}
|
|
5326
|
+
if (appsZeroTask.length) {
|
|
5327
|
+
const w = Math.min(50, appsZeroTask.length * 10);
|
|
5328
|
+
totalScore += w;
|
|
5329
|
+
fired.push({ file: `_apps/* (${appsZeroTask.length}개)`, ageDays: null, threshold: 0, weight: w, label: `task 0건 sub-app: ${appsZeroTask.slice(0, 3).join(', ')}${appsZeroTask.length > 3 ? '...' : ''}` });
|
|
5330
|
+
}
|
|
5331
|
+
}
|
|
5332
|
+
// 레벨 판정
|
|
5333
|
+
let level = '🟢 healthy';
|
|
5334
|
+
if (totalScore >= 100) level = '🔴 critical';
|
|
5335
|
+
else if (totalScore >= 50) level = '🟡 warning';
|
|
5336
|
+
else if (totalScore >= 20) level = '🟠 attention';
|
|
5337
|
+
|
|
5338
|
+
if (has('--json')) {
|
|
5339
|
+
log(JSON.stringify({ root, score: totalScore, level, signals, fired, appsZeroTask }, null, 2));
|
|
5340
|
+
return;
|
|
5341
|
+
}
|
|
5342
|
+
log(`# leerness drift check (1.9.37)`);
|
|
5343
|
+
log(`경로: ${root}`);
|
|
5344
|
+
log('');
|
|
5345
|
+
log(`상태: ${level} · 점수 ${totalScore}/200`);
|
|
5346
|
+
log('');
|
|
5347
|
+
log(`| 신호 | age | 임계 | 가중치 | 발화 |`);
|
|
5348
|
+
log(`|---|---:|---:|---:|---|`);
|
|
5349
|
+
for (const s of signals) {
|
|
5350
|
+
const fire = s.ageDays > s.threshold ? '🔥' : '✓';
|
|
5351
|
+
const age = s.ageDays === null ? '-' : `${s.ageDays.toFixed(1)}d`;
|
|
5352
|
+
log(`| ${s.label} | ${age} | ${s.threshold}d | ${s.weight} | ${fire} |`);
|
|
5353
|
+
}
|
|
5354
|
+
if (appsZeroTask.length) {
|
|
5355
|
+
log('');
|
|
5356
|
+
log(`task 0건 sub-app (${appsZeroTask.length}개): ${appsZeroTask.join(', ')}`);
|
|
5357
|
+
}
|
|
5358
|
+
if (totalScore >= 50) {
|
|
5359
|
+
log('');
|
|
5360
|
+
log(`💡 권장 조치:`);
|
|
5361
|
+
log(` - 즉시: leerness session close . (handoff/current-state 갱신)`);
|
|
5362
|
+
log(` - 또는: leerness audit . --fix (자동 갱신 가능 항목 적용)`);
|
|
5363
|
+
log(` - sub-app에 task 등록: cd _apps/X && leerness task add "..."`);
|
|
5364
|
+
log(` - 이 검사 끄기: --no-drift-check 또는 LEERNESS_NO_DRIFT_CHECK=1`);
|
|
5365
|
+
}
|
|
5366
|
+
if (level === '🔴 critical') process.exitCode = 1;
|
|
5367
|
+
}
|
|
5368
|
+
|
|
5099
5369
|
// 1.9.35 개선 #3: contract verify <spec.md> <impl.js>
|
|
5100
5370
|
// 사양 문서(spec.md)에 명시된 함수 이름이 실제 module.exports에 모두 있는지 검사.
|
|
5101
5371
|
// 사용 예: leerness contract verify TICK_SPEC.md src/format.js
|
|
@@ -5119,28 +5389,27 @@ function contractVerifyCmd(specPath, implPath) {
|
|
|
5119
5389
|
for (const m of specText.matchAll(/`([A-Za-z_$][\w$]*)\s*\(/g)) fnSpec.add(m[1]);
|
|
5120
5390
|
// 필드: tick.<name>
|
|
5121
5391
|
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); }
|
|
5392
|
+
// 1.9.36 BUG-fix: require()는 side-effect 실행 위험 (CLI 스크립트는 require로 실행됨).
|
|
5393
|
+
// 대신 정적 소스 분석 — module.exports = { foo, bar } / exports.foo = ... / module.exports.foo = ... 패턴 grep.
|
|
5394
|
+
const implSrc = read(implFile);
|
|
5129
5395
|
const implExports = new Set();
|
|
5130
|
-
|
|
5131
|
-
|
|
5396
|
+
// pattern 1: module.exports = { foo, bar, baz }
|
|
5397
|
+
for (const m of implSrc.matchAll(/module\.exports\s*=\s*\{([^}]+)\}/g)) {
|
|
5398
|
+
for (const k of m[1].split(',')) {
|
|
5399
|
+
const name = k.replace(/:.*/, '').trim();
|
|
5400
|
+
if (/^[A-Za-z_$][\w$]*$/.test(name)) implExports.add(name);
|
|
5401
|
+
}
|
|
5132
5402
|
}
|
|
5133
|
-
//
|
|
5403
|
+
// pattern 2: exports.foo = / module.exports.foo =
|
|
5404
|
+
for (const m of implSrc.matchAll(/(?:module\.)?exports\.([A-Za-z_$][\w$]*)\s*=/g)) implExports.add(m[1]);
|
|
5405
|
+
// pattern 3: function foo + module.exports에 포함되었는지는 위에서 처리됨
|
|
5406
|
+
// 검사: spec에 명시된 함수 중 impl exports에 없는 것
|
|
5134
5407
|
const missing = [];
|
|
5135
5408
|
for (const fn of fnSpec) {
|
|
5136
|
-
|
|
5137
|
-
if (typeof exports[fn] === 'function') continue;
|
|
5409
|
+
if (implExports.has(fn)) continue;
|
|
5138
5410
|
// spec에 'function fnName('이 있지만 impl exports에 없으면 미구현
|
|
5139
|
-
// 단, 헬퍼 함수(internal _)는 spec에 없을 수 있으니 단방향만
|
|
5140
5411
|
if (specText.includes(`function ${fn}`) && !implExports.has(fn)) missing.push(fn);
|
|
5141
5412
|
}
|
|
5142
|
-
// 필드 검증: impl 소스 파일에 spec 필드명이 있는지 grep
|
|
5143
|
-
const implSrc = read(implFile);
|
|
5144
5413
|
const fieldMissing = [];
|
|
5145
5414
|
for (const f of fieldSpec) {
|
|
5146
5415
|
if (!new RegExp(`\\b${f}\\b`).test(implSrc)) fieldMissing.push(f);
|
|
@@ -5182,20 +5451,28 @@ function contractVerifyCmd(specPath, implPath) {
|
|
|
5182
5451
|
// src/*.js의 module.exports를 스캔해서 reuse-map.md에 capability 후보 등록.
|
|
5183
5452
|
function reuseAutodetectCmd(root) {
|
|
5184
5453
|
root = absRoot(root || process.cwd());
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5454
|
+
// 1.9.36 BUG-fix: src/만이 아니라 bin/, lib/, app/도 스캔. require() 대신 정적 분석 (side-effect 차단).
|
|
5455
|
+
const candidateDirs = ['src', 'bin', 'lib', 'app'].filter(d => exists(path.join(root, d)));
|
|
5456
|
+
if (!candidateDirs.length) { fail(`스캔할 디렉토리 없음 (src/, bin/, lib/, app/ 중 하나 필요): ${root}`); return process.exit(1); }
|
|
5188
5457
|
const found = [];
|
|
5189
|
-
for (const
|
|
5190
|
-
const
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5458
|
+
for (const dir of candidateDirs) {
|
|
5459
|
+
const files = fs.readdirSync(path.join(root, dir)).filter(f => f.endsWith('.js'));
|
|
5460
|
+
for (const f of files) {
|
|
5461
|
+
const full = path.join(root, dir, f);
|
|
5462
|
+
const src = read(full);
|
|
5463
|
+
// 정적 분석: module.exports = { foo, bar } / exports.foo = / module.exports.foo =
|
|
5464
|
+
const names = new Set();
|
|
5465
|
+
for (const m of src.matchAll(/module\.exports\s*=\s*\{([^}]+)\}/g)) {
|
|
5466
|
+
for (const k of m[1].split(',')) {
|
|
5467
|
+
const name = k.replace(/:.*/, '').trim();
|
|
5468
|
+
if (/^[A-Za-z_$][\w$]*$/.test(name)) names.add(name);
|
|
5469
|
+
}
|
|
5470
|
+
}
|
|
5471
|
+
for (const m of src.matchAll(/(?:module\.)?exports\.([A-Za-z_$][\w$]*)\s*=/g)) names.add(m[1]);
|
|
5472
|
+
for (const name of names) {
|
|
5473
|
+
if (name.startsWith('_')) continue; // internal helpers 제외
|
|
5474
|
+
found.push({ file: `${dir}/${f}`, name });
|
|
5475
|
+
}
|
|
5199
5476
|
}
|
|
5200
5477
|
}
|
|
5201
5478
|
if (has('--json')) {
|
|
@@ -5281,6 +5558,7 @@ async function main() {
|
|
|
5281
5558
|
if (cmd === 'review') return reviewCmd(arg('--path', process.cwd()), args[1]);
|
|
5282
5559
|
if (cmd === 'agents') return agentsCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
|
|
5283
5560
|
if (cmd === 'contract' && args[1] === 'verify') return contractVerifyCmd(args[2], args[3]);
|
|
5561
|
+
if (cmd === 'drift' && (args[1] === 'check' || !args[1])) return driftCheckCmd(args[2] || arg('--path', process.cwd()));
|
|
5284
5562
|
if (cmd === 'reuse' && args[1] === 'autodetect') return reuseAutodetectCmd(args[2] || arg('--path', process.cwd()));
|
|
5285
5563
|
if (cmd === 'setup-agents' || cmd === 'setup' && args[1] === 'agents') return await setupAgentsCmd(args[1] && args[1] !== 'agents' ? args[1] : (args[2] || process.cwd()));
|
|
5286
5564
|
if (cmd === 'session' && args[1] === 'close') { const r = sessionClose(args[2] || process.cwd()); viewworkEmit(args[2] || process.cwd(), { action: 'task', tool: 'session-close', note: 'session close' }); return r; }
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -950,6 +950,133 @@ total++;
|
|
|
950
950
|
if (!ok) { failed++; console.log(r.stdout.slice(0, 800)); }
|
|
951
951
|
}
|
|
952
952
|
|
|
953
|
+
// 1.9.37 회귀: drift detection
|
|
954
|
+
total++;
|
|
955
|
+
{
|
|
956
|
+
// drift check: 신규 init 직후 (메타파일은 fresh) → healthy 또는 attention
|
|
957
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-drift-'));
|
|
958
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
959
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'drift', 'check', tmpC], { encoding: 'utf8', timeout: 15000 });
|
|
960
|
+
const ok = r.status === 0
|
|
961
|
+
&& /leerness drift check \(1\.9\.37\)/.test(r.stdout)
|
|
962
|
+
&& /(healthy|attention|warning)/.test(r.stdout); // 막 init이라 critical은 안 됨
|
|
963
|
+
console.log(ok ? '✓ B(1.9.37) drift check: 신규 init → healthy/attention 등급' : `✗ drift check 실패`);
|
|
964
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
total++;
|
|
968
|
+
{
|
|
969
|
+
// drift check --json: 점수/신호 구조 검증
|
|
970
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-drift2-'));
|
|
971
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
972
|
+
// 인공적으로 progress-tracker를 옛날 날짜로 만들기 어려우니 신호 갯수만 검증
|
|
973
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'drift', 'check', tmpC, '--json'], { encoding: 'utf8', timeout: 15000 });
|
|
974
|
+
let parsed = null;
|
|
975
|
+
try { parsed = JSON.parse(r.stdout); } catch {}
|
|
976
|
+
const ok = parsed
|
|
977
|
+
&& typeof parsed.score === 'number'
|
|
978
|
+
&& typeof parsed.level === 'string'
|
|
979
|
+
&& Array.isArray(parsed.signals)
|
|
980
|
+
&& parsed.signals.length >= 3; // session-handoff/current-state/progress-tracker 최소
|
|
981
|
+
console.log(ok ? '✓ B(1.9.37) drift check --json: 점수/레벨/신호 구조' : `✗ drift --json 실패`);
|
|
982
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
total++;
|
|
986
|
+
{
|
|
987
|
+
// handoff 자동 drift 경고 — 인공 stale 시뮬 (session-handoff.md의 Last generated를 옛 날짜로)
|
|
988
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-drift3-'));
|
|
989
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
990
|
+
// session-handoff.md에 옛 날짜 주입
|
|
991
|
+
const shPath = path.join(tmpC, '.harness', 'session-handoff.md');
|
|
992
|
+
if (fs.existsSync(shPath)) {
|
|
993
|
+
let body = fs.readFileSync(shPath, 'utf8');
|
|
994
|
+
const oldDate = new Date(Date.now() - 5 * 86400000).toISOString();
|
|
995
|
+
body = body.replace(/Last generated:.*/, `Last generated: ${oldDate}`);
|
|
996
|
+
if (!/Last generated:/.test(body)) body = `Last generated: ${oldDate}\n\n` + body;
|
|
997
|
+
fs.writeFileSync(shPath, body, 'utf8');
|
|
998
|
+
}
|
|
999
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'handoff', tmpC, '--compact'], { encoding: 'utf8', timeout: 15000 });
|
|
1000
|
+
const ok = /leerness drift 감지/.test(r.stdout) && /session close/.test(r.stdout);
|
|
1001
|
+
console.log(ok ? '✓ B(1.9.37) handoff 자동 drift 경고: 5일 stale → 알림 표시' : `✗ handoff drift 경고 실패`);
|
|
1002
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
total++;
|
|
1006
|
+
{
|
|
1007
|
+
// LEERNESS_NO_DRIFT_CHECK=1: 자동 경고 스킵
|
|
1008
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-drift4-'));
|
|
1009
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
1010
|
+
const shPath = path.join(tmpC, '.harness', 'session-handoff.md');
|
|
1011
|
+
if (fs.existsSync(shPath)) {
|
|
1012
|
+
const oldDate = new Date(Date.now() - 5 * 86400000).toISOString();
|
|
1013
|
+
let body = fs.readFileSync(shPath, 'utf8');
|
|
1014
|
+
body = body.replace(/Last generated:.*/, `Last generated: ${oldDate}`);
|
|
1015
|
+
if (!/Last generated:/.test(body)) body = `Last generated: ${oldDate}\n\n` + body;
|
|
1016
|
+
fs.writeFileSync(shPath, body, 'utf8');
|
|
1017
|
+
}
|
|
1018
|
+
const env = { ...process.env, LEERNESS_NO_DRIFT_CHECK: '1' };
|
|
1019
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'handoff', tmpC, '--compact'], { encoding: 'utf8', timeout: 15000, env });
|
|
1020
|
+
const ok = !/leerness drift 감지/.test(r.stdout);
|
|
1021
|
+
console.log(ok ? '✓ B(1.9.37) LEERNESS_NO_DRIFT_CHECK=1: 경고 스킵' : `✗ drift skip 실패`);
|
|
1022
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// 1.9.36 회귀: dispatch 권장 플래그 + bench + 작업 유형 추천
|
|
1026
|
+
total++;
|
|
1027
|
+
{
|
|
1028
|
+
// dispatch --write 시 gemini --yolo 자동 추가
|
|
1029
|
+
const env = { ...process.env, LEERNESS_ENABLE_GEMINI: '1' };
|
|
1030
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'agents', 'dispatch', '코드 분석해서 요약', '--to', 'gemini', '--write'], { encoding: 'utf8', timeout: 15000, env });
|
|
1031
|
+
// gemini가 ready면 명령 출력에 --yolo 포함, 비-ready면 거부 — 둘 다 OK
|
|
1032
|
+
const ok = (r.status === 0 && /--yolo/.test(r.stdout) && /write \(파일 수정 가능\)/.test(r.stdout))
|
|
1033
|
+
|| (r.status !== 0 && /비활성|disabled|not-installed/.test(r.stdout));
|
|
1034
|
+
console.log(ok ? '✓ B(1.9.36) dispatch --write: gemini --yolo 자동 첨부 또는 비활성 거부' : `✗ dispatch --write 실패`);
|
|
1035
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
total++;
|
|
1039
|
+
{
|
|
1040
|
+
// dispatch read-only (기본) — --yolo/--dangerously 같은 위험 플래그 없음
|
|
1041
|
+
const env = { ...process.env, LEERNESS_ENABLE_CLAUDE: '1' };
|
|
1042
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'agents', 'dispatch', '번역해줘', '--to', 'claude'], { encoding: 'utf8', timeout: 15000, env });
|
|
1043
|
+
// claude가 ready면 read-only 표시 + dangerously 플래그 없음
|
|
1044
|
+
const ok = (r.status === 0 && /read-only/.test(r.stdout) && !/--dangerously-skip-permissions/.test(r.stdout))
|
|
1045
|
+
|| (r.status !== 0 && /비활성|disabled|not-installed/.test(r.stdout));
|
|
1046
|
+
console.log(ok ? '✓ B(1.9.36) dispatch read-only 기본: 위험 플래그 미첨부 또는 비활성 거부' : `✗ dispatch read-only 실패`);
|
|
1047
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
total++;
|
|
1051
|
+
{
|
|
1052
|
+
// 작업 유형 추천 — 비활성 CLI에도 추천 메시지 우선 출력
|
|
1053
|
+
const env = { ...process.env, LEERNESS_ENABLE_GEMINI: '0', LEERNESS_ENABLE_CLAUDE: '0' };
|
|
1054
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'agents', 'dispatch', '번역해줘 한국어를 영어로', '--to', 'gemini'], { encoding: 'utf8', timeout: 15000, env });
|
|
1055
|
+
// 번역 → claude 추천. ready 체크 전에 추천 출력 → stdout에 "추천...claude" 포함
|
|
1056
|
+
const ok = /추천.*claude/.test(r.stdout);
|
|
1057
|
+
console.log(ok ? '✓ B(1.9.36) 작업 유형 추천: 번역→claude 추천 (비활성이어도 출력)' : `✗ 추천 실패`);
|
|
1058
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
total++;
|
|
1062
|
+
{
|
|
1063
|
+
// bench 명령: ready CLI 없을 때 거부
|
|
1064
|
+
const env = { ...process.env, LEERNESS_ENABLE_CLAUDE: '0', LEERNESS_ENABLE_CODEX: '0', LEERNESS_ENABLE_GEMINI: '0', LEERNESS_ENABLE_COPILOT: '0' };
|
|
1065
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'agents', 'bench', 'test'], { encoding: 'utf8', timeout: 15000, env });
|
|
1066
|
+
const ok = r.status !== 0 && /ready CLI 없음/.test(r.stdout);
|
|
1067
|
+
console.log(ok ? '✓ B(1.9.36) agents bench: ready 없을 때 거부' : `✗ bench 거부 실패`);
|
|
1068
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 300)); }
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
total++;
|
|
1072
|
+
{
|
|
1073
|
+
// 사용법 메시지에 bench 포함
|
|
1074
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'agents', 'unknown'], { encoding: 'utf8', timeout: 10000 });
|
|
1075
|
+
const ok = r.status !== 0 && /bench/.test(r.stdout + r.stderr);
|
|
1076
|
+
console.log(ok ? '✓ B(1.9.36) agents 사용법에 bench 명시' : `✗ usage bench 실패`);
|
|
1077
|
+
if (!ok) { failed++; console.log((r.stdout + r.stderr).slice(0, 300)); }
|
|
1078
|
+
}
|
|
1079
|
+
|
|
953
1080
|
// 1.9.35 회귀: 5개 신규 기능
|
|
954
1081
|
total++;
|
|
955
1082
|
{
|