leerness 1.36.80 → 1.36.81

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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.36.81 — 2026-07-28 — 게시본 클린룸 실측이 잡은 2건 (referee 상태 신호 정확화 · 출하 소스 생 NUL 제거)
4
+
5
+ 1.36.80 을 게시한 뒤 **게시된 tarball 로** 신규 기능을 실행해 본 결과 2건이 나왔다. 둘 다 게이트(selftest 334 · e2e 404)는 초록이었다 — 단언이 없던 자리다.
6
+
7
+ - **referee 상태 신호가 부정확했다** — `isStale()` 이 "캘리브레이션이 아예 없음"까지 stale 로 묶어, **한 번도 검증한 적 없는** 검증기가 `verify-claim` 게이트에서 `referee_stale` 로 보고됐다. 두 상태는 사용자가 취할 조치가 다르다(캘리브레이션 실행 vs 재검증). 정직한 신호가 존재 이유인 기능에서 신호 자체가 틀린 셈이라 고쳤다: stale 은 **"한때 증명됐으나 정의가 바뀌어 무효"** 만을 뜻하고, 미증명은 `referee_uncalibrated`, 캘리브레이션 실패도 `referee_uncalibrated`(사유 포함). fail-closed 동작은 이전과 동일 — 어느 상태든 통과시키지 않는다. e2e 가 미존재/미증명/무효/검증기실패 **4구분**을 고정한다.
8
+ - **출하 소스에 생 NUL 바이트** — 지문·ID 구분자를 소스에 raw NUL 로 써서 git/grep 이 `bin/leerness.js` 와 `lib/referee.js` 를 **바이너리로 취급**했다(diff 가 `Bin 16408 -> 17121 bytes` 로만 표시, 코드 검색 차단). 더 위험한 쪽은 정규화 도구가 이 바이트를 조용히 지우면 해시 입력이 바뀌어 **기존 프로젝트의 캘리브레이션 지문과 api-skill ID 가 한꺼번에 어긋난다**는 점이다. 런타임 값이 동일한 유니코드 이스케이프 표기로 치환(기존 지문/ID 불변 — 실측 확인) + selftest 메타가드로 bin/lib 전수 0 을 상시 강제.
9
+ **검수 23회전이 잡은 4건** (전건 직접 재현 확인):
10
+
11
+ - **High — fail-closed 기능의 fail-open**: `calibration.ok` 가 `"false"`(truthy 문자열)나 `1` 로 손상/조작되면 `!r.calibration.ok` 가 false 가 되어 **미증명 검증기가 게이트를 그대로 통과**했고 `referee show` 는 `calibrated: true` 로 표시했다. 이 기능의 전제 자체를 뚫는 구멍이다. `_entryValid` 가 `ok`(boolean)·`fingerprint`(비어있지 않은 문자열)를 형상 검증해 거부하고(스토어 전체 fail-closed — previews/teams 와 동일 규율), 판정 지점도 `=== true` 엄격 비교로 이중 방어. 형상 무효는 "없음" 대신 `referee_store_invalid` 로 정확히 보고한다.
12
+ - **캘리브레이션 실패 + 정의 변경을 `referee_stale` 로 오분류** — 증명된 적이 없는데 "있던 증명이 무효화됐다"는 거짓 서사였다. 실패 상태는 정의가 바뀌어도 `referee_uncalibrated`.
13
+ - **메타가드 스캔 범위가 이름보다 좁음** — `scripts/` 도 `package.json` `files` 에 포함돼 npm 에 함께 실리는데 스캔하지 않았다. bin/lib/scripts 전수로 확장.
14
+ - **공허한 e2e 단언 2건** — "FP 프로브 3종 거부" 중 둘은 `--expect-bad` 없이 `add` 해서 **add 자체가 `missing_args` 로 실패**했고, 이어진 `verify` 가 돌려준 `not_found`(ok:false)를 e2e 가 "거부됨"으로 통과 처리했다. 탐지력 없는 검증기를 거부한다는 걸 전혀 증명하지 못했다. add 성공을 먼저 단언하고 거부 **사유**까지 확인하도록 교체.
15
+
16
+ - 검증: selftest 335 · e2e 전건 · 게시본 실측 프로브(게이트 4구분 · 지문 동치 · 임시 워크스페이스 정리 · 포트 5337) · 검수 지적 4건 수정 전/후 실측 대조.
17
+
3
18
  ## 1.36.80 — 2026-07-28 — referee 캘리브레이션 · 라이브 미리보기 · 기본 스킬 정리 · 최소성 사다리 — MCP 89 도구 · R-0001 검수 22회전
