leerness 1.9.148 → 1.9.150
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 +82 -0
- package/README.md +3 -3
- package/bin/harness.js +382 -15
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,87 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.150 — 2026-05-20
|
|
4
|
+
|
|
5
|
+
**Sandboxing — `runCommandSafe()` wrapper + REPL slash-commands (3중 LLM 합의 #3 / Codex 권고).**
|
|
6
|
+
|
|
7
|
+
자율 모드 80 라운드 마일스톤. 1.9.149 REPL 위에 **샌드박스 보안 레이어** + **leerness 내부 명령 직접 호출**을 추가.
|
|
8
|
+
|
|
9
|
+
### Added — `runCommandSafe(cmd, args, opts)` sandbox wrapper
|
|
10
|
+
- **cwd jail** — `cwd` 가 root 밖 (path traversal) 이면 즉시 exit 126 + `blocked: 'cwd_jail'` 기록
|
|
11
|
+
- **shell:false 기본** — shell injection 표면 차단. `allowShell: true` 시만 shell:true (npm/pytest 호환)
|
|
12
|
+
- **env scrub** — 안전 화이트리스트만 통과 (`PATH`, `HOME`, `TMP`, `LEERNESS_*`, `NPM_CONFIG_*`, ...)
|
|
13
|
+
- 시크릿 환경변수 (DB_PASSWORD, API_KEY 등) 자식 프로세스에 누출 방지
|
|
14
|
+
- **timeout 한도** — 기본 5분, max 10분 (clamp)
|
|
15
|
+
- **permissions 검증** — 1.9.146 `permissions.shell.allowList` 자동 연동
|
|
16
|
+
- basic 모드 (`shell.exec=false`) 에선 핵심 도구 (git/npm/node/pnpm/yarn) 만 허용
|
|
17
|
+
- allowList 외 명령은 즉시 reject + `blocked: 'permissions'` 기록
|
|
18
|
+
- **자동 observability** — 호출마다 `_recordRun` 으로 cmd/args/durationMs/status/cwd 자동 기록
|
|
19
|
+
|
|
20
|
+
### Changed — 위험 호출 sandbox 치환
|
|
21
|
+
- `verify-code` (line ~7473): `cp.spawnSync(t.cmd, [], { shell: true })` → `runCommandSafe(t.cmd, ...)`
|
|
22
|
+
- `deploy auto` (line ~10580): `cp.spawnSync(meta.deployCommand, [], { shell: true })` → `runCommandSafe(...)`
|
|
23
|
+
- `agents bench` (line ~8866): `cp.spawnSync(cmd, cliArgs, { shell: true })` → `runCommandSafe(...)`
|
|
24
|
+
- 3 곳 모두 env scrub + cwd jail + observability 자동 적용
|
|
25
|
+
|
|
26
|
+
### Added — REPL slash-commands (1.9.149 위에)
|
|
27
|
+
- `:verify` — `leerness verify-code` 직접 호출 (sandboxed)
|
|
28
|
+
- `:audit` — `leerness audit` (보안 + drift + lazy)
|
|
29
|
+
- `:handoff` — `leerness handoff --quiet --no-drift-check`
|
|
30
|
+
- `:health` — `leerness health --json`
|
|
31
|
+
- 모두 `runCommandSafe` 경유 — 자식 leerness 호출도 sandbox 적용
|
|
32
|
+
- REPL 안에서 agent가 "현재 상태 점검해줘" 같은 메타 명령으로 leerness 기능을 즉시 호출 가능
|
|
33
|
+
|
|
34
|
+
### Security
|
|
35
|
+
- 시크릿 환경변수 누출 표면 대폭 축소 — `runCommandSafe` 호출 시 화이트리스트 외 env 미전달
|
|
36
|
+
- 사용자 글로벌 룰 준수: API 키/DB 비밀번호 절대 자식 프로세스에 자동 전파 금지
|
|
37
|
+
- `_reports/`, `.harness/agent-sessions/`, `.harness/runs/`, `.harness/credentials.local.json` 비공개 정책 유지
|
|
38
|
+
|
|
39
|
+
### Verified
|
|
40
|
+
- e2e 220/220 (slash-commands handleMeta 통합 + sandbox wrapper)
|
|
41
|
+
- stress-v95: 19/19 (sandbox 5종 + REPL slash 4종 + 누적 회귀 1.9.146~149)
|
|
42
|
+
- VERSION = 1.9.150 / autonomous-rounds = 80
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 1.9.149 — 2026-05-20
|
|
47
|
+
|
|
48
|
+
**REPL agent (Hermes/OpenClaw/OpenCode 스타일) + observability lite — 사용자 명시 요청 + 3중 LLM 합의 #2.**
|
|
49
|
+
|
|
50
|
+
### Added — `leerness agent` REPL 모드
|
|
51
|
+
- 인자 없이 `leerness agent` (또는 `--interactive` / `--repl`) → 대화형 REPL 진입
|
|
52
|
+
- 시작 시 **Ollama 모델 자동 감지** (`/api/tags`) → 사용자가 번호로 선택
|
|
53
|
+
- 모델 없으면 `LEERNESS_OLLAMA_MODEL` env 또는 `llama3` fallback
|
|
54
|
+
- 대화 history 유지 (마지막 6턴까지 컨텍스트로 전송)
|
|
55
|
+
- 6턴마다 `.harness/agent-sessions/sess-<ts>.jsonl` 자동 저장
|
|
56
|
+
- 종료 시 (`:quit` / `:exit` / `:q` / Ctrl+D) 최종 저장
|
|
57
|
+
|
|
58
|
+
### Added — REPL 메타 명령 (Hermes/OpenClaw 패턴)
|
|
59
|
+
- `:help` / `:?` — 도움말
|
|
60
|
+
- `:model <name>` — 모델 변경 (예: `:model qwen2.5-coder`)
|
|
61
|
+
- `:models` — Ollama 사용 가능 모델 목록
|
|
62
|
+
- `:role <r>` — planner/reviewer/actor 즉시 전환 (프롬프트 색상 변경)
|
|
63
|
+
- `:provider <p>` — provider 전환 (ollama/claude/codex/gemini)
|
|
64
|
+
- `:clear` — 화면 클리어
|
|
65
|
+
- `:reset` — history 초기화
|
|
66
|
+
- `:history` — 최근 10턴 표시
|
|
67
|
+
- `:save` — 세션 즉시 저장
|
|
68
|
+
- `:permissions` — 현재 권한 모드 표시
|
|
69
|
+
- `:quit` / `:exit` / `:q` — 종료 (자동 저장)
|
|
70
|
+
|
|
71
|
+
### Added — observability lite (3중 LLM 합의 #2)
|
|
72
|
+
- `.harness/runs/run-<ts>.jsonl` — 모든 agent 호출 자동 기록
|
|
73
|
+
- 필드: `traceId / kind / provider / model / role / durationMs / ok / error / responseChars`
|
|
74
|
+
- `leerness runs list [--json]` — 최근 50건 (시간 역순)
|
|
75
|
+
- `leerness runs show <id>` — 단일 run 상세
|
|
76
|
+
- agent REPL 매 턴 + 1회 호출 + 세션 전체 모두 자동 기록
|
|
77
|
+
|
|
78
|
+
### Security
|
|
79
|
+
- `.gitignore` 자동 추가: `.harness/agent-sessions/` (대화 내용 보호), `.harness/runs/` (실행 메타데이터 보호)
|
|
80
|
+
|
|
81
|
+
### Validation
|
|
82
|
+
- stress-v94: PASS
|
|
83
|
+
- e2e: 219/219 PASS
|
|
84
|
+
|
|
3
85
|
## 1.9.148 — 2026-05-20
|
|
4
86
|
|
|
5
87
|
**사용자 명시 4종 + 3중 LLM 합의 (GPT-5.5 + Codex + Gemini) 우선 라운드 진행.**
|
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,8 +12,8 @@
|
|
|
12
12
|
║ ██║ ██╔══╝ ██╔══╝ ██╔══██╗██║╚██╗██║██╔══╝ ╚════██║ ║
|
|
13
13
|
║ ███████╗███████╗███████╗██║ ██║██║ ╚████║███████╗███████║ ║
|
|
14
14
|
║ ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝ ║
|
|
15
|
-
║ v1.9.
|
|
16
|
-
║ verify · remember · orchestrate · audit ·
|
|
15
|
+
║ v1.9.150 AI Agent Reliability Harness + Sandbox ║
|
|
16
|
+
║ verify · remember · orchestrate · audit · sandbox · drift ║
|
|
17
17
|
╚══════════════════════════════════════════════════════════════╝
|
|
18
18
|
```
|
|
19
19
|
|
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.150';
|
|
10
10
|
const MARK = '<!-- leerness:managed -->';
|
|
11
11
|
const README_START = '<!-- leerness:project-readme:start -->';
|
|
12
12
|
const README_END = '<!-- leerness:project-readme:end -->';
|
|
@@ -795,7 +795,9 @@ async function install(root, opts = {}) {
|
|
|
795
795
|
'.harness/skill-publish.local.json','.harness/**/*.local.json','.env.local',
|
|
796
796
|
'.harness/archive/','.harness/migration-report.md','.harness/cache/',
|
|
797
797
|
// 1.9.147: 자동 유지보수 — 자격증명 + incident 페이로드 비공개 (보안)
|
|
798
|
-
'.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/'
|
|
799
801
|
]);
|
|
800
802
|
// 1.9.146: agentsOptIn 선택에 따라 LEERNESS_ENABLE_* 플래그 자동 설정 (사용자 명시 요청 #3 — Ollama 추가)
|
|
801
803
|
const a = resolved.agentsOptIn || 'none';
|
|
@@ -7468,7 +7470,8 @@ function verifyCodeCmd(root) {
|
|
|
7468
7470
|
for (const t of tasks) {
|
|
7469
7471
|
log(`\n## ${t.name}: ${t.cmd}`);
|
|
7470
7472
|
const start = Date.now();
|
|
7471
|
-
|
|
7473
|
+
// 1.9.150: runCommandSafe — cwd jail + env scrub + observability 자동 (shell:true 유지 — npm/pytest 호환)
|
|
7474
|
+
const r = runCommandSafe(t.cmd, [], { cwd: root, root, timeout: 5 * 60 * 1000, allowShell: true, kind: 'verify_code_task', label: `verify-${t.name}` });
|
|
7472
7475
|
const dur = Date.now() - start;
|
|
7473
7476
|
if (r.status === 0) ok(`${t.name} passed (${dur}ms)`);
|
|
7474
7477
|
else if (t.optional && r.status === 127) warn(`${t.name} 스킵 (${t.cmd} 없음)`);
|
|
@@ -8860,7 +8863,8 @@ async function _benchmarkMeasure(root, task) {
|
|
|
8860
8863
|
else if (agent.id === 'gemini') { cmd = 'gemini'; cliArgs = ['-p', task]; }
|
|
8861
8864
|
else continue;
|
|
8862
8865
|
const t0 = Date.now();
|
|
8863
|
-
|
|
8866
|
+
// 1.9.150: runCommandSafe — agent CLI bench sandbox (env scrub + observability)
|
|
8867
|
+
const r = runCommandSafe(cmd, cliArgs, { cwd: process.cwd(), root, timeout: 60000, allowShell: true, kind: 'agent_bench', label: `bench-${agent.id}`, allowOutsideCwd: true });
|
|
8864
8868
|
const baseTime = Date.now() - t0;
|
|
8865
8869
|
// leerness 검수 layer time 추정 (verify-claim 형식)
|
|
8866
8870
|
const t1 = Date.now();
|
|
@@ -9973,25 +9977,381 @@ async function _ollamaChat(prompt, model) {
|
|
|
9973
9977
|
} catch (e) { resolve({ ok: false, error: e.message, model: mdl }); }
|
|
9974
9978
|
});
|
|
9975
9979
|
}
|
|
9980
|
+
|
|
9981
|
+
// 1.9.149: Ollama 사용 가능 모델 목록 — /api/tags
|
|
9982
|
+
async function _ollamaListModels() {
|
|
9983
|
+
const url = (process.env.LEERNESS_OLLAMA_BASE_URL || 'http://localhost:11434').replace(/\/+$/, '') + '/api/tags';
|
|
9984
|
+
return new Promise((resolve) => {
|
|
9985
|
+
try {
|
|
9986
|
+
const u = new URL(url);
|
|
9987
|
+
const lib = u.protocol === 'https:' ? require('https') : require('http');
|
|
9988
|
+
const req = lib.request({ hostname: u.hostname, port: u.port || 11434, path: u.pathname, method: 'GET', timeout: 4000 }, (res) => {
|
|
9989
|
+
let data = ''; res.on('data', c => data += c);
|
|
9990
|
+
res.on('end', () => {
|
|
9991
|
+
try { const j = JSON.parse(data); resolve({ ok: true, models: (j.models || []).map(m => m.name || m) }); }
|
|
9992
|
+
catch { resolve({ ok: false, models: [] }); }
|
|
9993
|
+
});
|
|
9994
|
+
});
|
|
9995
|
+
req.on('error', () => resolve({ ok: false, models: [] }));
|
|
9996
|
+
req.on('timeout', () => { req.destroy(); resolve({ ok: false, models: [] }); });
|
|
9997
|
+
req.end();
|
|
9998
|
+
} catch { resolve({ ok: false, models: [] }); }
|
|
9999
|
+
});
|
|
10000
|
+
}
|
|
10001
|
+
|
|
10002
|
+
// 1.9.149: observability lite — 모든 agent 호출의 traceId + duration + exit + failureCause 기록
|
|
10003
|
+
function _runsDir(root) { return path.join(absRoot(root), '.harness', 'runs'); }
|
|
10004
|
+
function _recordRun(root, entry) {
|
|
10005
|
+
try {
|
|
10006
|
+
const dir = _runsDir(root); mkdirp(dir);
|
|
10007
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
10008
|
+
const id = `run-${ts}`;
|
|
10009
|
+
const fp = path.join(dir, `${id}.jsonl`);
|
|
10010
|
+
const line = JSON.stringify({ id, at: new Date().toISOString(), ...entry }) + '\n';
|
|
10011
|
+
fs.appendFileSync(fp, line);
|
|
10012
|
+
return id;
|
|
10013
|
+
} catch { return null; }
|
|
10014
|
+
}
|
|
10015
|
+
|
|
10016
|
+
// 1.9.150: Sandboxing — runCommandSafe wrapper (Codex 권고: 3중 LLM 합의 #3)
|
|
10017
|
+
// cwd jail (root 밖 거부) + shell:false 기본 + timeout + env scrub + permissions allowList 검증 + _recordRun 자동
|
|
10018
|
+
const _ENV_SAFE_KEYS = new Set([
|
|
10019
|
+
'PATH', 'HOME', 'USERPROFILE', 'TEMP', 'TMP', 'TMPDIR', 'NODE_PATH', 'NODE_ENV',
|
|
10020
|
+
'LANG', 'LC_ALL', 'LC_CTYPE', 'SHELL', 'COMSPEC', 'SYSTEMROOT', 'WINDIR', 'OS',
|
|
10021
|
+
'PROCESSOR_ARCHITECTURE', 'PROCESSOR_IDENTIFIER', 'NUMBER_OF_PROCESSORS',
|
|
10022
|
+
'PROGRAMFILES', 'PROGRAMFILES(X86)', 'APPDATA', 'LOCALAPPDATA',
|
|
10023
|
+
'GITHUB_TOKEN', 'NPM_TOKEN', 'CI', 'GH_TOKEN'
|
|
10024
|
+
]);
|
|
10025
|
+
function _scrubEnv(extraEnv) {
|
|
10026
|
+
const out = {};
|
|
10027
|
+
for (const k of Object.keys(process.env || {})) {
|
|
10028
|
+
if (_ENV_SAFE_KEYS.has(k) || k.startsWith('LEERNESS_') || k.startsWith('NPM_CONFIG_')) {
|
|
10029
|
+
out[k] = process.env[k];
|
|
10030
|
+
}
|
|
10031
|
+
}
|
|
10032
|
+
if (extraEnv && typeof extraEnv === 'object') {
|
|
10033
|
+
for (const k of Object.keys(extraEnv)) {
|
|
10034
|
+
// Allow caller overrides — explicit opt-in
|
|
10035
|
+
if (extraEnv[k] !== undefined) out[k] = String(extraEnv[k]);
|
|
10036
|
+
}
|
|
10037
|
+
}
|
|
10038
|
+
return out;
|
|
10039
|
+
}
|
|
10040
|
+
function _isCwdSafe(root, cwd) {
|
|
10041
|
+
try {
|
|
10042
|
+
if (!cwd) return true;
|
|
10043
|
+
const r = path.resolve(absRoot(root));
|
|
10044
|
+
const c = path.resolve(cwd);
|
|
10045
|
+
if (c === r) return true;
|
|
10046
|
+
const rel = path.relative(r, c);
|
|
10047
|
+
return !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
10048
|
+
} catch { return false; }
|
|
10049
|
+
}
|
|
10050
|
+
function runCommandSafe(cmd, args, opts) {
|
|
10051
|
+
// opts: { cwd, root, timeout, env, stdio, kind, label, allowShell, encoding, input, allowOutsideCwd }
|
|
10052
|
+
opts = opts || {};
|
|
10053
|
+
const root = opts.root || opts.cwd || process.cwd();
|
|
10054
|
+
const cwd = opts.cwd || root;
|
|
10055
|
+
const cmdStr = String(cmd || '').trim();
|
|
10056
|
+
const argList = Array.isArray(args) ? args.slice() : [];
|
|
10057
|
+
const t0 = Date.now();
|
|
10058
|
+
const label = opts.label || opts.kind || 'shell_exec';
|
|
10059
|
+
// 1) cwd jail
|
|
10060
|
+
if (!opts.allowOutsideCwd && !_isCwdSafe(root, cwd)) {
|
|
10061
|
+
const r = { status: 126, stdout: '', stderr: `runCommandSafe: cwd outside root rejected (${cwd})`, error: 'cwd_jail', blocked: true };
|
|
10062
|
+
try { _recordRun(root, { kind: label, cmd: cmdStr, args: argList, durationMs: Date.now() - t0, ok: false, blocked: 'cwd_jail' }); } catch {}
|
|
10063
|
+
return r;
|
|
10064
|
+
}
|
|
10065
|
+
// 2) permissions allowList (1.9.146)
|
|
10066
|
+
try {
|
|
10067
|
+
const perms = _readPermissions(root);
|
|
10068
|
+
const exec = perms.shell?.exec !== false; // basic 에선 false
|
|
10069
|
+
const allow = perms.shell?.allowList || [];
|
|
10070
|
+
if (!exec && !opts.allowOutsideCwd) {
|
|
10071
|
+
// basic 모드 — git/npm/node 같은 핵심 도구는 허용 (release/install 흐름 유지)
|
|
10072
|
+
const coreAllow = ['git', 'npm', 'npx', 'node', 'pnpm', 'yarn'];
|
|
10073
|
+
const first = cmdStr.split(/\s+/)[0];
|
|
10074
|
+
if (!coreAllow.includes(first) && !allow.includes('*') && !allow.includes(first)) {
|
|
10075
|
+
const r = { status: 126, stdout: '', stderr: `runCommandSafe: shell.exec=false (mode=${perms.mode}). allowList: ${allow.join(',') || '(없음)'} / core: ${coreAllow.join(',')}`, error: 'permissions', blocked: true };
|
|
10076
|
+
try { _recordRun(root, { kind: label, cmd: cmdStr, args: argList, durationMs: Date.now() - t0, ok: false, blocked: 'permissions', mode: perms.mode }); } catch {}
|
|
10077
|
+
return r;
|
|
10078
|
+
}
|
|
10079
|
+
}
|
|
10080
|
+
} catch {}
|
|
10081
|
+
// 3) spawn — shell:false 기본 (shell injection 차단). allowShell=true 시만 shell:true (deploy/build 호환)
|
|
10082
|
+
const useShell = !!opts.allowShell;
|
|
10083
|
+
const timeout = Math.min(opts.timeout || 5 * 60 * 1000, 10 * 60 * 1000);
|
|
10084
|
+
const spawnOpts = {
|
|
10085
|
+
cwd,
|
|
10086
|
+
encoding: opts.encoding || 'utf8',
|
|
10087
|
+
timeout,
|
|
10088
|
+
shell: useShell,
|
|
10089
|
+
env: _scrubEnv(opts.env),
|
|
10090
|
+
input: opts.input,
|
|
10091
|
+
stdio: opts.stdio || 'pipe'
|
|
10092
|
+
};
|
|
10093
|
+
let r;
|
|
10094
|
+
try {
|
|
10095
|
+
if (useShell) {
|
|
10096
|
+
// shell:true 모드 — 인자가 cmd 안에 포함된 단일 문자열인 경우 처리
|
|
10097
|
+
r = cp.spawnSync(cmdStr + (argList.length ? ' ' + argList.join(' ') : ''), [], spawnOpts);
|
|
10098
|
+
} else {
|
|
10099
|
+
// 단일 명령어로 들어온 경우 자동 분리
|
|
10100
|
+
let bin = cmdStr, finalArgs = argList;
|
|
10101
|
+
if (!argList.length && /\s/.test(cmdStr)) {
|
|
10102
|
+
const parts = cmdStr.split(/\s+/);
|
|
10103
|
+
bin = parts[0]; finalArgs = parts.slice(1);
|
|
10104
|
+
}
|
|
10105
|
+
r = cp.spawnSync(bin, finalArgs, spawnOpts);
|
|
10106
|
+
}
|
|
10107
|
+
} catch (e) {
|
|
10108
|
+
r = { status: 1, stdout: '', stderr: e.message, error: 'spawn_exception' };
|
|
10109
|
+
}
|
|
10110
|
+
const dt = Date.now() - t0;
|
|
10111
|
+
try {
|
|
10112
|
+
_recordRun(root, {
|
|
10113
|
+
kind: label, cmd: cmdStr, args: argList,
|
|
10114
|
+
durationMs: dt, status: r.status, ok: r.status === 0,
|
|
10115
|
+
shell: useShell, cwd: path.relative(absRoot(root), cwd) || '.'
|
|
10116
|
+
});
|
|
10117
|
+
} catch {}
|
|
10118
|
+
return r;
|
|
10119
|
+
}
|
|
10120
|
+
function runsListCmd(root) {
|
|
10121
|
+
root = absRoot(root || process.cwd());
|
|
10122
|
+
const dir = _runsDir(root);
|
|
10123
|
+
if (!exists(dir)) { log('(runs 없음 — leerness agent 호출 시 자동 기록됨)'); return; }
|
|
10124
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl')).sort().reverse();
|
|
10125
|
+
if (has('--json')) {
|
|
10126
|
+
const items = files.slice(0, 50).map(f => {
|
|
10127
|
+
try { const c = read(path.join(dir, f)).trim().split('\n').map(l => JSON.parse(l)); return { file: f, entries: c }; }
|
|
10128
|
+
catch { return null; }
|
|
10129
|
+
}).filter(Boolean);
|
|
10130
|
+
log(JSON.stringify({ total: files.length, items }, null, 2));
|
|
10131
|
+
return;
|
|
10132
|
+
}
|
|
10133
|
+
log(`# leerness runs list (1.9.149)`);
|
|
10134
|
+
log(`총 ${files.length}건${files.length > 20 ? ' (최근 20)' : ''}`);
|
|
10135
|
+
for (const f of files.slice(0, 20)) {
|
|
10136
|
+
try {
|
|
10137
|
+
const lines = read(path.join(dir, f)).trim().split('\n');
|
|
10138
|
+
const first = JSON.parse(lines[0]);
|
|
10139
|
+
const dur = first.durationMs ? ` ${first.durationMs}ms` : '';
|
|
10140
|
+
const ok = first.ok === false ? ' ⚠fail' : '';
|
|
10141
|
+
log(` ${first.id} · ${first.kind || '?'}${dur}${ok} · ${first.model || first.provider || ''}`);
|
|
10142
|
+
} catch {}
|
|
10143
|
+
}
|
|
10144
|
+
}
|
|
10145
|
+
function runsShowCmd(root, id) {
|
|
10146
|
+
root = absRoot(root || process.cwd());
|
|
10147
|
+
const fp = path.join(_runsDir(root), `${id}.jsonl`);
|
|
10148
|
+
if (!exists(fp)) return fail(`run 없음: ${id}`);
|
|
10149
|
+
log(read(fp));
|
|
10150
|
+
}
|
|
9976
10151
|
// 1.9.148: planner/reviewer/actor 역할 시스템 프롬프트 (Gemini 권고 — 자기-승인 편향 방지)
|
|
9977
10152
|
const _AGENT_ROLE_PROMPTS = {
|
|
9978
10153
|
planner: '역할: planner. task를 step 3-6개로 분해, 각 step의 입출력/검증 방법 명시. 코드 작성 금지, 계획만.',
|
|
9979
10154
|
reviewer: '역할: reviewer. planner 의 계획 또는 actor 의 결과를 비판적으로 검토. 누락된 검증, 잠재 cascade, 오류 가능성 지적. 동의/수정 결론 명시.',
|
|
9980
10155
|
actor: '역할: actor. 계획에 따라 정확한 명령/코드만 실행. evidence(파일 경로 + 테스트 결과) 함께 기록. 새 계획 생성 금지.'
|
|
9981
10156
|
};
|
|
10157
|
+
// 1.9.149: REPL 모드 — Hermes/OpenClaw/OpenCode 스타일 자율형 CLI 에이전트
|
|
10158
|
+
async function _agentRepl(root, opts) {
|
|
10159
|
+
const readline = require('readline');
|
|
10160
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
10161
|
+
const isTty = process.stdout.isTTY;
|
|
10162
|
+
const C = isTty ? {
|
|
10163
|
+
cy: s => `\x1b[36m${s}\x1b[0m`, dim: s => `\x1b[2m${s}\x1b[0m`,
|
|
10164
|
+
bold: s => `\x1b[1m${s}\x1b[0m`, green: s => `\x1b[32m${s}\x1b[0m`,
|
|
10165
|
+
yel: s => `\x1b[33m${s}\x1b[0m`, mag: s => `\x1b[35m${s}\x1b[0m`
|
|
10166
|
+
} : { cy:s=>s, dim:s=>s, bold:s=>s, green:s=>s, yel:s=>s, mag:s=>s };
|
|
10167
|
+
// 세션 state
|
|
10168
|
+
let state = {
|
|
10169
|
+
provider: opts.provider || 'ollama',
|
|
10170
|
+
model: opts.model || process.env.LEERNESS_OLLAMA_MODEL || null,
|
|
10171
|
+
role: opts.role || 'actor',
|
|
10172
|
+
history: [], // [{role: 'user'|'assistant', content: ''}]
|
|
10173
|
+
startedAt: new Date().toISOString(),
|
|
10174
|
+
sessionId: 'sess-' + new Date().toISOString().replace(/[:.]/g, '-')
|
|
10175
|
+
};
|
|
10176
|
+
const sessionPath = () => path.join(absRoot(root), '.harness', 'agent-sessions', `${state.sessionId}.jsonl`);
|
|
10177
|
+
const saveSession = () => {
|
|
10178
|
+
try {
|
|
10179
|
+
mkdirp(path.dirname(sessionPath()));
|
|
10180
|
+
const lines = state.history.map(m => JSON.stringify({ at: new Date().toISOString(), ...m })).join('\n');
|
|
10181
|
+
writeUtf8(sessionPath(), lines + '\n');
|
|
10182
|
+
} catch {}
|
|
10183
|
+
};
|
|
10184
|
+
// 환영 메시지 + 모델 선택
|
|
10185
|
+
log('');
|
|
10186
|
+
log(C.bold(C.cy(' ╔════════════════════════════════════════════════════╗')));
|
|
10187
|
+
log(C.bold(C.cy(' ║ leerness agent — REPL mode (1.9.150) ║')));
|
|
10188
|
+
log(C.bold(C.cy(' ║ Hermes / OpenClaw / OpenCode 스타일 + Sandbox ║')));
|
|
10189
|
+
log(C.bold(C.cy(' ╚════════════════════════════════════════════════════╝')));
|
|
10190
|
+
log('');
|
|
10191
|
+
// Ollama 모델 자동 감지 — model이 명시되지 않았으면 사용자에게 선택지 제공
|
|
10192
|
+
if (state.provider === 'ollama' && !state.model) {
|
|
10193
|
+
log(C.dim(' Ollama 모델 목록 조회 중...'));
|
|
10194
|
+
const r = await _ollamaListModels();
|
|
10195
|
+
if (r.ok && r.models.length) {
|
|
10196
|
+
log(C.green(` 사용 가능 모델 ${r.models.length}개:`));
|
|
10197
|
+
r.models.slice(0, 8).forEach((m, i) => log(` ${i + 1}) ${m}`));
|
|
10198
|
+
const choice = await new Promise(res => rl.question(C.cy('\n 모델 번호 선택 (Enter=1): '), res));
|
|
10199
|
+
const idx = parseInt(choice, 10) - 1;
|
|
10200
|
+
state.model = (idx >= 0 && idx < r.models.length) ? r.models[idx] : r.models[0];
|
|
10201
|
+
log(C.green(` ✓ 모델 선택: ${state.model}`));
|
|
10202
|
+
} else {
|
|
10203
|
+
log(C.yel(` ⚠ Ollama 미가동 또는 모델 없음 — ollama serve + ollama pull <model>`));
|
|
10204
|
+
state.model = process.env.LEERNESS_OLLAMA_MODEL || 'llama3';
|
|
10205
|
+
log(C.dim(` fallback: ${state.model}`));
|
|
10206
|
+
}
|
|
10207
|
+
}
|
|
10208
|
+
log('');
|
|
10209
|
+
log(C.dim(' 메타 명령: :help | :model <m> | :role <r> | :provider <p> | :clear | :save | :history | :quit'));
|
|
10210
|
+
log(C.dim(' Slash 명령 (1.9.150): :verify | :audit | :handoff | :health'));
|
|
10211
|
+
log(C.dim(` 현재 — provider=${state.provider} model=${state.model || '(없음)'} role=${state.role} permissions=${_readPermissions(root).mode}`));
|
|
10212
|
+
log('');
|
|
10213
|
+
const prompt = () => isTty ? C.cy(`agent[${state.role}]> `) : 'agent> ';
|
|
10214
|
+
rl.setPrompt(prompt());
|
|
10215
|
+
rl.prompt();
|
|
10216
|
+
const handleMeta = async (cmd) => {
|
|
10217
|
+
const [op, ...rest] = cmd.slice(1).split(/\s+/);
|
|
10218
|
+
if (op === 'quit' || op === 'exit' || op === 'q') {
|
|
10219
|
+
saveSession();
|
|
10220
|
+
log(C.dim(` 세션 저장: ${rel(root, sessionPath())}`));
|
|
10221
|
+
rl.close(); return true;
|
|
10222
|
+
}
|
|
10223
|
+
if (op === 'help' || op === '?') {
|
|
10224
|
+
log(C.bold('\n 메타 명령:'));
|
|
10225
|
+
log(' :help / :? — 이 도움말');
|
|
10226
|
+
log(' :model <name> — 모델 변경 (예: :model qwen2.5-coder)');
|
|
10227
|
+
log(' :models — Ollama 사용 가능 모델 목록');
|
|
10228
|
+
log(' :role <r> — 역할 변경 (planner / reviewer / actor)');
|
|
10229
|
+
log(' :provider <p> — provider 변경 (ollama / claude / codex / gemini)');
|
|
10230
|
+
log(' :clear — 화면 클리어 + history 유지');
|
|
10231
|
+
log(' :reset — history 초기화');
|
|
10232
|
+
log(' :history — 대화 history 표시');
|
|
10233
|
+
log(' :save — 세션 즉시 저장');
|
|
10234
|
+
log(' :permissions — 현재 권한 모드 표시');
|
|
10235
|
+
log(' :quit / :exit / :q — 종료 (자동 저장)');
|
|
10236
|
+
log(C.bold('\n Slash 명령 (1.9.150) — leerness 내부 명령 직접 호출:'));
|
|
10237
|
+
log(' :verify — leerness verify-code (테스트/타입/린트 자동 검수)');
|
|
10238
|
+
log(' :audit — leerness audit (보안 + drift + lazy)');
|
|
10239
|
+
log(' :handoff — leerness handoff --quiet (현재 컨텍스트 요약)');
|
|
10240
|
+
log(' :health — leerness health --json (종합 헬스 체크)');
|
|
10241
|
+
return false;
|
|
10242
|
+
}
|
|
10243
|
+
if (op === 'model') { state.model = rest.join(' ') || state.model; log(C.green(` model = ${state.model}`)); return false; }
|
|
10244
|
+
if (op === 'models') {
|
|
10245
|
+
const r = await _ollamaListModels();
|
|
10246
|
+
if (r.ok && r.models.length) { log(C.green(` ${r.models.length}개:`)); r.models.forEach(m => log(' • ' + m)); }
|
|
10247
|
+
else log(C.yel(' ⚠ Ollama 미가동'));
|
|
10248
|
+
return false;
|
|
10249
|
+
}
|
|
10250
|
+
if (op === 'role') {
|
|
10251
|
+
const r = rest[0] || 'actor';
|
|
10252
|
+
if (!['planner', 'reviewer', 'actor'].includes(r)) { log(C.yel(` ⚠ role 은 planner/reviewer/actor`)); return false; }
|
|
10253
|
+
state.role = r; rl.setPrompt(prompt()); log(C.green(` role = ${r}`)); return false;
|
|
10254
|
+
}
|
|
10255
|
+
if (op === 'provider') { state.provider = rest[0] || state.provider; log(C.green(` provider = ${state.provider}`)); return false; }
|
|
10256
|
+
if (op === 'clear') { process.stdout.write('\x1b[2J\x1b[H'); return false; }
|
|
10257
|
+
if (op === 'reset') { state.history = []; log(C.dim(' history 초기화됨')); return false; }
|
|
10258
|
+
if (op === 'history') {
|
|
10259
|
+
log(C.bold(`\n 대화 history ${state.history.length}건:`));
|
|
10260
|
+
state.history.slice(-10).forEach((m, i) => log(` [${m.role}] ${m.content.slice(0, 80)}${m.content.length > 80 ? '…' : ''}`));
|
|
10261
|
+
return false;
|
|
10262
|
+
}
|
|
10263
|
+
if (op === 'save') { saveSession(); log(C.dim(` → ${rel(root, sessionPath())}`)); return false; }
|
|
10264
|
+
if (op === 'permissions') { permissionsListCmd(root); return false; }
|
|
10265
|
+
// 1.9.150: leerness 내부 명령 slash-commands — :verify / :audit / :handoff / :health
|
|
10266
|
+
if (op === 'verify' || op === 'audit' || op === 'handoff' || op === 'health') {
|
|
10267
|
+
const subArgs = {
|
|
10268
|
+
verify: ['verify-code', root],
|
|
10269
|
+
audit: ['audit', root],
|
|
10270
|
+
handoff: ['handoff', root, '--quiet', '--no-drift-check'],
|
|
10271
|
+
health: ['health', root, '--json']
|
|
10272
|
+
}[op];
|
|
10273
|
+
log(C.dim(` → leerness ${subArgs.join(' ')}`));
|
|
10274
|
+
const t0 = Date.now();
|
|
10275
|
+
const r = runCommandSafe(process.execPath, [__filename, ...subArgs], {
|
|
10276
|
+
cwd: root, root, timeout: 60000, kind: 'agent_repl_slash', label: `repl-${op}`,
|
|
10277
|
+
env: { LEERNESS_NO_BANNER: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' }
|
|
10278
|
+
});
|
|
10279
|
+
const dt = Date.now() - t0;
|
|
10280
|
+
if (r.stdout) log(r.stdout.trim().split('\n').slice(0, 30).join('\n'));
|
|
10281
|
+
if (r.status === 0) log(C.green(` ✓ :${op} 완료 (${dt}ms)`));
|
|
10282
|
+
else log(C.yel(` ⚠ :${op} 실패 (exit ${r.status}, ${dt}ms)`));
|
|
10283
|
+
return false;
|
|
10284
|
+
}
|
|
10285
|
+
log(C.yel(` 알 수 없는 명령: :${op} (:help 참고)`));
|
|
10286
|
+
return false;
|
|
10287
|
+
};
|
|
10288
|
+
return new Promise(resolve => {
|
|
10289
|
+
rl.on('line', async (line) => {
|
|
10290
|
+
const input = line.trim();
|
|
10291
|
+
if (!input) { rl.prompt(); return; }
|
|
10292
|
+
if (input.startsWith(':')) {
|
|
10293
|
+
const shouldQuit = await handleMeta(input);
|
|
10294
|
+
if (shouldQuit) { resolve(); return; }
|
|
10295
|
+
rl.prompt(); return;
|
|
10296
|
+
}
|
|
10297
|
+
// LLM 호출
|
|
10298
|
+
state.history.push({ role: 'user', content: input });
|
|
10299
|
+
const rolePrompt = _AGENT_ROLE_PROMPTS[state.role] || _AGENT_ROLE_PROMPTS.actor;
|
|
10300
|
+
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}:`;
|
|
10301
|
+
const t0 = Date.now();
|
|
10302
|
+
let result;
|
|
10303
|
+
if (state.provider === 'ollama') {
|
|
10304
|
+
log(C.dim(` → ollama (${state.model}) 호출 중...`));
|
|
10305
|
+
result = await _ollamaChat(finalPrompt, state.model);
|
|
10306
|
+
} else {
|
|
10307
|
+
log(C.yel(` ⚠ ${state.provider} REPL 미지원 — leerness agents dispatch 사용 권장`));
|
|
10308
|
+
rl.prompt(); return;
|
|
10309
|
+
}
|
|
10310
|
+
const dt = Date.now() - t0;
|
|
10311
|
+
_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 });
|
|
10312
|
+
if (result.ok) {
|
|
10313
|
+
state.history.push({ role: 'assistant', content: result.response });
|
|
10314
|
+
log('');
|
|
10315
|
+
log(C.bold(`assistant (${state.model}, role=${state.role}, ${dt}ms)`));
|
|
10316
|
+
log(result.response);
|
|
10317
|
+
log('');
|
|
10318
|
+
if (state.history.length % 6 === 0) saveSession(); // 6턴마다 자동 저장
|
|
10319
|
+
} else {
|
|
10320
|
+
log(C.yel(` ⚠ 실패: ${result.error || 'unknown'}`));
|
|
10321
|
+
}
|
|
10322
|
+
rl.prompt();
|
|
10323
|
+
});
|
|
10324
|
+
rl.on('close', () => { saveSession(); resolve(); });
|
|
10325
|
+
});
|
|
10326
|
+
}
|
|
10327
|
+
|
|
9982
10328
|
async function agentCmd(root, taskArg) {
|
|
9983
10329
|
root = absRoot(root || process.cwd());
|
|
9984
10330
|
const task = (taskArg || arg('--task', '') || '').trim();
|
|
9985
|
-
|
|
9986
|
-
|
|
10331
|
+
// 1.9.149: REPL 진입 — 인자 없거나 --interactive 명시 (Hermes/OpenClaw 스타일)
|
|
10332
|
+
if (!task || has('--interactive') || has('--repl')) {
|
|
10333
|
+
if (process.stdin.isTTY && !has('--no-repl') && process.env.LEERNESS_NO_PROMPT !== '1') {
|
|
10334
|
+
const t0 = Date.now();
|
|
10335
|
+
await _agentRepl(root, {
|
|
10336
|
+
provider: arg('--provider', null),
|
|
10337
|
+
model: arg('--model', null),
|
|
10338
|
+
role: arg('--role', 'actor')
|
|
10339
|
+
});
|
|
10340
|
+
_recordRun(root, { kind: 'agent_repl_session', durationMs: Date.now() - t0, ok: true });
|
|
10341
|
+
return;
|
|
10342
|
+
}
|
|
10343
|
+
// non-TTY: 사용법만 출력
|
|
10344
|
+
log('# leerness agent (1.9.146/148/149/150) — Hermes/OpenClaw 스타일 CLI 에이전트 + Sandbox');
|
|
9987
10345
|
log('');
|
|
9988
10346
|
log('사용법:');
|
|
9989
|
-
log(' leerness agent
|
|
9990
|
-
log(' leerness agent "<task>"
|
|
9991
|
-
log(' leerness agent "<task>" --role
|
|
9992
|
-
log(' leerness agent "<task>" --role
|
|
9993
|
-
log(' leerness agent
|
|
9994
|
-
log('
|
|
10347
|
+
log(' leerness agent # 1.9.149 REPL 모드 (모델 선택 + 채팅)');
|
|
10348
|
+
log(' leerness agent "<task>" # 1회 위임 (actor 역할 기본)');
|
|
10349
|
+
log(' leerness agent "<task>" --role planner # 계획만 (1.9.148)');
|
|
10350
|
+
log(' leerness agent "<task>" --role reviewer # 비판적 검토 (1.9.148)');
|
|
10351
|
+
log(' leerness agent --interactive --model qwen2.5-coder # 명시적 REPL + model 선택');
|
|
10352
|
+
log('');
|
|
10353
|
+
log('REPL 메타 명령: :help / :model / :role / :provider / :history / :save / :quit');
|
|
10354
|
+
log('REPL Slash 명령 (1.9.150): :verify / :audit / :handoff / :health (sandboxed runCommandSafe)');
|
|
9995
10355
|
log('');
|
|
9996
10356
|
log('현재 활성 provider: ' + (_activeCliAgents().join(', ') || '(없음) — .env에서 LEERNESS_ENABLE_* 활성화'));
|
|
9997
10357
|
log('권한 모드: ' + (_readPermissions(root).mode || 'basic'));
|
|
@@ -10024,10 +10384,13 @@ async function agentCmd(root, taskArg) {
|
|
|
10024
10384
|
log('\n[ollama 호출 중...]');
|
|
10025
10385
|
// 1.9.148: role prompt 자동 prepend
|
|
10026
10386
|
const finalPrompt = `${rolePrompt}\n\nTask: ${task}`;
|
|
10387
|
+
const t0 = Date.now();
|
|
10027
10388
|
const r = await _ollamaChat(finalPrompt);
|
|
10389
|
+
const dt = Date.now() - t0;
|
|
10390
|
+
// 1.9.149: observability 기록
|
|
10391
|
+
_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 });
|
|
10028
10392
|
if (r.ok) {
|
|
10029
|
-
log('\n[response (model=' + r.model + ', role=' + role + ')]\n' + r.response);
|
|
10030
|
-
// task-log 자동 기록
|
|
10393
|
+
log('\n[response (model=' + r.model + ', role=' + role + ', ' + dt + 'ms)]\n' + r.response);
|
|
10031
10394
|
try {
|
|
10032
10395
|
const tlp = taskLogPath(root);
|
|
10033
10396
|
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`;
|
|
@@ -10346,7 +10709,8 @@ async function deployAutoCmd(root, service) {
|
|
|
10346
10709
|
log(`service: ${service} · command: ${meta.deployCommand}`);
|
|
10347
10710
|
if (has('--dry-run')) { log('(dry-run) 실제 실행 스킵'); return; }
|
|
10348
10711
|
const t0 = Date.now();
|
|
10349
|
-
|
|
10712
|
+
// 1.9.150: runCommandSafe — deploy 명령 sandbox (env scrub + permissions 검증 + observability)
|
|
10713
|
+
const r = runCommandSafe(meta.deployCommand, [], { cwd: root, root, timeout: 10 * 60 * 1000, allowShell: true, stdio: 'inherit', kind: 'deploy_auto', label: `deploy-${service}` });
|
|
10350
10714
|
const dt = Date.now() - t0;
|
|
10351
10715
|
if (r.status === 0) {
|
|
10352
10716
|
ok(`deploy 성공: ${service} (${dt}ms)`);
|
|
@@ -10876,6 +11240,9 @@ async function main() {
|
|
|
10876
11240
|
if (cmd === 'creds' && args[1] === 'check') return credsCheckCmd(arg('--path', process.cwd()), args[2]);
|
|
10877
11241
|
if (cmd === 'creds' && args[1] === 'refresh') return credsRefreshTimestampCmd(arg('--path', process.cwd()), args[2]);
|
|
10878
11242
|
if (cmd === 'deploy' && args[1] === 'auto') return deployAutoCmd(arg('--path', process.cwd()), args[2]);
|
|
11243
|
+
// 1.9.149: observability lite + runs list/show
|
|
11244
|
+
if (cmd === 'runs' && args[1] === 'list') return runsListCmd(arg('--path', process.cwd()));
|
|
11245
|
+
if (cmd === 'runs' && args[1] === 'show') return runsShowCmd(arg('--path', process.cwd()), args[2]);
|
|
10879
11246
|
// 1.9.85: leerness health — 종합 헬스 체크
|
|
10880
11247
|
if (cmd === 'health') return healthCmd(args[1] || arg('--path', process.cwd()));
|
|
10881
11248
|
if (cmd === 'whats-new') return whatsNewCmd(args[1] || arg('--path', process.cwd()));
|