leerness 1.9.187 → 1.9.189

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,201 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.189 — 2026-05-21
4
+
5
+ **⌨️ "/" slash 명령 자동 list + Tab cycle 한 줄 갱신 (사용자 명시 3종).**
6
+
7
+ 자율 모드 119 라운드. 사용자 명시:
8
+ 1. *"권한 기본값 설정을 / 으로 설정해둘 수 있고 (모든 모델 공통)"* — `/` slash default trigger
9
+ 2. *"/ 를 입력하면 관련 명령어가 첨부이미지처럼 나열"* — claude code 영감 UX
10
+ 3. *"탭키로 모델 변경시, 채팅 이력으로 남아서 지져분"* — 한 줄 갱신 (in-place)
11
+
12
+ ### Fix #1 — `/` slash 명령 자동 list
13
+
14
+ **Before** (claude code 화면 처럼 자동 안 됨):
15
+ ```
16
+ agent[claude/actor/▶]> /
17
+ (처리 안 됨 또는 에러)
18
+ ```
19
+
20
+ **After** (claude code 스타일):
21
+ ```
22
+ agent[claude · opus-4-7/actor/▶]> /
23
+
24
+ Available commands (also accepts ":" prefix)
25
+ ────────────────────────────────────────────────...
26
+ meta
27
+ /help 명령 목록
28
+ /quit REPL 종료 (세션 자동 저장)
29
+ /clear 대화 히스토리 초기화
30
+ /status 현재 provider / model / role / perms 상태
31
+ /provider <id> provider 전환
32
+ /model <id> 현재 provider의 모델 전환
33
+ /stream on|off 실시간 스트리밍 토글
34
+ review
35
+ /review "<req>" 무조건 구현 전 사전 검토
36
+ /permissions <m> 권한 변경 basic|extended|full
37
+ internal
38
+ /verify /audit /handoff /health
39
+ memory
40
+ /lessons /brainstorm <q> /tasks /plan
41
+ bridge
42
+ /web <op> /pc <op> /lsp <op>
43
+ ────────────────────────────────────────────────...
44
+ 💡 "/" 또는 ":" 접두사로 명령 호출
45
+ ```
46
+
47
+ `/` 와 `:` **양쪽 동등** (alias 처리). 모든 모델 공통.
48
+
49
+ ### Fix #2 — Tab cycle 한 줄 갱신 (in-place overwrite)
50
+
51
+ **Before** (1.9.188까지) — 매 Tab마다 새 줄 누적 → 채팅 이력 지저분:
52
+ ```
53
+ ⇄ provider [3/5]: codex ✓ ready
54
+ └ 5개 모델 catalog · Shift+Tab으로 model cycle
55
+ ⇄ provider [4/5]: gemini ✓ ready
56
+ └ 3개 모델 catalog · Shift+Tab으로 model cycle
57
+ ⇄ provider [5/5]: copilot ✓ ready
58
+ └ 1개 모델 catalog · Shift+Tab으로 model cycle
59
+ ⇄ provider [1/5]: ollama
60
+ └ 4개 모델 catalog · Shift+Tab으로 model cycle
61
+ ...
62
+ (채팅 영역이 cycle 흔적으로 가득 참)
63
+ ```
64
+
65
+ **After** (1.9.189) — 마지막 1건만 보임:
66
+ ```
67
+ ⇄ provider [2/5]: claude ✓ ready
68
+ └ 5개 모델 catalog · Shift+Tab으로 model cycle
69
+ agent[claude · opus-4-7/actor/▶]> _
70
+ ```
71
+
72
+ ```js
73
+ // 1.9.189: ANSI cursor up + line clear 로 이전 cycle 라인 덮어씀
74
+ let _lastCycleLines = 0;
75
+ const _clearLastCycle = () => {
76
+ if (!isTty || _lastCycleLines === 0) return;
77
+ process.stdout.write('\r\x1b[K'); // 현재 prompt 줄 클리어
78
+ for (let i = 0; i < _lastCycleLines; i++) {
79
+ process.stdout.write('\x1b[1A\x1b[K'); // 한 줄 위로 + 클리어
80
+ }
81
+ _lastCycleLines = 0;
82
+ };
83
+ // cycleProvider/cycleModel 시작 시 호출 → 이전 cycle 출력 제거 → 새 cycle 출력
84
+ // 사용자 line 입력 시 _lastCycleLines = 0 reset (cycle 흔적 더 이상 클리어 X)
85
+ ```
86
+
87
+ ### Fix #3 — Welcome 안내 갱신
88
+ ```
89
+ ───────────────────────────── 채팅 시작 ─────────────────────────────
90
+ 메시지 입력 후 Enter · "/" 입력 시 명령 list · :help 으로도 가능 · Ctrl+C 로 종료
91
+ ```
92
+
93
+ ### Verified
94
+ - stress-v134: **15/15 PASS** (사용자 명시 7 + live 2 + 누적 6)
95
+ - e2e 217/217 baseline 유지
96
+ - `/` 접두사 7개 명령 모두 정상 동작 (alias 처리)
97
+ - Tab cycle in-place overwrite — 채팅 영역 깔끔 유지
98
+ - VERSION = 1.9.189 · autonomous-rounds = 119 · main 자동 push 50 라운드 연속
99
+
100
+ ---
101
+
102
+ ## 1.9.188 — 2026-05-21
103
+
104
+ **🐛 REPL 한글 prompt 전달 BUG fix (stdin) + 세부 모델 표시 + 입력 구분선 (사용자 명시 3종).**
105
+
106
+ 자율 모드 118 라운드. 사용자 명시 핵심 버그:
107
+ ```
108
+ agent[claude/actor/▶]> 이 폴더에 파이썬 프로그램을 하나 제작해줘
109
+ ── claude stream ──
110
+ 저는 이 프로젝트의 **수석 개발자이자 프로젝트 매니저**입니다.
111
+ ...
112
+ 메시지가 "역할:"로만 끝나서 의도를 확신하기 어렵습니다.
113
+ ```
114
+ 사용자 의도와 무관한 응답 — **claude가 한글 prompt 끝부분("역할:") fragment만 받음**.
115
+
116
+ ### 근본 원인 (코드 audit)
117
+
118
+ `cp.spawn(cmd, args, { shell: true })` + Windows cmd.exe + 한글/특수문자 promptText:
119
+ - shell:true 일 때 args 가 join되어 shell command line 형성
120
+ - Windows cmd.exe가 한글/공백 escape 실패 → claude에 일부 fragment만 전달
121
+
122
+ ### Fix #1 — prompt를 stdin으로 전달 (shell escape 우회)
123
+
124
+ ```diff
125
+ - if (provider === 'claude') { cmd = 'claude'; args = ['--print', promptText]; }
126
+ + // 1.9.188: promptText 는 stdin 으로 → args 에서 제거 (한글/특수문자 안전)
127
+ + let useStdinForPrompt = false;
128
+ + if (provider === 'claude') {
129
+ + cmd = 'claude';
130
+ + args = ['--print']; // promptText 제거
131
+ + useStdinForPrompt = true;
132
+ + }
133
+ ```
134
+
135
+ spawn 시:
136
+ ```js
137
+ child = cp.spawn(cmd, args, {
138
+ stdio: [useStdinForPrompt ? 'pipe' : 'ignore', 'pipe', 'pipe'],
139
+ shell: true
140
+ });
141
+ if (useStdinForPrompt && child.stdin) {
142
+ child.stdin.write(promptText);
143
+ child.stdin.end();
144
+ }
145
+ ```
146
+
147
+ `_cliChat` 도 동일하게 `runCommandSafe(..., { input: stdinInput })` 으로 변경.
148
+
149
+ ### Live 검증 (한글 prompt 정확 전달)
150
+ ```bash
151
+ $ leerness agents multi "이 폴더에 파이썬 hello world 프로그램 제작 (응답만 코드)" --only claude --execute
152
+ ✓ claude · 8279ms · 9 토큰
153
+ --- 처음 600자 ---
154
+ `hello.py` 생성 권한이 필요합니다. 승인해 주세요.
155
+ ```
156
+ **1.9.187까지**: "수석 개발자/프로젝트 매니저..." 같은 무관한 응답 (역할 fragment만 받음)
157
+ **1.9.188**: `hello.py 생성 권한 필요` — 정확한 요청 이해 ✓
158
+
159
+ ### Fix #2 — 세부 모델 표시
160
+ **Before**: `⚡ provider=claude · model=(기본)`
161
+ **After**: `⚡ provider=claude · model=claude-opus-4-7`
162
+
163
+ ```js
164
+ // 1.9.188 (사용자 명시): state.model 자동 default (catalog 첫 모델)
165
+ if (!state.model) {
166
+ const cat = _PROVIDER_MODEL_CATALOG[state.provider];
167
+ if (cat && cat.length) state.model = cat[0].id;
168
+ }
169
+ ```
170
+
171
+ prompt 도 세부 모델 명시:
172
+ ```
173
+ Before: agent[claude/actor/▶]>
174
+ After: agent[claude · opus-4-7/actor/▶]>
175
+ ```
176
+
177
+ ### Fix #3 — 입력 구분선 (Hermes UX 영감)
178
+ 각 응답 끝에 가로 디바이더 자동 출력 → 입력 영역 시각 명확:
179
+
180
+ ```
181
+ [assistant: claude/claude-opus-4-7, role=actor, 4598ms · 425자]
182
+
183
+ ────────────────────────────────────────────────...
184
+ agent[claude · opus-4-7/actor/▶]> _
185
+ ```
186
+
187
+ `_printInputDivider()` 함수 도입 — 터미널 width 기준 자동 길이.
188
+
189
+ ### Verified
190
+ - stress-v133: **15/15 PASS** (사용자 명시 6 + live 2 + 누적 7)
191
+ - e2e 217/217 baseline 유지
192
+ - live 검증:
193
+ - 한글 prompt "이 폴더에 파이썬 hello world 제작" → claude 정확 응답 (12.5초)
194
+ - 영문 prompt "1+1=?" → claude 응답 (9.3초, 회귀 없음)
195
+ - VERSION = 1.9.188 · autonomous-rounds = 118 · main 자동 push 49 라운드 연속
196
+
197
+ ---
198
+
3
199
  ## 1.9.187 — 2026-05-21
