leerness 1.9.245 → 1.9.247

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,101 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.247 — 2026-05-26 — UR-0016 2단계 + UR-0015 2단계
4
+
5
+ **🔁 REPL multi-provider auto-fallback + 📚 api-skill audit 통합.**
6
+
7
+ ### 1. UR-0016 2단계 — REPL multi-provider auto-fallback
8
+ 사용자 보고 (1.9.246 스크린샷): `codex CLI 응답 실패 (exit=null)` → 멀티 provider auto-fallback 검토.
9
+
10
+ **구현:**
11
+ - `state.autoFallback` 필드 추가 (default OFF — opt-in)
12
+ - 활성화: `:fallback on` (REPL slash) 또는 `LEERNESS_REPL_AUTO_FALLBACK=1` env
13
+ - transient 실패 (`exit=null|timeout`) 감지 시 next ready agent 로 자동 전환 + 재시도 1회
14
+ - 응답 헤더: `↪ auto-fallback (1.9.247): codex 실패 → claude 자동 전환 + 재시도 중...`
15
+ - 성공 시 `✓ fallback 성공 [claude/...]`, 실패 시 친절한 진단 + 수동 :provider 안내
16
+
17
+ ### 2. UR-0015 2단계 — api-skill audit 통합
18
+ 사용자 명시 (UR-0015): *"AI가 정리해둔 파일이 참조되는지 확인"*
19
+
20
+ **구현:**
21
+ - `audit` 명령에 새 finding 추가: `api_skill_missing`
22
+ - 현재 in-progress task 의 request/nextAction/evidence 에 API 키워드 (`API|endpoint|REST|GraphQL|OAuth|webhook|http[s]?://`) 감지
23
+ - 매칭되는 api-skill 0건 → `warn` + `leerness api-skill add <url> --direction "..."` 안내
24
+ - 매칭 1+건 → `✓ API skill 매칭 OK (현재 task → N건)` 로그
25
+ - non-API task 는 영향 없음
26
+
27
+ ### 3. 누적 회귀 (1.9.207~246) — 모두 유지
28
+ - REPL UX status bar (1.9.246) · api-skill cache (1.9.245) · _lastCycleLines HOTFIX (1.9.244)
29
+ - CJK 분류 (1.9.243) · env encoding --apply (1.9.242) · 모두 유지
30
+ - handoff JSON 11 필드 · MCP 70 도구 · agents dispatch 거부 회귀 (1.9.246 flake 방지) 검증
31
+
32
+ ### 4. stress-v192 — **20/20 PASS · 100%**
33
+ - 1.9.247 (8): VERSION + autoFallback state + :fallback slash + 재시도 로직 + audit api_skill_missing + 실제 동작 (3 시나리오)
34
+ - 성능 (1): cold start avg 588ms
35
+ - 누적 회귀 (11): 1.9.207~246 + 보안 + e2e flake 방지
36
+
37
+ ### 5. 자동 release (109 main-push streak · 70 npm publish streak · R203)
38
+
39
+ 🔁 **사용자 실 환경 대응 보강** — 1.9.246 보고된 codex CLI exit=null 케이스 자동 회복 + 후속 API task UR-0015 자동 참조 확인.
40
+
41
+ ---
42
+
43
+ ## 1.9.246 — 2026-05-26 — UR-0016 REPL UX/UI 개선
44
+
45
+ **🎯 사용자 명시 (UR-0016): REPL agent 채팅 입력칸 옆 컨텍스트 게이지 + 서브 에이전트 가시화 + 정상 완료 초록색 강조.**
46
+
47
+ ### 사용자 명시 (UR-0016)
48
+ > *"REPL agent 모드의 기능과 UX UI를 개선해줘: 서브 에이전트 활성화 여부, 채팅입력칸 근처에는 컨텍스트 창 게이지 등 표시, 정상완료된 작업은 초록색 등으로 표시. 별도의 신규 프로젝트를 생성하여 직접 REPL agent 모드를 구동하여 유저가 사용하기 쉽게 개선해줘."*
49
+
50
+ dogfood 검증: `C:\Users\leehy\AppData\Local\Temp\dogfood-1.9.246` 신규 프로젝트에서 `leerness init` + REPL 진입 흐름 직접 검증.
51
+
52
+ ### 1. 컨텍스트 창 게이지 (채팅 입력칸 직전 자동 표시)
53
+ 모든 prompt 호출 직전에 status bar 한 줄 자동 출력:
54
+ ```
55
+ 💬 ctx 12msg ~3.4k/200k ████░░░░░░░░░░ 1.7% · agents: *claude ✓codex ·gemini ·copilot · ▶stream
56
+ ```
57
+ - `ctx Nmsg` — 누적 대화 메시지 수
58
+ - `~XXk/YYk` — 추정 토큰 / provider별 컨텍스트 윈도우 (claude 200k, claude-4-7 1M, gpt-5 200k, gpt-4.1 1M, gemini 1M, copilot 64k, ollama 8k)
59
+ - `████░░░░░░░░░░ %` — 14문자 progress bar (50% 이상 cyan, 80% 이상 yellow 경고)
60
+ - 토큰 추정 휴리스틱: `chars / 3.5` (CJK/영문 평균치)
61
+
62
+ ### 2. 서브 에이전트 활성화 가시화 (한 줄 5종 표시)
63
+ - `*claude` — 현재 활성 provider (bold + green)
64
+ - `✓codex` — 설치+인증 완료 ready (green)
65
+ - `·gemini` — 미설치 또는 비활성 (dim)
66
+ - 5종 모두 한 줄에 표시 → 사용자가 즉시 다른 provider 가용성 인지 → Tab 키로 전환
67
+
68
+ ### 3. 정상 완료 작업 초록색 강조 (사용자 명시)
69
+ - assistant 응답 헤더에 `✓` 마커 + `C.green()` 적용
70
+ - stream 모드: `✓ [assistant: claude/sonnet-4-7, role=actor, 1234ms · 567자]` (green)
71
+ - non-stream: `✓ assistant (claude, role=actor, 1234ms)` (green bold)
72
+ - 기존 `[assistant: ...]` (dim) → `✓ [assistant: ...]` (green)
73
+
74
+ ### 4. dogfood 신규 프로젝트 직접 구동 검증
75
+ - 임시 디렉토리에 빈 프로젝트 생성 → `leerness init` 정상 동작 확인
76
+ - `leerness agent --interactive` 진입 흐름 검증 (provider 없음 시 안내 출력)
77
+ - `--version` 등 핵심 명령 동작 확인
78
+
79
+ ### 5. promptWithStatus wrapper 도입
80
+ - `rl.prompt()` 7회 호출처 모두 `promptWithStatus()` 로 통일
81
+ - line handler / cycle / 초기 진입 등 일관된 status bar 노출
82
+ - wrapper 내부 1건만 실제 `rl.prompt()` 호출 (DRY)
83
+
84
+ ### 6. 누적 회귀 (1.9.207~245) — 모두 유지
85
+ - api-skill cache (1.9.245) + CJK 분류 (1.9.243) + env encoding --apply (1.9.242) + 모두 유지
86
+ - handoff JSON 11 필드 · MCP 70 도구 · CJK + py-check + 비정상종료 모두 유지
87
+
88
+ ### 7. stress-v191 — **23/23 PASS · 100%**
89
+ - 1.9.246 (11): VERSION + 4 helper + promptWithStatus + 컨텍스트 bar + sub-agent 가시화 + 초록색 ✓ + dogfood init + 통일성
90
+ - 성능 (1): cold start avg 378ms
91
+ - 누적 회귀 (11): 1.9.207~245
92
+
93
+ ### 8. 자동 release (108 라운드 main-push streak · 69 라운드 npm publish streak · R202)
94
+
95
+ 🎯 **REPL UX 사용자 요구 100% 반영** — 컨텍스트 + 에이전트 + 초록색 강조 통합 status bar.
96
+
97
+ ---
98
+
3
99
  ## 1.9.245 — 2026-05-26 — UR-0015 API skill cache
