leerness 1.9.169 → 1.9.170

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,77 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.170 — 2026-05-21 — 🎉 100 라운드 자율 마일스톤
4
+
5
+ **사용자 명시 요청 2종: REPL Tab cycle provider/model + 실시간 스트리밍.**
6
+
7
+ 자율 모드 100 라운드 도달 (1.9.71~1.9.170). 사용자 직접 요청 2종 통합:
8
+ 1. **Tab 키 cycle** — provider/model 빠른 전환
9
+ 2. **실시간 스트리밍** — 추론중/diff/thinking 과정 즉시 표시
10
+
11
+ ### 1. REPL Tab cycle (사용자 명시 — "탭 키 등으로 provider/모델 셀렉과 선택 간편하게")
12
+
13
+ ```
14
+ agent[claude/actor/▶]> [Tab] # → ollama로 cycle
15
+ agent[ollama/actor/▶]> [Tab] # → claude로 cycle
16
+ agent[claude/actor/▶]> [Shift+Tab] # → 현재 provider의 다음 model로
17
+ ⇄ model: claude-opus-4-7 — 최신 1M context (Anthropic Opus 4.7)
18
+ ```
19
+
20
+ **구현**:
21
+ - `readline.emitKeypressEvents(process.stdin, rl)` — Tab 키 감지 활성화
22
+ - `process.stdin.on('keypress', ...)` — Tab/Shift+Tab 가로채서 cycle 실행
23
+ - `getProviders()` — 빌트인 5종 + `.harness/providers.json` 사용자 정의 통합
24
+ - prompt 갱신: `agent[provider/role/▶]>` (스트리밍 ON 시 ▶ 표시)
25
+
26
+ ### 2. 실제 모델 catalog 확장 (사용자 명시 — "gpt-5.5, gpt-5.4, claude opus4.7 등 실제 모델")
27
+
28
+ | Provider | 모델 |
29
+ |---|---|
30
+ | **claude** | **claude-opus-4-7** (1M context), opus-4-5, sonnet-4-7, sonnet-4-5, haiku-4-5 |
31
+ | **codex** | **gpt-5.5** (최신 추론), gpt-5.4, gpt-5, gpt-5-codex, o4-mini |
32
+ | **gemini** | gemini-2.5-pro, gemini-2.5-flash, gemini-3.0-pro |
33
+ | ollama | llama3, qwen2.5-coder, gpt-oss, deepseek-coder-v2 |
34
+ | copilot | default |
35
+
36
+ ### 3. 실시간 스트리밍 (사용자 명시 — "추론중, diff, 생각하는 과정 실시간 표시")
37
+
38
+ ```
39
+ user> 이 함수 리팩토링 해줘
40
+ → claude CLI stream 호출 중... (Ctrl+C 로 중단)
41
+ ── claude stream ──
42
+ [claude-opus-4-7] 분석 중...
43
+ function refactored() {
44
+ // 실시간으로 한 글자씩 흘러나옴
45
+ ...
46
+ }
47
+ ── /stream (3421ms) ──
48
+ [assistant: claude/claude-opus-4-7, role=actor, 3421ms · 1248자]
49
+ ```
50
+
51
+ **구현**:
52
+ - `_cliChatStream()` — `cp.spawn(..., { stdio: ['ignore', 'pipe', 'pipe'] })`
53
+ - Claude: `--output-format=stream-json --verbose` 활용 → `content_block_delta` / `thinking_delta` 파싱
54
+ - 다른 provider (codex/gemini/copilot): stdout raw pipe
55
+ - env scrub + cwd jail 유지 (1.9.150 sandboxing 호환)
56
+ - observability: `_recordRun(kind: 'agent_repl_cli_stream')`
57
+ - `:stream on|off` 토글, default ON (env `LEERNESS_REPL_STREAM=0` 로 끄기)
58
+
59
+ ### 100 라운드 마일스톤
60
+ | 마일스톤 | 라운드 | 도구 |
61
+ |---|---|---|
62
+ | MCP 30 도구 | 1.9.110 | Memory CRUD 완성 |
63
+ | MCP 50 도구 | 1.9.159 | Provider Registry CRUD |
64
+ | 90 라운드 | 1.9.160 | provider sync |
65
+ | MCP 53 도구 | 1.9.168 | Bridge 3종 외부 노출 |
66
+ | **100 라운드** | **1.9.170** | **🎉 Tab cycle + 실시간 스트리밍** |
67
+
68
+ ### Verified
69
+ - e2e 217/217 baseline 유지 (1.9.169 hotfix 후)
70
+ - stress-v115: **23/23** (catalog 4 + Tab cycle 3 + 스트리밍 4 + :stream + REPL 통합 5 + 누적 회귀 7)
71
+ - VERSION = 1.9.170 / autonomous-rounds = **100** 🎉 / main 자동 push 31 라운드 연속
72
+
73
+ ---
74
+
3
75
  ## 1.9.169 — 2026-05-20
