leerness 1.9.238 β†’ 1.9.240

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,81 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.240 β€” 2026-05-24
4
+
5
+ **🐍 UR-0013 2단계: handoff/session close/health --json pyFiles (9 ν•„λ“œ) + ν—€λ“œλΌμΈ μžλ™.**
6
+
7
+ ### 1. handoff/session close/health --json 9번째 톡합 ν•„λ“œ `pyFiles`
8
+ - Python 파일 μžλ™ detect (1.9.239 `_collectPyFiles` μž¬μ‚¬μš©)
9
+ - ν˜•μ‹: `{ total, analyzed, totalLOC, totalImports, totalFuncs, totalClasses }`
10
+ - 3 λͺ…λ Ή 일관성 맀트릭슀 μ§„ν™”: 7 β†’ 8 β†’ **9 ν•„λ“œ** (3 Γ— 9 = 27 톡합 포인트)
11
+ - AI κ°€ 닀쀑 μ–Έμ–΄ μ›Œν¬μŠ€νŽ˜μ΄μŠ€ (.md + .py) 인지
12
+
13
+ ### 2. handoff ν—€λ“œλΌμΈ 18번째 μš”μ†Œ
14
+ - `.py` 파일 β‰₯ 1 μ‹œ: `🐍 py N` μžλ™ λ…ΈμΆœ
15
+ - 0개일 λ•Œ λ―Έλ…ΈμΆœ (μ‘°μš©ν•¨)
16
+
17
+ ### 3. UR-0013 2단계 μ™„λ£Œ
18
+ - 1.9.239 1단계: py-check CLI + agent-mode CLI + MCP 67/68
19
+ - **1.9.240 2단계**: JSON 9 ν•„λ“œ + ν—€λ“œλΌμΈ μžλ™
20
+ - 닀쀑 μ–Έμ–΄ ν‘œλ©΄ (.md + .py) μžλ™ κ°€μ‹œν™”
21
+
22
+ ### 4. λˆ„μ  νšŒκ·€ (1.9.207~239) β€” λͺ¨λ‘ μœ μ§€
23
+
24
+ ### 5. stress-v185 β€” 16/16 PASS
25
+ - 1.9.240 (7): VERSION + handoff/session close/health JSON + 9 ν•„λ“œ + ν—€λ“œλΌμΈ (있음/μ—†μŒ)
26
+ - μ„±λŠ₯ (1): cold_start avg 386ms
27
+ - λˆ„μ  νšŒκ·€ (8): 1.9.207~239
28
+
29
+ ### 6. μžλ™ release (102 λΌμš΄λ“œ main-push streak Β· 63 λΌμš΄λ“œ npm publish streak)
30
+
31
+ 🐍 **닀쀑 μ–Έμ–΄ ν‘œλ©΄ κ°€μ‹œν™”** β€” .md + .py μžλ™ 톡합 (μ™ΈλΆ€ AI κ°€ 두 ν‘œλ©΄ λͺ¨λ‘ 회수)
32
+
33
+ ---
34
+
35
+ ## 1.9.239 β€” 2026-05-24
36
+
37
+ **🐍 leerness py-check + πŸ€– agent-mode + MCP 67/68 (μ‚¬μš©μž λͺ…μ‹œ UR-0013).**
38
+
39
+ ### μ‚¬μš©μž λͺ…μ‹œ (UR-0013)
40
+ > *"leernessκ°€ md뿐만 μ•„λ‹ˆλΌ pyλ₯Ό ν¬ν•¨ν•œ 파이썬 슀크립트 등이 νš¨μœ¨μ μ΄λΌκ±°λ‚˜, μ—μ΄μ „νŠΈ λͺ¨λ“œλ₯Ό μœ„ν•œ μž₯치 λ“± 더 효율적으둜 μž‘λ™ν•  수 μžˆλ„λ‘ κ΅¬ν˜„"*
41
+
42
+ ### 1. `leerness py-check` μƒˆ λͺ…λ Ή β€” Python 파일 뢄석
43
+ - μ˜μ‘΄μ„± 0, regex fallback (Node λ‚΄μž₯만)
44
+ - 뢄석: 파일 수 / LOC / imports / functions / classes / TODO/FIXME
45
+ - Top 5 파일 (LOC κΈ°μ€€)
46
+ - `--json` μ˜΅μ…˜ (μ™ΈλΆ€ μžλ™ν™”)
47
+ - `.harness` / `node_modules` / `venv` / `__pycache__` λ“± μžλ™ skip
48
+ - leerness κ°€ .md 외에도 **.py 인지** β€” 닀쀑 μ–Έμ–΄ μ›Œν¬μŠ€νŽ˜μ΄μŠ€ 지원
49
+
50
+ ### 2. `leerness agent-mode <start|tick|stop>` β€” 자율 λͺ¨λ“œ μ „μš© 톡합
51
+ - **start**: handoff + drift --auto-fix + session-resume --auto-fix (μ§„μž… μžλ™ν™”)
52
+ - **tick**: pulse ν•œ 쀄 (λ§€ λΌμš΄λ“œ 가벼움)
53
+ - **stop**: session close --auto-apply-delivered --auto-cleanup-branches (마감 톡합)
54
+ - AI 자율 λΌμš΄λ“œ μ§„μž…/λ§€ λΌμš΄λ“œ/λ§ˆκ°μ„ **1 λͺ…λ ΉμœΌλ‘œ μ••μΆ•** β†’ 호좜 λΉ„μš© 절감
55
+
56
+ ### 3. MCP 67/68번째 도ꡬ
57
+ - `leerness_py_check` β€” μ™ΈλΆ€ AI κ°€ Python ν‘œλ©΄ 뢄석
58
+ - `leerness_agent_mode` β€” μ™ΈλΆ€ AI κ°€ 자율 λͺ¨λ“œ 단일 호좜
59
+ - MCP 66 β†’ 68 (+2)
60
+
61
+ ### 4. 검증 β€” μ‹€ Python 파일 (격리)
62
+ - 2 파일 β†’ 22 LOC / 4 imports / 4 funcs / 2 classes / 2 TODOs (μ •ν™• 회수)
63
+ - 200 file limit μ•ˆμ „ κ°€λ“œ + heavy dir skip
64
+
65
+ ### 5. λˆ„μ  νšŒκ·€ (1.9.207~238) β€” λͺ¨λ‘ μœ μ§€
66
+
67
+ ### 6. stress-v184 β€” 18/18 PASS
68
+ - 1.9.239 (8): VERSION + py-check (격리/human/fallback) + agent-mode (help/tick) + MCP 68 + μ‹€ 호좜
69
+ - μ„±λŠ₯ (2): cold_start avg 372ms / py-check 387ms
70
+ - λˆ„μ  νšŒκ·€ (8): 1.9.207~238
71
+
72
+ ### 7. μžλ™ release (101 λΌμš΄λ“œ main-push streak Β· 62 λΌμš΄λ“œ npm publish streak)
73
+
74
+ 🐍 **leerness 닀쀑 μ–Έμ–΄ 지원 μ‹ μ„€** β€” .md β†’ .md + .py (ν™•μž₯ κ°€λŠ₯ 베이슀)
75
+ πŸ€– **agent-mode 톡합** β€” 자율 λͺ¨λ“œ μ§„μž…/λ§€λΌμš΄λ“œ/λ§ˆκ°μ„ 3 λͺ…λ ΉμœΌλ‘œ 단좕
76
+
77
+ ---
78
+
3
79
  ## 1.9.238 β€” 2026-05-24
