leerness 1.9.169 → 1.9.171
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 +126 -0
- package/README.md +3 -3
- package/bin/harness.js +231 -18
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,131 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.171 — 2026-05-21
|
|
4
|
+
|
|
5
|
+
**AGENTS.md / CLAUDE.md / session-workflow.md 1.9.88~170 누적 갱신 (drift 차단).**
|
|
6
|
+
|
|
7
|
+
자율 모드 101 라운드. 1.9.87 마지막 metadata 갱신 후 84 라운드 간격 — 다음 세션에서 새 기능 (REPL/Bridge/Tab cycle/53 MCP/6 능력) 인지 못 할 위험 차단.
|
|
8
|
+
|
|
9
|
+
### Updated
|
|
10
|
+
- **session-workflow.md**: 1.9.140~170 31 라운드 누적 변경사항 추가
|
|
11
|
+
- release sync-main 자동 (31 라운드 연속 main 자동 push)
|
|
12
|
+
- Feature Causality Graph (1.9.141~143)
|
|
13
|
+
- env detect (1.9.145)
|
|
14
|
+
- CLI 에이전트 모드 + 3-tier 권한 시스템 (1.9.146)
|
|
15
|
+
- REPL agent + Sandboxing runCommandSafe (1.9.149~150)
|
|
16
|
+
- REPL UX 강화 (1.9.151~155)
|
|
17
|
+
- agents multi --execute + consensus (1.9.156)
|
|
18
|
+
- Provider Registry CRUD CLI + MCP **50 도구 마일스톤** (1.9.157~159)
|
|
19
|
+
- 90 라운드 마일스톤 (1.9.160)
|
|
20
|
+
- REPL slash 4종 (1.9.161~162)
|
|
21
|
+
- 5능력 매트릭스 health 통합 (1.9.163)
|
|
22
|
+
- leerness which 진단 (1.9.164)
|
|
23
|
+
- web/pc/lsp bridge 3종 (1.9.165~167)
|
|
24
|
+
- MCP **53 도구 마일스톤** + Bridge 외부 노출 (1.9.168)
|
|
25
|
+
- --include explicit-only hotfix (1.9.169)
|
|
26
|
+
- **100 라운드 + Tab cycle + 실시간 스트리밍** (1.9.170)
|
|
27
|
+
|
|
28
|
+
- **AGENTS.md**:
|
|
29
|
+
- "REPL Agent + Bridge 명령 (1.9.149~170)" 섹션 신설 (자연어 매핑 11종)
|
|
30
|
+
- "6 능력 매트릭스 (1.9.167+)" 섹션 — overallScore + production-ready/beta-ready/mvp 라벨
|
|
31
|
+
|
|
32
|
+
- **CLAUDE.md**:
|
|
33
|
+
- REPL Agent 100 라운드 자율 마일스톤 섹션 (Tab/Shift+Tab/스트리밍)
|
|
34
|
+
- Bridge 3종 opt-in 안내
|
|
35
|
+
- 6 능력 매트릭스 72% production-ready
|
|
36
|
+
|
|
37
|
+
### 자연어 매핑 신규 추가 (AGENTS.md)
|
|
38
|
+
| 사용자 발화 | 즉시 실행 |
|
|
39
|
+
|---|---|
|
|
40
|
+
| "에이전트 켜줘 / REPL 모드" | `leerness agent .` |
|
|
41
|
+
| "Claude/Codex 대화" | `leerness agent . --provider claude` |
|
|
42
|
+
| "다른 provider / Tab" | REPL에서 `Tab` / `Shift+Tab` |
|
|
43
|
+
| "웹 스크린샷 / URL 캡처" | `leerness web screenshot <url>` |
|
|
44
|
+
| "마우스 클릭 / 자동화" | `leerness pc click <x> <y>` |
|
|
45
|
+
| "함수 찾아줘 / 심볼 추출" | `leerness lsp symbols <file>` |
|
|
46
|
+
| "참조 검색" | `leerness lsp references <name> --in <dir>` |
|
|
47
|
+
| "권한 모드 확인" | `leerness permissions list` |
|
|
48
|
+
| "최신 버전 작동 확인" | `leerness which` |
|
|
49
|
+
|
|
50
|
+
### Verified
|
|
51
|
+
- e2e 217/217 baseline 유지
|
|
52
|
+
- stress-v116: **20/20** (session-workflow 6 + AGENTS 4 + CLAUDE 3 + 누적 회귀 7)
|
|
53
|
+
- VERSION = 1.9.171 / autonomous-rounds = 101 / main 자동 push 32 라운드 연속
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 1.9.170 — 2026-05-21 — 🎉 100 라운드 자율 마일스톤
|
|
58
|
+
|
|
59
|
+
**사용자 명시 요청 2종: REPL Tab cycle provider/model + 실시간 스트리밍.**
|
|
60
|
+
|
|
61
|
+
자율 모드 100 라운드 도달 (1.9.71~1.9.170). 사용자 직접 요청 2종 통합:
|
|
62
|
+
1. **Tab 키 cycle** — provider/model 빠른 전환
|
|
63
|
+
2. **실시간 스트리밍** — 추론중/diff/thinking 과정 즉시 표시
|
|
64
|
+
|
|
65
|
+
### 1. REPL Tab cycle (사용자 명시 — "탭 키 등으로 provider/모델 셀렉과 선택 간편하게")
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
agent[claude/actor/▶]> [Tab] # → ollama로 cycle
|
|
69
|
+
agent[ollama/actor/▶]> [Tab] # → claude로 cycle
|
|
70
|
+
agent[claude/actor/▶]> [Shift+Tab] # → 현재 provider의 다음 model로
|
|
71
|
+
⇄ model: claude-opus-4-7 — 최신 1M context (Anthropic Opus 4.7)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**구현**:
|
|
75
|
+
- `readline.emitKeypressEvents(process.stdin, rl)` — Tab 키 감지 활성화
|
|
76
|
+
- `process.stdin.on('keypress', ...)` — Tab/Shift+Tab 가로채서 cycle 실행
|
|
77
|
+
- `getProviders()` — 빌트인 5종 + `.harness/providers.json` 사용자 정의 통합
|
|
78
|
+
- prompt 갱신: `agent[provider/role/▶]>` (스트리밍 ON 시 ▶ 표시)
|
|
79
|
+
|
|
80
|
+
### 2. 실제 모델 catalog 확장 (사용자 명시 — "gpt-5.5, gpt-5.4, claude opus4.7 등 실제 모델")
|
|
81
|
+
|
|
82
|
+
| Provider | 모델 |
|
|
83
|
+
|---|---|
|
|
84
|
+
| **claude** | **claude-opus-4-7** (1M context), opus-4-5, sonnet-4-7, sonnet-4-5, haiku-4-5 |
|
|
85
|
+
| **codex** | **gpt-5.5** (최신 추론), gpt-5.4, gpt-5, gpt-5-codex, o4-mini |
|
|
86
|
+
| **gemini** | gemini-2.5-pro, gemini-2.5-flash, gemini-3.0-pro |
|
|
87
|
+
| ollama | llama3, qwen2.5-coder, gpt-oss, deepseek-coder-v2 |
|
|
88
|
+
| copilot | default |
|
|
89
|
+
|
|
90
|
+
### 3. 실시간 스트리밍 (사용자 명시 — "추론중, diff, 생각하는 과정 실시간 표시")
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
user> 이 함수 리팩토링 해줘
|
|
94
|
+
→ claude CLI stream 호출 중... (Ctrl+C 로 중단)
|
|
95
|
+
── claude stream ──
|
|
96
|
+
[claude-opus-4-7] 분석 중...
|
|
97
|
+
function refactored() {
|
|
98
|
+
// 실시간으로 한 글자씩 흘러나옴
|
|
99
|
+
...
|
|
100
|
+
}
|
|
101
|
+
── /stream (3421ms) ──
|
|
102
|
+
[assistant: claude/claude-opus-4-7, role=actor, 3421ms · 1248자]
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**구현**:
|
|
106
|
+
- `_cliChatStream()` — `cp.spawn(..., { stdio: ['ignore', 'pipe', 'pipe'] })`
|
|
107
|
+
- Claude: `--output-format=stream-json --verbose` 활용 → `content_block_delta` / `thinking_delta` 파싱
|
|
108
|
+
- 다른 provider (codex/gemini/copilot): stdout raw pipe
|
|
109
|
+
- env scrub + cwd jail 유지 (1.9.150 sandboxing 호환)
|
|
110
|
+
- observability: `_recordRun(kind: 'agent_repl_cli_stream')`
|
|
111
|
+
- `:stream on|off` 토글, default ON (env `LEERNESS_REPL_STREAM=0` 로 끄기)
|
|
112
|
+
|
|
113
|
+
### 100 라운드 마일스톤
|
|
114
|
+
| 마일스톤 | 라운드 | 도구 |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| MCP 30 도구 | 1.9.110 | Memory CRUD 완성 |
|
|
117
|
+
| MCP 50 도구 | 1.9.159 | Provider Registry CRUD |
|
|
118
|
+
| 90 라운드 | 1.9.160 | provider sync |
|
|
119
|
+
| MCP 53 도구 | 1.9.168 | Bridge 3종 외부 노출 |
|
|
120
|
+
| **100 라운드** | **1.9.170** | **🎉 Tab cycle + 실시간 스트리밍** |
|
|
121
|
+
|
|
122
|
+
### Verified
|
|
123
|
+
- e2e 217/217 baseline 유지 (1.9.169 hotfix 후)
|
|
124
|
+
- stress-v115: **23/23** (catalog 4 + Tab cycle 3 + 스트리밍 4 + :stream + REPL 통합 5 + 누적 회귀 7)
|
|
125
|
+
- VERSION = 1.9.170 / autonomous-rounds = **100** 🎉 / main 자동 push 31 라운드 연속
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
3
129
|
## 1.9.169 — 2026-05-20
|
|
4
130
|
|
|
5
131
|
**🔧 Hotfix — `_collectWorkspacePaths()` --include 명시 시 cwd 자동 추가 안 함.**
|
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.171 AI Agent Reliability Harness + Sandbox ║
|
|
16
16
|
║ verify · remember · orchestrate · audit · sandbox · drift ║
|
|
17
|
-
║
|
|
17
|
+
║ metadata 1.9.88~170 누적 갱신 · 다음 세션 drift 차단 ║
|
|
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.171';
|
|
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-
|
|
10707
|
-
{ id: 'claude-
|
|
10708
|
-
{ 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' }
|
|
10709
10815
|
],
|
|
10710
10816
|
codex: [
|
|
10711
|
-
{ id: 'gpt-5', note: 'OpenAI 최신' },
|
|
10712
|
-
{ 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)' },
|
|
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-
|
|
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
|
-
|
|
11040
|
-
|
|
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
|
-
|
|
11050
|
-
|
|
11051
|
-
|
|
11052
|
-
|
|
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'}`));
|