leerness 1.9.240 β†’ 1.9.242

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,89 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.242 β€” 2026-05-26
4
+
5
+ **🌐 UR-0014 2단계: env encoding --apply (BOM μžλ™ μΆ”κ°€) + JSON 10 ν•„λ“œ envInfo + drift --auto-fix 톡합.**
6
+
7
+ ### μ‚¬μš©μž λͺ…μ‹œ (UR-0014) β€” 2단계 μžλ™ 회볡
8
+
9
+ 1.9.241 μ—μ„œ κ°μ§€λ§Œ μ œκ³΅ν–ˆλ˜ μœ„ν—˜μ„ 1.9.242 κ°€ **μ‹€μ œ μžλ™ μˆ˜μ •**으둜 ν™•μž₯:
10
+
11
+ ### 1. `leerness env encoding --apply` μžλ™ BOM μΆ”κ°€
12
+ - `.ps1` / `.bat` / `.cmd` / `.sh` 의 μœ„ν—˜ νŒŒμΌμ— UTF-8 BOM (`EF BB BF`) μžλ™ prepend
13
+ - `--auto-fix-bom` 별칭 동일 λ™μž‘
14
+ - κΈ°λ³Έ dry-run μœ μ§€ (--apply λͺ…μ‹œ μ‹œμ—λ§Œ λ³€κ²½) β€” μ•ˆμ „ 원칙
15
+ - 적용 κ²°κ³Ό `result.applied` λ°°μ—΄λ‘œ λ…ΈμΆœ (file/action: utf8-bom-added | failed)
16
+
17
+ ### 2. handoff/session close/health --json **10 ν•„λ“œ** envInfo 톡합
18
+ - JSON 톡합 맀트릭슀 9 β†’ **10 ν•„λ“œ** μ§„ν™” (3 λͺ…λ Ή Γ— 10 = 30 톡합 포인트)
19
+ - `envInfo`: os / isKoreanWindows / codepage / nodeVersion / shellScriptsScanned / encodingRiskCount / encodingRiskFiles[0..5]
20
+ - μ™ΈλΆ€ AI κ°€ ν™˜κ²½ μ»¨ν…μŠ€νŠΈ + 인코딩 μœ„ν—˜μ„ 단일 ν˜ΈμΆœμ—μ„œ 회수
21
+
22
+ ### 3. drift check --auto-fix env encoding BOM 톡합
23
+ - 1.9.82 λ³΄μ•ˆ 회볡 + 1.9.225 delivered + 1.9.236 release cleanup μžλ™ 회볡 νŒ¨ν„΄ ν™•μž₯
24
+ - `drift check --auto-fix` μ‹œ μ…Έ 슀크립트 μœ„ν—˜ μžλ™ BOM μΆ”κ°€
25
+ - log: `🌐 --auto-fix ν™œμ„± (1.9.242) β€” μ…Έ 슀크립트 인코딩 μœ„ν—˜ N건 BOM μžλ™ μΆ”κ°€ 쀑...`
26
+
27
+ ### 4. λˆ„μ  νšŒκ·€ (1.9.207~241) β€” λͺ¨λ‘ μœ μ§€
28
+ - MCP 69 도ꡬ μœ μ§€ (1.9.242 λŠ” μ‹ κ·œ CLI μ˜΅μ…˜ + 톡합, 도ꡬ 증가 X)
29
+ - handoff JSON: userRequestsAudit + preWakeAudit + idempotencyAudit + abnormalShutdown + deliveredRequests + roundHistory + milestones + recentChanges + pyFiles + **envInfo** (10)
30
+
31
+ ### 5. stress-v187 β€” **22/22 PASS Β· 100%**
32
+ - 1.9.242 (8): VERSION + env --apply (격리) + --auto-fix-bom 별칭 + handoff/session close/health 10ν•„λ“œ envInfo + drift --auto-fix BOM 톡합 + envInfo μŠ€ν‚€λ§ˆ
33
+ - μ„±λŠ₯ (2): cold_start avg 371ms Β· env --apply 372ms
34
+ - λˆ„μ  νšŒκ·€ (12): 1.9.207~241
35
+
36
+ ### 6. μžλ™ release (104 λΌμš΄λ“œ main-push streak Β· 65 λΌμš΄λ“œ npm publish streak)
37
+
38
+ 🌐 **UR-0014 μ™„μ „ μžλ™ν™”** β€” 감지 (1.9.241) β†’ μžλ™ μˆ˜μ • (1.9.242)
39
+
40
+ ---
41
+
42
+ ## 1.9.241 β€” 2026-05-24
43
+
44
+ **🌐 leerness env + ν•œκ΅­μ–΄ PowerShell 인코딩 μœ„ν—˜ 사전 감지 (μ‚¬μš©μž λͺ…μ‹œ UR-0014).**
45
+
46
+ ### μ‚¬μš©μž λͺ…μ‹œ (UR-0014)
47
+ > *"leerness μ„€μΉ˜ ν”„λ‘œμ νŠΈμ—μ„œ PowerShell/터미널 ν•œκ΅­μ–΄ 인코딩 였λ₯˜ 사전 감지 β€” OS/μ–Έμ–΄μ„€μ •/ν•˜λ“œμ›¨μ–΄/터미널/SW 버전 데이터 μˆ™μ§€λ‘œ 개발/ν…ŒμŠ€νŠΈ/디버깅 μ‹œ ν™˜κ²½ ν˜Έν™˜μ„± 보μž₯"*
48
+
49
+ λ°°κ²½: μ‚¬μš©μžκ°€ `3d-map-maker` ν”„λ‘œμ νŠΈμ—μ„œ `npm run setup` β†’ `setup.ps1` νŒŒμ‹± 였λ₯˜ (ν•œκ΅­μ–΄ + BOM μ—†λŠ” UTF-8 β†’ PowerShell CP949 μ˜€μΈμ‹)
50
+
51
+ ### 1. `leerness env summary` μƒˆ λͺ…λ Ή (1.9.145 envCheck 와 λ³„κ°œ)
52
+ - ν™˜κ²½ μ’…ν•© 좜λ ₯: OS / Node / Locale / ν•œκ΅­μ–΄ Windows 감지 / Hardware / Terminal (PowerShell 버전) / Tools (git/npm/python)
53
+ - `chcp` 좜λ ₯으둜 μ½”λ“œνŽ˜μ΄μ§€ μžλ™ 감지 (949 = ν•œκ΅­μ–΄)
54
+ - `--json` μ˜΅μ…˜
55
+ - μ‹ κ·œ helper: `_collectRuntimeEnv()` (κΈ°μ‘΄ `_detectEnvironment` 와 λͺ…λͺ… 뢄리)
56
+
57
+ ### 2. `leerness env encoding` μƒˆ λͺ…λ Ή β€” μ…Έ 슀크립트 인코딩 μœ„ν—˜ 감지
58
+ - `.ps1` / `.bat` / `.cmd` / `.sh` μžλ™ μŠ€μΊ”
59
+ - BOM μ—†λŠ” λΉ„-ASCII λ°”μ΄νŠΈ κ²€μΆœ (μž¬κ·€, 4096 byte 이내)
60
+ - μœ„ν—˜ μ‹œ: `Windows ν•œκ΅­μ–΄ PowerShell μ—μ„œ CP949 둜 μ˜€μΈμ‹ κ°€λŠ₯ (BOM μΆ”κ°€ ꢌμž₯)`
61
+ - ν•΄κ²°μ±… μžλ™ μ œμ‹œ (UTF-8 BOM μΆ”κ°€ λ˜λŠ” `$OutputEncoding` μ„€μ •)
62
+
63
+ ### 3. handoff body μžλ™ λ…ΈμΆœ
64
+ - `## ⚠ μ…Έ 슀크립트 인코딩 μœ„ν—˜ N건 (1.9.241, UR-0014)` λ³Έλ¬Έ μ„Ήμ…˜
65
+ - top 3 μœ„ν—˜ 파일 + ν•΄κ²° μ•ˆλ‚΄ (BOM μΆ”κ°€)
66
+ - `leerness env encoding` κ°€μ΄λ“œ μžλ™
67
+
68
+ ### 4. MCP 69번째 도ꡬ β€” `leerness_env_info`
69
+ - μ™ΈλΆ€ AI κ°€ ν™˜κ²½ ν˜Έν™˜μ„± 사전 인지 β†’ 인코딩 였λ₯˜ 예방
70
+ - `encodingCheck: true` μ˜΅μ…˜ μ‹œ μ…Έ 슀크립트 μŠ€μΊ”
71
+ - MCP 68 β†’ **69** (+1)
72
+
73
+ ### 5. λˆ„μ  νšŒκ·€ (1.9.207~240) β€” λͺ¨λ‘ μœ μ§€
74
+ - κΈ°μ‘΄ `_detectEnvironment(root)` (1.9.145) 와 좩돌 없이 곡쑴
75
+
76
+ ### 6. stress-v186 β€” 16/16 PASS
77
+ - 1.9.241 (7): VERSION + env summary + JSON + encoding μœ„ν—˜ 감지 + μ•ˆμ „ μ‹œ μœ„ν—˜ 0 + handoff body + MCP 69
78
+ - μ„±λŠ₯ (1): cold_start avg 598ms
79
+ - λˆ„μ  νšŒκ·€ (8): 1.9.207~240
80
+
81
+ ### 7. μžλ™ release (103 λΌμš΄λ“œ main-push streak Β· 64 λΌμš΄λ“œ npm publish streak)
82
+
83
+ 🌐 **ν•œκ΅­μ–΄ Windows ν™˜κ²½ ν˜Έν™˜μ„± 보강** β€” μ‚¬μš©μž 보고 .ps1 νŒŒμ‹± 였λ₯˜ 같은 μΌ€μ΄μŠ€ 사전 감지
84
+
85
+ ---
86
+
3
87
  ## 1.9.240 β€” 2026-05-24
