leerness 1.9.239 β†’ 1.9.241

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,82 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.241 β€” 2026-05-24
4
+
5
+ **🌐 leerness env + ν•œκ΅­μ–΄ PowerShell 인코딩 μœ„ν—˜ 사전 감지 (μ‚¬μš©μž λͺ…μ‹œ UR-0014).**
6
+
7
+ ### μ‚¬μš©μž λͺ…μ‹œ (UR-0014)
8
+ > *"leerness μ„€μΉ˜ ν”„λ‘œμ νŠΈμ—μ„œ PowerShell/터미널 ν•œκ΅­μ–΄ 인코딩 였λ₯˜ 사전 감지 β€” OS/μ–Έμ–΄μ„€μ •/ν•˜λ“œμ›¨μ–΄/터미널/SW 버전 데이터 μˆ™μ§€λ‘œ 개발/ν…ŒμŠ€νŠΈ/디버깅 μ‹œ ν™˜κ²½ ν˜Έν™˜μ„± 보μž₯"*
9
+
10
+ λ°°κ²½: μ‚¬μš©μžκ°€ `3d-map-maker` ν”„λ‘œμ νŠΈμ—μ„œ `npm run setup` β†’ `setup.ps1` νŒŒμ‹± 였λ₯˜ (ν•œκ΅­μ–΄ + BOM μ—†λŠ” UTF-8 β†’ PowerShell CP949 μ˜€μΈμ‹)
11
+
12
+ ### 1. `leerness env summary` μƒˆ λͺ…λ Ή (1.9.145 envCheck 와 λ³„κ°œ)
13
+ - ν™˜κ²½ μ’…ν•© 좜λ ₯: OS / Node / Locale / ν•œκ΅­μ–΄ Windows 감지 / Hardware / Terminal (PowerShell 버전) / Tools (git/npm/python)
14
+ - `chcp` 좜λ ₯으둜 μ½”λ“œνŽ˜μ΄μ§€ μžλ™ 감지 (949 = ν•œκ΅­μ–΄)
15
+ - `--json` μ˜΅μ…˜
16
+ - μ‹ κ·œ helper: `_collectRuntimeEnv()` (κΈ°μ‘΄ `_detectEnvironment` 와 λͺ…λͺ… 뢄리)
17
+
18
+ ### 2. `leerness env encoding` μƒˆ λͺ…λ Ή β€” μ…Έ 슀크립트 인코딩 μœ„ν—˜ 감지
19
+ - `.ps1` / `.bat` / `.cmd` / `.sh` μžλ™ μŠ€μΊ”
20
+ - BOM μ—†λŠ” λΉ„-ASCII λ°”μ΄νŠΈ κ²€μΆœ (μž¬κ·€, 4096 byte 이내)
21
+ - μœ„ν—˜ μ‹œ: `Windows ν•œκ΅­μ–΄ PowerShell μ—μ„œ CP949 둜 μ˜€μΈμ‹ κ°€λŠ₯ (BOM μΆ”κ°€ ꢌμž₯)`
22
+ - ν•΄κ²°μ±… μžλ™ μ œμ‹œ (UTF-8 BOM μΆ”κ°€ λ˜λŠ” `$OutputEncoding` μ„€μ •)
23
+
24
+ ### 3. handoff body μžλ™ λ…ΈμΆœ
25
+ - `## ⚠ μ…Έ 슀크립트 인코딩 μœ„ν—˜ N건 (1.9.241, UR-0014)` λ³Έλ¬Έ μ„Ήμ…˜
26
+ - top 3 μœ„ν—˜ 파일 + ν•΄κ²° μ•ˆλ‚΄ (BOM μΆ”κ°€)
27
+ - `leerness env encoding` κ°€μ΄λ“œ μžλ™
28
+
29
+ ### 4. MCP 69번째 도ꡬ β€” `leerness_env_info`
30
+ - μ™ΈλΆ€ AI κ°€ ν™˜κ²½ ν˜Έν™˜μ„± 사전 인지 β†’ 인코딩 였λ₯˜ 예방
31
+ - `encodingCheck: true` μ˜΅μ…˜ μ‹œ μ…Έ 슀크립트 μŠ€μΊ”
32
+ - MCP 68 β†’ **69** (+1)
33
+
34
+ ### 5. λˆ„μ  νšŒκ·€ (1.9.207~240) β€” λͺ¨λ‘ μœ μ§€
35
+ - κΈ°μ‘΄ `_detectEnvironment(root)` (1.9.145) 와 좩돌 없이 곡쑴
36
+
37
+ ### 6. stress-v186 β€” 16/16 PASS
38
+ - 1.9.241 (7): VERSION + env summary + JSON + encoding μœ„ν—˜ 감지 + μ•ˆμ „ μ‹œ μœ„ν—˜ 0 + handoff body + MCP 69
39
+ - μ„±λŠ₯ (1): cold_start avg 598ms
40
+ - λˆ„μ  νšŒκ·€ (8): 1.9.207~240
41
+
42
+ ### 7. μžλ™ release (103 λΌμš΄λ“œ main-push streak Β· 64 λΌμš΄λ“œ npm publish streak)
43
+
44
+ 🌐 **ν•œκ΅­μ–΄ Windows ν™˜κ²½ ν˜Έν™˜μ„± 보강** β€” μ‚¬μš©μž 보고 .ps1 νŒŒμ‹± 였λ₯˜ 같은 μΌ€μ΄μŠ€ 사전 감지
45
+
46
+ ---
47
+
48
+ ## 1.9.240 β€” 2026-05-24
49
+
50
+ **🐍 UR-0013 2단계: handoff/session close/health --json pyFiles (9 ν•„λ“œ) + ν—€λ“œλΌμΈ μžλ™.**
51
+
52
+ ### 1. handoff/session close/health --json 9번째 톡합 ν•„λ“œ `pyFiles`
53
+ - Python 파일 μžλ™ detect (1.9.239 `_collectPyFiles` μž¬μ‚¬μš©)
54
+ - ν˜•μ‹: `{ total, analyzed, totalLOC, totalImports, totalFuncs, totalClasses }`
55
+ - 3 λͺ…λ Ή 일관성 맀트릭슀 μ§„ν™”: 7 β†’ 8 β†’ **9 ν•„λ“œ** (3 Γ— 9 = 27 톡합 포인트)
56
+ - AI κ°€ 닀쀑 μ–Έμ–΄ μ›Œν¬μŠ€νŽ˜μ΄μŠ€ (.md + .py) 인지
57
+
58
+ ### 2. handoff ν—€λ“œλΌμΈ 18번째 μš”μ†Œ
59
+ - `.py` 파일 β‰₯ 1 μ‹œ: `🐍 py N` μžλ™ λ…ΈμΆœ
60
+ - 0개일 λ•Œ λ―Έλ…ΈμΆœ (μ‘°μš©ν•¨)
61
+
62
+ ### 3. UR-0013 2단계 μ™„λ£Œ
63
+ - 1.9.239 1단계: py-check CLI + agent-mode CLI + MCP 67/68
64
+ - **1.9.240 2단계**: JSON 9 ν•„λ“œ + ν—€λ“œλΌμΈ μžλ™
65
+ - 닀쀑 μ–Έμ–΄ ν‘œλ©΄ (.md + .py) μžλ™ κ°€μ‹œν™”
66
+
67
+ ### 4. λˆ„μ  νšŒκ·€ (1.9.207~239) β€” λͺ¨λ‘ μœ μ§€
68
+
69
+ ### 5. stress-v185 β€” 16/16 PASS
70
+ - 1.9.240 (7): VERSION + handoff/session close/health JSON + 9 ν•„λ“œ + ν—€λ“œλΌμΈ (있음/μ—†μŒ)
71
+ - μ„±λŠ₯ (1): cold_start avg 386ms
72
+ - λˆ„μ  νšŒκ·€ (8): 1.9.207~239
73
+
74
+ ### 6. μžλ™ release (102 λΌμš΄λ“œ main-push streak Β· 63 λΌμš΄λ“œ npm publish streak)
75
+
76
+ 🐍 **닀쀑 μ–Έμ–΄ ν‘œλ©΄ κ°€μ‹œν™”** β€” .md + .py μžλ™ 톡합 (μ™ΈλΆ€ AI κ°€ 두 ν‘œλ©΄ λͺ¨λ‘ 회수)
77
+
78
+ ---
79
+
3
80
  ## 1.9.239 β€” 2026-05-24
