leerness 1.9.407 → 1.9.408
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 +14 -0
- package/README.md +5 -5
- package/bin/harness.js +2 -1
- package/lib/pure-utils.js +3 -1
- package/package.json +1 -1
- package/scripts/e2e.js +23 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.408 — 2026-06-07 — CRLF/CR SKILL.md frontmatter 파싱 복구 (8번째 버그헌트, UR-0112)
|
|
4
|
+
|
|
5
|
+
**🪟 Windows 호환 — CRLF/CR 줄바꿈의 SKILL.md frontmatter 가 통째로 소실돼 `skill install` 이 "name 필수" 로 실패하던 것 수정.**
|
|
6
|
+
|
|
7
|
+
### 배경 (2차 버그헌트 encoding 차원 · Windows 직격)
|
|
8
|
+
`_parseSkillMd` 의 frontmatter 값 정규식 `(.+)$` 의 `.` 은 CR(`\r`)을 매칭하지 못함 → `name: x\r` 라인이 통째로 매칭 실패 → CRLF SKILL.md(Windows/Notepad 기본)의 meta 가 `{}` 로 소실 → `skill install` 이 frontmatter 의 name 을 못 찾아 "name 필수" 로 실패. 직접 재현: LF 는 정상, CRLF 는 meta={}.
|
|
9
|
+
|
|
10
|
+
### 구현
|
|
11
|
+
- `_parseSkillMd` 상단에서 BOM strip 후 `replace(/\r\n?/g, '\n')` 로 CRLF/CR → LF 정규화. 모든 줄바꿈 스타일(LF/CRLF/CR/BOM+CRLF) 동일 파싱.
|
|
12
|
+
|
|
13
|
+
### 검증 (회귀 0)
|
|
14
|
+
- **selftest 153→154 PASS** (LF/CRLF/CR/BOM+CRLF 4종 meta 파싱).
|
|
15
|
+
- **E2E 346→347 PASS** (CRLF SKILL.md `skill install` exit0 + skill list 에 등록 확인).
|
|
16
|
+
|
|
3
17
|
## 1.9.407 — 2026-06-07 — MCP feature_link 권한경계 + NaN --limit 가드 (8번째 버그헌트, UR-0111)
|
|
4
18
|
|
|
5
19
|
**🛡 MCP 권한경계 — read-only로 잘못 선언된 그래프-변경 도구 수정 + NaN --limit 가 결과를 조용히 숨기던 것 차단.**
|
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
|
╔══════════════════════════════════════════════════════════════╗
|
|
@@ -471,7 +471,7 @@ MIT — © leerness contributors
|
|
|
471
471
|
<!-- leerness:project-readme:start -->
|
|
472
472
|
## Leerness Project Harness
|
|
473
473
|
|
|
474
|
-
이 프로젝트는 Leerness v1.9.
|
|
474
|
+
이 프로젝트는 Leerness v1.9.408 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
475
475
|
|
|
476
476
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
477
477
|
|
|
@@ -525,7 +525,7 @@ leerness memory restore decision <date|title>
|
|
|
525
525
|
|
|
526
526
|
### MCP server (외부 AI 통합)
|
|
527
527
|
|
|
528
|
-
Leerness v1.9.
|
|
528
|
+
Leerness v1.9.408는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
|
|
529
529
|
|
|
530
530
|
```jsonc
|
|
531
531
|
// 카테고리별
|
|
@@ -546,7 +546,7 @@ Leerness v1.9.407는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
546
546
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
547
547
|
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) 다음 라운드 예약.
|
|
548
548
|
|
|
549
|
-
현재 누적: **70 라운드 (1.9.40 → 1.9.
|
|
549
|
+
현재 누적: **70 라운드 (1.9.40 → 1.9.408)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
550
550
|
|
|
551
551
|
### 성능 가이드 (1.9.140 측정)
|
|
552
552
|
|
|
@@ -584,6 +584,6 @@ leerness release pack --close --auto-main-push
|
|
|
584
584
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
585
585
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
586
586
|
|
|
587
|
-
Last synced by Leerness v1.9.
|
|
587
|
+
Last synced by Leerness v1.9.408: 2026-06-06
|
|
588
588
|
<!-- leerness:project-readme:end -->
|
|
589
589
|
|
package/bin/harness.js
CHANGED
|
@@ -31,7 +31,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
|
|
|
31
31
|
// 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
|
|
32
32
|
const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST, _DEFAULT_PLATFORM_CONSTRAINTS, _DEFAULT_DOMAIN_CATALOG, _LSP_LANG_PATTERNS, OPTIMISM_PATTERNS, BUILT_IN_PERSONAS, STRINGS, BUILTIN_CATALOG, ROADMAP_STATUS_LABEL, ROADMAP_STATUS_COLOR, SECRET_PATTERNS, MERGE_OVERWRITE_FILES, MINIMAL_SKIP_KEYS, REQUIRED_WORKSPACE_FILES, KEYWORD_STOPWORDS, SKILL_CATALOG_PRESETS } = require('../lib/catalogs'); // 1.9.344/368/369 (UR-0025): catalog 분리 (MERGE_OVERWRITE_FILES/MINIMAL_SKIP_KEYS 포함)
|
|
33
33
|
|
|
34
|
-
const VERSION = '1.9.
|
|
34
|
+
const VERSION = '1.9.408';
|
|
35
35
|
|
|
36
36
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
37
37
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -3012,6 +3012,7 @@ function _selfTestCases() {
|
|
|
3012
3012
|
{ name: '8번째 버그헌트 회귀수정 (UR-0109): 긴 서술형 placeholder FP 차단(마커 우선) + 실키 FN 유지 (1.9.405)', run: () => { const m = require('../lib/pure-utils'); const ph = m._isPlaceholderSecret; const fpFixed = ph('your-super-secret-api-key-example-value') === true && ph('this-is-just-an-example-placeholder-value') === true && ph('example-api-key-do-not-use-1234567890') === true; const fnKept = ph('sk-EXAMPLEab12cd34ef56gh78ij90kl') === false && ph('sk-proj-realKEYexample9988776655') === false; const realKept = ph('a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6') === false; const shortPh = ph('your-api-key-here') === true && ph('changeme') === true; return fpFixed && fnKept && realKept && shortPh; } },
|
|
3013
3013
|
{ name: '8번째 버그헌트 (UR-0110): rule/decision/lesson add 동시쓰기 _withLock 직렬화 (UR-0043 갭 메움) (1.9.406)', run: () => { const src = read(__filename); const L = '_withLock('; const ruleLock = src.includes(L + 'rulesPath' + '(root), () =>'); const decLock = src.includes(L + 'decisionsJsonPath' + '(root), () =>'); const lesLock = src.includes(L + 'lessonsJsonPath' + '(root), () =>'); return ruleLock && decLock && lesLock; } },
|
|
3014
3014
|
{ name: '8번째 버그헌트 (UR-0111): MCP feature_link safe-write tier(권한경계) + _parseLimit NaN/음수 가드 (1.9.407)', run: () => { const m = require('../lib/pure-utils'); const pl = m._parseLimit('abc', 10) === 10 && m._parseLimit('-5', 10) === 10 && m._parseLimit('0', 10) === 10 && m._parseLimit('3', 10) === 3 && m._parseLimit('7.9', 10) === 7; const tools = require('../lib/mcp-tools'); const arr = Array.isArray(tools) ? tools : (tools.MCP_TOOLS || []); const fl = arr.find(x => x.name === 'leerness_feature_link'); const tierOk = !!fl && fl.requiredTier === 'safe-write'; return pl && tierOk; } },
|
|
3015
|
+
{ name: '8번째 버그헌트 (UR-0112): _parseSkillMd CRLF/CR 줄바꿈 정규화 (Windows SKILL.md meta 소실 차단) (1.9.408)', run: () => { const m = require('../lib/pure-utils'); const lf = m._parseSkillMd('---\nname: s\ndescription: d\n---\nbody'); const crlf = m._parseSkillMd('---\r\nname: s\r\ndescription: d\r\n---\r\nbody'); const cr = m._parseSkillMd('---\rname: s\rdescription: d\r---\rbody'); const bom = m._parseSkillMd('---\r\nname: s\r\n---\r\nbody'); return lf.meta.name === 's' && crlf.meta.name === 's' && crlf.meta.description === 'd' && cr.meta.name === 's' && bom.meta.name === 's'; } },
|
|
3015
3016
|
{ name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
|
|
3016
3017
|
];
|
|
3017
3018
|
}
|
package/lib/pure-utils.js
CHANGED
|
@@ -432,7 +432,9 @@ function _roadmapTokenStyles(designTokens, cssVariables) {
|
|
|
432
432
|
|
|
433
433
|
// 1.9.347 (UR-0025 심층): SKILL.md frontmatter 파서 — { meta, body }, BOM-aware (Windows Notepad 호환). 순수.
|
|
434
434
|
function _parseSkillMd(text) {
|
|
435
|
-
|
|
435
|
+
// 1.9.408 (8번째 버그헌트, UR-0112): BOM strip + CRLF/CR→LF 정규화.
|
|
436
|
+
// 기존 버그: frontmatter 값 정규식 (.+)$ 의 '.'은 CR(\r)을 매칭 못 해 'name: x\r' 라인이 통째로 실패 → CRLF SKILL.md(Windows/Notepad)의 meta 전체 소실 → skill install "name 필수" 실패.
|
|
437
|
+
const cleaned = String(text || '').replace(/^/, '').replace(/\r\n?/g, '\n'); // BOM strip (U+FEFF) + 줄바꿈 정규화
|
|
436
438
|
const m = cleaned.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
437
439
|
if (!m) return { meta: {}, body: cleaned };
|
|
438
440
|
const meta = {};
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -5767,5 +5767,28 @@ total++;
|
|
|
5767
5767
|
if (!ok) failed++;
|
|
5768
5768
|
}
|
|
5769
5769
|
|
|
5770
|
+
// 1.9.408 회귀 (8번째 버그헌트, UR-0112): CRLF/CR SKILL.md frontmatter 파싱 — Windows/Notepad 줄바꿈으로 meta 소실되던 skill install 복구
|
|
5771
|
+
total++;
|
|
5772
|
+
{
|
|
5773
|
+
let ok = false;
|
|
5774
|
+
try {
|
|
5775
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-crlfskill-'));
|
|
5776
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
|
|
5777
|
+
const skillDir = path.join(d, 'crlfSkillSrc');
|
|
5778
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
5779
|
+
// CRLF 줄바꿈 SKILL.md (Windows/Notepad)
|
|
5780
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), '---\r\nname: crlf-skill\r\ndescription: a windows skill\r\n---\r\nbody content\r\n');
|
|
5781
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'skill', 'install', skillDir, '--path', d], { encoding: 'utf8', timeout: 20000 });
|
|
5782
|
+
const installOk = r.status === 0 && !/name.{0,4}필수/.test((r.stdout || '') + (r.stderr || ''));
|
|
5783
|
+
// 설치 확인
|
|
5784
|
+
const ls = cp.spawnSync(process.execPath, [CLI, 'skill', 'list', '--path', d], { encoding: 'utf8', timeout: 15000 });
|
|
5785
|
+
const found = /crlf-skill/.test(ls.stdout || '');
|
|
5786
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
5787
|
+
ok = installOk && found;
|
|
5788
|
+
} catch {}
|
|
5789
|
+
console.log(ok ? '✓ B(1.9.408) 8th버그헌트: CRLF SKILL.md frontmatter 파싱 복구(Windows skill install) (UR-0112)' : '✗ CRLF SKILL.md 파싱 실패');
|
|
5790
|
+
if (!ok) failed++;
|
|
5791
|
+
}
|
|
5792
|
+
|
|
5770
5793
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
5771
5794
|
if (failed > 0) process.exit(1);
|