4
88
 
5
89
  **🐍 UR-0013 2단계: handoff/session close/health --json pyFiles (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.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)]()
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.242-green)]() [![tests](https://img.shields.io/badge/e2e-217%2F217-success)]() [![stress](https://img.shields.io/badge/stress--v187-22%2F22-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-69-brightgreen)]() [![cli](https://img.shields.io/badge/CLI_commands-56-brightgreen)]() [![json-fields](https://img.shields.io/badge/JSON_톡합-10_ν•„λ“œ-blueviolet)]() [![rounds](https://img.shields.io/badge/autonomous--rounds-198-blueviolet)]() [![main-push](https://img.shields.io/badge/πŸŽ‰_main--push_streak-104_rounds-success)]() [![npm-streak](https://img.shields.io/badge/πŸŽ‰_npm--publish-65_streak-success)]() [![encoding](https://img.shields.io/badge/🌐_env_encoding-BOM_μžλ™μΆ”κ°€-blueviolet)]() [![perf](https://img.shields.io/badge/cold_start-371ms-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.240 AI Agent Reliability Harness + Sandbox β•‘
15
+ β•‘ v1.9.242 AI Agent Reliability Harness + Sandbox β•‘
16
16
  β•‘ verify Β· remember Β· orchestrate Β· audit Β· sandbox Β· drift β•‘
17
- β•‘ 🐍 JSON 9 ν•„λ“œ (3 λͺ…λ Ή Γ— 9 = 27 톡합 β€” UR-0013 2단계) β•‘
17
+ β•‘ 🌐 env encoding --apply BOM μžλ™ μΆ”κ°€ (UR-0014 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.240';
10
+ const VERSION = '1.9.242';
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,181 @@ 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
+ // 1.9.242: --apply μ˜΅μ…˜ β€” μœ„ν—˜ νŒŒμΌμ— UTF-8 BOM μžλ™ μΆ”κ°€ (μ‚¬μš©μž λͺ…μ‹œ UR-0014 2단계)
2495
+ const apply = has('--apply') || has('--auto-fix-bom');
2496
+ if (apply) result.applied = [];
2497
+ if (apply && result.atRisk.length > 0) {
2498
+ for (const r of result.atRisk) {
2499
+ try {
2500
+ const fullPath = path.join(root, r.file);
2501
+ const orig = fs.readFileSync(fullPath);
2502
+ const bom = Buffer.from([0xEF, 0xBB, 0xBF]);
2503
+ const fixed = Buffer.concat([bom, orig]);
2504
+ fs.writeFileSync(fullPath, fixed);
2505
+ result.applied.push({ file: r.file, action: 'utf8-bom-added' });
2506
+ } catch (e) {
2507
+ result.applied.push({ file: r.file, action: 'failed', error: e.message });
2508
+ }
2509
+ }
2510
+ }
2511
+ if (has('--json')) { log(JSON.stringify(result, null, 2)); return; }
2512
+ log(cy(`# leerness env encoding-check (1.9.241/242, UR-0014) β€” μ…Έ 슀크립트 인코딩 μœ„ν—˜ 감지`));
2513
+ log('');
2514
+ log(` μŠ€μΊ”: ${gr(String(result.scanned))} 파일 (.ps1/.bat/.cmd/.sh)`);
2515
+ if (result.atRisk.length === 0) {
2516
+ log(gr(` βœ“ 인코딩 μœ„ν—˜ μ—†μŒ`));
2517
+ } else {
2518
+ log(rd(` ⚠ μœ„ν—˜ ${result.atRisk.length}건 (BOM μ—†λŠ” λΉ„-ASCII):`));
2519
+ result.atRisk.slice(0, 10).forEach(r => {
2520
+ log(` β€’ ${r.file}`);
2521
+ log(dm(` ${r.risk}`));
2522
+ });
2523
+ log('');
2524
+ if (apply) {
2525
+ const okCnt = (result.applied || []).filter(a => a.action === 'utf8-bom-added').length;
2526
+ const failCnt = (result.applied || []).filter(a => a.action === 'failed').length;
2527
+ log(gr(` βœ“ UTF-8 BOM μΆ”κ°€ ${okCnt}건${failCnt > 0 ? ` (μ‹€νŒ¨ ${failCnt}건)` : ''} (1.9.242 --apply)`));
2528
+ } else {
2529
+ result.notes.forEach(n => log(yl(` πŸ’‘ ${n}`)));
2530
+ log(dm(` β†’ μžλ™ μˆ˜μ •: leerness env encoding --apply (BOM μžλ™ μΆ”κ°€)`));
2531
+ }
2532
+ }
2533
+ return;
2534
+ }
2535
+ // default: env summary
2536
+ const env = _collectRuntimeEnv();
2537
+ if (has('--json')) { log(JSON.stringify(env, null, 2)); return; }
2538
+ log(cy(`# leerness env (1.9.241, UR-0014) β€” ν™˜κ²½ μ’…ν•©`));
2539
+ log('');
2540
+ log(` πŸ’» OS: ${gr(env.os.platform)} ${env.os.release} (${env.os.arch})`);
2541
+ log(` πŸ“¦ Node: ${gr(env.node.version)}`);
2542
+ log(` 🌐 Locale: LANG=${env.locale.LANG || 'unset'} Β· CP=${env.locale.codepage || 'unknown'}${env.locale.isKoreanWindows ? yl(' (ν•œκ΅­μ–΄ Windows)') : ''}`);
2543
+ log(` πŸ–₯ Hardware: ${env.hardware.cpus} CPUs Β· ${env.hardware.memGB} GB RAM`);
2544
+ log(` πŸ–± Terminal: TTY=${env.terminal.isTTY}${env.terminal.powershell ? ` Β· PowerShell v${env.terminal.powershell}` : ''}`);
2545
+ log(` πŸ”§ Tools:`);
2546
+ for (const [k, v] of Object.entries(env.tools)) {
2547
+ log(` ${v ? gr('βœ“') : dm('βœ—')} ${k}: ${v || dm('not found')}`);
2548
+ }
2549
+ log('');
2550
+ log(dm(` β†’ 인코딩 검사: leerness env encoding-check (μ…Έ 슀크립트 .ps1/.bat μœ„ν—˜ 감지)`));
2551
+ log(dm(` β†’ JSON 좜λ ₯: leerness env --json`));
2552
+ }
2553
+
2379
2554
  // 1.9.231: leerness pulse β€” ν•œ 쀄 μ’…ν•© μš”μ•½ (10 핡심 μ§€ν‘œ)
2380
2555
  // handoff ν—€λ“œλΌμΈμ˜ μΆ•μ•½ 버전 β€” μ‚¬μš©μžκ°€ λΉ λ₯΄κ²Œ μƒνƒœ νŒŒμ•…
2381
2556
  // 응닡 (--json): { version, roundCount, mcpTools, memorySurface, security, health, driftScore, nextMilestone, etaDays, abnormal }
@@ -6403,6 +6578,20 @@ function handoff(root) {
6403
6578
  totalClasses: analyses.reduce((s, a) => s + a.classes, 0)
6404
6579
  };
6405
6580
  } catch {}
