leerness 1.9.301 → 1.9.302

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,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.302 — 2026-06-04 — UR-0042: verify-claim git diff 시맨틱 교차검증 (외부 AI 리뷰 R3, Opus G-1)
4
+
5
+ **🔍 Opus가 "가장 전략적 약점"으로 꼽은 거짓완료 검증의 실질화 — "파일 존재 + N passed" 문자열매칭에 git diff 교차검증 추가: 주장한 파일이 실제로 변경됐는가를 git working tree + 직전 커밋으로 대조.**
6
+
7
+ ### 배경 (Opus G-1)
8
+ verify-claim 의 차별점은 "거짓 완료 차단"인데, 기존 메커니즘은 evidence 텍스트에서 파일경로 추출 → `fs.existsSync` (존재만 확인) + "N passed" 정규식 파싱뿐. **변경 내용이 실제로 일어났는지는 검증 안 함** → "테스트 통과 = 구현 완료" 오인 가능. Opus: "파일이 존재하는가 + 테스트가 통과한다고 적혀있는가만 검증."
9
+
10
+ ### 구현 (UR-0042)
11
+ 1. **`_gitChangedFiles(root)`** — git working tree(staged/unstaged/untracked, `status --porcelain`) + 직전 커밋(`diff HEAD~1 HEAD`) 변경 파일 집합. git repo 아니면 null(검증 불가 → 페널티 없음).
12
+ 2. **`_claimFileInGit(claimed, gitSet)`** — 주장 파일이 git 변경에 있는지(상대경로 prefix 차이 허용).
13
+ 3. **verify-claim 종합에 git 교차검증** — 주장 N개 중 실제 변경 X개 표시. advisory 기본. `--strict-claims` 시 **강한 불일치**(working tree 변경 있는데 주장 파일이 git 변경에 전무)는 `overallFail` 기여(exit 1).
14
+ 4. 정직한 한계 고지 유지(시맨틱 정확성까지는 보장 X, 단 "주장↔실제 변경" 링크는 검증). selftest 49→50 · e2e 246→247.
15
+
16
+ ### 검증
17
+ - **selftest 50/50 PASS** · **E2E 247/247 PASS** (회귀 0).
18
+ - 실측(실 git repo): src/api.js 수정 후 "src/api.js 수정" 주장 → git 교차검증 **✓** · 미변경 old.js 주장 + `--strict-claims` → **⚠ 불일치 + exit 1**. git repo 아니면 skip(페널티 없음).
19
+ - false-positive 완화: working tree 변경 0(이미 커밋/미변경) 시 skip, 직전 커밋도 변경 집합에 포함.
20
+
3
21
  ## 1.9.301 — 2026-06-04 — UR-0041 (1단계): MCP 도구 requiredTier 메타데이터 + 정책 메타데이터 게이트 (R2)
4
22
 
