leerness 1.36.122 → 1.36.123

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/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.122 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
125
+ 이 프로젝트는 Leerness v1.36.123 하네스를 사용합니다. 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.122는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **89개 도구**를 노출:
179
+ Leerness v1.36.123는 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.122는 stdio JSON-RPC MCP server를 내장합니다 — Claude Co
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.122 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
200
+ 현재 누적: **v1.9.x → 1.36.123 릴리스 태그 이력** (수백 라운드) · _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.122: 2026-08-15
238
+ Last synced by Leerness v1.36.123: 2026-08-16
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.122';
37
+ const VERSION = '1.36.123';
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') 시 호스트 프로세스 오염.
@@ -1037,6 +1037,21 @@ function createBackup(root, reason, files, dry = false) {
1037
1037
  return { archiveDir: ar, candidates, pruned };
1038
1038
  }
1039
1039
 
1040
+ // 1.36.115: 구버전은 스킬을 `.claude/skills/leerness.md`(평면)로 깔았고 그 형식은 발견되지 않는다.
1041
+ // 새 위치로 옮겨 심었지만 **옛 파일은 지우지 않는다** — 사용자가 손댔을 수 있고, 이 저장소의 계약은 false-DROP 이 버그다.
1042
+ // 대신 사본 2벌이 생겼다는 사실과 어느 쪽이 실제로 읽히는지 알린다(안 그러면 옛 파일을 고치고 반영 안 돼 혼란).
1043
+ // 1.36.123 (자기 저장소 도그푸딩): 이 경고가 `init` 에만 있었다 — 도구 배선의 **문서화된 진입점**인 `adapter`
1044
+ // 로 들어온 사용자는 사본 2벌을 갖고도 아무 말을 못 들었다(우리 repo 에서 실제로 그랬다). 두 경로가 같이 쓴다.
1045
+ function _warnLegacySkillFile(root) {
1046
+ try {
1047
+ const f = path.join(root, '.claude', 'skills', 'leerness.md');
1048
+ if (!exists(f)) return false;
1049
+ warn(`구 스킬 파일이 남아 있습니다: .claude/skills/leerness.md`);
1050
+ log(` → 실제로 읽히는 것은 .claude/skills/leerness/SKILL.md 입니다(Claude Code 규약). 옛 파일은 무시되며, 필요 없으면 직접 지우세요.`);
1051
+ return true;
1052
+ } catch { return false; }
1053
+ }
1054
+
1040
1055
  // 1.9.1 P2 / 1.9.368 (UR-0025): MERGE_OVERWRITE_FILES → lib/catalogs, managedMerge 순수 코어 → lib/pure-utils. 얇은 래퍼.
