leerness 1.9.147 → 1.9.149
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 +310 -63
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,81 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.149 — 2026-05-20
|
|
4
|
+
|
|
5
|
+
**REPL agent (Hermes/OpenClaw/OpenCode 스타일) + observability lite — 사용자 명시 요청 + 3중 LLM 합의 #2.**
|
|
6
|
+
|
|
7
|
+
### Added — `leerness agent` REPL 모드
|
|
8
|
+
- 인자 없이 `leerness agent` (또는 `--interactive` / `--repl`) → 대화형 REPL 진입
|
|
9
|
+
- 시작 시 **Ollama 모델 자동 감지** (`/api/tags`) → 사용자가 번호로 선택
|
|
10
|
+
- 모델 없으면 `LEERNESS_OLLAMA_MODEL` env 또는 `llama3` fallback
|
|
11
|
+
- 대화 history 유지 (마지막 6턴까지 컨텍스트로 전송)
|
|
12
|
+
- 6턴마다 `.harness/agent-sessions/sess-<ts>.jsonl` 자동 저장
|
|
13
|
+
- 종료 시 (`:quit` / `:exit` / `:q` / Ctrl+D) 최종 저장
|
|
14
|
+
|
|
15
|
+
### Added — REPL 메타 명령 (Hermes/OpenClaw 패턴)
|
|
16
|
+
- `:help` / `:?` — 도움말
|
|
17
|
+
- `:model <name>` — 모델 변경 (예: `:model qwen2.5-coder`)
|
|
18
|
+
- `:models` — Ollama 사용 가능 모델 목록
|
|
19
|
+
- `:role <r>` — planner/reviewer/actor 즉시 전환 (프롬프트 색상 변경)
|
|
20
|
+
- `:provider <p>` — provider 전환 (ollama/claude/codex/gemini)
|
|
21
|
+
- `:clear` — 화면 클리어
|
|
22
|
+
- `:reset` — history 초기화
|
|
23
|
+
- `:history` — 최근 10턴 표시
|
|
24
|
+
- `:save` — 세션 즉시 저장
|
|
25
|
+
- `:permissions` — 현재 권한 모드 표시
|
|
26
|
+
- `:quit` / `:exit` / `:q` — 종료 (자동 저장)
|
|
27
|
+
|
|
28
|
+
### Added — observability lite (3중 LLM 합의 #2)
|
|
29
|
+
- `.harness/runs/run-<ts>.jsonl` — 모든 agent 호출 자동 기록
|
|
30
|
+
- 필드: `traceId / kind / provider / model / role / durationMs / ok / error / responseChars`
|
|
31
|
+
- `leerness runs list [--json]` — 최근 50건 (시간 역순)
|
|
32
|
+
- `leerness runs show <id>` — 단일 run 상세
|
|
33
|
+
- agent REPL 매 턴 + 1회 호출 + 세션 전체 모두 자동 기록
|
|
34
|
+
|
|
35
|
+
### Security
|
|
36
|
+
- `.gitignore` 자동 추가: `.harness/agent-sessions/` (대화 내용 보호), `.harness/runs/` (실행 메타데이터 보호)
|
|
37
|
+
|
|
38
|
+
### Validation
|
|
39
|
+
- stress-v94: PASS
|
|
40
|
+
- e2e: 219/219 PASS
|
|
41
|
+
|
|
42
|
+
## 1.9.148 — 2026-05-20
|
|
43
|
+
|
|
44
|
+
**사용자 명시 4종 + 3중 LLM 합의 (GPT-5.5 + Codex + Gemini) 우선 라운드 진행.**
|
|
45
|
+
|
|
46
|
+
### Fixed — 방향키 선택 UI 중첩 출력 버그 (사용자 명시)
|
|
47
|
+
- `_selectOne` / `_selectMany` 의 question + 안내 라인에 `\x1b[2K` (clear entire line) ANSI 추가
|
|
48
|
+
- 이전: 매 render 마다 같은 위치에 question 라인이 누적되어 표시
|
|
49
|
+
- 이후: 화살표 이동 시 라인 깔끔히 덮어쓰기
|
|
50
|
+
|
|
51
|
+
### Changed — 스킬 prompt 제거 (사용자 명시)
|
|
52
|
+
- "스킬 라이브러리 자동 설치 / 건너뛰기" 2-option prompt 완전 제거
|
|
53
|
+
- leerness가 자동으로 표준 공식 5종 설치 (office / commerce-api / ai-verified-skill-publisher / feature-implementation / project-roadmap-generator)
|
|
54
|
+
- 사용자 추가 설치는 `leerness skill install <id>` 명시 호출
|
|
55
|
+
|
|
56
|
+
### Removed — CLI 에이전트 prompt 중복 (사용자 명시)
|
|
57
|
+
- 1.9.32 에서 추가된 "외부 AI CLI 활용하시겠습니까?" prompt 제거 (install 끝부분)
|
|
58
|
+
- 1.9.146 의 4지선다 prompt (resolveInstallOptions 안) 만 유지 — 모든 prompt 단일 위치 통합
|
|
59
|
+
|
|
60
|
+
### Added — verify-code 다중 런타임 자동 감지 (3중 LLM 합의 — top priority)
|
|
61
|
+
- Node: `vitest`/`jest`/`mocha` 의존성 자동 감지 (script 없어도)
|
|
62
|
+
- Python: `pyproject.toml` / `setup.py` / `tests/` → `pytest -q`
|
|
63
|
+
- Go: `go.mod` → `go test ./...`
|
|
64
|
+
- Rust: `Cargo.toml` → `cargo test`
|
|
65
|
+
- TypeScript: `tsconfig.json` → `tsc --noEmit`
|
|
66
|
+
- `--strict` 또는 `LEERNESS_AUTONOMOUS=1`: no-test 감지 시 exit 1 (production 강제)
|
|
67
|
+
|
|
68
|
+
### Added — agent 모드 고도화 (Gemini 권고)
|
|
69
|
+
- `--role planner|reviewer|actor` — 자기-승인 편향 방지
|
|
70
|
+
- planner: step 분해, 코드 작성 금지
|
|
71
|
+
- reviewer: 비판적 검토 (cascade 가능성 지적)
|
|
72
|
+
- actor: 계획대로 정확한 명령/코드만 실행 (기본값)
|
|
73
|
+
- Ollama 호출 시 role prompt 자동 prepend
|
|
74
|
+
|
|
75
|
+
### Validation
|
|
76
|
+
- stress-v93: PASS
|
|
77
|
+
- e2e: 219/219 PASS
|
|
78
|
+
|
|
3
79
|
## 1.9.147 — 2026-05-20
|
|
4
80
|
|
|
5
81
|
**자동 유지보수 시스템 — 사용자 명시 요청.**
|
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.149 AI Agent Reliability Harness ║
|
|
16
16
|
║ verify · remember · orchestrate · audit · prevent 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.149';
|
|
10
10
|
const MARK = '<!-- leerness:managed -->';
|
|
11
11
|
const README_START = '<!-- leerness:project-readme:start -->';
|
|
12
12
|
const README_END = '<!-- leerness:project-readme:end -->';
|
|
@@ -117,7 +117,7 @@ function arg(name, def = null) { const i = process.argv.indexOf(name); return i
|
|
|
117
117
|
function has(name) { return process.argv.includes(name); }
|
|
118
118
|
function nonFlagArgs() {
|
|
119
119
|
const out = [];
|
|
120
|
-
const withValue = new Set(['--language','--skills','--path','--status','--progress','--goal','--reason','--next','--target','--token-env','--package','--out','--from','--repo','--id','--note','--evidence','--query','--limit','--action','--agent','--tool','--doc','--command','--capability','--before','--after','--display','--threshold','--trigger','--check','--set','--min-score','--include','--days','--gh-pages-src','--roadmap','--since','--agents','--model','--timeout','--retry-on-fail','--label','--score','--tokens','--alternatives','--impact','--tag','--surface','--depends-on','--affects','--co-changes-with','--files','--branch','--remote','--task-add','--next-action']);
|
|
120
|
+
const withValue = new Set(['--language','--skills','--path','--status','--progress','--goal','--reason','--next','--target','--token-env','--package','--out','--from','--repo','--id','--note','--evidence','--query','--limit','--action','--agent','--tool','--doc','--command','--capability','--before','--after','--display','--threshold','--trigger','--check','--set','--min-score','--include','--days','--gh-pages-src','--roadmap','--since','--agents','--model','--timeout','--retry-on-fail','--label','--score','--tokens','--alternatives','--impact','--tag','--surface','--depends-on','--affects','--co-changes-with','--files','--branch','--remote','--task-add','--next-action','--role','--provider','--env-var','--deploy','--token-lifetime-hours','--port','--secret']);
|
|
121
121
|
const a = process.argv.slice(2);
|
|
122
122
|
for (let i = 0; i < a.length; i++) {
|
|
123
123
|
const x = a[i];
|
|
@@ -685,22 +685,9 @@ async function resolveInstallOptions(root, opts = {}) {
|
|
|
685
685
|
lang = a === '2' ? 'ko' : a === '3' ? 'en' : detectLanguageValue(root, 'auto');
|
|
686
686
|
}
|
|
687
687
|
}
|
|
688
|
-
// 1.9.
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
const opt = await _selectOne('스킬 라이브러리 설치 (표준 공식 5종)', [
|
|
692
|
-
{ label: '표준 공식 5종 자동 설치 (추천)', description: 'office · commerce-api · ai-verified-skill-publisher · feature-implementation · project-roadmap-generator', id: 'recommended' },
|
|
693
|
-
{ label: '건너뛰기 (필요할 때 leerness skill install 로 추가)', description: '하네스만 설치, 스킬은 없음', id: 'none' }
|
|
694
|
-
], { defaultIndex: 0 });
|
|
695
|
-
skills = (opt && opt.id === 'recommended') ? parseSkillsValue('recommended') : [];
|
|
696
|
-
} else {
|
|
697
|
-
log('\n스킬 라이브러리 설치를 선택하세요.');
|
|
698
|
-
log('1) 표준 공식 5종 자동 설치 (추천)');
|
|
699
|
-
log('2) 건너뛰기 (leerness skill install 로 추가 가능)');
|
|
700
|
-
const a = await ask('선택 [1]: ');
|
|
701
|
-
skills = (a === '2') ? [] : parseSkillsValue('recommended');
|
|
702
|
-
}
|
|
703
|
-
}
|
|
688
|
+
// 1.9.148: 스킬 prompt 제거 (사용자 명시 요청) — leerness가 자동으로 공식 표준 스킬 5종 설치.
|
|
689
|
+
// 필요할 때 사용자가 leerness skill install <id> 로 추가 가능.
|
|
690
|
+
if (!explicitSkills) skills = parseSkillsValue('recommended');
|
|
704
691
|
// 1.9.146: CLI 에이전트 활성화 선택 (사용자 명시 요청 #3 — Ollama 추가)
|
|
705
692
|
// 설치 마지막에 .env.example 에 활성화 옵트인 키만 기록 (실제 토큰 입력은 사용자가 직접).
|
|
706
693
|
let agentsOptIn = null;
|
|
@@ -808,7 +795,9 @@ async function install(root, opts = {}) {
|
|
|
808
795
|
'.harness/skill-publish.local.json','.harness/**/*.local.json','.env.local',
|
|
809
796
|
'.harness/archive/','.harness/migration-report.md','.harness/cache/',
|
|
810
797
|
// 1.9.147: 자동 유지보수 — 자격증명 + incident 페이로드 비공개 (보안)
|
|
811
|
-
'.harness/credentials.local.json','.harness/incidents/'
|
|
798
|
+
'.harness/credentials.local.json','.harness/incidents/',
|
|
799
|
+
// 1.9.149: agent REPL 세션 + observability runs 비공개 (대화 내용 보호)
|
|
800
|
+
'.harness/agent-sessions/','.harness/runs/'
|
|
812
801
|
]);
|
|
813
802
|
// 1.9.146: agentsOptIn 선택에 따라 LEERNESS_ENABLE_* 플래그 자동 설정 (사용자 명시 요청 #3 — Ollama 추가)
|
|
814
803
|
const a = resolved.agentsOptIn || 'none';
|
|
@@ -877,21 +866,9 @@ async function install(root, opts = {}) {
|
|
|
877
866
|
if (!has('--no-auto-roadmap')) {
|
|
878
867
|
try { _autoRoadmap(root, 'install'); } catch (e) { warn('auto-roadmap 실패: ' + (e && e.message)); }
|
|
879
868
|
}
|
|
880
|
-
// 1.9.
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
if (isFreshInit && process.stdin.isTTY && !skipSetup) {
|
|
884
|
-
try {
|
|
885
|
-
log('');
|
|
886
|
-
log('💡 외부 AI CLI(claude/codex/gemini/copilot)를 sub-agent로 활용하시겠습니까?');
|
|
887
|
-
const wantSetup = await _confirm(' 지금 설정할까요? (나중에 `leerness setup-agents`로도 가능)', true);
|
|
888
|
-
if (wantSetup) {
|
|
889
|
-
await setupAgentsCmd(root);
|
|
890
|
-
} else {
|
|
891
|
-
log(' → 나중에 `leerness setup-agents .` 명령으로 설정 가능');
|
|
892
|
-
}
|
|
893
|
-
} catch (e) { warn('setup-agents skipped: ' + (e && e.message)); }
|
|
894
|
-
}
|
|
869
|
+
// 1.9.148: 1.9.32 중복 prompt 제거 (사용자 명시 — CLI 에이전트 prompt 중복).
|
|
870
|
+
// resolveInstallOptions (1.9.146) 가 이미 모든 prompt 모은 위치에 통합된 4지선다 prompt 있음.
|
|
871
|
+
// 별도 setupAgents 명령은 사용자가 명시적으로 `leerness setup-agents` 호출 시에만.
|
|
895
872
|
}
|
|
896
873
|
}
|
|
897
874
|
|
|
@@ -4784,8 +4761,9 @@ async function _selectOne(question, options, opts = {}) {
|
|
|
4784
4761
|
// 이전 출력 지우기: options.length + 2줄 (제목 + 안내)
|
|
4785
4762
|
stdout.write(`\x1b[${options.length + 2}A`);
|
|
4786
4763
|
}
|
|
4787
|
-
|
|
4788
|
-
stdout.write(
|
|
4764
|
+
// 1.9.148 fix: question + 안내 라인에도 \x1b[2K (clear entire line) — 중첩 출력 방지 (사용자 명시 버그)
|
|
4765
|
+
stdout.write(`\x1b[2K\r${C.bold(question)}\n`);
|
|
4766
|
+
stdout.write(`\x1b[2K\r${C.dim(' ↑↓ 이동, Enter 확정, q 취소')}\n`);
|
|
4789
4767
|
for (let i = 0; i < options.length; i++) {
|
|
4790
4768
|
const label = typeof options[i] === 'string' ? options[i] : (options[i].label || String(options[i]));
|
|
4791
4769
|
const desc = typeof options[i] === 'object' && options[i].description ? C.dim(' — ' + options[i].description) : '';
|
|
@@ -4841,8 +4819,9 @@ async function _selectMany(question, options, opts = {}) {
|
|
|
4841
4819
|
const selected = new Set((opts.defaults || []).map(d => typeof d === 'number' ? d : options.findIndex(o => o === d || (o && o.id === d))).filter(i => i >= 0));
|
|
4842
4820
|
const render = (first) => {
|
|
4843
4821
|
if (!first) stdout.write(`\x1b[${options.length + 2}A`);
|
|
4844
|
-
|
|
4845
|
-
stdout.write(
|
|
4822
|
+
// 1.9.148 fix: question + 안내 라인에도 \x1b[2K — 중첩 출력 방지
|
|
4823
|
+
stdout.write(`\x1b[2K\r${C.bold(question)}\n`);
|
|
4824
|
+
stdout.write(`\x1b[2K\r${C.dim(' ↑↓ 이동, Space 토글, a 전체, n 해제, Enter 확정, q 취소')}\n`);
|
|
4846
4825
|
for (let i = 0; i < options.length; i++) {
|
|
4847
4826
|
const opt = options[i];
|
|
4848
4827
|
const label = typeof opt === 'string' ? opt : (opt.label || String(opt));
|
|
@@ -7440,25 +7419,49 @@ function releasePublish(root) {
|
|
|
7440
7419
|
}
|
|
7441
7420
|
|
|
7442
7421
|
// ===== 1.9.7 A: verify-code — npm scripts 자동 감지 + evidence 자동 기록 =====
|
|
7422
|
+
// 1.9.148: 다중 런타임 자동 감지 강화 (3중 LLM 합의 — Codex+Gemini+GPT-5.5)
|
|
7423
|
+
// Node (vitest/jest/mocha), Python (pytest), Go (go test), Rust (cargo test), TypeScript (tsc)
|
|
7443
7424
|
function verifyCodeCmd(root) {
|
|
7444
7425
|
root = absRoot(root);
|
|
7445
|
-
const pkgFile = path.join(root, 'package.json');
|
|
7446
|
-
if (!exists(pkgFile)) return fail('package.json 없음 — Node 프로젝트 위치에서 실행하세요.');
|
|
7447
|
-
let pkg;
|
|
7448
|
-
try { pkg = JSON.parse(read(pkgFile)); } catch (e) { return fail('package.json 파싱 실패: ' + e.message); }
|
|
7449
|
-
const scripts = pkg.scripts || {};
|
|
7450
7426
|
const tasks = [];
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
if (
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
7427
|
+
// (1) Node: package.json 우선
|
|
7428
|
+
const pkgFile = path.join(root, 'package.json');
|
|
7429
|
+
if (exists(pkgFile)) {
|
|
7430
|
+
let pkg = {};
|
|
7431
|
+
try { pkg = JSON.parse(read(pkgFile)); } catch (e) { return fail('package.json 파싱 실패: ' + e.message); }
|
|
7432
|
+
const scripts = pkg.scripts || {};
|
|
7433
|
+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
7434
|
+
if (scripts.test) tasks.push({ name: 'test', cmd: 'npm test', runtime: 'node' });
|
|
7435
|
+
else if (scripts['test:smoke']) tasks.push({ name: 'test', cmd: 'npm run test:smoke', runtime: 'node' });
|
|
7436
|
+
// 1.9.148: 명시 script 없어도 인기 러너 의존성 발견 시 시도
|
|
7437
|
+
else if (deps.vitest) tasks.push({ name: 'test', cmd: 'npx --yes vitest run', runtime: 'node' });
|
|
7438
|
+
else if (deps.jest) tasks.push({ name: 'test', cmd: 'npx --yes jest --ci', runtime: 'node' });
|
|
7439
|
+
else if (deps.mocha) tasks.push({ name: 'test', cmd: 'npx --yes mocha', runtime: 'node' });
|
|
7440
|
+
if (scripts.lint) tasks.push({ name: 'lint', cmd: 'npm run lint', runtime: 'node' });
|
|
7441
|
+
if (scripts.typecheck) tasks.push({ name: 'typecheck', cmd: 'npm run typecheck', runtime: 'node' });
|
|
7442
|
+
else if (scripts.tsc) tasks.push({ name: 'typecheck', cmd: 'npm run tsc', runtime: 'node' });
|
|
7443
|
+
else if (exists(path.join(root, 'tsconfig.json'))) tasks.push({ name: 'typecheck', cmd: 'npx --yes tsc --noEmit', runtime: 'node', optional: true });
|
|
7444
|
+
if (has('--build') && scripts.build) tasks.push({ name: 'build', cmd: 'npm run build', runtime: 'node' });
|
|
7445
|
+
if (has('--bench') && scripts.bench) tasks.push({ name: 'bench', cmd: 'npm run bench', runtime: 'node', optional: true });
|
|
7446
|
+
}
|
|
7447
|
+
// (2) Python: pyproject.toml / setup.py / tests/ 존재 시 pytest 시도
|
|
7448
|
+
if (exists(path.join(root, 'pyproject.toml')) || exists(path.join(root, 'setup.py')) || exists(path.join(root, 'tests'))) {
|
|
7449
|
+
if (!tasks.find(t => t.name === 'test')) tasks.push({ name: 'test', cmd: 'pytest -q', runtime: 'python', optional: true });
|
|
7450
|
+
}
|
|
7451
|
+
// (3) Go: go.mod 존재 시 go test ./...
|
|
7452
|
+
if (exists(path.join(root, 'go.mod'))) {
|
|
7453
|
+
tasks.push({ name: 'test:go', cmd: 'go test ./...', runtime: 'go' });
|
|
7454
|
+
}
|
|
7455
|
+
// (4) Rust: Cargo.toml 존재 시 cargo test
|
|
7456
|
+
if (exists(path.join(root, 'Cargo.toml'))) {
|
|
7457
|
+
tasks.push({ name: 'test:rust', cmd: 'cargo test', runtime: 'rust' });
|
|
7458
|
+
}
|
|
7460
7459
|
if (!tasks.length) {
|
|
7461
|
-
|
|
7460
|
+
// 1.9.148: --strict 또는 LEERNESS_AUTONOMOUS=1 시 no-test 도 실패로 (3중 LLM 합의: production-grade test 강제)
|
|
7461
|
+
const strict = has('--strict') || process.env.LEERNESS_AUTONOMOUS === '1';
|
|
7462
|
+
const msg = '검증 task 없음 (package.json#scripts test/lint/typecheck, pytest, go test, cargo test 중 하나도 미발견)';
|
|
7463
|
+
if (strict) { fail(msg + ' — --strict/autonomous 모드: 실패 처리 (exit 1)'); process.exitCode = 1; return; }
|
|
7464
|
+
warn(msg);
|
|
7462
7465
|
return;
|
|
7463
7466
|
}
|
|
7464
7467
|
log(`# verify-code (${tasks.length}개)`);
|
|
@@ -9972,16 +9975,249 @@ async function _ollamaChat(prompt, model) {
|
|
|
9972
9975
|
} catch (e) { resolve({ ok: false, error: e.message, model: mdl }); }
|
|
9973
9976
|
});
|
|
9974
9977
|
}
|
|
9978
|
+
|
|
9979
|
+
// 1.9.149: Ollama 사용 가능 모델 목록 — /api/tags
|
|
9980
|
+
async function _ollamaListModels() {
|
|
9981
|
+
const url = (process.env.LEERNESS_OLLAMA_BASE_URL || 'http://localhost:11434').replace(/\/+$/, '') + '/api/tags';
|
|
9982
|
+
return new Promise((resolve) => {
|
|
9983
|
+
try {
|
|
9984
|
+
const u = new URL(url);
|
|
9985
|
+
const lib = u.protocol === 'https:' ? require('https') : require('http');
|
|
9986
|
+
const req = lib.request({ hostname: u.hostname, port: u.port || 11434, path: u.pathname, method: 'GET', timeout: 4000 }, (res) => {
|
|
9987
|
+
let data = ''; res.on('data', c => data += c);
|
|
9988
|
+
res.on('end', () => {
|
|
9989
|
+
try { const j = JSON.parse(data); resolve({ ok: true, models: (j.models || []).map(m => m.name || m) }); }
|
|
9990
|
+
catch { resolve({ ok: false, models: [] }); }
|
|
9991
|
+
});
|
|
9992
|
+
});
|
|
9993
|
+
req.on('error', () => resolve({ ok: false, models: [] }));
|
|
9994
|
+
req.on('timeout', () => { req.destroy(); resolve({ ok: false, models: [] }); });
|
|
9995
|
+
req.end();
|
|
9996
|
+
} catch { resolve({ ok: false, models: [] }); }
|
|
9997
|
+
});
|
|
9998
|
+
}
|
|
9999
|
+
|
|
10000
|
+
// 1.9.149: observability lite — 모든 agent 호출의 traceId + duration + exit + failureCause 기록
|
|
10001
|
+
function _runsDir(root) { return path.join(absRoot(root), '.harness', 'runs'); }
|
|
10002
|
+
function _recordRun(root, entry) {
|
|
10003
|
+
try {
|
|
10004
|
+
const dir = _runsDir(root); mkdirp(dir);
|
|
10005
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
10006
|
+
const id = `run-${ts}`;
|
|
10007
|
+
const fp = path.join(dir, `${id}.jsonl`);
|
|
10008
|
+
const line = JSON.stringify({ id, at: new Date().toISOString(), ...entry }) + '\n';
|
|
10009
|
+
fs.appendFileSync(fp, line);
|
|
10010
|
+
return id;
|
|
10011
|
+
} catch { return null; }
|
|
10012
|
+
}
|
|
10013
|
+
function runsListCmd(root) {
|
|
10014
|
+
root = absRoot(root || process.cwd());
|
|
10015
|
+
const dir = _runsDir(root);
|
|
10016
|
+
if (!exists(dir)) { log('(runs 없음 — leerness agent 호출 시 자동 기록됨)'); return; }
|
|
10017
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl')).sort().reverse();
|
|
10018
|
+
if (has('--json')) {
|
|
10019
|
+
const items = files.slice(0, 50).map(f => {
|
|
10020
|
+
try { const c = read(path.join(dir, f)).trim().split('\n').map(l => JSON.parse(l)); return { file: f, entries: c }; }
|
|
10021
|
+
catch { return null; }
|
|
10022
|
+
}).filter(Boolean);
|
|
10023
|
+
log(JSON.stringify({ total: files.length, items }, null, 2));
|
|
10024
|
+
return;
|
|
10025
|
+
}
|
|
10026
|
+
log(`# leerness runs list (1.9.149)`);
|
|
10027
|
+
log(`총 ${files.length}건${files.length > 20 ? ' (최근 20)' : ''}`);
|
|
10028
|
+
for (const f of files.slice(0, 20)) {
|
|
10029
|
+
try {
|
|
10030
|
+
const lines = read(path.join(dir, f)).trim().split('\n');
|
|
10031
|
+
const first = JSON.parse(lines[0]);
|
|
10032
|
+
const dur = first.durationMs ? ` ${first.durationMs}ms` : '';
|
|
10033
|
+
const ok = first.ok === false ? ' ⚠fail' : '';
|
|
10034
|
+
log(` ${first.id} · ${first.kind || '?'}${dur}${ok} · ${first.model || first.provider || ''}`);
|
|
10035
|
+
} catch {}
|
|
10036
|
+
}
|
|
10037
|
+
}
|
|
10038
|
+
function runsShowCmd(root, id) {
|
|
10039
|
+
root = absRoot(root || process.cwd());
|
|
10040
|
+
const fp = path.join(_runsDir(root), `${id}.jsonl`);
|
|
10041
|
+
if (!exists(fp)) return fail(`run 없음: ${id}`);
|
|
10042
|
+
log(read(fp));
|
|
10043
|
+
}
|
|
10044
|
+
// 1.9.148: planner/reviewer/actor 역할 시스템 프롬프트 (Gemini 권고 — 자기-승인 편향 방지)
|
|
10045
|
+
const _AGENT_ROLE_PROMPTS = {
|
|
10046
|
+
planner: '역할: planner. task를 step 3-6개로 분해, 각 step의 입출력/검증 방법 명시. 코드 작성 금지, 계획만.',
|
|
10047
|
+
reviewer: '역할: reviewer. planner 의 계획 또는 actor 의 결과를 비판적으로 검토. 누락된 검증, 잠재 cascade, 오류 가능성 지적. 동의/수정 결론 명시.',
|
|
10048
|
+
actor: '역할: actor. 계획에 따라 정확한 명령/코드만 실행. evidence(파일 경로 + 테스트 결과) 함께 기록. 새 계획 생성 금지.'
|
|
10049
|
+
};
|
|
10050
|
+
// 1.9.149: REPL 모드 — Hermes/OpenClaw/OpenCode 스타일 자율형 CLI 에이전트
|
|
10051
|
+
async function _agentRepl(root, opts) {
|
|
10052
|
+
const readline = require('readline');
|
|
10053
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
10054
|
+
const isTty = process.stdout.isTTY;
|
|
10055
|
+
const C = isTty ? {
|
|
10056
|
+
cy: s => `\x1b[36m${s}\x1b[0m`, dim: s => `\x1b[2m${s}\x1b[0m`,
|
|
10057
|
+
bold: s => `\x1b[1m${s}\x1b[0m`, green: s => `\x1b[32m${s}\x1b[0m`,
|
|
10058
|
+
yel: s => `\x1b[33m${s}\x1b[0m`, mag: s => `\x1b[35m${s}\x1b[0m`
|
|
10059
|
+
} : { cy:s=>s, dim:s=>s, bold:s=>s, green:s=>s, yel:s=>s, mag:s=>s };
|
|
10060
|
+
// 세션 state
|
|
10061
|
+
let state = {
|
|
10062
|
+
provider: opts.provider || 'ollama',
|
|
10063
|
+
model: opts.model || process.env.LEERNESS_OLLAMA_MODEL || null,
|
|
10064
|
+
role: opts.role || 'actor',
|
|
10065
|
+
history: [], // [{role: 'user'|'assistant', content: ''}]
|
|
10066
|
+
startedAt: new Date().toISOString(),
|
|
10067
|
+
sessionId: 'sess-' + new Date().toISOString().replace(/[:.]/g, '-')
|
|
10068
|
+
};
|
|
10069
|
+
const sessionPath = () => path.join(absRoot(root), '.harness', 'agent-sessions', `${state.sessionId}.jsonl`);
|
|
10070
|
+
const saveSession = () => {
|
|
10071
|
+
try {
|
|
10072
|
+
mkdirp(path.dirname(sessionPath()));
|
|
10073
|
+
const lines = state.history.map(m => JSON.stringify({ at: new Date().toISOString(), ...m })).join('\n');
|
|
10074
|
+
writeUtf8(sessionPath(), lines + '\n');
|
|
10075
|
+
} catch {}
|
|
10076
|
+
};
|
|
10077
|
+
// 환영 메시지 + 모델 선택
|
|
10078
|
+
log('');
|
|
10079
|
+
log(C.bold(C.cy(' ╔════════════════════════════════════════════════════╗')));
|
|
10080
|
+
log(C.bold(C.cy(' ║ leerness agent — REPL mode (1.9.149) ║')));
|
|
10081
|
+
log(C.bold(C.cy(' ║ Hermes / OpenClaw / OpenCode 스타일 채팅 에이전트 ║')));
|
|
10082
|
+
log(C.bold(C.cy(' ╚════════════════════════════════════════════════════╝')));
|
|
10083
|
+
log('');
|
|
10084
|
+
// Ollama 모델 자동 감지 — model이 명시되지 않았으면 사용자에게 선택지 제공
|
|
10085
|
+
if (state.provider === 'ollama' && !state.model) {
|
|
10086
|
+
log(C.dim(' Ollama 모델 목록 조회 중...'));
|
|
10087
|
+
const r = await _ollamaListModels();
|
|
10088
|
+
if (r.ok && r.models.length) {
|
|
10089
|
+
log(C.green(` 사용 가능 모델 ${r.models.length}개:`));
|
|
10090
|
+
r.models.slice(0, 8).forEach((m, i) => log(` ${i + 1}) ${m}`));
|
|
10091
|
+
const choice = await new Promise(res => rl.question(C.cy('\n 모델 번호 선택 (Enter=1): '), res));
|
|
10092
|
+
const idx = parseInt(choice, 10) - 1;
|
|
10093
|
+
state.model = (idx >= 0 && idx < r.models.length) ? r.models[idx] : r.models[0];
|
|
10094
|
+
log(C.green(` ✓ 모델 선택: ${state.model}`));
|
|
10095
|
+
} else {
|
|
10096
|
+
log(C.yel(` ⚠ Ollama 미가동 또는 모델 없음 — ollama serve + ollama pull <model>`));
|
|
10097
|
+
state.model = process.env.LEERNESS_OLLAMA_MODEL || 'llama3';
|
|
10098
|
+
log(C.dim(` fallback: ${state.model}`));
|
|
10099
|
+
}
|
|
10100
|
+
}
|
|
10101
|
+
log('');
|
|
10102
|
+
log(C.dim(' 메타 명령: :help | :model <m> | :role <r> | :provider <p> | :clear | :save | :history | :quit'));
|
|
10103
|
+
log(C.dim(` 현재 — provider=${state.provider} model=${state.model || '(없음)'} role=${state.role} permissions=${_readPermissions(root).mode}`));
|
|
10104
|
+
log('');
|
|
10105
|
+
const prompt = () => isTty ? C.cy(`agent[${state.role}]> `) : 'agent> ';
|
|
10106
|
+
rl.setPrompt(prompt());
|
|
10107
|
+
rl.prompt();
|
|
10108
|
+
const handleMeta = async (cmd) => {
|
|
10109
|
+
const [op, ...rest] = cmd.slice(1).split(/\s+/);
|
|
10110
|
+
if (op === 'quit' || op === 'exit' || op === 'q') {
|
|
10111
|
+
saveSession();
|
|
10112
|
+
log(C.dim(` 세션 저장: ${rel(root, sessionPath())}`));
|
|
10113
|
+
rl.close(); return true;
|
|
10114
|
+
}
|
|
10115
|
+
if (op === 'help' || op === '?') {
|
|
10116
|
+
log(C.bold('\n 메타 명령:'));
|
|
10117
|
+
log(' :help / :? — 이 도움말');
|
|
10118
|
+
log(' :model <name> — 모델 변경 (예: :model qwen2.5-coder)');
|
|
10119
|
+
log(' :models — Ollama 사용 가능 모델 목록');
|
|
10120
|
+
log(' :role <r> — 역할 변경 (planner / reviewer / actor)');
|
|
10121
|
+
log(' :provider <p> — provider 변경 (ollama / claude / codex / gemini)');
|
|
10122
|
+
log(' :clear — 화면 클리어 + history 유지');
|
|
10123
|
+
log(' :reset — history 초기화');
|
|
10124
|
+
log(' :history — 대화 history 표시');
|
|
10125
|
+
log(' :save — 세션 즉시 저장');
|
|
10126
|
+
log(' :permissions — 현재 권한 모드 표시');
|
|
10127
|
+
log(' :quit / :exit / :q — 종료 (자동 저장)');
|
|
10128
|
+
return false;
|
|
10129
|
+
}
|
|
10130
|
+
if (op === 'model') { state.model = rest.join(' ') || state.model; log(C.green(` model = ${state.model}`)); return false; }
|
|
10131
|
+
if (op === 'models') {
|
|
10132
|
+
const r = await _ollamaListModels();
|
|
10133
|
+
if (r.ok && r.models.length) { log(C.green(` ${r.models.length}개:`)); r.models.forEach(m => log(' • ' + m)); }
|
|
10134
|
+
else log(C.yel(' ⚠ Ollama 미가동'));
|
|
10135
|
+
return false;
|
|
10136
|
+
}
|
|
10137
|
+
if (op === 'role') {
|
|
10138
|
+
const r = rest[0] || 'actor';
|
|
10139
|
+
if (!['planner', 'reviewer', 'actor'].includes(r)) { log(C.yel(` ⚠ role 은 planner/reviewer/actor`)); return false; }
|
|
10140
|
+
state.role = r; rl.setPrompt(prompt()); log(C.green(` role = ${r}`)); return false;
|
|
10141
|
+
}
|
|
10142
|
+
if (op === 'provider') { state.provider = rest[0] || state.provider; log(C.green(` provider = ${state.provider}`)); return false; }
|
|
10143
|
+
if (op === 'clear') { process.stdout.write('\x1b[2J\x1b[H'); return false; }
|
|
10144
|
+
if (op === 'reset') { state.history = []; log(C.dim(' history 초기화됨')); return false; }
|
|
10145
|
+
if (op === 'history') {
|
|
10146
|
+
log(C.bold(`\n 대화 history ${state.history.length}건:`));
|
|
10147
|
+
state.history.slice(-10).forEach((m, i) => log(` [${m.role}] ${m.content.slice(0, 80)}${m.content.length > 80 ? '…' : ''}`));
|
|
10148
|
+
return false;
|
|
10149
|
+
}
|
|
10150
|
+
if (op === 'save') { saveSession(); log(C.dim(` → ${rel(root, sessionPath())}`)); return false; }
|
|
10151
|
+
if (op === 'permissions') { permissionsListCmd(root); return false; }
|
|
10152
|
+
log(C.yel(` 알 수 없는 명령: :${op} (:help 참고)`));
|
|
10153
|
+
return false;
|
|
10154
|
+
};
|
|
10155
|
+
return new Promise(resolve => {
|
|
10156
|
+
rl.on('line', async (line) => {
|
|
10157
|
+
const input = line.trim();
|
|
10158
|
+
if (!input) { rl.prompt(); return; }
|
|
10159
|
+
if (input.startsWith(':')) {
|
|
10160
|
+
const shouldQuit = await handleMeta(input);
|
|
10161
|
+
if (shouldQuit) { resolve(); return; }
|
|
10162
|
+
rl.prompt(); return;
|
|
10163
|
+
}
|
|
10164
|
+
// LLM 호출
|
|
10165
|
+
state.history.push({ role: 'user', content: input });
|
|
10166
|
+
const rolePrompt = _AGENT_ROLE_PROMPTS[state.role] || _AGENT_ROLE_PROMPTS.actor;
|
|
10167
|
+
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}:`;
|
|
10168
|
+
const t0 = Date.now();
|
|
10169
|
+
let result;
|
|
10170
|
+
if (state.provider === 'ollama') {
|
|
10171
|
+
log(C.dim(` → ollama (${state.model}) 호출 중...`));
|
|
10172
|
+
result = await _ollamaChat(finalPrompt, state.model);
|
|
10173
|
+
} else {
|
|
10174
|
+
log(C.yel(` ⚠ ${state.provider} REPL 미지원 — leerness agents dispatch 사용 권장`));
|
|
10175
|
+
rl.prompt(); return;
|
|
10176
|
+
}
|
|
10177
|
+
const dt = Date.now() - t0;
|
|
10178
|
+
_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 });
|
|
10179
|
+
if (result.ok) {
|
|
10180
|
+
state.history.push({ role: 'assistant', content: result.response });
|
|
10181
|
+
log('');
|
|
10182
|
+
log(C.bold(`assistant (${state.model}, role=${state.role}, ${dt}ms)`));
|
|
10183
|
+
log(result.response);
|
|
10184
|
+
log('');
|
|
10185
|
+
if (state.history.length % 6 === 0) saveSession(); // 6턴마다 자동 저장
|
|
10186
|
+
} else {
|
|
10187
|
+
log(C.yel(` ⚠ 실패: ${result.error || 'unknown'}`));
|
|
10188
|
+
}
|
|
10189
|
+
rl.prompt();
|
|
10190
|
+
});
|
|
10191
|
+
rl.on('close', () => { saveSession(); resolve(); });
|
|
10192
|
+
});
|
|
10193
|
+
}
|
|
10194
|
+
|
|
9975
10195
|
async function agentCmd(root, taskArg) {
|
|
9976
10196
|
root = absRoot(root || process.cwd());
|
|
9977
10197
|
const task = (taskArg || arg('--task', '') || '').trim();
|
|
9978
|
-
|
|
9979
|
-
|
|
10198
|
+
// 1.9.149: REPL 진입 — 인자 없거나 --interactive 명시 (Hermes/OpenClaw 스타일)
|
|
10199
|
+
if (!task || has('--interactive') || has('--repl')) {
|
|
10200
|
+
if (process.stdin.isTTY && !has('--no-repl') && process.env.LEERNESS_NO_PROMPT !== '1') {
|
|
10201
|
+
const t0 = Date.now();
|
|
10202
|
+
await _agentRepl(root, {
|
|
10203
|
+
provider: arg('--provider', null),
|
|
10204
|
+
model: arg('--model', null),
|
|
10205
|
+
role: arg('--role', 'actor')
|
|
10206
|
+
});
|
|
10207
|
+
_recordRun(root, { kind: 'agent_repl_session', durationMs: Date.now() - t0, ok: true });
|
|
10208
|
+
return;
|
|
10209
|
+
}
|
|
10210
|
+
// non-TTY: 사용법만 출력
|
|
10211
|
+
log('# leerness agent (1.9.146/148/149) — Hermes/OpenClaw 스타일 CLI 에이전트');
|
|
9980
10212
|
log('');
|
|
9981
10213
|
log('사용법:');
|
|
9982
|
-
log(' leerness agent
|
|
9983
|
-
log(' leerness agent
|
|
9984
|
-
log(' leerness agent --
|
|
10214
|
+
log(' leerness agent # 🆕 1.9.149 REPL 모드 (모델 선택 + 채팅)');
|
|
10215
|
+
log(' leerness agent "<task>" # 1회 위임 (actor 역할 기본)');
|
|
10216
|
+
log(' leerness agent "<task>" --role planner # 계획만 (1.9.148)');
|
|
10217
|
+
log(' leerness agent "<task>" --role reviewer # 비판적 검토 (1.9.148)');
|
|
10218
|
+
log(' leerness agent --interactive --model qwen2.5-coder # 명시적 REPL + model 선택');
|
|
10219
|
+
log('');
|
|
10220
|
+
log('REPL 메타 명령: :help / :model / :role / :provider / :history / :save / :quit');
|
|
9985
10221
|
log('');
|
|
9986
10222
|
log('현재 활성 provider: ' + (_activeCliAgents().join(', ') || '(없음) — .env에서 LEERNESS_ENABLE_* 활성화'));
|
|
9987
10223
|
log('권한 모드: ' + (_readPermissions(root).mode || 'basic'));
|
|
@@ -9989,10 +10225,13 @@ async function agentCmd(root, taskArg) {
|
|
|
9989
10225
|
}
|
|
9990
10226
|
const dryRun = has('--dry-run');
|
|
9991
10227
|
const providerArg = arg('--provider', null);
|
|
10228
|
+
const role = arg('--role', 'actor'); // 1.9.148
|
|
10229
|
+
const rolePrompt = _AGENT_ROLE_PROMPTS[role] || _AGENT_ROLE_PROMPTS.actor;
|
|
9992
10230
|
const active = _activeCliAgents();
|
|
9993
10231
|
const provider = providerArg || active[0] || null;
|
|
9994
|
-
log(`# leerness agent (1.9.146)`);
|
|
10232
|
+
log(`# leerness agent (1.9.146/148)`);
|
|
9995
10233
|
log(`task: ${task.slice(0, 120)}${task.length > 120 ? '…' : ''}`);
|
|
10234
|
+
log(`role: ${role} (${rolePrompt.split('. ')[1] || rolePrompt.slice(0, 60)})`);
|
|
9996
10235
|
log(`provider: ${provider || '(없음 — .env 에서 LEERNESS_ENABLE_* 활성화 필요)'}`);
|
|
9997
10236
|
const perms = _readPermissions(root);
|
|
9998
10237
|
log(`permission mode: ${perms.mode || 'basic'}`);
|
|
@@ -10009,13 +10248,18 @@ async function agentCmd(root, taskArg) {
|
|
|
10009
10248
|
// MVP: Ollama 지원 (로컬). 다른 CLI 는 사용자가 직접 호출 (leerness agents dispatch 이미 존재).
|
|
10010
10249
|
if (provider === 'ollama') {
|
|
10011
10250
|
log('\n[ollama 호출 중...]');
|
|
10012
|
-
|
|
10251
|
+
// 1.9.148: role prompt 자동 prepend
|
|
10252
|
+
const finalPrompt = `${rolePrompt}\n\nTask: ${task}`;
|
|
10253
|
+
const t0 = Date.now();
|
|
10254
|
+
const r = await _ollamaChat(finalPrompt);
|
|
10255
|
+
const dt = Date.now() - t0;
|
|
10256
|
+
// 1.9.149: observability 기록
|
|
10257
|
+
_recordRun(root, { kind: 'agent_one_shot', provider: 'ollama', model: r.model, role, durationMs: dt, ok: r.ok, error: r.error, task: task.slice(0, 200), responseChars: (r.response || '').length });
|
|
10013
10258
|
if (r.ok) {
|
|
10014
|
-
log('\n[response (model=' + r.model + ')]\n' + r.response);
|
|
10015
|
-
// task-log 자동 기록
|
|
10259
|
+
log('\n[response (model=' + r.model + ', role=' + role + ', ' + dt + 'ms)]\n' + r.response);
|
|
10016
10260
|
try {
|
|
10017
10261
|
const tlp = taskLogPath(root);
|
|
10018
|
-
const block = `\n## ${today()} leerness agent (ollama:${r.model})\n- task: ${task.slice(0, 200)}\n- response (preview): ${r.response.slice(0, 240).replace(/\n+/g, ' ')}\n`;
|
|
10262
|
+
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`;
|
|
10019
10263
|
append(tlp, block);
|
|
10020
10264
|
} catch {}
|
|
10021
10265
|
} else {
|
|
@@ -10861,6 +11105,9 @@ async function main() {
|
|
|
10861
11105
|
if (cmd === 'creds' && args[1] === 'check') return credsCheckCmd(arg('--path', process.cwd()), args[2]);
|
|
10862
11106
|
if (cmd === 'creds' && args[1] === 'refresh') return credsRefreshTimestampCmd(arg('--path', process.cwd()), args[2]);
|
|
10863
11107
|
if (cmd === 'deploy' && args[1] === 'auto') return deployAutoCmd(arg('--path', process.cwd()), args[2]);
|
|
11108
|
+
// 1.9.149: observability lite + runs list/show
|
|
11109
|
+
if (cmd === 'runs' && args[1] === 'list') return runsListCmd(arg('--path', process.cwd()));
|
|
11110
|
+
if (cmd === 'runs' && args[1] === 'show') return runsShowCmd(arg('--path', process.cwd()), args[2]);
|
|
10864
11111
|
// 1.9.85: leerness health — 종합 헬스 체크
|
|
10865
11112
|
if (cmd === 'health') return healthCmd(args[1] || arg('--path', process.cwd()));
|
|
10866
11113
|
if (cmd === 'whats-new') return whatsNewCmd(args[1] || arg('--path', process.cwd()));
|