4
100
 
5
101
  **📚 사용자 명시 (UR-0015): API 문서/관련링크 자동 정리 + AI 자동 참조 시스템.**
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **AI 코딩 에이전트의 거짓 완료·중복·망각·충돌을 막아주는 검수·기억·협업 CLI 하네스.**
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.245-green)]() [![tests](https://img.shields.io/badge/e2e-217%2F217-success)]() [![stress](https://img.shields.io/badge/stress--v190-26%2F26-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-70-brightgreen)]() [![cli](https://img.shields.io/badge/CLI_commands-57-brightgreen)]() [![json-fields](https://img.shields.io/badge/JSON_통합-11_필드-blueviolet)]() [![rounds](https://img.shields.io/badge/autonomous_R-201-blueviolet)]() [![main-push](https://img.shields.io/badge/🎉_main--push_streak-107_rounds-success)]() [![npm-streak](https://img.shields.io/badge/🎉_npm--publish-68_streak-success)]() [![api-skill](https://img.shields.io/badge/📚_API_skill_cache-UR--0015-blueviolet)]() [![perf](https://img.shields.io/badge/cold_start-331ms-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.247-green)]() [![tests](https://img.shields.io/badge/e2e-217%2F217-success)]() [![stress](https://img.shields.io/badge/stress--v192-20%2F20-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-70-brightgreen)]() [![cli](https://img.shields.io/badge/CLI_commands-57-brightgreen)]() [![json-fields](https://img.shields.io/badge/JSON_통합-11_필드-blueviolet)]() [![rounds](https://img.shields.io/badge/autonomous_R-203-blueviolet)]() [![main-push](https://img.shields.io/badge/🎉_main--push_streak-109_rounds-success)]() [![npm-streak](https://img.shields.io/badge/🎉_npm--publish-70_streak-success)]() [![fallback](https://img.shields.io/badge/🔁_REPL_fallback-multi--provider-blueviolet)]() [![perf](https://img.shields.io/badge/cold_start-588ms-success)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
6
6
 
7
7
  ```
8
8
  ╔══════════════════════════════════════════════════════════════╗
@@ -12,9 +12,9 @@
12
12
  ║ ██║ ██╔══╝ ██╔══╝ ██╔══██╗██║╚██╗██║██╔══╝ ╚════██║ ║
13
13
  ║ ███████╗███████╗███████╗██║ ██║██║ ╚████║███████╗███████║ ║
14
14
  ║ ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝ ║
15
- ║ v1.9.245 AI Agent Reliability Harness + Sandbox ║
15
+ ║ v1.9.247 AI Agent Reliability Harness + Sandbox ║
16
16
  ║ verify · remember · orchestrate · audit · sandbox · drift ║
17
- 📚 API skill cache (UR-0015) 공식 문서 자동 정리 + AI 참조
17
+ 🔁 REPL multi-provider fallback + 📚 api-skill audit
18
18
  ╚══════════════════════════════════════════════════════════════╝
19
19
  ```
20
20
 
package/bin/harness.js CHANGED
@@ -7,7 +7,7 @@ const cp = require('child_process');
7
7
  const os = require('os'); // 1.9.178: _publishToNpm 에서 os.tmpdir() 사용 (전역 import)
8
8
  const readline = require('readline');
9
9
 
10
- const VERSION = '1.9.245';
10
+ const VERSION = '1.9.247';
11
11
 
12
12
  // 1.9.184: DEP0190 (child_process shell: true) deprecation warning 억제 (사용자 명시).
13
13
  // leerness 는 cross-platform PATH resolution 을 위해 shell: true 를 의도적으로 사용 (claude.cmd / ollama.cmd 등 Windows .cmd 처리).
@@ -6491,6 +6491,35 @@ function audit(root, opts = {}) {
6491
6491
  }
6492
6492
  } catch {}
6493
6493
  }
