leerness 1.9.285 → 1.9.287
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 +43 -0
- package/README.md +5 -5
- package/bin/harness.js +112 -9
- package/package.json +1 -1
- package/scripts/e2e.js +56 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.287 — 2026-06-03 — Codex 외부 리뷰 수렴: 허위 완료 차단 강화 + handoff 펜스 + status minimal
|
|
4
|
+
|
|
5
|
+
**🛡️ 다른 AI(Codex)가 실측한 핵심 한계 "테스트만 통과하면 미구현도 done 통과"를 보강 + 발견된 품질 버그 2건 수정.**
|
|
6
|
+
|
|
7
|
+
### 배경 (Codex 직접 테스트 결과 수렴)
|
|
8
|
+
Codex 가 별도 프로젝트에 설치해 평가: 메모리/인계/추적은 쓸 만하나, (1) **허위 완료 태스크가 `verify-claim --strict-claims`/`gate` 를 통과**(테스트 통과 = 구현 인정 아님), (2) `session-handoff.md` 가 evidence 코드펜스를 끌어와 **마크다운 깨짐**, (3) `--minimal` 설치인데 `status` 가 전체 기준 누락 파일을 경고(UX 혼란). 타당한 지적을 수렴.
|
|
9
|
+
|
|
10
|
+
### 구현
|
|
11
|
+
1. **`verify-claim --require-evidence`** (허위 완료 갭) — `_evidenceQuality()` 로 done 주장의 evidence 완전성 강제: **수정 파일 경로 + 테스트명/개수** 가 없으면 FAIL (테스트 통과만으로 불충분). Codex 의 명시 권고("evidence 에 수정 파일/테스트명/개수/로그 강제") 반영. + **정직한 한계 고지**("테스트 통과 ≠ 의미적 구현 정확성").
|
|
12
|
+
2. **handoff 코드펜스 sanitize** — `_sanitizeFences()`: session-handoff 임베딩 시 inner ``` → `'''` 중립화 → wrapper 펜스 균형 유지, 마크다운 안 깨짐.
|
|
13
|
+
3. **`status` minimal 인지** — `manifest.json` 에 `minimal` 플래그 기록 + `status` 가 minimal 설치 시 생략 파일을 missing 으로 경고 안 함 ("minimal set" 표기).
|
|
14
|
+
4. selftest 33→35 (evidence 완전성 + 펜스 sanitize) + e2e 231→232.
|
|
15
|
+
|
|
16
|
+
### 검증
|
|
17
|
+
- **selftest 35/35 PASS** · **E2E 232/232 PASS** (회귀 0) · 허위("테스트 통과함")=exit 1 차단 / 완전(파일+테스트+실제파일)=exit 0 통과 / 펜스 균형 / status "minimal set" 실측.
|
|
18
|
+
|
|
19
|
+
### Codex 평가 중 인지 (즉시 조치 외)
|
|
20
|
+
- 단일 대형 파일 → UR-0025 진행 중(lib/ 14종 추출). 잦은 릴리스 → dist-tag(1.9.275). e2e 시간 → handoff 회귀 수정(1.9.284) + test:fast.
|
|
21
|
+
|
|
22
|
+
## 1.9.286 — 2026-06-03 — UR-0024: 스킬 설치 영향 경량 상관추적 + HuggingFace 안내 (백로그 완전 소진)
|
|
23
|
+
|
|
24
|
+
**📊 "설치 스킬이 코딩 성능/정확도에 도움?" — 무거운 벤치 없이 기존 데이터로 정직한 상관 advisory (GPT-5.5 / 사용자 요청). 마지막 추적 백로그.**
|
|
25
|
+
|
|
26
|
+
### 배경 + 설계 판단
|
|
27
|
+
사용자 요청(UR-0024): 스킬 소스(HuggingFace) + 설치 영향 측정. **권고대로**: (1) 설치는 이미 존재(skill install/discover) → HF 는 모델/데이터셋 중심이라 agent-skill 가치 낮음 → **discover --github 안내만**(저우선), (2) 영향 측정은 **경량 상관추적**(인과 아님, 표본 부족 시 판단 보류)으로 honest 구현.
|
|
28
|
+
|
|
29
|
+
### 구현
|
|
30
|
+
1. **`leerness skill impact [--json]`** — 설치 스킬 × 사용 빈도(skill-suggestions 이력) + `review-evidence.md` 검증 통과율 상관. **정직한 advisory**: 표본<5 시 "판단 보류", 그 외 "상관추적(인과 아님)".
|
|
31
|
+
2. **`_parseEvidenceStats(text)` 순수 함수** — review-evidence 의 Exit code / PASS·FAIL 키워드로 통과율 집계.
|
|
32
|
+
3. **HuggingFace 안내** — `skill discover` 도움말에 "HF 는 모델/데이터셋 중심 → agent-skill 은 GitHub 미러 repo 를 --github 로 지정 권장".
|
|
33
|
+
4. **offline-first 유지** — 실제 외부 스킬 탐색은 호스트 AI, leerness 는 네트워크 자동 호출 안 함.
|
|
34
|
+
5. selftest 32→33 + e2e 230→231.
|
|
35
|
+
|
|
36
|
+
### 디버그 (테스트 인프라)
|
|
37
|
+
- `skill impact` 가 id 없는 skill 엔트리에서 `.replace` crash → `Object.entries` + 가드로 수정.
|
|
38
|
+
- e2e exit 127 크래시 추적 → **누적 temp 디렉토리 6409개**(세션 내 e2e 반복 실행 잔재)로 인한 OS spawn 자원 고갈 확인. 정리 후 정상(코드/CI 무관 — CI 는 fresh runner).
|
|
39
|
+
|
|
40
|
+
### 검증
|
|
41
|
+
- **selftest 33/33 PASS** · **E2E 231/231 PASS** (회귀 0).
|
|
42
|
+
|
|
43
|
+
### 🎉 전체 백로그 완전 소진
|
|
44
|
+
GPT-5.5 1차 리뷰(UR-0025~0027) + 2차 리뷰(설치부담/모듈화/e2e) + 범용 하네스 전략(UR-0030~0035) + 사용자 요청(UR-0022~0024) — **전부 구현·배포 완료**.
|
|
45
|
+
|
|
3
46
|
## 1.9.285 — 2026-06-03 — UR-0023: 외부 OSS "빌드 vs 재사용" 결정 게이트 (오프라인)
|
|
4
47
|
|
|
5
48
|
**🔍 기능 계획 시 "이미 검증된 OSS 가 있는데 새로 만드는가?" 를 묻는 오프라인 구조적 게이트 (GPT-5.5 / 사용자 요청). 네트워크 크롤러 아님 — offline-first 유지.**
|
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.287 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
475
475
|
|
|
476
476
|
### Core Commands
|
|
477
477
|
|
|
@@ -513,7 +513,7 @@ leerness memory restore decision <date|title>
|
|
|
513
513
|
|
|
514
514
|
### MCP server (외부 AI 통합)
|
|
515
515
|
|
|
516
|
-
Leerness v1.9.
|
|
516
|
+
Leerness v1.9.287는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **42개 도구**를 노출:
|
|
517
517
|
|
|
518
518
|
```jsonc
|
|
519
519
|
// 카테고리별
|
|
@@ -534,7 +534,7 @@ Leerness v1.9.285는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
534
534
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
535
535
|
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) 다음 라운드 예약.
|
|
536
536
|
|
|
537
|
-
현재 누적: **70 라운드 (1.9.40 → 1.9.
|
|
537
|
+
현재 누적: **70 라운드 (1.9.40 → 1.9.287)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
538
538
|
|
|
539
539
|
### 성능 가이드 (1.9.140 측정)
|
|
540
540
|
|
|
@@ -572,6 +572,6 @@ leerness release pack --close --auto-main-push
|
|
|
572
572
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
573
573
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
574
574
|
|
|
575
|
-
Last synced by Leerness v1.9.
|
|
575
|
+
Last synced by Leerness v1.9.287: 2026-06-03
|
|
576
576
|
<!-- leerness:project-readme:end -->
|
|
577
577
|
|
package/bin/harness.js
CHANGED
|
@@ -10,7 +10,7 @@ const readline = require('readline');
|
|
|
10
10
|
const { _isSecretKey, compareVer, parseHarnessVersion, _classifyCJK, _riskLabel, _detectSystemLang, _parseSlashFromHelp,
|
|
11
11
|
PERMISSION_TIERS, _tierRank, _requiredTier, _policyAllows, _resolveNpmTag, _mcpJsonContent, _newRunRecord } = require('../lib/pure-utils');
|
|
12
12
|
|
|
13
|
-
const VERSION = '1.9.
|
|
13
|
+
const VERSION = '1.9.287';
|
|
14
14
|
|
|
15
15
|
// 1.9.184: DEP0190 (child_process shell: true) deprecation warning 억제 (사용자 명시).
|
|
16
16
|
// leerness 는 cross-platform PATH resolution 을 위해 shell: true 를 의도적으로 사용 (claude.cmd / ollama.cmd 등 Windows .cmd 처리).
|
|
@@ -149,6 +149,9 @@ function log(s = '') { console.log(s); }
|
|
|
149
149
|
function ok(s) { log('✓ ' + s); }
|
|
150
150
|
function warn(s) { log('⚠ ' + s); }
|
|
151
151
|
function fail(s) { log('✗ ' + s); }
|
|
152
|
+
// 1.9.287 (Codex 리뷰 수렴): 임베딩 텍스트의 코드펜스(```)를 중립화해 외부 마크다운(session-handoff)이 깨지지 않게.
|
|
153
|
+
// ``` → ''' (펜스가 아닌 표시), 인라인 백틱은 보존. 순수 함수.
|
|
154
|
+
function _sanitizeFences(s) { return String(s || '').replace(/```+/g, "'''"); }
|
|
152
155
|
function absRoot(p) { return path.resolve(p || process.cwd()); }
|
|
153
156
|
function exists(p) { return fs.existsSync(p); }
|
|
154
157
|
function read(p) {
|
|
@@ -415,7 +418,7 @@ function coreFiles(root, lang = 'ko', selectedSkills = [], opts = {}) {
|
|
|
415
418
|
'.github/copilot-instructions.md': `${MARK}\n# Copilot Instructions\n\nUse AGENTS.md and .harness/ as project memory.\nDo not remove protected Leerness files.\nBefore completion, ensure plan.md, progress-tracker.md, current-state.md, session-handoff.md are updated.\n`,
|
|
416
419
|
'.harness/HARNESS_VERSION': VERSION + '\n',
|
|
417
420
|
'.harness/LANGUAGE': lang + '\n',
|
|
418
|
-
'.harness/manifest.json': JSON.stringify({ project, leernessVersion: VERSION, language: lang, installedAt: now() }, null, 2) + '\n',
|
|
421
|
+
'.harness/manifest.json': JSON.stringify({ project, leernessVersion: VERSION, language: lang, installedAt: now(), minimal: !!opts.minimal }, null, 2) + '\n',
|
|
419
422
|
'.harness/skills-lock.json': skillLock(selectedSkills),
|
|
420
423
|
'.harness/project-brief.md': fm('project-brief', ['프로젝트 목적 확인','신규 기능 판단','계획 수립'], ['프로젝트 목적 변경','사용자/범위 변경'], `# Project Brief\n\n## Project\n${project}\n\n## Purpose\n- 이 프로젝트의 목적을 실제 내용으로 업데이트하세요.\n\n## Users\n-\n\n## Success Criteria\n-\n`),
|
|
421
424
|
'.harness/plan.md': fm('plan', ['작업 시작 전','새 요청 접수','범위 변경','신규 프로젝트 감지'], ['계획 추가/수정/드랍','milestone 변경','목표 변경'], `# Plan\n\n## Goal\n- 사용자 목적을 기준으로 전체 계획을 유지합니다.\n\n## Scope\n- 포함 범위를 기록합니다.\n\n## Out of Scope / Dropped\n| ID | Item | Reason | Date |\n|---|---|---|---|\n\n## Milestones\n\n### M-0001. 프로젝트 계획 정리\nStatus: planned\nProgress: 0%\n\nTasks:\n- [ ] project-brief.md를 실제 프로젝트 목적에 맞게 작성\n- [ ] context-map.md를 실제 파일 구조에 맞게 작성\n`),
|
|
@@ -805,6 +808,11 @@ function syncReadme(root) {
|
|
|
805
808
|
updated = updated.replace(/badge\/e2e-(\d+)%2F(\d+)-success/g, `badge/e2e-${total}%2F${total}-success`);
|
|
806
809
|
}
|
|
807
810
|
}
|
|
811
|
+
// 1.9.287: selftest 배지 자동 동기화 — _selfTestCases() 실제 케이스 수 직접 카운트(정확). 배지 stale 방지(GPT 지적).
|
|
812
|
+
try {
|
|
813
|
+
const stCount = _selfTestCases().length;
|
|
814
|
+
if (stCount > 0) updated = updated.replace(/badge\/selftest-\d+(?:%2F\d+)?/g, `badge/selftest-${stCount}`);
|
|
815
|
+
} catch {}
|
|
808
816
|
} catch {}
|
|
809
817
|
if (updated !== existing) writeUtf8(p, updated);
|
|
810
818
|
ok('README.md Leerness section synced');
|
|
@@ -1692,8 +1700,10 @@ async function skillDiscoverCmd(root) {
|
|
|
1692
1700
|
' leerness skill discover --preset anthropic # anthropics/skills',
|
|
1693
1701
|
' leerness skill discover --all-presets # 모든 preset 동시 탐색',
|
|
1694
1702
|
' leerness skill discover --github owner/repo # 직접 GitHub repo 지정',
|
|
1703
|
+
' leerness skill discover --github <hf-owner>/<repo> # HuggingFace 미러/agent-skills repo 도 동일 방식 (1.9.286)',
|
|
1695
1704
|
'',
|
|
1696
1705
|
'예: leerness skill discover --source https://agentskills.io/llms.txt',
|
|
1706
|
+
'※ HuggingFace 는 모델/데이터셋 중심 — agent-skill 은 GitHub 미러 repo 를 --github 로 지정 권장 (1.9.286 UR-0024).',
|
|
1697
1707
|
'',
|
|
1698
1708
|
'(정책: leerness는 사용자 동의 없이 외부 URL을 fetch하지 않음 — 1.9.42 opt-in)'
|
|
1699
1709
|
].join('\n'));
|
|
@@ -2961,6 +2971,9 @@ function _selfTestCases() {
|
|
|
2961
2971
|
{ name: '_pickModel: top/code/fast 등급 선택 (1.9.270)', run: () => { return _pickModel('codex', 'code') === 'gpt-5-codex' && _pickModel('claude', 'top') === 'claude-opus-4-7' && /haiku/.test(_pickModel('claude', 'fast')); } },
|
|
2962
2972
|
{ name: 'CAPABILITY_SURFACE: 6 영역 + risk/optOut + 주의명령 (1.9.272)', run: () => { const ks = Object.keys(CAPABILITY_SURFACE); return ks.length === 6 && ks.includes('automationBridges') && Object.values(CAPABILITY_SURFACE).every(v => ['low','medium','high'].includes(v.risk) && v.optOut && v.desc) && POWERFUL_COMMANDS.length >= 5; } },
|
|
2963
2973
|
{ name: '_resolveNpmTag: latest 기본 + next 허용 + 잘못된 형식 폴백 (1.9.275)', run: () => _resolveNpmTag(null, {}) === 'latest' && _resolveNpmTag('next', {}) === 'next' && _resolveNpmTag(null, { LEERNESS_NPM_TAG: 'next' }) === 'next' && _resolveNpmTag('bad tag!', {}) === 'latest' && _resolveNpmTag('Beta', {}) === 'beta' },
|
|
2974
|
+
{ name: '_sanitizeFences: 코드펜스 중립화 (Codex 수렴 1.9.287)', run: () => { const out = _sanitizeFences('text\n```js\ncode\n```\nmore'); return !/```/.test(out) && /'''/.test(out) && /code/.test(out); } },
|
|
2975
|
+
{ name: '_evidenceQuality: 파일+테스트 근거 강제 (Codex 수렴 1.9.287)', run: () => { const good = _evidenceQuality('src/api.js 수정, npm test 12/12 통과 (Exit: 0)'); const weak = _evidenceQuality('테스트 통과함'); const noFile = _evidenceQuality('12 tests passed'); return good.ok === true && good.hasFile && good.hasTest && weak.ok === false && weak.missing.includes('수정 파일 경로') && noFile.ok === false; } },
|
|
2976
|
+
{ name: '_parseEvidenceStats: review-evidence pass/fail 집계 (1.9.286)', run: () => { const s = _parseEvidenceStats('## 2026-06-03\nCommand: npm test\nExit: 0\n\n## 2026-06-03\nCommand: build\nExit: 1\n\n## 2026-06-03\nverify PASS 통과\n'); return s.entries === 3 && s.pass === 2 && s.fail === 1 && s.rate === 67; } },
|
|
2964
2977
|
{ name: '_reuseDetect: 키워드→OSS 카테고리 + 체크리스트 (1.9.285)', run: () => { const a = _reuseDetect('JWT 인증 구현'); const b = _reuseDetect('날짜 포맷 date'); const c = _reuseDetect('전혀무관한xyzzy'); return a.some(x => x.key === 'auth') && b.some(x => x.key === 'date') && c.length === 0 && REUSE_CHECKLIST.length >= 5 && REUSE_CATEGORIES.length >= 12; } },
|
|
2965
2978
|
{ name: 'AGENTS.md: 정적 vs 동적 leerness 역할 경계 (1.9.282)', run: () => { const a = coreFiles('.', 'ko', [])['AGENTS.md']; return /정적 vs 동적/.test(a) && /대체하지 않고 \*\*보완\*\*|대체하지 않고 보완/.test(a) && /leerness state/.test(a) && /\.leerness\//.test(a); } },
|
|
2966
2979
|
{ name: '권한 등급: _requiredTier + _policyAllows 순서 (1.9.281)', run: () => { return _requiredTier('release publish') === 'publish' && _requiredTier('agents multi --execute') === 'shell-write' && _requiredTier('handoff') === 'read-only' && _requiredTier('init') === 'project-write' && _policyAllows('project-write', 'safe-write') === true && _policyAllows('project-write', 'publish') === false && _tierRank('read-only') < _tierRank('publish'); } },
|
|
@@ -3611,7 +3624,7 @@ function commandsCmd(root) {
|
|
|
3611
3624
|
{ cmd: 'scan secrets [path]', desc: '시크릿 탐지' },
|
|
3612
3625
|
{ cmd: 'encoding check [path]', desc: '인코딩 검증' },
|
|
3613
3626
|
{ cmd: 'lazy detect [path] [--json]', desc: '게으른 작업 감지 (1.9.101)' },
|
|
3614
|
-
{ cmd: 'verify-claim <T-ID> [--run-tests] [--strict-claims]', desc: '주장 검증 (1.9.18~26)' },
|
|
3627
|
+
{ cmd: 'verify-claim <T-ID> [--run-tests] [--strict-claims] [--require-evidence]', desc: '주장 검증 (1.9.18~26) — --require-evidence: done 주장에 파일+테스트 근거 강제 (1.9.287)' },
|
|
3615
3628
|
{ cmd: 'optimism-check <T-ID>', desc: '낙관적 API 감지 (1.9.26)' },
|
|
3616
3629
|
{ cmd: 'requests audit|list|complete|drop|auto-complete', desc: '사용자 요청 추적 (1.9.207/223)' },
|
|
3617
3630
|
{ cmd: 'pre-wake-audit [path] [--last]', desc: 'sleep 전 점검 (1.9.209)' },
|
|
@@ -3628,6 +3641,7 @@ function commandsCmd(root) {
|
|
|
3628
3641
|
{ cmd: 'adapter <tool>|list [--dry-run]', desc: '도구별 지침/.mcp.json 선택 생성 (claude/cursor/codex/goose/...) — 1.9.280' },
|
|
3629
3642
|
{ cmd: 'policy show|set|check', desc: '권한 등급 (read-only…publish) — opt-in enforced (위험 명령 차단) — 1.9.281' },
|
|
3630
3643
|
{ cmd: 'reuse-check "<기능>"', desc: '외부 OSS 빌드 vs 재사용 결정 게이트 (오프라인 카테고리+체크리스트) — 1.9.285' },
|
|
3644
|
+
{ cmd: 'skill impact', desc: '스킬 설치 영향 경량 상관추적 (사용 빈도 ↔ 검증 통과율, advisory) — 1.9.286' },
|
|
3631
3645
|
{ cmd: 'release channel [--json]', desc: '릴리스 채널 정책 (latest 안정 / next 실험 / 버전 고정) — 1.9.275' },
|
|
3632
3646
|
{ cmd: 'slash-commands [agent] [--json --record --detect --refresh]', desc: 'CLI 에이전트 슬래시 명령 레지스트리 + --help probe 자동 갱신 (1.9.265~267, UR-0021)' },
|
|
3633
3647
|
{ cmd: 'review-request "<request>"', desc: '사용자 요청 사전 검토 (1.9.176)' },
|
|
@@ -6789,12 +6803,15 @@ function status(root) {
|
|
|
6789
6803
|
const verF = path.join(root,'.harness/HARNESS_VERSION');
|
|
6790
6804
|
const ver = exists(verF) ? read(verF).trim() : 'not installed';
|
|
6791
6805
|
const lang = exists(path.join(root,'.harness/LANGUAGE')) ? read(path.join(root,'.harness/LANGUAGE')).trim() : 'ko';
|
|
6792
|
-
|
|
6806
|
+
// 1.9.287 (Codex 리뷰 수렴): --minimal 설치는 manifest.minimal=true → 의도적으로 생략된 파일을 missing 으로 경고하지 않음.
|
|
6807
|
+
let isMinimal = false;
|
|
6808
|
+
try { const mf = path.join(root, '.harness/manifest.json'); if (exists(mf)) isMinimal = !!JSON.parse(read(mf)).minimal; } catch {}
|
|
6809
|
+
const files = Object.keys(coreFiles(root, lang, [], { minimal: isMinimal }));
|
|
6793
6810
|
const missing = files.filter(f => !exists(path.join(root,f)));
|
|
6794
|
-
log(`Leerness: ${ver}`);
|
|
6811
|
+
log(`Leerness: ${ver}${isMinimal ? ' (minimal)' : ''}`);
|
|
6795
6812
|
log(`Files: ${files.length - missing.length}/${files.length}`);
|
|
6796
6813
|
if (missing.length) missing.forEach(x => warn('missing: ' + x));
|
|
6797
|
-
else ok(
|
|
6814
|
+
else ok(`required files present${isMinimal ? ' (minimal set)' : ''}`);
|
|
6798
6815
|
}
|
|
6799
6816
|
function verify(root) {
|
|
6800
6817
|
root = absRoot(root);
|
|
@@ -9239,6 +9256,19 @@ function reuseMapCmd(root) {
|
|
|
9239
9256
|
|
|
9240
9257
|
// 1.9.18: verify-claim — progress-tracker의 evidence 컬럼 자동 검증
|
|
9241
9258
|
// "src/foo.js + 5개 테스트 (54/54 통과)" 같은 주장을 파싱해 실제 파일/카운트 확인
|
|
9259
|
+
// 1.9.287 (Codex 리뷰 수렴): evidence 완전성 평가 — "테스트 통과만으로 done" 차단.
|
|
9260
|
+
// done 주장은 (a) 수정 파일 경로 (b) 테스트명/개수 가 evidence 에 있어야 신뢰 가능. 순수 함수(selftest).
|
|
9261
|
+
function _evidenceQuality(evidence) {
|
|
9262
|
+
const e = String(evidence || '');
|
|
9263
|
+
const hasFile = /(?:[A-Za-z][\w-]*[\/\\])?[A-Za-z][\w./\\-]*\.(?:js|ts|tsx|jsx|mjs|cjs|py|go|rs|rb|kt|cs|gd|java|php|swift|c|cpp|h|html|css|scss|vue|svelte|json|yaml|yml|toml|md|sql|sh)\b/i.test(e);
|
|
9264
|
+
const hasTest = /(\d+)\s*(?:\/\s*\d+\s*)?(?:통과|passed|passing|개\s*테스트)|\btests?\b\s*[:=]?\s*\d|Tests?:\s*\d|\b\d+\s*tests?\b/i.test(e);
|
|
9265
|
+
const hasLog = /Exit\s*[:=]|exit\s*code|Command\s*[:=]|npm\s+(?:test|run)|pytest|cargo\s+test|go\s+test/i.test(e);
|
|
9266
|
+
const missing = [];
|
|
9267
|
+
if (!hasFile) missing.push('수정 파일 경로');
|
|
9268
|
+
if (!hasTest) missing.push('테스트명/개수');
|
|
9269
|
+
if (!hasLog) missing.push('실행 로그(Command/Exit)');
|
|
9270
|
+
return { hasFile, hasTest, hasLog, ok: hasFile && hasTest, missing };
|
|
9271
|
+
}
|
|
9242
9272
|
function verifyClaimCmd(root, taskId) {
|
|
9243
9273
|
root = absRoot(root);
|
|
9244
9274
|
if (!taskId) return fail('verify-claim <T-ID> 필요. 예: leerness verify-claim T-0008');
|
|
@@ -9430,7 +9460,23 @@ function verifyClaimCmd(root, taskId) {
|
|
|
9430
9460
|
for (const s of optimismSuspects) log(` · [${s.kind}] ${s.label}: evidence에 주장 있는데 코드에 호출 흔적 없음`);
|
|
9431
9461
|
}
|
|
9432
9462
|
}
|
|
9433
|
-
|
|
9463
|
+
// 1.9.287 (Codex 리뷰 수렴): --require-evidence — done 주장의 evidence 완전성(파일+테스트) 강제.
|
|
9464
|
+
// "테스트 통과만으로 done" 차단 — Codex 가 발견한 허위 완료 통과 갭 보강.
|
|
9465
|
+
const reqEvidence = has('--require-evidence');
|
|
9466
|
+
const isDoneClaim = /done|완료/i.test(row.status || '');
|
|
9467
|
+
let evidenceQualityOk = true;
|
|
9468
|
+
if (reqEvidence) {
|
|
9469
|
+
const evq = _evidenceQuality(evidence);
|
|
9470
|
+
evidenceQualityOk = !isDoneClaim || evq.ok;
|
|
9471
|
+
log(` - evidence 완전성 (--require-evidence): ${evidenceQualityOk ? '✓ pass (파일+테스트 근거 있음)' : `✗ FAIL (누락: ${evq.missing.join(', ')})`}`);
|
|
9472
|
+
if (!evidenceQualityOk) log(` · done 주장은 수정 파일 경로 + 테스트명/개수 가 evidence 에 있어야 함 (테스트 통과만으로는 불충분)`);
|
|
9473
|
+
}
|
|
9474
|
+
const overallFail = !allFilesOk || !testOk || (runResult && !runResult.skipped && !runTestsOk) || (has('--strict-claims') && !strictOk) || !evidenceQualityOk;
|
|
9475
|
+
// 1.9.287: 정직한 한계 고지 — 테스트 통과 ≠ 의미적 구현 정확성
|
|
9476
|
+
if (has('--strict-claims') || reqEvidence) {
|
|
9477
|
+
log('');
|
|
9478
|
+
log(` ℹ 한계: 테스트 통과는 "의미적 구현 정확성"을 보장하지 않음 — evidence 가 해당 주장(수정 파일/테스트)을 직접 링크해야 신뢰도↑.`);
|
|
9479
|
+
}
|
|
9434
9480
|
if (overallFail) {
|
|
9435
9481
|
log('');
|
|
9436
9482
|
log(` ⚠ evidence 주장과 실제가 일치하지 않음 — task 상태 재검토 권장`);
|
|
@@ -11993,7 +12039,8 @@ function sessionClose(root, opts = {}) {
|
|
|
11993
12039
|
return arr.map(r => `- ${r.id} ${r.request} → next: ${r.nextAction}`).join('\n');
|
|
11994
12040
|
}
|
|
11995
12041
|
|
|
11996
|
-
|
|
12042
|
+
// 1.9.287 (Codex 리뷰 수렴): evidence 임베딩 시 코드펜스(```) 가 session-handoff.md 마크다운을 깨뜨리는 품질 버그 수정.
|
|
12043
|
+
const evidenceSummary = _sanitizeFences(exists(evidencePath(root)) ? (read(evidencePath(root)).split('\n').slice(-30).join('\n')) : '(no review-evidence.md)');
|
|
11997
12044
|
const block = [
|
|
11998
12045
|
`# Session Handoff`,
|
|
11999
12046
|
``,
|
|
@@ -18381,6 +18428,56 @@ function reuseCheckCmd(root, feature, opts = {}) {
|
|
|
18381
18428
|
return;
|
|
18382
18429
|
}
|
|
18383
18430
|
|
|
18431
|
+
// ===== 1.9.286 (UR-0024, GPT-5.5): 스킬 설치 영향 경량 상관추적 =====
|
|
18432
|
+
// "설치한 스킬이 코딩 성능/정확도에 긍정 영향?" — 무거운 벤치 harness 가 아니라, 기존 데이터
|
|
18433
|
+
// (skill 사용 빈도 + review-evidence 검증 통과율)를 상관(인과 아님)으로 advisory 제시. offline.
|
|
18434
|
+
// review-evidence.md 파싱 (순수) — 검증 항목 수 + pass/fail 추정 (Exit 0 / 'PASS' vs 'fail'/Exit !=0).
|
|
18435
|
+
function _parseEvidenceStats(text) {
|
|
18436
|
+
const t = String(text || '');
|
|
18437
|
+
const blocks = t.split(/\n(?=## )/).filter(b => /Command:|Exit:|verify|test/i.test(b));
|
|
18438
|
+
let pass = 0, fail = 0;
|
|
18439
|
+
for (const b of blocks) {
|
|
18440
|
+
const exitM = b.match(/Exit:\s*(-?\d+)/i);
|
|
18441
|
+
if (exitM) { (parseInt(exitM[1], 10) === 0 ? pass++ : fail++); continue; }
|
|
18442
|
+
if (/\bPASS\b|통과|성공|✓/i.test(b)) pass++;
|
|
18443
|
+
else if (/\bFAIL\b|실패|오류|error|✗/i.test(b)) fail++;
|
|
18444
|
+
}
|
|
18445
|
+
const entries = blocks.length;
|
|
18446
|
+
return { entries, pass, fail, rate: (pass + fail) ? Math.round(pass / (pass + fail) * 100) : null };
|
|
18447
|
+
}
|
|
18448
|
+
function skillImpactCmd(root) {
|
|
18449
|
+
root = absRoot(root || process.cwd());
|
|
18450
|
+
const json = has('--json');
|
|
18451
|
+
// 설치 스킬 + 사용 빈도 (skill-suggestions.md rolling history 의 id 언급 카운트)
|
|
18452
|
+
let skills = [];
|
|
18453
|
+
try { skills = Object.entries(listAllSkills(root)).map(([k, s]) => ({ id: (s && (s.id || s.name)) || k, name: (s && (s.displayNameKo || s.name)) || k })).filter(s => s.id); } catch {}
|
|
18454
|
+
const histPath = path.join(absRoot(root), '.harness', 'skill-suggestions.md');
|
|
18455
|
+
const hist = exists(histPath) ? read(histPath) : '';
|
|
18456
|
+
const uses = {};
|
|
18457
|
+
for (const s of skills) { const re = new RegExp(String(s.id).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'); uses[s.id] = (hist.match(re) || []).length; }
|
|
18458
|
+
const ranked = skills.map(s => ({ ...s, uses: uses[s.id] || 0 })).sort((a, b) => b.uses - a.uses);
|
|
18459
|
+
const ev = _parseEvidenceStats(exists(evidencePath(root)) ? read(evidencePath(root)) : '');
|
|
18460
|
+
const sampleSize = ev.entries;
|
|
18461
|
+
const advisory = sampleSize < 5
|
|
18462
|
+
? '표본 부족 (검증 기록 < 5) — 판단 보류. 더 많은 verify-code 누적 후 재확인.'
|
|
18463
|
+
: '상관추적(인과 아님): 스킬 사용 빈도와 검증 통과율은 참고 신호일 뿐, 인과관계를 보장하지 않음.';
|
|
18464
|
+
if (json) { log(JSON.stringify({ skills: ranked, usedCount: ranked.filter(s => s.uses > 0).length, evidence: ev, sampleSize, advisory, network: false }, null, 2)); return; }
|
|
18465
|
+
const isTty = process.stdout && process.stdout.isTTY;
|
|
18466
|
+
const cy = s => isTty ? `\x1b[36m${s}\x1b[0m` : s; const dm = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
18467
|
+
log(cy(`# leerness skill impact (1.9.286, UR-0024) — 스킬 영향 경량 상관추적`));
|
|
18468
|
+
log(`설치 스킬: ${skills.length} · 사용 이력 있는 스킬: ${ranked.filter(s => s.uses > 0).length}`);
|
|
18469
|
+
log(`검증 기록(review-evidence): ${ev.entries}건 · 통과율: ${ev.rate === null ? 'N/A' : ev.rate + '%'} (pass ${ev.pass} / fail ${ev.fail})`);
|
|
18470
|
+
if (ranked.filter(s => s.uses > 0).length) {
|
|
18471
|
+
log('');
|
|
18472
|
+
log(`## 사용 빈도 top (skill-suggestions 이력)`);
|
|
18473
|
+
for (const s of ranked.filter(x => x.uses > 0).slice(0, 8)) log(` ${String(s.uses).padStart(3)}× ${s.id} ${dm(s.name)}`);
|
|
18474
|
+
}
|
|
18475
|
+
log('');
|
|
18476
|
+
log(dm(` ⚠ ${advisory}`));
|
|
18477
|
+
log(dm(` ※ HuggingFace/외부 스킬 소스 탐색은 호스트 AI 가 수행 (leerness 는 네트워크 자동 호출 안 함).`));
|
|
18478
|
+
return;
|
|
18479
|
+
}
|
|
18480
|
+
|
|
18384
18481
|
// 1.9.149+1.9.153: REPL 모드 — leerness 자율 AI 에이전트 (multi-provider 세션)
|
|
18385
18482
|
async function _agentRepl(root, opts) {
|
|
18386
18483
|
// 1.9.153: .env 자동 로드 (REPL 진입 직전) — install 직후 LEERNESS_ENABLE_* 즉시 반영
|
|
@@ -21241,6 +21338,8 @@ async function main() {
|
|
|
21241
21338
|
if (cmd === 'skill' && args[1] === 'consolidate') return skillConsolidate(absRoot(arg('--path', process.cwd())));
|
|
21242
21339
|
if (cmd === 'skill' && args[1] === 'install') return await skillInstallCmd(absRoot(arg('--path', process.cwd())), args[2]);
|
|
21243
21340
|
if (cmd === 'skill' && args[1] === 'discover') return await skillDiscoverCmd(absRoot(arg('--path', process.cwd())));
|
|
21341
|
+
// 1.9.286 (UR-0024): leerness skill impact — 스킬 설치 영향 경량 상관추적
|
|
21342
|
+
if (cmd === 'skill' && args[1] === 'impact') return skillImpactCmd(args[2] || arg('--path', process.cwd()));
|
|
21244
21343
|
if (cmd === 'skill' && args[1] === 'export') return skillExportCmd(absRoot(arg('--path', process.cwd())), args[2]);
|
|
21245
21344
|
if (cmd === 'skill' && args[1] === 'export-all') return skillExportAllCmd(absRoot(arg('--path', process.cwd())));
|
|
21246
21345
|
if (cmd === 'skill' && args[1] === 'match') return skillMatchCmd(absRoot(arg('--path', process.cwd())), args.slice(2).filter(x => !x.startsWith('-')).join(' '));
|
|
@@ -21508,5 +21607,9 @@ module.exports = {
|
|
|
21508
21607
|
// 1.9.281: 권한 등급 (UR-0034) — 단위 테스트
|
|
21509
21608
|
PERMISSION_TIERS, _tierRank, _requiredTier, _policyAllows, _loadPolicy, _savePolicy, _policyEnforce, policyCmd,
|
|
21510
21609
|
// 1.9.285: 외부 OSS 재사용 게이트 (UR-0023) — 단위 테스트
|
|
21511
|
-
REUSE_CATEGORIES, REUSE_CHECKLIST, _reuseDetect, reuseCheckCmd
|
|
21610
|
+
REUSE_CATEGORIES, REUSE_CHECKLIST, _reuseDetect, reuseCheckCmd,
|
|
21611
|
+
// 1.9.286: 스킬 영향 상관추적 (UR-0024) — 단위 테스트
|
|
21612
|
+
_parseEvidenceStats, skillImpactCmd,
|
|
21613
|
+
// 1.9.287: evidence 완전성 + 코드펜스 sanitize (Codex 리뷰 수렴) — 단위 테스트
|
|
21614
|
+
_evidenceQuality, _sanitizeFences
|
|
21512
21615
|
};
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -2986,5 +2986,61 @@ total++;
|
|
|
2986
2986
|
if (!ok) failed++;
|
|
2987
2987
|
}
|
|
2988
2988
|
|
|
2989
|
+
// 1.9.286 회귀 (UR-0024): skill impact — 영향 경량 상관추적 + 정직한 advisory
|
|
2990
|
+
total++;
|
|
2991
|
+
{
|
|
2992
|
+
const iDir = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-impact-'));
|
|
2993
|
+
cp.spawnSync(process.execPath, [CLI, 'init', iDir, '--minimal', '--no-env', '--yes'], { encoding: 'utf8', timeout: 30000, env: { ...process.env, LEERNESS_OFFLINE: '1' } });
|
|
2994
|
+
// 표본 부족 (검증 0건) → 판단 보류 advisory
|
|
2995
|
+
let lowOk = false;
|
|
2996
|
+
try { const j = JSON.parse(cp.spawnSync(process.execPath, [CLI, 'skill', 'impact', iDir, '--json'], { encoding: 'utf8', timeout: 15000 }).stdout); lowOk = Array.isArray(j.skills) && j.sampleSize === 0 && /표본 부족/.test(j.advisory) && j.network === false; } catch {}
|
|
2997
|
+
// review-evidence 에 pass/fail 6건 시드 → 통과율 계산
|
|
2998
|
+
const evFile = path.join(iDir, '.harness', 'review-evidence.md');
|
|
2999
|
+
let seed = '# Review Evidence\n';
|
|
3000
|
+
for (let k = 0; k < 5; k++) seed += `\n## 2026-06-03 0${k}:00\nCommand: npm test\nExit: 0\n`;
|
|
3001
|
+
seed += `\n## 2026-06-03 06:00\nCommand: build\nExit: 1\n`;
|
|
3002
|
+
fs.appendFileSync(evFile, seed);
|
|
3003
|
+
let rateOk = false;
|
|
3004
|
+
try { const j = JSON.parse(cp.spawnSync(process.execPath, [CLI, 'skill', 'impact', iDir, '--json'], { encoding: 'utf8', timeout: 15000 }).stdout); rateOk = j.sampleSize >= 6 && j.evidence.pass === 5 && j.evidence.fail === 1 && j.evidence.rate === 83 && /상관추적/.test(j.advisory); } catch {}
|
|
3005
|
+
const ok = lowOk && rateOk;
|
|
3006
|
+
console.log(ok ? '✓ B(1.9.286) skill impact: 표본부족 advisory + 검증 통과율 상관 + json' : `✗ skill impact 실패 (low=${lowOk} rate=${rateOk})`);
|
|
3007
|
+
if (!ok) failed++;
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
// 1.9.287 회귀 (Codex 리뷰 수렴): verify-claim --require-evidence + handoff 펜스 sanitize + status minimal
|
|
3011
|
+
total++;
|
|
3012
|
+
{
|
|
3013
|
+
const cDir = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-codex-'));
|
|
3014
|
+
const env = { ...process.env, LEERNESS_OFFLINE: '1' };
|
|
3015
|
+
cp.spawnSync(process.execPath, [CLI, 'init', cDir, '--minimal', '--no-env', '--yes'], { encoding: 'utf8', timeout: 30000, env });
|
|
3016
|
+
// A) 허위 완료(테스트 통과만) → --require-evidence FAIL (exit 1)
|
|
3017
|
+
cp.spawnSync(process.execPath, [CLI, 'task', 'add', 'fake done', '--path', cDir], { encoding: 'utf8', timeout: 15000, env });
|
|
3018
|
+
cp.spawnSync(process.execPath, [CLI, 'task', 'update', 'T-0002', '--status', 'done', '--evidence', '테스트 통과함', '--path', cDir], { encoding: 'utf8', timeout: 15000, env });
|
|
3019
|
+
const rFake = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0002', '--require-evidence', '--path', cDir], { encoding: 'utf8', timeout: 20000, env });
|
|
3020
|
+
const fakeBlocked = rFake.status === 1 && /evidence 완전성.*FAIL/.test(rFake.stdout);
|
|
3021
|
+
// 완전한 evidence(파일+테스트) + 실제 파일 존재 → pass (exit 0)
|
|
3022
|
+
fs.mkdirSync(path.join(cDir, 'src'), { recursive: true });
|
|
3023
|
+
fs.writeFileSync(path.join(cDir, 'src', 'api.js'), 'module.exports = {};\n');
|
|
3024
|
+
cp.spawnSync(process.execPath, [CLI, 'task', 'update', 'T-0002', '--evidence', 'src/api.js 구현, npm test 5/5 통과 (Exit: 0)', '--path', cDir], { encoding: 'utf8', timeout: 15000, env });
|
|
3025
|
+
const rGood = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0002', '--require-evidence', '--path', cDir], { encoding: 'utf8', timeout: 20000, env });
|
|
3026
|
+
const goodPass = rGood.status === 0 && /evidence 완전성.*pass/.test(rGood.stdout);
|
|
3027
|
+
// B) handoff 펜스 sanitize — review-evidence 에 ``` 넣고 session close → session-handoff 에 ``` 불균형 없음
|
|
3028
|
+
fs.appendFileSync(path.join(cDir, '.harness', 'review-evidence.md'), '\n## 2026-06-03\n```js\nconst x=1;\n```\n');
|
|
3029
|
+
cp.spawnSync(process.execPath, [CLI, 'session', 'close', cDir], { encoding: 'utf8', timeout: 20000, env });
|
|
3030
|
+
// 펜스 균형: 감싸는 wrapper ``` 는 허용하되, review-evidence 의 inner ``` 는 '''로 sanitize 되어 불균형이 없어야 함.
|
|
3031
|
+
let fenceOk = false;
|
|
3032
|
+
try {
|
|
3033
|
+
const sh = fs.readFileSync(path.join(cDir, '.harness', 'session-handoff.md'), 'utf8');
|
|
3034
|
+
const bareFences = sh.split('\n').filter(l => l.trim() === '`'.repeat(3)).length;
|
|
3035
|
+
fenceOk = (bareFences % 2 === 0) && sh.includes("'''"); // 균형(짝수) + inner sanitize 적용 확인
|
|
3036
|
+
} catch {}
|
|
3037
|
+
// C) status minimal 인지 — minimal set 표기 + missing 경고 없음
|
|
3038
|
+
const rStat = cp.spawnSync(process.execPath, [CLI, 'status', cDir], { encoding: 'utf8', timeout: 15000, env });
|
|
3039
|
+
const statOk = /minimal/.test(rStat.stdout) && !/missing:/.test(rStat.stdout);
|
|
3040
|
+
const ok = fakeBlocked && goodPass && fenceOk && statOk;
|
|
3041
|
+
console.log(ok ? '✓ B(1.9.287) Codex 수렴: require-evidence(허위차단/완전통과) + 펜스 sanitize + status minimal' : `✗ Codex 수렴 실패 (fake=${fakeBlocked} good=${goodPass} fence=${fenceOk} stat=${statOk})`);
|
|
3042
|
+
if (!ok) { failed++; console.log((rFake.stdout || '').slice(-300)); }
|
|
3043
|
+
}
|
|
3044
|
+
|
|
2989
3045
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
2990
3046
|
if (failed > 0) process.exit(1);
|