leerness 1.9.20 → 1.9.22

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/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.20';
9
+ const VERSION = '1.9.22';
10
10
  const MARK = '<!-- leerness:managed -->';
11
11
  const README_START = '<!-- leerness:project-readme:start -->';
12
12
  const README_END = '<!-- leerness:project-readme:end -->';
@@ -113,7 +113,7 @@ function arg(name, def = null) { const i = process.argv.indexOf(name); return i
113
113
  function has(name) { return process.argv.includes(name); }
114
114
  function nonFlagArgs() {
115
115
  const out = [];
116
- 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']);
116
+ 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']);
117
117
  const a = process.argv.slice(2);
118
118
  for (let i = 0; i < a.length; i++) {
119
119
  const x = a[i];
@@ -435,7 +435,11 @@ async function install(root, opts = {}) {
435
435
  ]);
436
436
  mergeLinesFile(path.join(root, '.env.example'), [
437
437
  '# Leerness uses environment variable names only. Do not store real secrets here.',
438
- 'LEERNESS_NPM_TOKEN=','LEERNESS_GITHUB_TOKEN='
438
+ 'LEERNESS_NPM_TOKEN=','LEERNESS_GITHUB_TOKEN=',
439
+ '# 1.9.22 — orchestrate opt-in. URL이 설정되면 leerness가 Ollama를 사용 가능. 미설정 시 LLM 호출 자동 시작 금지.',
440
+ 'LEERNESS_OLLAMA_BASE_URL=',
441
+ '# 선택. 기본 모델 (orchestrate --model 로 override 가능).',
442
+ 'LEERNESS_OLLAMA_MODEL='
439
443
  ]);
440
444
  mergeLinesFile(path.join(root, '.gitattributes'), [
441
445
  '* text=auto eol=lf','*.bat text eol=crlf','*.ps1 text eol=crlf'
@@ -1258,7 +1262,27 @@ function _handoffWorkspace(rootBase) {
1258
1262
  } }, null, 2));
1259
1263
  return;
1260
1264
  }
1261
- log(`# Workspace Handoff${paths.length}개 프로젝트 (1.9.18)`);
1265
+ // 1.9.22: --compact 모드 LLM 시스템 프롬프트 최적화용 1줄 요약 (~500 chars)
1266
+ if (has('--compact')) {
1267
+ let totalDone = 0, totalTasks = 0, totalWIP = 0, totalRecent = 0;
1268
+ const projSummaries = [];
1269
+ for (const p of paths) {
1270
+ const rows = readProgressRows(p);
1271
+ const buckets = {};
1272
+ for (const r of rows) (buckets[r.status] || (buckets[r.status] = [])).push(r);
1273
+ const done = (buckets['done'] || []).length;
1274
+ const wip = (buckets['in-progress'] || []).length;
1275
+ const recent = sinceCutoff ? rows.filter(isRecent).length : 0;
1276
+ totalDone += done; totalTasks += rows.length; totalWIP += wip; totalRecent += recent;
1277
+ const pct = rows.length ? Math.round(done / rows.length * 100) : 0;
1278
+ projSummaries.push(`${path.basename(p)} ${done}/${rows.length}(${pct}%)`);
1279
+ }
1280
+ log(`leerness compact (1.9.22): ${paths.length}프로젝트 · ${totalDone}/${totalTasks}(${totalTasks?Math.round(totalDone/totalTasks*100):0}%) done · WIP ${totalWIP}${sinceCutoff?` · 🆕${totalRecent}`:''}`);
1281
+ log(`projects: ${projSummaries.join(' | ')}`);
1282
+ log(`핵심 규칙: 의존성0 · 한국어주석 · UTF-8noBOM · reuse-map등록 · anti-lazy-work · verify-claim자동검수`);
1283
+ return;
1284
+ }
1285
+ log(`# Workspace Handoff — ${paths.length}개 프로젝트 (1.9.22)`);
1262
1286
  log(`Date: ${today()}`);
1263
1287
  if (sinceCutoff) log(`Filter: since ${sinceArg} (${sinceCutoff} 이후 수정된 항목 🆕 강조)`);
