leerness 1.36.75 → 1.36.76

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,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.36.76 — 2026-07-25 — 9차 헌트 이월 3건 종결 (P1 1·P2 2) — 헌트 8/8 전건 처리
4
+
5
+ - **P1 (#3) 손상 아카이브 복원 거부**: 잘린 UTF-8 아카이브가 U+FFFD 를 품은 채 "정상 복원"돼 손상 데이터가 active 로 유입되고 아카이브 원본이 소비되던 것(codex 정확 재현으로 확정 — 지난 라운드 제 절단 변형은 미재현이라 이월했었다). 치환문자 감지 시 복원 중단·아카이브 무변경.
6
+ - **P2 (#2) MCP read-only 가 대상을 변형**: ① 텔레메트리가 존재하지 않는 대상까지 디렉토리·usage-stats 를 생성 → 초기화된 하네스에만 기록. ② read-only 선언 `leerness_env_detect` 가 environment.json 을 기록 → MCP 경로는 `--no-write`(CLI 직접 호출은 종전대로 persist).
7
+ - **P2 (#8) 긴/이상 URL 파생 id**: 300자 호스트·IPv6 가 raw ENOENT 로 터지던 것 — 호스트 슬러그화 + 길이 상한 80(초과 시 절단+짧은 해시). 정상 id 는 종전과 동일(문서화 예시 무변).
8
+ - **(R-0001 검수 19회전, read-only 도구 59종 전수 스윕)**: (P1) **read-only 로 오선언된 쓰기 도구 9종** — handoff(last-handoff/tech-profile)·session_close(6파일)·skill_match·lsp·review_request·pre_wake_audit·audit(fix)·slash_commands 를 `safe-write` 로, task_export(--to 임의 경로)는 `project-write` 로 재선언 — enforce 정책이 이제 실제로 막는다. (#2) U+FFFD 판정을 **원시 바이트 fatal decode** 로 교체 — 사용자가 정당하게 붙여넣은 유효 U+FFFD 문자는 복원 가능(오탐 제거), malformed 바이트만 거부. (#3) 절단 해시 시드에 name 포함 — 같은 긴 URL·다른 name 이 같은 id 로 붕괴해 덮어쓰던 것. (#4) 충돌 접미사 후에도 상한 80 유지(단일 캡 함수). (Low) Windows 예약 이름(con 등) 회피.
9
+ - 무회귀 실측: 정상 아카이브 복원·유효 U+FFFD 복원·초기화된 하네스 텔레메트리·CLI env persist·기존 id 형식.
10
+ - 검증: selftest 334(티어 단언 신분류로 갱신), e2e +1(7 단언). 9차 헌트 8건 전건 처리 완료.
11
+
3
12
  ## 1.36.75 — 2026-07-25 — UR-0066: 디자인 시안 우선 워크플로 (preview mockup) — R-0001 검수 18회전
4
13
 
5
14
  사용자 요청: 웹페이지/디자인 작업은 코드 구현 전에 디자인 시안을 먼저 제시하고 수정/승인을 질문으로 받도록.
package/README.md CHANGED
@@ -122,7 +122,7 @@ MIT
122
122
  <!-- leerness:project-readme:start -->
123
123
  ## Leerness Project Harness
124
124
 
125
- 이 프로젝트는 Leerness v1.36.75 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
125
+ 이 프로젝트는 Leerness v1.36.76 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
126
126
 
127
127
  ### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
128
128
 
@@ -176,7 +176,7 @@ leerness memory restore decision <date|title>
176
176
 
177
177
  ### MCP server (외부 AI 통합)
178
178
 
179
- Leerness v1.36.75는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
179
+ Leerness v1.36.76는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
180
180
 
181
181
  ```jsonc
182
182
  // 카테고리별
@@ -197,7 +197,7 @@ Leerness v1.36.75는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
197
197
  `<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
198
198
  1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) 회귀 테스트 갱신 → 4) 전체 e2e 스위트 통과 → 5) npm publish + git tag → 6) main push → 7) session close → 8) 다음 라운드 예약.
199
199
 
200
- 현재 누적: **v1.9.x → 1.36.75 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
200
+ 현재 누적: **v1.9.x → 1.36.76 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
201
201
 
202
202
  ### 성능 가이드
203
203
 
@@ -235,5 +235,5 @@ leerness release pack --close --auto-main-push
235
235
  - `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
236
236
  - `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
237
237
 
238
- Last synced by Leerness v1.36.75: 2026-07-25
238
+ Last synced by Leerness v1.36.76: 2026-07-25
239
239
  <!-- leerness:project-readme:end -->
package/bin/leerness.js CHANGED
@@ -34,7 +34,7 @@ const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE
34
34
  const { tokenizeForRank: _tokenizeForRank, expandQuery: _expandQuery, scoreHits: _scoreHits, suggestTerms: _suggestTerms } = require('../lib/search-core'); // 1.36.23: memory search 랭킹 코어(순수·0-deps)
35
35
  const { findCorruptedStateJson: _findCorruptedStateJson } = require('../lib/state-integrity'); // 1.36.1 (클린룸 리뷰 FN): .harness/*.json 상태 무결성 (audit/health/check 공유)
36
36
 
37
- const VERSION = '1.36.75';
37
+ const VERSION = '1.36.76';
38
38
 
39
39
  // 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
40
40
  // 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
