leerness 1.9.34 → 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 +62 -0
- package/README.md +28 -11
- package/bin/harness.js +323 -12
- package/package.json +1 -1
- package/scripts/e2e.js +126 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,67 @@
|
|
|
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
|
+
|
|
31
|
+
## 1.9.35 — 2026-05-17
|
|
32
|
+
|
|
33
|
+
**파이프라인 메타-감사에서 도출된 5개 개선 사항 통합**.
|
|
34
|
+
|
|
35
|
+
이전 라운드(1.9.34)에서 멀티 에이전트 오케스트레이션 전체 파이프라인을 메타-검증한 결과 8개 개선점을 도출. 그 중 high-impact 5건을 1.9.35에 즉시 통합.
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- **`leerness contract verify <spec.md> <impl.js>`** (#3) — 사양 ↔ 구현 일치 검사.
|
|
40
|
+
- spec 문서에서 `function fooBar(` / `` `bar(` `` / `tick.<field>` 패턴 추출
|
|
41
|
+
- impl의 `module.exports`와 비교 → 누락된 함수/필드 보고
|
|
42
|
+
- `--json` 출력 지원, exit code 1 if 불일치 (CI 친화)
|
|
43
|
+
- 1.9.34 멀티 에이전트 검증에서 발견한 "tick 페이로드 필드명 불일치" 자동 차단
|
|
44
|
+
- **`leerness reuse autodetect [path]`** (#2) — `src/*.js`의 `module.exports`를 스캔하여 reuse-map.md 후보 자동 등록.
|
|
45
|
+
- `_internal` 헬퍼는 제외 (밑줄로 시작하는 export 자동 필터)
|
|
46
|
+
- `--apply`로 reuse-map.md에 자동 추가, 기본은 dry-run
|
|
47
|
+
- **`leerness audit --fix`** (#5) — 누락된 메타 파일 자동 갱신.
|
|
48
|
+
- `session-handoff.md`의 `Last generated: (자동)` → 실제 타임스탬프
|
|
49
|
+
- `current-state.md`의 stale `Updated` 라인 → today로 갱신
|
|
50
|
+
- `--fix` 미지정 시 기존 경고 동작 유지 (안전한 opt-in)
|
|
51
|
+
- **`handoff <path>` .harness 부재 자동 경고** (#1) — 신규 디렉토리에서 handoff 호출 시 즉시 노랑색 경고 + `leerness init` 명령 안내. `--no-init-check` 또는 `--all-apps` 시 스킵.
|
|
52
|
+
- **`agents dispatch` 안내문에 안전 규칙 추가** (#4) — 멀티 에이전트 분배 시 파일 경로 격리, mtime 검증 요구, contract verify 권장을 안내문에 자동 포함.
|
|
53
|
+
|
|
54
|
+
### Policy
|
|
55
|
+
- ✅ 모든 신규 명령은 기본 read-only · destructive 동작은 명시적 플래그(`--fix`, `--apply`) 필요
|
|
56
|
+
- ✅ 1.9.34 멀티 에이전트 검증의 모범 사례(파일 경로 격리, mtime 자기 검증, 사양 사전 합의)를 도구로 코드화
|
|
57
|
+
- ❌ 자동 init은 destructive이므로 자동 실행 안 함 — 사용자에게 명령만 안내
|
|
58
|
+
|
|
59
|
+
### 실측 (이번 라운드)
|
|
60
|
+
- 메타-감사 보고서: `_reports/PIPELINE_META_AUDIT.md` (10 phases, 8 개선점)
|
|
61
|
+
- rpg-replay 통합 패치: 회귀 0건 · 128/128 PASS · BUG-A/B/C 모두 해결 (별도 라운드)
|
|
62
|
+
- contract verify 실 사용 사례: format.js에 spec의 `tick.effect` 필드 없음 발견
|
|
63
|
+
- e2e: 161/161 PASS (1.9.34 156 + 신규 5)
|
|
64
|
+
|
|
3
65
|
## 1.9.34 — 2026-05-16
|
|
4
66
|
|
|
5
67
|
**방향키 + 스페이스 인터랙티브 multi-select + 256색 그라데이션 배너 + 멀티레벨 sub-agent 오케스트레이션 검증**.
|
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# Leerness
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> **AI 에이전트 검수·다중 협업 CLI 하네스** — 거짓 완료 차단, 멀티 에이전트 오케스트레이션, 사양 ↔ 구현 일치 검증, 워크스페이스 통합 가시성. **한국어 우선**.
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/leerness) [](https://www.npmjs.com/package/leerness) []() []() []()
|
|
6
|
+
|
|
7
|
+
> Claude Code · Cursor · Copilot · Codex · Gemini CLI — 어떤 AI 에이전트로 코드를 짜든, leerness는 **"진짜로 했나? 중복 아닌가? 합의된 사양인가?"** 를 자동으로 검수합니다.
|
|
6
8
|
|
|
7
9
|
```
|
|
8
10
|
╔══════════════════════════════════════════════════════════════╗
|
|
@@ -22,19 +24,24 @@
|
|
|
22
24
|
|
|
23
25
|
---
|
|
24
26
|
|
|
25
|
-
## 🤔 leerness가
|
|
27
|
+
## 🤔 leerness가 해결하는 문제
|
|
26
28
|
|
|
27
|
-
AI 에이전트(Claude Code, Cursor, Copilot, Codex, Gemini CLI 등)
|
|
29
|
+
AI 에이전트(Claude Code, Cursor, Copilot, Codex, Gemini CLI 등)는 **빠르게 코드를 쓰지만, 다음 함정에 반복해서 빠집니다**:
|
|
28
30
|
|
|
29
|
-
| AI와 함께 일할 때 흔한
|
|
31
|
+
| AI와 함께 일할 때 흔한 함정 | leerness의 해결책 |
|
|
30
32
|
|---|---|
|
|
31
33
|
| 🚨 "완료했습니다"라고 했지만 실제로 코드에 변경이 없음 | `verify-claim --run-tests` — 증거 파일 존재 + 테스트 실제 실행 검증 |
|
|
32
34
|
| 🚨 "API 호출 완료" 라고 했지만 코드에 URL이 없음 | `optimism-check` — 10 카테고리 패턴 + URL 매핑 + 신뢰도 점수 |
|
|
33
35
|
| 🚨 같은 함수를 여러 프로젝트에 중복 생성 | `reuse-map --all-apps --strict-elements` — 함수명 fuzzy 중복 감지 |
|
|
34
|
-
| 🚨 다음 세션이 컨텍스트를 잃음 | `handoff` —
|
|
36
|
+
| 🚨 다음 세션이 컨텍스트를 잃음 | `handoff` — plan/progress/decisions 3채널 자동 적재 + `--compact` (500자) |
|
|
35
37
|
| 🚨 표면적 코드 리뷰 (도메인 깊이 부족) | `review --persona security,performance,ux` — 5 도메인 sub-agent 자동 부여 |
|
|
36
|
-
| 🚨 외부 AI CLI를
|
|
37
|
-
| 🚨 npx 캐시로 옛 버전이 실행됨 | `
|
|
38
|
+
| 🚨 외부 AI CLI를 자동 호출하면 위험 | `agents list/dispatch/quota` — 환경변수 활성화 + 명시적 분배 (자동 호출 X) |
|
|
39
|
+
| 🚨 npx 캐시로 옛 버전이 실행됨 | `_warnIfStale` — install 시 npm latest 자동 비교 + 경고 (1.9.33+) |
|
|
40
|
+
| 🚨 멀티 sub-agent가 같은 파일을 동시에 써서 작업 손실 | 프롬프트 템플릿에 **파일 경로 격리 의무**, `agents dispatch` 안내문이 자동 추가 (1.9.34/35) |
|
|
41
|
+
| 🚨 sub-agent마다 사양 해석이 달라 통합 시 BUG 양산 | **`leerness contract verify <spec.md> <impl.js>`** — 명세 ↔ 구현 함수/필드 자동 검사 (1.9.35) |
|
|
42
|
+
| 🚨 신규 모듈의 capability가 reuse-map에 등록 안 됨 | **`leerness reuse autodetect`** — `module.exports` 스캔 + 자동 등록 (1.9.35) |
|
|
43
|
+
| 🚨 신규 디렉토리에서 sub-agent가 컨텍스트 없이 작업 시작 | `handoff` 호출 시 **.harness 부재 자동 경고** (1.9.35) |
|
|
44
|
+
| 🚨 audit warning이 쌓이지만 수동 fix가 번거로움 | **`audit --fix`** — session-handoff/current-state 자동 갱신 (1.9.35) |
|
|
38
45
|
|
|
39
46
|
---
|
|
40
47
|
|
|
@@ -263,12 +270,16 @@ leerness brainstorm "키워드" --all-apps --include-code
|
|
|
263
270
|
leerness deps <capability> --run-tests # 영향 추적 + 자동 회귀
|
|
264
271
|
```
|
|
265
272
|
|
|
266
|
-
### 외부 AI CLI (1.9.30~
|
|
273
|
+
### 외부 AI CLI · 멀티 에이전트 (1.9.30~35)
|
|
267
274
|
```bash
|
|
268
275
|
leerness setup-agents . # 인터랙티브 활성화 + 자동 설치
|
|
269
276
|
leerness agents list # 4 CLI 상태표
|
|
270
277
|
leerness agents quota # 사용량 추정
|
|
271
|
-
leerness agents dispatch "<task>" --to gemini #
|
|
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)
|
|
282
|
+
leerness audit . --fix # 1.9.35 누락 메타 자동 갱신
|
|
272
283
|
```
|
|
273
284
|
|
|
274
285
|
### 페르소나·리뷰 (1.9.29)
|
|
@@ -379,6 +390,10 @@ leerness update --from <tgz> # 오프라인/사내 미러
|
|
|
379
390
|
| `agents dispatch --to X` | ready CLI에 대상 명령 자동 생성 | 1.9.30 |
|
|
380
391
|
| `agents quota` | provider별 사용량/한도 추정 | 1.9.31 |
|
|
381
392
|
| `setup-agents` | 인터랙티브 CLI 활성화 + 자동 설치 시도 | 1.9.32 |
|
|
393
|
+
| `contract verify <spec> <impl>` | 명세 ↔ 구현 함수/필드 일치 자동 검사 | 1.9.35 |
|
|
394
|
+
| `reuse autodetect [--apply]` | `module.exports` 스캔 → reuse-map 후보 등록 | 1.9.35 |
|
|
395
|
+
| `audit --fix` | session-handoff/current-state 자동 갱신 | 1.9.35 |
|
|
396
|
+
| `handoff` (init 부재 경고) | 신규 디렉토리에서 즉시 init 안내 | 1.9.35 |
|
|
382
397
|
| `_warnIfStale` (자동) | npx 캐시 옛 버전 자동 경고 | 1.9.33 |
|
|
383
398
|
| `_selectOne/_selectMany` | 방향키/Space 인터랙티브 multi-select | 1.9.34 |
|
|
384
399
|
|
|
@@ -450,12 +465,14 @@ A. 256색 ANSI를 지원하지 않는 환경입니다. `LEERNESS_NO_INTERACTIVE=
|
|
|
450
465
|
npm test # = node ./scripts/e2e.js
|
|
451
466
|
```
|
|
452
467
|
|
|
453
|
-
**
|
|
468
|
+
**161/161 시나리오** 통과 (1.9.7~1.9.35 회귀 + 신규 검증).
|
|
454
469
|
|
|
455
470
|
---
|
|
456
471
|
|
|
457
472
|
## 📜 변경 이력 (최근)
|
|
458
473
|
|
|
474
|
+
- **1.9.36** — 외부 AI CLI 오케스트레이션 강화: `agents bench` (3 CLI 동시 비교) + `dispatch --write` 자동 권장 플래그 + 작업 유형 키워드 추천. stress test에서 발견한 `contract verify` require() side-effect (보안 위험 + 25× 속도 회복) 즉시 수정.
|
|
475
|
+
- **1.9.35** — 파이프라인 메타-감사에서 도출된 5개 개선 통합: `contract verify` · `reuse autodetect` · `audit --fix` · `handoff` init 부재 경고 · `agents dispatch` 안전 규칙 안내.
|
|
459
476
|
- **1.9.34** — 방향키/스페이스 인터랙티브 multi-select (`_selectOne`/`_selectMany`) + 256색 그라데이션 배너 + 3단계 sub-agent 오케스트레이션 검증 (2.2× 효율 실측).
|
|
460
477
|
- **1.9.33** — `_warnIfStale()` — `npx leerness init` 시 옛 버전 자동 경고 + 해결 안내.
|
|
461
478
|
- **1.9.32** — ASCII 배너 + `setup-agents` 인터랙티브 설정 + 미설치 CLI 자동 설치 시도.
|
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 -->';
|
|
@@ -960,6 +960,9 @@ function debug(root) {
|
|
|
960
960
|
function audit(root) {
|
|
961
961
|
root = absRoot(root);
|
|
962
962
|
let warnings = 0, failures = 0;
|
|
963
|
+
// 1.9.35 개선 #5: --fix 옵션 — 자동 수정 가능한 항목 적용
|
|
964
|
+
const fix = has('--fix');
|
|
965
|
+
let fixed = 0;
|
|
963
966
|
const designCands = ['designguide.md','design-guide.md','docs/designguide.md','docs/design-guide.md','.harness/designguide.md'];
|
|
964
967
|
const dups = designCands.filter(f => exists(path.join(root,f)));
|
|
965
968
|
if (dups.length) { warnings++; warn(`design guide duplicates outside canonical: ${dups.join(', ')} (run: leerness consistency merge-design-guide)`); }
|
|
@@ -991,16 +994,34 @@ function audit(root) {
|
|
|
991
994
|
}
|
|
992
995
|
else if (milestoneIds.length) ok('all milestones linked in progress-tracker');
|
|
993
996
|
const handoff = exists(handoffPath(root)) ? read(handoffPath(root)) : '';
|
|
994
|
-
if (handoff.includes('Last generated: (자동)')) {
|
|
997
|
+
if (handoff.includes('Last generated: (자동)')) {
|
|
998
|
+
warnings++; warn('session-handoff.md never auto-generated (run: leerness session close .)');
|
|
999
|
+
// 1.9.35 #5: --fix → session-handoff.md 자동 생성 마커 갱신
|
|
1000
|
+
if (fix) {
|
|
1001
|
+
const stamped = handoff.replace('Last generated: (자동)', `Last generated: ${today()} (leerness audit --fix)`);
|
|
1002
|
+
writeUtf8(handoffPath(root), stamped);
|
|
1003
|
+
ok(' ↳ fixed: session-handoff.md timestamp 갱신');
|
|
1004
|
+
fixed++;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
995
1007
|
else if (handoff.includes('Last generated:')) ok('session-handoff.md auto-generated previously');
|
|
996
1008
|
const cur = exists(currentStatePath(root)) ? read(currentStatePath(root)) : '';
|
|
997
1009
|
const updMatch = cur.match(/Updated: (\d{4}-\d{2}-\d{2})/);
|
|
998
1010
|
if (updMatch) {
|
|
999
1011
|
const dDays = (Date.now() - new Date(updMatch[1]).getTime()) / 86400000;
|
|
1000
|
-
if (dDays > 7) {
|
|
1012
|
+
if (dDays > 7) {
|
|
1013
|
+
warnings++; warn(`current-state.md stale (${Math.round(dDays)} days)`);
|
|
1014
|
+
// 1.9.35 #5: --fix → current-state.md Updated 라인 갱신
|
|
1015
|
+
if (fix) {
|
|
1016
|
+
const stamped = cur.replace(/Updated: \d{4}-\d{2}-\d{2}/, `Updated: ${today()}`);
|
|
1017
|
+
writeUtf8(currentStatePath(root), stamped);
|
|
1018
|
+
ok(' ↳ fixed: current-state.md Updated 갱신');
|
|
1019
|
+
fixed++;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1001
1022
|
else ok('current-state.md fresh');
|
|
1002
1023
|
}
|
|
1003
|
-
log(`Audit summary: warnings=${warnings} failures=${failures}`);
|
|
1024
|
+
log(`Audit summary: warnings=${warnings} failures=${failures}${fix ? ` fixed=${fixed}` : ''}`);
|
|
1004
1025
|
if (failures) process.exitCode = 1;
|
|
1005
1026
|
}
|
|
1006
1027
|
|
|
@@ -1411,6 +1432,24 @@ function handoffCmd(root) {
|
|
|
1411
1432
|
if (has('--all-apps') || arg('--include', null)) {
|
|
1412
1433
|
return _handoffWorkspace(absRoot(root));
|
|
1413
1434
|
}
|
|
1435
|
+
// 1.9.35 개선 #1: .harness 부재 시 즉시 경고 (자동 init 권장)
|
|
1436
|
+
// 사용자가 신규 디렉토리에서 handoff 호출 시 sub-agent 작업이 길을 잃지 않도록.
|
|
1437
|
+
const absR = absRoot(root || process.cwd());
|
|
1438
|
+
if (!exists(path.join(absR, '.harness')) && !has('--no-init-check')) {
|
|
1439
|
+
const isTty = process.stdout && process.stdout.isTTY;
|
|
1440
|
+
const yel = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
|
|
1441
|
+
const dim = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
1442
|
+
log('');
|
|
1443
|
+
log(yel(' ⚠ leerness init 미실행 디렉토리'));
|
|
1444
|
+
log(dim(' ' + absR));
|
|
1445
|
+
log(dim(' handoff가 표시할 컨텍스트(plan/progress/decisions)가 없습니다.'));
|
|
1446
|
+
log('');
|
|
1447
|
+
log(dim(' 해결:'));
|
|
1448
|
+
log(' ' + yel(`leerness init "${absR}" --yes --language ko`));
|
|
1449
|
+
log('');
|
|
1450
|
+
log(dim(' (--no-init-check 로 끄기)'));
|
|
1451
|
+
log('');
|
|
1452
|
+
}
|
|
1414
1453
|
return handoff(root);
|
|
1415
1454
|
}
|
|
1416
1455
|
|
|
@@ -2330,6 +2369,30 @@ const EXTERNAL_AGENTS = [
|
|
|
2330
2369
|
installCmd: 'gh extension install github/gh-copilot', installHint: 'https://github.com/github/gh-copilot (gh CLI 선행 설치 필요)' }
|
|
2331
2370
|
];
|
|
2332
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
|
+
|
|
2333
2396
|
function _checkAgent(agent, opts = {}) {
|
|
2334
2397
|
const enabled = process.env[agent.envFlag] === '1';
|
|
2335
2398
|
// PATH 존재 확인 (which / where)
|
|
@@ -2767,35 +2830,139 @@ function agentsCmd(root, sub, ...args) {
|
|
|
2767
2830
|
if (!target) { fail('--to <agent_id> 필요 (claude/codex/gemini/copilot)'); return process.exit(1); }
|
|
2768
2831
|
const agentDef = EXTERNAL_AGENTS.find(a => a.id === target);
|
|
2769
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
|
+
}
|
|
2770
2839
|
const status = _checkAgent(agentDef);
|
|
2771
2840
|
if (status.status !== 'ready') {
|
|
2772
2841
|
fail(`${target} 비활성 (${status.status}). 환경변수 ${agentDef.envFlag}=1 + CLI 설치 필요.`);
|
|
2773
2842
|
return process.exit(1);
|
|
2774
2843
|
}
|
|
2844
|
+
// 1.9.36: --write 시 파일 수정 가능 권장 플래그 자동 첨부, 미명시 시 read-only 안전 모드
|
|
2845
|
+
const writeMode = has('--write');
|
|
2846
|
+
const readOnly = has('--readonly') || !writeMode;
|
|
2775
2847
|
// 실제 호출은 안 함 — 프롬프트만 생성 (사용자가 명시적으로 실행)
|
|
2776
|
-
log(`# leerness agents dispatch (1.9.
|
|
2848
|
+
log(`# leerness agents dispatch (1.9.36)`);
|
|
2777
2849
|
log(`대상: ${target} (${agentDef.bin})`);
|
|
2778
2850
|
log(`상태: 🟢 ready, 버전 ${status.version || '?'}`);
|
|
2851
|
+
log(`모드: ${writeMode ? '✏ write (파일 수정 가능)' : '🔒 read-only (분석 전용, 안전)'}`);
|
|
2779
2852
|
log('');
|
|
2780
2853
|
log(`## 실행 명령 (사용자가 복사해서 실행)`);
|
|
2781
2854
|
log('');
|
|
2855
|
+
const q = task.replace(/"/g, '\\"');
|
|
2782
2856
|
if (target === 'claude') {
|
|
2783
|
-
|
|
2857
|
+
const flags = writeMode ? '--print --dangerously-skip-permissions' : '--print';
|
|
2858
|
+
log(`claude ${flags} "${q}"`);
|
|
2859
|
+
if (writeMode) log(`# ⚠ --dangerously-skip-permissions: 도구 권한 자동 승인 (파일 수정 가능)`);
|
|
2784
2860
|
} else if (target === 'codex') {
|
|
2785
|
-
|
|
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 우회`);
|
|
2786
2865
|
} else if (target === 'gemini') {
|
|
2787
|
-
|
|
2866
|
+
const flags = writeMode ? '-p --yolo' : '-p';
|
|
2867
|
+
log(`gemini ${flags} "${q}"`);
|
|
2868
|
+
if (writeMode) log(`# ⚠ --yolo: 워크스페이스 파일 직접 수정 가능`);
|
|
2788
2869
|
} else if (target === 'copilot') {
|
|
2789
|
-
log(`gh copilot suggest "${
|
|
2870
|
+
log(`gh copilot suggest "${q}"`);
|
|
2790
2871
|
}
|
|
2791
2872
|
log('');
|
|
2792
|
-
log(`## 정책 (1.9.
|
|
2873
|
+
log(`## 정책 (1.9.36)`);
|
|
2793
2874
|
log(` - leerness는 외부 CLI를 자동 호출하지 않음 (사용자 명시적 실행)`);
|
|
2794
2875
|
log(` - 메인 에이전트(Claude)가 위 명령을 보고 sub-agent로 spawn 가능`);
|
|
2795
2876
|
log(` - quota 체크: \`leerness agents quota\` (1.9.31+)`);
|
|
2877
|
+
log(` - 동시 호출 시: \`leerness agents bench "<task>"\` (1.9.36)`);
|
|
2878
|
+
log('');
|
|
2879
|
+
log(`## 분배 시 안전 규칙 (1.9.35)`);
|
|
2880
|
+
log(` - sub-agent 프롬프트에 "당신만 수정할 파일 경로"를 명시 (파일 경로 격리)`);
|
|
2881
|
+
log(` - sub-agent에 "보고 시 \`stat <file>\` 또는 mtime 확인 결과 첨부" 요구 (자기 격리 검증)`);
|
|
2882
|
+
log(` - 사양 사전 정의 (예: TICK_SPEC.md) → \`leerness contract verify\`로 사후 검증`);
|
|
2883
|
+
log(` - 같은 파일 동시 쓰기는 last-writer-wins 위험 (1.9.34 검증)`);
|
|
2796
2884
|
return;
|
|
2797
2885
|
}
|
|
2798
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
|
+
|
|
2799
2966
|
if (sub === 'quota') {
|
|
2800
2967
|
// 1.9.31: 각 CLI 사용량/쿼터 추정 + provider 대시보드 링크
|
|
2801
2968
|
const results = [];
|
|
@@ -2858,7 +3025,7 @@ function agentsCmd(root, sub, ...args) {
|
|
|
2858
3025
|
return;
|
|
2859
3026
|
}
|
|
2860
3027
|
|
|
2861
|
-
fail('사용법: leerness agents list|check|quota|dispatch "<task>" --to <id>');
|
|
3028
|
+
fail('사용법: leerness agents list|check|quota|dispatch|bench [--write] "<task>" [--to <id>]');
|
|
2862
3029
|
return process.exit(1);
|
|
2863
3030
|
}
|
|
2864
3031
|
|
|
@@ -5051,8 +5218,150 @@ function viewworkInstall(root) {
|
|
|
5051
5218
|
ok('claude .claude/settings.local.json updated (Stop hook adds a viewwork event)');
|
|
5052
5219
|
}
|
|
5053
5220
|
|
|
5221
|
+
// 1.9.35 개선 #3: contract verify <spec.md> <impl.js>
|
|
5222
|
+
// 사양 문서(spec.md)에 명시된 함수 이름이 실제 module.exports에 모두 있는지 검사.
|
|
5223
|
+
// 사용 예: leerness contract verify TICK_SPEC.md src/format.js
|
|
5224
|
+
function contractVerifyCmd(specPath, implPath) {
|
|
5225
|
+
if (!specPath || !implPath) { fail('사용법: leerness contract verify <spec.md> <impl.js>'); return process.exit(1); }
|
|
5226
|
+
const spec = absRoot('.') + path.sep; // dummy to avoid abs
|
|
5227
|
+
const specFile = path.resolve(specPath);
|
|
5228
|
+
const implFile = path.resolve(implPath);
|
|
5229
|
+
if (!exists(specFile)) { fail(`spec 파일 없음: ${specFile}`); return process.exit(1); }
|
|
5230
|
+
if (!exists(implFile)) { fail(`impl 파일 없음: ${implFile}`); return process.exit(1); }
|
|
5231
|
+
const specText = read(specFile);
|
|
5232
|
+
// spec에서 함수 이름 추출:
|
|
5233
|
+
// `function fooBar(...)` 형태 (markdown 코드블럭 내 JS)
|
|
5234
|
+
// 또는 `**fooBar**` (한국어 문서에서 함수명 강조)
|
|
5235
|
+
// 또는 `tick.amount` (필드명)
|
|
5236
|
+
const fnSpec = new Set();
|
|
5237
|
+
const fieldSpec = new Set();
|
|
5238
|
+
// function 시그니처
|
|
5239
|
+
for (const m of specText.matchAll(/function\s+([A-Za-z_$][\w$]*)\s*\(/g)) fnSpec.add(m[1]);
|
|
5240
|
+
// backtick에 싸인 함수 호출 같은 형태: `xxx(`
|
|
5241
|
+
for (const m of specText.matchAll(/`([A-Za-z_$][\w$]*)\s*\(/g)) fnSpec.add(m[1]);
|
|
5242
|
+
// 필드: tick.<name>
|
|
5243
|
+
for (const m of specText.matchAll(/tick\.([A-Za-z_$][\w$]*)/g)) fieldSpec.add(m[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);
|
|
5247
|
+
const implExports = new Set();
|
|
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
|
+
}
|
|
5254
|
+
}
|
|
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에 없는 것
|
|
5259
|
+
const missing = [];
|
|
5260
|
+
for (const fn of fnSpec) {
|
|
5261
|
+
if (implExports.has(fn)) continue;
|
|
5262
|
+
// spec에 'function fnName('이 있지만 impl exports에 없으면 미구현
|
|
5263
|
+
if (specText.includes(`function ${fn}`) && !implExports.has(fn)) missing.push(fn);
|
|
5264
|
+
}
|
|
5265
|
+
const fieldMissing = [];
|
|
5266
|
+
for (const f of fieldSpec) {
|
|
5267
|
+
if (!new RegExp(`\\b${f}\\b`).test(implSrc)) fieldMissing.push(f);
|
|
5268
|
+
}
|
|
5269
|
+
// 출력
|
|
5270
|
+
if (has('--json')) {
|
|
5271
|
+
log(JSON.stringify({
|
|
5272
|
+
spec: specFile, impl: implFile,
|
|
5273
|
+
specFunctions: [...fnSpec], specFields: [...fieldSpec],
|
|
5274
|
+
implExports: [...implExports],
|
|
5275
|
+
missingFunctions: missing, missingFields: fieldMissing,
|
|
5276
|
+
ok: missing.length === 0 && fieldMissing.length === 0
|
|
5277
|
+
}, null, 2));
|
|
5278
|
+
return;
|
|
5279
|
+
}
|
|
5280
|
+
log(`# leerness contract verify (1.9.35)`);
|
|
5281
|
+
log(`spec: ${rel(process.cwd(), specFile)}`);
|
|
5282
|
+
log(`impl: ${rel(process.cwd(), implFile)}`);
|
|
5283
|
+
log(``);
|
|
5284
|
+
log(`spec 명시 함수: ${[...fnSpec].join(', ') || '(없음)'}`);
|
|
5285
|
+
log(`spec 명시 필드: ${[...fieldSpec].join(', ') || '(없음)'}`);
|
|
5286
|
+
log(`impl exports: ${[...implExports].join(', ') || '(없음)'}`);
|
|
5287
|
+
log(``);
|
|
5288
|
+
if (missing.length) {
|
|
5289
|
+
log(`✗ 누락된 함수 (${missing.length}건):`);
|
|
5290
|
+
for (const m of missing) log(` - ${m}`);
|
|
5291
|
+
} else log(`✓ 모든 spec 함수가 impl에 존재`);
|
|
5292
|
+
if (fieldMissing.length) {
|
|
5293
|
+
log(`✗ 누락된 필드 (${fieldMissing.length}건):`);
|
|
5294
|
+
for (const m of fieldMissing) log(` - tick.${m}`);
|
|
5295
|
+
} else log(`✓ 모든 spec 필드가 impl 소스에 존재`);
|
|
5296
|
+
const ok = missing.length === 0 && fieldMissing.length === 0;
|
|
5297
|
+
log('');
|
|
5298
|
+
log(ok ? '✅ contract OK' : '❌ contract 불일치');
|
|
5299
|
+
if (!ok) process.exitCode = 1;
|
|
5300
|
+
}
|
|
5301
|
+
|
|
5302
|
+
// 1.9.35 개선 #2: reuse autodetect [path]
|
|
5303
|
+
// src/*.js의 module.exports를 스캔해서 reuse-map.md에 capability 후보 등록.
|
|
5304
|
+
function reuseAutodetectCmd(root) {
|
|
5305
|
+
root = absRoot(root || process.cwd());
|
|
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); }
|
|
5309
|
+
const found = [];
|
|
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
|
+
}
|
|
5328
|
+
}
|
|
5329
|
+
}
|
|
5330
|
+
if (has('--json')) {
|
|
5331
|
+
log(JSON.stringify({ project: path.basename(root), found }, null, 2));
|
|
5332
|
+
return;
|
|
5333
|
+
}
|
|
5334
|
+
log(`# leerness reuse autodetect (1.9.35)`);
|
|
5335
|
+
log(`project: ${path.basename(root)}`);
|
|
5336
|
+
log(`발견된 capability 후보: ${found.length}건`);
|
|
5337
|
+
log('');
|
|
5338
|
+
log('| Capability | Where | Kind | Note |');
|
|
5339
|
+
log('|---|---|---|---|');
|
|
5340
|
+
for (const c of found) log(`| ${c.name} | ${c.file} | util | (autodetect from module.exports) |`);
|
|
5341
|
+
log('');
|
|
5342
|
+
if (has('--apply')) {
|
|
5343
|
+
// reuse-map.md에 추가 (헤더 보존 + 후보 라인 append)
|
|
5344
|
+
const reusePath = path.join(root, '.harness', 'reuse-map.md');
|
|
5345
|
+
if (!exists(reusePath)) {
|
|
5346
|
+
fail(`.harness/reuse-map.md 없음 — leerness init 먼저 실행`);
|
|
5347
|
+
return process.exit(1);
|
|
5348
|
+
}
|
|
5349
|
+
let body = read(reusePath);
|
|
5350
|
+
let added = 0;
|
|
5351
|
+
for (const c of found) {
|
|
5352
|
+
if (body.includes(`| ${c.name} |`)) continue; // 이미 있음
|
|
5353
|
+
body += `| ${c.name} | ${c.file} | util | autodetect 1.9.35 |\n`;
|
|
5354
|
+
added++;
|
|
5355
|
+
}
|
|
5356
|
+
writeUtf8(reusePath, body);
|
|
5357
|
+
log(`✓ ${added}건 reuse-map.md에 추가됨`);
|
|
5358
|
+
} else {
|
|
5359
|
+
log(`(--apply 로 reuse-map.md에 자동 추가)`);
|
|
5360
|
+
}
|
|
5361
|
+
}
|
|
5362
|
+
|
|
5054
5363
|
function help() {
|
|
5055
|
-
log(`Leerness v${VERSION}\n\nUsage:\n leerness init [path] [--language auto|ko|en] [--skills recommended|all|a,b]\n leerness migrate [path] [--dry-run] [--force]\n leerness update [path] [--check|--yes|--force|--from <tarball>]\n leerness auto-update install [path]\n leerness status [path]\n leerness verify [path]\n leerness debug [path]\n leerness audit [path]\n leerness check [path]\n leerness scan secrets [path]\n leerness encoding check [path]\n leerness lazy detect [path]\n leerness memory search "query" [--limit 5]\n leerness handoff [path] [--all-apps] [--include p1,p2] [--since 24h|3d] [--compact] [--json] # 1.9.17-22 워크스페이스 (--compact: LLM 시스템 프롬프트용 1줄 요약)\n leerness orchestrate "<목표>" [--agents N] [--model qwen2.5:7b-instruct] [--retry-on-fail K] # 1.9.22 Ollama opt-in (LEERNESS_OLLAMA_BASE_URL 필요)\n leerness llm-bench record --score N --model X [--label L] [--tokens T] # 1.9.22 LLM 벤치 히스토리 누적\n leerness deps <capability> [--run-tests] [--json] # 1.9.24 depends-on 역방향 추적 + 자동 회귀 sweep\n leerness memory search "키" [--include-code] # 1.9.25 소스 코드 본문도 검색 (모순 감지 핵심)\n leerness brainstorm "주제" [--include-code] # 1.9.25 코드 본문 hits 포함\n leerness register-pending "<요청>" [--agent X] [--note Y] # 1.9.25 다중 세션 in-progress 즉시 등록\n leerness optimism-check <T-ID> [--json] # 1.9.26/27 낙관적 표시 감지 (1.9.27: 10 카테고리 + URL/메서드 매핑 + 신뢰도 점수)\n leerness persona list|show <id>|add <id> # 1.9.29 페르소나 카탈로그 (보안/성능/UX/testing/docs 5종 내장)\n leerness review <file> --persona <id1,id2,...> # 1.9.29 도메인 페르소나 리뷰 프롬프트 자동 생성\n leerness agents list|check|quota # 1.9.30/31 외부 AI CLI 가용성 + quota 추정 (claude/codex/gemini/copilot)\n leerness agents dispatch "<task>" --to <id> # 1.9.30 활성 CLI 대상 실행 명령 생성 (실 호출 X, 사용자 실행)\n leerness setup-agents [path] [--yes|--no-setup-agents] # 1.9.32 sub-agent CLI 인터랙티브 설정 (.env + 미설치 자동 설치)\n leerness init [path] [--no-stale-check] # 1.9.33 npx 캐시 함정 — 옛 버전 자동 경고 (끄려면 --no-stale-check)\n leerness verify-claim <T-ID> ... [--strict-claims] # 1.9.26 verify-claim에 낙관적 표시 자동 검사 통합\n leerness reuse-map [path] [--all-apps] [--include p1,p2] [--strict-elements] [--json] # 1.9.18 중복/잠재중복/depends-on\n leerness verify-claim <T-ID> [--path .] [--run-tests] [--json] # 1.9.18-20 evidence 자동 검증 (1.9.20: scenes/scripts 등 도메인 폴더 + jest/mocha 파싱)\n leerness verify-code [path] [--build] [--bench] # 1.9.20 --bench: scripts.bench 추가 실행 + evidence 누적\n leerness session close [path]\n leerness viewwork install [path]\n leerness viewwork emit [path] [--action a] [--note n] [--agent x] [--tool t]\n leerness route <task-type>\n leerness self check [path]\n leerness readme sync [path]\n leerness consistency check [path]\n leerness consistency merge-design-guide [path]\n leerness plan show|init|add|drop|progress|sync [args]\n leerness task list|add|update|drop|fix-evidence|relink [args]\n leerness skill list|info <name>\n leerness skill learn <id> --doc <url> --command "..." --capability "..." [--note ...]\n leerness skill use <id> [--note ...]\n leerness skill optimize <id> --before "..." --after "..." [--note ...]\n leerness skill remove <id>\n leerness skill consolidate [--threshold 0.3]\n leerness gate [path] # verify+audit+scan+encoding+lazy
|
|
5364
|
+
log(`Leerness v${VERSION}\n\nUsage:\n leerness init [path] [--language auto|ko|en] [--skills recommended|all|a,b]\n leerness migrate [path] [--dry-run] [--force]\n leerness update [path] [--check|--yes|--force|--from <tarball>]\n leerness auto-update install [path]\n leerness status [path]\n leerness verify [path]\n leerness debug [path]\n leerness audit [path]\n leerness check [path]\n leerness scan secrets [path]\n leerness encoding check [path]\n leerness lazy detect [path]\n leerness memory search "query" [--limit 5]\n leerness handoff [path] [--all-apps] [--include p1,p2] [--since 24h|3d] [--compact] [--json] # 1.9.17-22 워크스페이스 (--compact: LLM 시스템 프롬프트용 1줄 요약)\n leerness orchestrate "<목표>" [--agents N] [--model qwen2.5:7b-instruct] [--retry-on-fail K] # 1.9.22 Ollama opt-in (LEERNESS_OLLAMA_BASE_URL 필요)\n leerness llm-bench record --score N --model X [--label L] [--tokens T] # 1.9.22 LLM 벤치 히스토리 누적\n leerness deps <capability> [--run-tests] [--json] # 1.9.24 depends-on 역방향 추적 + 자동 회귀 sweep\n leerness memory search "키" [--include-code] # 1.9.25 소스 코드 본문도 검색 (모순 감지 핵심)\n leerness brainstorm "주제" [--include-code] # 1.9.25 코드 본문 hits 포함\n leerness register-pending "<요청>" [--agent X] [--note Y] # 1.9.25 다중 세션 in-progress 즉시 등록\n leerness optimism-check <T-ID> [--json] # 1.9.26/27 낙관적 표시 감지 (1.9.27: 10 카테고리 + URL/메서드 매핑 + 신뢰도 점수)\n leerness persona list|show <id>|add <id> # 1.9.29 페르소나 카탈로그 (보안/성능/UX/testing/docs 5종 내장)\n leerness review <file> --persona <id1,id2,...> # 1.9.29 도메인 페르소나 리뷰 프롬프트 자동 생성\n leerness agents list|check|quota # 1.9.30/31 외부 AI CLI 가용성 + quota 추정 (claude/codex/gemini/copilot)\n leerness agents dispatch "<task>" --to <id> # 1.9.30 활성 CLI 대상 실행 명령 생성 (실 호출 X, 사용자 실행)\n leerness setup-agents [path] [--yes|--no-setup-agents] # 1.9.32 sub-agent CLI 인터랙티브 설정 (.env + 미설치 자동 설치)\n leerness init [path] [--no-stale-check] # 1.9.33 npx 캐시 함정 — 옛 버전 자동 경고 (끄려면 --no-stale-check)\n leerness contract verify <spec.md> <impl.js> [--json] # 1.9.35 명세 ↔ 구현 일치 검사 (함수/필드)\n leerness reuse autodetect [path] [--apply] [--json] # 1.9.35 src/*.js의 module.exports → reuse-map 후보 등록\n leerness audit [path] [--fix] # 1.9.35 --fix: session-handoff/current-state 자동 갱신\n leerness verify-claim <T-ID> ... [--strict-claims] # 1.9.26 verify-claim에 낙관적 표시 자동 검사 통합\n leerness reuse-map [path] [--all-apps] [--include p1,p2] [--strict-elements] [--json] # 1.9.18 중복/잠재중복/depends-on\n leerness verify-claim <T-ID> [--path .] [--run-tests] [--json] # 1.9.18-20 evidence 자동 검증 (1.9.20: scenes/scripts 등 도메인 폴더 + jest/mocha 파싱)\n leerness verify-code [path] [--build] [--bench] # 1.9.20 --bench: scripts.bench 추가 실행 + evidence 누적\n leerness session close [path]\n leerness viewwork install [path]\n leerness viewwork emit [path] [--action a] [--note n] [--agent x] [--tool t]\n leerness route <task-type>\n leerness self check [path]\n leerness readme sync [path]\n leerness consistency check [path]\n leerness consistency merge-design-guide [path]\n leerness plan show|init|add|drop|progress|sync [args]\n leerness task list|add|update|drop|fix-evidence|relink [args]\n leerness skill list|info <name>\n leerness skill learn <id> --doc <url> --command "..." --capability "..." [--note ...]\n leerness skill use <id> [--note ...]\n leerness skill optimize <id> --before "..." --after "..." [--note ...]\n leerness skill remove <id>\n leerness skill consolidate [--threshold 0.3]\n leerness gate [path] # verify+audit+scan+encoding+lazy
|
|
5056
5365
|
leerness retro [path] [--days 7] [--all-apps] [--include p1,p2] [--json] # 회고 (1.9.13~1.9.16)
|
|
5057
5366
|
leerness insights [path] [--all-apps] [--include p1,p2] [--json] # 누적 통계 (1.9.13~1.9.16)
|
|
5058
5367
|
leerness brainstorm "<주제>" [--all-apps] [--include p1,p2] [--json] # 브레인스토밍 (1.9.13~1.9.16)
|
|
@@ -5100,6 +5409,8 @@ async function main() {
|
|
|
5100
5409
|
if (cmd === 'persona') return personaCmd(arg('--path', process.cwd()), args[1], args[2]);
|
|
5101
5410
|
if (cmd === 'review') return reviewCmd(arg('--path', process.cwd()), args[1]);
|
|
5102
5411
|
if (cmd === 'agents') return agentsCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
|
|
5412
|
+
if (cmd === 'contract' && args[1] === 'verify') return contractVerifyCmd(args[2], args[3]);
|
|
5413
|
+
if (cmd === 'reuse' && args[1] === 'autodetect') return reuseAutodetectCmd(args[2] || arg('--path', process.cwd()));
|
|
5103
5414
|
if (cmd === 'setup-agents' || cmd === 'setup' && args[1] === 'agents') return await setupAgentsCmd(args[1] && args[1] !== 'agents' ? args[1] : (args[2] || process.cwd()));
|
|
5104
5415
|
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; }
|
|
5105
5416
|
if (cmd === 'viewwork' && args[1] === 'install') return viewworkInstall(args[2] || process.cwd());
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -950,6 +950,131 @@ 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
|
+
|
|
1008
|
+
// 1.9.35 회귀: 5개 신규 기능
|
|
1009
|
+
total++;
|
|
1010
|
+
{
|
|
1011
|
+
// 개선#1: handoff 시 .harness 부재 자동 경고
|
|
1012
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-noinit-'));
|
|
1013
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'handoff', tmpC], { encoding: 'utf8', timeout: 10000 });
|
|
1014
|
+
const ok = /leerness init 미실행 디렉토리/.test(r.stdout) || /leerness init/.test(r.stdout);
|
|
1015
|
+
console.log(ok ? '✓ B(1.9.35) handoff: .harness 부재 자동 경고' : `✗ #1 handoff 경고 실패`);
|
|
1016
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
total++;
|
|
1020
|
+
{
|
|
1021
|
+
// 개선#5: audit --fix 옵션 (플래그 인식만 검증, 실 fix는 통합 환경 필요)
|
|
1022
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-fix-'));
|
|
1023
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
1024
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'audit', tmpC, '--fix'], { encoding: 'utf8', timeout: 15000 });
|
|
1025
|
+
const ok = r.status === 0 && /(Audit summary|fixed=)/.test(r.stdout);
|
|
1026
|
+
console.log(ok ? '✓ B(1.9.35) audit --fix: 자동 fix 옵션 인식' : `✗ #5 audit --fix 실패`);
|
|
1027
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
total++;
|
|
1031
|
+
{
|
|
1032
|
+
// 개선#3: contract verify
|
|
1033
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-contract-'));
|
|
1034
|
+
const specFile = path.join(tmpDir, 'spec.md');
|
|
1035
|
+
const implFile = path.join(tmpDir, 'impl.js');
|
|
1036
|
+
fs.writeFileSync(specFile, '# Spec\n\nfunction foo(x) {}\nfunction bar(y) {}\n`baz(`\n\ntick.amount\ntick.isCritical\n', 'utf8');
|
|
1037
|
+
fs.writeFileSync(implFile, '"use strict";\nfunction foo(x){return x}\nfunction baz(y){return tick.amount}\nmodule.exports={foo, baz};\n', 'utf8');
|
|
1038
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'contract', 'verify', specFile, implFile, '--json'], { encoding: 'utf8', timeout: 10000 });
|
|
1039
|
+
let parsed = null;
|
|
1040
|
+
try { parsed = JSON.parse(r.stdout); } catch {}
|
|
1041
|
+
const ok = parsed
|
|
1042
|
+
&& Array.isArray(parsed.missingFunctions) && parsed.missingFunctions.includes('bar')
|
|
1043
|
+
&& Array.isArray(parsed.missingFields) && parsed.missingFields.includes('isCritical')
|
|
1044
|
+
&& parsed.ok === false;
|
|
1045
|
+
console.log(ok ? '✓ B(1.9.35) contract verify: bar 함수 + isCritical 필드 누락 정확 감지' : `✗ #3 contract verify 실패`);
|
|
1046
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
total++;
|
|
1050
|
+
{
|
|
1051
|
+
// 개선#2: reuse autodetect — src/*.js의 module.exports 스캔
|
|
1052
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-autodetect-'));
|
|
1053
|
+
fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true });
|
|
1054
|
+
fs.writeFileSync(path.join(tmpDir, 'src', 'util.js'), 'function hello(){}\nfunction _internal(){}\nmodule.exports={hello, _internal};\n', 'utf8');
|
|
1055
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'reuse', 'autodetect', tmpDir, '--json'], { encoding: 'utf8', timeout: 10000 });
|
|
1056
|
+
let parsed = null;
|
|
1057
|
+
try { parsed = JSON.parse(r.stdout); } catch {}
|
|
1058
|
+
// _internal은 _로 시작하므로 제외, hello만 발견
|
|
1059
|
+
const ok = parsed && parsed.found && parsed.found.length === 1 && parsed.found[0].name === 'hello';
|
|
1060
|
+
console.log(ok ? '✓ B(1.9.35) reuse autodetect: module.exports 스캔 + _internal 제외' : `✗ #2 autodetect 실패`);
|
|
1061
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
total++;
|
|
1065
|
+
{
|
|
1066
|
+
// 개선#4: agents dispatch 안내문에 mtime/contract 안전 규칙 추가
|
|
1067
|
+
const env = { ...process.env, LEERNESS_ENABLE_CLAUDE: '1' };
|
|
1068
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'agents', 'dispatch', 'test task', '--to', 'claude'], { encoding: 'utf8', timeout: 15000, env });
|
|
1069
|
+
// claude가 ready면 안내문 출력. 환경 따라 ready 아닐 수도 — 안내문 내용만 확인.
|
|
1070
|
+
const text = r.stdout + r.stderr;
|
|
1071
|
+
const ok = /분배 시 안전 규칙 \(1\.9\.35\)/.test(text) || /파일 경로 격리/.test(text) || /last-writer-wins/.test(text)
|
|
1072
|
+
// claude 미활성 시 거부 메시지도 통과
|
|
1073
|
+
|| /비활성|disabled|not-installed/i.test(text);
|
|
1074
|
+
console.log(ok ? '✓ B(1.9.35) agents dispatch: 안전 규칙 안내 (mtime/contract) 또는 비활성 거부' : `✗ #4 dispatch 안내 실패`);
|
|
1075
|
+
if (!ok) { failed++; console.log(text.slice(0, 400)); }
|
|
1076
|
+
}
|
|
1077
|
+
|
|
953
1078
|
// 1.9.34 회귀: 인터랙티브 multi-select (방향키/스페이스) — 비-TTY 폴백
|
|
954
1079
|
total++;
|
|
955
1080
|
{
|
|
@@ -974,7 +1099,7 @@ total++;
|
|
|
974
1099
|
&& /███████╗/.test(r.stdout)
|
|
975
1100
|
&& /verify · reuse-map/.test(r.stdout)
|
|
976
1101
|
&& /한국어 우선 AI 개발 하네스/.test(r.stdout)
|
|
977
|
-
&& /v1\.9\.
|
|
1102
|
+
&& /v1\.9\.3\d/.test(r.stdout);
|
|
978
1103
|
console.log(ok ? '✓ B(1.9.34) 배너 색상 + ASCII + 한국어' : `✗ 배너 색상 실패`);
|
|
979
1104
|
if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
|
|
980
1105
|
}
|