leerness 1.9.273 → 1.9.274

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,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.274 — 2026-06-03 — UR-0025 1단계: bin/harness.js 모듈 분리 시작 (lib/ 순수 유틸 추출)
4
+
5
+ **🧩 단일 대형 파일(1.2MB) 모듈 분리의 비파괴 1단계 — `lib/` 모듈화 패턴 확립 + 순수 유틸 7종 추출 (GPT-5.5 리뷰).**
6
+
7
+ ### 배경
8
+ GPT-5.5 리뷰: 거의 모든 기능이 `bin/harness.js` 한 파일(~20k줄)에 있어 유지보수성·리뷰 가능성이 낮음(UR-0025). 한 번에 전부 분리하면 고위험이므로, **점진적·비파괴**로 진행: 먼저 require 기반 `lib/` 모듈화 패턴을 확립하고, 부작용 0인 순수 함수부터 추출. selftest 가 추출 함수 전부를 검증하므로 회귀를 즉시 포착.
9
+
10
+ ### 구현
11
+ 1. **`lib/pure-utils.js` 신규** — 순수 함수 7종 추출: `_isSecretKey` / `compareVer` / `parseHarnessVersion` / `_classifyCJK` / `_riskLabel` / `_detectSystemLang` / `_parseSlashFromHelp`. harness 내부 상태·타 함수 의존 0.
12
+ 2. **harness.js require 재연결** — 상단에서 `require('../lib/pure-utils')` 로 동일 이름 바인딩, 인라인 정의 제거. `module.exports` 재노출 유지(단위 테스트 호환).
13
+ 3. **`package.json files` 에 `lib` 추가** — npm tarball 포함 확인(`npm pack --dry-run`: `lib/pure-utils.js` 4.6kB 포함). 미포함 시 require 실패 → 검증으로 차단.
14
+ 4. **e2e +1 (221→222)** — lib 모듈 존재 + 7종 export + 동작 + files 포함 회귀 검증.
15
+
16
+ ### 다음 단계 (UR-0025 계속)
17
+ - 2단계+: 모델 catalog/roles/슬래시 레지스트리 등 응집도 높은 클러스터를 순차 추출 (매 단계 selftest+e2e 녹색 유지).
18
+
19
+ ### 검증
20
+ - **selftest 25/25 PASS** · **E2E 222/222 PASS** (회귀 0) · `npm pack` tarball 에 lib 포함 확인. 동작 동일(비파괴).
21
+
3
22
  ## 1.9.273 — 2026-06-03 — UR-0027: 빠른 테스트 서브셋 (test:fast smoke) — npm test 5분 초과 해소
4
23
 
5
24
  **⚡ 전체 e2e(220+, 수 분) 외에 핵심-경로만 빠르게 확인하는 `npm run test:fast`(~10초) 추가 (GPT-5.5 리뷰 반영).**
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > **AI 코딩 에이전트의 거짓 완료·중복·망각·충돌을 막아주는 검수·기억·협업 CLI 하네스.**
4
4
  > **A CLI harness that stops AI coding agents from faking completion, duplicating work, forgetting context, and colliding.**
5
5
 
