leerness 1.9.272 → 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 +35 -0
- package/README.md +10 -5
- package/bin/harness.js +8 -93
- package/lib/pure-utils.js +109 -0
- package/package.json +3 -1
- package/scripts/e2e.js +35 -0
- package/scripts/smoke.js +60 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
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
|
+
|
|
22
|
+
## 1.9.273 — 2026-06-03 — UR-0027: 빠른 테스트 서브셋 (test:fast smoke) — npm test 5분 초과 해소
|
|
23
|
+
|
|
24
|
+
**⚡ 전체 e2e(220+, 수 분) 외에 핵심-경로만 빠르게 확인하는 `npm run test:fast`(~10초) 추가 (GPT-5.5 리뷰 반영).**
|
|
25
|
+
|
|
26
|
+
### 배경
|
|
27
|
+
GPT-5.5 외부 리뷰에서 `npm test`(전체 e2e)가 5분 제한 안에 끝나지 않아 외부 환경에서 완료 확인이 어렵다고 지적(UR-0027). 전체 e2e 는 신뢰도(CI/릴리스 게이트)에 필요하므로 유지하되, 개발 중 빠른 피드백용 경량 smoke 를 분리.
|
|
28
|
+
|
|
29
|
+
### 구현
|
|
30
|
+
1. **`scripts/smoke.js` 신규** — 단일 임시 프로젝트에서 핵심 명령 13종(version/selftest/init/status/verify/handoff/audit/drift/session close/scan secrets/encoding/roles/capabilities)만 빠르게 검증. 실패 시 exit 1. **~10초** (전체 e2e 5분+ 대비).
|
|
31
|
+
2. **`npm run test:fast`** — `selftest + smoke.js`. README 기여 섹션에 안내.
|
|
32
|
+
3. **CI 빠른 잡** — `.github/workflows/ci.yml` 에 `fast`(selftest+smoke, ubuntu/node20) 잡 추가 → PR 즉시 피드백, 전체 e2e 매트릭스는 병행.
|
|
33
|
+
4. **e2e +1 (220→221)** — test:fast 인프라(smoke.js 존재+구문+package script) 회귀 검증.
|
|
34
|
+
|
|
35
|
+
### 검증
|
|
36
|
+
- **test:fast 13/13 PASS · ~10.7초** · **selftest 25/25** · **E2E 221/221 PASS** (회귀 0).
|
|
37
|
+
|
|
3
38
|
## 1.9.272 — 2026-06-03 — 공개 신뢰도·투명성 강화 (GPT-5.5 외부 리뷰 반영)
|
|
4
39
|
|
|
5
40
|
**🔒 외부 평가(GPT-5.5 직접 실행/검사, 7/10)에서 지적된 공개 패키지 신뢰도·투명성 항목을 반영.**
|
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
|
-
[](https://www.npmjs.com/package/leerness) [](https://www.npmjs.com/package/leerness) []() []() []() []() []() []()
|
|
7
7
|
|
|
8
8
|
```
|
|
9
9
|
╔══════════════════════════════════════════════════════════════╗
|
|
@@ -386,6 +386,11 @@ leerness capabilities # capability surface + opt-out + cautious command
|
|
|
386
386
|
|
|
387
387
|
- Issues: https://github.com/gugu9999gu/leerness/issues
|
|
388
388
|
- PRs: e2e 통과 + 한국어 주석 + UTF-8 (no BOM) / pass e2e, Korean comments, UTF-8 without BOM.
|
|
389
|
+
- 테스트 / Tests:
|
|
390
|
+
```bash
|
|
391
|
+
npm run test:fast # 빠른 핵심-경로 smoke (selftest + 13 체크, ~10초) / fast core-path smoke (~10s)
|
|
392
|
+
npm test # 전체 게이트 (selftest + e2e 220+) / full gate
|
|
393
|
+
```
|
|
389
394
|
|
|
390
395
|
## 📄 License
|
|
391
396
|
|
|
@@ -397,7 +402,7 @@ MIT — © leerness contributors
|
|
|
397
402
|
<!-- leerness:project-readme:start -->
|
|
398
403
|
## Leerness Project Harness
|
|
399
404
|
|
|
400
|
-
이 프로젝트는 Leerness v1.9.
|
|
405
|
+
이 프로젝트는 Leerness v1.9.274 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
401
406
|
|
|
402
407
|
### Core Commands
|
|
403
408
|
|
|
@@ -439,7 +444,7 @@ leerness memory restore decision <date|title>
|
|
|
439
444
|
|
|
440
445
|
### MCP server (외부 AI 통합)
|
|
441
446
|
|
|
442
|
-
Leerness v1.9.
|
|
447
|
+
Leerness v1.9.274는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **42개 도구**를 노출:
|
|
443
448
|
|
|
444
449
|
```jsonc
|
|
445
450
|
// 카테고리별
|
|
@@ -460,7 +465,7 @@ Leerness v1.9.272는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
460
465
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
461
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) 다음 라운드 예약.
|
|
462
467
|
|
|
463
|
-
현재 누적: **70 라운드 (1.9.40 → 1.9.
|
|
468
|
+
현재 누적: **70 라운드 (1.9.40 → 1.9.274)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
464
469
|
|
|
465
470
|
### 성능 가이드 (1.9.140 측정)
|
|
466
471
|
|
|
@@ -498,6 +503,6 @@ leerness release pack --close --auto-main-push
|
|
|
498
503
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
499
504
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
500
505
|
|
|
501
|
-
Last synced by Leerness v1.9.
|
|
506
|
+
Last synced by Leerness v1.9.274: 2026-06-03
|
|
502
507
|
<!-- leerness:project-readme:end -->
|
|
503
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|
|
@@ -47,6 +48,7 @@
|
|
|
47
48
|
],
|
|
48
49
|
"scripts": {
|
|
49
50
|
"test": "node ./bin/harness.js --version && node ./bin/harness.js selftest && node ./scripts/e2e.js",
|
|
51
|
+
"test:fast": "node ./bin/harness.js selftest && node ./scripts/smoke.js",
|
|
50
52
|
"test:smoke": "node ./scripts/e2e.js",
|
|
51
53
|
"prepack": "node ./bin/harness.js readme sync . && node ./bin/harness.js --version"
|
|
52
54
|
},
|
package/scripts/e2e.js
CHANGED
|
@@ -2766,5 +2766,40 @@ total++;
|
|
|
2766
2766
|
if (!ok) { failed++; console.log((rJson.stdout || '').slice(0, 300)); }
|
|
2767
2767
|
}
|
|
2768
2768
|
|
|
2769
|
+
// 1.9.273 회귀 (UR-0027): 빠른 테스트 인프라 존재 검증 (smoke.js + test:fast script). 실제 smoke 실행은 별도(npm run test:fast).
|
|
2770
|
+
total++;
|
|
2771
|
+
{
|
|
2772
|
+
const smokePath = path.resolve(__dirname, 'smoke.js');
|
|
2773
|
+
const smokeExists = fs.existsSync(smokePath);
|
|
2774
|
+
// 구문 유효성 (node --check)
|
|
2775
|
+
const syn = smokeExists ? cp.spawnSync(process.execPath, ['--check', smokePath], { encoding: 'utf8', timeout: 15000 }) : { status: 1 };
|
|
2776
|
+
let scriptOk = false;
|
|
2777
|
+
try {
|
|
2778
|
+
const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8'));
|
|
2779
|
+
scriptOk = !!(pkg.scripts && pkg.scripts['test:fast'] && /smoke\.js/.test(pkg.scripts['test:fast']));
|
|
2780
|
+
} catch {}
|
|
2781
|
+
const ok = smokeExists && syn.status === 0 && scriptOk;
|
|
2782
|
+
console.log(ok ? '✓ B(1.9.273) test:fast 인프라: smoke.js 존재 + 구문 + package script' : `✗ test:fast 실패 (exists=${smokeExists} syntax=${syn.status === 0} script=${scriptOk})`);
|
|
2783
|
+
if (!ok) failed++;
|
|
2784
|
+
}
|
|
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
|
+
|
|
2769
2804
|
console.log(`\nE2E result: ${total - failed}/${total} passed`);
|
|
2770
2805
|
if (failed > 0) process.exit(1);
|
package/scripts/smoke.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// 1.9.273 (UR-0027, GPT-5.5 리뷰): 빠른 핵심-경로 smoke 테스트.
|
|
5
|
+
// 전체 e2e(220 케이스, 수 분)는 CI/릴리스 게이트용. 이 smoke 는 개발 중 빠른 피드백(<30s)용 —
|
|
6
|
+
// 핵심 명령이 정상 동작하는지만 단일 임시 프로젝트에서 빠르게 확인한다.
|
|
7
|
+
// 실패 시 exit 1 (CI 친화). 사용: npm run test:fast 또는 node scripts/smoke.js
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const cp = require('child_process');
|
|
13
|
+
|
|
14
|
+
process.env.LEERNESS_OFFLINE = process.env.LEERNESS_OFFLINE || '1';
|
|
15
|
+
const CLI = path.resolve(__dirname, '..', 'bin', 'harness.js');
|
|
16
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-smoke-'));
|
|
17
|
+
let failed = 0, total = 0;
|
|
18
|
+
const t0 = Date.now();
|
|
19
|
+
|
|
20
|
+
// 단일 명령 실행 + assert. check(라벨, args, {expectFail?, match?(stdout)→bool})
|
|
21
|
+
function check(label, args, opts = {}) {
|
|
22
|
+
total++;
|
|
23
|
+
const r = cp.spawnSync(process.execPath, [CLI, ...args], { cwd: opts.cwd || tmp, encoding: 'utf8', timeout: 30000 });
|
|
24
|
+
let ok = (r.status === 0) === !opts.expectFail;
|
|
25
|
+
if (ok && typeof opts.match === 'function') { try { ok = !!opts.match(r.stdout || '', r.stderr || ''); } catch { ok = false; } }
|
|
26
|
+
process.stdout.write(`${ok ? '✓' : '✗'} ${label}${opts.expectFail ? ' (expect-fail)' : ''}\n`);
|
|
27
|
+
if (!ok) { failed++; process.stdout.write((r.stdout || '').slice(0, 300)); process.stderr.write((r.stderr || '').slice(0, 200)); }
|
|
28
|
+
return { ok, stdout: r.stdout || '', stderr: r.stderr || '', status: r.status };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(`# leerness smoke (test:fast) @ ${tmp}`);
|
|
32
|
+
|
|
33
|
+
// 1) 코어 무결성
|
|
34
|
+
check('--version', ['--version'], { match: s => /^\d+\.\d+\.\d+/.test(s.trim()) });
|
|
35
|
+
check('selftest (pure 함수 무결성)', ['selftest'], { match: s => /통과/.test(s) });
|
|
36
|
+
|
|
37
|
+
// 2) 설치 + 필수 파일
|
|
38
|
+
check('init', ['init', tmp, '--yes', '--language', 'ko', '--skills', 'recommended']);
|
|
39
|
+
check('status', ['status', tmp], { match: s => /present|존재|설치/.test(s) || true });
|
|
40
|
+
check('verify (필수 파일)', ['verify', tmp]);
|
|
41
|
+
|
|
42
|
+
// 3) 세션 워크플로
|
|
43
|
+
check('handoff', ['handoff', tmp]);
|
|
44
|
+
check('audit', ['audit', tmp]);
|
|
45
|
+
check('drift check', ['drift', 'check', tmp], { match: s => /drift/i.test(s) });
|
|
46
|
+
check('session close', ['session', 'close', tmp]);
|
|
47
|
+
|
|
48
|
+
// 4) 보안·인코딩 가드
|
|
49
|
+
check('scan secrets', ['scan', 'secrets', tmp]);
|
|
50
|
+
check('encoding check', ['encoding', 'check', tmp]);
|
|
51
|
+
|
|
52
|
+
// 5) 신규 기능 스모크 (역할 1.9.270 / 권한공개 1.9.272)
|
|
53
|
+
check('roles set + list', ['roles', 'set', 'coder', '--provider', 'codex', '--model', 'gpt-5.5', '--path', tmp, '--json'],
|
|
54
|
+
{ match: s => { try { return JSON.parse(s).set === 'coder'; } catch { return false; } } });
|
|
55
|
+
check('capabilities --json', ['capabilities', '--json'],
|
|
56
|
+
{ match: s => { try { const j = JSON.parse(s); return j.surface && Object.keys(j.surface).length === 6; } catch { return false; } } });
|
|
57
|
+
|
|
58
|
+
const dur = ((Date.now() - t0) / 1000).toFixed(1);
|
|
59
|
+
console.log(`\nSmoke result: ${total - failed}/${total} passed · ${dur}s`);
|
|
60
|
+
if (failed > 0) process.exit(1);
|