leerness 1.9.168 → 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 +116 -0
- package/README.md +3 -3
- package/bin/harness.js +239 -20
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,121 @@
|
|
|
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
|
+
|
|
75
|
+
## 1.9.169 — 2026-05-20
|
|
76
|
+
|
|
77
|
+
**🔧 Hotfix — `_collectWorkspacePaths()` --include 명시 시 cwd 자동 추가 안 함.**
|
|
78
|
+
|
|
79
|
+
자율 모드 99 라운드. 1.9.168 release 후 발견된 e2e flake (209/217) 의 영구 해결.
|
|
80
|
+
|
|
81
|
+
### 문제 진단
|
|
82
|
+
- `cwd: os.tmpdir()` 명령 실행 시 leerness가 cwd의 `.harness` 자동 발견 → 카운트 +1
|
|
83
|
+
- `os.tmpdir()` 에 잔존 `.harness` 디렉토리 존재 시 `--include` 명시했어도 cwd 추가됨
|
|
84
|
+
- 결과: `brainstorm --include p1,p2` 호출 시 "3개 프로젝트" (cwd + p1 + p2) 잘못 카운트
|
|
85
|
+
- 1.9.168 회귀 아님 — 1.9.15 이래 누적된 잠재 버그 (환경 의존 flake)
|
|
86
|
+
- 24,877개 누적 leerness-* 임시 디렉토리 + 잔존 `Temp/.harness` 가 트리거
|
|
87
|
+
|
|
88
|
+
### Fix — _collectWorkspacePaths() (harness.js, 1.9.15 도입 함수)
|
|
89
|
+
```javascript
|
|
90
|
+
function _collectWorkspacePaths(rootBase) {
|
|
91
|
+
const set = new Set();
|
|
92
|
+
const include = arg('--include', null);
|
|
93
|
+
// 1.9.169 fix: --include 명시 시 cwd 자동 추가 스킵 (explicit-only)
|
|
94
|
+
if (!include) {
|
|
95
|
+
if (exists(path.join(rootBase, '.harness'))) set.add(rootBase);
|
|
96
|
+
}
|
|
97
|
+
// ... --all-apps 동작 유지
|
|
98
|
+
if (include) { /* explicit paths only */ }
|
|
99
|
+
return Array.from(set);
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**원칙**: `--include` 가 명시되면 사용자가 의도한 explicit 경로만 사용. `--all-apps` 단독 또는 인자 없는 경우 기존 동작 (cwd 자동 추가) 유지.
|
|
104
|
+
|
|
105
|
+
### 영향 범위
|
|
106
|
+
- `brainstorm --include`, `insights --include`, `handoff --include`, `reuse-map --include`, `retro --include`
|
|
107
|
+
- 모든 `--include` / 다중 프로젝트 명령 정확한 카운트
|
|
108
|
+
|
|
109
|
+
### Verified
|
|
110
|
+
- e2e **217/217 ✓** (1.9.168 시점 209/217 → 1.9.169 217/217 회복)
|
|
111
|
+
- stress-v114: 14/14 (hotfix 4 + e2e 회복 3 + 누적 회귀 7)
|
|
112
|
+
- 진단 검증: `os.tmpdir()/.harness` 시뮬 환경에서도 정확 카운트
|
|
113
|
+
- VERSION = 1.9.169 / autonomous-rounds = 99 / main 자동 push 30 라운드 연속
|
|
114
|
+
|
|
115
|
+
### 다음 라운드 (1.9.170) — 🎉 100 라운드 마일스톤 도달
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
3
119
|
## 1.9.168 — 2026-05-20
|
|
4
120
|
|
|
5
121
|
**MCP bridge 3종 노출 (web/pc/lsp) — 50 → 53 도구 + 외부 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.170 AI Agent Reliability Harness + Sandbox ║
|
|
16
16
|
║ verify · remember · orchestrate · audit · sandbox · drift ║
|
|
17
|
-
║
|
|
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.
|
|
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 -->';
|
|
@@ -6343,9 +6343,16 @@ function _retroOneLine(agg) {
|
|
|
6343
6343
|
}
|
|
6344
6344
|
|
|
6345
6345
|
// 1.9.15: --all-apps / --include 경로 모음
|
|
6346
|
+
// 1.9.169 fix: --include 명시되면 cwd 자동 추가 안 함 (explicit-only).
|
|
6347
|
+
// 기존: cwd/.harness 자동 추가 → 잔존 .harness 시 의도치 않은 카운트 증가 (e2e flake 원인)
|
|
6348
|
+
// 변경: --include 시 사용자가 명시한 경로만 사용. --all-apps 단독은 기존 동작 유지.
|
|
6346
6349
|
function _collectWorkspacePaths(rootBase) {
|
|
6347
6350
|
const set = new Set();
|
|
6348
|
-
|
|
6351
|
+
const include = arg('--include', null);
|
|
6352
|
+
// --include 명시 시 cwd 자동 추가 스킵 (explicit-only 보장)
|
|
6353
|
+
if (!include) {
|
|
6354
|
+
if (exists(path.join(rootBase, '.harness'))) set.add(rootBase);
|
|
6355
|
+
}
|
|
6349
6356
|
if (has('--all-apps')) {
|
|
6350
6357
|
const baseCandidates = [path.resolve(rootBase, '_apps'), path.resolve(rootBase, '..', '_apps')];
|
|
6351
6358
|
for (const base of baseCandidates) {
|
|
@@ -6360,7 +6367,6 @@ function _collectWorkspacePaths(rootBase) {
|
|
|
6360
6367
|
}
|
|
6361
6368
|
}
|
|
6362
6369
|
}
|
|
6363
|
-
const include = arg('--include', null);
|
|
6364
6370
|
if (include) {
|
|
6365
6371
|
for (const p of String(include).split(',')) {
|
|
6366
6372
|
const abs = path.resolve(p.trim());
|
|
@@ -10545,6 +10551,108 @@ async function _cliChat(root, provider, prompt, opts) {
|
|
|
10545
10551
|
};
|
|
10546
10552
|
}
|
|
10547
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
|
+
|
|
10548
10656
|
// 1.9.149: observability lite — 모든 agent 호출의 traceId + duration + exit + failureCause 기록
|
|
10549
10657
|
function _runsDir(root) { return path.join(absRoot(root), '.harness', 'runs'); }
|
|
10550
10658
|
function _recordRun(root, entry) {
|
|
@@ -10695,20 +10803,27 @@ function runsShowCmd(root, id) {
|
|
|
10695
10803
|
log(read(fp));
|
|
10696
10804
|
}
|
|
10697
10805
|
// 1.9.155: provider 별 추천 모델 카탈로그 — REPL :models 명령에서 노출 (실제 가용성은 사용자 CLI 가 결정)
|
|
10806
|
+
// 1.9.170: provider × 실제 모델 catalog 확장 (Tab cycle 지원 — 사용자 명시 요청)
|
|
10807
|
+
// 각 provider 의 최신 실제 모델 ID 를 반영. Tab/Shift+Tab 키로 cycle.
|
|
10698
10808
|
const _PROVIDER_MODEL_CATALOG = {
|
|
10699
10809
|
claude: [
|
|
10700
|
-
{ id: 'claude-opus-4-
|
|
10701
|
-
{ id: 'claude-
|
|
10702
|
-
{ id: 'claude-
|
|
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' }
|
|
10703
10815
|
],
|
|
10704
10816
|
codex: [
|
|
10705
|
-
{ id: 'gpt-5', note: 'OpenAI 최신' },
|
|
10706
|
-
{ id: 'gpt-5
|
|
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)' },
|
|
10707
10821
|
{ id: 'o4-mini', note: '빠른 reasoning' }
|
|
10708
10822
|
],
|
|
10709
10823
|
gemini: [
|
|
10710
|
-
{ id: 'gemini-2.5-pro', note: 'Google 최고급' },
|
|
10711
|
-
{ 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: '실험적 (사용 가능 시)' }
|
|
10712
10827
|
],
|
|
10713
10828
|
copilot: [
|
|
10714
10829
|
{ id: 'default', note: 'gh copilot 기본 (모델 선택 불가)' }
|
|
@@ -10716,10 +10831,14 @@ const _PROVIDER_MODEL_CATALOG = {
|
|
|
10716
10831
|
ollama: [
|
|
10717
10832
|
{ id: 'llama3', note: 'Meta — :models 로 실시간 조회 권장' },
|
|
10718
10833
|
{ id: 'qwen2.5-coder', note: 'Alibaba — 코드 특화' },
|
|
10719
|
-
{ id: 'gpt-oss', note: 'OpenAI 오픈소스' }
|
|
10834
|
+
{ id: 'gpt-oss', note: 'OpenAI 오픈소스' },
|
|
10835
|
+
{ id: 'deepseek-coder-v2', note: 'DeepSeek 코드 모델' }
|
|
10720
10836
|
]
|
|
10721
10837
|
};
|
|
10722
10838
|
|
|
10839
|
+
// 1.9.170: provider cycle 순서 (Tab) — 빌트인 5종. user provider는 동적으로 뒤에 추가.
|
|
10840
|
+
const _PROVIDER_CYCLE_ORDER = ['ollama', 'claude', 'codex', 'gemini', 'copilot'];
|
|
10841
|
+
|
|
10723
10842
|
// 1.9.148: planner/reviewer/actor 역할 시스템 프롬프트 (Gemini 권고 — 자기-승인 편향 방지)
|
|
10724
10843
|
const _AGENT_ROLE_PROMPTS = {
|
|
10725
10844
|
planner: '역할: planner. task를 step 3-6개로 분해, 각 step의 입출력/검증 방법 명시. 코드 작성 금지, 계획만.',
|
|
@@ -10764,8 +10883,11 @@ async function _agentRepl(root, opts) {
|
|
|
10764
10883
|
role: opts.role || 'actor',
|
|
10765
10884
|
history: [], // [{role: 'user'|'assistant', content: ''}]
|
|
10766
10885
|
startedAt: new Date().toISOString(),
|
|
10767
|
-
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)
|
|
10768
10889
|
};
|
|
10890
|
+
if (process.env.LEERNESS_REPL_STREAM === '0') state.streamMode = false;
|
|
10769
10891
|
const sessionPath = () => path.join(absRoot(root), '.harness', 'agent-sessions', `${state.sessionId}.jsonl`);
|
|
10770
10892
|
const saveSession = () => {
|
|
10771
10893
|
try {
|
|
@@ -10828,6 +10950,7 @@ async function _agentRepl(root, opts) {
|
|
|
10828
10950
|
log(C.dim(' 메타 명령: :help | :model <m> | :role <r> | :provider <p> | :status | :clear | :save | :history | :quit'));
|
|
10829
10951
|
log(C.dim(' Slash 명령 (1.9.150): :verify | :audit | :handoff | :health'));
|
|
10830
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 (실시간 출력)'));
|
|
10831
10954
|
log(C.dim(` 현재 — provider=${state.provider} model=${state.model || '(기본)'} role=${state.role} permissions=${_readPermissions(root).mode}`));
|
|
10832
10955
|
// 1.9.155: REPL 진입 시 handoff 컨텍스트 자동 노출 (UX 개선 — 사용자가 매번 :handoff 안 해도 컨텍스트 인지)
|
|
10833
10956
|
try {
|
|
@@ -10844,8 +10967,73 @@ async function _agentRepl(root, opts) {
|
|
|
10844
10967
|
}
|
|
10845
10968
|
} catch {}
|
|
10846
10969
|
log('');
|
|
10847
|
-
const prompt = () => isTty ? C.cy(`agent[${state.role}]> `) : 'agent> ';
|
|
10970
|
+
const prompt = () => isTty ? C.cy(`agent[${state.provider}/${state.role}${state.streamMode ? '/▶' : ''}]> `) : 'agent> ';
|
|
10848
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
|
+
|
|
10849
11037
|
rl.prompt();
|
|
10850
11038
|
const handleMeta = async (cmd) => {
|
|
10851
11039
|
const [op, ...rest] = cmd.slice(1).split(/\s+/);
|
|
@@ -10857,10 +11045,11 @@ async function _agentRepl(root, opts) {
|
|
|
10857
11045
|
if (op === 'help' || op === '?') {
|
|
10858
11046
|
log(C.bold('\n 메타 명령 (provider/모델/역할 전환):'));
|
|
10859
11047
|
log(' :help / :? — 이 도움말');
|
|
10860
|
-
log(' :model <name> — 모델 변경 (1.9.155 모든 provider 지원, 예: :model claude-opus-4-
|
|
11048
|
+
log(' :model <name> — 모델 변경 (1.9.155 모든 provider 지원, 예: :model claude-opus-4-7)');
|
|
10861
11049
|
log(' :models — provider 별 모델 목록 (ollama 실시간 / 그 외 추천 카탈로그)');
|
|
10862
11050
|
log(' :role <r> — 역할 변경 (planner / reviewer / actor)');
|
|
10863
11051
|
log(' :provider <p> — provider 변경 (ollama / claude / codex / gemini / copilot — ready 검증)');
|
|
11052
|
+
log(' :stream on|off — 🆕 1.9.170 실시간 스트리밍 토글 (추론중/diff/thinking 실시간 표시)');
|
|
10864
11053
|
log(' :status — 현재 세션 상태 자세히 (1.9.155)');
|
|
10865
11054
|
log(' :clear — 화면 클리어 + history 유지');
|
|
10866
11055
|
log(' :reset — history 초기화');
|
|
@@ -10868,6 +11057,9 @@ async function _agentRepl(root, opts) {
|
|
|
10868
11057
|
log(' :save — 세션 즉시 저장');
|
|
10869
11058
|
log(' :permissions — 현재 권한 모드 표시');
|
|
10870
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 기준)');
|
|
10871
11063
|
log(C.bold('\n Slash 명령 (1.9.150) — leerness 내부 명령 직접 호출:'));
|
|
10872
11064
|
log(' :verify — leerness verify-code (테스트/타입/린트 자동 검수)');
|
|
10873
11065
|
log(' :audit — leerness audit (보안 + drift + lazy)');
|
|
@@ -10948,6 +11140,21 @@ async function _agentRepl(root, opts) {
|
|
|
10948
11140
|
log(C.green(` provider = ${state.provider}`));
|
|
10949
11141
|
return false;
|
|
10950
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
|
+
}
|
|
10951
11158
|
if (op === 'clear') { process.stdout.write('\x1b[2J\x1b[H'); return false; }
|
|
10952
11159
|
if (op === 'reset') { state.history = []; log(C.dim(' history 초기화됨')); return false; }
|
|
10953
11160
|
if (op === 'history') {
|
|
@@ -11026,12 +11233,18 @@ async function _agentRepl(root, opts) {
|
|
|
11026
11233
|
const t0 = Date.now();
|
|
11027
11234
|
let result;
|
|
11028
11235
|
// 1.9.153: multi-provider REPL — ollama 외 claude/codex/gemini/copilot 도 세션 관리 (사용자 명시)
|
|
11236
|
+
// 1.9.170: streamMode === true 이면 _cliChatStream 사용 (사용자 명시 — 추론중/diff 실시간 표시)
|
|
11029
11237
|
if (state.provider === 'ollama') {
|
|
11030
11238
|
log(C.dim(` → ollama${state.model ? ' (' + state.model + ')' : ''} 호출 중...`));
|
|
11031
11239
|
result = await _ollamaChat(finalPrompt, state.model);
|
|
11032
11240
|
} else if (['claude', 'codex', 'gemini', 'copilot'].includes(state.provider)) {
|
|
11033
|
-
|
|
11034
|
-
|
|
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
|
+
}
|
|
11035
11248
|
} else {
|
|
11036
11249
|
log(C.yel(` ⚠ ${state.provider} provider 미지원 — :provider ollama|claude|codex|gemini|copilot`));
|
|
11037
11250
|
rl.prompt(); return;
|
|
@@ -11040,10 +11253,16 @@ async function _agentRepl(root, opts) {
|
|
|
11040
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 });
|
|
11041
11254
|
if (result.ok) {
|
|
11042
11255
|
state.history.push({ role: 'assistant', content: result.response });
|
|
11043
|
-
|
|
11044
|
-
|
|
11045
|
-
|
|
11046
|
-
|
|
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
|
+
}
|
|
11047
11266
|
if (state.history.length % 6 === 0) saveSession(); // 6턴마다 자동 저장
|
|
11048
11267
|
} else {
|
|
11049
11268
|
log(C.yel(` ⚠ 실패: ${result.error || 'unknown'}`));
|