6581
+ // 1.9.242: envInfo 톡합 (handoff JSON 10번째 톡합 ν•„λ“œ) β€” ν™˜κ²½ + 인코딩 μœ„ν—˜ (UR-0014 2단계)
6582
+ try {
6583
+ const runtimeEnv = _collectRuntimeEnv();
6584
+ const encScan = _scanShellScriptsEncoding(root);
6585
+ result.envInfo = {
6586
+ os: runtimeEnv.os.platform,
6587
+ isKoreanWindows: runtimeEnv.locale.isKoreanWindows || false,
6588
+ codepage: runtimeEnv.locale.codepage || null,
6589
+ nodeVersion: runtimeEnv.node.version,
6590
+ shellScriptsScanned: encScan.scanned,
6591
+ encodingRiskCount: encScan.atRisk.length,
6592
+ encodingRiskFiles: encScan.atRisk.slice(0, 5).map(r => r.file)
6593
+ };
6594
+ } catch {}
6406
6595
  } catch {}
6407
6596
  try {
6408
6597
  const pwState = _loadPreWakeReport(root);
@@ -6743,6 +6932,27 @@ function handoff(root) {
6743
6932
  }
6744
6933
  } catch {}
6745
6934
 
6935
+ // 1.9.241: handoff body μ…Έ 슀크립트 인코딩 μœ„ν—˜ 감지 (μ‚¬μš©μž λͺ…μ‹œ UR-0014)
6936
+ // ν•œκ΅­μ–΄ Windows + .ps1/.bat λΉ„-ASCII + BOM μ—†μŒ β†’ CP949 μ˜€μΈμ‹ μœ„ν—˜
6937
+ try {
6938
+ const scan = _scanShellScriptsEncoding(root);
6939
+ if (scan.atRisk && scan.atRisk.length > 0) {
6940
+ const isTty = process.stdout && process.stdout.isTTY;
6941
+ const rd5 = s => isTty ? `\x1b[31m${s}\x1b[0m` : s;
6942
+ const yl5 = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
6943
+ const dm5 = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
6944
+ log('');
6945
+ log(rd5(`## ⚠ μ…Έ 슀크립트 인코딩 μœ„ν—˜ ${scan.atRisk.length}건 (1.9.241, UR-0014)`));
6946
+ scan.atRisk.slice(0, 3).forEach(r => {
6947
+ log(yl5(` β€’ ${r.file}`));
6948
+ log(dm5(` ${r.risk}`));
6949
+ });
6950
+ if (scan.atRisk.length > 3) log(dm5(` ... +${scan.atRisk.length - 3}건 더`));
6951
+ log(dm5(` β†’ 상세: leerness env encoding`));
6952
+ log(dm5(` β†’ ν•΄κ²°: 파일 μ‹œμž‘μ— UTF-8 BOM (EF BB BF) μΆ”κ°€`));
6953
+ }
6954
+ } catch {}
6955
+
6746
6956
  // 1.9.237: handoff λ³Έλ¬Έ release/* λˆ„μ  κ²½κ³  β€” 50+ branches merged μ‹œ μžλ™ μ•ˆλ‚΄