4
200
 
5
201
  **🔓 비시크릿 LEERNESS_* 설정을 .env → .harness/leerness-config.json 으로 분리 (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.187-green)]() [![tests](https://img.shields.io/badge/e2e-217%2F217-success)]() [![stress](https://img.shields.io/badge/stress--v132-19%2F19-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-54-brightgreen)]() [![rounds](https://img.shields.io/badge/autonomous--rounds-117-blueviolet)]() [![main-push](https://img.shields.io/badge/release--main--push-48_rounds-success)]() [![config-split](https://img.shields.io/badge/.env_분리-비시크릿_AI가시성-success)]() [![secret-guard](https://img.shields.io/badge/시크릿_가드-load%2Bwrite_이중망-success)]() [![install](https://img.shields.io/badge/npm_i_leerness-즉시_사용_가능-success)]() [![repl-fix](https://img.shields.io/badge/REPL_stream-shell%3Atrue_fix-success)]() [![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.189-green)]() [![tests](https://img.shields.io/badge/e2e-217%2F217-success)]() [![stress](https://img.shields.io/badge/stress--v134-15%2F15-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-54-brightgreen)]() [![rounds](https://img.shields.io/badge/autonomous--rounds-119-blueviolet)]() [![main-push](https://img.shields.io/badge/release--main--push-50_rounds-success)]() [![slash-list](https://img.shields.io/badge/"/"_slash-자동_명령_list-success)]() [![cycle-clean](https://img.shields.io/badge/Tab_cycle-한_줄_갱신-success)]() [![hangul](https://img.shields.io/badge/한글_prompt-stdin_정확전달-success)]() [![install](https://img.shields.io/badge/npm_i_leerness-즉시_사용_가능-success)]() [![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.187 AI Agent Reliability Harness + Sandbox ║
15
+ ║ v1.9.189 AI Agent Reliability Harness + Sandbox ║
16
16
  ║ verify · remember · orchestrate · audit · sandbox · drift ║
17
- 🔓 .env 분리 (시크릿) · leerness-config.json (AI 가시)
17
+ "/" slash 자동 list · Tab cycle 한 줄 갱신 · 50 라운드
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.187';
10
+ const VERSION = '1.9.189';
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 처리).
@@ -11048,10 +11048,11 @@ async function _cliChat(root, provider, prompt, opts) {
11048
11048
  return { ok: false, error: `${provider} 비활성 (${status.status}) — .env 에서 ${agent.envFlag}=1 + CLI 설치 필요`, provider };
11049
11049
  }
11050
11050
  // CLI 별 비-인터랙티브 호출 인자 매핑 (read-only 모드 — REPL 안에서 파일 수정 X)
11051
- let cmd, args;
11052
- if (provider === 'claude') { cmd = 'claude'; args = ['--print', prompt]; }
11053
- else if (provider === 'codex') { cmd = 'codex'; args = ['exec', '--skip-git-repo-check', prompt]; }
11054
- else if (provider === 'gemini') { cmd = 'gemini'; args = ['-p', prompt]; }
11051
+ // 1.9.188 (사용자 명시 fix): 한글/특수문자 prompt 시 shell escape 실패 회피 → stdin 으로 전달.
11052
+ let cmd, args, stdinInput = null;
11053
+ if (provider === 'claude') { cmd = 'claude'; args = ['--print']; stdinInput = prompt; }
11054
+ else if (provider === 'codex') { cmd = 'codex'; args = ['exec', '--skip-git-repo-check', '-']; stdinInput = prompt; }
11055
+ else if (provider === 'gemini') { cmd = 'gemini'; args = ['-p', prompt]; } // gemini 는 인자 only
11055
11056
  else if (provider === 'copilot') { cmd = 'gh'; args = ['copilot', 'suggest', prompt]; }
11056
11057
  else return { ok: false, error: `provider ${provider} 미지원`, provider };
11057
11058
  // runCommandSafe — env scrub + observability 자동
@@ -11059,7 +11060,8 @@ async function _cliChat(root, provider, prompt, opts) {
11059
11060
  cwd: process.cwd(), root,
11060
11061
  timeout: opts.timeout || 60000,
11061
11062
  allowOutsideCwd: true, // CLI 가 cwd 밖에서 실행될 수 있음
11062
- kind: 'agent_repl_cli', label: `repl-${provider}`
11063
+ kind: 'agent_repl_cli', label: `repl-${provider}`,
11064
+ input: stdinInput // 1.9.188: claude/codex 는 stdin 전달
11063
11065
  });