4
80
 
5
81
  **πŸŽ‰ R100 main-push streak 달성 + CLAUDE/AGENTS/session-workflow drift 차단 (1.9.228~237 9 λΌμš΄λ“œ λˆ„μ ).**
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.238-green)]() [![tests](https://img.shields.io/badge/e2e-217%2F217-success)]() [![stress](https://img.shields.io/badge/stress--v183-13%2F13-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-66-brightgreen)]() [![cli](https://img.shields.io/badge/CLI_commands-52-brightgreen)]() [![rounds](https://img.shields.io/badge/autonomous--rounds-194-blueviolet)]() [![main-push](https://img.shields.io/badge/πŸŽ‰_main--push_streak-100_rounds-success)]() [![npm-streak](https://img.shields.io/badge/πŸŽ‰_npm--publish-61_streak-success)]() [![goal-matrix](https://img.shields.io/badge/λͺ©μ _맀트릭슀-100%2F100_(5μΆ•)-brightgreen)]() [![drift-prevent](https://img.shields.io/badge/πŸ“š_drift--차단-9_λΌμš΄λ“œ_κ°±μ‹ -blueviolet)]() [![perf](https://img.shields.io/badge/cold_start-312ms-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.240-green)]() [![tests](https://img.shields.io/badge/e2e-217%2F217-success)]() [![stress](https://img.shields.io/badge/stress--v185-16%2F16-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-68-brightgreen)]() [![cli](https://img.shields.io/badge/CLI_commands-54-brightgreen)]() [![rounds](https://img.shields.io/badge/autonomous--rounds-196-blueviolet)]() [![main-push](https://img.shields.io/badge/πŸŽ‰_main--push_streak-102_rounds-success)]() [![npm-streak](https://img.shields.io/badge/πŸŽ‰_npm--publish-63_streak-success)]() [![json-9fields](https://img.shields.io/badge/πŸ“œ_JSON_톡합-9%2F9_ν•„λ“œ-blueviolet)]() [![langs](https://img.shields.io/badge/🐍_md_%2B_py-μžλ™_detect-blueviolet)]() [![perf](https://img.shields.io/badge/cold_start-386ms-success)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
6
6
 
7
7
  ```
8
8
  ╔══════════════════════════════════════════════════════════════╗
@@ -12,9 +12,9 @@
12
12
  β•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•”β•β•β• β–ˆβ–ˆβ•”β•β•β• β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β• β•šβ•β•β•β•β–ˆβ–ˆβ•‘ β•‘
13
13
  β•‘ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘ β•‘
14
14
  β•‘ β•šβ•β•β•β•β•β•β•β•šβ•β•β•β•β•β•β•β•šβ•β•β•β•β•β•β•β•šβ•β• β•šβ•β•β•šβ•β• β•šβ•β•β•β•β•šβ•β•β•β•β•β•β•β•šβ•β•β•β•β•β•β• β•‘
15
- β•‘ v1.9.238 AI Agent Reliability Harness + Sandbox β•‘
15
+ β•‘ v1.9.240 AI Agent Reliability Harness + Sandbox β•‘
16
16
  β•‘ verify Β· remember Β· orchestrate Β· audit Β· sandbox Β· drift β•‘
17
- β•‘ πŸŽ‰ R100 main-push streak Β· 9 λΌμš΄λ“œ doc drift 차단 β•‘
17
+ β•‘ 🐍 JSON 9 ν•„λ“œ (3 λͺ…λ Ή Γ— 9 = 27 톡합 β€” UR-0013 2단계) β•‘
18
18
  β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
19
19
  ```
20
20
 
package/bin/harness.js CHANGED
@@ -7,7 +7,7 @@ const cp = require('child_process');
7
7
  const os = require('os'); // 1.9.178: _publishToNpm μ—μ„œ os.tmpdir() μ‚¬μš© (μ „μ—­ import)
8
8
  const readline = require('readline');
9
9
 
10
- const VERSION = '1.9.238';
10
+ const VERSION = '1.9.240';
11
11
 
12
12
  // 1.9.184: DEP0190 (child_process shell: true) deprecation warning μ–΅μ œ (μ‚¬μš©μž λͺ…μ‹œ).
13
13
  // leerness λŠ” cross-platform PATH resolution 을 μœ„ν•΄ shell: true λ₯Ό μ˜λ„μ μœΌλ‘œ μ‚¬μš© (claude.cmd / ollama.cmd λ“± Windows .cmd 처리).
@@ -2196,6 +2196,156 @@ function milestonesCmd(root) {
2196
2196
  }
2197
2197
  }
2198
2198
 
2199
+ // 1.9.239: Python 파일 뢄석 (μ‚¬μš©μž λͺ…μ‹œ UR-0013) β€” μ˜μ‘΄μ„± 0 regex fallback
2200
+ // leerness κ°€ .md 외에 .py 파이썬 μŠ€ν¬λ¦½νŠΈλ„ 효율적으둜 μΈμ§€ν•˜λ„λ‘.
2201
+ // 뢄석: 파일 수 / import 수 / ν•¨μˆ˜ 수 / class 수 / λ³΅μž‘λ„(approx LOC) / TODO/FIXME
2202
+ // 응닡 (--json): { totalFiles, totalLOC, imports[], funcs[], classes[], complexity, todoCount }
2203
+ function _collectPyFiles(root, limit = 100) {
2204
+ const out = [];
2205
+ function walk(dir) {
2206
+ if (out.length >= limit) return;
2207
+ let entries;
2208
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
2209
+ catch { return; }
2210
+ for (const e of entries) {
2211
+ if (out.length >= limit) return;
2212
+ const fp = path.join(dir, e.name);
2213
+ if (e.isDirectory()) {
2214
+ // skip common heavy dirs
2215
+ if (/^(node_modules|\.git|__pycache__|\.harness|venv|\.venv|dist|build|_apps|_reports)$/.test(e.name)) continue;
2216
+ walk(fp);
2217
+ } else if (e.isFile() && /\.py$/i.test(e.name)) {
2218
+ out.push(fp);
2219
+ }
2220
+ }
2221
+ }
2222
+ walk(root);
2223
+ return out;
2224
+ }
2225
+
2226
+ function _analyzePyFile(fp) {
2227
+ let txt = '';
2228
+ try { txt = fs.readFileSync(fp, 'utf8'); } catch { return null; }
2229
+ const lines = txt.split('\n');
2230
+ const importRe = /^\s*(?:from\s+\S+\s+)?import\s+\S+/;
2231
+ const funcRe = /^\s*(?:async\s+)?def\s+(\w+)\s*\(/;
2232
+ const classRe = /^\s*class\s+(\w+)/;
2233
+ const todoRe = /#\s*(TODO|FIXME|XXX|HACK)\b/;
2234
+ let imports = 0, funcs = [], classes = [], todos = 0;
2235
+ for (const line of lines) {
2236
+ if (importRe.test(line)) imports++;
2237
+ const fm = line.match(funcRe); if (fm) funcs.push(fm[1]);
2238
+ const cm = line.match(classRe); if (cm) classes.push(cm[1]);
2239
+ if (todoRe.test(line)) todos++;
2240
+ }
2241
+ return {
2242
+ file: fp,
2243
+ loc: lines.length,
2244
+ imports,
2245
+ funcs: funcs.length,
2246
+ classes: classes.length,
2247
+ todos
2248
+ };
2249
+ }
2250
+
2251
+ function pyCheckCmd(root) {
2252
+ root = absRoot(root);
2253
+ const isTty = process.stdout && process.stdout.isTTY;
2254
+ const cy = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
2255
+ const gr = s => isTty ? `\x1b[32m${s}\x1b[0m` : s;
2256
+ const yl = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
2257
+ const dm = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
2258
+ const files = _collectPyFiles(root, 200);
2259
+ const analyses = files.map(f => _analyzePyFile(f)).filter(Boolean);
2260
+ const totalLOC = analyses.reduce((s, a) => s + a.loc, 0);
2261
+ const totalImports = analyses.reduce((s, a) => s + a.imports, 0);
2262
+ const totalFuncs = analyses.reduce((s, a) => s + a.funcs, 0);
2263
+ const totalClasses = analyses.reduce((s, a) => s + a.classes, 0);
2264
+ const totalTodos = analyses.reduce((s, a) => s + a.todos, 0);
2265
+ const result = {
2266
+ totalFiles: analyses.length,
2267
+ totalLOC, totalImports, totalFuncs, totalClasses, totalTodos,
2268
+ biggest: analyses.slice().sort((a, b) => b.loc - a.loc).slice(0, 5).map(a => ({ file: path.relative(root, a.file), loc: a.loc, funcs: a.funcs, classes: a.classes }))
2269
+ };
2270
+ if (has('--json')) { log(JSON.stringify(result, null, 2)); return; }
2271
+ log(cy(`# leerness py-check (1.9.239) β€” Python 파일 뢄석 (μ‚¬μš©μž λͺ…μ‹œ UR-0013)`));
2272
+ log('');
2273
+ if (analyses.length === 0) {
2274
+ log(dm(` Python 파일 μ—†μŒ (.py 검색됨: ${files.length}, 뢄석 κ°€λŠ₯: 0)`));
2275
+ log(dm(` β†’ leerness λŠ” .md/.py λͺ¨λ‘ 뢄석 κ°€λŠ₯ (1.9.239)`));
2276
+ return;
2277
+ }
2278
+ log(` πŸ“Š 톡계:`);
2279
+ log(` 파일 ${gr(String(analyses.length))} Β· LOC ${gr(String(totalLOC))} Β· 평균 ${gr(String(Math.round(totalLOC / analyses.length)))} loc/file`);
2280
+ log(` imports ${totalImports} Β· functions ${totalFuncs} Β· classes ${totalClasses}`);
2281
+ if (totalTodos > 0) log(yl(` ⚠ TODO/FIXME ${totalTodos}건`));
2282
+ log('');
2283
+ if (result.biggest.length > 0) {
2284
+ log(` Top 5 파일 (LOC κΈ°μ€€):`);
2285
+ result.biggest.forEach(f => log(` β€’ ${f.file} β€” ${f.loc} loc / ${f.funcs} func / ${f.classes} class`));
2286
+ }
2287
+ log('');
2288
+ log(dm(` β†’ JSON 좜λ ₯: leerness py-check --json`));
2289
+ }
2290
+
2291
+ // 1.9.239: agent-mode β€” 자율 λͺ¨λ“œ μ „μš© 톡합 λͺ…λ Ή (μ‚¬μš©μž λͺ…μ‹œ UR-0013)
2292
+ // handoff + pulse + drift auto-fix + session-resume λ₯Ό 1 λͺ…λ ΉμœΌλ‘œ
2293
+ // start: μ§„μž… (handoff + drift auto-fix + λΉ„μ •μƒμ’…λ£Œ μžλ™ 회볡)
2294
+ // tick: λ§€ λΌμš΄λ“œ (pulse + 1 line)
2295
+ // stop: 마감 (session close + auto-apply-delivered + auto-cleanup-branches)
2296
+ function agentModeCmd(root, sub) {
2297
+ root = absRoot(root);
2298
+ const isTty = process.stdout && process.stdout.isTTY;
2299
+ const cy = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
2300
+ const gr = s => isTty ? `\x1b[32m${s}\x1b[0m` : s;
2301
+ const yl = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
2302
+ const dm = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
2303
+ function spawnChild(args, opts) {
2304
+ return cp.spawnSync(process.execPath, [__filename, ...args], Object.assign({ encoding: 'utf8', cwd: root, env: { ...process.env, LEERNESS_NO_BANNER: '1', LEERNESS_NO_PROMPT: '1' } }, opts || {}));
2305
+ }
2306
+ if (!sub || sub === 'help') {
2307
+ log(cy(`# leerness agent-mode (1.9.239) β€” 자율 λͺ¨λ“œ μ „μš© 톡합 λͺ…λ Ή`));
2308
+ log('');
2309
+ log(` start μ§„μž… (handoff + drift --auto-fix + session-resume --auto-fix)`);
2310
+ log(` tick λ§€ λΌμš΄λ“œ (pulse + 핡심 μ‹ ν˜Έ)`);
2311
+ log(` stop 마감 (session close --auto-apply-delivered --auto-cleanup-branches)`);
2312
+ log('');
2313
+ log(dm(` μ‚¬μš©μž λͺ…μ‹œ UR-0013 (Python + agent-mode νš¨μœ¨ν™”)`));
2314
+ return;
2315
+ }
2316
+ if (sub === 'start') {
2317
+ log(cy(`# leerness agent-mode start (1.9.239)`));
2318
+ log(dm(` β†’ 1. drift check --auto-fix (λ³΄μ•ˆ + delivered + release cleanup)`));
2319
+ const r1 = spawnChild(['drift', 'check', root, '--auto-fix']);
2320
+ if (r1.status === 0) log(gr(` βœ“ drift auto-fix μ™„λ£Œ`)); else log(yl(` ⚠ drift auto-fix μ’…λ£Œ ${r1.status}`));
2321
+ log(dm(` β†’ 2. session-resume --auto-fix (λΉ„μ •μƒμ’…λ£Œ μžλ™ 회볡)`));
2322
+ const r2 = spawnChild(['session-resume', '--path', root, '--auto-fix']);
2323
+ if (r2.status === 0) log(gr(` βœ“ session-resume μ™„λ£Œ`)); else log(yl(` ⚠ session-resume μ’…λ£Œ ${r2.status}`));
2324
+ log(dm(` β†’ 3. handoff (μ»¨ν…μŠ€νŠΈ 적재)`));
2325
+ const r3 = spawnChild(['handoff', root]);
2326
+ if (r3.status === 0) log(gr(` βœ“ handoff 좜λ ₯ μ™„λ£Œ (μœ„ μ°Έμ‘°)`)); else log(yl(` ⚠ handoff μ’…λ£Œ ${r3.status}`));
2327
+ log('');
2328
+ log(gr(`βœ“ agent-mode start μ™„λ£Œ β€” 자율 λΌμš΄λ“œ μ§„ν–‰ μ€€λΉ„`));
2329
+ return;
2330
+ }
2331
+ if (sub === 'tick') {
2332
+ // 가벼움 μš°μ„  β€” pulse 만 호좜
2333
+ log(cy(`# leerness agent-mode tick (1.9.239) β€” κ°€λ²Όμš΄ μƒνƒœ 확인`));
2334
+ const r = spawnChild(['pulse', '--path', root]);
2335
+ process.stdout.write(r.stdout || '');
2336
+ return;
2337
+ }
2338
+ if (sub === 'stop') {
2339
+ log(cy(`# leerness agent-mode stop (1.9.239) β€” 톡합 마감`));
2340
+ log(dm(` β†’ session close --auto-apply-delivered --auto-cleanup-branches`));
2341
+ const r = spawnChild(['session', 'close', root, '--auto-apply-delivered', '--auto-cleanup-branches']);
2342
+ process.stdout.write(r.stdout || '');
2343
+ return;
2344
+ }
2345
+ log(yl(`unknown subcommand: ${sub}`));
2346
+ log(dm(` β†’ leerness agent-mode help`));
2347
+ }
2348
+
2199
2349
  // 1.9.234: recentChanges β€” 졜근 N λΌμš΄λ“œμ˜ 핡심 λ³€κ²½ μš”μ•½ (git tag commit subject)
2200
2350
  // 응닡: [{ version, date, subject }] (κ°€μž₯ μ΅œκ·ΌλΆ€ν„°)
2201
2351
  function _computeRecentChanges(root, limit = 5) {
@@ -6240,6 +6390,19 @@ function handoff(root) {
6240
6390
  try {
6241
6391
  result.recentChanges = _computeRecentChanges(root, 5);
6242
6392
  } catch {}
6393
+ // 1.9.240: pyFiles 톡합 (handoff JSON 9번째 톡합 ν•„λ“œ) β€” Python 닀쀑 μ–Έμ–΄ κ°€μ‹œν™” (UR-0013 2단계)
6394
+ try {
6395
+ const pyFiles = _collectPyFiles(root, 200);
6396
+ const analyses = pyFiles.slice(0, 200).map(f => _analyzePyFile(f)).filter(Boolean);
6397
+ result.pyFiles = {
6398
+ total: pyFiles.length,
6399
+ analyzed: analyses.length,
6400
+ totalLOC: analyses.reduce((s, a) => s + a.loc, 0),
6401
+ totalImports: analyses.reduce((s, a) => s + a.imports, 0),
6402
+ totalFuncs: analyses.reduce((s, a) => s + a.funcs, 0),
6403
+ totalClasses: analyses.reduce((s, a) => s + a.classes, 0)
6404
+ };
6405
+ } catch {}
6243
6406
  } catch {}
6244
6407
  try {
6245
6408
  const pwState = _loadPreWakeReport(root);
@@ -6402,6 +6565,13 @@ function handoff(root) {
6402
6565
  parts.push(`🌐 official ${m.total}/${m.cacheTotal} (${ageStr}${status})`);
6403
6566
  }
6404
6567
  } catch {}
6568
+ // 18) 1.9.240: pyFiles ν—€λ“œλΌμΈ β€” Python 닀쀑 μ–Έμ–΄ κ°€μ‹œν™” (UR-0013 2단계, .py 파일 β‰₯ 1)
6569
+ try {
6570
+ const pyFiles = _collectPyFiles(root, 200);
6571
+ if (pyFiles.length > 0) {
6572
+ parts.push(`🐍 py ${pyFiles.length}`);
6573
+ }
6574
+ } catch {}
6405
6575
  // 17) 1.9.226: round-history ν—€λ“œλΌμΈ β€” 자율 λΌμš΄λ“œ μΉ΄μš΄ν„° + λ‹€μŒ λ§ˆμΌμŠ€ν†€ (5 λΌμš΄λ“œ 이상 μ‹œλ§Œ ν‘œμ‹œ)
6406
6576
  // 1.9.230: λ‹€μŒ λ§ˆμΌμŠ€ν†€ ETA (10R 이내 + ETA 7일 이내일 λ•Œ κ°•μ‘°)
6407
6577
  try {
@@ -10422,6 +10592,19 @@ function sessionClose(root, opts = {}) {
10422
10592
  try {
10423
10593
  jsonResult.recentChanges = _computeRecentChanges(root, 5);
10424
10594
  } catch {}
10595
+ // 1.9.240: pyFiles 톡합 (session close JSON 9번째 톡합 ν•„λ“œ) β€” UR-0013 2단계
10596
+ try {
10597
+ const pyFiles = _collectPyFiles(root, 200);
10598
+ const analyses = pyFiles.slice(0, 200).map(f => _analyzePyFile(f)).filter(Boolean);
10599
+ jsonResult.pyFiles = {
10600
+ total: pyFiles.length,
10601
+ analyzed: analyses.length,
10602
+ totalLOC: analyses.reduce((s, a) => s + a.loc, 0),
10603
+ totalImports: analyses.reduce((s, a) => s + a.imports, 0),
10604
+ totalFuncs: analyses.reduce((s, a) => s + a.funcs, 0),
10605
+ totalClasses: analyses.reduce((s, a) => s + a.classes, 0)
10606
+ };
10607
+ } catch {}
10425
10608
  } catch {}
10426
10609
  try {
10427
10610
  // 1.9.209: pre-wake-audit μžλ™ μ‹€ν–‰ + μ €μž₯ (sleep μ „ μžλ™ 점검)
@@ -14558,7 +14741,9 @@ function mcpServeCmd(root) {
14558
14741
  { name: 'leerness_milestones', description: '1.9.229 (1.9.226 ν™•μž₯) β€” 도달 λ§ˆμΌμŠ€ν†€ + λ‹€μŒ ETA. git tag 순차 λΆ„μ„μœΌλ‘œ 25/50/75/100/125/150/175/200/250/300/400/500 λ§ˆμΌμŠ€ν†€ 도달 일자 + λ‹€μŒ λ§ˆμΌμŠ€ν†€ ETA (ν˜„μž¬ 속도 κΈ°μ€€) 계산. 응닡: { totalRounds, reached: [{milestone, version, reachedAt, daysFromBaseline}], next: {milestone, roundsRemaining, etaDays, etaDate}, baselineAt, avgRoundsPerDay }. μ™ΈλΆ€ AIκ°€ "μ§€κΈˆκΉŒμ§€ μ–΄λ–€ λ§ˆμΌμŠ€ν†€μ„ μ–Έμ œ λ‹¬μ„±ν–ˆκ³  λ‹€μŒ λ§ˆμΌμŠ€ν†€ μ˜ˆμƒ 도달일은?"을 회수. 인자: { path? }', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
14559
14742
  { name: 'leerness_pulse', description: '1.9.231 β€” ν•œ 쀄 μ’…ν•© μš”μ•½ (10 핡심 μ§€ν‘œ). 응닡: { version, roundCount, mcpTools, memorySurface, security, health, driftScore, nextMilestone, etaDays, abnormalShutdown }. μ™ΈλΆ€ AIκ°€ "leerness μƒνƒœ ν•œ λˆˆμ— 보기"λ₯Ό κ°€λ²Όμš΄ 단일 호좜둜 회수. handoff 보닀 5λ°° 빠름 (drift/health 계산 skip). 인자: { path? }', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
14560
14743
  { name: 'leerness_commands', description: '1.9.233 β€” μΉ΄ν…Œκ³ λ¦¬ν™”λœ 전체 CLI λͺ…λ Ή λͺ©λ‘ (9 μΉ΄ν…Œκ³ λ¦¬). 응닡: { version, totalCommands, categories: { status, task, memory, audit, workflow, release, skill, bridge, config } }. μ™ΈλΆ€ AIκ°€ "leerness κ°€ μ œκ³΅ν•˜λŠ” λͺ…령이 뭐가 μžˆλ‚˜"λ₯Ό 직접 회수. 맀뉴얼/도움말 동적 생성. 인자: { path? }', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
14561
- { name: 'leerness_release_cleanup', description: '1.9.236 (1.9.235 μžλ™ 회수) β€” local release/* branches 정리. main 에 merge된 κ²ƒλ§Œ 후보 (unmerged 보호, ν˜„μž¬ branch 보호). 응닡: { apply, keep, total, merged, unmerged, deleteCount, toDelete[], recent[], unmergedSample[] }. μ™ΈλΆ€ AIκ°€ "운영 λˆ„μ  release branches 정리 κ°€λŠ₯?"을 회수. κΈ°λ³Έ dry-run, apply: true μ‹œμ—λ§Œ μ‹€ μ‚­μ œ. 인자: { path?, apply? (default false), keep? (default 5) }', inputSchema: { type: 'object', properties: { path: { type: 'string' }, apply: { type: 'boolean' }, keep: { type: 'number' } } } }
14744
+ { name: 'leerness_release_cleanup', description: '1.9.236 (1.9.235 μžλ™ 회수) β€” local release/* branches 정리. main 에 merge된 κ²ƒλ§Œ 후보 (unmerged 보호, ν˜„μž¬ branch 보호). 응닡: { apply, keep, total, merged, unmerged, deleteCount, toDelete[], recent[], unmergedSample[] }. μ™ΈλΆ€ AIκ°€ "운영 λˆ„μ  release branches 정리 κ°€λŠ₯?"을 회수. κΈ°λ³Έ dry-run, apply: true μ‹œμ—λ§Œ μ‹€ μ‚­μ œ. 인자: { path?, apply? (default false), keep? (default 5) }', inputSchema: { type: 'object', properties: { path: { type: 'string' }, apply: { type: 'boolean' }, keep: { type: 'number' } } } },
14745
+ { name: 'leerness_py_check', description: '1.9.239 (μ‚¬μš©μž λͺ…μ‹œ UR-0013) β€” Python 파일 뢄석. μ˜μ‘΄μ„± 0 regex fallback. .md μ™Έ .py 도 leerness 인지. 응닡: { totalFiles, totalLOC, totalImports, totalFuncs, totalClasses, totalTodos, biggest: [{ file, loc, funcs, classes }] }. μ™ΈλΆ€ AIκ°€ "이 ν”„λ‘œμ νŠΈ Python ν‘œλ©΄μ΄ μ–Όλ§ˆλ‚˜ λ˜λ‚˜"λ₯Ό 회수. 인자: { path? }', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
14746
+ { name: 'leerness_agent_mode', description: '1.9.239 (μ‚¬μš©μž λͺ…μ‹œ UR-0013) β€” 자율 λͺ¨λ“œ μ „μš© 톡합 λͺ…λ Ή. start: handoff + drift --auto-fix + session-resume --auto-fix (μ§„μž…). tick: pulse ν•œ 쀄 (λ§€ λΌμš΄λ“œ). stop: session close --auto-apply-delivered --auto-cleanup-branches (마감). μ™ΈλΆ€ AI κ°€ 자율 λΌμš΄λ“œ μ§„μž…/λ§€ λΌμš΄λ“œ/λ§ˆκ°μ„ 단일 호좜둜 μˆ˜ν–‰. 인자: { path?, sub (required: "start"|"tick"|"stop"|"help") }', inputSchema: { type: 'object', properties: { path: { type: 'string' }, sub: { type: 'string', enum: ['start', 'tick', 'stop', 'help'] } }, required: ['sub'] } }
14562
14747
  ];
14563
14748
 
14564
14749
  function send(obj) {
@@ -14735,6 +14920,14 @@ function mcpServeCmd(root) {
14735
14920
  // 1.9.233: μΉ΄ν…Œκ³ λ¦¬ν™”λœ 전체 CLI λͺ…λ Ή λͺ©λ‘
14736
14921
  cliArgs = ['commands', '--json'];
14737
14922
  break;
14923
+ case 'leerness_py_check':
14924
+ // 1.9.239 (UR-0013): Python 파일 뢄석
14925
+ cliArgs = ['py-check', '--path', targetPath, '--json'];
14926
+ break;
14927
+ case 'leerness_agent_mode':
14928
+ // 1.9.239 (UR-0013): 자율 λͺ¨λ“œ 톡합 (start/tick/stop)
14929
+ cliArgs = ['agent-mode', String(args.sub || 'help'), '--path', targetPath];
14930
+ break;
14738
14931
  case 'leerness_release_cleanup':
14739
14932
  // 1.9.236 (1.9.235): local release/* branches 정리
14740
14933
  cliArgs = ['release', 'cleanup', '--json'];
@@ -16965,6 +17158,19 @@ function healthCmd(root) {
16965
17158
  try {
16966
17159
  out.recentChanges = _computeRecentChanges(root, 5);
16967
17160
  } catch { out.recentChanges = { error: 'recentChanges 점검 μ‹€νŒ¨' }; }
17161
+ // 1.9.240: health --json pyFiles 톡합 (3 λͺ…λ Ή 9 ν•„λ“œ β€” UR-0013 2단계)
17162
+ try {
17163
+ const pyFiles = _collectPyFiles(root, 200);
17164
+ const analyses = pyFiles.slice(0, 200).map(f => _analyzePyFile(f)).filter(Boolean);
17165
+ out.pyFiles = {
17166
+ total: pyFiles.length,
17167
+ analyzed: analyses.length,
17168
+ totalLOC: analyses.reduce((s, a) => s + a.loc, 0),
17169
+ totalImports: analyses.reduce((s, a) => s + a.imports, 0),
17170
+ totalFuncs: analyses.reduce((s, a) => s + a.funcs, 0),
17171
+ totalClasses: analyses.reduce((s, a) => s + a.classes, 0)
17172
+ };
17173
+ } catch { out.pyFiles = { error: 'pyFiles 점검 μ‹€νŒ¨' }; }
16968
17174
  // 1.9.163: 5λŠ₯λ ₯ 맀트릭슀 μžλ™ 평가 (1.9.155 sub-agent 점검 β†’ μ½”λ“œ 기반 μžλ™ν™”)
16969
17175
  // 각 λŠ₯λ ₯을 μ½”λ“œ grep 으둜 κ²€μΆœ β†’ 0~100 점수. μ‚¬μš©μžκ°€ λ§€ health 호좜 μ‹œ leerness 자기 평가 확인.
16970
17176
  try {
@@ -18406,6 +18612,10 @@ async function main() {
18406
18612
  if (cmd === 'pulse') return pulseCmd(arg('--path', process.cwd()));
18407
18613
  // 1.9.233: leerness commands β€” μΉ΄ν…Œκ³ λ¦¬ν™”λœ 전체 CLI λͺ…λ Ή λͺ©λ‘
18408
18614
  if (cmd === 'commands') return commandsCmd(arg('--path', process.cwd()));
18615
+ // 1.9.239: leerness py-check β€” Python 파일 뢄석 (μ‚¬μš©μž λͺ…μ‹œ UR-0013)
18616
+ if (cmd === 'py-check') return pyCheckCmd(arg('--path', process.cwd()));
18617
+ // 1.9.239: leerness agent-mode <start|tick|stop> β€” 자율 λͺ¨λ“œ μ „μš© 톡합 λͺ…λ Ή (μ‚¬μš©μž λͺ…μ‹œ UR-0013)
18618
+ if (cmd === 'agent-mode') return agentModeCmd(arg('--path', process.cwd()), args[1]);
18409
18619
  // 1.9.208: leerness constraints <list|check|add> β€” ν”Œλž«νΌ/API μ œμ•½ 사전 체크 (μ‚¬μš©μž λͺ…μ‹œ)
18410
18620
  if (cmd === 'constraints') return constraintsCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
18411
18621
  // 1.9.209: leerness pre-wake-audit β€” sleep μ „ sub-agent audit (μ‚¬μš©μž λͺ…μ‹œ)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.238",
3
+ "version": "1.9.240",
4
4
  "description": "Leerness: λΉ„νŒŒκ΄΄ λ§ˆμ΄κ·Έλ ˆμ΄μ…˜, μžλ™ 버전 κ°μ§€Β·μ—…λ°μ΄νŠΈ, κ³„νš/μ§„ν–‰/ν•Έλ“œμ˜€ν”„ μžλ™ν™”, κ²ŒμœΌλ¦„Β·μ‹œν¬λ¦ΏΒ·μΈμ½”λ”© μžλ™ κ°€λ“œ, Claude Code μŠ¬λž˜μ‹œ 톡합을 κ°–μΆ˜ ν•œκ΅­μ–΄ μš°μ„  AI 개발 ν•˜λ„€μŠ€.",
5
5
  "keywords": [
6
6
  "leerness",