1041
1056
  function managedMerge(file, next, previous, archiveDir, mergeOpts = {}) {
1042
1057
  // 1.36.105 (codex 검수 HIGH#4): 경로를 **cwd 기준**으로 만들어 프로젝트 파일 안에 박았다 —
@@ -1046,7 +1061,16 @@ function managedMerge(file, next, previous, archiveDir, mergeOpts = {}) {
1046
1061
  // 1.36.122 (자기 저장소 도그푸딩): 백업을 만들지 않는 실행(`adapter` 등)은 여기서 일반 경로를 **채워** 넘겼고,
1047
1062
  // 그러면 이전 마이그레이션이 적어 둔 **구체 백업 경로가 덮여** 사라졌다(우리 repo 에서 실측).
1048
1063
  // 백업이 없으면 `null` 을 넘겨, 이전 문서의 포인터를 그대로 두게 한다(보존 블록의 정보 손실 방지).
1049
- const archiveRel = archiveDir ? path.relative(_base, archiveDir).replace(/\\/g, '/') : null;
1064
+ // ⚠ 1.36.123 (자체 헌트): 유지가 **없어진 아카이브를 영구 고착**시킬 수 있다 — 그 문장은
1065
+ // "전체 원본 백업: <경로>" 라는 **약속**이므로, 가리키는 곳이 사라졌으면 거짓 약속이 된다.
1066
+ // 여기(fs 접근 가능)에서 실재를 확인해, 없으면 일반 경로로 되돌린다. 순수 함수는 그대로 둔다.
1067
+ let archiveRel = archiveDir ? path.relative(_base, archiveDir).replace(/\\/g, '/') : null;
1068
+ if (!archiveRel && previous) {
1069
+ const _m = /(?:전체 원본 백업|Full original backup):\s*`([^`]+)`/.exec(String(previous));
1070
+ if (_m && _m[1] && _m[1] !== '.harness/archive') {
1071
+ archiveRel = exists(path.join(_base, _m[1])) ? _m[1] : '.harness/archive';
1072
+ }
1073
+ }
1050
1074
  return _managedMerge(file, next, previous, archiveRel, MERGE_OVERWRITE_FILES, mergeOpts); // 1.36.60: altTemplate/lang 전달
1051
1075
  }
1052
1076
 
@@ -1592,13 +1616,7 @@ async function install(root, opts = {}) {
1592
1616
  // 1.36.115: 구버전은 스킬을 `.claude/skills/leerness.md`(평면)로 깔았고 그 형식은 발견되지 않는다.
1593
1617
  // 새 위치로 옮겨 심었지만 **옛 파일은 지우지 않는다** — 사용자가 손댔을 수 있고, 이 저장소의 계약은 false-DROP 이 버그다.
1594
1618
  // 대신 사본 2벌이 생겼다는 사실과 어느 쪽이 실제로 읽히는지 알린다(안 그러면 옛 파일을 고치고 반영 안 돼 혼란).
1595
- try {
1596
- const _legacySkill = path.join(root, '.claude', 'skills', 'leerness.md');
1597
- if (exists(_legacySkill)) {
1598
- warn(`구 스킬 파일이 남아 있습니다: .claude/skills/leerness.md`);
1599
- log(` → 실제로 읽히는 것은 .claude/skills/leerness/SKILL.md 입니다(Claude Code 규약). 옛 파일은 무시되며, 필요 없으면 직접 지우세요.`);
1600
- }
1601
- } catch {}
1619
+ _warnLegacySkillFile(root);
1602
1620
  if (!has('--no-auto-update') && !opts.minimal) {
1603
1621
  try { autoUpdateInstall(root); } catch (e) { warn('auto-update hook install skipped: ' + (e && e.message)); }
1604
1622
  } else if (opts.minimal) {
@@ -23943,6 +23961,8 @@ function adapterCmd(root, tool, opts = {}) {
23943
23961
  // 주장은 **파일 단위**로 한다 — 우리가 쓴 파일만 이름을 대고 말한다.
23944
23962
  _wrote.forEach(r => log(` ℹ ${r.file} 에 MCP 등록 — 이 파일을 읽는 도구는 leerness MCP verb(state_show/start/record/verify/handoff)를 직접 호출 가능`));
23945
23963
  _kept.forEach(r => log(` ℹ ${r.file} 의 기존 leerness 항목을 그대로 두었습니다 — 그 항목이 leerness MCP 를 띄우는지는 확인하지 않았습니다`));
23964
+ // 스킬을 까는 어댑터면 구 평면 파일도 같이 알린다(1.36.123: `init` 에만 있어 이 경로는 조용했다)
23965
+ if (a.keys.some(k => String(k).includes('.claude/skills/'))) _warnLegacySkillFile(root);
23946
23966
  // 보존은 안전하지만 **막다른 길이면 안 된다** — 우리 것이 망가졌을 때 되돌릴 방법을 알려 준다(자체 헌트).
23947
23967
  if (_kept.length || _brk.length) log(` → 표준 설정으로 되돌리려면 그 파일의 "leerness" 항목을 지우고 이 명령을 다시 실행하세요`);
23948
23968
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.36.122",
3
+ "version": "1.36.123",
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
@@ -5124,6 +5124,9 @@ total++;
5124
5124
  if (!j) { bad.push(`${c}:stdout이JSON아님`); continue; }
5125
5125
  // 문구가 "0 바이트" 면 검사도 0 바이트여야 한다 — `trim()===''` 은 개행만 있는 출력을 통과시킨다(검수 P3).
5126
5126
  if (err.length !== 0) bad.push(`${c}:stderr잡음(${err.length}B:${err.trim().split('\n')[0].slice(0, 40)})`);
5127
+ // "성공 경로 15종" 이라고 말하려면 **성공인지도** 봐야 한다 — 실패 JSON(`ok:false`, exit 1)도 세고 있었다(검수 P2).
5128
+ if (r.status !== 0) bad.push(`${c}:exit=${r.status}`);
5129
+ if (j.ok === false) bad.push(`${c}:ok=false`);
5127
5130
  }
5128
5131
  // 판별력 — 이 검사가 실제로 무언가를 실행했는지(전부 건너뛰면 공허하게 통과한다)
5129
5132
  if (bad.length === 0 && CMDS.length < 10) bad.push('대상이_너무적음');
@@ -5201,10 +5204,29 @@ total++;
5201
5204
  // 사용자 커스텀 + 구체 백업 포인터를 가진 보존 블록을 만든다
5202
5205
  fs.writeFileSync(cf, fs.readFileSync(cf, 'utf8')
5203
5206
  + '\n<!-- leerness:migration-preserved -->\n## Preserved previous content\n\n> 이전 버전에서 이어진 사용자/프로젝트 커스텀 내용 — 마이그레이션마다 자동 이월됩니다. 전체 원본 백업: `.harness/archive/leerness-9.9.9-STAMP`\n\nUSER-KEEP-LINE\n');
5204
- cp.spawnSync(process.execPath, [CLI, 'adapter', 'claude', '--path', d], { cwd: d, encoding: 'utf8', timeout: 300000, env: ENVp });
5207
+ // adapter **실제로 무언가 했는지**부터 확인한다 no-op/실패여도 미리 심은 포인터와 사용자 줄은
5208
+ // 그대로 남아 이 단언이 공허하게 통과한다(검수 P2). 아카이브도 실재해야 유지가 정당하다.
5209
+ fs.mkdirSync(path.join(d, '.harness', 'archive', 'leerness-9.9.9-STAMP'), { recursive: true });
5210
+ fs.rmSync(path.join(d, '.mcp.json'), { force: true });
5211
+ const rAd = cp.spawnSync(process.execPath, [CLI, 'adapter', 'claude', '--path', d], { cwd: d, encoding: 'utf8', timeout: 300000, env: ENVp });
5212
+ if (rAd.status !== 0) bad.push(`adapter실패(exit=${rAd.status})`);
5213
+ if (!fs.existsSync(path.join(d, '.mcp.json'))) bad.push('adapter가_아무것도_안함(공허)');
5205
5214
  const after2 = fs.readFileSync(cf, 'utf8');
5206
5215
  if (!/leerness-9\.9\.9-STAMP/.test(after2)) bad.push('CLI경로에서_포인터_격하');
5207
5216
  if (!after2.includes('USER-KEEP-LINE')) bad.push('CLI경로에서_사용자라인_유실');
5217
+ // ⚠ 위 픽스처는 아카이브가 **실재**한다(위에서 만든다). 실재하지 않으면 그 문장은 거짓 약속이므로
5218
+ // 일반 경로로 되돌려야 한다 — 유지가 과하면 없어진 백업을 영구 고착시킨다(자체 헌트가 잡은 자리).
5219
+ const dgone = path.join(sb, 'gone'); fs.mkdirSync(dgone, { recursive: true });
5220
+ fs.writeFileSync(path.join(dgone, 'package.json'), '{"name":"g","version":"0.1.0"}');
5221
+ cp.spawnSync(process.execPath, [CLI, 'init', dgone, '--yes'], { cwd: dgone, encoding: 'utf8', timeout: 300000, env: ENVp });
5222
+ const gf = path.join(dgone, 'CLAUDE.md');
5223
+ fs.writeFileSync(gf, fs.readFileSync(gf, 'utf8')
5224
+ + '\n<!-- leerness:migration-preserved -->\n## Preserved previous content\n\n> 이전 버전에서 이어진 사용자/프로젝트 커스텀 내용 — 마이그레이션마다 자동 이월됩니다. 전체 원본 백업: `.harness/archive/GONE-9.9.9`\n\nUSER-KEEP-LINE\n');
5225
+ cp.spawnSync(process.execPath, [CLI, 'adapter', 'claude', '--path', dgone], { cwd: dgone, encoding: 'utf8', timeout: 300000, env: ENVp });
5226
+ const gAfter = fs.readFileSync(gf, 'utf8');
5227
+ if (/GONE-9\.9\.9/.test(gAfter)) bad.push('없어진_아카이브를_계속_가리킴');
5228
+ if (!/전체 원본 백업: `\.harness\/archive`/.test(gAfter)) bad.push('없어진_아카이브인데_일반경로로_안내림');
5229
+ if (!gAfter.includes('USER-KEEP-LINE')) bad.push('격하시_사용자라인_유실');
5208
5230
  } finally { try { fs.rmSync(sb, { recursive: true, force: true }); } catch {} }
5209
5231
  ok = bad.length === 0;
5210
5232
  } catch (e) { bad.push('ERR:' + String(e && e.message).slice(0, 80)); }
@@ -11284,6 +11306,22 @@ total++;
11284
11306
  }
11285
11307
  // 평면 파일을 새로 만들지 않는다(구버전 잔재는 별개 — 지우지 않되 새로 만들지도 않는다)
11286
11308
  if (fs.existsSync(path.join(d, '.claude', 'skills', 'leerness.md'))) bad.push('skill:평면파일을새로만듦');
11309
+ // (자기 저장소 도그푸딩) 구 평면 잔재 경고가 `init` 에만 있었다 — 도구 배선의 문서화된 진입점인
11310
+ // `adapter` 로 들어온 사용자는 사본 2벌을 갖고도 아무 말을 못 들었다(우리 repo 가 그 상태였다).
11311
+ {
11312
+ const dleg = mk();
11313
+ fs.rmSync(path.join(dleg, '.claude', 'skills', 'leerness'), { recursive: true, force: true });
11314
+ fs.writeFileSync(path.join(dleg, '.claude', 'skills', 'leerness.md'), '---\nname: leerness\n---\nold\n');
11315
+ const rleg = cp.spawnSync(process.execPath, [CLI, 'adapter', 'claude', '--path', dleg], { cwd: dleg, encoding: 'utf8', timeout: 300000, env: ENV });
11316
+ const oleg = String(rleg.stdout || '') + String(rleg.stderr || '');
11317
+ if (!fs.existsSync(path.join(dleg, '.claude', 'skills', 'leerness', 'SKILL.md'))) bad.push('skill:adapter가_새경로_미생성');
11318
+ if (!/구 스킬 파일/.test(oleg)) bad.push('skill:adapter가_구파일_미경고');
11319
+ if (!fs.existsSync(path.join(dleg, '.claude', 'skills', 'leerness.md'))) bad.push('skill:구파일을_지움(false-DROP)');
11320
+ // 판별력 — 잔재가 없으면 그 경고가 뜨면 안 된다
11321
+ const dclean = mk();
11322
+ const rcl = cp.spawnSync(process.execPath, [CLI, 'adapter', 'claude', '--path', dclean], { cwd: dclean, encoding: 'utf8', timeout: 300000, env: ENV });
11323
+ if (/구 스킬 파일/.test(String(rcl.stdout || '') + String(rcl.stderr || ''))) bad.push('skill:잔재없는데_경고');
11324
+ }
11287
11325
  // ⚠ `init` 만 밟으면 안 된다. 실제로 `lib/catalogs.js` 의 어댑터 목록이 옛 평면 경로를 유지하고 있어
11288
11326
  // `adapter claude` 가 이 수정을 되돌리고 있었다(1.36.116 에서 발견) — 가드가 밟지 않은 경로였다.
11289
11327
  const da = mk(['--minimal']);
@@ -12041,6 +12079,26 @@ total++;
12041
12079
  const starInp = AUD._collectGuardInputs(dstar);
12042
12080
  if (starInp.runners.some(r => /ghost\.js$/.test(r.file))) bad.push('블록주석에서_러너_수집');
12043
12081
  if (!PU._detectOrphanGuards(starInp).orphanScripts.includes('check:leaf')) bad.push('블록주석이_고아를_덮음');
12082
+ // ⚠ 위는 **수집기만** 잰다 — 판정기의 `*` 처리를 지워도 통과한다(검수 P3: 수집기가 이미 걸러 놨으니까).
12083
+ // 판정기 쪽은 러너를 **직접 주입해** 따로 잰다: 주석 줄에만 이름이 있으면 고아여야 한다.
12084
+ const starDet = PU._detectOrphanGuards({
12085
+ packageScripts: { test: 'node t.js', 'check:leaf': 'node l.js' },
12086
+ runners: [{ file: '.github/workflows/ci.yml', viaScript: null, viaScripts: [], unconditional: true,
12087
+ text: 'jobs:\n a:\n steps:\n - run: npm test\n' },
12088
+ { file: 'scripts/x.js', viaScript: 'test', viaScripts: ['test'],
12089
+ text: '/*\n * npm run check:leaf\n */\nconsole.log(1);\n' }],
12090
+ scriptFiles: [],
12091
+ });
12092
+ if (!starDet.orphanScripts.includes('check:leaf')) bad.push('판정기가_블록주석을_실행으로');
12093
+ // 판별력 — 주석이 아닌 실제 호출은 여전히 덮여야 한다
12094
+ const starLive = PU._detectOrphanGuards({
12095
+ packageScripts: { test: 'node t.js', 'check:leaf': 'node l.js' },
12096
+ runners: [{ file: '.github/workflows/ci.yml', viaScript: null, viaScripts: [], unconditional: true,
12097
+ text: 'jobs:\n a:\n steps:\n - run: npm test\n' },
12098
+ { file: 'scripts/x.js', viaScript: 'test', viaScripts: ['test'], text: 'sh("npm run check:leaf");\n' }],
12099
+ scriptFiles: [],
12100
+ });
12101
+ if (starLive.orphanScripts.length) bad.push('판정기_주석필터가_실제호출까지_지움');
12044
12102
  // (자체 헌트) Windows CI 는 `node scripts\x\check.js` 로 적는다. 후보 경로는 `/` 표기라
12045
12103
  // 전체 경로가 안 맞고 basename 경계 판정도 앞의 `\` 때문에 걸러져 **정상 배선이 고아로 보고**됐다.
12046
12104
  for (const [label, ref] of [['posix', 'scripts/x/check.js'], ['win', 'scripts\\x\\check.js'], ['win-dot', '.\\scripts\\x\\check.js']]) {