11064
11066
  if (r.status === 0) {
11065
11067
  return { ok: true, response: (r.stdout || '').trim(), provider, model: provider };
@@ -11088,15 +11090,25 @@ async function _cliChatStream(root, provider, promptText, opts) {
11088
11090
  // 1.9.186 (사용자 명시 fix): claude --output-format=stream-json 가 일부 버전에서 빈 응답.
11089
11091
  // default 를 plain --print 로 변경 → _cliChat 과 동일한 인자, 작동 검증된 패턴.
11090
11092
  // stream 형식 사용 opt-in: LEERNESS_REPL_STREAM_FORMAT=json (실시간 thinking/tool_use 보고 싶을 때).
11093
+ // 1.9.188 (사용자 명시 fix): shell:true + Windows cmd.exe + 한글/특수문자 promptText → args escape 실패.
11094
+ // 사용자 보고: "이 폴더에 파이썬 프로그램 제작" 보냈으나 claude는 "역할:" fragment만 받음.
11095
+ // 해결: promptText 를 args 에서 제거하고 stdin 으로 전달 (shell escape 우회).
11091
11096
  const useStreamJson = process.env.LEERNESS_REPL_STREAM_FORMAT === 'json';
11097
+ let useStdinForPrompt = false; // 1.9.188: claude/codex 는 stdin 으로 prompt 전달 (shell escape 우회)
11092
11098
  if (provider === 'claude') {
11093
11099
  cmd = 'claude';
11100
+ // 1.9.188: promptText 는 stdin 으로 → args 에서 제거 (한글/특수문자 안전)
11094
11101
  args = useStreamJson
11095
- ? ['--print', '--output-format=stream-json', '--verbose', promptText]
11096
- : ['--print', promptText]; // plain text 응답 (작동 검증됨)
11102
+ ? ['--print', '--output-format=stream-json', '--verbose']
11103
+ : ['--print'];
11104
+ useStdinForPrompt = true;
11105
+ }
11106
+ else if (provider === 'codex') {
11107
+ cmd = 'codex';
11108
+ args = ['exec', '--skip-git-repo-check', '-']; // - = stdin from claude/codex convention
11109
+ useStdinForPrompt = true;
11097
11110
  }
11098
- else if (provider === 'codex') { cmd = 'codex'; args = ['exec', '--skip-git-repo-check', promptText]; }
11099
- else if (provider === 'gemini') { cmd = 'gemini'; args = ['-p', promptText]; }
11111
+ else if (provider === 'gemini') { cmd = 'gemini'; args = ['-p', promptText]; } // gemini -p 는 인자 모드만 지원
11100
11112
  else if (provider === 'copilot') { cmd = 'gh'; args = ['copilot', 'suggest', promptText]; }
11101
11113
  else return { ok: false, error: `provider ${provider} 미지원`, provider };
11102
11114
  const t0 = Date.now();
@@ -11156,17 +11168,26 @@ async function _cliChatStream(root, provider, promptText, opts) {
11156
11168
  process.stdout.write(dim(`\n ── ${provider} stream ──\n`));
11157
11169
  let child;
11158
11170
  try {
11159
- // 1.9.186 (사용자 명시 fix): Windows .cmd 호환을 위해 shell: true.
11160
- // 1.9.185 까지 shell: false → Windows 에서 claude.cmd 찾아 응답 (사용자 보고: 27초 후 0자).
11161
- // _cliChat (작동하는 함수) runCommandSafe spawnSync shell: true 패턴이라서 작동.
11162
- // _cliChatStream 도 동일하게 shell: true 사용 (DEP0190 1.9.184/185 fix 로 억제됨).
11163
- // security: shell escape 우려는 promptText 사용자 직접 입력 — sandboxed REPL 안에서만 사용.
11171
+ // 1.9.186 + 1.9.188 (사용자 명시 fix):
11172
+ // Windows .cmd 호환을 위해 shell: true 유지.
11173
+ // 1.9.188: promptText 한글/특수문자일 shell escape 실패 stdin 으로 전달.
11174
+ // useStdinForPrompt=true stdio[0]='pipe' + child.stdin.write(promptText) + end().
11175
+ // shell 인자에는 escape-safe flags 남김 ('--print' 등).
11164
11176
  child = cp.spawn(cmd, args, {
11165
11177
  cwd: process.cwd(),
11166
11178
  env: _scrubEnv({}),
11167
11179
  shell: true,
11168
- stdio: ['ignore', 'pipe', 'pipe']
11180
+ stdio: [useStdinForPrompt ? 'pipe' : 'ignore', 'pipe', 'pipe']
11169
11181
  });
11182
+ // 1.9.188: promptText 를 stdin 으로 안전 전달
11183
+ if (useStdinForPrompt && child.stdin) {
11184
+ try {
11185
+ child.stdin.write(promptText);
11186
+ child.stdin.end();
11187
+ } catch (e) {
11188
+ // stdin 쓰기 실패 시 child 가 stdin 안 받을 수도 — 무시하고 진행
11189
+ }
11190
+ }
11170
11191
  } catch (e) {
11171
11192
  if (spinnerInterval) clearInterval(spinnerInterval);
11172
11193
  return resolve({ ok: false, error: 'spawn 실패: ' + e.message, provider });
@@ -11571,10 +11592,16 @@ async function _agentRepl(root, opts) {
11571
11592
  log('');
11572
11593
  log(C.dim(' ⌨ Tab=provider cycle · Shift+Tab=model · Ctrl+C=quit'));
11573
11594
  log('');
11574
- // 1.9.179: 상태바 색깔/구분자 강화
11595
+ // 1.9.188 (사용자 명시): state.model 비어있으면 catalog 모델 자동 설정 → 세부 모델 표시.
11596
+ // "model=(기본)" 같은 모호한 표시 대신 실제 사용될 모델 이름 (예: claude-opus-4-7) 명시.
11597
+ if (!state.model) {
11598
+ const cat = _PROVIDER_MODEL_CATALOG[state.provider];
11599
+ if (cat && cat.length) state.model = cat[0].id;
11600
+ }
11601
+ // 1.9.179+1.9.188: 상태바 한 줄 — 세부 모델 + 색깔/구분자 강화
11575
11602
  const permMode = _readPermissions(root).mode || 'basic';
11576
11603
  log(' ' + C.bold('⚡ ') + C.cy(`provider=${state.provider}`) + ' · '
11577
- + C.mag(`model=${state.model || '(기본)'}`) + ' · '
11604
+ + C.mag(`model=${state.model || '(unknown)'}`) + ' · '
11578
11605
  + C.green(`role=${state.role}`) + ' · '
11579
11606
  + C.yel(`perms=${permMode}`) + ' · '
11580
11607
  + (state.streamMode ? C.green('▶ stream=on') : C.dim('□ stream=off')));
@@ -11582,7 +11609,7 @@ async function _agentRepl(root, opts) {
11582
11609
  // 환영 화면 끝에 명확한 구분선 + 안내 → 채팅 시작 명확.
11583
11610
  log('');
11584
11611
  log(C.dim(' ───────────────────────────── 채팅 시작 ─────────────────────────────'));
11585
- log(C.dim(' 메시지 입력 후 Enter · :help 으로 명령 목록 · Ctrl+C 로 종료'));
11612
+ log(C.dim(' 메시지 입력 후 Enter · ') + C.cy('"/"') + C.dim(' 입력 시 명령 list · ') + C.cy(':help') + C.dim(' 으로도 가능 · Ctrl+C 로 종료'));
11586
11613
  // 1.9.155: REPL 진입 시 handoff 컨텍스트 자동 노출 (UX 개선 — 사용자가 매번 :handoff 안 해도 컨텍스트 인지)
11587
11614
  try {
11588
11615
  const hf = cp.spawnSync(process.execPath, [__filename, 'handoff', root, '--compact', '--no-drift-check', '--no-headline'], {
@@ -11598,7 +11625,24 @@ async function _agentRepl(root, opts) {
11598
11625
  }
11599
11626
  } catch {}
11600
11627
  log('');
11601
- const prompt = () => isTty ? C.cy(`agent[${state.provider}/${state.role}${state.streamMode ? '/▶' : ''}]> `) : 'agent> ';
11628
+ // 1.9.188 (사용자 명시): prompt 위/아래 구분선 + 세부 모델 명시 (사용자 입력칸 시각 강조).
11629
+ // 매 응답 직후 readline.question 호출 전에 _printInputDivider() 호출하여 입력 영역 명확화.
11630
+ const _termCols = () => {
11631
+ try { return Math.max(40, (process.stdout.columns || 80)); } catch { return 80; }
11632
+ };
11633
+ const _printInputDivider = () => {
11634
+ if (!isTty) return;
11635
+ const cols = _termCols();
11636
+ const line = '─'.repeat(Math.min(cols - 4, 120));
11637
+ process.stdout.write('\n' + C.dim(' ' + line) + '\n');
11638
+ };
11639
+ // prompt 에 세부 모델 표시 (사용자 명시): agent[claude · claude-opus-4-7 / actor / ▶]>
11640
+ const prompt = () => {
11641
+ if (!isTty) return 'agent> ';
11642
+ const modelShort = (state.model || '').replace(/^(claude|gpt|gemini)-/, '').slice(0, 18);
11643
+ const modelTag = modelShort ? C.dim(' · ') + C.mag(modelShort) : '';
11644
+ return C.cy(`agent[${state.provider}${modelTag}${C.cy('/' + state.role)}${state.streamMode ? C.cy('/▶') : ''}${C.cy(']>')} `);
11645
+ };
11602
11646
  rl.setPrompt(prompt());
11603
11647
 
11604
11648
  // 1.9.170: Tab cycle — provider (Tab) / model within provider (Shift+Tab)
@@ -11620,17 +11664,31 @@ async function _agentRepl(root, opts) {
11620
11664
  } catch {}
11621
11665
  return _PROVIDER_CYCLE_ORDER.slice();
11622
11666
  };
11623
- // 1.9.180: cycleProvider/cycleModel — 시각 피드백 강화 (사용자 명시 "원활하지 않다").
11624
- // 이전: 한 줄 ⇄ 메시지만. 1.9.180: provider 위치 표시 [1/5] + bold highlight + 활성 여부 표시.
11667
+ // 1.9.180+1.9.189: cycleProvider/cycleModel — 갱신 (in-place overwrite).
11668
+ // 1.9.180까지: Tab 누름마다 출력 채팅 이력으로 누적 (사용자 명시: "지져분해보여").
11669
+ // 1.9.189: ANSI cursor up + line clear 로 이전 cycle 라인 덮어씀 → 마지막 1건만 표시.
11670
+ let _lastCycleLines = 0; // 직전 cycle 출력 라인 수 (overwrite 용)
11671
+ const _clearLastCycle = () => {
11672
+ if (!isTty || _lastCycleLines === 0) return;
11673
+ // 현재 prompt 라인 + 이전 cycle 라인들 클리어
11674
+ // 현재 cursor는 prompt 줄. 위로 _lastCycleLines+1 만큼 올라가서 클리어.
11675
+ process.stdout.write('\r\x1b[K'); // 현재 prompt 줄 클리어
11676
+ for (let i = 0; i < _lastCycleLines; i++) {
11677
+ process.stdout.write('\x1b[1A\x1b[K'); // 한 줄 위로 + 클리어
11678
+ }
11679
+ _lastCycleLines = 0;
11680
+ };
11625
11681
  const cycleProvider = (reverse) => {
11682
+ _clearLastCycle();
11626
11683
  const list = getProviders();
11627
11684
  let idx = list.indexOf(state.provider);
11628
11685
  if (idx < 0) idx = 0;
11629
11686
  idx = reverse ? (idx - 1 + list.length) % list.length : (idx + 1) % list.length;
11630
11687
  state.provider = list[idx];
11631
11688
  state.model = null;
11689
+ // 1.9.188: state.model auto-default → 다음 cycle/prompt 에 catalog 첫 모델
11632
11690
  const cat = _PROVIDER_MODEL_CATALOG[state.provider];
11633
- // 활성 여부 사전 확인 (1.9.180 UX — 사용자가 즉시 인지)
11691
+ if (cat && cat.length) state.model = cat[0].id;
11634
11692
  let activeMark = '';
11635
11693
  try {
11636
11694
  if (state.provider !== 'ollama') {
@@ -11641,19 +11699,21 @@ async function _agentRepl(root, opts) {
11641
11699
  }
11642
11700
  }
11643
11701
  } catch {}
11644
- process.stdout.write('\r\x1b[K');
11645
11702
  process.stdout.write(C.bold(C.green(` ⇄ provider [${idx + 1}/${list.length}]: ${state.provider}`)) + activeMark + '\n');
11703
+ _lastCycleLines = 1;
11646
11704
  if (cat?.length) {
11647
11705
  process.stdout.write(C.dim(` └ ${cat.length}개 모델 catalog · Shift+Tab으로 model cycle\n`));
11706
+ _lastCycleLines = 2;
11648
11707
  }
11649
11708
  rl.setPrompt(prompt());
11650
11709
  rl.prompt(true);
11651
11710
  };
11652
11711
  const cycleModel = (reverse) => {
11712
+ _clearLastCycle();
11653
11713
  const cat = _PROVIDER_MODEL_CATALOG[state.provider] || [];
11654
11714
  if (cat.length === 0) {
11655
- process.stdout.write('\r\x1b[K');
11656
- process.stdout.write(C.yel(` ⚠ ${state.provider} 추천 모델 catalog 없음 (:model <name> 으로 직접 지정)\n`));
11715
+ process.stdout.write(C.yel(` ⚠ ${state.provider} 추천 모델 catalog 없음 (/model <name> 으로 직접 지정)\n`));
11716
+ _lastCycleLines = 1;
11657
11717
  rl.prompt(true);
11658
11718
  return;
11659
11719
  }
@@ -11661,14 +11721,17 @@ async function _agentRepl(root, opts) {
11661
11721
  if (idx < 0) idx = -1;
11662
11722
  idx = reverse ? (idx - 1 + cat.length) % cat.length : (idx + 1) % cat.length;
11663
11723
  state.model = cat[idx].id;
11664
- process.stdout.write('\r\x1b[K');
11665
11724
  process.stdout.write(C.bold(C.mag(` ⇄ model [${idx + 1}/${cat.length}]: ${state.model}`)) + '\n');
11725
+ _lastCycleLines = 1;
11666
11726
  if (cat[idx].note) {
11667
11727
  process.stdout.write(C.dim(` └ ${cat[idx].note}\n`));
11728
+ _lastCycleLines = 2;
11668
11729
  }
11669
11730
  rl.setPrompt(prompt());
11670
11731
  rl.prompt(true);
11671
11732
  };
11733
+ // 1.9.189: 사용자가 다른 입력 시작하면 cycle line clear 흔적 정리 (다음 Tab까지 유지하면 cycle 위치 보임)
11734
+ // 단순화: 사용자가 enter 누르면 _lastCycleLines=0 으로 reset (응답 후 다시 cycle 출력 시 새로 시작).
11672
11735
  process.stdin.on('keypress', (str, key) => {
11673
11736
  if (!key) return;
11674
11737
  if (key.name === 'tab') {
@@ -11687,6 +11750,55 @@ async function _agentRepl(root, opts) {
11687
11750
  }
11688
11751
  }
11689
11752
 
11753
+ // 1.9.189 (사용자 명시): "/" 입력만으로 명령 list 표시 (claude code 영감 — autocomplete UX).
11754
+ // "/" 또는 "/?" → 명령 카탈로그 출력 후 prompt 복귀. 이미지 같은 list 스타일 (명령 + 한 줄 설명).
11755
+ const _showSlashCommandList = () => {
11756
+ log('');
11757
+ log(C.bold(' Available commands') + C.dim(' (also accepts ":" prefix)'));
11758
+ log(C.dim(' ────────────────────────────────────────────────────────────────────────────────'));
11759
+ const groups = [
11760
+ { title: 'meta', items: [
11761
+ ['/help', '명령 목록 (이 화면) — 또는 ":?"'],
11762
+ ['/quit', 'REPL 종료 (세션 자동 저장)'],
11763
+ ['/clear', '대화 히스토리 초기화'],
11764
+ ['/status', '현재 provider / model / role / perms 상태'],
11765
+ ['/provider <id>', 'provider 전환 (ollama, claude, codex, gemini, copilot)'],
11766
+ ['/model <id>', '현재 provider의 모델 전환'],
11767
+ ['/role <id>', '역할 전환 (actor / planner / reviewer)'],
11768
+ ['/stream on|off', '실시간 스트리밍 토글 (default ON)']
11769
+ ]},
11770
+ { title: 'review', items: [
11771
+ ['/review "<req>"', '무조건 구현 전 사전 검토 (1.9.176)'],
11772
+ ['/permissions <m>', '권한 변경 basic|extended|full (1.9.174)']
11773
+ ]},
11774
+ { title: 'internal', items: [
11775
+ ['/verify', 'task evidence 검증'],
11776
+ ['/audit', 'audit 실행'],
11777
+ ['/handoff', 'handoff context 출력'],
11778
+ ['/health', 'health check']
11779
+ ]},
11780
+ { title: 'memory', items: [
11781
+ ['/lessons', 'lessons.md 회수'],
11782
+ ['/brainstorm <q>', 'brainstorm 회수 (memory + code + tasks)'],
11783
+ ['/tasks', '현재 task list'],
11784
+ ['/plan', 'plan.md milestones']
11785
+ ]},
11786
+ { title: 'bridge', items: [
11787
+ ['/web <op>', 'playwright bridge (check/screenshot/extract)'],
11788
+ ['/pc <op>', 'robotjs bridge (check/click/type/screenshot)'],
11789
+ ['/lsp <op>', 'LSP adapter (check/symbols/references)']
11790
+ ]}
11791
+ ];
11792
+ for (const g of groups) {
11793
+ log(C.bold(` ${g.title}`));
11794
+ for (const [name, desc] of g.items) {
11795
+ log(' ' + C.cy(name.padEnd(22)) + C.dim(desc));
11796
+ }
11797
+ }
11798
+ log(C.dim(' ────────────────────────────────────────────────────────────────────────────────'));
11799
+ log(C.dim(' 💡 메시지는 직접 입력 · "/" 또는 ":" 접두사로 명령 호출 · Tab=provider · Shift+Tab=model'));
11800
+ log('');
11801
+ };
11690
11802
  rl.prompt();
11691
11803
  const handleMeta = async (cmd) => {
11692
11804
  const [op, ...rest] = cmd.slice(1).split(/\s+/);
@@ -11976,7 +12088,21 @@ async function _agentRepl(root, opts) {
11976
12088
  return new Promise(resolve => {
11977
12089
  rl.on('line', async (line) => {
11978
12090
  const input = line.trim();
12091
+ _lastCycleLines = 0; // 1.9.189: 사용자 입력 시 cycle overwrite 추적 reset
11979
12092
  if (!input) { rl.prompt(); return; }
12093
+ // 1.9.189 (사용자 명시): "/" slash 입력만으로 명령 list 표시 (claude code 영감).
12094
+ // "/" 단독 → 명령 list 출력 후 prompt 복귀.
12095
+ // "/help", "/quit" 등 "/" 접두사 → ":" 동등 처리 (alias).
12096
+ if (input === '/' || input === '/?') {
12097
+ _showSlashCommandList();
12098
+ rl.prompt(); return;
12099
+ }
12100
+ // 1.9.189: "/" 접두사를 ":" 와 동등하게 처리 (사용자 명시 — "/ 가 모든 모델 공통 권한 trigger default")
12101
+ if (input.startsWith('/') && !input.startsWith('//')) {
12102
+ const shouldQuit = await handleMeta(':' + input.slice(1));
12103
+ if (shouldQuit) { resolve(); return; }
12104
+ rl.prompt(); return;
12105
+ }
11980
12106
  if (input.startsWith(':')) {
11981
12107
  const shouldQuit = await handleMeta(input);
11982
12108
  if (shouldQuit) { resolve(); return; }
@@ -12012,13 +12138,13 @@ async function _agentRepl(root, opts) {
12012
12138
  // 1.9.170: stream 모드에서는 이미 실시간으로 출력됐으므로 헤더만 표시 (응답 중복 방지)
12013
12139
  if (state.streamMode && ['claude', 'codex', 'gemini', 'copilot'].includes(state.provider)) {
12014
12140
  log(C.dim(` [assistant: ${state.provider}/${state.model || 'default'}, role=${state.role}, ${dt}ms · ${result.response.length}자]`));
12015
- log('');
12016
12141
  } else {
12017
12142
  log('');
12018
12143
  log(C.bold(`assistant (${state.model || state.provider}, role=${state.role}, ${dt}ms)`));
12019
12144
  log(result.response);
12020
- log('');
12021
12145
  }
12146
+ // 1.9.188 (사용자 명시): 응답 끝 + 입력 구분선 (입력칸 시각 명확)
12147
+ _printInputDivider();
12022
12148
  if (state.history.length % 6 === 0) saveSession(); // 6턴마다 자동 저장
12023
12149
  } else {
12024
12150
  // 1.9.185 (사용자 명시): REPL 호출 실패 친절한 진단 — 어느 단계에서 실패했는지 + 즉시 검증 명령 안내
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.187",
3
+ "version": "1.9.189",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",