leerness 1.9.152 → 1.9.154
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 +76 -0
- package/README.md +2 -2
- package/bin/harness.js +179 -38
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,81 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.154 — 2026-05-20
|
|
4
|
+
|
|
5
|
+
**agent 1-shot multi-provider + REPL `:provider` 활성 검증 (1.9.153 후속, 일관성 강화).**
|
|
6
|
+
|
|
7
|
+
자율 모드 84 라운드.
|
|
8
|
+
|
|
9
|
+
### Added — `leerness agent "<task>" --provider <p>` 1-shot multi-provider
|
|
10
|
+
- 기존: 1-shot 모드는 Ollama 만 호출, 다른 CLI 는 `agents dispatch` 안내만
|
|
11
|
+
- 변경: **claude / codex / gemini / copilot 도 직접 호출** (1.9.153 `_cliChat` 재사용)
|
|
12
|
+
- `_recordRun` observability — provider/model 필드 동적 (`agent_one_shot` kind)
|
|
13
|
+
- task-log 기록도 provider 동적 (`leerness agent (claude:claude, role=actor)` 형식)
|
|
14
|
+
- 실패 시 provider 별 friendly 안내 (Ollama: BASE_URL 확인 / 외부 CLI: `LEERNESS_ENABLE_<X>=1` + 설치)
|
|
15
|
+
|
|
16
|
+
### Added — REPL `:provider <p>` 전환 시 활성 사전 검증
|
|
17
|
+
- validProviders 화이트리스트 5종 — 알 수 없는 provider 거부
|
|
18
|
+
- **비활성 (`ready` 아님) provider 전환 시 즉시 거부** — 실제 호출 시 실패 방지
|
|
19
|
+
- `_checkAgent` 결과로 status/installed/enabled 종합 판정
|
|
20
|
+
- Ollama 는 `LEERNESS_OLLAMA_BASE_URL` 미설정 시 친절한 안내 (블록 아님 — fallback URL 시도)
|
|
21
|
+
- 전환 성공 시 `rl.setPrompt(prompt())` 으로 프롬프트 즉시 갱신
|
|
22
|
+
|
|
23
|
+
### Verified — setup-agents `_selectMany` 일관성 (회귀 방지)
|
|
24
|
+
- 1.9.34 이래 setup-agents 이미 `_selectMany` 사용 중 — 1.9.151 install 흐름과 일관
|
|
25
|
+
- stress-v99 회귀 테스트 추가 (사용자 명시 요청 일관성 보장)
|
|
26
|
+
|
|
27
|
+
### Verified
|
|
28
|
+
- e2e 217/217 ✓
|
|
29
|
+
- stress-v99: 18/18 (1-shot multi-provider 6종 + REPL :provider 검증 4종 + setup-agents 1종 + 누적 회귀 7종)
|
|
30
|
+
- VERSION = 1.9.154 / autonomous-rounds = 84
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 1.9.153 — 2026-05-20
|
|
35
|
+
|
|
36
|
+
**`.env` 직접 생성/마이그레이션 + REPL 배너 leerness 고유 문구 + multi-provider REPL (사용자 명시 3종).**
|
|
37
|
+
|
|
38
|
+
자율 모드 83 라운드.
|
|
39
|
+
|
|
40
|
+
### Added — install 흐름에서 `.env` 직접 생성/마이그레이션 (사용자 명시)
|
|
41
|
+
- 기존 `.env.example` 만 작성 → **이제 `.env` 도 직접 생성** (보안 = 빈 키만)
|
|
42
|
+
- `mergeEnvFile()` — KEY 기준 처리:
|
|
43
|
+
- 기존 키 (사용자가 채운 값 포함) **절대 덮어쓰지 않음**
|
|
44
|
+
- 누락된 키만 빈 값으로 추가
|
|
45
|
+
- 주석/빈 줄은 substring 미포함 시만 append
|
|
46
|
+
- `.gitignore` 에 `.env` 자동 등록 (1.9.71/75 audit 검증과 통합)
|
|
47
|
+
- `.env.example` 은 계속 생성 (참조 템플릿)
|
|
48
|
+
|
|
49
|
+
### Changed — REPL 배너 leerness 고유 문구 (사용자 명시)
|
|
50
|
+
- 기존: `Hermes / OpenClaw / OpenCode 스타일 + Sandbox`
|
|
51
|
+
- 변경: `검수·기억·샌드박스 통합 자율 AI 에이전트`
|
|
52
|
+
- agent 사용법 (non-TTY) 헤더도 동일 — leerness 자체 정체성 강화
|
|
53
|
+
|
|
54
|
+
### Added — REPL multi-provider 세션 관리 (사용자 명시)
|
|
55
|
+
- 기존: Ollama 전용 채팅
|
|
56
|
+
- 변경: **ollama / claude / codex / gemini / copilot** 5종 세션 관리
|
|
57
|
+
- `_cliChat(root, provider, prompt, opts)` — 외부 CLI 호출 헬퍼
|
|
58
|
+
- 각 CLI 별 비-인터랙티브 호출 인자 자동 매핑
|
|
59
|
+
- `runCommandSafe` 경유 (env scrub + permissions + observability 자동)
|
|
60
|
+
- 활성 (`_checkAgent` ready) 확인 후 실행 — 비활성 시 friendly 에러
|
|
61
|
+
- REPL 진입 시:
|
|
62
|
+
- `.env` 자동 로드 (LEERNESS_ENABLE_* 즉시 반영)
|
|
63
|
+
- 활성 CLI 단일 → 자동 선택 / 복수 → 사용자 번호 선택 / 0개 → Ollama fallback
|
|
64
|
+
- `:provider <p>` 메타 명령으로 세션 중 전환 가능
|
|
65
|
+
- `:role <r>` 와 조합하여 planner=claude / actor=codex 같은 multi-CLI 워크플로 가능
|
|
66
|
+
|
|
67
|
+
### Security
|
|
68
|
+
- `.env` 가 `.gitignore` 에 등록 (실제 시크릿 누출 방지)
|
|
69
|
+
- `_cliChat` 가 `runCommandSafe` 경유 → env scrub 화이트리스트만 자식 프로세스에 전파
|
|
70
|
+
- `.harness/runs/run-*.jsonl` 에 `kind: 'agent_repl_cli'` 로 모든 외부 CLI 호출 기록
|
|
71
|
+
|
|
72
|
+
### Verified
|
|
73
|
+
- e2e 217/217 ✓
|
|
74
|
+
- stress-v98: 23/23 (env 생성 7종 + REPL 배너 3종 + multi-provider 6종 + 누적 회귀 7종)
|
|
75
|
+
- VERSION = 1.9.153 / autonomous-rounds = 83
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
3
79
|
## 1.9.152 — 2026-05-20
|
|
4
80
|
|
|
5
81
|
**`agents multi` — 활성 N개 에이전트 일괄 dispatch + handoff 헤드라인 활성 에이전트 카운트 (1.9.151 복수 선택 후속).**
|
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,7 +12,7 @@
|
|
|
12
12
|
║ ██║ ██╔══╝ ██╔══╝ ██╔══██╗██║╚██╗██║██╔══╝ ╚════██║ ║
|
|
13
13
|
║ ███████╗███████╗███████╗██║ ██║██║ ╚████║███████╗███████║ ║
|
|
14
14
|
║ ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝ ║
|
|
15
|
-
║ v1.9.
|
|
15
|
+
║ v1.9.154 AI Agent Reliability Harness + Sandbox ║
|
|
16
16
|
║ verify · remember · orchestrate · audit · sandbox · drift ║
|
|
17
17
|
╚══════════════════════════════════════════════════════════════╝
|
|
18
18
|
```
|
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.154';
|
|
10
10
|
const MARK = '<!-- leerness:managed -->';
|
|
11
11
|
const README_START = '<!-- leerness:project-readme:start -->';
|
|
12
12
|
const README_END = '<!-- leerness:project-readme:end -->';
|
|
@@ -579,6 +579,31 @@ function mergeLinesFile(p, lines) {
|
|
|
579
579
|
writeUtf8(p, next);
|
|
580
580
|
}
|
|
581
581
|
|
|
582
|
+
// 1.9.153: env 파일 전용 key-aware merge — KEY=VALUE 줄을 키 기준 처리 (기존 값 보존, 빈 키만 추가)
|
|
583
|
+
// 사용자가 .env 의 LEERNESS_NPM_TOKEN=abc123 처럼 직접 편집한 값을 절대 덮어쓰지 않음.
|
|
584
|
+
// 주석 / 빈 줄은 substring includes 로 중복 방지 (mergeLinesFile 와 동일).
|
|
585
|
+
function mergeEnvFile(p, lines) {
|
|
586
|
+
const current = exists(p) ? read(p) : '';
|
|
587
|
+
const existingKeys = new Set();
|
|
588
|
+
for (const ln of current.split(/\r?\n/)) {
|
|
589
|
+
const m = ln.match(/^\s*([A-Z][A-Z0-9_]+)\s*=/);
|
|
590
|
+
if (m) existingKeys.add(m[1]);
|
|
591
|
+
}
|
|
592
|
+
let next = current;
|
|
593
|
+
for (const line of lines) {
|
|
594
|
+
const km = line.match(/^\s*([A-Z][A-Z0-9_]+)\s*=/);
|
|
595
|
+
if (km) {
|
|
596
|
+
if (existingKeys.has(km[1])) continue; // 기존 키 값 보존 (덮어쓰기 X)
|
|
597
|
+
next += (next.endsWith('\n') || !next ? '' : '\n') + line + '\n';
|
|
598
|
+
existingKeys.add(km[1]);
|
|
599
|
+
} else {
|
|
600
|
+
// 주석 또는 빈 줄 — substring 미포함 시만 append
|
|
601
|
+
if (!next.includes(line)) next += (next.endsWith('\n') || !next ? '' : '\n') + line + '\n';
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
writeUtf8(p, next);
|
|
605
|
+
}
|
|
606
|
+
|
|
582
607
|
function writeMigrationReport(root, backup, actions, opts = {}) {
|
|
583
608
|
const p = path.join(root, '.harness/migration-report.md');
|
|
584
609
|
const rows = actions.map(a => `| ${a.file} | ${a.action} |`).join('\n');
|
|
@@ -820,7 +845,10 @@ async function install(root, opts = {}) {
|
|
|
820
845
|
}
|
|
821
846
|
if (!opts.dry) {
|
|
822
847
|
mergeLinesFile(path.join(root, '.gitignore'), [
|
|
823
|
-
|
|
848
|
+
// 1.9.153: .env 직접 생성 + 사용자 글로벌 룰 SECRET_PATTERNS 6종 일괄 ignore (audit 통합)
|
|
849
|
+
// audit 가 검사하는 6 패턴: .env / .env.local / .env.production / .env.*.local / *.pem / credentials.json
|
|
850
|
+
'.env', '.env.local', '.env.production', '.env.*.local', '*.pem', 'credentials.json',
|
|
851
|
+
'.harness/skill-publish.local.json','.harness/**/*.local.json',
|
|
824
852
|
'.harness/archive/','.harness/migration-report.md','.harness/cache/',
|
|
825
853
|
// 1.9.147: 자동 유지보수 — 자격증명 + incident 페이로드 비공개 (보안)
|
|
826
854
|
'.harness/credentials.local.json','.harness/incidents/',
|
|
@@ -836,8 +864,12 @@ async function install(root, opts = {}) {
|
|
|
836
864
|
return new Set([a]); // back-compat: 단일 문자열
|
|
837
865
|
})();
|
|
838
866
|
const enable = (cli) => enabledSet.has(cli);
|
|
839
|
-
|
|
840
|
-
|
|
867
|
+
// 1.9.153: .env.example 은 템플릿 (배포 가능, 실제 시크릿 값 없음)
|
|
868
|
+
// .env 는 실 사용 파일 — 사용자가 토큰 채워 넣음. 보안 정책: 토큰 값은 절대 자동 채우지 않음 (키만).
|
|
869
|
+
// .gitignore 에 .env 가 들어가 있어야 함 (audit 가 자동 검증). mergeLinesFile 은 기존 키 유지 + 신규 추가.
|
|
870
|
+
const envLines = [
|
|
871
|
+
'# Leerness — environment variable names only. Do not commit real secrets (this file is in .gitignore).',
|
|
872
|
+
`# Generated/migrated by leerness v${VERSION} at ${new Date().toISOString().slice(0, 10)}.`,
|
|
841
873
|
'LEERNESS_NPM_TOKEN=','LEERNESS_GITHUB_TOKEN=',
|
|
842
874
|
'# 1.9.22 — orchestrate opt-in. URL이 설정되면 leerness가 Ollama를 사용 가능. 미설정 시 LLM 호출 자동 시작 금지.',
|
|
843
875
|
`LEERNESS_OLLAMA_BASE_URL=${enable('ollama') ? 'http://localhost:11434' : ''}`,
|
|
@@ -850,11 +882,22 @@ async function install(root, opts = {}) {
|
|
|
850
882
|
`LEERNESS_ENABLE_COPILOT=${enable('copilot') ? 1 : 0}`,
|
|
851
883
|
`LEERNESS_ENABLE_OLLAMA=${enable('ollama') ? 1 : 0}`,
|
|
852
884
|
'# 1.9.42 — agentskills.io 공개 표준 스킬 자동 탐색 (opt-in). URL 설정 시 `leerness skill discover` 사용 가능.',
|
|
853
|
-
'#
|
|
885
|
+
'# 예시 URL: https://agentskills.io/llms.txt',
|
|
854
886
|
'LEERNESS_SKILL_DISCOVER_URL=',
|
|
855
887
|
'# (선택) 사용자 요청 분석 시 자동 매칭 스킬 추천. 1=활성, 0/미설정=비활성.',
|
|
856
888
|
'LEERNESS_SKILL_AUTO_DISCOVER=0'
|
|
857
|
-
]
|
|
889
|
+
];
|
|
890
|
+
mergeLinesFile(path.join(root, '.env.example'), envLines);
|
|
891
|
+
// 1.9.153: .env 직접 생성/마이그레이션 (사용자 명시 요청). 보안 = 빈 값만 — 사용자가 직접 토큰 채움.
|
|
892
|
+
// 기존 .env 가 있으면 mergeEnvFile 이 KEY 기준 처리:
|
|
893
|
+
// - 기존 키 (사용자가 채운 값 포함) 는 절대 덮어쓰지 않음
|
|
894
|
+
// - 누락된 키만 빈 값으로 추가
|
|
895
|
+
// .env 가 .gitignore 에 등록되어 있는지 audit 가 검증 (1.9.75+).
|
|
896
|
+
try {
|
|
897
|
+
mergeEnvFile(path.join(root, '.env'), envLines);
|
|
898
|
+
} catch (e) {
|
|
899
|
+
warn(`.env 생성/마이그레이션 실패 (계속 진행): ${e.message}`);
|
|
900
|
+
}
|
|
858
901
|
// 1.9.146: agent 권한 파일 자동 생성 (사용자 명시 요청 #5)
|
|
859
902
|
if (resolved.permissionMode) {
|
|
860
903
|
try { _writePermissionsPreset(root, resolved.permissionMode); } catch (e) { warn('permissions 생성 실패: ' + e.message); }
|
|
@@ -10073,6 +10116,41 @@ async function _ollamaListModels() {
|
|
|
10073
10116
|
});
|
|
10074
10117
|
}
|
|
10075
10118
|
|
|
10119
|
+
// 1.9.153: 외부 CLI 채팅 호출 (multi-provider REPL — 사용자 명시 요청)
|
|
10120
|
+
// claude/codex/gemini/copilot 를 child_process 로 호출 후 stdout 캡처.
|
|
10121
|
+
// runCommandSafe 경유 — env scrub + permissions + observability 자동 적용.
|
|
10122
|
+
async function _cliChat(root, provider, prompt, opts) {
|
|
10123
|
+
opts = opts || {};
|
|
10124
|
+
const agent = EXTERNAL_AGENTS.find(a => a.id === provider);
|
|
10125
|
+
if (!agent) return { ok: false, error: `unknown provider: ${provider}`, provider };
|
|
10126
|
+
const status = _checkAgent(agent);
|
|
10127
|
+
if (status.status !== 'ready') {
|
|
10128
|
+
return { ok: false, error: `${provider} 비활성 (${status.status}) — .env 에서 ${agent.envFlag}=1 + CLI 설치 필요`, provider };
|
|
10129
|
+
}
|
|
10130
|
+
// CLI 별 비-인터랙티브 호출 인자 매핑 (read-only 모드 — REPL 안에서 파일 수정 X)
|
|
10131
|
+
let cmd, args;
|
|
10132
|
+
if (provider === 'claude') { cmd = 'claude'; args = ['--print', prompt]; }
|
|
10133
|
+
else if (provider === 'codex') { cmd = 'codex'; args = ['exec', '--skip-git-repo-check', prompt]; }
|
|
10134
|
+
else if (provider === 'gemini') { cmd = 'gemini'; args = ['-p', prompt]; }
|
|
10135
|
+
else if (provider === 'copilot') { cmd = 'gh'; args = ['copilot', 'suggest', prompt]; }
|
|
10136
|
+
else return { ok: false, error: `provider ${provider} 미지원`, provider };
|
|
10137
|
+
// runCommandSafe — env scrub + observability 자동
|
|
10138
|
+
const r = runCommandSafe(cmd, args, {
|
|
10139
|
+
cwd: process.cwd(), root,
|
|
10140
|
+
timeout: opts.timeout || 60000,
|
|
10141
|
+
allowOutsideCwd: true, // CLI 가 cwd 밖에서 실행될 수 있음
|
|
10142
|
+
kind: 'agent_repl_cli', label: `repl-${provider}`
|
|
10143
|
+
});
|
|
10144
|
+
if (r.status === 0) {
|
|
10145
|
+
return { ok: true, response: (r.stdout || '').trim(), provider, model: provider };
|
|
10146
|
+
}
|
|
10147
|
+
return {
|
|
10148
|
+
ok: false,
|
|
10149
|
+
error: `exit=${r.status} ${(r.stderr || r.stdout || '').slice(0, 200)}`,
|
|
10150
|
+
provider
|
|
10151
|
+
};
|
|
10152
|
+
}
|
|
10153
|
+
|
|
10076
10154
|
// 1.9.149: observability lite — 모든 agent 호출의 traceId + duration + exit + failureCause 기록
|
|
10077
10155
|
function _runsDir(root) { return path.join(absRoot(root), '.harness', 'runs'); }
|
|
10078
10156
|
function _recordRun(root, entry) {
|
|
@@ -10228,8 +10306,10 @@ const _AGENT_ROLE_PROMPTS = {
|
|
|
10228
10306
|
reviewer: '역할: reviewer. planner 의 계획 또는 actor 의 결과를 비판적으로 검토. 누락된 검증, 잠재 cascade, 오류 가능성 지적. 동의/수정 결론 명시.',
|
|
10229
10307
|
actor: '역할: actor. 계획에 따라 정확한 명령/코드만 실행. evidence(파일 경로 + 테스트 결과) 함께 기록. 새 계획 생성 금지.'
|
|
10230
10308
|
};
|
|
10231
|
-
// 1.9.149: REPL 모드 —
|
|
10309
|
+
// 1.9.149+1.9.153: REPL 모드 — leerness 자율 AI 에이전트 (multi-provider 세션)
|
|
10232
10310
|
async function _agentRepl(root, opts) {
|
|
10311
|
+
// 1.9.153: .env 자동 로드 (REPL 진입 직전) — install 직후 LEERNESS_ENABLE_* 즉시 반영
|
|
10312
|
+
try { _loadEnvFile(root); } catch {}
|
|
10233
10313
|
const readline = require('readline');
|
|
10234
10314
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
10235
10315
|
const isTty = process.stdout.isTTY;
|
|
@@ -10238,9 +10318,28 @@ async function _agentRepl(root, opts) {
|
|
|
10238
10318
|
bold: s => `\x1b[1m${s}\x1b[0m`, green: s => `\x1b[32m${s}\x1b[0m`,
|
|
10239
10319
|
yel: s => `\x1b[33m${s}\x1b[0m`, mag: s => `\x1b[35m${s}\x1b[0m`
|
|
10240
10320
|
} : { cy:s=>s, dim:s=>s, bold:s=>s, green:s=>s, yel:s=>s, mag:s=>s };
|
|
10321
|
+
// 1.9.153: provider 자동 선택 — opts.provider 명시 안 됨 + 활성 CLI 가 있으면 사용자에게 선택지 표시
|
|
10322
|
+
let initialProvider = opts.provider;
|
|
10323
|
+
if (!initialProvider) {
|
|
10324
|
+
const ready = EXTERNAL_AGENTS.map(a => ({ def: a, status: _checkAgent(a) }))
|
|
10325
|
+
.filter(x => x.status.status === 'ready');
|
|
10326
|
+
if (ready.length === 1) {
|
|
10327
|
+
initialProvider = ready[0].def.id; // 단일 활성 → 자동 선택
|
|
10328
|
+
} else if (ready.length > 1 && isTty) {
|
|
10329
|
+
// 복수 활성 → 사용자에게 선택지 (Ollama 우선이 아닌, 활성된 CLI 중 선택)
|
|
10330
|
+
console.log('');
|
|
10331
|
+
console.log(` 사용 가능한 CLI 에이전트 ${ready.length}개:`);
|
|
10332
|
+
ready.forEach((x, i) => console.log(` ${i + 1}) ${x.def.id}${x.status.version ? ' (v' + x.status.version + ')' : ''}`));
|
|
10333
|
+
const choice = await new Promise(res => rl.question(`\n provider 선택 (Enter=1): `, res));
|
|
10334
|
+
const idx = parseInt(choice, 10) - 1;
|
|
10335
|
+
initialProvider = (idx >= 0 && idx < ready.length) ? ready[idx].def.id : ready[0].def.id;
|
|
10336
|
+
} else {
|
|
10337
|
+
initialProvider = 'ollama'; // 활성 0개 → fallback (사용 시 friendly 경고)
|
|
10338
|
+
}
|
|
10339
|
+
}
|
|
10241
10340
|
// 세션 state
|
|
10242
10341
|
let state = {
|
|
10243
|
-
provider:
|
|
10342
|
+
provider: initialProvider,
|
|
10244
10343
|
model: opts.model || process.env.LEERNESS_OLLAMA_MODEL || null,
|
|
10245
10344
|
role: opts.role || 'actor',
|
|
10246
10345
|
history: [], // [{role: 'user'|'assistant', content: ''}]
|
|
@@ -10258,8 +10357,8 @@ async function _agentRepl(root, opts) {
|
|
|
10258
10357
|
// 환영 메시지 + 모델 선택
|
|
10259
10358
|
log('');
|
|
10260
10359
|
log(C.bold(C.cy(' ╔════════════════════════════════════════════════════╗')));
|
|
10261
|
-
log(C.bold(C.cy(' ║ leerness agent — REPL mode
|
|
10262
|
-
log(C.bold(C.cy(' ║
|
|
10360
|
+
log(C.bold(C.cy(' ║ leerness agent — REPL mode ║')));
|
|
10361
|
+
log(C.bold(C.cy(' ║ 검수·기억·샌드박스 통합 자율 AI 에이전트 ║')));
|
|
10263
10362
|
log(C.bold(C.cy(' ╚════════════════════════════════════════════════════╝')));
|
|
10264
10363
|
log('');
|
|
10265
10364
|
// Ollama 모델 자동 감지 — model이 명시되지 않았으면 사용자에게 선택지 제공
|
|
@@ -10326,7 +10425,36 @@ async function _agentRepl(root, opts) {
|
|
|
10326
10425
|
if (!['planner', 'reviewer', 'actor'].includes(r)) { log(C.yel(` ⚠ role 은 planner/reviewer/actor`)); return false; }
|
|
10327
10426
|
state.role = r; rl.setPrompt(prompt()); log(C.green(` role = ${r}`)); return false;
|
|
10328
10427
|
}
|
|
10329
|
-
if (op === 'provider') {
|
|
10428
|
+
if (op === 'provider') {
|
|
10429
|
+
const newProv = rest[0] || state.provider;
|
|
10430
|
+
const validProviders = ['ollama', 'claude', 'codex', 'gemini', 'copilot'];
|
|
10431
|
+
if (!validProviders.includes(newProv)) {
|
|
10432
|
+
log(C.yel(` ⚠ provider 는 ${validProviders.join(' / ')} (받음: ${newProv})`));
|
|
10433
|
+
return false;
|
|
10434
|
+
}
|
|
10435
|
+
// 1.9.154: provider 전환 시 활성 ready 사전 검증 — 비활성/미설치이면 친절한 안내 후 거부 (실제 호출 시 실패 방지)
|
|
10436
|
+
if (newProv === 'ollama') {
|
|
10437
|
+
// Ollama 는 HTTP 기반 — 단순히 LEERNESS_OLLAMA_BASE_URL 확인
|
|
10438
|
+
const url = process.env.LEERNESS_OLLAMA_BASE_URL || '';
|
|
10439
|
+
if (!url) {
|
|
10440
|
+
log(C.yel(` ⚠ ollama base URL 미설정 (LEERNESS_OLLAMA_BASE_URL) — 기본 http://localhost:11434 시도`));
|
|
10441
|
+
}
|
|
10442
|
+
} else {
|
|
10443
|
+
const agent = EXTERNAL_AGENTS.find(a => a.id === newProv);
|
|
10444
|
+
if (agent) {
|
|
10445
|
+
const st = _checkAgent(agent);
|
|
10446
|
+
if (st.status !== 'ready') {
|
|
10447
|
+
log(C.yel(` ⚠ ${newProv} 비활성 (${st.status}) — .env 에서 LEERNESS_ENABLE_${newProv.toUpperCase()}=1 + CLI 설치 필요`));
|
|
10448
|
+
log(C.dim(` (leerness agents list 로 상태 확인) — provider 전환 취소`));
|
|
10449
|
+
return false;
|
|
10450
|
+
}
|
|
10451
|
+
}
|
|
10452
|
+
}
|
|
10453
|
+
state.provider = newProv;
|
|
10454
|
+
rl.setPrompt(prompt());
|
|
10455
|
+
log(C.green(` provider = ${state.provider}`));
|
|
10456
|
+
return false;
|
|
10457
|
+
}
|
|
10330
10458
|
if (op === 'clear') { process.stdout.write('\x1b[2J\x1b[H'); return false; }
|
|
10331
10459
|
if (op === 'reset') { state.history = []; log(C.dim(' history 초기화됨')); return false; }
|
|
10332
10460
|
if (op === 'history') {
|
|
@@ -10374,11 +10502,15 @@ async function _agentRepl(root, opts) {
|
|
|
10374
10502
|
const finalPrompt = `${rolePrompt}\n\nConversation so far:\n${state.history.slice(-6).map(m => `[${m.role}] ${m.content}`).join('\n')}\n\nRespond as ${state.role}:`;
|
|
10375
10503
|
const t0 = Date.now();
|
|
10376
10504
|
let result;
|
|
10505
|
+
// 1.9.153: multi-provider REPL — ollama 외 claude/codex/gemini/copilot 도 세션 관리 (사용자 명시)
|
|
10377
10506
|
if (state.provider === 'ollama') {
|
|
10378
|
-
log(C.dim(` → ollama
|
|
10507
|
+
log(C.dim(` → ollama${state.model ? ' (' + state.model + ')' : ''} 호출 중...`));
|
|
10379
10508
|
result = await _ollamaChat(finalPrompt, state.model);
|
|
10509
|
+
} else if (['claude', 'codex', 'gemini', 'copilot'].includes(state.provider)) {
|
|
10510
|
+
log(C.dim(` → ${state.provider} CLI 호출 중...`));
|
|
10511
|
+
result = await _cliChat(root, state.provider, finalPrompt, { timeout: 90000 });
|
|
10380
10512
|
} else {
|
|
10381
|
-
log(C.yel(` ⚠ ${state.provider}
|
|
10513
|
+
log(C.yel(` ⚠ ${state.provider} provider 미지원 — :provider ollama|claude|codex|gemini|copilot`));
|
|
10382
10514
|
rl.prompt(); return;
|
|
10383
10515
|
}
|
|
10384
10516
|
const dt = Date.now() - t0;
|
|
@@ -10415,17 +10547,19 @@ async function agentCmd(root, taskArg) {
|
|
|
10415
10547
|
return;
|
|
10416
10548
|
}
|
|
10417
10549
|
// non-TTY: 사용법만 출력
|
|
10418
|
-
log('# leerness agent
|
|
10550
|
+
log('# leerness agent — 검수·기억·샌드박스 통합 자율 AI 에이전트');
|
|
10419
10551
|
log('');
|
|
10420
10552
|
log('사용법:');
|
|
10421
|
-
log(' leerness agent #
|
|
10553
|
+
log(' leerness agent # REPL 모드 (provider 자동 선택 + 채팅)');
|
|
10422
10554
|
log(' leerness agent "<task>" # 1회 위임 (actor 역할 기본)');
|
|
10423
10555
|
log(' leerness agent "<task>" --role planner # 계획만 (1.9.148)');
|
|
10424
10556
|
log(' leerness agent "<task>" --role reviewer # 비판적 검토 (1.9.148)');
|
|
10425
|
-
log(' leerness agent --
|
|
10557
|
+
log(' leerness agent --provider claude # provider 명시 (ollama/claude/codex/gemini/copilot)');
|
|
10558
|
+
log(' leerness agent --interactive --model qwen2.5-coder # 명시적 REPL + Ollama 모델 선택');
|
|
10426
10559
|
log('');
|
|
10427
10560
|
log('REPL 메타 명령: :help / :model / :role / :provider / :history / :save / :quit');
|
|
10428
10561
|
log('REPL Slash 명령 (1.9.150): :verify / :audit / :handoff / :health (sandboxed runCommandSafe)');
|
|
10562
|
+
log('REPL Multi-provider (1.9.153): ollama / claude / codex / gemini / copilot — 활성 CLI 자동 감지');
|
|
10429
10563
|
log('');
|
|
10430
10564
|
log('현재 활성 provider: ' + (_activeCliAgents().join(', ') || '(없음) — .env에서 LEERNESS_ENABLE_* 활성화'));
|
|
10431
10565
|
log('권한 모드: ' + (_readPermissions(root).mode || 'basic'));
|
|
@@ -10453,32 +10587,39 @@ async function agentCmd(root, taskArg) {
|
|
|
10453
10587
|
} catch {}
|
|
10454
10588
|
if (dryRun) { log('\n(dry-run) LLM 호출 스킵 — provider/권한/컨텍스트만 출력'); return; }
|
|
10455
10589
|
if (!provider) { fail('활성 provider 없음 — .env 에서 LEERNESS_ENABLE_OLLAMA=1 또는 LEERNESS_ENABLE_CLAUDE=1 활성화'); process.exitCode = 1; return; }
|
|
10456
|
-
//
|
|
10590
|
+
// 1.9.148: role prompt 자동 prepend (모든 provider 공통)
|
|
10591
|
+
const finalPrompt = `${rolePrompt}\n\nTask: ${task}`;
|
|
10592
|
+
const t0 = Date.now();
|
|
10593
|
+
// 1.9.154: 1-shot 모드도 multi-provider — Ollama 외 claude/codex/gemini/copilot 직접 호출 (1.9.153 _cliChat 재사용)
|
|
10594
|
+
let r;
|
|
10457
10595
|
if (provider === 'ollama') {
|
|
10458
10596
|
log('\n[ollama 호출 중...]');
|
|
10459
|
-
|
|
10460
|
-
|
|
10461
|
-
|
|
10462
|
-
|
|
10463
|
-
|
|
10464
|
-
|
|
10465
|
-
|
|
10466
|
-
|
|
10467
|
-
log('\n[response (model=' + r.model + ', role=' + role + ', ' + dt + 'ms)]\n' + r.response);
|
|
10468
|
-
try {
|
|
10469
|
-
const tlp = taskLogPath(root);
|
|
10470
|
-
const block = `\n## ${today()} leerness agent (ollama:${r.model}, role=${role})\n- task: ${task.slice(0, 200)}\n- response (preview): ${r.response.slice(0, 240).replace(/\n+/g, ' ')}\n`;
|
|
10471
|
-
append(tlp, block);
|
|
10472
|
-
} catch {}
|
|
10473
|
-
} else {
|
|
10474
|
-
fail(`ollama 호출 실패: ${r.error || 'unknown'}`);
|
|
10475
|
-
log(` → ollama serve 실행 + LEERNESS_OLLAMA_BASE_URL 확인`);
|
|
10476
|
-
process.exitCode = 1;
|
|
10477
|
-
}
|
|
10597
|
+
r = await _ollamaChat(finalPrompt);
|
|
10598
|
+
} else if (['claude', 'codex', 'gemini', 'copilot'].includes(provider)) {
|
|
10599
|
+
log(`\n[${provider} CLI 호출 중...]`);
|
|
10600
|
+
r = await _cliChat(root, provider, finalPrompt, { timeout: 90000 });
|
|
10601
|
+
if (r.ok && !r.model) r.model = provider; // _cliChat 결과 보강
|
|
10602
|
+
} else {
|
|
10603
|
+
fail(`알 수 없는 provider: ${provider} (ollama/claude/codex/gemini/copilot)`);
|
|
10604
|
+
process.exitCode = 1;
|
|
10478
10605
|
return;
|
|
10479
10606
|
}
|
|
10480
|
-
|
|
10481
|
-
|
|
10607
|
+
const dt = Date.now() - t0;
|
|
10608
|
+
// 1.9.149: observability 기록
|
|
10609
|
+
_recordRun(root, { kind: 'agent_one_shot', provider, model: r.model || provider, role, durationMs: dt, ok: r.ok, error: r.error, task: task.slice(0, 200), responseChars: (r.response || '').length });
|
|
10610
|
+
if (r.ok) {
|
|
10611
|
+
log(`\n[response (provider=${provider}, model=${r.model || provider}, role=${role}, ${dt}ms)]\n${r.response}`);
|
|
10612
|
+
try {
|
|
10613
|
+
const tlp = taskLogPath(root);
|
|
10614
|
+
const block = `\n## ${today()} leerness agent (${provider}:${r.model || provider}, role=${role})\n- task: ${task.slice(0, 200)}\n- response (preview): ${(r.response || '').slice(0, 240).replace(/\n+/g, ' ')}\n`;
|
|
10615
|
+
append(tlp, block);
|
|
10616
|
+
} catch {}
|
|
10617
|
+
} else {
|
|
10618
|
+
fail(`${provider} 호출 실패: ${r.error || 'unknown'}`);
|
|
10619
|
+
if (provider === 'ollama') log(` → ollama serve 실행 + LEERNESS_OLLAMA_BASE_URL 확인`);
|
|
10620
|
+
else log(` → .env 에서 LEERNESS_ENABLE_${provider.toUpperCase()}=1 + CLI 설치 확인 (leerness agents list)`);
|
|
10621
|
+
process.exitCode = 1;
|
|
10622
|
+
}
|
|
10482
10623
|
}
|
|
10483
10624
|
|
|
10484
10625
|
// ===== 1.9.147: 자동 유지보수 시스템 (사용자 명시 요청) =====
|