1264
1288
  log('');
@@ -1496,7 +1520,8 @@ function verifyClaimCmd(root, taskId) {
1496
1520
  // (1.9.19까지: src|bin|tests|public|lib 하드코딩 → Godot scenes/scripts 미검출)
1497
1521
  // 변경: 확장자 화이트리스트 기반. 디렉토리는 선택적 (project.godot 같은 루트 파일도 잡음).
1498
1522
  // 확장자는 길이 내림차순(긴 것 먼저 매치) + \b 종결로 .ts vs .tscn 구분.
1499
- const FILE_EXTS = 'webmanifest|dockerfile|tscn|tres|godot|json5|jsx|tsx|yaml|html|scss|sass|less|gltf|json|toml|mdx|xml|css|svg|yml|md|js|ts|gd|cs|py|rb|go|rs|kt|sh|h';
1523
+ // 1.9.21: 설정/메타 파일 확장자 추가 — Godot export_presets.cfg 등 false negative 보완
1524
+ const FILE_EXTS = 'webmanifest|dockerfile|properties|tscn|tres|godot|json5|jsx|tsx|yaml|html|scss|sass|less|gltf|conf|json|toml|lock|mdx|xml|css|svg|yml|cfg|ini|env|md|js|ts|gd|cs|py|rb|go|rs|kt|sh|h';
1500
1525
  const FILE_RE = new RegExp(`(?:[A-Za-z][A-Za-z0-9_-]*\\/)?[A-Za-z][\\w./-]*\\.(?:${FILE_EXTS})\\b`, 'g');
1501
1526
  const filePatterns = evidence.match(FILE_RE) || [];
1502
1527
  // 중복 제거 + "tests/test.js" 같은 결과를 유지 (이미 `..` 없으니 그대로)
@@ -1668,6 +1693,193 @@ function verifyClaimCmd(root, taskId) {
1668
1693
  log(` ✓ evidence 주장이 실제 파일·테스트${runResult && !runResult.skipped ? '·실행 결과' : ''}와 일치`);
1669
1694
  }
1670
1695
 
1696
+ // 1.9.22: orchestrate — Ollama 로컬 LLM으로 best-of-N 멀티 에이전트 시뮬
1697
+ // 정책 (사용자 명시 1.9.22):
1698
+ // 1) 자동 적용 금지. LEERNESS_OLLAMA_BASE_URL 환경변수 감지 opt-in 전용
1699
+ // 2) .env 파일 자동 로드 (간단 파서)
1700
+ // 3) --agents N 가변 (1~256)
1701
+ // 4) 환경변수 없으면 명령 거부 + 안내
1702
+ function _loadEnvFile(root) {
1703
+ // root 경로(또는 cwd)의 .env 파일을 간단 파싱해 process.env에 머지 (이미 있는 키는 덮어쓰지 않음)
1704
+ const envFile = path.join(root || process.cwd(), '.env');
1705
+ if (!exists(envFile)) return false;
1706
+ try {
1707
+ const txt = read(envFile);
1708
+ for (const line of txt.split(/\r?\n/)) {
1709
+ const m = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*?)\s*$/);
1710
+ if (!m) continue;
1711
+ const key = m[1];
1712
+ let val = m[2];
1713
+ // 주석 제거
1714
+ if (val.startsWith('#')) continue;
1715
+ // 따옴표 제거
1716
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) val = val.slice(1, -1);
1717
+ if (!process.env[key]) process.env[key] = val;
1718
+ }
1719
+ return true;
1720
+ } catch { return false; }
1721
+ }
1722
+
1723
+ function _httpPostJson(urlStr, body, timeoutMs = 300000) {
1724
+ return new Promise((resolve, reject) => {
1725
+ let u;
1726
+ try { u = new URL(urlStr); } catch (e) { return reject(e); }
1727
+ const mod = u.protocol === 'https:' ? require('https') : require('http');
1728
+ const data = JSON.stringify(body);
1729
+ const req = mod.request({
1730
+ hostname: u.hostname, port: u.port || (u.protocol === 'https:' ? 443 : 80), path: u.pathname + (u.search || ''),
1731
+ method: 'POST',
1732
+ headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(data) },
1733
+ timeout: timeoutMs
1734
+ }, (res) => {
1735
+ const chunks = [];
1736
+ res.on('data', (c) => chunks.push(c));
1737
+ res.on('end', () => {
1738
+ const raw = Buffer.concat(chunks).toString('utf8');
1739
+ try { resolve({ status: res.statusCode, body: JSON.parse(raw), raw }); }
1740
+ catch (e) { resolve({ status: res.statusCode, body: null, raw }); }
1741
+ });
1742
+ });
1743
+ req.on('error', reject);
1744
+ req.on('timeout', () => { req.destroy(new Error('timeout')); });
1745
+ req.write(data); req.end();
1746
+ });
1747
+ }
1748
+
1749
+ async function _ollamaChat({ baseUrl, model, system, user, timeoutMs = 300000, format }) {
1750
+ const t0 = Date.now();
1751
+ const url = baseUrl.replace(/\/$/, '') + '/api/chat';
1752
+ const body = {
1753
+ model,
1754
+ messages: [
1755
+ ...(system ? [{ role: 'system', content: system }] : []),
1756
+ { role: 'user', content: user }
1757
+ ],
1758
+ stream: false,
1759
+ options: { temperature: 0.3, num_predict: 4000 }
1760
+ };
1761
+ if (format) body.format = format;
1762
+ let res;
1763
+ try { res = await _httpPostJson(url, body, timeoutMs); }
1764
+ catch (e) { return { ok: false, error: e.message, elapsed: Date.now() - t0 }; }
1765
+ if (res.status !== 200) return { ok: false, error: `HTTP ${res.status}: ${(res.raw || '').slice(0, 200)}`, elapsed: Date.now() - t0 };
1766
+ return {
1767
+ ok: true, elapsed: Date.now() - t0,
1768
+ reply: res.body?.message?.content || '',
1769
+ promptTokens: res.body?.prompt_eval_count || 0,
1770
+ responseTokens: res.body?.eval_count || 0
1771
+ };
1772
+ }
1773
+
1774
+ async function orchestrateCmd(root, goalParts) {
1775
+ root = absRoot(root || process.cwd());
1776
+ const goal = (goalParts || []).join(' ').trim();
1777
+ // .env 자동 로드 (process.env에 없는 키만 채움)
1778
+ _loadEnvFile(root);
1779
+ _loadEnvFile(path.join(root, '..')); // 상위도 시도 (워크스페이스 루트)
1780
+
1781
+ const baseUrl = process.env.LEERNESS_OLLAMA_BASE_URL || '';
1782
+ if (!baseUrl) {
1783
+ fail('LEERNESS_OLLAMA_BASE_URL 미설정 — orchestrate는 opt-in입니다.');
1784
+ log('');
1785
+ log('## 활성화 방법');
1786
+ log(' 1) .env 파일에 추가:');
1787
+ log(' LEERNESS_OLLAMA_BASE_URL=http://192.168.68.89:11434');
1788
+ log(' 2) 또는 환경변수로:');
1789
+ log(' $env:LEERNESS_OLLAMA_BASE_URL="http://localhost:11434" (PowerShell)');
1790
+ log(' export LEERNESS_OLLAMA_BASE_URL=http://localhost:11434 (bash)');
1791
+ log(' 3) 다시 실행: leerness orchestrate "<목표>" --agents N');
1792
+ log('');
1793
+ log('정책 (1.9.22): 환경변수 없으면 LLM 호출 자동 시작 금지. 사용자 동의 후 활성화.');
1794
+ return process.exit(1);
1795
+ }
1796
+ if (!goal) {
1797
+ fail('orchestrate "<목표>" 필요. 예: leerness orchestrate "JSON validator 작성" --agents 4');
1798
+ return process.exit(1);
1799
+ }
1800
+
1801
+ const agentCount = Math.max(1, Math.min(256, parseInt(arg('--agents', '4'), 10)));
1802
+ const model = arg('--model', process.env.LEERNESS_OLLAMA_MODEL || 'qwen2.5:7b-instruct');
1803
+ const timeoutMs = parseInt(arg('--timeout', '300000'), 10);
1804
+ const retryOnFail = parseInt(arg('--retry-on-fail', '0'), 10); // 1.9.22 후보 2 통합
1805
+
1806
+ log(`# leerness orchestrate (1.9.22)`);
1807
+ log(`Opt-in 활성화: Ollama URL = ${baseUrl}`);
1808
+ log(`목표: ${goal}`);
1809
+ log(`에이전트 수: ${agentCount} · 모델: ${model}${retryOnFail ? ` · auto-fix retry: ${retryOnFail}회` : ''}`);
1810
+ log('');
1811
+
1812
+ // 시스템 프롬프트: compact handoff 자동 포함 (LLM 컨텍스트 절약)
1813
+ const compactCtx = `당신은 leerness 1.9.22 워크스페이스의 sub-agent입니다.\n핵심 규칙: 의존성0 · 한국어주석 · UTF-8noBOM · 검증가능한 산출물.\nJSON 형식으로만 응답하세요: {"files":[{"path":"src/x.js","content":"..."}], "summary": "..."}`;
1814
+
1815
+ // N개 동시 호출 (best-of-N 패턴)
1816
+ log(`## ${agentCount}개 에이전트 동시 호출 중...`);
1817
+ const tasks = [];
1818
+ for (let i = 0; i < agentCount; i++) {
1819
+ tasks.push((async () => {
1820
+ const t0 = Date.now();
1821
+ const r = await _ollamaChat({ baseUrl, model, system: compactCtx, user: goal, timeoutMs, format: 'json' });
1822
+ return { agent: i + 1, ...r, totalElapsed: Date.now() - t0 };
1823
+ })());
1824
+ }
1825
+ const results = await Promise.all(tasks);
1826
+
1827
+ // 결과 요약
1828
+ const ok = results.filter(r => r.ok);
1829
+ const failures = results.filter(r => !r.ok);
1830
+ log(`\n## 결과`);
1831
+ log(` 성공: ${ok.length}/${agentCount}`);
1832
+ log(` 실패: ${failures.length}`);
1833
+ if (failures.length) {
1834
+ for (const f of failures.slice(0, 3)) log(` · agent ${f.agent}: ${f.error}`);
1835
+ }
1836
+
1837
+ if (ok.length) {
1838
+ const totalPromptTokens = ok.reduce((a, b) => a + b.promptTokens, 0);
1839
+ const totalRespTokens = ok.reduce((a, b) => a + b.responseTokens, 0);
1840
+ const avgElapsed = ok.reduce((a, b) => a + b.elapsed, 0) / ok.length;
1841
+ const totalElapsedWallClock = Math.max(...results.map(r => r.totalElapsed));
1842
+ log('');
1843
+ log(`## 토큰`);
1844
+ log(` prompt 합계: ${totalPromptTokens} · response 합계: ${totalRespTokens}`);
1845
+ log(` 평균 latency: ${avgElapsed.toFixed(0)}ms · wall-clock 총: ${totalElapsedWallClock}ms (병렬 효과 ${(avgElapsed * ok.length / totalElapsedWallClock).toFixed(1)}x)`);
1846
+
1847
+ log('');
1848
+ log(`## 최고 응답 (longest by response token count, 임시 휴리스틱)`);
1849
+ const best = ok.reduce((a, b) => (b.responseTokens > a.responseTokens ? b : a));
1850
+ log(` agent ${best.agent} · ${best.responseTokens} 응답 토큰 · ${best.elapsed}ms`);
1851
+ log(` --- 처음 600자 ---`);
1852
+ log(best.reply.slice(0, 600));
1853
+ }
1854
+
1855
+ // .harness/orchestrate-log.md 누적 (1.9.22 후보 4)
1856
+ const logFile = path.join(root, '.harness', 'orchestrate-log.md');
1857
+ if (!exists(path.dirname(logFile))) fs.mkdirSync(path.dirname(logFile), { recursive: true });
1858
+ const entry = `\n## ${now()}\nmodel=${model} agents=${agentCount} success=${ok.length}/${agentCount} goal=${goal.slice(0, 100)}\n`;
1859
+ append(logFile, exists(logFile) ? entry : `# Orchestrate Log\n${entry}`);
1860
+ log('');
1861
+ log(`📜 누적 기록: .harness/orchestrate-log.md`);
1862
+ }
1863
+
1864
+ // 1.9.22 후보 4: llm-bench record + retro 통합
1865
+ function llmBenchRecordCmd(root) {
1866
+ root = absRoot(root || process.cwd());
1867
+ const label = arg('--label', 'manual');
1868
+ const score = arg('--score', null);
1869
+ const tokens = arg('--tokens', null);
1870
+ const model = arg('--model', 'unknown');
1871
+ if (!score) { fail('--score 필요'); return process.exit(1); }
1872
+ const histFile = path.join(root, '.harness', 'llm-bench-history.md');
1873
+ if (!exists(path.dirname(histFile))) fs.mkdirSync(path.dirname(histFile), { recursive: true });
1874
+ const row = `| ${today()} | ${model} | ${label} | ${score} | ${tokens || '?'} |\n`;
1875
+ if (!exists(histFile)) {
1876
+ writeUtf8(histFile, `# LLM Bench History\n\n| Date | Model | Label | Score | Tokens |\n|---|---|---|---:|---:|\n${row}`);
1877
+ } else {
1878
+ append(histFile, row);
1879
+ }
1880
+ ok(`기록됨: ${histFile}`);
1881
+ }
1882
+
1671
1883
  function sessionClose(root) {
1672
1884
  root = absRoot(root);
1673
1885
  const rows = readProgressRows(root);
@@ -3665,7 +3877,7 @@ function viewworkInstall(root) {
3665
3877
  }
3666
3878
 
3667
3879
  function help() {
3668
- log(`Leerness v${VERSION}\n\nUsage:\n leerness init [path] [--language auto|ko|en] [--skills recommended|all|a,b]\n leerness migrate [path] [--dry-run] [--force]\n leerness update [path] [--check|--yes|--force|--from <tarball>]\n leerness auto-update install [path]\n leerness status [path]\n leerness verify [path]\n leerness debug [path]\n leerness audit [path]\n leerness check [path]\n leerness scan secrets [path]\n leerness encoding check [path]\n leerness lazy detect [path]\n leerness memory search "query" [--limit 5]\n leerness handoff [path] [--all-apps] [--include p1,p2] [--since 24h|3d] [--json] # 1.9.17/18 워크스페이스\n leerness reuse-map [path] [--all-apps] [--include p1,p2] [--strict-elements] [--json] # 1.9.18 중복/잠재중복/depends-on\n leerness verify-claim <T-ID> [--path .] [--run-tests] [--json] # 1.9.18-20 evidence 자동 검증 (1.9.20: scenes/scripts 등 도메인 폴더 + jest/mocha 파싱)\n leerness verify-code [path] [--build] [--bench] # 1.9.20 --bench: scripts.bench 추가 실행 + evidence 누적\n leerness session close [path]\n leerness viewwork install [path]\n leerness viewwork emit [path] [--action a] [--note n] [--agent x] [--tool t]\n leerness route <task-type>\n leerness self check [path]\n leerness readme sync [path]\n leerness consistency check [path]\n leerness consistency merge-design-guide [path]\n leerness plan show|init|add|drop|progress|sync [args]\n leerness task list|add|update|drop|fix-evidence|relink [args]\n leerness skill list|info <name>\n leerness skill learn <id> --doc <url> --command "..." --capability "..." [--note ...]\n leerness skill use <id> [--note ...]\n leerness skill optimize <id> --before "..." --after "..." [--note ...]\n leerness skill remove <id>\n leerness skill consolidate [--threshold 0.3]\n leerness gate [path] # verify+audit+scan+encoding+lazy
3880
+ log(`Leerness v${VERSION}\n\nUsage:\n leerness init [path] [--language auto|ko|en] [--skills recommended|all|a,b]\n leerness migrate [path] [--dry-run] [--force]\n leerness update [path] [--check|--yes|--force|--from <tarball>]\n leerness auto-update install [path]\n leerness status [path]\n leerness verify [path]\n leerness debug [path]\n leerness audit [path]\n leerness check [path]\n leerness scan secrets [path]\n leerness encoding check [path]\n leerness lazy detect [path]\n leerness memory search "query" [--limit 5]\n leerness handoff [path] [--all-apps] [--include p1,p2] [--since 24h|3d] [--compact] [--json] # 1.9.17-22 워크스페이스 (--compact: LLM 시스템 프롬프트용 1줄 요약)\n leerness orchestrate "<목표>" [--agents N] [--model qwen2.5:7b-instruct] [--retry-on-fail K] # 1.9.22 Ollama opt-in (LEERNESS_OLLAMA_BASE_URL 필요)\n leerness llm-bench record --score N --model X [--label L] [--tokens T] # 1.9.22 LLM 벤치 히스토리 누적\n leerness reuse-map [path] [--all-apps] [--include p1,p2] [--strict-elements] [--json] # 1.9.18 중복/잠재중복/depends-on\n leerness verify-claim <T-ID> [--path .] [--run-tests] [--json] # 1.9.18-20 evidence 자동 검증 (1.9.20: scenes/scripts 등 도메인 폴더 + jest/mocha 파싱)\n leerness verify-code [path] [--build] [--bench] # 1.9.20 --bench: scripts.bench 추가 실행 + evidence 누적\n leerness session close [path]\n leerness viewwork install [path]\n leerness viewwork emit [path] [--action a] [--note n] [--agent x] [--tool t]\n leerness route <task-type>\n leerness self check [path]\n leerness readme sync [path]\n leerness consistency check [path]\n leerness consistency merge-design-guide [path]\n leerness plan show|init|add|drop|progress|sync [args]\n leerness task list|add|update|drop|fix-evidence|relink [args]\n leerness skill list|info <name>\n leerness skill learn <id> --doc <url> --command "..." --capability "..." [--note ...]\n leerness skill use <id> [--note ...]\n leerness skill optimize <id> --before "..." --after "..." [--note ...]\n leerness skill remove <id>\n leerness skill consolidate [--threshold 0.3]\n leerness gate [path] # verify+audit+scan+encoding+lazy
3669
3881
  leerness retro [path] [--days 7] [--all-apps] [--include p1,p2] [--json] # 회고 (1.9.13~1.9.16)
3670
3882
  leerness insights [path] [--all-apps] [--include p1,p2] [--json] # 누적 통계 (1.9.13~1.9.16)
3671
3883
  leerness brainstorm "<주제>" [--all-apps] [--include p1,p2] [--json] # 브레인스토밍 (1.9.13~1.9.16)
@@ -3701,6 +3913,8 @@ async function main() {
3701
3913
  if (cmd === 'handoff') return handoffCmd(args[1] || process.cwd());
3702
3914
  if (cmd === 'reuse-map') return reuseMapCmd(args[1] || process.cwd());
3703
3915
  if (cmd === 'verify-claim') return verifyClaimCmd(arg('--path', process.cwd()), args[1]);
3916
+ if (cmd === 'orchestrate') return await orchestrateCmd(arg('--path', process.cwd()), args.slice(1).filter(x => !x.startsWith('-')));
3917
+ if (cmd === 'llm-bench' && args[1] === 'record') return llmBenchRecordCmd(arg('--path', process.cwd()));
3704
3918
  if (cmd === 'session' && args[1] === 'close') { const r = sessionClose(args[2] || process.cwd()); viewworkEmit(args[2] || process.cwd(), { action: 'task', tool: 'session-close', note: 'session close' }); return r; }
3705
3919
  if (cmd === 'viewwork' && args[1] === 'install') return viewworkInstall(args[2] || process.cwd());
3706
3920
  if (cmd === 'viewwork' && args[1] === 'emit') return viewworkEmit(args[2] || process.cwd(), { action: arg('--action','task'), note: arg('--note',''), agent: arg('--agent','leerness'), tool: arg('--tool','leerness-cli') });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.20",
3
+ "version": "1.9.22",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -321,7 +321,7 @@ total++;
321
321
  cp.spawnSync(process.execPath, [CLI, 'init', tmpA, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
322
322
  cp.spawnSync(process.execPath, [CLI, 'init', tmpB, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
323
323
  const r = cp.spawnSync(process.execPath, [CLI, 'handoff', '--include', `${tmpA},${tmpB}`], { encoding: 'utf8', timeout: 15000, cwd: os.tmpdir() });
324
- const ok = r.status === 0 && /Workspace Handoff — 2개 프로젝트 \(1\.9\.1[78]\)/.test(r.stdout) && /워크스페이스 총합/.test(r.stdout) && /오케스트레이션 권장/.test(r.stdout);
324
+ const ok = r.status === 0 && /Workspace Handoff — 2개 프로젝트 \(1\.9\.\d+\)/.test(r.stdout) && /워크스페이스 총합/.test(r.stdout) && /오케스트레이션 권장/.test(r.stdout);
325
325
  console.log(ok ? '✓ B(1.9.17) handoff --include 통합 워크스페이스 뷰' : '✗ handoff --include 실패');
326
326
  if (!ok) { failed++; console.log(r.stdout.slice(0, 600)); }
327
327
  }
@@ -381,7 +381,7 @@ total++;
381
381
  const today = new Date().toISOString().slice(0,10);
382
382
  fs.appendFileSync(path.join(tmpA, '.harness/progress-tracker.md'), `| T-9999 | done | 신규 기능 | src/x.js | M-NEW | ${today} |\n`);
383
383
  const r = cp.spawnSync(process.execPath, [CLI, 'handoff', '--include', `${tmpA},${tmpB}`, '--since', '1d'], { encoding: 'utf8', timeout: 15000, cwd: os.tmpdir() });
384
- const ok = r.status === 0 && /1\.9\.18/.test(r.stdout) && /Filter: since 1d/.test(r.stdout) && /🆕/.test(r.stdout) && /최근 변경/.test(r.stdout);
384
+ const ok = r.status === 0 && /1\.9\.\d+/.test(r.stdout) && /Filter: since 1d/.test(r.stdout) && /🆕/.test(r.stdout) && /최근 변경/.test(r.stdout);
385
385
  console.log(ok ? '✓ B(1.9.18) handoff --since: 최근 변경 강조' : '✗ handoff --since 실패');
386
386
  if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
387
387
  }
@@ -566,6 +566,85 @@ total++;
566
566
  if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
567
567
  }
568
568
 
569
+ // 1.9.21 회귀: 설정 파일 확장자 (.cfg/.ini/.env/.toml/.lock) 추가
570
+ total++;
571
+ {
572
+ const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-cfg-'));
573
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
574
+ fs.writeFileSync(path.join(tmpC, 'export_presets.cfg'), '[preset.0]\n');
575
+ fs.writeFileSync(path.join(tmpC, 'config.ini'), '[main]\n');
576
+ fs.writeFileSync(path.join(tmpC, 'Cargo.lock'), '# lock\n');
577
+ fs.appendFileSync(path.join(tmpC, '.harness/progress-tracker.md'),
578
+ '| T-0030 | done | 설정 | export_presets.cfg + config.ini + Cargo.lock | next | 2026-05-14 |\n');
579
+ const r = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0030', '--path', tmpC], { encoding: 'utf8', timeout: 15000 });
580
+ const ok = r.status === 0
581
+ && /✓ export_presets\.cfg/.test(r.stdout)
582
+ && /✓ config\.ini/.test(r.stdout)
583
+ && /✓ Cargo\.lock/.test(r.stdout);
584
+ console.log(ok ? '✓ B(1.9.21) verify-claim regex: .cfg/.ini/.lock 등 설정 메타 파일' : '✗ .cfg/.ini 확장 실패');
585
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
586
+ }
587
+
588
+ // 1.9.22 회귀: handoff --compact + orchestrate opt-in 정책 + llm-bench record
589
+ total++;
590
+ {
591
+ // handoff --compact: 1줄 요약 출력
592
+ const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-compact-'));
593
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
594
+ const r = cp.spawnSync(process.execPath, [CLI, 'handoff', '--include', tmpC, '--compact'], { encoding: 'utf8', timeout: 15000, cwd: os.tmpdir() });
595
+ const ok = r.status === 0
596
+ && /leerness compact \(1\.9\.22\):/.test(r.stdout)
597
+ && /핵심 규칙: 의존성0/.test(r.stdout)
598
+ && r.stdout.length < 2000; // compact 모드는 짧아야 함
599
+ console.log(ok ? '✓ B(1.9.22) handoff --compact: LLM 프롬프트용 1줄 요약' : '✗ --compact 실패');
600
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
601
+ }
602
+
603
+ total++;
604
+ {
605
+ // orchestrate: LEERNESS_OLLAMA_BASE_URL 없으면 거부 (자동 적용 금지 정책)
606
+ const tmpO = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-orch-'));
607
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpO, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
608
+ // 환경변수 명시 제거
609
+ const env = { ...process.env };
610
+ delete env.LEERNESS_OLLAMA_BASE_URL;
611
+ const r = cp.spawnSync(process.execPath, [CLI, 'orchestrate', 'test goal', '--path', tmpO, '--agents', '3'], { encoding: 'utf8', timeout: 15000, env });
612
+ const ok = r.status !== 0
613
+ && /LEERNESS_OLLAMA_BASE_URL 미설정/.test(r.stdout)
614
+ && /opt-in/.test(r.stdout)
615
+ && /환경변수 없으면 LLM 호출 자동 시작 금지/.test(r.stdout);
616
+ console.log(ok ? '✓ B(1.9.22) orchestrate opt-in 정책: env 없으면 거부 + 안내' : '✗ orchestrate opt-in 정책 실패');
617
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
618
+ }
619
+
620
+ total++;
621
+ {
622
+ // orchestrate: .env 파일 자동 로드 (단, fake URL이라 실제 호출은 실패)
623
+ const tmpE = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-orch-env-'));
624
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpE, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
625
+ fs.writeFileSync(path.join(tmpE, '.env'), 'LEERNESS_OLLAMA_BASE_URL=http://127.0.0.1:1\n');
626
+ const env = { ...process.env };
627
+ delete env.LEERNESS_OLLAMA_BASE_URL;
628
+ const r = cp.spawnSync(process.execPath, [CLI, 'orchestrate', 'test', '--path', tmpE, '--agents', '1', '--timeout', '2000'], { encoding: 'utf8', timeout: 30000, env });
629
+ // .env에서 URL 감지됐다는 메시지가 stdout에 나와야 함 (실제 호출은 실패하지만 opt-in은 됨)
630
+ const ok = /Opt-in 활성화: Ollama URL = http:\/\/127\.0\.0\.1:1/.test(r.stdout);
631
+ console.log(ok ? '✓ B(1.9.22) orchestrate: .env 자동 로드 (LEERNESS_OLLAMA_BASE_URL 감지)' : '✗ .env auto-load 실패');
632
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
633
+ }
634
+
635
+ total++;
636
+ {
637
+ // llm-bench record: 히스토리 누적
638
+ const tmpL = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-llmb-'));
639
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpL, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
640
+ const r = cp.spawnSync(process.execPath, [CLI, 'llm-bench', 'record', '--path', tmpL, '--score', '7.5', '--model', 'llama3.1:8b', '--label', 'A_leerness', '--tokens', '1754'], { encoding: 'utf8', timeout: 10000 });
641
+ const ok = r.status === 0
642
+ && fs.existsSync(path.join(tmpL, '.harness', 'llm-bench-history.md'))
643
+ && fs.readFileSync(path.join(tmpL, '.harness', 'llm-bench-history.md'), 'utf8').includes('llama3.1:8b');
644
+ console.log(ok ? '✓ B(1.9.22) llm-bench record: 히스토리 누적 저장' : '✗ llm-bench record 실패');
645
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
646
+ }
647
+
569
648
  total++;
570
649
  {
571
650
  // jest 출력 파싱