leerness 1.9.148 → 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 CHANGED
@@ -1,5 +1,44 @@
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
+
3
42
  ## 1.9.148 — 2026-05-20
4
43
 
5
44
  **사용자 명시 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
- [![npm](https://img.shields.io/badge/npm-leerness-blue)](https://www.npmjs.com/package/leerness) [![version](https://img.shields.io/badge/version-1.9.148-green)]() [![tests](https://img.shields.io/badge/e2e-219%2F219-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-47-blue)]() [![json](https://img.shields.io/badge/--json-20_commands-blueviolet)]() [![rounds](https://img.shields.io/badge/autonomous--rounds-78-blueviolet)]() [![main-push](https://img.shields.io/badge/release--main--push-auto-success)]() [![multi-runtime](https://img.shields.io/badge/verify--code-node%2Fpython%2Fgo%2Frust-success)]() [![agent-roles](https://img.shields.io/badge/agent--roles-planner%2Freviewer%2Factor-success)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
5
+ [![npm](https://img.shields.io/badge/npm-leerness-blue)](https://www.npmjs.com/package/leerness) [![version](https://img.shields.io/badge/version-1.9.149-green)]() [![tests](https://img.shields.io/badge/e2e-219%2F219-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-47-blue)]() [![json](https://img.shields.io/badge/--json-20_commands-blueviolet)]() [![rounds](https://img.shields.io/badge/autonomous--rounds-79-blueviolet)]() [![main-push](https://img.shields.io/badge/release--main--push-auto-success)]() [![repl-agent](https://img.shields.io/badge/agent--REPL-Hermes%2FOpenClaw_style-success)]() [![observability](https://img.shields.io/badge/runs--jsonl-traceId%2Fduration%2Fmodel-success)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
6
6
 
7
7
  ```
8
8
  ╔══════════════════════════════════════════════════════════════╗
@@ -12,7 +12,7 @@
12
12
  ║ ██║ ██╔══╝ ██╔══╝ ██╔══██╗██║╚██╗██║██╔══╝ ╚════██║ ║
13
13
  ║ ███████╗███████╗███████╗██║ ██║██║ ╚████║███████╗███████║ ║
14
14
  ║ ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝ ║
15
- ║ v1.9.148 AI Agent Reliability Harness ║
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.148';
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 -->';
@@ -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';
@@ -9973,25 +9975,249 @@ async function _ollamaChat(prompt, model) {
9973
9975
  } catch (e) { resolve({ ok: false, error: e.message, model: mdl }); }
9974
9976
  });
9975
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
+ }
9976
10044
  // 1.9.148: planner/reviewer/actor 역할 시스템 프롬프트 (Gemini 권고 — 자기-승인 편향 방지)
9977
10045
  const _AGENT_ROLE_PROMPTS = {
9978
10046
  planner: '역할: planner. task를 step 3-6개로 분해, 각 step의 입출력/검증 방법 명시. 코드 작성 금지, 계획만.',
9979
10047
  reviewer: '역할: reviewer. planner 의 계획 또는 actor 의 결과를 비판적으로 검토. 누락된 검증, 잠재 cascade, 오류 가능성 지적. 동의/수정 결론 명시.',
9980
10048
  actor: '역할: actor. 계획에 따라 정확한 명령/코드만 실행. evidence(파일 경로 + 테스트 결과) 함께 기록. 새 계획 생성 금지.'
9981
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
+
9982
10195
  async function agentCmd(root, taskArg) {
9983
10196
  root = absRoot(root || process.cwd());
9984
10197
  const task = (taskArg || arg('--task', '') || '').trim();
9985
- if (!task) {
9986
- log('# leerness agent (1.9.146/148) 오픈소스 CLI 에이전트 모드');
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 에이전트');
9987
10212
  log('');
9988
10213
  log('사용법:');
9989
- log(' leerness agent "<task>" # 1 위임 (actor 역할 기본)');
9990
- log(' leerness agent "<task>" --role planner # 계획만, 코드 작성 없음 (1.9.148)');
9991
- log(' leerness agent "<task>" --role reviewer # 비판적 검토 (1.9.148)');
9992
- log(' leerness agent "<task>" --role actor # 계획대로 실행');
9993
- log(' leerness agent "<task>" --provider ollama # provider 선택');
9994
- log(' leerness agent "<task>" --dry-run # LLM 호출 없이 흐름만');
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');
9995
10221
  log('');
9996
10222
  log('현재 활성 provider: ' + (_activeCliAgents().join(', ') || '(없음) — .env에서 LEERNESS_ENABLE_* 활성화'));
9997
10223
  log('권한 모드: ' + (_readPermissions(root).mode || 'basic'));
@@ -10024,10 +10250,13 @@ async function agentCmd(root, taskArg) {
10024
10250
  log('\n[ollama 호출 중...]');
10025
10251
  // 1.9.148: role prompt 자동 prepend
10026
10252
  const finalPrompt = `${rolePrompt}\n\nTask: ${task}`;
10253
+ const t0 = Date.now();
10027
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 });
10028
10258
  if (r.ok) {
10029
- log('\n[response (model=' + r.model + ', role=' + role + ')]\n' + r.response);
10030
- // task-log 자동 기록
10259
+ log('\n[response (model=' + r.model + ', role=' + role + ', ' + dt + 'ms)]\n' + r.response);
10031
10260
  try {
10032
10261
  const tlp = taskLogPath(root);
10033
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`;
@@ -10876,6 +11105,9 @@ async function main() {
10876
11105
  if (cmd === 'creds' && args[1] === 'check') return credsCheckCmd(arg('--path', process.cwd()), args[2]);
10877
11106
  if (cmd === 'creds' && args[1] === 'refresh') return credsRefreshTimestampCmd(arg('--path', process.cwd()), args[2]);
10878
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]);
10879
11111
  // 1.9.85: leerness health — 종합 헬스 체크
10880
11112
  if (cmd === 'health') return healthCmd(args[1] || arg('--path', process.cwd()));
10881
11113
  if (cmd === 'whats-new') return whatsNewCmd(args[1] || arg('--path', process.cwd()));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.148",
3
+ "version": "1.9.149",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",