6747
6957
  // 1.9.220 λΉ„μ •μƒμ’…λ£Œ release-branch-pending μ‹ ν˜Έμ˜ λ³Έλ¬Έ 보강 (handoff ν—€λ“œλΌμΈμ€ β€˜πŸ”Œ λΉ„μ •μƒμ’…λ£Œβ€™ 만 ν‘œμ‹œ)
6748
6958
  try {
@@ -10605,6 +10815,20 @@ function sessionClose(root, opts = {}) {
10605
10815
  totalClasses: analyses.reduce((s, a) => s + a.classes, 0)
10606
10816
  };
10607
10817
  } catch {}
10818
+ // 1.9.242: envInfo 톡합 (session close JSON 10번째 톡합 ν•„λ“œ) β€” UR-0014 2단계
10819
+ try {
10820
+ const runtimeEnv = _collectRuntimeEnv();
10821
+ const encScan = _scanShellScriptsEncoding(root);
10822
+ jsonResult.envInfo = {
10823
+ os: runtimeEnv.os.platform,
10824
+ isKoreanWindows: runtimeEnv.locale.isKoreanWindows || false,
10825
+ codepage: runtimeEnv.locale.codepage || null,
10826
+ nodeVersion: runtimeEnv.node.version,
10827
+ shellScriptsScanned: encScan.scanned,
10828
+ encodingRiskCount: encScan.atRisk.length,
10829
+ encodingRiskFiles: encScan.atRisk.slice(0, 5).map(r => r.file)
10830
+ };
10831
+ } catch {}
10608
10832
  } catch {}