6
- [![npm](https://img.shields.io/badge/npm-leerness-blue)](https://www.npmjs.com/package/leerness) [![version](https://img.shields.io/badge/version-1.9.273-green)]() [![tests](https://img.shields.io/badge/e2e-221%2F221-success)]() [![selftest](https://img.shields.io/badge/selftest-24%2F24-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-75-brightgreen)]() [![providers](https://img.shields.io/badge/AI_providers-6-brightgreen)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
6
+ [![npm](https://img.shields.io/badge/npm-leerness-blue)](https://www.npmjs.com/package/leerness) [![version](https://img.shields.io/badge/version-1.9.274-green)]() [![tests](https://img.shields.io/badge/e2e-222%2F222-success)]() [![selftest](https://img.shields.io/badge/selftest-24%2F24-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-75-brightgreen)]() [![providers](https://img.shields.io/badge/AI_providers-6-brightgreen)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
7
7
 
8
8
  ```
9
9
  ╔══════════════════════════════════════════════════════════════╗
@@ -402,7 +402,7 @@ MIT — © leerness contributors
402
402
  <!-- leerness:project-readme:start -->
403
403
  ## Leerness Project Harness
404
404
 
405
- 이 프로젝트는 Leerness v1.9.273 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
405
+ 이 프로젝트는 Leerness v1.9.274 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
406
406
 
407
407
  ### Core Commands
408
408
 
@@ -444,7 +444,7 @@ leerness memory restore decision <date|title>
444
444
 
445
445
  ### MCP server (외부 AI 통합)
446
446
 
447
- Leerness v1.9.273는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **42개 도구**를 노출:
447
+ Leerness v1.9.274는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **42개 도구**를 노출:
448
448
 
449
449
  ```jsonc
450
450
  // 카테고리별
@@ -465,7 +465,7 @@ Leerness v1.9.273는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
465
465
  `<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
466
466
  1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) stress-v* 신규 작성 + 누적 회귀 → 4) e2e 219/219 → 5) npm pack + git tag + GitHub release → 6) main 자동 push (1.9.140+) → 7) session close → 8) 다음 라운드 예약.
467
467
 
468
- 현재 누적: **70 라운드 (1.9.40 → 1.9.273)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
468
+ 현재 누적: **70 라운드 (1.9.40 → 1.9.274)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
469
469
 
470
470
  ### 성능 가이드 (1.9.140 측정)
471
471
 
@@ -503,6 +503,6 @@ leerness release pack --close --auto-main-push
503
503
  - `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
504
504
  - `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
505
505
 
506
- Last synced by Leerness v1.9.273: 2026-06-03
506
+ Last synced by Leerness v1.9.274: 2026-06-03
507
507
  <!-- leerness:project-readme:end -->
508
508
 
package/bin/harness.js CHANGED
@@ -6,8 +6,10 @@ const path = require('path');
6
6
  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
+ // 1.9.274 (UR-0025 1단계): 순수 유틸 함수 모듈 분리 (require-based, 비파괴). selftest 7종이 동작 검증.
10
+ const { _isSecretKey, compareVer, parseHarnessVersion, _classifyCJK, _riskLabel, _detectSystemLang, _parseSlashFromHelp } = require('../lib/pure-utils');
9
11
 
10
- const VERSION = '1.9.273';
12
+ const VERSION = '1.9.274';
11
13
 
12
14
  // 1.9.184: DEP0190 (child_process shell: true) deprecation warning 억제 (사용자 명시).
13
15
  // leerness 는 cross-platform PATH resolution 을 위해 shell: true 를 의도적으로 사용 (claude.cmd / ollama.cmd 등 Windows .cmd 처리).
@@ -228,22 +230,7 @@ function detectProjectName(root) { try { const pkg = JSON.parse(read(path.join(r
228
230
  // 1.9.269 (UR-0022): OS 시스템 언어 감지 — 신규 init(빈 디렉토리, 프로젝트 콘텐츠 없음)에서 OS locale 로 설치 언어 판별.
229
231
  // 우선순위: POSIX 환경변수(LC_ALL>LC_CTYPE>LANG>LANGUAGE) > Intl ICU resolvedOptions().locale(Windows 등 LANG 미설정 시) > null.
230
232
  // 순수 함수(부작용 0) — env 주입 가능(selftest). 판별 불가 시 null 반환 → 호출부가 en 폴백.
231
- function _detectSystemLang(env) {
232
- env = env || process.env;
233
- const raw = String(env.LC_ALL || env.LC_CTYPE || env.LANG || env.LANGUAGE || '').toLowerCase();
234
- if (raw && raw !== 'c' && raw !== 'posix') {
235
- if (/(^|[^a-z])ko([_\-.]|$)|korean|[_-]kr([_\-.]|$)/.test(raw)) return 'ko';
236
- if (/(^|[^a-z])en([_\-.]|$)|english|[_-](us|gb)([_\-.]|$)/.test(raw)) return 'en';
237
- }
238
- // POSIX env 없거나 미매칭 → Node ICU locale (Windows 는 LANG 보통 미설정 → 여기서 OS 언어 회수)
239
- try {
240
- const loc = (Intl.DateTimeFormat().resolvedOptions().locale || '').toLowerCase();
241
- const primary = loc.split('-')[0];
242
- if (primary === 'ko') return 'ko';
243
- if (primary === 'en') return 'en';
244
- } catch {}
245
- return null;
246
- }
233
+ // _detectSystemLang → lib/pure-utils.js 로 이동 (1.9.274 UR-0025 1단계)
247
234
  function detectLanguageValue(root, value = 'auto') {
248
235
  const v = String(value || 'auto').toLowerCase();
249
236
  if (v === 'ko' || v === 'en') return v;
@@ -2598,33 +2585,7 @@ function _terminalEncodingNotice(opts = {}) {
2598
2585
  // 1.9.243: CJK 분류 — Korean/Japanese/Chinese 비-ASCII 패턴 통계 (UR-0014 3단계)
2599
2586
  // 1.9.257: _scanShellScriptsEncoding 내부 중첩 → 모듈 스코프로 추출 (순수 함수 — 단위 테스트 가능).
2600
2587
  // UTF-8 3-byte 시퀀스 시작 바이트로 CJK 언어 분류 (인코딩 오인식 위험 판단용).
2601
- function _classifyCJK(buf, len) {
2602
- let korean = 0, japanese = 0, chinese = 0, other = 0;
2603
- for (let i = 0; i < Math.min(buf.length, len); i++) {
2604
- const b = buf[i];
2605
- if (b < 0x80) continue;
2606
- // Korean Hangul Syllables U+AC00-D7AF → 3-byte: EA B0 80 ~ ED 9F BF
2607
- if (b >= 0xEA && b <= 0xED) korean++;
2608
- // Japanese Hiragana/Katakana U+3040-30FF → 3-byte: E3 81 80 ~ E3 83 BF
2609
- else if (b === 0xE3) japanese++;
2610
- // CJK Unified Ideographs U+4E00-9FFF → 3-byte: E4 B8 80 ~ E9 BF BF
2611
- else if (b >= 0xE4 && b <= 0xE9) chinese++;
2612
- else other++;
2613
- }
2614
- return { korean, japanese, chinese, other };
2615
- }
2616
- function _riskLabel(cjk) {
2617
- if (cjk.korean >= cjk.japanese && cjk.korean >= cjk.chinese && cjk.korean > 0) {
2618
- return { type: 'korean', risk: 'Windows 한국어 PowerShell 에서 CP949 로 오인식 가능 (BOM 추가 권장)' };
2619
- }
2620
- if (cjk.japanese > cjk.korean && cjk.japanese >= cjk.chinese) {
2621
- return { type: 'japanese', risk: 'Windows 일본어 PowerShell 에서 CP932 (Shift-JIS) 로 오인식 가능 (BOM 추가 권장)' };
2622
- }
2623
- if (cjk.chinese > 0) {
2624
- return { type: 'chinese', risk: 'Windows 중국어 PowerShell 에서 CP936 (GBK) 로 오인식 가능 (BOM 추가 권장)' };
2625
- }
2626
- return { type: 'non-ascii', risk: 'Windows 비-ASCII 셸 스크립트 — BOM 없는 UTF-8 인코딩 오인식 가능 (BOM 추가 권장)' };
2627
- }
2588
+ // _classifyCJK, _riskLabel → lib/pure-utils.js 로 이동 (1.9.274 UR-0025 1단계)
2628
2589
 
2629
2590
  // 셸 스크립트 (.ps1/.bat/.cmd/.sh) 인코딩 위험 감지
2630
2591
  // 위험 시그널: 한국어 (또는 비-ASCII) 문자 + BOM 없음 + Windows 환경
@@ -9470,9 +9431,7 @@ const _LEERNESS_NONSECRET_KEYS = new Set([
9470
9431
  'LEERNESS_SKILL_AUTO_PRESETS'
9471
9432
  ]);
9472
9433
  // 시크릿은 절대 .harness/leerness-config.json 으로 옮기지 않음 (TOKEN 패턴 자동 차단)
9473
- function _isSecretKey(k) {
9474
- return /TOKEN|SECRET|PASSWORD|API_KEY|PRIVATE/i.test(k);
9475
- }
9434
+ // _isSecretKey → lib/pure-utils.js 로 이동 (1.9.274 UR-0025 1단계)
9476
9435
  function _leernessConfigPath(root) { return path.join(absRoot(root || process.cwd()), '.harness', 'leerness-config.json'); }
9477
9436
  function _loadLeernessConfig(root) {
9478
9437
  const f = _leernessConfigPath(root);
@@ -10273,30 +10232,7 @@ function _agentSlashHint(root, agentId) {
10273
10232
  // 1.9.267 (UR-0021 3단계): CLI `--help` 출력에서 슬래시 명령/하위명령을 best-effort 파싱.
10274
10233
  // 순수 함수 (부작용 0, selftest 가능). invoke='slash' → `/cmd desc` 패턴, invoke='subcommand' → 들여쓰기 `cmd desc` 패턴.
10275
10234
  // CLI 플래그(--foo)·옵션은 의도적으로 제외. 검출 0건이면 큐레이션 빌트인을 그대로 유지(파괴적 덮어쓰기 방지).
10276
- function _parseSlashFromHelp(text, invoke = 'slash') {
10277
- const out = [];
10278
- const seen = new Set();
10279
- const lines = String(text || '').split(/\r?\n/);
10280
- for (const raw of lines) {
10281
- const ln = raw.replace(/\x1b\[[0-9;]*m/g, ''); // ANSI 색상 제거
10282
- if (invoke === 'subcommand') {
10283
- // 들여쓰기된 하위명령: " suggest 설명" — 슬래시/대시 시작 제외, 2칸+ 공백 구분
10284
- const m = ln.match(/^\s{2,}([a-z][a-z0-9][\w-]*)\s{2,}(\S.*)$/);
10285
- if (m && !/^--/.test(m[1])) {
10286
- const cmd = m[1];
10287
- if (!seen.has(cmd) && cmd.length <= 24) { seen.add(cmd); out.push({ cmd, desc: m[2].trim().slice(0, 80) }); }
10288
- }
10289
- continue;
10290
- }
10291
- // 슬래시 명령: "/help 설명" 또는 " /help - 설명"
10292
- const m = ln.match(/^\s*(\/[a-zA-Z][\w-]*)(?:\s+[-–:]?\s*(.*))?$/);
10293
- if (m) {
10294
- const cmd = m[1];
10295
- if (!seen.has(cmd) && cmd.length <= 24) { seen.add(cmd); out.push({ cmd, desc: (m[2] || '').trim().slice(0, 80) }); }
10296
- }
10297
- }
10298
- return out;
10299
- }
10235
+ // _parseSlashFromHelp → lib/pure-utils.js 로 이동 (1.9.274 UR-0025 1단계)
10300
10236
  // 1.9.267 (UR-0021 3단계): 설치된 CLI 의 `--help` 를 spawn 해 슬래시 명령을 probe (best-effort, offline-first).
10301
10237
  // extDef: EXTERNAL_AGENTS 항목 (bin/versionArgs 보유). copilot 처럼 subcommand 인 경우 versionArgs 의 base(예: ['copilot']) + '--help'.
10302
10238
  // 실패/0건이면 ok:false + reason — 호출부가 큐레이션 빌트인 유지. 타임아웃 5s (행 방지).
@@ -15134,28 +15070,7 @@ function guideCmd(root, target) {
15134
15070
  }
15135
15071
 
15136
15072
  // ===== Auto update =====
15137
- function compareVer(a, b) {
15138
- const sa = String(a || '0').split('.').map(n => parseInt(n || '0', 10));
15139
- const sb = String(b || '0').split('.').map(n => parseInt(n || '0', 10));
15140
- for (let i = 0; i < 3; i++) {
15141
- const x = sa[i] || 0, y = sb[i] || 0;
15142
- if (x > y) return 1;
15143
- if (x < y) return -1;
15144
- }
15145
- return 0;
15146
- }
15147
- function parseHarnessVersion(text) {
15148
- const t = String(text || '').trim();
15149
- // canonical: "1.9.0", legacy plus: "leerness@1.8.0+plus@1.0.1", legacy bare: "1.8.0", legacy "leerness@1.8.0"
15150
- const plus = t.match(/plus@(\d+\.\d+\.\d+)/);
15151
- const baseAt = t.match(/leerness@(\d+\.\d+\.\d+)/);
15152
- const bare = t.match(/^(\d+\.\d+\.\d+)\s*$/);
15153
- return {
15154
- plus: plus ? plus[1] : null,
15155
- base: baseAt ? baseAt[1] : (bare ? bare[1] : null),
15156
- raw: t || '(not installed)'
15157
- };
15158
- }
15073
+ // compareVer, parseHarnessVersion → lib/pure-utils.js 로 이동 (1.9.274 UR-0025 1단계)
15159
15074
  function updateCachePath(root) { return path.join(root, '.harness/cache/update-check.json'); }
15160
15075
  function readUpdateCache(root) { try { const p = updateCachePath(root); if (!exists(p)) return null; return JSON.parse(read(p)); } catch { return null; } }
15161
15076
  function writeUpdateCache(root, obj) { writeUtf8(updateCachePath(root), JSON.stringify({ at: Date.now(), ...obj }, null, 2) + '\n'); }
@@ -0,0 +1,109 @@
1
+ 'use strict';
2
+
3
+ // 1.9.274 (UR-0025 1단계, GPT-5.5 리뷰): bin/harness.js 단일 대형 파일 모듈 분리 — 점진적·비파괴 시작.
4
+ // 여기에는 harness 내부 상태/다른 함수에 의존하지 않는 "순수 함수"만 추출한다 (부작용 0, 단위 테스트 대상).
5
+ // harness.js 는 이 모듈을 require 해 동일 이름으로 사용한다. 동작 동일 — selftest 가 7종 모두 검증.
6
+
7
+ // 보안: 환경변수 키가 시크릿(TOKEN/SECRET/PASSWORD/API_KEY/PRIVATE)인지 판별.
8
+ function _isSecretKey(k) {
9
+ return /TOKEN|SECRET|PASSWORD|API_KEY|PRIVATE/i.test(k);
10
+ }
11
+
12
+ // semver 비교: a>b → 1, a<b → -1, 같음 → 0. (누락 파트/null 안전)
13
+ function compareVer(a, b) {
14
+ const sa = String(a || '0').split('.').map(n => parseInt(n || '0', 10));
15
+ const sb = String(b || '0').split('.').map(n => parseInt(n || '0', 10));
16
+ for (let i = 0; i < 3; i++) {
17
+ const x = sa[i] || 0, y = sb[i] || 0;
18
+ if (x > y) return 1;
19
+ if (x < y) return -1;
20
+ }
21
+ return 0;
22
+ }
23
+
24
+ // harness 버전 문자열 파싱: canonical "1.9.0" / legacy plus "leerness@1.8.0+plus@1.0.1" / "leerness@1.8.0".
25
+ function parseHarnessVersion(text) {
26
+ const t = String(text || '').trim();
27
+ const plus = t.match(/plus@(\d+\.\d+\.\d+)/);
28
+ const baseAt = t.match(/leerness@(\d+\.\d+\.\d+)/);
29
+ const bare = t.match(/^(\d+\.\d+\.\d+)\s*$/);
30
+ return {
31
+ plus: plus ? plus[1] : null,
32
+ base: baseAt ? baseAt[1] : (bare ? bare[1] : null),
33
+ raw: t || '(not installed)'
34
+ };
35
+ }
36
+
37
+ // UTF-8 바이트열의 CJK 분류 (한국어/일본어/중국어/기타) — 인코딩 오인식 위험 감지용.
38
+ function _classifyCJK(buf, len) {
39
+ let korean = 0, japanese = 0, chinese = 0, other = 0;
40
+ for (let i = 0; i < Math.min(buf.length, len); i++) {
41
+ const b = buf[i];
42
+ if (b < 0x80) continue;
43
+ if (b >= 0xEA && b <= 0xED) korean++;
44
+ else if (b === 0xE3) japanese++;
45
+ else if (b >= 0xE4 && b <= 0xE9) chinese++;
46
+ else other++;
47
+ }
48
+ return { korean, japanese, chinese, other };
49
+ }
50
+
51
+ // CJK 분류 결과 → 위험 라벨 (Windows 코드페이지 오인식 안내).
52
+ function _riskLabel(cjk) {
53
+ if (cjk.korean >= cjk.japanese && cjk.korean >= cjk.chinese && cjk.korean > 0) {
54
+ return { type: 'korean', risk: 'Windows 한국어 PowerShell 에서 CP949 로 오인식 가능 (BOM 추가 권장)' };
55
+ }
56
+ if (cjk.japanese > cjk.korean && cjk.japanese >= cjk.chinese) {
57
+ return { type: 'japanese', risk: 'Windows 일본어 PowerShell 에서 CP932 (Shift-JIS) 로 오인식 가능 (BOM 추가 권장)' };
58
+ }
59
+ if (cjk.chinese > 0) {
60
+ return { type: 'chinese', risk: 'Windows 중국어 PowerShell 에서 CP936 (GBK) 로 오인식 가능 (BOM 추가 권장)' };
61
+ }
62
+ return { type: 'non-ascii', risk: 'Windows 비-ASCII 셸 스크립트 — BOM 없는 UTF-8 인코딩 오인식 가능 (BOM 추가 권장)' };
63
+ }
64
+
65
+ // OS 시스템 언어 감지 (UR-0022): POSIX env > Intl ICU locale > null.
66
+ function _detectSystemLang(env) {
67
+ env = env || process.env;
68
+ const raw = String(env.LC_ALL || env.LC_CTYPE || env.LANG || env.LANGUAGE || '').toLowerCase();
69
+ if (raw && raw !== 'c' && raw !== 'posix') {
70
+ if (/(^|[^a-z])ko([_\-.]|$)|korean|[_-]kr([_\-.]|$)/.test(raw)) return 'ko';
71
+ if (/(^|[^a-z])en([_\-.]|$)|english|[_-](us|gb)([_\-.]|$)/.test(raw)) return 'en';
72
+ }
73
+ try {
74
+ const loc = (Intl.DateTimeFormat().resolvedOptions().locale || '').toLowerCase();
75
+ const primary = loc.split('-')[0];
76
+ if (primary === 'ko') return 'ko';
77
+ if (primary === 'en') return 'en';
78
+ } catch {}
79
+ return null;
80
+ }
81
+
82
+ // CLI `--help` 출력에서 슬래시 명령/하위명령 best-effort 파싱 (UR-0021 3단계). 순수 문자열 처리.
83
+ function _parseSlashFromHelp(text, invoke = 'slash') {
84
+ const out = [];
85
+ const seen = new Set();
86
+ const lines = String(text || '').split(/\r?\n/);
87
+ for (const raw of lines) {
88
+ const ln = raw.replace(/\x1b\[[0-9;]*m/g, ''); // ANSI 색상 제거
89
+ if (invoke === 'subcommand') {
90
+ const m = ln.match(/^\s{2,}([a-z][a-z0-9][\w-]*)\s{2,}(\S.*)$/);
91
+ if (m && !/^--/.test(m[1])) {
92
+ const cmd = m[1];
93
+ if (!seen.has(cmd) && cmd.length <= 24) { seen.add(cmd); out.push({ cmd, desc: m[2].trim().slice(0, 80) }); }
94
+ }
95
+ continue;
96
+ }
97
+ const m = ln.match(/^\s*(\/[a-zA-Z][\w-]*)(?:\s+[-–:]?\s*(.*))?$/);
98
+ if (m) {
99
+ const cmd = m[1];
100
+ if (!seen.has(cmd) && cmd.length <= 24) { seen.add(cmd); out.push({ cmd, desc: (m[2] || '').trim().slice(0, 80) }); }
101
+ }
102
+ }
103
+ return out;
104
+ }
105
+
106
+ module.exports = {
107
+ _isSecretKey, compareVer, parseHarnessVersion,
108
+ _classifyCJK, _riskLabel, _detectSystemLang, _parseSlashFromHelp
109
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.273",
3
+ "version": "1.9.274",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
@@ -38,6 +38,7 @@
38
38
  },
39
39
  "files": [
40
40
  "bin",
41
+ "lib",
41
42
  "scripts",
42
43
  "docs",
43
44
  "README.md",
package/scripts/e2e.js CHANGED
@@ -2783,5 +2783,23 @@ total++;
2783
2783
  if (!ok) failed++;
2784
2784
  }
2785
2785
 
2786
+ // 1.9.274 회귀 (UR-0025 1단계): lib/ 모듈 분리 — pure-utils.js 존재 + require + 7종 export + 패키지 files 포함.
2787
+ total++;
2788
+ {
2789
+ const libPath = path.resolve(__dirname, '..', 'lib', 'pure-utils.js');
2790
+ const libExists = fs.existsSync(libPath);
2791
+ let exportsOk = false;
2792
+ try {
2793
+ const m = require(libPath);
2794
+ exportsOk = ['_isSecretKey','compareVer','parseHarnessVersion','_classifyCJK','_riskLabel','_detectSystemLang','_parseSlashFromHelp']
2795
+ .every(n => typeof m[n] === 'function') && m.compareVer('1.9.1','1.9.0') === 1 && m._detectSystemLang({ LANG: 'ko_KR.UTF-8' }) === 'ko';
2796
+ } catch {}
2797
+ let filesOk = false;
2798
+ try { const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8')); filesOk = Array.isArray(pkg.files) && pkg.files.includes('lib'); } catch {}
2799
+ const ok = libExists && exportsOk && filesOk;
2800
+ console.log(ok ? '✓ B(1.9.274) lib 모듈 분리: pure-utils 7종 export + 동작 + files 포함' : `✗ lib 분리 실패 (exists=${libExists} exports=${exportsOk} files=${filesOk})`);
2801
+ if (!ok) failed++;
2802
+ }
2803
+
2786
2804
  console.log(`\nE2E result: ${total - failed}/${total} passed`);
2787
2805
  if (failed > 0) process.exit(1);