4
19
 
5
20
  **이 릴리스에 함께 들어간 것** (아래 상세):
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.80 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
125
+ 이 프로젝트는 Leerness v1.36.81 하네스를 사용합니다. 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.80는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **89개 도구**를 노출:
179
+ Leerness v1.36.81는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **89개 도구**를 노출:
180
180
 
181
181
  ```jsonc
182
182
  // 카테고리별
@@ -197,7 +197,7 @@ Leerness v1.36.80는 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.80 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
200
+ 현재 누적: **v1.9.x → 1.36.81 릴리스 태그 이력** (수백 라운드) · _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.80: 2026-07-28
238
+ Last synced by Leerness v1.36.81: 2026-07-28
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.80';
37
+ const VERSION = '1.36.81';
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') 시 호스트 프로세스 오염.
@@ -4934,6 +4934,19 @@ function _selfTestCases() {
4934
4934
  while ((m = re.exec(self)) !== null) { if (m[1].includes(NL)) offenders++; }
4935
4935
  return offenders === 0;
4936
4936
  } },
4937
+ { name: '출하 소스 생 NUL 금지 (1.36.81, 클린룸 실측): bin/lib 전수 0 — 구분자를 이스케이프로 (메타가드)', run: () => {
4938
+ // 1.36.80 사고: 지문/ID 구분자를 소스에 **생 NUL 바이트**로 써서 도구가 파일을 바이너리로 취급했다(grep 차단).
4939
+ // 정규화 도구가 조용히 지우면 해시 입력이 바뀌어 전 프로젝트의 캘리브레이션/ID 가 한꺼번에 어긋난다.
4940
+ // 런타임 값이 동일한 유니코드 이스케이프 표기로만 쓴다 — 이 가드 자신은 이스케이프 표기라 자기참조 안전.
4941
+ // (codex 검수 ③) scripts/ 도 package.json files 에 포함돼 npm 으로 함께 실린다 — "출하 소스"라면 같이 봐야 한다.
4942
+ const NUL = String.fromCharCode(0);
4943
+ const files = [__filename];
4944
+ for (const sub of ['lib', 'scripts']) {
4945
+ const dir = path.join(path.dirname(__filename), '..', sub);
4946
+ if (exists(dir)) for (const f of fs.readdirSync(dir)) if (f.endsWith('.js')) files.push(path.join(dir, f));
4947
+ }
4948
+ return files.every(f => !read(f).includes(NUL));
4949
+ } },
4937
4950
  { name: 'CLI 영어화 Phase 1 (1.20.2, UR-0010): _uiLang 해석(flag>env>manifest>ko) + 첫화면 _t 적용 (행위+소스)', run: () => {
4938
4951
  const save = process.argv; const saveEnv = process.env.LEERNESS_LANG;
4939
4952
  try {
@@ -6039,7 +6052,7 @@ function _apiSkillsDir(root) {
6039
6052
  // 정체성(같은 URL·다른 name 이 같은 id 로 붕괴하던 것), Windows 예약 이름(con/prn/aux/nul/com1..9/lpt1..9) 회피.
6040
6053
  // 충돌 접미사(-hash) 추가 후에도 이 함수를 다시 거쳐 상한이 유지된다.
6041
6054
  function _apiSkillIdCap(id, url, name) {
6042
- const seed = String(url || '') + '' + String(name || '');
6055
+ const seed = String(url || '') + '\u0000' + String(name || '');
6043
6056
  if (!id || id.length > 80) id = (id || 'api').slice(0, 72).replace(/-+$/, '') + '-' + _shortHash(seed);
6044
6057
  if (/^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(id)) id = id + '-' + _shortHash(seed);
6045
6058
  return id;
package/lib/referee.js CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.36.80",
3
+ "version": "1.36.81",
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
@@ -7723,15 +7723,23 @@ total++;
7723
7723
  const good = verify('okref');
7724
7724
  const happyOk = good && good.ok === true && good.goodExit === 0 && good.badExit === 1 && good.expectMatched === true;
7725
7725
  // FP 프로브 ①: known-good 이 이미 빨간 상태 → 거부(기준선 무효)
7726
- add('redbase', N('process.exit(0)'), N('process.exit(3)'), N('process.exit(1)'), '');
7726
+ // 1.36.81 (codex 검수 ④, 실측 확인): 이전 두 프로브는 --expect-bad 를 빼고 add 해서 add 자체가 missing_args 로
7727
+ // 실패했고, 이어진 verify 는 not_found(ok:false)를 돌려줬다 — e2e 는 ok===false 만 보고 "거부됨"으로 통과시켰다.
7728
+ // 즉 "탐지력 없는 검증기를 거부한다"를 전혀 증명하지 못하는 공허한 단언이었다.
7729
+ // → add 성공을 먼저 단언하고, 거부 **사유**까지 확인한다.
7730
+ const a1 = add('redbase', N('process.exit(0)'), N('process.exit(3)'), N("console.log('SIG'); process.exit(1)"), 'SIG');
7727
7731
  const p1 = verify('redbase');
7728
- // FP 프로브 ②: known-bad 가 통과 → 탐지력 없음 거부
7729
- add('nopower', N('process.exit(0)'), N('process.exit(0)'), N('process.exit(0)'), '');
7732
+ // FP 프로브 ②: known-bad 가 통과(exit 0) → 탐지력 없음 거부
7733
+ const a2 = add('nopower', N('process.exit(0)'), N('process.exit(0)'), N("console.log('SIG'); process.exit(0)"), 'SIG');
7730
7734
  const p2 = verify('nopower');
7731
7735
  // FP 프로브 ③: bad 가 무관한 이유로 실패 → 사유 불일치 거부
7732
- add('wrongreason', N('process.exit(0)'), N('process.exit(0)'), N("console.log('Error: cannot find module'); process.exit(1)"), 'AssertionError');
7736
+ const a3 = add('wrongreason', N('process.exit(0)'), N('process.exit(0)'), N("console.log('Error: cannot find module'); process.exit(1)"), 'AssertionError');
7733
7737
  const p3 = verify('wrongreason');
7734
- const probesOk = p1 && p1.ok === false && p2 && p2.ok === false && p3 && p3.ok === false && p3.expectMatched === false;
7738
+ const _rz = (p) => (p && p.reasons || []).join(' | ');
7739
+ const probesOk = a1.status === 0 && a2.status === 0 && a3.status === 0
7740
+ && p1 && p1.ok === false && /known-good/.test(_rz(p1)) // 기준선이 이미 빨감
7741
+ && p2 && p2.ok === false && /exit 0|통과시킴/.test(_rz(p2)) // 심어둔 결함을 못 잡음
7742
+ && p3 && p3.ok === false && p3.expectMatched === false && /사유 불일치|런처/.test(_rz(p3));
7735
7743
  // 지문(stale): 캘리브레이션 후 명령이 바뀌면 신뢰 철회
7736
7744
  const store = path.join(d, '.harness', 'referees.json');
7737
7745
  const js = JSON.parse(fs.readFileSync(store, 'utf8'));
@@ -7747,8 +7755,31 @@ total++;
7747
7755
  add('failing', N('process.exit(2)'), N('process.exit(0)'), N("console.log('AssertionError'); process.exit(1)"), 'AssertionError');
7748
7756
  verify('failing');
7749
7757
  const refFail = R(['verify-claim', 'T-0002', '--referee', 'failing', '--json']);
7758
+ // 1.36.81 (클린룸 실측): 세 상태는 서로 다른 코드로 보고돼야 한다 — 한 번도 검증 안 한 검증기를
7759
+ // stale("한때 유효했으나 무효화됨")로 보고하면 사용자가 취할 조치를 잘못 알려준다.
7760
+ add('nevercal', N('process.exit(0)'), N('process.exit(0)'), N("console.log('AssertionError'); process.exit(1)"), 'AssertionError');
7761
+ const neverUse = R(['verify-claim', 'T-0002', '--referee', 'nevercal', '--json']);
7762
+ const neverShown = J(R(['referee', 'show', 'nevercal', '--json']));
7763
+ const uncalOk = neverUse.status === 1 && (J(neverUse) || {}).code === 'referee_uncalibrated'
7764
+ && neverShown && neverShown.stale === false && neverShown.calibrated === false;
7765
+ // 1.36.81 (codex 검수 ① High, 실측 확인): calibration.ok 가 truthy 비-boolean 이면 게이트가 통과했다 —
7766
+ // "미증명 검증기는 신뢰하지 않는다"는 이 기능의 전제를 정면으로 깨는 fail-open. 손상/조작 모두 차단돼야 한다.
7767
+ add('failopen', N('process.exit(0)'), N('process.exit(0)'), N("console.log('AssertionError'); process.exit(1)"), 'AssertionError');
7768
+ verify('failopen');
7769
+ const foStore = JSON.parse(fs.readFileSync(store, 'utf8'));
7770
+ foStore.find(x => x.id === 'failopen').calibration.ok = 'false'; // truthy 문자열
7771
+ fs.writeFileSync(store, JSON.stringify(foStore, null, 2));
7772
+ const foUse = R(['verify-claim', 'T-0002', '--referee', 'failopen', '--json']);
7773
+ const foShow = J(R(['referee', 'show', 'failopen', '--json']));
7774
+ const failOpenOk = foUse.status === 1 && (J(foUse) || {}).code !== undefined
7775
+ && (J(foUse) || {}).code !== 'referee_failed' // 검증기 실행까지 갔다면 이미 신뢰한 것
7776
+ && !(foShow && foShow.calibrated === true); // 사용자에게 "증명됨"으로 표시해서도 안 된다
7777
+ // 원복(이후 단언들이 이 스토어를 씀)
7778
+ foStore.find(x => x.id === 'failopen').calibration.ok = true;
7779
+ fs.writeFileSync(store, JSON.stringify(foStore, null, 2));
7750
7780
  const gateOk = missing.status === 1 && (J(missing) || {}).code === 'referee_not_found'
7751
7781
  && staleUse.status === 1 && (J(staleUse) || {}).code === 'referee_stale'
7782
+ && uncalOk && failOpenOk
7752
7783
  && refFail.status === 1 && (J(refFail) || {}).code === 'referee_failed';
7753
7784
  // 스토어 규율: 손상 위 변경 거부 + --json 순수
7754
7785
  const listPure = (() => { try { JSON.parse(R(['referee', 'list', '--json']).stdout); return true; } catch { return false; } })();
@@ -7758,7 +7789,7 @@ total++;
7758
7789
  ok = happyOk && probesOk && staleOk && gateOk && storeOk;
7759
7790
  if (!ok) console.log(` [referee 디버그] happy=${happyOk} probes=${probesOk} stale=${staleOk} gate=${gateOk} store=${storeOk}`);
7760
7791
  } catch (e) {} finally { _d.forEach(x => { try { fs.rmSync(x, { recursive: true, force: true }); } catch {} }); }
7761
- console.log(ok ? '✓ B(1.36.80) referee 캘리브레이션: 탐지력 증명(good통과+bad를 기대사유로 거부) · FP프로브 3종 거부 · 명령변경 stale · verify-claim 게이팅 · 스토어 규율' : '✗ referee 캘리브레이션 실패');
7792
+ console.log(ok ? '✓ B(1.36.80) referee 캘리브레이션: 탐지력 증명(good통과+bad를 기대사유로 거부) · FP프로브 3종 거부 · 명령변경 stale · verify-claim 게이팅(미존재/미검증/무효/검증기실패 4구분) · 스토어 규율' : '✗ referee 캘리브레이션 실패');
7762
7793
  if (!ok) failed++;
7763
7794
  }
7764
7795