leerness 1.36.120 → 1.36.121

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.120 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
125
+ 이 프로젝트는 Leerness v1.36.121 하네스를 사용합니다. 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.120는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **89개 도구**를 노출:
179
+ Leerness v1.36.121는 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.120는 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.120 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
200
+ 현재 누적: **v1.9.x → 1.36.121 릴리스 태그 이력** (수백 라운드) · _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.120: 2026-08-15
238
+ Last synced by Leerness v1.36.121: 2026-08-15
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.120';
37
+ const VERSION = '1.36.121';
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') 시 호스트 프로세스 오염.
package/lib/audit.js CHANGED
@@ -83,7 +83,9 @@ function _collectGuardInputs(root) {
83
83
  // 1.36.116 (검수 #5): 경로에 `scripts/` 가 있어야만 수집했다 — `tools/run-ci.mjs`·`ci/run.mjs`·`bin/run-checks.js`
84
84
  // 같은 흔한 배치를 놓치고, 그 안의 동적 열거자를 못 보면 다시 오탐이 폭발한다. 디렉터리 이름을 넓힌다.
85
85
  // 경로 구분자는 `/`·`\` 둘 다 받는다 — Windows CI 는 `.\bin\cli.js` 로 적는다(검수 P2 미탐).
86
- const _FILE_RE = /[\w./\\-]*(?:scripts|tools|ci|bin|tasks)[/\\][\w.-]+\.(?:m?js|cjs|ts|sh)/g;
86
+ // ⚠ **중간 디렉터리**를 허용해야 한다: `scripts/a/run.js` 처럼 한 단계 더 들어간 러너가 아예 수집되지 않아
87
+ // 그 러너가 배선한 것이 전부 거짓 고아가 됐다(실측: `scripts/run.js` 는 잡히고 `scripts/a/run.js` 는 안 잡힘).
88
+ const _FILE_RE = /[\w./\\-]*(?:scripts|tools|ci|bin|tasks)[/\\](?:[\w.-]+[/\\])*[\w.-]+\.(?:m?js|cjs|ts|sh)/g;
87
89
  const _norm = (x) => String(x).replace(/\\/g, '/').replace(/^\.\//, '');
88
90
  // CI 설정 파일이 부르는 것은 출처 null(무조건 유효한 러너), npm 스크립트가 부르는 것은 그 스크립트가 출처다.
89
91
  // ⚠ 자기 진입점 제외는 **CI 설정이 직접 부르는 경우까지** 막으면 안 된다(검수 P2) — 그 파일이 실제로
package/lib/pure-utils.js CHANGED
@@ -1035,6 +1035,15 @@ function _managedMerge(file, next, previous, archiveRel, overwriteSet, opts = {}
1035
1035
  // 1.36.116 (검수 #3): `guard:*` 가 후보 어휘에 없었다 — 이 기능이 잡으려는 것의 **이름 그 자체**인데
1036
1036
  // `guard:firestore-parity` 같은 스크립트는 후보가 0개라 구조적으로 못 봤다. `validate` 도 접미 위치에 없었다.
1037
1037
  const _GUARD_NAME_RE = /^(?:pre|post)?(?:test|check|verify|validate|guard|gate|lint|typecheck|audit|scan|e2e|smoke)\b|[:-](?:test|check|verify|validate|guard|gate|lint|audit|scan|e2e|smoke)\b/i;
1038
+ // 경로 표기 정규화 — 이 파일 안의 모든 경로 매칭이 같은 규칙을 쓴다(정규화가 갈리면 한쪽만 맞는다).
1039
+ // ① backslash → `/` (이스케이프된 `\\` 도 접힌다) ② 반복 `/./` 축약 ③ 중복 `//` 축약.
1040
+ function _normPath(s) {
1041
+ let t = String(s).replace(/\\+/g, '/');
1042
+ let prev;
1043
+ do { prev = t; t = t.split('/./').join('/'); } while (t !== prev);
1044
+ do { prev = t; t = t.split('//').join('/'); } while (t !== prev);
1045
+ return t;
1046
+ }
1038
1047
  // 동적 열거자 판정은 **두 신호를 독립적으로** 본다 — 한 파일이 package.json 을 읽고, scripts 를 열거한다.
1039
1048
  // ⚠ 근접 창(예: 400자)을 요구했다가 실측에서 깨졌다: 실제 열거자는 읽기(L42)와 열거(L98)가 56줄 떨어져 있었고,
1040
1049
  // 그 결과 가장 잘 만든 프로젝트에서 85건 오탐이 났다. 거리 가정을 쓰지 않는다.
@@ -1124,7 +1133,10 @@ function _detectOrphanGuards(input) {
1124
1133
  // **주석 줄의 언급은 실행이 아니다.** 이 저장소에서 실제로 겪었다 — 이 함정을 설명하려고 쓴 주석에
1125
1134
  // 스크립트 이름을 적었더니 그 이름이 '덮임' 으로 처리돼 진짜 고아가 사라졌다(자기참조 3회차).
1126
1135
  // 전면 문자열 마스킹은 앞서 정상 입력에서 깨졌으므로 쓰지 않는다 — 줄 시작이 주석인 줄만 뺀다(저위험).
1127
- const _dropCommentLines = (s) => String(s).split('\n').filter(l => !/^\s*(\/\/|#|\*|--)/.test(l)).join('\n');
1136
+ // ⚠ 수집기(lib/audit.js) **같은 주석 표기**를 써야 한다 — 1.36.120 에서 수집기에만 CMD 표기(`::`·`@REM`)
1137
+ // 넣고 판정기에 안 넣어서, CMD 주석 줄의 이름이 '호출됨' 으로 계상돼 진짜 고아가 사라졌다(검수 재현).
1138
+ // `--`·`;` 는 여기서도 주석이 아니다(여러 줄 명령의 연속 인자를 지운다).
1139
+ const _dropCommentLines = (s) => String(s).split('\n').filter(l => !/^\s*(\/\/|#|\*|::|@?REM\s)/i.test(l)).join('\n');
1128
1140
  const _has = (hay, name) => {
1129
1141
  const esc = String(name).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1130
1142
  return new RegExp(`(^|[^\\w:.-])${esc}([^\\w:.-]|$)`, 'm').test(hay);
@@ -1255,8 +1267,11 @@ function _detectOrphanGuards(input) {
1255
1267
  // **실제로 실행되는 가드가 고아로 보고됐다**(자체 헌트 실측: POSIX 표기만 통과).
1256
1268
  // `scripts/./check.js` 같은 dot-segment 도 편다 — 전체 경로 비교가 정규화를 안 해서 실제 호출이
1257
1269
  // 고아로 보고됐다(검수 재현). basename 경계는 앞의 `/` 를 거부하므로 폴백으로도 안 잡혔다.
1258
- const allText = (runners.filter(_liveRunner).map(r => String(r.text || '')).join('\n') + '\n'
1259
- + liveScripts.map(([, b]) => String(b || '')).join('\n')).replace(/\\/g, '/').split('/./').join('/');
1270
+ // ⚠ 정규화는 **한 곳**에서 한다(`_normPath`): 실제 JS 원문에는 이스케이프된 `\\` 가 들어 있고
1271
+ // (`require(".\\check.js")` 파일 바이트는 backslash 2개), `a/././b` 처럼 dot-segment 가 반복될 수 있다.
1272
+ // 한 번만 치환하면 `.//check.js` 나 `a//b` 가 남아 실제 호출을 놓친다(검수 재현, 실제 파일 바이트로 확인).
1273
+ const allText = _normPath(runners.filter(_liveRunner).map(r => String(r.text || '')).join('\n') + '\n'
1274
+ + liveScripts.map(([, b]) => String(b || '')).join('\n'));
1260
1275
  // ⚠ basename 만으로 덮였다고 보면 **동명 파일이 서로를 살린다**(검수 P2): `a/check.js` 를 부르는 배선이
1261
1276
  // `b/check.js` 까지 덮어 버린다. basename 은 그 이름이 유일할 때만 근거로 쓴다.
1262
1277
  const _baseCount = new Map();
@@ -1273,11 +1288,13 @@ function _detectOrphanGuards(input) {
1273
1288
  // 설정 파일 위치(`.github/workflows`) 기준으로 풀면 `.github/workflows/scripts/x.js` 가 돼 실제 호출을 놓친다(검수 재현).
1274
1289
  // 코드 파일 안의 `require('./x')` 는 그 파일 기준이 맞다.
1275
1290
  const dir = _isConfigRunner(r.file) ? [] : String(r.file || '').replace(/\\/g, '/').split('/').slice(0, -1);
1276
- const body = _dropCommentLines(String(r.text || ''));
1291
+ // 원문을 먼저 정규화한다 — 실제 JS 는 `.\\check.js`(backslash 2개)로 저장되고, 한 번만 치환하면
1292
+ // `.//check.js` 가 남아 아래 정규식에 안 걸린다(검수 재현: 실제로 도는 가드가 고아로 보고됐다).
1293
+ const body = _normPath(_dropCommentLines(String(r.text || '')));
1277
1294
  for (const m of body.matchAll(/(\.{1,2}(?:[/\\][\w.-]+)+\.(?:m?js|cjs|ts|sh))/g)) {
1278
1295
  const segs = dir.slice();
1279
1296
  let escaped = false;
1280
- for (const s of m[1].replace(/\\/g, '/').split('/')) {
1297
+ for (const s of _normPath(m[1]).split('/')) {
1281
1298
  if (s === '.' || s === '') continue;
1282
1299
  if (s === '..') { if (!segs.length) { escaped = true; break; } segs.pop(); } else segs.push(s);
1283
1300
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.36.120",
3
+ "version": "1.36.121",
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
@@ -5098,6 +5098,41 @@ total++;
5098
5098
  if (!ok) failed++;
5099
5099
  }
5100
5100
 
5101
+ // 1.36.121 — `--json` stderr 계약을 **selftest 하나가 아니라 명령 전반**에 건다.
5102
+ // 1.36.120 에서 audit 이 json 모드에 stdout 만 막고 stderr 를 열어 둔 것이 드러났고, 그 잡음의 내용이
5103
+ // 프로젝트 상태에 따라 달라져 같은 코드로 게이트가 통과/실패로 갈렸다. 한 곳에서 나온 결함은 대개 클래스다 —
5104
+ // 실측 스윕(조회성 79개)에서 정보성 누출은 audit 하나였고 나머지 4건은 **사용법 오류의 stderr 진단**(CLI 관례)이었다.
5105
+ // 그 측정을 래칫으로 고정한다: 성공 경로의 `--json` 은 stderr 가 비어야 한다.
5106
+ total++;
5107
+ {
5108
+ let ok = false; const bad = [];
5109
+ const sb = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-json121-'));
5110
+ const ENV = Object.assign({}, process.env, { TMPDIR: sb, TEMP: sb, TMP: sb, LEERNESS_OFFLINE: '1', LEERNESS_NO_PROMPT: '1' });
5111
+ try {
5112
+ const d = path.join(sb, 'p'); fs.mkdirSync(d, { recursive: true });
5113
+ fs.writeFileSync(path.join(d, 'package.json'), '{"name":"p","version":"0.1.0","scripts":{"test":"node t.js"}}');
5114
+ cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes'], { cwd: d, encoding: 'utf8', timeout: 300000, env: ENV });
5115
+ // 성공 경로만 — 인자 없이도 동작하는 조회성 명령 (사용법 오류 경로는 stderr 진단이 관례라 대상 아님)
5116
+ const CMDS = ['audit', 'health', 'doctor', 'drift', 'plan', 'task', 'pulse', 'tech', 'handoff',
5117
+ 'capabilities', 'commands', 'which', 'gate', 'dashboard', 'state'];
5118
+ for (const c of CMDS) {
5119
+ const r = cp.spawnSync(process.execPath, [CLI, c, '--path', d, '--json'], { cwd: d, encoding: 'utf8', timeout: 180000, env: ENV });
5120
+ const out = String(r.stdout || ''), err = String(r.stderr || '');
5121
+ if (!out.trim()) { bad.push(`${c}:출력없음`); continue; }
5122
+ let j = null; try { j = JSON.parse(out.trim()); } catch {}
5123
+ if (!j) { bad.push(`${c}:stdout이JSON아님`); continue; }
5124
+ if (err.trim() !== '') bad.push(`${c}:stderr잡음(${err.length}B:${err.trim().split('\n')[0].slice(0, 40)})`);
5125
+ }
5126
+ // 판별력 — 이 검사가 실제로 무언가를 실행했는지(전부 건너뛰면 공허하게 통과한다)
5127
+ if (bad.length === 0 && CMDS.length < 10) bad.push('대상이_너무적음');
5128
+ ok = bad.length === 0;
5129
+ } catch (e) { bad.push('ERR:' + String(e && e.message).slice(0, 80)); }
5130
+ finally { try { fs.rmSync(sb, { recursive: true, force: true }); } catch {} }
5131
+ console.log(ok ? '✓ B(1.36.121) --json stderr 계약 스윕: 조회성 15종 모두 stdout 단일 JSON + stderr 0'
5132
+ : '✗ --json stderr 계약 스윕 실패 ' + JSON.stringify(bad.slice(0, 6)));
5133
+ if (!ok) failed++;
5134
+ }
5135
+
5101
5136
  // 1.9.367 회귀 (UR-0025): _mergeEnvLines 모듈 분리 — migrate 가 사용자 .env 값을 key-aware 로 보존 (덮어쓰기 X)
5102
5137
  total++;
5103
5138
  {
@@ -11908,6 +11943,39 @@ total++;
11908
11943
  scriptFiles: ['scripts/check.js'],
11909
11944
  });
11910
11945
  if (relDot.orphanFiles.length) bad.push(`dot세그먼트_경로_오탐(${relDot.orphanFiles.join(',')})`);
11946
+ // (검수) 합성 문자열이 아니라 **실제 파일 바이트**로 잰다: JS 원문의 `require(".\\check.js")` 는
11947
+ // backslash 가 두 개다. 한 번만 치환하면 `.//check.js` 가 남아 실제로 도는 가드가 고아로 보고됐다.
11948
+ // 같은 픽스처가 **중첩 러너**(`scripts/a/run.js`)도 함께 잰다 — 그건 아예 수집되지 않고 있었다.
11949
+ const dwin = path.join(sb4, 'winrel');
11950
+ fs.mkdirSync(path.join(dwin, 'scripts', 'a'), { recursive: true });
11951
+ fs.mkdirSync(path.join(dwin, 'scripts', 'b'), { recursive: true });
11952
+ fs.mkdirSync(path.join(dwin, '.github', 'workflows'), { recursive: true });
11953
+ fs.writeFileSync(path.join(dwin, 'package.json'), JSON.stringify({ name: 'w', version: '0.1.0', scripts: { test: 'node scripts/a/run.js' } }));
11954
+ fs.writeFileSync(path.join(dwin, '.github', 'workflows', 'ci.yml'), 'jobs:\n a:\n steps:\n - run: npm test\n');
11955
+ fs.writeFileSync(path.join(dwin, 'scripts', 'a', 'run.js'), 'require(".\\\\check.js");\n');
11956
+ fs.writeFileSync(path.join(dwin, 'scripts', 'a', 'check.js'), '// guard\n');
11957
+ fs.writeFileSync(path.join(dwin, 'scripts', 'b', 'check.js'), '// other\n');
11958
+ const winInp = AUD._collectGuardInputs(dwin);
11959
+ if (!winInp.runners.some(r => /scripts[\\/]a[\\/]run\.js$/.test(r.file))) bad.push('중첩러너_미수집');
11960
+ const winRes = PU._detectOrphanGuards(winInp);
11961
+ if (winRes.orphanFiles.includes('scripts/a/check.js')) bad.push('실제JS의_윈도우_상대경로_오탐');
11962
+ if (!winRes.orphanFiles.includes('scripts/b/check.js')) bad.push('판별력:무관파일까지_덮음');
11963
+ // 반복 dot-segment 정규화
11964
+ const relDot2 = PU._detectOrphanGuards({
11965
+ packageScripts: { test: 'node t.js' },
11966
+ runners: [{ file: '.github/workflows/ci.yml', viaScript: null, viaScripts: [], unconditional: true,
11967
+ text: 'jobs:\n a:\n steps:\n - run: node scripts/././check.js\n - run: npm test\n' }],
11968
+ scriptFiles: ['scripts/check.js'],
11969
+ });
11970
+ if (relDot2.orphanFiles.length) bad.push(`반복dot세그먼트_오탐(${relDot2.orphanFiles.join(',')})`);
11971
+ // 판정기의 주석 규칙이 수집기와 같아야 한다 — CMD 표기를 한쪽에만 넣으면 주석 속 이름이 호출로 계상된다
11972
+ const cmt = PU._detectOrphanGuards({
11973
+ packageScripts: { test: 'node t.js', 'check:leaf': 'node l.js' },
11974
+ runners: [{ file: '.github/workflows/ci.yml', viaScript: null, viaScripts: [], unconditional: true,
11975
+ text: 'jobs:\n a:\n steps:\n - shell: cmd\n run: |\n REM npm run check:leaf\n echo hi\n - run: npm test\n' }],
11976
+ scriptFiles: [],
11977
+ });
11978
+ if (!cmt.orphanScripts.includes('check:leaf')) bad.push('판정기가_CMD주석을_실행으로');
11911
11979
  // (자체 헌트) Windows CI 는 `node scripts\x\check.js` 로 적는다. 후보 경로는 `/` 표기라
11912
11980
  // 전체 경로가 안 맞고 basename 경계 판정도 앞의 `\` 때문에 걸러져 **정상 배선이 고아로 보고**됐다.
11913
11981
  for (const [label, ref] of [['posix', 'scripts/x/check.js'], ['win', 'scripts\\x\\check.js'], ['win-dot', '.\\scripts\\x\\check.js']]) {