4
76
 
5
77
  **🔧 Hotfix — `_collectWorkspacePaths()` --include 명시 시 cwd 자동 추가 안 함.**
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.169-green)]() [![tests](https://img.shields.io/badge/e2e-217%2F217-success)]() [![stress](https://img.shields.io/badge/stress--v114-14%2F14-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-53-brightgreen)]() [![rounds](https://img.shields.io/badge/autonomous--rounds-99-blueviolet)]() [![main-push](https://img.shields.io/badge/release--main--push-30_rounds-success)]() [![hotfix](https://img.shields.io/badge/1.9.169-include_explicit--only-success)]() [![mcp-bridge](https://img.shields.io/badge/MCP_bridge-web%2Fpc%2Flsp_노출-success)]() [![lsp-bridge](https://img.shields.io/badge/lsp_bridge-typescript_opt--in%2Bregex_fallback-success)]() [![pc-bridge](https://img.shields.io/badge/pc_bridge-robotjs%2Fnut--tree_opt--in-success)]() [![web-bridge](https://img.shields.io/badge/playwright_bridge-opt--in_MVP-success)]() [![capability](https://img.shields.io/badge/6_capability-72%25_production--ready-brightgreen)]() [![sandbox](https://img.shields.io/badge/runCommandSafe-cwd_jail%2Benv_scrub-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.170-green)]() [![tests](https://img.shields.io/badge/e2e-217%2F217-success)]() [![stress](https://img.shields.io/badge/stress--v115-23%2F23-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-53-brightgreen)]() [![rounds](https://img.shields.io/badge/autonomous--rounds-100🎉-blueviolet)]() [![main-push](https://img.shields.io/badge/release--main--push-31_rounds-success)]() [![repl-tab](https://img.shields.io/badge/REPL-Tab_cycle%2B실시간_스트리밍-success)]() [![mcp-bridge](https://img.shields.io/badge/MCP_bridge-web%2Fpc%2Flsp_노출-success)]() [![lsp-bridge](https://img.shields.io/badge/lsp_bridge-typescript_opt--in%2Bregex_fallback-success)]() [![pc-bridge](https://img.shields.io/badge/pc_bridge-robotjs%2Fnut--tree_opt--in-success)]() [![web-bridge](https://img.shields.io/badge/playwright_bridge-opt--in_MVP-success)]() [![capability](https://img.shields.io/badge/6_capability-72%25_production--ready-brightgreen)]() [![sandbox](https://img.shields.io/badge/runCommandSafe-cwd_jail%2Benv_scrub-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.169 AI Agent Reliability Harness + Sandbox ║
15
+ ║ v1.9.170 AI Agent Reliability Harness + Sandbox ║
16
16
  ║ verify · remember · orchestrate · audit · sandbox · drift ║
17
- hotfix --include explicit-only · 30 라운드 main 자동 push
17
+ 🎉 100 라운드 자율 마일스톤 · REPL Tab cycle + 실시간 스트림
18
18
  ╚══════════════════════════════════════════════════════════════╝
19
19
  ```
20
20
 
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.169';
9
+ const VERSION = '1.9.170';
10
10
  const MARK = '<!-- leerness:managed -->';
11
11
  const README_START = '<!-- leerness:project-readme:start -->';
12
12
  const README_END = '<!-- leerness:project-readme:end -->';
@@ -10551,6 +10551,108 @@ async function _cliChat(root, provider, prompt, opts) {
10551
10551
  };
10552
10552
  }
10553
10553
 
10554
+ // 1.9.170: REPL 실시간 스트리밍 — CLI stdout을 즉시 터미널에 표시 (사용자 명시 요청)
10555
+ // 추론중/diff/thinking 과정이 stdout 으로 흐르는 동안 실시간 렌더링.
10556
+ // runCommandSafe 의 배치 결과와 달리 cp.spawn 의 child.stdout 을 직접 pipe.
10557
+ // env scrub + cwd jail 은 동일하게 적용.
10558
+ async function _cliChatStream(root, provider, promptText, opts) {
10559
+ opts = opts || {};
10560
+ const agent = EXTERNAL_AGENTS.find(a => a.id === provider);
10561
+ if (!agent) return { ok: false, error: `unknown provider: ${provider}`, provider };
10562
+ const status = _checkAgent(agent);
10563
+ if (status.status !== 'ready') {
10564
+ return { ok: false, error: `${provider} 비활성 (${status.status}) — .env 에서 ${agent.envFlag}=1 필요`, provider };
10565
+ }
10566
+ let cmd, args;
10567
+ if (provider === 'claude') { cmd = 'claude'; args = ['--print', '--output-format=stream-json', '--verbose', promptText]; }
10568
+ else if (provider === 'codex') { cmd = 'codex'; args = ['exec', '--skip-git-repo-check', promptText]; }
10569
+ else if (provider === 'gemini') { cmd = 'gemini'; args = ['-p', promptText]; }
10570
+ else if (provider === 'copilot') { cmd = 'gh'; args = ['copilot', 'suggest', promptText]; }
10571
+ else return { ok: false, error: `provider ${provider} 미지원`, provider };
10572
+ const t0 = Date.now();
10573
+ return new Promise(resolve => {
10574
+ let out = '', err = '';
10575
+ let buf = ''; // claude stream-json 라인 버퍼
10576
+ let firstChunk = true;
10577
+ const isTty = process.stdout.isTTY;
10578
+ const dim = isTty ? (s) => `\x1b[2m${s}\x1b[0m` : s => s;
10579
+ const cy = isTty ? (s) => `\x1b[36m${s}\x1b[0m` : s => s;
10580
+ process.stdout.write(dim(`\n ── ${provider} stream ──\n`));
10581
+ let child;
10582
+ try {
10583
+ child = cp.spawn(cmd, args, {
10584
+ cwd: process.cwd(),
10585
+ env: _scrubEnv({}),
10586
+ shell: false,
10587
+ stdio: ['ignore', 'pipe', 'pipe']
10588
+ });
10589
+ } catch (e) {
10590
+ return resolve({ ok: false, error: 'spawn 실패: ' + e.message, provider });
10591
+ }
10592
+ const timer = setTimeout(() => {
10593
+ try { child.kill('SIGTERM'); } catch {}
10594
+ }, opts.timeout || 120000);
10595
+
10596
+ // Claude stream-json 모드: 각 라인이 JSON event. type=assistant_message_delta.text 만 출력
10597
+ function handleClaudeStream(chunk) {
10598
+ buf += chunk.toString();
10599
+ const lines = buf.split('\n');
10600
+ buf = lines.pop() || '';
10601
+ for (const ln of lines) {
10602
+ if (!ln.trim()) continue;
10603
+ try {
10604
+ const ev = JSON.parse(ln);
10605
+ // assistant 메시지 chunk만 표시 (thinking/system 은 dim)
10606
+ if (ev.type === 'content_block_delta' && ev.delta?.text) {
10607
+ process.stdout.write(ev.delta.text);
10608
+ out += ev.delta.text;
10609
+ } else if (ev.type === 'message_start' && firstChunk) {
10610
+ firstChunk = false;
10611
+ process.stdout.write(dim(` [${ev.message?.model || provider}] `));
10612
+ } else if (ev.type === 'thinking_delta' && ev.delta?.thinking) {
10613
+ process.stdout.write(dim(ev.delta.thinking));
10614
+ }
10615
+ } catch {
10616
+ // JSON 아니면 그대로 출력 (fallback)
10617
+ process.stdout.write(ln + '\n');
10618
+ out += ln + '\n';
10619
+ }
10620
+ }
10621
+ }
10622
+
10623
+ child.stdout.on('data', chunk => {
10624
+ if (provider === 'claude') {
10625
+ handleClaudeStream(chunk);
10626
+ } else {
10627
+ const s = chunk.toString();
10628
+ out += s;
10629
+ process.stdout.write(s);
10630
+ }
10631
+ });
10632
+ child.stderr.on('data', chunk => {
10633
+ const s = chunk.toString();
10634
+ err += s;
10635
+ // 추론중/진행 메시지는 dim 으로 (실제 에러는 출력 후에 종합)
10636
+ process.stdout.write(dim(s));
10637
+ });
10638
+ child.on('error', e => {
10639
+ clearTimeout(timer);
10640
+ resolve({ ok: false, error: 'child error: ' + e.message, provider });
10641
+ });
10642
+ child.on('close', code => {
10643
+ clearTimeout(timer);
10644
+ const dt = Date.now() - t0;
10645
+ process.stdout.write(dim(`\n ── /stream (${dt}ms) ──\n`));
10646
+ try { _recordRun(root, { kind: 'agent_repl_cli_stream', provider, durationMs: dt, ok: code === 0, exitCode: code, responseChars: out.length }); } catch {}
10647
+ if (code === 0) {
10648
+ resolve({ ok: true, response: out.trim(), provider, model: provider, durationMs: dt });
10649
+ } else {
10650
+ resolve({ ok: false, error: `exit=${code} ${(err || out).slice(0, 200)}`, provider, durationMs: dt });
10651
+ }
10652
+ });
10653
+ });
10654
+ }
10655
+
10554
10656
  // 1.9.149: observability lite — 모든 agent 호출의 traceId + duration + exit + failureCause 기록
10555
10657
  function _runsDir(root) { return path.join(absRoot(root), '.harness', 'runs'); }
10556
10658
  function _recordRun(root, entry) {
@@ -10701,20 +10803,27 @@ function runsShowCmd(root, id) {
10701
10803
  log(read(fp));
10702
10804
  }
10703
10805
  // 1.9.155: provider 별 추천 모델 카탈로그 — REPL :models 명령에서 노출 (실제 가용성은 사용자 CLI 가 결정)
10806
+ // 1.9.170: provider × 실제 모델 catalog 확장 (Tab cycle 지원 — 사용자 명시 요청)
10807
+ // 각 provider 의 최신 실제 모델 ID 를 반영. Tab/Shift+Tab 키로 cycle.
10704
10808
  const _PROVIDER_MODEL_CATALOG = {
10705
10809
  claude: [
10706
- { id: 'claude-opus-4-5', note: '최고 추론 (Anthropic)' },
10707
- { id: 'claude-sonnet-4-5', note: '균형형 (속도/품질)' },
10708
- { id: 'claude-haiku-4-5', note: '빠름' }
10810
+ { id: 'claude-opus-4-7', note: '최신 1M context (Anthropic Opus 4.7)' },
10811
+ { id: 'claude-opus-4-5', note: '안정 Opus 4.5' },
10812
+ { id: 'claude-sonnet-4-7', note: '균형형 — Sonnet 4.7 (속도/품질)' },
10813
+ { id: 'claude-sonnet-4-5', note: 'Sonnet 4.5 안정' },
10814
+ { id: 'claude-haiku-4-5', note: '빠름 — Haiku 4.5' }
10709
10815
  ],
10710
10816
  codex: [
10711
- { id: 'gpt-5', note: 'OpenAI 최신' },
10712
- { id: 'gpt-5-codex', note: '코드 특화' },
10817
+ { id: 'gpt-5.5', note: 'OpenAI 최신 추론 모델' },
10818
+ { id: 'gpt-5.4', note: 'OpenAI 안정 (이전 세대)' },
10819
+ { id: 'gpt-5', note: 'OpenAI gpt-5 (base)' },
10820
+ { id: 'gpt-5-codex', note: '코드 특화 (Codex)' },
10713
10821
  { id: 'o4-mini', note: '빠른 reasoning' }
10714
10822
  ],
10715
10823
  gemini: [
10716
- { id: 'gemini-2.5-pro', note: 'Google 최고급' },
10717
- { id: 'gemini-2.5-flash', note: '빠른 응답' }
10824
+ { id: 'gemini-2.5-pro', note: 'Google 최고급 (1M+ context)' },
10825
+ { id: 'gemini-2.5-flash', note: '빠른 응답' },
10826
+ { id: 'gemini-3.0-pro', note: '실험적 (사용 가능 시)' }
10718
10827
  ],
10719
10828
  copilot: [
10720
10829
  { id: 'default', note: 'gh copilot 기본 (모델 선택 불가)' }
@@ -10722,10 +10831,14 @@ const _PROVIDER_MODEL_CATALOG = {
10722
10831
  ollama: [
10723
10832
  { id: 'llama3', note: 'Meta — :models 로 실시간 조회 권장' },
10724
10833
  { id: 'qwen2.5-coder', note: 'Alibaba — 코드 특화' },
10725
- { id: 'gpt-oss', note: 'OpenAI 오픈소스' }
10834
+ { id: 'gpt-oss', note: 'OpenAI 오픈소스' },
10835
+ { id: 'deepseek-coder-v2', note: 'DeepSeek 코드 모델' }
10726
10836
  ]
10727
10837
  };
10728
10838
 
10839
+ // 1.9.170: provider cycle 순서 (Tab) — 빌트인 5종. user provider는 동적으로 뒤에 추가.
10840
+ const _PROVIDER_CYCLE_ORDER = ['ollama', 'claude', 'codex', 'gemini', 'copilot'];
10841
+
10729
10842
  // 1.9.148: planner/reviewer/actor 역할 시스템 프롬프트 (Gemini 권고 — 자기-승인 편향 방지)
10730
10843
  const _AGENT_ROLE_PROMPTS = {
10731
10844
  planner: '역할: planner. task를 step 3-6개로 분해, 각 step의 입출력/검증 방법 명시. 코드 작성 금지, 계획만.',
@@ -10770,8 +10883,11 @@ async function _agentRepl(root, opts) {
10770
10883
  role: opts.role || 'actor',
10771
10884
  history: [], // [{role: 'user'|'assistant', content: ''}]
10772
10885
  startedAt: new Date().toISOString(),
10773
- sessionId: 'sess-' + new Date().toISOString().replace(/[:.]/g, '-')
10886
+ sessionId: 'sess-' + new Date().toISOString().replace(/[:.]/g, '-'),
10887
+ // 1.9.170: 실시간 스트리밍 모드 (사용자 명시 — 추론중/diff/thinking 실시간 표시)
10888
+ streamMode: opts.stream !== false // default ON (env 로 끄려면 LEERNESS_REPL_STREAM=0)
10774
10889
  };
10890
+ if (process.env.LEERNESS_REPL_STREAM === '0') state.streamMode = false;
10775
10891
  const sessionPath = () => path.join(absRoot(root), '.harness', 'agent-sessions', `${state.sessionId}.jsonl`);
10776
10892
  const saveSession = () => {
10777
10893
  try {
@@ -10834,6 +10950,7 @@ async function _agentRepl(root, opts) {
10834
10950
  log(C.dim(' 메타 명령: :help | :model <m> | :role <r> | :provider <p> | :status | :clear | :save | :history | :quit'));
10835
10951
  log(C.dim(' Slash 명령 (1.9.150): :verify | :audit | :handoff | :health'));
10836
10952
  log(C.dim(' Memory Slash (1.9.161): :lessons | :brainstorm <topic> | :tasks | :plan'));
10953
+ log(C.dim(' 🆕 1.9.170 — Tab=provider cycle, Shift+Tab=model cycle, :stream on|off (실시간 출력)'));
10837
10954
  log(C.dim(` 현재 — provider=${state.provider} model=${state.model || '(기본)'} role=${state.role} permissions=${_readPermissions(root).mode}`));
10838
10955
  // 1.9.155: REPL 진입 시 handoff 컨텍스트 자동 노출 (UX 개선 — 사용자가 매번 :handoff 안 해도 컨텍스트 인지)
10839
10956
  try {
@@ -10850,8 +10967,73 @@ async function _agentRepl(root, opts) {
10850
10967
  }
10851
10968
  } catch {}
10852
10969
  log('');
10853
- const prompt = () => isTty ? C.cy(`agent[${state.role}]> `) : 'agent> ';
10970
+ const prompt = () => isTty ? C.cy(`agent[${state.provider}/${state.role}${state.streamMode ? '/▶' : ''}]> `) : 'agent> ';
10854
10971
  rl.setPrompt(prompt());
10972
+
10973
+ // 1.9.170: Tab cycle — provider (Tab) / model within provider (Shift+Tab)
10974
+ // 사용자 명시 요청: "탭 키 등으로 provider/모델 셀렉과 선택을 간편하게"
10975
+ // readline의 default tab=completion 동작을 keypress 리스너로 가로채서 cycle 수행.
10976
+ if (isTty) {
10977
+ try {
10978
+ const readlineLib = require('readline');
10979
+ readlineLib.emitKeypressEvents(process.stdin, rl);
10980
+ const getProviders = () => {
10981
+ // 빌트인 5종 + 사용자 정의 provider (.harness/providers.json)
10982
+ try {
10983
+ const userPath = path.join(absRoot(root), '.harness', 'providers.json');
10984
+ if (fs.existsSync(userPath)) {
10985
+ const j = JSON.parse(fs.readFileSync(userPath, 'utf8'));
10986
+ const userIds = (j.providers || []).map(p => p.id).filter(id => !_PROVIDER_CYCLE_ORDER.includes(id));
10987
+ return [..._PROVIDER_CYCLE_ORDER, ...userIds];
10988
+ }
10989
+ } catch {}
10990
+ return _PROVIDER_CYCLE_ORDER.slice();
10991
+ };
10992
+ const cycleProvider = (reverse) => {
10993
+ const list = getProviders();
10994
+ let idx = list.indexOf(state.provider);
10995
+ if (idx < 0) idx = 0;
10996
+ idx = reverse ? (idx - 1 + list.length) % list.length : (idx + 1) % list.length;
10997
+ state.provider = list[idx];
10998
+ state.model = null; // 새 provider 기본 모델
10999
+ const cat = _PROVIDER_MODEL_CATALOG[state.provider];
11000
+ const hint = cat?.length ? ` (${cat.length}개 모델 catalog — Shift+Tab으로 cycle)` : '';
11001
+ // 현재 입력 라인 보존: cursor를 라인 시작으로 이동 → 클리어 → status + prompt 재출력
11002
+ process.stdout.write('\r\x1b[K');
11003
+ process.stdout.write(C.green(` ⇄ provider: ${state.provider}${hint}\n`));
11004
+ rl.setPrompt(prompt());
11005
+ rl.prompt(true); // preserve cursor
11006
+ };
11007
+ const cycleModel = (reverse) => {
11008
+ const cat = _PROVIDER_MODEL_CATALOG[state.provider] || [];
11009
+ if (cat.length === 0) {
11010
+ process.stdout.write('\r\x1b[K');
11011
+ process.stdout.write(C.yel(` ⚠ ${state.provider} 추천 모델 catalog 없음 (:model <name> 으로 직접 지정)\n`));
11012
+ rl.prompt(true);
11013
+ return;
11014
+ }
11015
+ let idx = cat.findIndex(m => m.id === state.model);
11016
+ if (idx < 0) idx = -1;
11017
+ idx = reverse ? (idx - 1 + cat.length) % cat.length : (idx + 1) % cat.length;
11018
+ state.model = cat[idx].id;
11019
+ process.stdout.write('\r\x1b[K');
11020
+ process.stdout.write(C.green(` ⇄ model: ${state.model} ${C.dim('— ' + (cat[idx].note || ''))}\n`));
11021
+ rl.setPrompt(prompt());
11022
+ rl.prompt(true);
11023
+ };
11024
+ process.stdin.on('keypress', (str, key) => {
11025
+ if (!key) return;
11026
+ if (key.name === 'tab') {
11027
+ cycleProvider(key.shift === true);
11028
+ }
11029
+ // Shift+\ 또는 다른 모델 cycle alias — 일부 터미널에서 Shift+Tab 처리 어려움 대비
11030
+ // (Shift+Tab은 key.name='tab' + key.shift=true 로 위에서 처리됨)
11031
+ });
11032
+ } catch (e) {
11033
+ log(C.dim(` (Tab cycle 비활성: ${e.message})`));
11034
+ }
11035
+ }
11036
+
10855
11037
  rl.prompt();
10856
11038
  const handleMeta = async (cmd) => {
10857
11039
  const [op, ...rest] = cmd.slice(1).split(/\s+/);
@@ -10863,10 +11045,11 @@ async function _agentRepl(root, opts) {
10863
11045
  if (op === 'help' || op === '?') {
10864
11046
  log(C.bold('\n 메타 명령 (provider/모델/역할 전환):'));
10865
11047
  log(' :help / :? — 이 도움말');
10866
- log(' :model <name> — 모델 변경 (1.9.155 모든 provider 지원, 예: :model claude-opus-4-5)');
11048
+ log(' :model <name> — 모델 변경 (1.9.155 모든 provider 지원, 예: :model claude-opus-4-7)');
10867
11049
  log(' :models — provider 별 모델 목록 (ollama 실시간 / 그 외 추천 카탈로그)');
10868
11050
  log(' :role <r> — 역할 변경 (planner / reviewer / actor)');
10869
11051
  log(' :provider <p> — provider 변경 (ollama / claude / codex / gemini / copilot — ready 검증)');
11052
+ log(' :stream on|off — 🆕 1.9.170 실시간 스트리밍 토글 (추론중/diff/thinking 실시간 표시)');
10870
11053
  log(' :status — 현재 세션 상태 자세히 (1.9.155)');
10871
11054
  log(' :clear — 화면 클리어 + history 유지');
10872
11055
  log(' :reset — history 초기화');
@@ -10874,6 +11057,9 @@ async function _agentRepl(root, opts) {
10874
11057
  log(' :save — 세션 즉시 저장');
10875
11058
  log(' :permissions — 현재 권한 모드 표시');
10876
11059
  log(' :quit / :exit / :q — 종료 (자동 저장)');
11060
+ log(C.bold('\n 🆕 1.9.170 키보드 단축키:'));
11061
+ log(' Tab — 다음 provider 로 cycle (ollama → claude → codex → gemini → copilot)');
11062
+ log(' Shift+Tab — 현재 provider 의 다음 model 로 cycle (catalog 기준)');
10877
11063
  log(C.bold('\n Slash 명령 (1.9.150) — leerness 내부 명령 직접 호출:'));
10878
11064
  log(' :verify — leerness verify-code (테스트/타입/린트 자동 검수)');
10879
11065
  log(' :audit — leerness audit (보안 + drift + lazy)');
@@ -10954,6 +11140,21 @@ async function _agentRepl(root, opts) {
10954
11140
  log(C.green(` provider = ${state.provider}`));
10955
11141
  return false;
10956
11142
  }
11143
+ if (op === 'stream') {
11144
+ // 1.9.170: 실시간 스트리밍 모드 토글 (사용자 명시 — 추론중/diff/thinking 실시간 표시)
11145
+ const v = (rest[0] || '').toLowerCase();
11146
+ if (v === 'on' || v === '1' || v === 'true') {
11147
+ state.streamMode = true;
11148
+ log(C.green(' ▶ streaming mode: ON (CLI stdout 실시간 표시)'));
11149
+ } else if (v === 'off' || v === '0' || v === 'false') {
11150
+ state.streamMode = false;
11151
+ log(C.dim(' □ streaming mode: OFF (배치 응답)'));
11152
+ } else {
11153
+ log(C.dim(` 현재 streaming: ${state.streamMode ? 'ON ▶' : 'OFF □'} — 사용: :stream on|off`));
11154
+ }
11155
+ rl.setPrompt(prompt());
11156
+ return false;
11157
+ }
10957
11158
  if (op === 'clear') { process.stdout.write('\x1b[2J\x1b[H'); return false; }
10958
11159
  if (op === 'reset') { state.history = []; log(C.dim(' history 초기화됨')); return false; }
10959
11160
  if (op === 'history') {
@@ -11032,12 +11233,18 @@ async function _agentRepl(root, opts) {
11032
11233
  const t0 = Date.now();
11033
11234
  let result;
11034
11235
  // 1.9.153: multi-provider REPL — ollama 외 claude/codex/gemini/copilot 도 세션 관리 (사용자 명시)
11236
+ // 1.9.170: streamMode === true 이면 _cliChatStream 사용 (사용자 명시 — 추론중/diff 실시간 표시)
11035
11237
  if (state.provider === 'ollama') {
11036
11238
  log(C.dim(` → ollama${state.model ? ' (' + state.model + ')' : ''} 호출 중...`));
11037
11239
  result = await _ollamaChat(finalPrompt, state.model);
11038
11240
  } else if (['claude', 'codex', 'gemini', 'copilot'].includes(state.provider)) {
11039
- log(C.dim(` → ${state.provider} CLI 호출 중...`));
11040
- result = await _cliChat(root, state.provider, finalPrompt, { timeout: 90000 });
11241
+ if (state.streamMode) {
11242
+ log(C.dim(` → ${state.provider} CLI stream 호출 중... (Ctrl+C 로 중단)`));
11243
+ result = await _cliChatStream(root, state.provider, finalPrompt, { timeout: 120000 });
11244
+ } else {
11245
+ log(C.dim(` → ${state.provider} CLI 호출 중...`));
11246
+ result = await _cliChat(root, state.provider, finalPrompt, { timeout: 90000 });
11247
+ }
11041
11248
  } else {
11042
11249
  log(C.yel(` ⚠ ${state.provider} provider 미지원 — :provider ollama|claude|codex|gemini|copilot`));
11043
11250
  rl.prompt(); return;
@@ -11046,10 +11253,16 @@ async function _agentRepl(root, opts) {
11046
11253
  _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 });
11047
11254
  if (result.ok) {
11048
11255
  state.history.push({ role: 'assistant', content: result.response });
11049
- log('');
11050
- log(C.bold(`assistant (${state.model}, role=${state.role}, ${dt}ms)`));
11051
- log(result.response);
11052
- log('');
11256
+ // 1.9.170: stream 모드에서는 이미 실시간으로 출력됐으므로 헤더만 표시 (응답 중복 방지)
11257
+ if (state.streamMode && ['claude', 'codex', 'gemini', 'copilot'].includes(state.provider)) {
11258
+ log(C.dim(` [assistant: ${state.provider}/${state.model || 'default'}, role=${state.role}, ${dt}ms · ${result.response.length}자]`));
11259
+ log('');
11260
+ } else {
11261
+ log('');
11262
+ log(C.bold(`assistant (${state.model || state.provider}, role=${state.role}, ${dt}ms)`));
11263
+ log(result.response);
11264
+ log('');
11265
+ }
11053
11266
  if (state.history.length % 6 === 0) saveSession(); // 6턴마다 자동 저장
11054
11267
  } else {
11055
11268
  log(C.yel(` ⚠ 실패: ${result.error || 'unknown'}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.169",
3
+ "version": "1.9.170",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",