@@ -3204,7 +3204,7 @@ function _selfTestCases() {
3204
3204
  { name: 'writeUtf8: 원자적 쓰기(temp→rename) 손상방지 행위 (UR-0038 외부리뷰 / CV-5 행위화 1.9.366)', run: () => { if (typeof writeUtf8 !== 'function') return false; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_wu_')); try { const f = path.join(tmp, 'sub', 'a.txt'); writeUtf8(f, '한글 UTF-8 내용'); const okContent = read(f) === '한글 UTF-8 내용'; const noTmpLeft = fs.readdirSync(path.dirname(f)).every(n => !n.includes('.tmp-')); return okContent && noTmpLeft; } finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } } },
3205
3205
  { 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; } },
3206
3206
  { name: 'shell 주입 표면 제거: fetchNpmLatest execFile+pkg검증 + runCommandSafe argList 인용 (UR-0040 외부리뷰 1.9.300)', run: () => { const src = read(__filename); const npmFix = /'view', pkg, 'version'/.test(src) && !/cp\.exec\(.npm view \$\{pkg\}/.test(src) && /패키지명 charset/.test(src) && !/cp\.execFile\('npm', \[[^\]]*\], \{ timeout: 12000, shell:/.test(src); const argFix = /argList\.map\(_shellQuoteArg\)\.join/.test(src); return npmFix && argFix && typeof _shellQuoteArg === 'function'; } },
3207
- { 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; } },
3207
+ { 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') === 'safe-write' && get('leerness_audit') === 'safe-write' && get('leerness_task_export') === 'project-write'; const src = read(__filename); const gateOk = /_tierRank\(minTier\) > _tierRank\(required\)/.test(src) && /_policyEnforce\(targetPath, cliArgs\.join\(' '\), _toolDef/.test(src); return allValid && classOk && gateOk; } },
3208
3208
  { 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; } },
3209
3209
  { name: '_withLock/_updateRun: lost-update 락(O_EXCL+재진입) + 적용 (UR-0043 외부리뷰 1.9.303)', run: () => { const src = read(__filename); const fnOk = typeof _withLock === 'function' && typeof _sleepSyncMs === 'function' && typeof _updateRun === 'function'; const reentrant = /if \(_heldLocks\.has\(lockPath\)\) return fn\(\)/.test(src); const excl = /fs\.openSync\(lockPath, 'wx'\)/.test(src); const applied = /const id = _withLock\(progressPath\(root\)/.test(src) && /_updateRun\(root, curId/.test(src); return fnOk && reentrant && excl && applied; } },
3210
3210
  { name: 'lib/analyzers: 분석/검증 함수 4종 모듈 단일출처 분리 (UR-0025 1.9.304)', run: () => { const m = require('../lib/analyzers'); return m._evidenceQuality === _evidenceQuality && m._shellGuardAnalyze === _shellGuardAnalyze && m._parseEvidenceStats === _parseEvidenceStats && m._claimFileInGit === _claimFileInGit && !/function _evidenceQuality\(evidence\) \{/.test(read(__filename)) && !/function _shellGuardAnalyze\(cmd, ctx\) \{/.test(read(__filename)); } },
@@ -3316,7 +3316,7 @@ function _selfTestCases() {
3316
3316
  const guardOk = s.includes('function _assertStoreParsable') && s.includes("code: 'E_STORE_CORRUPT'")
3317
3317
  && s.includes('_guardStore(has(') && s.includes("_assertStoreParsable(teamsJsonPath")
3318
3318
  && s.includes("_assertStoreParsable(_platformConstraintsPath");
3319
- const hashOk = s.includes('function _shortHash') && s.includes("id = id + '-' + _shortHash(url)") && s.includes('urls:\\s*\\r?\\n');
3319
+ const hashOk = s.includes('function _shortHash') && s.includes("id = _apiSkillIdCap(id + '-' + _shortHash(url), url, name)") && s.includes('urls:\\s*\\r?\\n');
3320
3320
  return transitiveOk && cycleSafe && aliasOk && guardOk && hashOk;
3321
3321
  } },
3322
3322
  { name: '반복 마이그레이션 커스텀 보존 (1.36.28, 사용자 보고): _managedMerge 라인-diff 이월 — 2번째부터 유실되던 CLAUDE/AGENTS 커스텀 지시 (행위검사)', run: () => {
@@ -5977,14 +5977,26 @@ function _shortHash(s) {
5977
5977
  function _apiSkillsDir(root) {
5978
5978
  return path.join(absRoot(root), '.harness', 'api-skills');
5979
5979
  }
5980
+ // 1.36.76 (검수 #3/#4/Low): id 최종 안전화 단일 지점 — 상한 80(초과 시 절단+해시), 해시 시드는 url+name 전체
5981
+ // 정체성(같은 URL·다른 name 이 같은 id 로 붕괴하던 것), Windows 예약 이름(con/prn/aux/nul/com1..9/lpt1..9) 회피.
5982
+ // 충돌 접미사(-hash) 추가 후에도 이 함수를 다시 거쳐 상한이 유지된다.
5983
+ function _apiSkillIdCap(id, url, name) {
5984
+ const seed = String(url || '') + '' + String(name || '');
5985
+ if (!id || id.length > 80) id = (id || 'api').slice(0, 72).replace(/-+$/, '') + '-' + _shortHash(seed);
5986
+ if (/^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(id)) id = id + '-' + _shortHash(seed);
5987
+ return id;
5988
+ }
5980
5989
  function _apiSkillId(url, name) {
5981
5990
  // 도메인 + path slug 로 id 생성. 예: developers.coupangcorp.com/articles/360033877853 → coupang-articles-360033877853
5982
5991
  try {
5983
5992
  const u = new URL(url);
5984
- const host = u.hostname.replace(/^www\./, '').split('.').slice(0, -1).join('-') || u.hostname;
5993
+ // 1.36.76 (9차 헌트 #8): 파생 id 를 항상 안전한 파일명으로 — 긴 호스트(300자)·IPv6([::1])
5994
+ // raw ENOENT 를 뱉던 것. 호스트도 슬러그화 + 전체 길이 상한(80) 초과 시 절단+짧은 해시로 유일성 유지.
5995
+ const host = (u.hostname.replace(/^www\./, '').split('.').slice(0, -1).join('-') || u.hostname).replace(/[^a-z0-9가-힣\-_]+/gi, '-');
5985
5996
  const pathSlug = u.pathname.replace(/^\/+|\/+$/g, '').replace(/[^a-z0-9가-힣\-_]+/gi, '-').slice(-50);
5986
5997
  const base = `${host}-${pathSlug}`.replace(/-+/g, '-').replace(/^-|-$/g, '');
5987
- return (name ? base + '-' + name.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 30) : base).replace(/-+/g, '-');
5998
+ let id = (name ? base + '-' + name.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 30) : base).replace(/-+/g, '-').replace(/^-|-$/g, '');
5999
+ return _apiSkillIdCap(id, url, name);
5988
6000
  } catch { return 'api-' + Date.now(); }
5989
6001
  }
5990
6002
  function _fetchUrl(url, opts = {}) {
@@ -6239,7 +6251,7 @@ async function apiSkillCmd(root, sub) {
6239
6251
  const existing = fs.readFileSync(_idFile(id), 'utf8');
6240
6252
  const um = existing.match(/^urls:\s*\r?\n\s*-\s*(\S.*)$/m); // 'urls:' 블록의 첫 항목만 (--- 구분선 오매치 방지)
6241
6253
  const storedUrl = um ? um[1].trim() : '';
6242
- if (storedUrl && storedUrl !== url) id = id + '-' + _shortHash(url);
6254
+ if (storedUrl && storedUrl !== url) id = _apiSkillIdCap(id + '-' + _shortHash(url), url, name); // 1.36.76 (검수 #4): 접미 후에도 상한 80 유지
6243
6255
  }
6244
6256
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
6245
6257
  const md = _serializeAPISkill(id, skillName, [url], direction, doc);
@@ -6263,7 +6275,7 @@ async function apiSkillCmd(root, sub) {
6263
6275
  const existing = fs.readFileSync(_idFileS(id), 'utf8');
6264
6276
  const um = existing.match(/^urls:\s*\r?\n\s*-\s*(\S.*)$/m);
6265
6277
  const storedUrl = um ? um[1].trim() : '';
6266
- if (storedUrl && storedUrl !== url) id = id + '-' + _shortHash(url);
6278
+ if (storedUrl && storedUrl !== url) id = _apiSkillIdCap(id + '-' + _shortHash(url), url, name); // 1.36.76 (검수 #4): 접미 후에도 상한 80 유지
6267
6279
  }
6268
6280
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
6269
6281
  const stub = {
@@ -9738,6 +9750,11 @@ function memoryRestoreCmd(root, surface, target) {
9738
9750
  const hd = path.join(root, '.harness');
9739
9751
  const archivePath = path.join(hd, `${surface}.archive.md`);
9740
9752
  if (!exists(archivePath)) return fail(`${surface}.archive.md 없음 — 복원할 항목 없음`);
9753
+ // 1.36.76 (9차 헌트 #3 P1) → (검수 #2): 잘린/깨진 UTF-8 아카이브가 손상 데이터를 active 로 유입시키던 것.
9754
+ // 판정은 "디코딩 결과에 U+FFFD 포함"이 아니라 **원시 바이트의 UTF-8 유효성**(fatal decode) — 사용자가 정당하게
9755
+ // 붙여넣은 실제 U+FFFD 문자(유효 EF BF BD)는 통과하고, 진짜 malformed 바이트만 거부한다.
9756
+ try { new TextDecoder('utf-8', { fatal: true }).decode(readBuf(archivePath)); }
9757
+ catch { failJson(has('--json'), 'archive_corrupt', `${surface}.archive.md 의 UTF-8 바이트 손상 — 파일이 잘렸거나 인코딩 깨짐. 복원 중단(아카이브 무변경) — 원본 복구 후 재시도`); return; }
9741
9758
  const text = read(archivePath);
9742
9759
  // archive 헤더 (# X archive) 와 본문 분리
9743
9760
  const headerMatch = text.match(/^(# [^\n]*\n+)([\s\S]*)$/);
@@ -18037,6 +18054,9 @@ function _bumpUsage(root, cmdName) {
18037
18054
  // 1.9.70: MCP tools/call 자동 사용 통계 — 도구별 호출 카운트
18038
18055
  function _bumpMcpUsage(root, toolName) {
18039
18056
  try {
18057
+ // 1.36.76 (9차 헌트 #2a): read-only 호출조차 텔레메트리가 대상 프로젝트에 디렉토리·파일을 만들었다
18058
+ // (존재하지 않는 대상까지 통째로 생성). 통계는 이미 초기화된 하네스에만 기록 — 읽기가 대상을 변형하지 않는다.
18059
+ if (!exists(path.join(absRoot(root), '.harness'))) return;
18040
18060
  const stats = _readUsageStats(root);
18041
18061
  if (!stats.mcp) stats.mcp = { tools: {} };
18042
18062
  if (!stats.mcp.tools) stats.mcp.tools = {};
@@ -18810,7 +18830,7 @@ function _mcpToCliArgs(name, args, targetPath) {
18810
18830
  if (args.coChangesWith) cliArgs.push('--co-changes-with', String(args.coChangesWith));
18811
18831
  break;
18812
18832
  // 1.9.145: 실행 환경 자동 감지
18813
- case 'leerness_env_detect': cliArgs = ['env', 'detect', targetPath, '--json']; break;
18833
+ case 'leerness_env_detect': cliArgs = ['env', 'detect', targetPath, '--json', '--no-write']; break; // 1.36.76 (#2b): read-only 선언 도구는 environment.json 을 쓰지 않는다 (persist 는 CLI 직접 호출로)
18814
18834
  // 1.9.158: Provider Registry — 외부 AI 가 등록된 provider 회수
18815
18835
  case 'leerness_provider_list': cliArgs = ['provider', 'list', '--path', targetPath, '--json']; break;
18816
18836
  // 1.9.159: Provider Registry CRUD — 외부 AI 가 자가 확장
package/lib/mcp-tools.js CHANGED
@@ -4,9 +4,9 @@
4
4
  'use strict';
5
5
 
6
6
  module.exports = [
7
- { name: 'leerness_handoff', requiredTier: 'read-only', description: '워크스페이스 컨텍스트(plan/progress/decisions) 적재', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
7
+ { name: 'leerness_handoff', requiredTier: 'safe-write', description: '워크스페이스 컨텍스트(plan/progress/decisions) 적재', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
8
8
  { name: 'leerness_drift_check', requiredTier: 'read-only', description: '1.9.136 — AI 에이전트 leerness 미사용 drift 자동 감지 JSON ({ root, score, level, signals[], healthy }). 5+ 신호 + 4단계 레벨 (🟢 healthy / 🟡 warning / 🟠 caution / 🔴 critical). 보안 신호 통합 (1.9.78)', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
9
- { name: 'leerness_audit', requiredTier: 'read-only', description: '1.9.102 — 워크스페이스 일관성 감사 JSON (warnings/failures/fixed/healthy + findings[]. kind 11종: design_dup/design_system_default/reuse_map_empty/milestone_unlinked/handoff_not_generated/current_state_stale/readme_version_mismatch/npm_cve/gitignore_missing_secrets/env_keys_missing/strict_promoted)', inputSchema: { type: 'object', properties: { path: { type: 'string' }, fix: { type: 'boolean' }, strict: { type: 'boolean' } } } },
9
+ { name: 'leerness_audit', requiredTier: 'safe-write', description: '1.9.102 — 워크스페이스 일관성 감사 JSON (warnings/failures/fixed/healthy + findings[]. kind 11종: design_dup/design_system_default/reuse_map_empty/milestone_unlinked/handoff_not_generated/current_state_stale/readme_version_mismatch/npm_cve/gitignore_missing_secrets/env_keys_missing/strict_promoted)', inputSchema: { type: 'object', properties: { path: { type: 'string' }, fix: { type: 'boolean' }, strict: { type: 'boolean' } } } },
10
10
  { name: 'leerness_verify_claim', requiredTier: 'read-only', description: 'AI 거짓 완료 자동 검증 (evidence 파일 + 실 테스트 실행). 1.9.309(UR-0048): done/완료 주장은 evidence(수정파일+테스트) 기본 강제 — 증거 없는 done 은 FAIL(exit 1). lenient=true 로 완화, runTests/strictClaims 추가 가능. 응답 verdict.evidenceComplete 포함.', inputSchema: { type: 'object', properties: { taskId: { type: 'string' }, path: { type: 'string' }, runTests: { type: 'boolean' }, strictClaims: { type: 'boolean' }, lenient: { type: 'boolean' } }, required: ['taskId'] } },
11
11
  { name: 'leerness_verify_claim_all', requiredTier: 'read-only', description: '1.33.3 — 모든 done/완료 주장을 한 번에 검증(CI·스케일). progress-tracker 의 done 항목 전부를 verify-claim 정밀 검사(파일 존재·스텁·부풀린 카운트·증거 완전성)로 일괄 점검. 응답 { ok, total, failed, results:[{id,ok,reasons}] }. 세션 마감 전 "내 완료 주장 전부 증거와 맞는가?" 자가 점검용. runTests/strictClaims/lenient 추가 가능.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, runTests: { type: 'boolean' }, strictClaims: { type: 'boolean' }, lenient: { type: 'boolean' } } } },
12
12
  { name: 'leerness_contract_verify', requiredTier: 'read-only', description: '명세 ↔ 구현 함수/필드 일치 자동 검사', inputSchema: { type: 'object', properties: { spec: { type: 'string' }, impl: { type: 'string' } }, required: ['spec', 'impl'] } },
@@ -14,13 +14,13 @@ module.exports = [
14
14
  { name: 'leerness_reuse_map', requiredTier: 'read-only', description: '워크스페이스 중복 함수/capability 자동 감지 (--all-apps + fuzzy 매칭)', inputSchema: { type: 'object', properties: { path: { type: 'string' }, allApps: { type: 'boolean' }, strictElements: { type: 'boolean' } } } },
15
15
  { name: 'leerness_whats_new', requiredTier: 'read-only', description: 'CHANGELOG 차분 자동 추출 (from → to 사이 신규 명령/플래그/파일). 1.36.69: 기본 최신 30개 버전만 반환(응답 폭주 방지) — 응답의 totalVersions/shown/truncated 로 절단 여부 확인, 더 필요하면 limit(개수) 또는 all=true(전체, 대용량 주의) 지정.', inputSchema: { type: 'object', properties: { from: { type: 'string' }, to: { type: 'string' }, limit: { type: 'number' }, all: { type: 'boolean' } } } },
16
16
  { name: 'leerness_usage_stats', requiredTier: 'read-only', description: 'leerness 명령별 누적 호출 통계 + drift 통계', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
17
- { name: 'leerness_session_close', requiredTier: 'read-only', description: '1.9.103 — 세션 마감 JSON (handoff/current-state/task-log 갱신 + taskCounts + rules + skillCandidates + drift + topCommands + mcpStats). 외부 AI가 마감 통계 자동 회수', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
17
+ { name: 'leerness_session_close', requiredTier: 'safe-write', description: '1.9.103 — 세션 마감 JSON (handoff/current-state/task-log 갱신 + taskCounts + rules + skillCandidates + drift + topCommands + mcpStats). 외부 AI가 마감 통계 자동 회수', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
18
18
  { name: 'leerness_skill_suggest', requiredTier: 'read-only', description: '1.9.53 — 사용 패턴 자동 분석 → 새 skill 후보 제안 (Hermes-style 자동 학습)', inputSchema: { type: 'object', properties: { path: { type: 'string' }, min: { type: 'number' }, days: { type: 'number' } } } },
19
19
  { name: 'leerness_lessons', requiredTier: 'read-only', description: '1.9.7/54 — 과거 결정·실수 자동 회수 (--auto: 현재 task 키워드 자동 추출)', inputSchema: { type: 'object', properties: { path: { type: 'string' }, query: { type: 'string' }, auto: { type: 'boolean' }, limit: { type: 'number' } } } },
20
- { name: 'leerness_task_export', requiredTier: 'read-only', description: '1.9.60/66 — leerness task → Claude Code TodoWrite 호환 JSON (외부 AI 양방향 sync)', inputSchema: { type: 'object', properties: { path: { type: 'string' }, to: { type: 'string' } } } },
20
+ { name: 'leerness_task_export', requiredTier: 'project-write', description: '1.9.60/66 — leerness task → Claude Code TodoWrite 호환 JSON (외부 AI 양방향 sync)', inputSchema: { type: 'object', properties: { path: { type: 'string' }, to: { type: 'string' } } } },
21
21
  { name: 'leerness_env_check', requiredTier: 'read-only', description: '1.9.71/73 — .env vs .env.example 동기화 검사 (보안: 키만, 값 미노출). exit 1 if 누락 키 있음', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
22
22
  { name: 'leerness_brainstorm', requiredTier: 'read-only', description: '1.9.16/72/77 — 누적 컨텍스트(decisions+skills+tasks+rules+evidence+lessons+skillHistory+taskLogFails) 자원 회수. 외부 AI가 새 작업 시작 전 호출', inputSchema: { type: 'object', properties: { topic: { type: 'string' }, path: { type: 'string' }, allApps: { type: 'boolean' } }, required: ['topic'] } },
23
- { name: 'leerness_skill_match', requiredTier: 'read-only', description: '1.9.45/50/83 — 사용자 task 키워드에 매칭되는 설치된 skill 추천 (jaccard 또는 embedding). 1.9.68 rolling history 자동 누적', inputSchema: { type: 'object', properties: { query: { type: 'string' }, path: { type: 'string' }, useEmbedding: { type: 'boolean' } }, required: ['query'] } },
23
+ { name: 'leerness_skill_match', requiredTier: 'safe-write', description: '1.9.45/50/83 — 사용자 task 키워드에 매칭되는 설치된 skill 추천 (jaccard 또는 embedding). 1.9.68 rolling history 자동 누적', inputSchema: { type: 'object', properties: { query: { type: 'string' }, path: { type: 'string' }, useEmbedding: { type: 'boolean' } }, required: ['query'] } },
24
24
  { name: 'leerness_skill_list', requiredTier: 'read-only', description: '1.9.84 — 워크스페이스에 설치된 skill 목록 + 사용 횟수 + 출처 (catalog/user). 외부 AI가 사용 가능한 skill 조회', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
25
25
  { name: 'leerness_health', requiredTier: 'read-only', description: '1.9.85/86 — 종합 헬스 체크 (drift + 보안 + skills + MCP + tasks + issues 배열). 외부 AI가 워크스페이스 상태 한 번에 확인', inputSchema: { type: 'object', properties: { path: { type: 'string' }, strict: { type: 'boolean' } } } },
26
26
  { name: 'leerness_skill_search', requiredTier: 'read-only', description: '1.9.90/91 — capability 배열에서 부분 일치하는 skill 검색 (substring + case-insensitive). skill match와 다른 정확 매칭', inputSchema: { type: 'object', properties: { capability: { type: 'string' }, path: { type: 'string' } }, required: ['capability'] } },
@@ -57,11 +57,11 @@ module.exports = [
57
57
  { name: 'leerness_provider_remove', requiredTier: 'safe-write', description: '1.9.159 — Provider Registry 에서 사용자 정의 provider 제거. 인자: { id (required), path? }. 빌트인 5종 id 는 제거 불가 (override 만 제거 가능). 🎉 MCP 50 도구 마일스톤 — Provider Registry CRUD MCP 완성 (list/add/remove)', inputSchema: { type: 'object', properties: { id: { type: 'string' }, path: { type: 'string' } }, required: ['id'] } },
58
58
  { name: 'leerness_web', requiredTier: 'network', description: '1.9.168 — Web Bridge (1.9.165 playwright opt-in). sub: check (설치 + permissions.browser 확인) | screenshot (URL → PNG) | extract (URL + CSS selector → DOM 텍스트). 외부 AI가 leerness 의 웹 자동화 능력을 직접 호출. playwright 미설치 시 친절 안내 (graceful). 인자: { sub (required), url?, out?, selector?, path? }', inputSchema: { type: 'object', properties: { sub: { type: 'string', enum: ['check', 'screenshot', 'extract'] }, url: { type: 'string' }, out: { type: 'string' }, selector: { type: 'string' }, path: { type: 'string' } }, required: ['sub'] } },
59
59
  { name: 'leerness_pc', requiredTier: 'shell-write', description: '1.9.168 — PC Bridge (1.9.166 robotjs/nut-tree opt-in). sub: check (설치 + permissions.mouse/keyboard) | click (x,y) | type (text) | screenshot (out). ⚠ full permissions 권장 (mouse/keyboard 접근). 외부 AI가 데스크탑 자동화 능력을 직접 호출. 인자: { sub (required), x?, y?, text?, out?, path? }', inputSchema: { type: 'object', properties: { sub: { type: 'string', enum: ['check', 'click', 'type', 'screenshot'] }, x: { type: 'number' }, y: { type: 'number' }, text: { type: 'string' }, out: { type: 'string' }, path: { type: 'string' } }, required: ['sub'] } },
60
- { name: 'leerness_lsp', requiredTier: 'read-only', description: '1.9.168 — LSP Bridge (1.9.167 typescript opt-in + regex fallback). sub: check (설치 여부) | symbols (file → function/class/interface/type/enum 목록) | references (name + in 디렉토리 → 호출 위치). 외부 AI가 코드 인텔리전스를 직접 호출 (의존성 0 fallback 동작). 🎉 MCP 53 도구 마일스톤. 인자: { sub (required), file?, name?, in?, path? }', inputSchema: { type: 'object', properties: { sub: { type: 'string', enum: ['check', 'symbols', 'references'] }, file: { type: 'string' }, name: { type: 'string' }, in: { type: 'string' }, path: { type: 'string' } }, required: ['sub'] } },
61
- { name: 'leerness_review_request', requiredTier: 'read-only', description: '1.9.176 — 사용자 요청 사전 검토 (사용자 명시 요청). AI 에이전트가 사용자 요구를 **무조건 구현 전**에 호출. 분석: 1) estimatedType (route 추정), 2) conflicts (lesson 실패/진행중 task), 3) reuseCandidates (skill/reuse-map 매칭), 4) lessonsRecall (과거 결정), 5) planConflicts (진행중 milestone), 6) featureConflicts (feature graph 영역 겹침), 7) recommendedSteps (작업 유형별 3-5 단계), 8) efficiencyHints, 9) proceed (true/false). 사용자 결정 도움. 인자: { request (required), path? }', inputSchema: { type: 'object', properties: { request: { type: 'string' }, path: { type: 'string' } }, required: ['request'] } },
60
+ { name: 'leerness_lsp', requiredTier: 'safe-write', description: '1.9.168 — LSP Bridge (1.9.167 typescript opt-in + regex fallback). sub: check (설치 여부) | symbols (file → function/class/interface/type/enum 목록) | references (name + in 디렉토리 → 호출 위치). 외부 AI가 코드 인텔리전스를 직접 호출 (의존성 0 fallback 동작). 🎉 MCP 53 도구 마일스톤. 인자: { sub (required), file?, name?, in?, path? }', inputSchema: { type: 'object', properties: { sub: { type: 'string', enum: ['check', 'symbols', 'references'] }, file: { type: 'string' }, name: { type: 'string' }, in: { type: 'string' }, path: { type: 'string' } }, required: ['sub'] } },
61
+ { name: 'leerness_review_request', requiredTier: 'safe-write', description: '1.9.176 — 사용자 요청 사전 검토 (사용자 명시 요청). AI 에이전트가 사용자 요구를 **무조건 구현 전**에 호출. 분석: 1) estimatedType (route 추정), 2) conflicts (lesson 실패/진행중 task), 3) reuseCandidates (skill/reuse-map 매칭), 4) lessonsRecall (과거 결정), 5) planConflicts (진행중 milestone), 6) featureConflicts (feature graph 영역 겹침), 7) recommendedSteps (작업 유형별 3-5 단계), 8) efficiencyHints, 9) proceed (true/false). 사용자 결정 도움. 인자: { request (required), path? }', inputSchema: { type: 'object', properties: { request: { type: 'string' }, path: { type: 'string' } }, required: ['request'] } },
62
62
  { name: 'leerness_requests_audit', requiredTier: 'read-only', description: '1.9.216 (1.9.207 사용자 명시) — 사용자 명시 요청 누락 확인 절차. .harness/user-requests.json 의 open/in-progress 요청을 task-log/plan/decisions 와 매칭 → missing/tracked/stale 분류. 외부 AI가 "사용자가 했던 요청 중 누락된 게 있나?"를 직접 회수. 응답: { total, open, missing[], tracked[], stale[], completed, dropped }. 인자: { path? }', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
63
63
  { name: 'leerness_constraints_check', requiredTier: 'read-only', description: '1.9.216 (1.9.208 사용자 명시) — 플랫폼/API 제약 사전 체크. 사용자 요청 텍스트에서 플랫폼 alias 매칭 (stripe/openai/anthropic/github/discord/twitter 6종 + 사용자 정의) → 각 플랫폼의 rate-limit / idempotency / auth / cost 제약 노출. 외부 AI가 "이 기능 구현 전 어떤 규정을 봐야 하나?"를 직접 회수. 인자: { request (required), path? }', inputSchema: { type: 'object', properties: { request: { type: 'string' }, path: { type: 'string' } }, required: ['request'] } },
64
- { name: 'leerness_pre_wake_audit', requiredTier: 'read-only', description: '1.9.216 (1.9.209 사용자 명시) — sleep 전 sub-agent audit. 6 영역 점검: missing-user-requests / stale-in-progress / drift-handoff-stale / wakeup-missed / next-action-pending / auto-resume-plan. 외부 AI가 "깨어나기 전 점검할 부분"을 회수. 응답: { auditedAt, findings: {critical, warning, info}, summary }. 인자: { path? }', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
64
+ { name: 'leerness_pre_wake_audit', requiredTier: 'safe-write', description: '1.9.216 (1.9.209 사용자 명시) — sleep 전 sub-agent audit. 6 영역 점검: missing-user-requests / stale-in-progress / drift-handoff-stale / wakeup-missed / next-action-pending / auto-resume-plan. 외부 AI가 "깨어나기 전 점검할 부분"을 회수. 응답: { auditedAt, findings: {critical, warning, info}, summary }. 인자: { path? }', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
65
65
  { name: 'leerness_intent_classify', requiredTier: 'read-only', description: '1.9.216 (1.9.213 사용자 명시) — 사용자 의도 파악 + scope expansion 게이트. 응답: { intent: precise|broad|default, signals, domain, explicitMentions, expansionCandidates, mode: dry-run }. 3원칙 안전: (1) Always-Off Opt-In, (2) Dry-run 기본 (실행 X), (3) 명시 vs 추론 분리 라벨링. 5 도메인 (game/web/api/cli/data). 외부 AI가 "이 요청은 정확히 그것만 / 포괄적 / 기본인가?"를 회수. 인자: { request (required), path? }', inputSchema: { type: 'object', properties: { request: { type: 'string' }, path: { type: 'string' } }, required: ['request'] } },
66
66
  { name: 'leerness_idempotency_audit', requiredTier: 'read-only', description: '1.9.216 (1.9.212 사용자 명시) — 멱등성 위반 탐지. 4영역 점검: rule-duplicate (medium) / task-duplicate-request (medium) / user-request-duplicate (low) / wakeup-duplicate (high). 응답: { violations[], verified[], summary: {totalViolations, high/medium/low, overall} }. 외부 AI가 "워크스페이스에 중복/충돌이 있나?"를 회수. 🎉 MCP 58 도구 마일스톤 (50→58). 인자: { path? }', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
67
67
  { name: 'leerness_session_resume', requiredTier: 'read-only', description: '1.9.221 (1.9.220 사용자 명시) — 비정상 종료 감지 + 자율 재개. 5신호 분석: last-handoff-stale / wakeup-missed / in-progress-stale / auto-resume-plan-unused / release-branch-pending. 응답: { abnormalShutdown, severity (none/low/medium/high), signals[], resumeGuide[] }. 외부 AI가 "절전/시스템종료/세션종료 후 leerness 상태가 정상인가? 어떻게 재개?"를 회수. 🎉 MCP 60 도구 마일스톤 (53→60, +7 in 1.9.168/216/221). 인자: { path? }', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
@@ -79,7 +79,7 @@ module.exports = [
79
79
  { name: 'leerness_api_skill', requiredTier: 'safe-write', description: '1.9.245 (사용자 명시 UR-0015) — API 문서·관련링크 자동 캐시. 공식 API 문서 URL을 fetch 하고 1단계 same-domain 관련 링크까지 정리 → .harness/api-skills/<id>.md 저장. 후속 같은 API 관련 작업 시 자동 참조. 응답: list (skills 배열) / show (전체 본문) / match (task 매칭 결과). 인자: { path?, sub ("list"|"show"|"match"|"add"|"drop"), url? (add), id? (show/drop), query? (match), direction? (add: 구현 방향 텍스트) }. 외부 AI가 "이 프로젝트 어떤 API 문서가 정리되어 있나?" / "내 작업과 매칭되는 API skill 있나?" 회수.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, sub: { type: 'string', enum: ['list', 'show', 'match', 'add', 'drop'] }, url: { type: 'string' }, id: { type: 'string' }, query: { type: 'string' }, direction: { type: 'string' } }, required: ['sub'] } },
80
80
  { name: 'leerness_selftest', requiredTier: 'read-only', description: '1.9.258/259 — 설치된 leerness 바이너리의 코어 함수(보안 _isSecretKey / 버전 compareVer / 인코딩 _classifyCJK 등) 무결성 자가 검증. 응답: { version, total, pass, fail, ok, results[] }. 외부 AI/CI 가 "이 leerness 설치가 정상인가?(npx 캐시 손상·부분 설치 감지)" 를 1초 내 확인. 인자: 없음.', inputSchema: { type: 'object', properties: {} } },
81
81
  { name: 'leerness_shell_guard', requiredTier: 'read-only', description: '1.9.260/261 (사용자 명시 UR-0020) — 터미널 명령 셸 호환성 린터. 외부 AI 가 명령을 실행하기 전에 셸 호환성 문제를 사전 점검. 예: Windows PowerShell 5.1 은 && / || 미지원 → A; if ($?) { B } 제안. 6 규칙(ps5-chain/ps-devnull/ps-inline-env/ps-rm-rf/cmd-semicolon/ps-version-unknown) + 과거 실패 회수(.harness/shell-failures.json). 응답: { shell, psVersion, issues[{rule,severity,detail,suggestion}], pastSame, pastSimilar, ok }. 인자: { command (required), path? }. 현재 셸/PS 버전 자동 감지.', inputSchema: { type: 'object', properties: { command: { type: 'string' }, path: { type: 'string' } }, required: ['command'] } },
82
- { name: 'leerness_slash_commands', requiredTier: 'read-only', description: '1.9.265/266 (사용자 명시 UR-0021) — CLI AI 에이전트별 슬래시 명령어 레지스트리. 외부 AI(메인)가 sub-agent(codex/agy/claude/grok/copilot)를 호출할 때 각 에이전트에 알맞는 슬래시 명령을 참조. 빌트인 + 사용자 .harness/agent-slash-commands.json override 병합. 응답: { agents: { <id>: { label, asOf, invoke(slash|subcommand), note, source, count, commands[{cmd,desc}] } } }. 인자: { path?, agent? (생략 시 전체), refresh? (1.9.267 — 설치된 CLI --help probe 자동 갱신), dryRun? }. agent 지정 시 단일.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, agent: { type: 'string' }, refresh: { type: 'boolean' }, dryRun: { type: 'boolean' } } } },
82
+ { name: 'leerness_slash_commands', requiredTier: 'safe-write', description: '1.9.265/266 (사용자 명시 UR-0021) — CLI AI 에이전트별 슬래시 명령어 레지스트리. 외부 AI(메인)가 sub-agent(codex/agy/claude/grok/copilot)를 호출할 때 각 에이전트에 알맞는 슬래시 명령을 참조. 빌트인 + 사용자 .harness/agent-slash-commands.json override 병합. 응답: { agents: { <id>: { label, asOf, invoke(slash|subcommand), note, source, count, commands[{cmd,desc}] } } }. 인자: { path?, agent? (생략 시 전체), refresh? (1.9.267 — 설치된 CLI --help probe 자동 갱신), dryRun? }. agent 지정 시 단일.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, agent: { type: 'string' }, refresh: { type: 'boolean' }, dryRun: { type: 'boolean' } } } },
83
83
  { name: 'leerness_roles', requiredTier: 'safe-write', description: '1.9.270 (사용자 명시) — 모델별 역할 부여. 여러 AI 에이전트 활성 시 역할(commander/reviewer/coder/architect/designer/debugger/dispatcher)을 provider+model 에 매핑하고, agents dispatch --role 로 라우팅. sub=suggest 면 활성 에이전트 기반 최적 배치 + 근거 반환(방향성 판단). sub=set 시 role/provider/model 필요. 응답: list/suggest/verify 별 JSON. 인자: { path?, sub(list|set|unset|catalog|suggest|verify), role?, provider?, model?, apply? }.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, sub: { type: 'string' }, role: { type: 'string' }, provider: { type: 'string' }, model: { type: 'string' }, apply: { type: 'boolean' } } } },
84
84
  // 1.9.279 (UR-0031, GPT-5.5 범용 하네스): 상태 substrate(.leerness/, 1.9.278)를 MCP 시맨틱 verb 로 노출 — 모든 에이전트 공통 호출 표면.
85
85
  { name: 'leerness_state_show', requiredTier: 'read-only', description: '1.9.279 (UR-0031) — get_project_context / get_current_task. .leerness/ 구조화 상태(현재 run + 누적)를 회수. 외부 에이전트가 작업 시작 전 "지금 무슨 작업이 진행 중인가 / 무엇을 읽고 무엇이 변경됐나"를 JSON 으로 파악. 응답: { state, currentRun }. 인자: { path? }.', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.36.75",
3
+ "version": "1.36.76",
4
4
  "description": "The AI-coding operations layer that makes \"done\" require evidence — persistent memory, evidence-gated completion checks, and clean handoffs for any AI agent (Claude Code, Codex, Cursor). State lives as plain files in your repo. CLI + MCP, 0 runtime dependencies.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -3378,9 +3378,12 @@ total++;
3378
3378
  // provider_add: regex 는 read-only(아래 정책 통과 가능) 인데 메타데이터 safe-write → read-only enforce 에서 차단되어야
3379
3379
  const pa = callMcp('leerness_provider_add', { path: pDir, id: 'x', cmd: 'y' });
3380
3380
  const blocked = pa && pa.isError === true && /정책 차단/.test(pa.content[0].text);
3381
- // handoff: read-only 허용
3381
+ // 1.36.76 (검수 P1): handoff 는 last-handoff/tech-profile 을 쓰므로 safe-write 로 재분류 — read-only enforce 에서 차단되어야
3382
3382
  const hd = callMcp('leerness_handoff', { path: pDir });
3383
- const allowed = hd && hd.isError !== true;
3383
+ const handoffBlocked = hd && hd.isError === true && /정책 차단/.test(hd.content[0].text);
3384
+ // 진짜 read-only 도구(drift_check)는 허용
3385
+ const dc = callMcp('leerness_drift_check', { path: pDir });
3386
+ const allowed = handoffBlocked && dc && dc.isError !== true;
3384
3387
  // 모든 도구 유효 tier
3385
3388
  const T = require(path.resolve(__dirname, '..', 'lib', 'mcp-tools.js'));
3386
3389
  const tierOk = T.every(t => typeof t.requiredTier === 'string' && t.requiredTier.length > 0);
@@ -7469,5 +7472,44 @@ total++;
7469
7472
  if (!ok) failed++;
7470
7473
  }
7471
7474
 
7475
+ // 1.36.76 (9차 헌트 이월 3건 종결): 손상 아카이브 복원 거부 · MCP read-only 무변형 · 긴 파생 id 안전화
7476
+ total++;
7477
+ {
7478
+ let ok = false;
7479
+ const _d = [];
7480
+ try {
7481
+ // #3 U+FFFD 아카이브 → 복원 거부 + 아카이브 무변경
7482
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-h9x3-')); _d.push(d);
7483
+ cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--minimal', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
7484
+ const ap = path.join(d, '.harness', 'decisions.archive.md');
7485
+ const bytes = Buffer.from('# Decisions archive\n\n## 제거 2026-07-25 (target: "Trunc")\n\n### 2026-07-25 — Trunc\n- Decision: 데이터', 'utf8');
7486
+ fs.writeFileSync(ap, bytes.slice(0, bytes.length - 1));
7487
+ const before = fs.readFileSync(ap);
7488
+ const rr = cp.spawnSync(process.execPath, [CLI, 'memory', 'restore', 'decisions', 'Trunc', '--path', d], { encoding: 'utf8', timeout: 20000 });
7489
+ const truncOk = rr.status === 1 && before.equals(fs.readFileSync(ap));
7490
+ // #2a 없는 대상에 read-only MCP 호출 → 디렉토리·텔레메트리 미생성 / 초기화된 하네스엔 기록 유지
7491
+ const ghost = path.join(d, 'no-such-target');
7492
+ cp.spawnSync(process.execPath, [CLI, 'mcp', 'serve'], { encoding: 'utf8', timeout: 25000, input: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'leerness_state_show', arguments: { path: ghost } } }) + '\n' });
7493
+ const ghostOk = !fs.existsSync(ghost);
7494
+ cp.spawnSync(process.execPath, [CLI, 'mcp', 'serve'], { encoding: 'utf8', timeout: 25000, input: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'leerness_pulse', arguments: { path: d } } }) + '\n' });
7495
+ const statsOk = fs.existsSync(path.join(d, '.harness', 'cache', 'usage-stats.json'));
7496
+ // #2b MCP env_detect 는 environment.json 을 쓰지 않음 / CLI 직접 호출은 persist
7497
+ fs.rmSync(path.join(d, '.harness', 'environment.json'), { force: true });
7498
+ const envResp = cp.spawnSync(process.execPath, [CLI, 'mcp', 'serve'], { encoding: 'utf8', timeout: 25000, input: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: 'leerness_env_detect', arguments: { path: d } } }) + '\n' });
7499
+ const noWriteOk = /snapshot/.test(envResp.stdout) && !fs.existsSync(path.join(d, '.harness', 'environment.json'));
7500
+ cp.spawnSync(process.execPath, [CLI, 'env', 'detect', d, '--json'], { encoding: 'utf8', timeout: 20000 });
7501
+ const cliPersistOk = fs.existsSync(path.join(d, '.harness', 'environment.json'));
7502
+ // #8 긴/이상 URL 파생 id — raw ENOENT 없이 저장, 정상 id 는 종전과 동일
7503
+ const A = (u) => cp.spawnSync(process.execPath, [CLI, 'api-skill', 'add', u, '--skeleton', '--no-crawl', '--path', d, '--json'], { encoding: 'utf8', timeout: 25000 });
7504
+ let longOk = false, normOk = false;
7505
+ try { const j = JSON.parse(A('ftp://' + 'a'.repeat(300) + '.test/docs').stdout); longOk = j.ok === true && j.id.length <= 80; } catch {}
7506
+ try { normOk = JSON.parse(A('https://developers.coupangcorp.com/articles/360033877853').stdout).id === 'developers-coupangcorp-articles-360033877853'; } catch {}
7507
+ ok = truncOk && ghostOk && statsOk && noWriteOk && cliPersistOk && longOk && normOk;
7508
+ if (!ok) console.log(` [h9x 디버그] trunc=${truncOk} ghost=${ghostOk} stats=${statsOk} noWrite=${noWriteOk} cli=${cliPersistOk} long=${longOk} norm=${normOk}`);
7509
+ } catch (e) {} finally { _d.forEach(x => { try { fs.rmSync(x, { recursive: true, force: true }); } catch {} }); }
7510
+ console.log(ok ? '✓ B(1.36.76) 9차 이월 종결: U+FFFD 아카이브 복원거부 · MCP read-only 무변형(대상/텔레메트리/env) · 긴 id 안전화' : '✗ 9차 이월 수정 실패');
7511
+ if (!ok) failed++;
7512
+ }
7513
+
7472
7514
  console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
7473
7515
  if (failed > 0) process.exit(1);