4
81
 
5
82
  **🐍 leerness py-check + πŸ€– agent-mode + MCP 67/68 (μ‚¬μš©μž λͺ…μ‹œ UR-0013).**
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.239-green)]() [![tests](https://img.shields.io/badge/e2e-217%2F217-success)]() [![stress](https://img.shields.io/badge/stress--v184-18%2F18-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-195-blueviolet)]() [![main-push](https://img.shields.io/badge/πŸŽ‰_main--push_streak-101_rounds-success)]() [![npm-streak](https://img.shields.io/badge/πŸŽ‰_npm--publish-62_streak-success)]() [![langs](https://img.shields.io/badge/🐍_supported-md_%2B_py-blueviolet)]() [![agent-mode](https://img.shields.io/badge/πŸ€–_agent--mode-start%2Ftick%2Fstop-blueviolet)]() [![perf](https://img.shields.io/badge/cold_start-372ms-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.241-green)]() [![tests](https://img.shields.io/badge/e2e-217%2F217-success)]() [![stress](https://img.shields.io/badge/stress--v186-16%2F16-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-69-brightgreen)]() [![cli](https://img.shields.io/badge/CLI_commands-56-brightgreen)]() [![rounds](https://img.shields.io/badge/autonomous--rounds-197-blueviolet)]() [![main-push](https://img.shields.io/badge/πŸŽ‰_main--push_streak-103_rounds-success)]() [![npm-streak](https://img.shields.io/badge/πŸŽ‰_npm--publish-64_streak-success)]() [![encoding](https://img.shields.io/badge/🌐_env_encoding-ν•œκ΅­μ–΄_PS_μœ„ν—˜_감지-blueviolet)]() [![perf](https://img.shields.io/badge/cold_start-598ms-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.239 AI Agent Reliability Harness + Sandbox β•‘
15
+ β•‘ v1.9.241 AI Agent Reliability Harness + Sandbox β•‘
16
16
  β•‘ verify Β· remember Β· orchestrate Β· audit Β· sandbox Β· drift β•‘
17
- β•‘ 🐍 py-check Β· πŸ€– agent-mode Β· MCP 68 (μ‚¬μš©μž λͺ…μ‹œ UR-0013) β•‘
17
+ β•‘ 🌐 env encoding μœ„ν—˜ 사전 감지 (μ‚¬μš©μž λͺ…μ‹œ UR-0014) β•‘
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.239';
10
+ const VERSION = '1.9.241';
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 처리).
@@ -2376,6 +2376,157 @@ function _computeRecentChanges(root, limit = 5) {
2376
2376
  return items;
2377
2377
  }
2378
2378
 
2379
+ // 1.9.241: ν™˜κ²½ 정보 μ’…ν•© μˆ˜μ§‘ + μ…Έ 슀크립트 인코딩 μœ„ν—˜ 감지 (μ‚¬μš©μž λͺ…μ‹œ UR-0014)
2380
+ // μ‚¬μš©μž 보고: Windows + ν•œκ΅­μ–΄ PowerShell μ—μ„œ .ps1 UTF-8 (no BOM) 파일이 CP949 둜 잘λͺ» μΈμ‹λ˜μ–΄ νŒŒμ‹± 였λ₯˜
2381
+ // ν•΄κ²°: leerness κ°€ ν™˜κ²½ 정보 + μ…Έ 슀크립트 인코딩을 사전 감지 β†’ handoff body κ²½κ³ 
2382
+ // 주의: κΈ°μ‘΄ _detectEnvironment (1.9.145) 와 λͺ…λͺ… 좩돌 β†’ _collectRuntimeEnv 둜 뢄리
2383
+ function _collectRuntimeEnv() {
2384
+ const env = {
2385
+ os: { platform: os.platform(), release: os.release(), arch: os.arch() },
2386
+ node: { version: process.version, exe: process.execPath },
2387
+ locale: {
2388
+ LANG: process.env.LANG || null,
2389
+ LC_ALL: process.env.LC_ALL || null,
2390
+ LC_CTYPE: process.env.LC_CTYPE || null,
2391
+ // Windows ν•œκ΅­μ–΄ ν™˜κ²½ detect
2392
+ OEMCP: process.env.OEMCP || null, // CP949 = 949
2393
+ isKoreanWindows: false
2394
+ },
2395
+ hardware: {
2396
+ cpus: os.cpus().length,
2397
+ memGB: Math.round(os.totalmem() / 1024 / 1024 / 1024),
2398
+ hostname: os.hostname()
2399
+ },
2400
+ terminal: {
2401
+ isTTY: !!(process.stdout && process.stdout.isTTY),
2402
+ term: process.env.TERM || null,
2403
+ shell: process.env.SHELL || process.env.ComSpec || null,
2404
+ powershell: null
2405
+ },
2406
+ tools: { git: null, npm: null, python: null, python3: null }
2407
+ };
2408
+ // ν•œκ΅­μ–΄ Windows 감지
2409
+ try {
2410
+ if (env.os.platform === 'win32') {
2411
+ // chcp 좜λ ₯으둜 μ½”λ“œνŽ˜μ΄μ§€ 확인
2412
+ const r = cp.spawnSync('chcp', [], { encoding: 'utf8', shell: true, timeout: 3000 });
2413
+ const m = (r.stdout || '').match(/(\d{3,4})/);
2414
+ if (m) {
2415
+ const cp_num = parseInt(m[1], 10);
2416
+ env.locale.codepage = cp_num;
2417
+ if (cp_num === 949 || cp_num === 51949) env.locale.isKoreanWindows = true;
2418
+ }
2419
+ }
2420
+ } catch {}
2421
+ // PowerShell 감지
2422
+ try {
2423
+ if (env.os.platform === 'win32') {
2424
+ const r = cp.spawnSync('powershell', ['-NoProfile', '-Command', '$PSVersionTable.PSVersion.Major'], { encoding: 'utf8', timeout: 5000 });
2425
+ if (r.status === 0) env.terminal.powershell = (r.stdout || '').trim().split('\n')[0];
2426
+ }
2427
+ } catch {}
2428
+ // 도ꡬ 버전 감지
2429
+ for (const [tool, cmd] of [['git', 'git --version'], ['npm', 'npm --version'], ['python', 'python --version'], ['python3', 'python3 --version']]) {
2430
+ try {
2431
+ const r = cp.spawnSync(cmd.split(' ')[0], cmd.split(' ').slice(1), { encoding: 'utf8', shell: true, timeout: 3000 });
2432
+ if (r.status === 0) {
2433
+ const v = (r.stdout || r.stderr || '').trim().split('\n')[0];
2434
+ env.tools[tool] = v;
2435
+ }
2436
+ } catch {}
2437
+ }
2438
+ return env;
2439
+ }
2440
+
2441
+ // μ…Έ 슀크립트 (.ps1/.bat/.cmd/.sh) 인코딩 μœ„ν—˜ 감지
2442
+ // μœ„ν—˜ μ‹œκ·Έλ„: ν•œκ΅­μ–΄ (λ˜λŠ” λΉ„-ASCII) 문자 + BOM μ—†μŒ + Windows ν™˜κ²½
2443
+ function _scanShellScriptsEncoding(root) {
2444
+ const result = { scanned: 0, atRisk: [], notes: [] };
2445
+ function walk(dir, depth = 0) {
2446
+ if (depth > 3) return; // λ„ˆλ¬΄ 깊이 λ“€μ–΄κ°€μ§€ μ•ŠμŒ
2447
+ let entries;
2448
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
2449
+ for (const e of entries) {
2450
+ const fp = path.join(dir, e.name);
2451
+ if (e.isDirectory()) {
2452
+ if (/^(node_modules|\.git|__pycache__|venv|\.venv|dist|build)$/.test(e.name)) continue;
2453
+ walk(fp, depth + 1);
2454
+ } else if (e.isFile() && /\.(ps1|bat|cmd|sh)$/i.test(e.name)) {
2455
+ result.scanned++;
2456
+ try {
2457
+ const buf = fs.readFileSync(fp);
2458
+ const hasBOM = buf.length >= 3 && buf[0] === 0xEF && buf[1] === 0xBB && buf[2] === 0xBF;
2459
+ // λΉ„-ASCII (β‰₯ 0x80) λ°”μ΄νŠΈ 쑴재 μ—¬λΆ€ 검사
2460
+ let nonAscii = false;
2461
+ for (let i = 0; i < Math.min(buf.length, 4096); i++) {
2462
+ if (buf[i] >= 0x80) { nonAscii = true; break; }
2463
+ }
2464
+ if (nonAscii && !hasBOM) {
2465
+ result.atRisk.push({
2466
+ file: path.relative(root, fp),
2467
+ ext: path.extname(fp),
2468
+ hasBOM: false,
2469
+ nonAscii: true,
2470
+ risk: 'Windows ν•œκ΅­μ–΄ PowerShell μ—μ„œ CP949 둜 μ˜€μΈμ‹ κ°€λŠ₯ (BOM μΆ”κ°€ ꢌμž₯)'
2471
+ });
2472
+ }
2473
+ } catch {}
2474
+ }
2475
+ }
2476
+ }
2477
+ walk(root);
2478
+ if (result.atRisk.length > 0) {
2479
+ result.notes.push('ν•΄κ²°: 파일 μ‹œμž‘μ— UTF-8 BOM μΆ”κ°€ (EF BB BF) λ˜λŠ” .ps1 첫 쀄에 $OutputEncoding/[Console]::OutputEncoding μ„€μ •');
2480
+ }
2481
+ return result;
2482
+ }
2483
+
2484
+ function envCmd(root, sub) {
2485
+ root = absRoot(root);
2486
+ const isTty = process.stdout && process.stdout.isTTY;
2487
+ const cy = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
2488
+ const gr = s => isTty ? `\x1b[32m${s}\x1b[0m` : s;
2489
+ const yl = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
2490
+ const rd = s => isTty ? `\x1b[31m${s}\x1b[0m` : s;
2491
+ const dm = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
2492
+ if (sub === 'encoding-check') {
2493
+ const result = _scanShellScriptsEncoding(root);
2494
+ if (has('--json')) { log(JSON.stringify(result, null, 2)); return; }
2495
+ log(cy(`# leerness env encoding-check (1.9.241, UR-0014) β€” μ…Έ 슀크립트 인코딩 μœ„ν—˜ 감지`));
2496
+ log('');
2497
+ log(` μŠ€μΊ”: ${gr(String(result.scanned))} 파일 (.ps1/.bat/.cmd/.sh)`);
2498
+ if (result.atRisk.length === 0) {
2499
+ log(gr(` βœ“ 인코딩 μœ„ν—˜ μ—†μŒ`));
2500
+ } else {
2501
+ log(rd(` ⚠ μœ„ν—˜ ${result.atRisk.length}건 (BOM μ—†λŠ” λΉ„-ASCII):`));
2502
+ result.atRisk.slice(0, 10).forEach(r => {
2503
+ log(` β€’ ${r.file}`);
2504
+ log(dm(` ${r.risk}`));
2505
+ });
2506
+ log('');
2507
+ result.notes.forEach(n => log(yl(` πŸ’‘ ${n}`)));
2508
+ }
2509
+ return;
2510
+ }
2511
+ // default: env summary
2512
+ const env = _collectRuntimeEnv();
2513
+ if (has('--json')) { log(JSON.stringify(env, null, 2)); return; }
2514
+ log(cy(`# leerness env (1.9.241, UR-0014) β€” ν™˜κ²½ μ’…ν•©`));
2515
+ log('');
2516
+ log(` πŸ’» OS: ${gr(env.os.platform)} ${env.os.release} (${env.os.arch})`);
2517
+ log(` πŸ“¦ Node: ${gr(env.node.version)}`);
2518
+ log(` 🌐 Locale: LANG=${env.locale.LANG || 'unset'} Β· CP=${env.locale.codepage || 'unknown'}${env.locale.isKoreanWindows ? yl(' (ν•œκ΅­μ–΄ Windows)') : ''}`);
2519
+ log(` πŸ–₯ Hardware: ${env.hardware.cpus} CPUs Β· ${env.hardware.memGB} GB RAM`);
2520
+ log(` πŸ–± Terminal: TTY=${env.terminal.isTTY}${env.terminal.powershell ? ` Β· PowerShell v${env.terminal.powershell}` : ''}`);
2521
+ log(` πŸ”§ Tools:`);
2522
+ for (const [k, v] of Object.entries(env.tools)) {
2523
+ log(` ${v ? gr('βœ“') : dm('βœ—')} ${k}: ${v || dm('not found')}`);
2524
+ }
2525
+ log('');
2526
+ log(dm(` β†’ 인코딩 검사: leerness env encoding-check (μ…Έ 슀크립트 .ps1/.bat μœ„ν—˜ 감지)`));
2527
+ log(dm(` β†’ JSON 좜λ ₯: leerness env --json`));
2528
+ }
2529
+
2379
2530
  // 1.9.231: leerness pulse β€” ν•œ 쀄 μ’…ν•© μš”μ•½ (10 핡심 μ§€ν‘œ)
2380
2531
  // handoff ν—€λ“œλΌμΈμ˜ μΆ•μ•½ 버전 β€” μ‚¬μš©μžκ°€ λΉ λ₯΄κ²Œ μƒνƒœ νŒŒμ•…
2381
2532
  // 응닡 (--json): { version, roundCount, mcpTools, memorySurface, security, health, driftScore, nextMilestone, etaDays, abnormal }
@@ -6390,6 +6541,19 @@ function handoff(root) {
6390
6541
  try {
6391
6542
  result.recentChanges = _computeRecentChanges(root, 5);
6392
6543
  } catch {}
6544
+ // 1.9.240: pyFiles 톡합 (handoff JSON 9번째 톡합 ν•„λ“œ) β€” Python 닀쀑 μ–Έμ–΄ κ°€μ‹œν™” (UR-0013 2단계)
6545
+ try {
6546
+ const pyFiles = _collectPyFiles(root, 200);
6547
+ const analyses = pyFiles.slice(0, 200).map(f => _analyzePyFile(f)).filter(Boolean);
6548
+ result.pyFiles = {
6549
+ total: pyFiles.length,
6550
+ analyzed: analyses.length,
6551
+ totalLOC: analyses.reduce((s, a) => s + a.loc, 0),
6552
+ totalImports: analyses.reduce((s, a) => s + a.imports, 0),
6553
+ totalFuncs: analyses.reduce((s, a) => s + a.funcs, 0),
6554
+ totalClasses: analyses.reduce((s, a) => s + a.classes, 0)
6555
+ };
6556
+ } catch {}
6393
6557
  } catch {}
6394
6558
  try {
6395
6559
  const pwState = _loadPreWakeReport(root);
@@ -6552,6 +6716,13 @@ function handoff(root) {
6552
6716
  parts.push(`🌐 official ${m.total}/${m.cacheTotal} (${ageStr}${status})`);
6553
6717
  }
6554
6718
  } catch {}
6719
+ // 18) 1.9.240: pyFiles ν—€λ“œλΌμΈ β€” Python 닀쀑 μ–Έμ–΄ κ°€μ‹œν™” (UR-0013 2단계, .py 파일 β‰₯ 1)
6720
+ try {
6721
+ const pyFiles = _collectPyFiles(root, 200);
6722
+ if (pyFiles.length > 0) {
6723
+ parts.push(`🐍 py ${pyFiles.length}`);
6724
+ }
6725
+ } catch {}
6555
6726
  // 17) 1.9.226: round-history ν—€λ“œλΌμΈ β€” 자율 λΌμš΄λ“œ μΉ΄μš΄ν„° + λ‹€μŒ λ§ˆμΌμŠ€ν†€ (5 λΌμš΄λ“œ 이상 μ‹œλ§Œ ν‘œμ‹œ)
6556
6727
  // 1.9.230: λ‹€μŒ λ§ˆμΌμŠ€ν†€ ETA (10R 이내 + ETA 7일 이내일 λ•Œ κ°•μ‘°)
6557
6728
  try {
@@ -6723,6 +6894,27 @@ function handoff(root) {
6723
6894
  }
6724
6895
  } catch {}
6725
6896
 
6897
+ // 1.9.241: handoff body μ…Έ 슀크립트 인코딩 μœ„ν—˜ 감지 (μ‚¬μš©μž λͺ…μ‹œ UR-0014)
6898
+ // ν•œκ΅­μ–΄ Windows + .ps1/.bat λΉ„-ASCII + BOM μ—†μŒ β†’ CP949 μ˜€μΈμ‹ μœ„ν—˜
6899
+ try {
6900
+ const scan = _scanShellScriptsEncoding(root);
6901
+ if (scan.atRisk && scan.atRisk.length > 0) {
6902
+ const isTty = process.stdout && process.stdout.isTTY;
6903
+ const rd5 = s => isTty ? `\x1b[31m${s}\x1b[0m` : s;
6904
+ const yl5 = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
6905
+ const dm5 = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
6906
+ log('');
6907
+ log(rd5(`## ⚠ μ…Έ 슀크립트 인코딩 μœ„ν—˜ ${scan.atRisk.length}건 (1.9.241, UR-0014)`));
6908
+ scan.atRisk.slice(0, 3).forEach(r => {
6909
+ log(yl5(` β€’ ${r.file}`));
6910
+ log(dm5(` ${r.risk}`));
6911
+ });
6912
+ if (scan.atRisk.length > 3) log(dm5(` ... +${scan.atRisk.length - 3}건 더`));
6913
+ log(dm5(` β†’ 상세: leerness env encoding`));
6914
+ log(dm5(` β†’ ν•΄κ²°: 파일 μ‹œμž‘μ— UTF-8 BOM (EF BB BF) μΆ”κ°€`));
6915
+ }
6916
+ } catch {}
6917
+
6726
6918
  // 1.9.237: handoff λ³Έλ¬Έ release/* λˆ„μ  κ²½κ³  β€” 50+ branches merged μ‹œ μžλ™ μ•ˆλ‚΄
6727
6919
  // 1.9.220 λΉ„μ •μƒμ’…λ£Œ release-branch-pending μ‹ ν˜Έμ˜ λ³Έλ¬Έ 보강 (handoff ν—€λ“œλΌμΈμ€ β€˜πŸ”Œ λΉ„μ •μƒμ’…λ£Œβ€™ 만 ν‘œμ‹œ)
6728
6920
  try {
@@ -10572,6 +10764,19 @@ function sessionClose(root, opts = {}) {
10572
10764
  try {
10573
10765
  jsonResult.recentChanges = _computeRecentChanges(root, 5);
10574
10766
  } catch {}
10767
+ // 1.9.240: pyFiles 톡합 (session close JSON 9번째 톡합 ν•„λ“œ) β€” UR-0013 2단계
10768
+ try {
10769
+ const pyFiles = _collectPyFiles(root, 200);
10770
+ const analyses = pyFiles.slice(0, 200).map(f => _analyzePyFile(f)).filter(Boolean);
10771
+ jsonResult.pyFiles = {
10772
+ total: pyFiles.length,
10773
+ analyzed: analyses.length,
10774
+ totalLOC: analyses.reduce((s, a) => s + a.loc, 0),
10775
+ totalImports: analyses.reduce((s, a) => s + a.imports, 0),
10776
+ totalFuncs: analyses.reduce((s, a) => s + a.funcs, 0),
10777
+ totalClasses: analyses.reduce((s, a) => s + a.classes, 0)
10778
+ };
10779
+ } catch {}
10575
10780
  } catch {}
10576
10781
  try {
10577
10782
  // 1.9.209: pre-wake-audit μžλ™ μ‹€ν–‰ + μ €μž₯ (sleep μ „ μžλ™ 점검)
@@ -14710,7 +14915,8 @@ function mcpServeCmd(root) {
14710
14915
  { 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' } } } },
14711
14916
  { 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' } } } },
14712
14917
  { 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' } } } },
14713
- { 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'] } }
14918
+ { 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'] } },
14919
+ { name: 'leerness_env_info', description: '1.9.241 (μ‚¬μš©μž λͺ…μ‹œ UR-0014) β€” ν™˜κ²½ μ’…ν•© 정보 회수. OS / μ–Έμ–΄ (LANG, μ½”λ“œνŽ˜μ΄μ§€) / ν•œκ΅­μ–΄ Windows / ν•˜λ“œμ›¨μ–΄ / 터미널 (TTY, PowerShell 버전) / 도ꡬ 버전 (git, npm, python). μ™ΈλΆ€ AI κ°€ ν™˜κ²½ ν˜Έν™˜μ„±μ„ 미리 인지 β†’ 인코딩 였λ₯˜ 예방. 응닡: { os, node, locale, hardware, terminal, tools }. 인자: { path?, encodingCheck? }. encodingCheck: true μ‹œ μ…Έ 슀크립트 (.ps1/.bat/.cmd/.sh) BOM μ—†λŠ” λΉ„-ASCII μœ„ν—˜ 감지', inputSchema: { type: 'object', properties: { path: { type: 'string' }, encodingCheck: { type: 'boolean' } } } }
14714
14920
  ];
14715
14921
 
14716
14922
  function send(obj) {
@@ -14895,6 +15101,10 @@ function mcpServeCmd(root) {
14895
15101
  // 1.9.239 (UR-0013): 자율 λͺ¨λ“œ 톡합 (start/tick/stop)
14896
15102
  cliArgs = ['agent-mode', String(args.sub || 'help'), '--path', targetPath];
14897
15103
  break;
15104
+ case 'leerness_env_info':
15105
+ // 1.9.241 (UR-0014): ν™˜κ²½ μ’…ν•© + 인코딩 μœ„ν—˜ 감지
15106
+ cliArgs = ['env', args.encodingCheck === true ? 'encoding' : 'summary', '--path', targetPath, '--json'];
15107
+ break;
14898
15108
  case 'leerness_release_cleanup':
14899
15109
  // 1.9.236 (1.9.235): local release/* branches 정리
14900
15110
  cliArgs = ['release', 'cleanup', '--json'];
@@ -17125,6 +17335,19 @@ function healthCmd(root) {
17125
17335
  try {
17126
17336
  out.recentChanges = _computeRecentChanges(root, 5);
17127
17337
  } catch { out.recentChanges = { error: 'recentChanges 점검 μ‹€νŒ¨' }; }
17338
+ // 1.9.240: health --json pyFiles 톡합 (3 λͺ…λ Ή 9 ν•„λ“œ β€” UR-0013 2단계)
17339
+ try {
17340
+ const pyFiles = _collectPyFiles(root, 200);
17341
+ const analyses = pyFiles.slice(0, 200).map(f => _analyzePyFile(f)).filter(Boolean);
17342
+ out.pyFiles = {
17343
+ total: pyFiles.length,
17344
+ analyzed: analyses.length,
17345
+ totalLOC: analyses.reduce((s, a) => s + a.loc, 0),
17346
+ totalImports: analyses.reduce((s, a) => s + a.imports, 0),
17347
+ totalFuncs: analyses.reduce((s, a) => s + a.funcs, 0),
17348
+ totalClasses: analyses.reduce((s, a) => s + a.classes, 0)
17349
+ };
17350
+ } catch { out.pyFiles = { error: 'pyFiles 점검 μ‹€νŒ¨' }; }
17128
17351
  // 1.9.163: 5λŠ₯λ ₯ 맀트릭슀 μžλ™ 평가 (1.9.155 sub-agent 점검 β†’ μ½”λ“œ 기반 μžλ™ν™”)
17129
17352
  // 각 λŠ₯λ ₯을 μ½”λ“œ grep 으둜 κ²€μΆœ β†’ 0~100 점수. μ‚¬μš©μžκ°€ λ§€ health 호좜 μ‹œ leerness 자기 평가 확인.
17130
17353
  try {
@@ -18570,6 +18793,10 @@ async function main() {
18570
18793
  if (cmd === 'py-check') return pyCheckCmd(arg('--path', process.cwd()));
18571
18794
  // 1.9.239: leerness agent-mode <start|tick|stop> β€” 자율 λͺ¨λ“œ μ „μš© 톡합 λͺ…λ Ή (μ‚¬μš©μž λͺ…μ‹œ UR-0013)
18572
18795
  if (cmd === 'agent-mode') return agentModeCmd(arg('--path', process.cwd()), args[1]);
18796
+ // 1.9.241: leerness env summary|encoding β€” ν™˜κ²½ μ’…ν•© + μ…Έ 슀크립트 인코딩 μœ„ν—˜ (μ‚¬μš©μž λͺ…μ‹œ UR-0014)
18797
+ // κΈ°μ‘΄ env check/sync/detect (1.9.71) 와 μΆ©λŒν•˜μ§€ μ•ŠμŒ β€” summary/encoding μ‹ κ·œ μ„œλΈŒ
18798
+ if (cmd === 'env' && (args[1] === 'summary' || args[1] === 'encoding' || args[1] === 'encoding-check'))
18799
+ return envCmd(arg('--path', process.cwd()), args[1] === 'summary' ? null : 'encoding-check');
18573
18800
  // 1.9.208: leerness constraints <list|check|add> β€” ν”Œλž«νΌ/API μ œμ•½ 사전 체크 (μ‚¬μš©μž λͺ…μ‹œ)
18574
18801
  if (cmd === 'constraints') return constraintsCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
18575
18802
  // 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.239",
3
+ "version": "1.9.241",
4
4
  "description": "Leerness: λΉ„νŒŒκ΄΄ λ§ˆμ΄κ·Έλ ˆμ΄μ…˜, μžλ™ 버전 κ°μ§€Β·μ—…λ°μ΄νŠΈ, κ³„νš/μ§„ν–‰/ν•Έλ“œμ˜€ν”„ μžλ™ν™”, κ²ŒμœΌλ¦„Β·μ‹œν¬λ¦ΏΒ·μΈμ½”λ”© μžλ™ κ°€λ“œ, Claude Code μŠ¬λž˜μ‹œ 톡합을 κ°–μΆ˜ ν•œκ΅­μ–΄ μš°μ„  AI 개발 ν•˜λ„€μŠ€.",
5
5
  "keywords": [
6
6
  "leerness",