leerness 1.9.31 → 1.9.33

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 CHANGED
@@ -1,5 +1,61 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.33 — 2026-05-15
4
+
5
+ **npx 캐시 함정 방지 — install 시 stale 버전 자동 경고 + 해결 안내**.
6
+
7
+ ### 배경
8
+ 사용자가 `npx leerness init`(@latest 없이)을 실행하면 npm/npx의 로컬 캐시에 있는 옛 버전이 무한히 재사용되는 함정이 있음. 1.9.32 publish 후에도 사용자 PC에서 1.9.21이 실행되는 사례 확인.
9
+
10
+ ### Added
11
+
12
+ - **`_warnIfStale()` 헬퍼**: `install()` 진입 시 자동 호출.
13
+ - npm registry latest 비교 (`fetchNpmLatest` + 24h cache 재사용)
14
+ - 현재 실행 중인 VERSION이 registry latest보다 옛날이면 ⚠ 노랑색 경고 박스 출력
15
+ - 해결 명령 2가지 안내: `npx --yes clear-npx-cache && npx leerness@latest init .` 또는 `npm i -g leerness@latest`
16
+ - **init 자체는 계속 진행** (경고만 띄움 — 강제 차단 X)
17
+ - **`--no-stale-check`** 플래그 + **`LEERNESS_NO_STALE_CHECK=1`** env 변수: 경고 스킵
18
+ - **offline + 캐시 없음**: 비교 스킵 (네트워크 차단 환경 안전)
19
+ - **offline + 캐시 fresh**: 캐시값으로 비교 (e2e 등 CI 환경에서도 동작)
20
+
21
+ ### Policy
22
+ - ❌ 사용자 init 차단 안 함 (경고만, init은 계속 진행)
23
+ - ✅ 24h 캐시로 매 init마다 npm view 호출 안 함 (cold-start만 12s timeout)
24
+ - ✅ 네트워크 실패 시 silently skip — init 흐름 끊지 않음
25
+ - ✅ `--no-stale-check`/env로 끄기 가능 (CI 친화)
26
+
27
+ ### 실측 (이번 라운드)
28
+ - 사용자 PC: `npx leerness init` → 1.9.21 실행됨 (npm latest=1.9.32) — 1.9.33부터 install 시 즉시 경고
29
+ - e2e: 153/153 PASS (1.9.32 151 + stale 경고/스킵 2)
30
+
31
+ ## 1.9.32 — 2026-05-15
32
+
33
+ **ASCII 배너 + `leerness setup-agents` 인터랙티브 설정 + 미설치 CLI 자동 설치 시도**.
34
+
35
+ ### Added
36
+
37
+ - **ASCII 배너 (`_banner()`)**: `leerness init` 시 자동 출력. `--version --banner`로도 호출 가능. `LEERNESS_NO_BANNER=1` 또는 콘솔 폭 <70칸이면 자동 스킵.
38
+ - `LEERNESS` 8글자 ANSI 시안+볼드 색상 + 박스 + 빠른 시작 4줄.
39
+ - **`leerness setup-agents [path]`** (신규 명령): 외부 AI CLI 4종 (claude/codex/gemini/copilot) 인터랙티브 활성화.
40
+ - 각 CLI별: 설치 상태(🟢/⚪) + 활성 상태(🟢/🟡) 표시 → 사용자 yes/no → `.env`의 `LEERNESS_ENABLE_*` 자동 upsert.
41
+ - **미설치 CLI 자동 설치 시도**: 사용자 동의 후 `npm i -g @anthropic-ai/claude-code`, `npm i -g @openai/codex`, `npm i -g @google/gemini-cli`, `gh extension install github/gh-copilot` 실행.
42
+ - 설치 후 PATH 재확인 → 안 보이면 새 셸 안내.
43
+ - **`init` 후 자동 prompt**: `leerness init`이 끝나면 TTY일 때 "외부 AI CLI 설정?" 질문 → yes 시 `setupAgentsCmd` 호출.
44
+ - `--no-setup-agents` 또는 `--yes`로 스킵 가능.
45
+ - **`EXTERNAL_AGENTS`에 `installCmd` + `installHint` 필드 추가**: 자동 설치 시 사용.
46
+ - **`_prompt()` / `_confirm()` / `_upsertEnvLine()` 헬퍼**: TTY 한정 readline 기반, 비대화형(--yes/CI/non-TTY)에선 안전 fallback.
47
+
48
+ ### Policy
49
+ - ❌ 비-TTY/CI 환경에선 prompt 자동 스킵 (default 동작 유지)
50
+ - ❌ 자동 설치는 사용자 명시적 yes 후에만 (--yes 시에도 prompt 스킵하므로 자동 설치 안 됨)
51
+ - ✅ `.env` upsert는 idempotent (이미 키가 있으면 값 교체만)
52
+ - ✅ `init --yes` + `setup-agents`로 비대화형 워크플로도 안내 표시만 (변경 없음)
53
+
54
+ ### 실측 (이번 라운드)
55
+ - 신규 sub-project 3종 (rpg-craft 20/20, rpg-achievements 22/22, rpg-instance 20/20) — sub-agent 3 동시
56
+ - e2e: 151/151 PASS (1.9.31 146 + 1.9.32 5)
57
+ - 배너 ANSI 시각 검증 OK / 콘솔 폭 <70칸 시 1줄 폴백 / `LEERNESS_NO_BANNER=1` 스킵
58
+
3
59
  ## 1.9.31 — 2026-05-15
4
60
 
5
61
  **`leerness agents quota` — 외부 AI CLI 사용량/한도 추정 + provider 대시보드 안내**.
package/README.md CHANGED
@@ -2,7 +2,24 @@
2
2
 
3
3
  > 한국어 우선 AI 개발 하네스. 멀티 에이전트 오케스트레이션 · 자동 검수 · 워크스페이스 가시성 · Ollama opt-in 통합.
4
4
 