6494
+ // 1.9.247 (UR-0015 2단계): api-skill 참조 audit — API 관련 task 인데 .harness/api-skills/ 미참조 시 경고
6495
+ // 사용자 명시 (UR-0015): "AI가 정리해둔 파일이 참조되는지 확인"
6496
+ // 현재 in-progress task 의 request/nextAction 에 API 키워드 (URL, "API", "endpoint", "REST", "GraphQL", "OAuth", "webhook") 있는데
6497
+ // _matchAPISkills() 결과가 0 이면 → 경고 + leerness api-skill add <url> 안내
6498
+ try {
6499
+ const rows = readProgressRows(root);
6500
+ const ip = rows.find(r => r.status === 'in-progress');
6501
+ if (ip) {
6502
+ const taskText = (ip.request || '') + ' ' + (ip.nextAction || '') + ' ' + (ip.evidence || '');
6503
+ const apiKeywords = /\bAPI\b|endpoint|REST|GraphQL|OAuth|webhook|https?:\/\/[^\s]+/i;
6504
+ if (apiKeywords.test(taskText)) {
6505
+ const matched = _matchAPISkills(root, taskText);
6506
+ const allSkills = _listAPISkills(root);
6507
+ if (matched.length === 0) {
6508
+ warnings++;
6509
+ warn(`API 관련 task 감지 (현재: "${(ip.request || '').slice(0, 60)}") — .harness/api-skills/ 매칭 0건 (저장 ${allSkills.length})`);
6510
+ warn(` → leerness api-skill add <url> --direction "구현 방향" 으로 정리 권장 (1.9.245 UR-0015 / 1.9.247 audit)`);
6511
+ _finding('api_skill_missing', 'warn', 'API 관련 task 인데 .harness/api-skills/ 매칭 없음', {
6512
+ taskRequest: (ip.request || '').slice(0, 100),
6513
+ apiSkillsTotal: allSkills.length,
6514
+ matched: 0,
6515
+ hint: 'leerness api-skill add <url> --direction "..."'
6516
+ });
6517
+ } else {
6518
+ ok(`API skill 매칭 OK (현재 task → ${matched.length}건 매칭 in .harness/api-skills/, 1.9.247 UR-0015 2단계)`);
6519
+ }
6520
+ }
6521
+ }
6522
+ } catch {}
6494
6523
  // 1.9.63: --strict — warnings ≥ threshold 시 failures로 승격 (CI 친화)