10609
10833
  try {
10610
10834
  // 1.9.209: pre-wake-audit μžλ™ μ‹€ν–‰ + μ €μž₯ (sleep μ „ μžλ™ 점검)
@@ -13696,6 +13920,31 @@ function driftCheckCmd(root, opts = {}) {
13696
13920
  log(`⚠ auto-fix λ³΄μ•ˆ 회볡 였λ₯˜: ${e.message}`);
13697
13921
  }
13698
13922
  }
13923
+ // 1.9.242: drift check --auto-fix 에 env encoding BOM μžλ™ μΆ”κ°€ 톡합 (μ‚¬μš©μž λͺ…μ‹œ UR-0014 2단계)
13924
+ // 1.9.82 νŒ¨ν„΄ ν™•μž₯ β€” drift 회볡 μ‹œ μ…Έ 슀크립트 인코딩 μœ„ν—˜λ„ μžλ™ ν•΄κ²°
13925
+ if (autoFix) {
13926
+ try {
13927
+ const encScan = _scanShellScriptsEncoding(root);
13928
+ if (encScan.atRisk && encScan.atRisk.length > 0) {
13929
+ log('');
13930
+ log(`🌐 --auto-fix ν™œμ„± (1.9.242) β€” μ…Έ 슀크립트 인코딩 μœ„ν—˜ ${encScan.atRisk.length}건 BOM μžλ™ μΆ”κ°€ 쀑...`);
13931
+ let ok = 0;
13932
+ for (const r of encScan.atRisk) {
13933
+ try {
13934
+ const fullPath = path.join(root, r.file);
13935
+ const orig = fs.readFileSync(fullPath);
13936
+ const bom = Buffer.from([0xEF, 0xBB, 0xBF]);
13937
+ const fixed = Buffer.concat([bom, orig]);
13938
+ fs.writeFileSync(fullPath, fixed);
13939
+ ok++;
13940
+ } catch {}
13941
+ }
13942
+ log(`βœ“ UTF-8 BOM μΆ”κ°€ ${ok}/${encScan.atRisk.length}건 (1.9.242 UR-0014)`);
13943
+ }
13944
+ } catch (e) {
13945
+ log(`⚠ env encoding auto-fix 였λ₯˜ (1.9.242): ${e.message}`);
13946
+ }
13947
+ }
13699
13948
  // 1.9.225: drift check --auto-fix 에 delivered νŒ¨ν„΄ μžλ™ 적용 톡합 (1.9.223/224 μ‹œμŠ€ν…œ 회수)