5
23
  **🛡 외부 AI 리뷰 3종이 공통 지적한 "정책이 regex라 취약 + 도구 정의/dispatch/tier 3중 분산"의 핵심부 해소 — 81개 MCP 도구에 `requiredTier` 메타데이터를 부여하고, 정책 게이트가 regex와 메타데이터 중 더 엄격한 tier로 판정.**
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.301-green)]() [![tests](https://img.shields.io/badge/e2e-246%2F246-success)]() [![selftest](https://img.shields.io/badge/selftest-49-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-81-brightgreen)]() [![providers](https://img.shields.io/badge/AI_providers-10-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.302-green)]() [![tests](https://img.shields.io/badge/e2e-247%2F247-success)]() [![selftest](https://img.shields.io/badge/selftest-50-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-81-brightgreen)]() [![providers](https://img.shields.io/badge/AI_providers-10-brightgreen)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
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.301 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
474
+ 이 프로젝트는 Leerness v1.9.302 하네스를 사용합니다. 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.301는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **81개 도구**를 노출:
528
+ Leerness v1.9.302는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **81개 도구**를 노출:
529
529
 
530
530
  ```jsonc
531
531
  // 카테고리별
@@ -546,7 +546,7 @@ Leerness v1.9.301는 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.301)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
549
+ 현재 누적: **70 라운드 (1.9.40 → 1.9.302)** · 매 라운드 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.301: 2026-06-04
587
+ Last synced by Leerness v1.9.302: 2026-06-04
588
588
  <!-- leerness:project-readme:end -->
589
589
 
package/bin/harness.js CHANGED
@@ -12,7 +12,7 @@ const { _isSecretKey, compareVer, parseHarnessVersion, _classifyCJK, _riskLabel,
12
12
  // 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
13
13
  const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST } = require('../lib/catalogs');
14
14
 
15
- const VERSION = '1.9.301';
15
+ const VERSION = '1.9.302';
16
16
 
17
17
  // 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
18
18
  // 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
@@ -3037,6 +3037,7 @@ function _selfTestCases() {
3037
3037
  { name: '_scrubTestEnv: npm test 시크릿 차단(_scrubEnv는 release 토큰 유지) (UR-0039 외부리뷰 1.9.299)', run: () => { const o = { N: process.env.NPM_TOKEN, L: process.env.LEERNESS_NPM_TOKEN }; process.env.NPM_TOKEN = 'sec1'; process.env.LEERNESS_NPM_TOKEN = 'sec2'; const base = _scrubEnv(); const test = _scrubTestEnv(); const r = base.NPM_TOKEN === 'sec1' && base.LEERNESS_NPM_TOKEN === 'sec2' && !test.NPM_TOKEN && !test.LEERNESS_NPM_TOKEN && !!test.PATH; if (o.N === undefined) delete process.env.NPM_TOKEN; else process.env.NPM_TOKEN = o.N; if (o.L === undefined) delete process.env.LEERNESS_NPM_TOKEN; else process.env.LEERNESS_NPM_TOKEN = o.L; return r; } },
3038
3038
  { name: 'shell 주입 표면 제거: fetchNpmLatest execFile+pkg검증 + runCommandSafe argList 인용 (UR-0040 외부리뷰 1.9.300)', run: () => { const src = read(__filename); const npmFix = /cp\.execFile\('npm', \['view', pkg, 'version'\]/.test(src) && !/cp\.exec\(.npm view \$\{pkg\}/.test(src) && /패키지명 charset/.test(src); const argFix = /argList\.map\(_shellQuoteArg\)\.join/.test(src); return npmFix && argFix && typeof _shellQuoteArg === 'function'; } },
3039
3039
  { name: 'MCP requiredTier 메타데이터 + 정책 minTier 게이트 (UR-0041 외부리뷰 1.9.301)', run: () => { const T = require('../lib/mcp-tools'); const allValid = T.length >= 81 && T.every(t => PERMISSION_TIERS.includes(t.requiredTier)); const get = n => (T.find(t => t.name === n) || {}).requiredTier; const classOk = get('leerness_state_record') === 'safe-write' && get('leerness_provider_add') === 'safe-write' && get('leerness_web') === 'network' && get('leerness_handoff') === 'read-only' && get('leerness_audit') === 'read-only'; const src = read(__filename); const gateOk = /_tierRank\(minTier\) > _tierRank\(required\)/.test(src) && /_policyEnforce\(targetPath, cliArgs\.join\(' '\), _toolDef/.test(src); return allValid && classOk && gateOk; } },
3040
+ { name: 'verify-claim git diff 시맨틱 교차검증: _gitChangedFiles/_claimFileInGit + strict FAIL 통합 (UR-0042 외부리뷰 1.9.302)', run: () => { const fnOk = typeof _gitChangedFiles === 'function' && typeof _claimFileInGit === 'function'; const matchOk = _claimFileInGit('src/api.js', new Set(['src/api.js'])) === true && _claimFileInGit('./src/api.js', new Set(['src/api.js'])) === true && _claimFileInGit('other.js', new Set(['src/api.js'])) === false && _claimFileInGit('x', null) === null; const src = read(__filename); const wired = /git diff 교차검증/.test(src) && /\|\| !gitClaimOk/.test(src) && /_gitChangedFiles\(root\)/.test(src); return fnOk && matchOk && wired; } },
3040
3041
  { name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
3041
3042
  ];
3042
3043
  }
@@ -9404,6 +9405,33 @@ function _evidenceQuality(evidence) {
9404
9405
  if (!hasLog) missing.push('실행 로그(Command/Exit)');
9405
9406
  return { hasFile, hasTest, hasLog, ok: hasFile && hasTest, missing };
9406
9407
  }
9408
+ // 1.9.302 (UR-0042, 외부리뷰 Opus G-1): git 변경 파일 집합 — verify-claim 시맨틱 교차검증용.
9409
+ // working tree(staged/unstaged/untracked) + 직전 커밋(HEAD~1..HEAD) 변경을 합쳐, "주장한 파일이 실제로 변경됐는가" 판정.
9410
+ // git repo 아니거나 git 미설치면 null(검증 불가 → 페널티 X). 경로는 root-relative forward-slash.
9411
+ function _gitChangedFiles(root) {
9412
+ try {
9413
+ const st = cp.spawnSync('git', ['-C', root, 'status', '--porcelain', '--untracked-files=all'], { encoding: 'utf8', timeout: 10000 });
9414
+ if (st.status !== 0) return null; // git repo 아님 / git 없음
9415
+ const out = new Set();
9416
+ for (let line of (st.stdout || '').split('\n')) {
9417
+ if (line.length < 4) continue;
9418
+ let p = line.slice(3).trim(); // 'XY ' 상태 프리픽스 제거
9419
+ if (p.includes(' -> ')) p = p.split(' -> ').pop(); // rename
9420
+ p = p.replace(/^"|"$/g, ''); // quoted path
9421
+ if (p) out.add(p.replace(/\\/g, '/'));
9422
+ }
9423
+ const df = cp.spawnSync('git', ['-C', root, 'diff', '--name-only', 'HEAD~1', 'HEAD'], { encoding: 'utf8', timeout: 10000 });
9424
+ if (df.status === 0) for (let line of (df.stdout || '').split('\n')) { line = line.trim(); if (line) out.add(line.replace(/\\/g, '/')); }
9425
+ return out;
9426
+ } catch { return null; }
9427
+ }
9428
+ // 주장 파일이 git 변경 집합에 있는지(상대경로 prefix 차이 허용).
9429
+ function _claimFileInGit(claimed, gitSet) {
9430
+ if (!gitSet) return null;
9431
+ const c = String(claimed).replace(/\\/g, '/').replace(/^\.\//, '');
9432
+ for (const g of gitSet) { if (g === c || g.endsWith('/' + c) || c.endsWith('/' + g)) return true; }
9433
+ return false;
9434
+ }
9407
9435
  function verifyClaimCmd(root, taskId) {
9408
9436
  root = absRoot(root);
9409
9437
  if (!taskId) return fail('verify-claim <T-ID> 필요. 예: leerness verify-claim T-0008');
@@ -9450,6 +9478,12 @@ function verifyClaimCmd(root, taskId) {
9450
9478
 
9451
9479
  // 실제 파일 존재 검사
9452
9480
  const fileChecks = files.map(f => ({ file: f, exists: exists(path.join(root, f)) }));
9481
+ // 1.9.302 (UR-0042, 외부리뷰 Opus G-1): git diff 시맨틱 교차검증 — 주장한 파일이 실제로 변경됐는가.
9482
+ // "파일 존재"만으로는 "테스트만 통과하면 done" 허위완료를 못 막음(Opus). git working tree+직전커밋 변경과 대조.
9483
+ const gitChanged = _gitChangedFiles(root); // Set | null(git repo 아님 → 검증 불가)
9484
+ const gitApplicable = !!gitChanged && gitChanged.size > 0 && files.length > 0;
9485
+ const claimedInGit = gitApplicable ? files.filter(f => _claimFileInGit(f, gitChanged)) : [];
9486
+ const claimedNotInGit = gitApplicable ? files.filter(f => !_claimFileInGit(f, gitChanged)) : [];
9453
9487
  // 테스트 카운트: tests/test.js의 check( 또는 it( 또는 test( 개수
9454
9488
  let actualTestCount = null;
9455
9489
  const candidateTestFiles = ['tests/test.js', 'test/test.js', 'tests/index.js'];
@@ -9596,6 +9630,19 @@ function verifyClaimCmd(root, taskId) {
9596
9630
  for (const s of optimismSuspects) log(` · [${s.kind}] ${s.label}: evidence에 주장 있는데 코드에 호출 흔적 없음`);
9597
9631
  }
9598
9632
  }
9633
+ // 1.9.302 (UR-0042): git diff 시맨틱 교차검증 — 주장한 파일이 실제 git 변경(working tree+직전커밋)에 있는가.
9634
+ // advisory 기본 표시. --strict-claims 시 강한 불일치(변경 있는데 주장 파일이 하나도 git 변경에 없음)는 FAIL 기여.
9635
+ let gitClaimOk = true;
9636
+ if (gitChanged === null) {
9637
+ log(` - git diff 교차검증: ⊘ skip (git repo 아님 — 검증 불가)`);
9638
+ } else if (!gitApplicable) {
9639
+ log(` - git diff 교차검증: ⊘ skip (working tree 변경 0 또는 주장 파일 0 — 이미 커밋됐거나 해당 없음)`);
9640
+ } else {
9641
+ const strongMismatch = claimedInGit.length === 0; // 변경 있는데 주장 파일이 git 변경에 전무
9642
+ log(` - git diff 교차검증: ${strongMismatch ? '⚠ 불일치' : '✓'} 주장 ${files.length}개 중 실제 변경 ${claimedInGit.length}개${claimedNotInGit.length ? ` · git 변경에 없음: ${claimedNotInGit.slice(0, 5).join(', ')}` : ''}`);
9643
+ if (strongMismatch) log(` · 주장한 파일이 working tree/직전커밋 변경에 전무 — 변경이 더 오래전 커밋이거나, 실제로 변경 안 됐을 수 있음(허위완료 의심)`);
9644
+ if (has('--strict-claims') && strongMismatch) gitClaimOk = false; // strict 시 강한 불일치는 FAIL
9645
+ }
9599
9646
  // 1.9.287 (Codex 리뷰 수렴): --require-evidence — done 주장의 evidence 완전성(파일+테스트) 강제.
9600
9647
  // "테스트 통과만으로 done" 차단 — Codex 가 발견한 허위 완료 통과 갭 보강.
9601
9648
  const reqEvidence = has('--require-evidence');
@@ -9607,7 +9654,7 @@ function verifyClaimCmd(root, taskId) {
9607
9654
  log(` - evidence 완전성 (--require-evidence): ${evidenceQualityOk ? '✓ pass (파일+테스트 근거 있음)' : `✗ FAIL (누락: ${evq.missing.join(', ')})`}`);
9608
9655
  if (!evidenceQualityOk) log(` · done 주장은 수정 파일 경로 + 테스트명/개수 가 evidence 에 있어야 함 (테스트 통과만으로는 불충분)`);
9609
9656
  }
9610
- const overallFail = !allFilesOk || !testOk || (runResult && !runResult.skipped && !runTestsOk) || (has('--strict-claims') && !strictOk) || !evidenceQualityOk;
9657
+ const overallFail = !allFilesOk || !testOk || (runResult && !runResult.skipped && !runTestsOk) || (has('--strict-claims') && !strictOk) || !evidenceQualityOk || !gitClaimOk;
9611
9658
  // 1.9.287: 정직한 한계 고지 — 테스트 통과 ≠ 의미적 구현 정확성
9612
9659
  if (has('--strict-claims') || reqEvidence) {
9613
9660
  log('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.301",
3
+ "version": "1.9.302",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -3374,5 +3374,33 @@ total++;
3374
3374
  if (!ok) failed++;
3375
3375
  }
3376
3376
 
3377
+ // 1.9.302 회귀 (UR-0042 외부리뷰): verify-claim git diff 시맨틱 교차검증 (변경 파일 주장 ✓ / 미변경 주장 ⚠ + strict FAIL)
3378
+ total++;
3379
+ {
3380
+ let ok = false;
3381
+ try {
3382
+ const vDir = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-vc-'));
3383
+ const git = (...a) => cp.spawnSync('git', ['-C', vDir, ...a], { encoding: 'utf8', timeout: 15000 });
3384
+ const gi = git('init');
3385
+ if (gi.status !== 0) throw new Error('git 없음'); // git 미설치 환경 → skip(아래 catch 로 ok=false 방지 위해 통과 처리)
3386
+ git('config', 'user.email', 't@t'); git('config', 'user.name', 't');
3387
+ cp.spawnSync(process.execPath, [CLI, 'init', vDir, '--yes', '--language', 'ko', '--skills', 'recommended'], { encoding: 'utf8', timeout: 30000 });
3388
+ fs.mkdirSync(path.join(vDir, 'src'), { recursive: true });
3389
+ fs.writeFileSync(path.join(vDir, 'src', 'api.js'), 'v1'); fs.writeFileSync(path.join(vDir, 'old.js'), 'old');
3390
+ git('add', '-A'); git('commit', '-m', 'init');
3391
+ fs.writeFileSync(path.join(vDir, 'src', 'api.js'), 'v2 changed'); // working tree 변경
3392
+ cp.spawnSync(process.execPath, [CLI, 'task', 'add', '작업1', '--path', vDir, '--status', 'done', '--evidence', 'src/api.js 수정'], { encoding: 'utf8', timeout: 15000 });
3393
+ cp.spawnSync(process.execPath, [CLI, 'task', 'add', '작업2', '--path', vDir, '--status', 'done', '--evidence', 'old.js 수정'], { encoding: 'utf8', timeout: 15000 });
3394
+ const r1 = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0002', '--path', vDir], { encoding: 'utf8', timeout: 20000 });
3395
+ const r2 = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0003', '--path', vDir, '--strict-claims'], { encoding: 'utf8', timeout: 20000 });
3396
+ const changedClaimOk = /git diff 교차검증: ✓/.test(r1.stdout || ''); // 변경 파일 주장 → 매칭
3397
+ const mismatchDetected = /git diff 교차검증: ⚠ 불일치/.test(r2.stdout || '') && r2.status === 1; // 미변경 주장 + strict → FAIL
3398
+ ok = changedClaimOk && mismatchDetected;
3399
+ fs.rmSync(vDir, { recursive: true, force: true });
3400
+ } catch (e) { if (/git 없음/.test(e.message)) { ok = true; console.log(' (git 미설치 — git 교차검증 e2e skip)'); } }
3401
+ console.log(ok ? '✓ B(1.9.302) verify-claim git diff 교차검증: 변경파일 주장 ✓ / 미변경 주장 ⚠ strict FAIL (UR-0042)' : '✗ verify-claim git 교차검증 실패');
3402
+ if (!ok) failed++;
3403
+ }
3404
+
3377
3405
  console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
3378
3406
  if (failed > 0) process.exit(1);