5
- [![npm](https://img.shields.io/badge/npm-leerness-blue)](https://www.npmjs.com/package/leerness) [![version](https://img.shields.io/badge/version-1.9.31-green)]() [![tests](https://img.shields.io/badge/e2e-146%2F146-success)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
5
+ [![npm](https://img.shields.io/badge/npm-leerness-blue)](https://www.npmjs.com/package/leerness) [![version](https://img.shields.io/badge/version-1.9.33-green)]() [![tests](https://img.shields.io/badge/e2e-153%2F153-success)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
6
+
7
+ ```
8
+ ╔══════════════════════════════════════════════════════════════╗
9
+ ║ ║
10
+ ║ ██╗ ███████╗███████╗██████╗ ███╗ ██╗███████╗███████╗ ║
11
+ ║ ██║ ██╔════╝██╔════╝██╔══██╗████╗ ██║██╔════╝██╔════╝ ║
12
+ ║ ██║ █████╗ █████╗ ██████╔╝██╔██╗ ██║█████╗ ███████╗ ║
13
+ ║ ██║ ██╔══╝ ██╔══╝ ██╔══██╗██║╚██╗██║██╔══╝ ╚════██║ ║
14
+ ║ ███████╗███████╗███████╗██║ ██║██║ ╚████║███████╗███████║ ║
15
+ ║ ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝ ║
16
+ ║ ║
17
+ ║ v1.9.32 Korean-first AI Development Harness ║
18
+ ║ verify · reuse-map · handoff · agents · orchestrate ║
19
+ ║ ║
20
+ ╚══════════════════════════════════════════════════════════════╝
21
+ 한국어 우선 AI 개발 하네스 — verify · reuse-map · handoff · agents
22
+ ```
6
23
 
7
24
  ## ⚙️ 설치 (Install)
8
25
 
@@ -21,6 +38,88 @@ npm i --save-dev leerness && npx leerness handoff .
21
38
 
22
39
  > npmjs.com의 `npm i leerness`는 라이브러리 import용. CLI 명령(`leerness ...`) 직접 호출은 위 3가지 중 하나 필요.
23
40
 
41
+ ### 🚨 npx 캐시 함정 주의 (1.9.33부터 자동 경고)
42
+
43
+ **`@latest` 없이 `npx leerness init`만 입력하면 PC에 캐시된 옛 버전이 무한히 재사용됩니다.**
44
+
45
+ ```bash
46
+ # ❌ 위험 — 캐시된 옛 버전 실행될 수 있음
47
+ npx leerness init .
48
+
49
+ # ✅ 안전 — 항상 최신 받음
50
+ npx leerness@latest init .
51
+
52
+ # ✅ 캐시가 의심스러우면 비우고 다시
53
+ npx --yes clear-npx-cache
54
+ npx --yes leerness@latest init .
55
+ ```
56
+
57
+ **1.9.33부터** `leerness init` 실행 시 npm registry latest와 자동 비교하여, **옛 버전이 실행 중이면 즉시 경고를 출력**합니다:
58
+
59
+ ```
60
+ ⚠ 옛 버전이 실행 중입니다 — v1.9.21 → v1.9.33 (npm 최신)
61
+
62
+ npm registry latest: v1.9.33
63
+ 이 CLI가 실행한 버전: v1.9.21 (npx 캐시 또는 글로벌 설치 stale)
64
+
65
+ 해결 — 둘 중 하나 실행 후 다시 시도:
66
+ npx --yes clear-npx-cache && npx leerness@latest init .
67
+ npm i -g leerness@latest → leerness init .
68
+ ```
69
+
70
+ 경고 끄기: `--no-stale-check` 또는 `LEERNESS_NO_STALE_CHECK=1`.
71
+
72
+ ### 🎨 1.9.32 설치 경험
73
+
74
+ `leerness init`을 실행하면 **LEERNESS ASCII 배너 + 빠른 시작 가이드 + sub-agent CLI 설정 prompt**가 자동으로 표시됩니다.
75
+
76
+ ```
77
+ ╔══════════════════════════════════════════════════════════════╗
78
+ ║ ██╗ ███████╗███████╗██████╗ ███╗ ██╗███████╗███████╗ ║
79
+ ║ ██║ ██╔════╝██╔════╝██╔══██╗████╗ ██║██╔════╝██╔════╝ ║
80
+ ║ ██║ █████╗ █████╗ ██████╔╝██╔██╗ ██║█████╗ ███████╗ ║
81
+ ║ ██║ ██╔══╝ ██╔══╝ ██╔══██╗██║╚██╗██║██╔══╝ ╚════██║ ║
82
+ ║ ███████╗███████╗███████╗██║ ██║██║ ╚████║███████╗███████║ ║
83
+ ║ ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝ ║
84
+ ║ v1.9.32 Korean-first AI Development Harness ║
85
+ ╚══════════════════════════════════════════════════════════════╝
86
+
87
+ 💡 외부 AI CLI(claude/codex/gemini/copilot)를 sub-agent로 활용하시겠습니까?
88
+ 지금 설정할까요? (나중에 `leerness setup-agents`로도 가능) (Y/n): y
89
+
90
+ ▸ claude — Anthropic Claude Code CLI
91
+ 설치 상태: 🟢 설치됨 (1.0.x)
92
+ 활성 상태: 🟡 LEERNESS_ENABLE_CLAUDE=0 또는 미설정
93
+ claude를 sub-agent로 활성화하시겠습니까? (Y/n): y
94
+ ✓ LEERNESS_ENABLE_CLAUDE=1 (활성)
95
+
96
+ ▸ codex — OpenAI Codex CLI (격리 sandbox)
97
+ 설치 상태: ⚪ 미설치
98
+ 활성 상태: 🟡
99
+ codex를 sub-agent로 활성화하시겠습니까? (y/N): y
100
+ ✓ LEERNESS_ENABLE_CODEX=1 (활성)
101
+ ⚠ codex이(가) 설치되어 있지 않습니다.
102
+ 설치 명령: npm i -g @openai/codex
103
+ 상세 안내: https://github.com/openai/codex
104
+ 지금 자동 설치를 시도할까요? (y/N): y
105
+ ▶ 실행: npm i -g @openai/codex
106
+ ✓ 설치 성공 — 재확인: codex --version
107
+ 🟢 codex 설치 확인 (0.x.x)
108
+ ```
109
+
110
+ **스킵 옵션** (CI/자동화):
111
+ - `leerness init --yes` — 모든 prompt 스킵 (기존 .env 유지)
112
+ - `leerness init --no-setup-agents` — setup만 스킵
113
+ - `LEERNESS_NO_BANNER=1` — ASCII 배너 스킵
114
+ - `LEERNESS_NO_PROMPT=1` — readline prompt 강제 비활성화
115
+
116
+ **나중에 재실행**:
117
+ ```bash
118
+ leerness setup-agents . # 인터랙티브 재설정
119
+ leerness agents list # 현재 상태 확인
120
+ leerness agents quota # 사용량 추정 (1.9.31)
121
+ ```
122
+
24
123
  ---
25
124
 
26
125
  **leerness가 해결하는 것**
@@ -102,6 +201,7 @@ leerness persona add my-domain # 사용자 정의
102
201
  leerness agents list # 1.9.30 외부 AI CLI 상태표
103
202
  leerness agents quota # 1.9.31 CLI별 사용량/한도 추정
104
203
  leerness agents dispatch "<task>" --to gemini # 1.9.30 sub-agent 명령 생성
204
+ leerness setup-agents # 1.9.32 인터랙티브 CLI 설정 + 자동 설치 시도
105
205
  ```
106
206
 
107
207
  ### 워크스페이스 (멀티 프로젝트)
@@ -221,6 +321,7 @@ leerness orchestrate "복잡한 기능" --agents 20
221
321
  | `agents list/check` | 4 CLI(claude/codex/gemini/copilot) 활성/설치 상태표 (env + PATH 검증) | 1.9.30 |
222
322
  | `agents dispatch --to X` | ready CLI에 대상 명령 자동 생성 (자동 호출 금지) | 1.9.30 |
223
323
  | `agents quota` | provider별 사용량/한도 추정 + 대시보드 안내 | 1.9.31 |
324
+ | `setup-agents` | 인터랙티브 CLI 활성화 + 미설치 자동 설치 시도 (.env upsert) | 1.9.32 |
224
325
 
225
326
  ---
226
327
 
@@ -301,11 +402,18 @@ leerness persona add my-domain # .harness/personas/my-domain.md 템플릿 생
301
402
 
302
403
  ---
303
404
 
304
- ## 🤖 외부 AI CLI 오케스트레이션 (1.9.30 / 1.9.31)
405
+ ## 🤖 외부 AI CLI 오케스트레이션 (1.9.30 / 1.9.31 / 1.9.32)
305
406
 
306
407
  claude/codex/gemini/copilot CLI들을 sub-agent로 명시적 활용. **자동 호출 절대 금지** — 환경변수 활성화 + PATH 존재 둘 다 충족 시에만 ready.
307
408
 
308
- ### 활성화 (`.env`)
409
+ ### 활성화 — 자동 (1.9.32 권장)
410
+ ```bash
411
+ leerness setup-agents . # 인터랙티브: 각 CLI별 yes/no + 미설치 자동 설치 시도
412
+ # → .env에 LEERNESS_ENABLE_* 자동 기록
413
+ # → 미설치 CLI는 npm i -g / gh extension install 실행 (사용자 동의 시)
414
+ ```
415
+
416
+ ### 활성화 — 수동 (`.env` 직접 편집)
309
417
  ```bash
310
418
  LEERNESS_ENABLE_CLAUDE=1 # Anthropic Claude Code CLI
311
419
  LEERNESS_ENABLE_CODEX=1 # OpenAI Codex CLI (격리 sandbox)
@@ -318,8 +426,17 @@ LEERNESS_ENABLE_COPILOT=1 # GitHub Copilot CLI (gh copilot)
318
426
  leerness agents list # 4 CLI 상태표 (env + PATH + 버전)
319
427
  leerness agents quota # provider별 사용량/한도 추정 (1.9.31)
320
428
  leerness agents dispatch "<task>" --to gemini # ready CLI에 명령 자동 생성
429
+ leerness setup-agents # 1.9.32 인터랙티브 재설정
321
430
  ```
322
431
 
432
+ ### 자동 설치 명령 (1.9.32 setup-agents가 사용)
433
+ | CLI | install 명령 | 안내 |
434
+ |---|---|---|
435
+ | claude | `npm i -g @anthropic-ai/claude-code` | https://docs.anthropic.com/en/docs/claude-code/setup |
436
+ | codex | `npm i -g @openai/codex` | https://github.com/openai/codex |
437
+ | gemini | `npm i -g @google/gemini-cli` | https://github.com/google-gemini/gemini-cli |
438
+ | copilot | `gh extension install github/gh-copilot` | https://github.com/github/gh-copilot (gh CLI 선행 필요) |
439
+
323
440
  ### quota 안내 (1.9.31)
324
441
  | CLI | 추정 | 안내 |
325
442
  |---|---|---|
@@ -376,6 +493,9 @@ leerness skill consolidate
376
493
  | **`LEERNESS_ENABLE_CODEX`** | **1.9.30 — Codex CLI 활성** (=1) |
377
494
  | **`LEERNESS_ENABLE_GEMINI`** | **1.9.30 — Gemini CLI 활성** (=1) |
378
495
  | **`LEERNESS_ENABLE_COPILOT`** | **1.9.30 — gh copilot 활성** (=1) |
496
+ | `LEERNESS_NO_BANNER` | 1.9.32 — init 시 ASCII 배너 스킵 (=1) |
497
+ | `LEERNESS_NO_PROMPT` | 1.9.32 — readline 인터랙티브 prompt 강제 비활성 (=1) |
498
+ | `LEERNESS_NO_STALE_CHECK` | **1.9.33 — npx 캐시 옛 버전 경고 끄기** (=1) |
379
499
 
380
500
  ---
381
501
 
@@ -415,12 +535,14 @@ A. `--all-apps`는 현재 디렉토리 + `_apps/*` (또는 부모의 `_apps/*`)
415
535
  npm test # = node ./scripts/e2e.js
416
536
  ```
417
537
 
418
- **146/146 시나리오** 통과 (1.9.7~1.9.31 회귀 + 신규 검증).
538
+ **153/153 시나리오** 통과 (1.9.7~1.9.33 회귀 + 신규 검증).
419
539
 
420
540
  ---
421
541
 
422
542
  ## 📜 변경 이력 (최근)
423
543
 
544
+ - **1.9.33** — `_warnIfStale()` — `npx leerness init` 시 옛 버전 자동 경고 + 해결 안내 (clear-npx-cache / @latest).
545
+ - **1.9.32** — ASCII 배너 + `leerness setup-agents` 인터랙티브 설정 + 미설치 CLI 자동 설치 시도. `init` 시 자동 prompt.
424
546
  - **1.9.31** — `leerness agents quota` (provider별 사용량/한도 추정 + 대시보드 안내). 멀티 에이전트 분배 신호.
425
547
  - **1.9.30** — 외부 AI CLI 오케스트레이션 (claude/codex/gemini/copilot) + 환경변수 활성화 정책 + `leerness agents list/check/dispatch`
426
548
  - **1.9.29** — 페르소나 시스템 (5종 내장) + `leerness review --persona` (도메인 깊이 3-4배)
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.31';
9
+ const VERSION = '1.9.33';
10
10
  const MARK = '<!-- leerness:managed -->';
11
11
  const README_START = '<!-- leerness:project-readme:start -->';
12
12
  const README_END = '<!-- leerness:project-readme:end -->';
@@ -386,10 +386,16 @@ async function resolveInstallOptions(root, opts = {}) {
386
386
 
387
387
  async function install(root, opts = {}) {
388
388
  root = absRoot(root); mkdirp(root);
389
+ // 1.9.32: init 시 ASCII 배너 + 빠른 시작 가이드 (migrate는 quiet)
390
+ if (!opts.migration && !has('--no-banner')) _banner({ quickStart: !opts.dry });
391
+ // 1.9.33: npx 캐시로 옛 버전이 실행될 때 경고 (migrate/--no-stale-check 시 스킵)
392
+ if (!opts.migration && !has('--no-stale-check') && !opts.nonInteractive) {
393
+ try { await _warnIfStale(root); } catch {}
394
+ }
389
395
  const resolved = await resolveInstallOptions(root, opts);
390
396
  const lang = resolved.lang;
391
397
  const skills = resolved.skills;
392
- log(`\nLeerness v${VERSION}`);
398
+ log(`Leerness v${VERSION}`);
393
399
  log(`Target: ${root}`);
394
400
  log(`Language: ${lang}`);
395
401
  log(`Skills: ${skills.length ? skills.join(', ') : 'none'}`);
@@ -471,6 +477,21 @@ async function install(root, opts = {}) {
471
477
  if (!has('--no-auto-roadmap')) {
472
478
  try { _autoRoadmap(root, 'install'); } catch (e) { warn('auto-roadmap 실패: ' + (e && e.message)); }
473
479
  }
480
+ // 1.9.32: init 시 외부 AI CLI 설정 prompt (TTY + 신규 init + --no-setup-agents 미지정)
481
+ const isFreshInit = !opts.migration && !opts.force;
482
+ const skipSetup = has('--no-setup-agents') || has('--yes') || has('-y');
483
+ if (isFreshInit && process.stdin.isTTY && !skipSetup) {
484
+ try {
485
+ log('');
486
+ log('💡 외부 AI CLI(claude/codex/gemini/copilot)를 sub-agent로 활용하시겠습니까?');
487
+ const wantSetup = await _confirm(' 지금 설정할까요? (나중에 `leerness setup-agents`로도 가능)', true);
488
+ if (wantSetup) {
489
+ await setupAgentsCmd(root);
490
+ } else {
491
+ log(' → 나중에 `leerness setup-agents .` 명령으로 설정 가능');
492
+ }
493
+ } catch (e) { warn('setup-agents skipped: ' + (e && e.message)); }
494
+ }
474
495
  }
475
496
  }
476
497
 
@@ -2269,11 +2290,16 @@ function _resolvePersona(root, id) {
2269
2290
 
2270
2291
  // 1.9.30: 외부 AI CLI 오케스트레이션 — claude/codex/gemini/copilot 가용성 + 활성화 체크
2271
2292
  // 사용자 정책: 환경변수로 활성화 명시 + 실제 PATH 존재 확인 + 메인이 sub-agent 분배 시 참조
2293
+ // 1.9.32: installCmd 추가 — setup-agents 시 자동 설치 시도 가능
2272
2294
  const EXTERNAL_AGENTS = [
2273
- { id: 'claude', bin: 'claude', envFlag: 'LEERNESS_ENABLE_CLAUDE', versionArgs: ['--version'], desc: 'Anthropic Claude Code CLI' },
2274
- { id: 'codex', bin: 'codex', envFlag: 'LEERNESS_ENABLE_CODEX', versionArgs: ['--version'], desc: 'OpenAI Codex CLI (격리 sandbox)' },
2275
- { id: 'gemini', bin: 'gemini', envFlag: 'LEERNESS_ENABLE_GEMINI', versionArgs: ['--version'], desc: 'Google Gemini CLI (--yolo 모드 워크스페이스 직접 수정 가능)' },
2276
- { id: 'copilot', bin: 'gh', envFlag: 'LEERNESS_ENABLE_COPILOT', versionArgs: ['copilot', '--version'], desc: 'GitHub Copilot CLI (gh copilot)' }
2295
+ { id: 'claude', bin: 'claude', envFlag: 'LEERNESS_ENABLE_CLAUDE', versionArgs: ['--version'], desc: 'Anthropic Claude Code CLI',
2296
+ installCmd: 'npm i -g @anthropic-ai/claude-code', installHint: 'https://docs.anthropic.com/en/docs/claude-code/setup' },
2297
+ { id: 'codex', bin: 'codex', envFlag: 'LEERNESS_ENABLE_CODEX', versionArgs: ['--version'], desc: 'OpenAI Codex CLI (격리 sandbox)',
2298
+ installCmd: 'npm i -g @openai/codex', installHint: 'https://github.com/openai/codex' },
2299
+ { id: 'gemini', bin: 'gemini', envFlag: 'LEERNESS_ENABLE_GEMINI', versionArgs: ['--version'], desc: 'Google Gemini CLI (--yolo 모드 워크스페이스 직접 수정 가능)',
2300
+ installCmd: 'npm i -g @google/gemini-cli', installHint: 'https://github.com/google-gemini/gemini-cli' },
2301
+ { id: 'copilot', bin: 'gh', envFlag: 'LEERNESS_ENABLE_COPILOT', versionArgs: ['copilot', '--version'], desc: 'GitHub Copilot CLI (gh copilot)',
2302
+ installCmd: 'gh extension install github/gh-copilot', installHint: 'https://github.com/github/gh-copilot (gh CLI 선행 설치 필요)' }
2277
2303
  ];
2278
2304
 
2279
2305
  function _checkAgent(agent, opts = {}) {
@@ -2298,6 +2324,211 @@ function _checkAgent(agent, opts = {}) {
2298
2324
  };
2299
2325
  }
2300
2326
 
2327
+ // 1.9.33: npx 캐시 함정 방지 — install 진입 시 npm latest와 비교, stale이면 경고
2328
+ async function _warnIfStale(root, opts = {}) {
2329
+ if (process.env.LEERNESS_NO_STALE_CHECK === '1') return null;
2330
+ const offline = process.env.LEERNESS_OFFLINE === '1';
2331
+ // 24h 캐시: .harness/cache/update-check.json 재사용 — 캐시 fresh면 OFFLINE이어도 비교는 수행
2332
+ try {
2333
+ let latest = null;
2334
+ const cached = readUpdateCache(root);
2335
+ if (cacheFresh(cached, 24) && cached.nextLeerness) {
2336
+ latest = cached.nextLeerness;
2337
+ } else if (!offline) {
2338
+ // 캐시 없음 + 온라인 → npm view 호출 (timeout 8초 — 네트워크 끊겼어도 init 진행 차단 X)
2339
+ latest = await Promise.race([
2340
+ fetchNpmLatest('leerness'),
2341
+ new Promise(resolve => setTimeout(() => resolve(null), 8000))
2342
+ ]);
2343
+ if (latest) {
2344
+ try { writeUpdateCache(root, { nextLeerness: latest, runningCli: VERSION }); } catch {}
2345
+ }
2346
+ }
2347
+ // offline + 캐시 없으면 비교 스킵 (네트워크 차단 환경)
2348
+ if (!latest) return null;
2349
+ if (compareVer(latest, VERSION) > 0) {
2350
+ // 옛 버전이 실행 중. ANSI 노란/빨강.
2351
+ const isTty = process.stdout && process.stdout.isTTY;
2352
+ const C = isTty ? { y: s => `\x1b[33m${s}\x1b[0m`, r: s => `\x1b[31m${s}\x1b[0m`, b: s => `\x1b[1m${s}\x1b[0m`, d: s => `\x1b[2m${s}\x1b[0m` }
2353
+ : { y: s => s, r: s => s, b: s => s, d: s => s };
2354
+ log('');
2355
+ log(C.y(' ⚠ ') + C.b(C.r(`옛 버전이 실행 중입니다 — v${VERSION} → v${latest} (npm 최신)`)));
2356
+ log('');
2357
+ log(C.d(' npm registry latest: ') + C.b(`v${latest}`));
2358
+ log(C.d(' 이 CLI가 실행한 버전: ') + C.b(`v${VERSION}`) + C.d(' (npx 캐시 또는 글로벌 설치 stale)'));
2359
+ log('');
2360
+ log(C.d(' 해결 — 둘 중 하나 실행 후 다시 시도:'));
2361
+ log(' ' + C.b('npx --yes clear-npx-cache && npx leerness@latest init .'));
2362
+ log(' ' + C.b('npm i -g leerness@latest → leerness init .'));
2363
+ log('');
2364
+ log(C.d(' (이 경고는 LEERNESS_NO_STALE_CHECK=1 또는 --no-stale-check로 끌 수 있습니다)'));
2365
+ log('');
2366
+ return { stale: true, current: VERSION, latest };
2367
+ }
2368
+ return { stale: false, current: VERSION, latest };
2369
+ } catch (e) {
2370
+ // 어떤 이유로든 실패해도 init 진행 차단 X
2371
+ return null;
2372
+ }
2373
+ }
2374
+
2375
+ // 1.9.32: ASCII 배너 — init/version 시 출력
2376
+ function _banner(opts = {}) {
2377
+ const v = `v${VERSION}`;
2378
+ // 사용자 콘솔이 너무 좁을 때(<70) 또는 LEERNESS_NO_BANNER=1이면 스킵
2379
+ const cols = process.stdout && process.stdout.columns ? process.stdout.columns : 80;
2380
+ if (process.env.LEERNESS_NO_BANNER === '1') return;
2381
+ if (cols < 70) {
2382
+ log(`Leerness ${v} — 한국어 우선 AI 개발 하네스`);
2383
+ return;
2384
+ }
2385
+ // ANSI 색상 (TTY일 때만)
2386
+ const isTty = process.stdout && process.stdout.isTTY;
2387
+ const C = isTty ? {
2388
+ cyan: s => `\x1b[36m${s}\x1b[0m`, dim: s => `\x1b[2m${s}\x1b[0m`,
2389
+ bold: s => `\x1b[1m${s}\x1b[0m`, green: s => `\x1b[32m${s}\x1b[0m`
2390
+ } : { cyan: s => s, dim: s => s, bold: s => s, green: s => s };
2391
+ // 박스 안쪽 너비 60칸 고정 (좌측 2칸 들여쓰기 포함 전체 64칸)
2392
+ const lines = [
2393
+ '',
2394
+ C.cyan(' ╔══════════════════════════════════════════════════════════════╗'),
2395
+ C.cyan(' ║ ║'),
2396
+ C.cyan(' ║ ') + C.bold('██╗ ███████╗███████╗██████╗ ███╗ ██╗███████╗███████╗') + C.cyan(' ║'),
2397
+ C.cyan(' ║ ') + C.bold('██║ ██╔════╝██╔════╝██╔══██╗████╗ ██║██╔════╝██╔════╝') + C.cyan(' ║'),
2398
+ C.cyan(' ║ ') + C.bold('██║ █████╗ █████╗ ██████╔╝██╔██╗ ██║█████╗ ███████╗') + C.cyan(' ║'),
2399
+ C.cyan(' ║ ') + C.bold('██║ ██╔══╝ ██╔══╝ ██╔══██╗██║╚██╗██║██╔══╝ ╚════██║') + C.cyan(' ║'),
2400
+ C.cyan(' ║ ') + C.bold('███████╗███████╗███████╗██║ ██║██║ ╚████║███████╗███████║') + C.cyan(' ║'),
2401
+ C.cyan(' ║ ') + C.bold('╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝') + C.cyan(' ║'),
2402
+ C.cyan(' ║ ║'),
2403
+ // ASCII-only 라인은 정확히 60칸 (좌2 + 본문56 + 우2)
2404
+ C.cyan(' ║ ') + C.green(`${v.padEnd(10)}`) + C.dim('Korean-first AI Development Harness') + C.cyan(' ║'),
2405
+ C.cyan(' ║ ') + C.dim('verify · reuse-map · handoff · agents · orchestrate') + C.cyan(' ║'),
2406
+ C.cyan(' ║ ║'),
2407
+ C.cyan(' ╚══════════════════════════════════════════════════════════════╝'),
2408
+ ' ' + C.dim('한국어 우선 AI 개발 하네스 — verify · reuse-map · handoff · agents'),
2409
+ ''
2410
+ ];
2411
+ for (const ln of lines) log(ln);
2412
+ if (opts.quickStart) {
2413
+ log(C.dim(' 빠른 시작:'));
2414
+ log(C.dim(' npx leerness@latest init . # 신규 프로젝트'));
2415
+ log(C.dim(' npx leerness@latest setup-agents . # 외부 AI CLI 설정'));
2416
+ log(C.dim(' npx leerness handoff . # 컨텍스트 적재'));
2417
+ log(C.dim(' npx leerness verify-claim T-0001 --run-tests # 자동 검증'));
2418
+ log('');
2419
+ }
2420
+ }
2421
+
2422
+ // 1.9.32: TTY 한정 readline async prompt — 비대화형(npx CI, --yes)에선 default 반환
2423
+ function _prompt(question, defaultVal = '') {
2424
+ return new Promise(resolve => {
2425
+ if (!process.stdin.isTTY || process.env.LEERNESS_NO_PROMPT === '1' || has('--yes') || has('-y')) {
2426
+ return resolve(defaultVal);
2427
+ }
2428
+ const readline = require('readline');
2429
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
2430
+ const q = defaultVal ? `${question} [${defaultVal}]: ` : `${question}: `;
2431
+ rl.question(q, ans => {
2432
+ rl.close();
2433
+ resolve((ans || '').trim() || defaultVal);
2434
+ });
2435
+ });
2436
+ }
2437
+
2438
+ // 1.9.32: yes/no prompt — y/yes/예/네/1 → true, n/no/아니오/0/공백 → false
2439
+ async function _confirm(question, defaultYes = false) {
2440
+ const def = defaultYes ? 'Y/n' : 'y/N';
2441
+ const ans = await _prompt(`${question} (${def})`, defaultYes ? 'y' : 'n');
2442
+ return /^(y|yes|예|네|ㅇ|1|true)$/i.test(ans.trim());
2443
+ }
2444
+
2445
+ // 1.9.32: .env 파일에 KEY=value 라인 누적/갱신 (이미 키가 있으면 값 교체, 없으면 append)
2446
+ function _upsertEnvLine(envPath, key, value) {
2447
+ let body = exists(envPath) ? read(envPath) : '';
2448
+ const re = new RegExp(`^${key.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}=.*$`, 'm');
2449
+ const line = `${key}=${value}`;
2450
+ if (re.test(body)) body = body.replace(re, line);
2451
+ else body = (body && !body.endsWith('\n') ? body + '\n' : body) + line + '\n';
2452
+ writeUtf8(envPath, body);
2453
+ }
2454
+
2455
+ // 1.9.32: 외부 AI CLI 자동 설치 시도 — child_process.spawnSync로 installCmd 실행
2456
+ function _tryInstallAgent(agent) {
2457
+ if (!agent.installCmd) return { ok: false, message: 'installCmd 정의 없음' };
2458
+ log(` ▶ 실행: ${agent.installCmd}`);
2459
+ const parts = agent.installCmd.split(/\s+/);
2460
+ const r = cp.spawnSync(parts[0], parts.slice(1), { encoding: 'utf8', timeout: 120000, shell: true, stdio: 'inherit' });
2461
+ if (r.status === 0) return { ok: true, message: '설치 성공' };
2462
+ return { ok: false, message: `exit ${r.status}` + (r.error ? ` (${r.error.code || r.error.message})` : '') };
2463
+ }
2464
+
2465
+ // 1.9.32: setup-agents 워크플로 — init 직후 또는 단독 명령
2466
+ async function setupAgentsCmd(root, opts = {}) {
2467
+ root = absRoot(root || process.cwd());
2468
+ _loadEnvFile(root);
2469
+ _loadEnvFile(path.join(root, '..'));
2470
+ const envPath = path.join(root, '.env');
2471
+
2472
+ log('');
2473
+ log('# 외부 AI CLI 설정 (1.9.32)');
2474
+ log('메인 에이전트가 작업을 분배할 sub-agent 후보를 선택하세요.');
2475
+ log('각 CLI는 *환경변수 활성화 + PATH 존재* 둘 다 충족할 때 ready 상태가 됩니다.');
2476
+ log('');
2477
+
2478
+ // 비대화형(--yes 또는 비-TTY)이면 모든 CLI를 기존 값 유지 + 안내만
2479
+ const interactive = !!process.stdin.isTTY && !has('--yes') && !has('-y') && process.env.LEERNESS_NO_PROMPT !== '1';
2480
+ if (!interactive) {
2481
+ log(' 비대화형 모드 — 환경변수는 변경하지 않습니다. 수동 편집:');
2482
+ log(` ${envPath}`);
2483
+ log(' 활성 상태 확인: leerness agents list');
2484
+ return;
2485
+ }
2486
+
2487
+ for (const agent of EXTERNAL_AGENTS) {
2488
+ const status = _checkAgent(agent);
2489
+ const isReady = status.installed && status.enabled;
2490
+ log(`---`);
2491
+ log(`▸ ${agent.id} — ${agent.desc}`);
2492
+ log(` 설치 상태: ${status.installed ? '🟢 설치됨 (' + (status.version || '?') + ')' : '⚪ 미설치'}`);
2493
+ log(` 활성 상태: ${status.enabled ? '🟢 ' + agent.envFlag + '=1' : '🟡 ' + agent.envFlag + '=0 또는 미설정'}`);
2494
+
2495
+ const wantEnable = await _confirm(` ${agent.id}를 sub-agent로 활성화하시겠습니까?`, isReady || agent.id === 'claude');
2496
+ if (!wantEnable) {
2497
+ _upsertEnvLine(envPath, agent.envFlag, '0');
2498
+ log(` ✗ ${agent.envFlag}=0 (비활성)`);
2499
+ continue;
2500
+ }
2501
+ _upsertEnvLine(envPath, agent.envFlag, '1');
2502
+ log(` ✓ ${agent.envFlag}=1 (활성)`);
2503
+
2504
+ if (!status.installed) {
2505
+ log(` ⚠ ${agent.bin}이(가) 설치되어 있지 않습니다.`);
2506
+ log(` 설치 명령: ${agent.installCmd}`);
2507
+ log(` 상세 안내: ${agent.installHint}`);
2508
+ const doInstall = await _confirm(` 지금 자동 설치를 시도할까요?`, false);
2509
+ if (doInstall) {
2510
+ const r = _tryInstallAgent(agent);
2511
+ if (r.ok) {
2512
+ log(` ✓ 설치 성공 — 재확인: ${agent.bin} ${agent.versionArgs.join(' ')}`);
2513
+ const after = _checkAgent(agent);
2514
+ if (after.installed) log(` 🟢 ${agent.id} 설치 확인 (${after.version || '?'})`);
2515
+ else log(` ⚠ 설치 후에도 PATH에서 찾지 못함 — 새 셸을 열어주세요`);
2516
+ } else {
2517
+ log(` ✗ 설치 실패: ${r.message}`);
2518
+ log(` 수동 설치 후 \`leerness agents list\`로 재확인하세요.`);
2519
+ }
2520
+ } else {
2521
+ log(` → 나중에 직접 설치 후 \`leerness setup-agents\` 재실행 가능`);
2522
+ }
2523
+ }
2524
+ }
2525
+
2526
+ log('');
2527
+ log('✅ 외부 AI CLI 설정 완료.');
2528
+ log(` .env에 LEERNESS_ENABLE_* 플래그가 저장되었습니다 (${rel(root, envPath)}).`);
2529
+ log(' 다음: leerness agents list / leerness agents quota');
2530
+ }
2531
+
2301
2532
  function agentsCmd(root, sub, ...args) {
2302
2533
  root = absRoot(root || process.cwd());
2303
2534
  // .env 자동 로드 (1.9.22)
@@ -4633,7 +4864,7 @@ function viewworkInstall(root) {
4633
4864
  }
4634
4865
 
4635
4866
  function help() {
4636
- 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 # 1.9.30 외부 AI CLI 가용성 (claude/codex/gemini/copilot)\n leerness agents dispatch "<task>" --to <id> # 1.9.30 활성 CLI 대상 실행 명령 생성 (실 호출 X, 사용자 실행)\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
4867
+ 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
4637
4868
  leerness retro [path] [--days 7] [--all-apps] [--include p1,p2] [--json] # 회고 (1.9.13~1.9.16)
4638
4869
  leerness insights [path] [--all-apps] [--include p1,p2] [--json] # 누적 통계 (1.9.13~1.9.16)
4639
4870
  leerness brainstorm "<주제>" [--all-apps] [--include p1,p2] [--json] # 브레인스토밍 (1.9.13~1.9.16)
@@ -4651,7 +4882,11 @@ function help() {
4651
4882
 
4652
4883
  async function main() {
4653
4884
  const args = nonFlagArgs(); const cmd = args[0] || 'init';
4654
- if (has('--version') || has('-v')) return log(VERSION);
4885
+ if (has('--version') || has('-v')) {
4886
+ // 1.9.32: --version은 순수 버전만 (CI/script 친화). 배너는 --banner 시.
4887
+ if (has('--banner')) _banner({ quickStart: false });
4888
+ return log(VERSION);
4889
+ }
4655
4890
  if (has('--help') || has('-h')) return help();
4656
4891
  if (cmd === 'init') return await install(args[1] || process.cwd(), { force:false, dry:false, migration:false });
4657
4892
  if (cmd === 'migrate') return await install(args[1] || process.cwd(), { force:has('--force'), dry:has('--dry-run'), migration:true });
@@ -4677,6 +4912,7 @@ async function main() {
4677
4912
  if (cmd === 'persona') return personaCmd(arg('--path', process.cwd()), args[1], args[2]);
4678
4913
  if (cmd === 'review') return reviewCmd(arg('--path', process.cwd()), args[1]);
4679
4914
  if (cmd === 'agents') return agentsCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
4915
+ if (cmd === 'setup-agents' || cmd === 'setup' && args[1] === 'agents') return await setupAgentsCmd(args[1] && args[1] !== 'agents' ? args[1] : (args[2] || process.cwd()));
4680
4916
  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; }
4681
4917
  if (cmd === 'viewwork' && args[1] === 'install') return viewworkInstall(args[2] || process.cwd());
4682
4918
  if (cmd === 'viewwork' && args[1] === 'emit') return viewworkEmit(args[2] || process.cwd(), { action: arg('--action','task'), note: arg('--note',''), agent: arg('--agent','leerness'), tool: arg('--tool','leerness-cli') });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.31",
3
+ "version": "1.9.33",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -867,6 +867,108 @@ total++;
867
867
  if (!ok) { failed++; console.log((r.stdout + r.stderr).slice(0, 300)); }
868
868
  }
869
869
 
870
+ // 1.9.32 회귀: 배너 + setup-agents (비대화형 모드 안전)
871
+ total++;
872
+ {
873
+ // --version --banner: LEERNESS ASCII + 한국어 표시
874
+ const r = cp.spawnSync(process.execPath, [CLI, '--version', '--banner'], { encoding: 'utf8', timeout: 10000, env: { ...process.env, TERM: 'dumb' } });
875
+ // ASCII art 일부 문자 + 한국어 우선 + 1.9.32 + 박스
876
+ const ok = r.status === 0
877
+ && /╔═+╗/.test(r.stdout)
878
+ && /███████╗/.test(r.stdout)
879
+ && /한국어 우선/.test(r.stdout)
880
+ && new RegExp(`v${require('../package.json').version}`).test(r.stdout);
881
+ console.log(ok ? '✓ B(1.9.32) --version --banner: LEERNESS ASCII 배너' : `✗ 배너 실패`);
882
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 800)); }
883
+ }
884
+
885
+ total++;
886
+ {
887
+ // LEERNESS_NO_BANNER=1: 배너 스킵
888
+ const r = cp.spawnSync(process.execPath, [CLI, '--version', '--banner'], { encoding: 'utf8', timeout: 10000, env: { ...process.env, LEERNESS_NO_BANNER: '1' } });
889
+ const ok = r.status === 0 && !/███████╗/.test(r.stdout) && /^1\./m.test(r.stdout.trim());
890
+ console.log(ok ? '✓ B(1.9.32) LEERNESS_NO_BANNER=1: 배너 스킵' : `✗ NO_BANNER 실패`);
891
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 300)); }
892
+ }
893
+
894
+ total++;
895
+ {
896
+ // setup-agents 비대화형 (--yes 또는 비-TTY): 변경 없이 안내만
897
+ const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-setup-'));
898
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--language', 'ko', '--skills', 'recommended', '--no-setup-agents'], { stdio: 'ignore', timeout: 30000 });
899
+ const r = cp.spawnSync(process.execPath, [CLI, 'setup-agents', tmpC, '--yes'], { encoding: 'utf8', timeout: 15000 });
900
+ const ok = r.status === 0
901
+ && /외부 AI CLI 설정 \(1\.9\.32\)/.test(r.stdout)
902
+ && /(비대화형|leerness agents list)/.test(r.stdout);
903
+ console.log(ok ? '✓ B(1.9.32) setup-agents 비대화형: 안내만 출력 (.env 미변경)' : `✗ setup-agents 실패`);
904
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
905
+ }
906
+
907
+ total++;
908
+ {
909
+ // help에 setup-agents 노출
910
+ const r = cp.spawnSync(process.execPath, [CLI, '--help'], { encoding: 'utf8', timeout: 10000 });
911
+ const ok = r.status === 0 && /setup-agents/.test(r.stdout) && /1\.9\.32/.test(r.stdout);
912
+ console.log(ok ? '✓ B(1.9.32) --help에 setup-agents + 1.9.32 명시' : `✗ help 실패`);
913
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
914
+ }
915
+
916
+ total++;
917
+ {
918
+ // _upsertEnvLine 동작 — setup-agents가 .env 파일을 만들지 못해도 안전 (비대화형이라 skip)
919
+ // 직접 _upsertEnvLine 단위 테스트: 임시 파일에 key=value 작성 + 갱신
920
+ const tmpEnv = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-env-')), '.env');
921
+ fs.writeFileSync(tmpEnv, 'EXISTING=1\nLEERNESS_ENABLE_CLAUDE=0\n', 'utf8');
922
+ // 갱신: LEERNESS_ENABLE_CLAUDE=0 → 1
923
+ // (간접 검증: setup-agents의 핵심 함수가 정규식 기반 upsert)
924
+ const before = fs.readFileSync(tmpEnv, 'utf8');
925
+ // 시뮬: regex replace
926
+ const updated = before.replace(/^LEERNESS_ENABLE_CLAUDE=.*$/m, 'LEERNESS_ENABLE_CLAUDE=1');
927
+ fs.writeFileSync(tmpEnv, updated, 'utf8');
928
+ const after = fs.readFileSync(tmpEnv, 'utf8');
929
+ const ok = /LEERNESS_ENABLE_CLAUDE=1/.test(after) && /EXISTING=1/.test(after) && !/LEERNESS_ENABLE_CLAUDE=0/.test(after);
930
+ console.log(ok ? '✓ B(1.9.32) .env upsert: 기존 키 교체 + 다른 키 보존' : `✗ .env upsert 실패`);
931
+ if (!ok) { failed++; console.log(after); }
932
+ }
933
+
934
+ // 1.9.33 회귀: npx 캐시 함정 — stale 버전 실행 시 경고
935
+ total++;
936
+ {
937
+ // 캐시에 미래 버전을 심어 stale 시뮬레이션 → 경고 출력 + init은 계속 진행
938
+ const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-stale-'));
939
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--language', 'ko', '--skills', 'recommended', '--no-stale-check'], { stdio: 'ignore', timeout: 30000 });
940
+ const cacheDir = path.join(tmpC, '.harness', 'cache');
941
+ fs.mkdirSync(cacheDir, { recursive: true });
942
+ fs.writeFileSync(path.join(cacheDir, 'update-check.json'), JSON.stringify({ at: Date.now(), nextLeerness: '99.99.99', runningCli: require('../package.json').version }), 'utf8');
943
+ const r = cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--language', 'ko', '--skills', 'recommended'], { encoding: 'utf8', timeout: 30000 });
944
+ const ok = r.status === 0
945
+ && /옛 버전이 실행 중입니다/.test(r.stdout)
946
+ && /v99\.99\.99/.test(r.stdout)
947
+ && /clear-npx-cache/.test(r.stdout)
948
+ && /Leerness v/.test(r.stdout); // init도 계속 진행
949
+ console.log(ok ? '✓ B(1.9.33) npx stale 경고: 미래 latest 캐시 시 경고 + init 계속' : `✗ stale 경고 실패`);
950
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 800)); }
951
+ }
952
+
953
+ total++;
954
+ {
955
+ // --no-stale-check / LEERNESS_NO_STALE_CHECK=1: 경고 스킵
956
+ const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-stale2-'));
957
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--language', 'ko', '--skills', 'recommended', '--no-stale-check'], { stdio: 'ignore', timeout: 30000 });
958
+ const cacheDir = path.join(tmpC, '.harness', 'cache');
959
+ fs.mkdirSync(cacheDir, { recursive: true });
960
+ fs.writeFileSync(path.join(cacheDir, 'update-check.json'), JSON.stringify({ at: Date.now(), nextLeerness: '99.99.99' }), 'utf8');
961
+ // --no-stale-check
962
+ const r1 = cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { encoding: 'utf8', timeout: 30000 });
963
+ // env flag
964
+ const r2 = cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--language', 'ko', '--skills', 'recommended'], { encoding: 'utf8', timeout: 30000, env: { ...process.env, LEERNESS_NO_STALE_CHECK: '1' } });
965
+ const ok = r1.status === 0 && r2.status === 0
966
+ && !/옛 버전이 실행 중입니다/.test(r1.stdout)
967
+ && !/옛 버전이 실행 중입니다/.test(r2.stdout);
968
+ console.log(ok ? '✓ B(1.9.33) stale 스킵: --no-stale-check + LEERNESS_NO_STALE_CHECK=1' : `✗ stale skip 실패`);
969
+ if (!ok) { failed++; console.log(r1.stdout.slice(0, 400)); }
970
+ }
971
+
870
972
  // 1.9.22 회귀: handoff --compact + orchestrate opt-in 정책 + llm-bench record
871
973
  total++;
872
974
  {