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 +196 -0
- package/README.md +3 -3
- package/bin/harness.js +155 -29
- package/package.json +1 -1
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
|
-
[](https://www.npmjs.com/package/leerness) [](https://www.npmjs.com/package/leerness) []() []() []() []() []() []() []() []() []() []() []() []()
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
╔══════════════════════════════════════════════════════════════╗
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
║ ██║ ██╔══╝ ██╔══╝ ██╔══██╗██║╚██╗██║██╔══╝ ╚════██║ ║
|
|
13
13
|
║ ███████╗███████╗███████╗██║ ██║██║ ╚████║███████╗███████║ ║
|
|
14
14
|
║ ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝ ║
|
|
15
|
-
║ v1.9.
|
|
15
|
+
║ v1.9.189 AI Agent Reliability Harness + Sandbox ║
|
|
16
16
|
║ verify · remember · orchestrate · audit · sandbox · drift ║
|
|
17
|
-
║
|
|
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.
|
|
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
|
-
|
|
11052
|
-
|
|
11053
|
-
|
|
11054
|
-
else if (provider === '
|
|
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'
|
|
11096
|
-
: ['--print'
|
|
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 === '
|
|
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):
|
|
11160
|
-
//
|
|
11161
|
-
//
|
|
11162
|
-
//
|
|
11163
|
-
//
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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(
|
|
11656
|
-
|
|
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 호출 실패 친절한 진단 — 어느 단계에서 실패했는지 + 즉시 검증 명령 안내
|