13700
13949
  // μ‚¬μš©μž μš”μ²­μ— "κ΅¬ν˜„ μ™„λ£Œ" νŒ¨ν„΄μ΄ λˆ„μ λ˜λ©΄ κ°€μ§œ λ―Έλ‹΅ μ‹ ν˜Έκ°€ drift score λ₯Ό κ°€μ€‘μ‹œν‚¬ 수 있음 β†’ μžλ™ 정리.
13701
13950
  // 1.9.82 audit --fix νŒ¨ν„΄κ³Ό 동일: --auto-fix μ‹œ μ¦‰μ‹œ 적용, 적용 ν›„ μž¬κ²€μ‚¬.
@@ -14743,7 +14992,8 @@ function mcpServeCmd(root) {
14743
14992
  { 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' } } } },
14744
14993
  { 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
14994
  { 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'] } }
14995
+ { 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'] } },
14996
+ { 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' } } } }
14747
14997
  ];
14748
14998
 
14749
14999
  function send(obj) {
@@ -14928,6 +15178,10 @@ function mcpServeCmd(root) {
14928
15178
  // 1.9.239 (UR-0013): 자율 λͺ¨λ“œ 톡합 (start/tick/stop)
14929
15179
  cliArgs = ['agent-mode', String(args.sub || 'help'), '--path', targetPath];
14930
15180
  break;
15181
+ case 'leerness_env_info':
15182
+ // 1.9.241 (UR-0014): ν™˜κ²½ μ’…ν•© + 인코딩 μœ„ν—˜ 감지
15183
+ cliArgs = ['env', args.encodingCheck === true ? 'encoding' : 'summary', '--path', targetPath, '--json'];
15184
+ break;
14931
15185
  case 'leerness_release_cleanup':
14932
15186
  // 1.9.236 (1.9.235): local release/* branches 정리
14933
15187
  cliArgs = ['release', 'cleanup', '--json'];
@@ -17171,6 +17425,20 @@ function healthCmd(root) {
17171
17425
  totalClasses: analyses.reduce((s, a) => s + a.classes, 0)
17172
17426
  };
17173
17427
  } catch { out.pyFiles = { error: 'pyFiles 점검 μ‹€νŒ¨' }; }
17428
+ // 1.9.242: health --json envInfo 톡합 (3 λͺ…λ Ή 10 ν•„λ“œ β€” UR-0014 2단계)
17429
+ try {
17430
+ const runtimeEnv = _collectRuntimeEnv();
17431
+ const encScan = _scanShellScriptsEncoding(root);
17432
+ out.envInfo = {
17433
+ os: runtimeEnv.os.platform,
17434
+ isKoreanWindows: runtimeEnv.locale.isKoreanWindows || false,
17435
+ codepage: runtimeEnv.locale.codepage || null,
17436
+ nodeVersion: runtimeEnv.node.version,
17437
+ shellScriptsScanned: encScan.scanned,
17438
+ encodingRiskCount: encScan.atRisk.length,
17439
+ encodingRiskFiles: encScan.atRisk.slice(0, 5).map(r => r.file)
17440
+ };
17441
+ } catch { out.envInfo = { error: 'envInfo 점검 μ‹€νŒ¨' }; }
17174
17442
  // 1.9.163: 5λŠ₯λ ₯ 맀트릭슀 μžλ™ 평가 (1.9.155 sub-agent 점검 β†’ μ½”λ“œ 기반 μžλ™ν™”)
17175
17443
  // 각 λŠ₯λ ₯을 μ½”λ“œ grep 으둜 κ²€μΆœ β†’ 0~100 점수. μ‚¬μš©μžκ°€ λ§€ health 호좜 μ‹œ leerness 자기 평가 확인.
17176
17444
  try {
@@ -18616,6 +18884,10 @@ async function main() {
18616
18884
  if (cmd === 'py-check') return pyCheckCmd(arg('--path', process.cwd()));
18617
18885
  // 1.9.239: leerness agent-mode <start|tick|stop> β€” 자율 λͺ¨λ“œ μ „μš© 톡합 λͺ…λ Ή (μ‚¬μš©μž λͺ…μ‹œ UR-0013)
18618
18886
  if (cmd === 'agent-mode') return agentModeCmd(arg('--path', process.cwd()), args[1]);
18887
+ // 1.9.241: leerness env summary|encoding β€” ν™˜κ²½ μ’…ν•© + μ…Έ 슀크립트 인코딩 μœ„ν—˜ (μ‚¬μš©μž λͺ…μ‹œ UR-0014)
18888
+ // κΈ°μ‘΄ env check/sync/detect (1.9.71) 와 μΆ©λŒν•˜μ§€ μ•ŠμŒ β€” summary/encoding μ‹ κ·œ μ„œλΈŒ
18889
+ if (cmd === 'env' && (args[1] === 'summary' || args[1] === 'encoding' || args[1] === 'encoding-check'))
18890
+ return envCmd(arg('--path', process.cwd()), args[1] === 'summary' ? null : 'encoding-check');
18619
18891
  // 1.9.208: leerness constraints <list|check|add> β€” ν”Œλž«νΌ/API μ œμ•½ 사전 체크 (μ‚¬μš©μž λͺ…μ‹œ)
18620
18892
  if (cmd === 'constraints') return constraintsCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
18621
18893
  // 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.240",
3
+ "version": "1.9.242",
4
4
  "description": "Leerness: λΉ„νŒŒκ΄΄ λ§ˆμ΄κ·Έλ ˆμ΄μ…˜, μžλ™ 버전 κ°μ§€Β·μ—…λ°μ΄νŠΈ, κ³„νš/μ§„ν–‰/ν•Έλ“œμ˜€ν”„ μžλ™ν™”, κ²ŒμœΌλ¦„Β·μ‹œν¬λ¦ΏΒ·μΈμ½”λ”© μžλ™ κ°€λ“œ, Claude Code μŠ¬λž˜μ‹œ 톡합을 κ°–μΆ˜ ν•œκ΅­μ–΄ μš°μ„  AI 개발 ν•˜λ„€μŠ€.",
5
5
  "keywords": [
6
6
  "leerness",