6495
6524
  if (has('--strict')) {
6496
6525
  const threshold = parseInt(arg('--threshold', '1'), 10);
@@ -16679,7 +16708,9 @@ async function _agentRepl(root, opts) {
16679
16708
  startedAt: new Date().toISOString(),
16680
16709
  sessionId: 'sess-' + new Date().toISOString().replace(/[:.]/g, '-'),
16681
16710
  // 1.9.170: 실시간 스트리밍 모드 (사용자 명시 — 추론중/diff/thinking 실시간 표시)
16682
- streamMode: opts.stream !== false // default ON (env 로 끄려면 LEERNESS_REPL_STREAM=0)
16711
+ streamMode: opts.stream !== false, // default ON (env 로 끄려면 LEERNESS_REPL_STREAM=0)
16712
+ // 1.9.247 (UR-0016 2단계): multi-provider auto-fallback — default OFF (opt-in)
16713
+ autoFallback: opts.autoFallback === true || process.env.LEERNESS_REPL_AUTO_FALLBACK === '1'
16683
16714
  };
16684
16715
  if (process.env.LEERNESS_REPL_STREAM === '0') state.streamMode = false;
16685
16716
  const sessionPath = () => path.join(absRoot(root), '.harness', 'agent-sessions', `${state.sessionId}.jsonl`);
@@ -16802,6 +16833,74 @@ async function _agentRepl(root, opts) {
16802
16833
  const modelTag = modelShort ? C.dim(' · ') + C.mag(modelShort) : '';
16803
16834
  return C.cy(`agent[${state.provider}${modelTag}${C.cy('/' + state.role)}${state.streamMode ? C.cy('/▶') : ''}${C.cy(']>')} `);
16804
16835
  };
16836
+ // 1.9.246: REPL UX/UI 개선 (사용자 명시 UR-0016) — 컨텍스트 게이지 + 서브 에이전트 가시화 + 정상 작업 강조
16837
+ // 3종 통합 status bar를 채팅 입력칸 직전에 표시.
16838
+ const _maxContextTokens = (provider, model) => {
16839
+ // provider/model 별 컨텍스트 윈도우 (대략값, 사용자 정보 제공용)
16840
+ const m = (model || '').toLowerCase();
16841
+ if (provider === 'claude') {
16842
+ if (m.includes('opus-4-7') || m.includes('sonnet-4-7') || m.includes('4-7')) return 1000000; // 1M extended
16843
+ if (m.includes('opus') || m.includes('sonnet') || m.includes('haiku')) return 200000;
16844
+ return 200000;
16845
+ }
16846
+ if (provider === 'codex' || provider === 'openai') {
16847
+ if (m.includes('gpt-5') || m.includes('5.5')) return 200000;
16848
+ if (m.includes('gpt-4-1') || m.includes('4.1')) return 1000000;
16849
+ if (m.includes('gpt-4')) return 128000;
16850
+ return 128000;
16851
+ }
16852
+ if (provider === 'gemini') return 1000000; // 2M까지 가능하지만 보수적으로 1M
16853
+ if (provider === 'copilot') return 64000;
16854
+ if (provider === 'ollama') return 8000; // 모델별 다르나 default 8k
16855
+ return 100000;
16856
+ };
16857
+ const _estimateTokens = (history) => {
16858
+ // 단순 휴리스틱: 영문 4chars=1tok, CJK 2chars=1tok 평균치 → 3.5chars/tok
16859
+ let totalChars = 0;
16860
+ for (const m of (history || [])) totalChars += (m.content || '').length;
16861
+ return Math.round(totalChars / 3.5);
16862
+ };
16863
+ const _renderStatusBar = () => {
16864
+ if (!isTty) return;
16865
+ try {
16866
+ // (1) 컨텍스트 게이지
16867
+ const msgs = (state.history || []).length;
16868
+ const tokens = _estimateTokens(state.history);
16869
+ const maxTok = _maxContextTokens(state.provider, state.model);
16870
+ const ratio = Math.min(1, tokens / maxTok);
16871
+ const pct = Math.round(ratio * 100);
16872
+ const barWidth = 14;
16873
+ const filled = Math.round(ratio * barWidth);
16874
+ const bar = '█'.repeat(filled) + '░'.repeat(barWidth - filled);
16875
+ const barColor = pct >= 80 ? C.yel : (pct >= 50 ? C.cy : C.green);
16876
+ const tokStr = tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k` : `${tokens}`;
16877
+ const maxStr = maxTok >= 1000 ? `${Math.round(maxTok / 1000)}k` : `${maxTok}`;
16878
+ // (2) 서브 에이전트 활성화 — EXTERNAL_AGENTS 5종 + ollama
16879
+ let ready = [];
16880
+ try {
16881
+ ready = EXTERNAL_AGENTS
16882
+ .map(a => ({ id: a.id, st: _checkAgent(a).status }))
16883
+ .filter(x => x.st === 'ready');
16884
+ } catch {}
16885
+ const allAgents = ['claude', 'codex', 'gemini', 'copilot'];
16886
+ const subBar = allAgents.map(id => {
16887
+ if (id === state.provider) return C.bold(C.green(`*${id}`));
16888
+ const isReady = ready.find(r => r.id === id);
16889
+ return isReady ? C.green(`✓${id}`) : C.dim(`·${id}`);
16890
+ }).join(' ');
16891
+ // (3) 한 줄 출력
16892
+ const line = ' ' + C.dim('💬 ctx ') + C.bold(`${msgs}msg`) + C.dim(` ~${tokStr}/${maxStr} `) + barColor(bar) + C.dim(` ${pct}%`)
16893
+ + C.dim(' · agents: ') + subBar
16894
+ + C.dim(' · ') + (state.streamMode ? C.green('▶stream') : C.dim('□stream'));
16895
+ process.stdout.write(line + '\n');
16896
+ } catch {}
16897
+ };
16898
+ // 1.9.246: rl.prompt() wrapper — status bar 자동 표시 (사용자 명시 UR-0016)
16899
+ const promptWithStatus = (forceRedraw) => {
16900
+ _renderStatusBar();
16901
+ if (forceRedraw === true) rl.prompt(true);
16902
+ else rl.prompt();
16903
+ };
16805
16904
  rl.setPrompt(prompt());
16806
16905
 
16807
16906
  // 1.9.244 HOTFIX: _lastCycleLines 를 outer 스코프로 hoist
@@ -16963,7 +17062,8 @@ async function _agentRepl(root, opts) {
16963
17062
  log(C.dim(' 💡 메시지는 직접 입력 · "/" 또는 ":" 접두사로 명령 호출 · Tab=provider · Shift+Tab=model'));
16964
17063
  log('');
16965
17064
  };
16966
- rl.prompt();
17065
+ // 1.9.246: 첫 prompt 표시 전에도 status bar 노출 (사용자 명시 UR-0016)
17066
+ promptWithStatus();
16967
17067
  const handleMeta = async (cmd) => {
16968
17068
  const [op, ...rest] = cmd.slice(1).split(/\s+/);
16969
17069
  if (op === 'quit' || op === 'exit' || op === 'q') {
@@ -17098,6 +17198,20 @@ async function _agentRepl(root, opts) {
17098
17198
  rl.setPrompt(prompt());
17099
17199
  return false;
17100
17200
  }
17201
+ // 1.9.247 (UR-0016 2단계): :fallback on|off — provider 실패 시 다음 ready agent 자동 전환 (opt-in)
17202
+ if (op === 'fallback') {
17203
+ const v = (rest[0] || '').toLowerCase();
17204
+ if (v === 'on' || v === '1' || v === 'true') {
17205
+ state.autoFallback = true;
17206
+ log(C.green(' ↪ auto-fallback: ON (1.9.247) — 현재 provider transient 실패 (timeout/exit=null) 시 다음 ready agent 자동 전환 + 재시도'));
17207
+ } else if (v === 'off' || v === '0' || v === 'false') {
17208
+ state.autoFallback = false;
17209
+ log(C.dim(' ↪ auto-fallback: OFF — 실패 시 사용자가 직접 :provider 전환'));
17210
+ } else {
17211
+ log(C.dim(` 현재 auto-fallback: ${state.autoFallback ? 'ON ↪' : 'OFF'} — 사용: :fallback on|off`));
17212
+ }
17213
+ return false;
17214
+ }
17101
17215
  if (op === 'clear') { process.stdout.write('\x1b[2J\x1b[H'); return false; }
17102
17216
  if (op === 'reset') { state.history = []; log(C.dim(' history 초기화됨')); return false; }
17103
17217
  if (op === 'history') {
@@ -17253,24 +17367,24 @@ async function _agentRepl(root, opts) {
17253
17367
  rl.on('line', async (line) => {
17254
17368
  const input = line.trim();
17255
17369
  _lastCycleLines = 0; // 1.9.189: 사용자 입력 시 cycle overwrite 추적 reset
17256
- if (!input) { rl.prompt(); return; }
17370
+ if (!input) { promptWithStatus(); return; }
17257
17371
  // 1.9.189 (사용자 명시): "/" slash 입력만으로 명령 list 표시 (claude code 영감).
17258
17372
  // "/" 단독 → 명령 list 출력 후 prompt 복귀.
17259
17373
  // "/help", "/quit" 등 "/" 접두사 → ":" 동등 처리 (alias).
17260
17374
  if (input === '/' || input === '/?') {
17261
17375
  _showSlashCommandList();
17262
- rl.prompt(); return;
17376
+ promptWithStatus(); return;
17263
17377
  }
17264
17378
  // 1.9.189: "/" 접두사를 ":" 와 동등하게 처리 (사용자 명시 — "/ 가 모든 모델 공통 권한 trigger default")
17265
17379
  if (input.startsWith('/') && !input.startsWith('//')) {
17266
17380
  const shouldQuit = await handleMeta(':' + input.slice(1));
17267
17381
  if (shouldQuit) { resolve(); return; }
17268
- rl.prompt(); return;
17382
+ promptWithStatus(); return;
17269
17383
  }
17270
17384
  if (input.startsWith(':')) {
17271
17385
  const shouldQuit = await handleMeta(input);
17272
17386
  if (shouldQuit) { resolve(); return; }
17273
- rl.prompt(); return;
17387
+ promptWithStatus(); return;
17274
17388
  }
17275
17389
  // LLM 호출
17276
17390
  state.history.push({ role: 'user', content: input });
@@ -17293,18 +17407,19 @@ async function _agentRepl(root, opts) {
17293
17407
  }
17294
17408
  } else {
17295
17409
  log(C.yel(` ⚠ ${state.provider} provider 미지원 — :provider ollama|claude|codex|gemini|copilot`));
17296
- rl.prompt(); return;
17410
+ promptWithStatus(); return;
17297
17411
  }
17298
17412
  const dt = Date.now() - t0;
17299
17413
  _recordRun(root, { kind: 'agent_repl_turn', provider: state.provider, model: state.model, role: state.role, durationMs: dt, ok: result.ok, error: result.error, promptChars: finalPrompt.length, responseChars: (result.response || '').length });
17300
17414
  if (result.ok) {
17301
17415
  state.history.push({ role: 'assistant', content: result.response });
17302
17416
  // 1.9.170: stream 모드에서는 이미 실시간으로 출력됐으므로 헤더만 표시 (응답 중복 방지)
17417
+ // 1.9.246: 정상 완료 작업은 초록색 ✓ 강조 (사용자 명시 UR-0016)
17303
17418
  if (state.streamMode && ['claude', 'codex', 'gemini', 'copilot'].includes(state.provider)) {
17304
- log(C.dim(` [assistant: ${state.provider}/${state.model || 'default'}, role=${state.role}, ${dt}ms · ${result.response.length}자]`));
17419
+ log(C.green(` [assistant: ${state.provider}/${state.model || 'default'}, role=${state.role}, ${dt}ms · ${result.response.length}자]`));
17305
17420
  } else {
17306
17421
  log('');
17307
- log(C.bold(`assistant (${state.model || state.provider}, role=${state.role}, ${dt}ms)`));
17422
+ log(C.green(`✓ assistant (${state.model || state.provider}, role=${state.role}, ${dt}ms)`));
17308
17423
  log(result.response);
17309
17424
  }
17310
17425
  // 1.9.188 (사용자 명시): 응답 끝 + 입력 구분선 (입력칸 시각 명확)
@@ -17323,6 +17438,47 @@ async function _agentRepl(root, opts) {
17323
17438
  log(C.dim(` ↳ 가능 원인: 인증 누락 (login 필요)`));
17324
17439
  log(C.dim(` ↳ 해결: ${state.provider} login ${state.provider === 'copilot' ? '(gh auth login)' : ''}`));
17325
17440
  }
17441
+ // 1.9.247 (UR-0016 2단계): 자동 fallback — 사용자 명시 (`:fallback on` 또는 LEERNESS_REPL_AUTO_FALLBACK=1) 시
17442
+ // 현재 provider 가 transient 실패 (timeout/exit=null/network) 시 다음 ready agent 로 자동 전환 + 1회 재시도.
17443
+ // 사용자 명시 (이전 codex CLI exit=null 보고): "멀티 provider auto-fallback 도 검토".
17444
+ const isTransient = /exit=null|timeout/i.test(errMsg);
17445
+ if (state.autoFallback && isTransient) {
17446
+ try {
17447
+ const others = EXTERNAL_AGENTS.filter(a => a.id !== state.provider)
17448
+ .map(a => ({ def: a, status: _checkAgent(a) }))
17449
+ .filter(x => x.status.status === 'ready');
17450
+ if (others.length > 0) {
17451
+ const next = others[0].def.id;
17452
+ log(C.green(` ↪ auto-fallback (1.9.247): ${state.provider} 실패 → ${next} 자동 전환 + 재시도 중...`));
17453
+ state.provider = next;
17454
+ state.model = null;
17455
+ const cat = _PROVIDER_MODEL_CATALOG[state.provider];
17456
+ if (cat && cat.length) state.model = cat[0].id;
17457
+ rl.setPrompt(prompt());
17458
+ const t1 = Date.now();
17459
+ let retry;
17460
+ if (state.streamMode) retry = await _cliChatStream(root, state.provider, finalPrompt, { timeout: 120000 });
17461
+ else retry = await _cliChat(root, state.provider, finalPrompt, { timeout: 90000 });
17462
+ const dt1 = Date.now() - t1;
17463
+ _recordRun(root, { kind: 'agent_repl_turn_fallback', provider: state.provider, model: state.model, role: state.role, durationMs: dt1, ok: retry.ok, error: retry.error, promptChars: finalPrompt.length, responseChars: (retry.response || '').length });
17464
+ if (retry.ok) {
17465
+ state.history.push({ role: 'assistant', content: retry.response });
17466
+ log(C.green(` ✓ fallback 성공 [${state.provider}/${state.model || 'default'}, ${dt1}ms · ${retry.response.length}자]`));
17467
+ if (!state.streamMode) log(retry.response);
17468
+ _printInputDivider();
17469
+ if (state.history.length % 6 === 0) saveSession();
17470
+ promptWithStatus();
17471
+ return;
17472
+ } else {
17473
+ log(C.yel(` ⚠ fallback 도 실패 (${next}): ${retry.error || 'unknown'}`));
17474
+ }
17475
+ } else {
17476
+ log(C.dim(` (auto-fallback: 다른 ready provider 없음)`));
17477
+ }
17478
+ } catch (e) {
17479
+ log(C.dim(` (auto-fallback 오류: ${e.message})`));
17480
+ }
17481
+ }
17326
17482
  // 전환 가능 provider 안내
17327
17483
  try {
17328
17484
  const others = EXTERNAL_AGENTS.filter(a => a.id !== state.provider)
@@ -17330,12 +17486,13 @@ async function _agentRepl(root, opts) {
17330
17486
  .filter(x => x.status.status === 'ready');
17331
17487
  if (others.length) {
17332
17488
  log(C.dim(` 💡 즉시 전환: ${others.map(x => `:provider ${x.def.id}`).join(' · ')} 또는 Tab 키`));
17489
+ if (!state.autoFallback) log(C.dim(` 💡 다음 실패 시 자동 전환: \`:fallback on\` (1.9.247 UR-0016 2단계)`));
17333
17490
  } else {
17334
17491
  log(C.dim(` 💡 다른 provider 활성화: .env 에서 LEERNESS_ENABLE_<CLAUDE|CODEX|GEMINI|COPILOT>=1`));
17335
17492
  }
17336
17493
  } catch {}
17337
17494
  }
17338
- rl.prompt();
17495
+ promptWithStatus();
17339
17496
  });
17340
17497
  rl.on('close', () => { saveSession(); resolve(); });
17341
17498
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.245",
3
+ "version": "1.9.247",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",