leerness 1.36.123 → 1.36.124

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.123 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
125
+ 이 프로젝트는 Leerness v1.36.124 하네스를 사용합니다. 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.123는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **89개 도구**를 노출:
179
+ Leerness v1.36.124는 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.123는 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.123 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
200
+ 현재 누적: **v1.9.x → 1.36.124 릴리스 태그 이력** (수백 라운드) · _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.123: 2026-08-16
238
+ Last synced by Leerness v1.36.124: 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.123';
37
+ const VERSION = '1.36.124';
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') 시 호스트 프로세스 오염.
@@ -1068,7 +1068,11 @@ function managedMerge(file, next, previous, archiveDir, mergeOpts = {}) {
1068
1068
  if (!archiveRel && previous) {
1069
1069
  const _m = /(?:전체 원본 백업|Full original backup):\s*`([^`]+)`/.exec(String(previous));
1070
1070
  if (_m && _m[1] && _m[1] !== '.harness/archive') {
1071
- archiveRel = exists(path.join(_base, _m[1])) ? _m[1] : '.harness/archive';
1071
+ // 절대 경로 포인터도 있다(구버전이 cwd 기준으로 박아 둔 것) — `path.join` 으로 이어 붙이면
1072
+ // 존재하지 않는 경로가 돼 정상 포인터를 격하시킨다(검수 P2). 절대면 그대로 본다.
1073
+ const _p = _m[1];
1074
+ const _abs = path.isAbsolute(_p) || /^[A-Za-z]:[\\/]/.test(_p);
1075
+ archiveRel = exists(_abs ? _p : path.join(_base, _p)) ? _p : '.harness/archive';
1072
1076
  }
1073
1077
  }
1074
1078
  return _managedMerge(file, next, previous, archiveRel, MERGE_OVERWRITE_FILES, mergeOpts); // 1.36.60: altTemplate/lang 전달
@@ -1382,7 +1386,7 @@ async function install(root, opts = {}) {
1382
1386
  if (!exists(p)) { _noop = false; if (process.env.LEERNESS_DEBUG_NOOP) log(`[noop-debug] missing: ${f}`); break; }
1383
1387
  if (managedOverwrite.has(f)) {
1384
1388
  let cur = ''; try { cur = read(p); } catch { _noop = false; break; }
1385
- const wouldBe = managedMerge(f, c, cur, null, { altTemplate: _altFiles[f], lang });
1389
+ const wouldBe = managedMerge(f, c, cur, null, { altTemplate: _altFiles[f], lang, root }); // 1.36.124: 같은 이유로 root 전달
1386
1390
  if (_normVolatile(f, wouldBe) !== _normVolatile(f, cur)) { _noop = false; if (process.env.LEERNESS_DEBUG_NOOP) log(`[noop-debug] differs: ${f}`); break; }
1387
1391
  }
1388
1392
  // 비-managed 는 preserve(불변) 경로 — 존재하면 변경 없음
@@ -6626,7 +6630,7 @@ function _selfTestCases() {
6626
6630
  const guards = /if \(level === '🔴 critical'\) process\.exitCode = 1;\s*\n\s*log\(JSON\.stringify/.test(dr)
6627
6631
  && s.includes("writeIfSafe(root, '.claude/commands/update.md', _updContent")
6628
6632
  && s.includes('force: !!opts.force, dry: false, migration: true')
6629
- && s.includes('const lang = detectLanguageValue(root, ') && s.includes("managedMerge(key, tmpl, cur, null, { lang })");
6633
+ && s.includes('const lang = detectLanguageValue(root, ') && s.includes("managedMerge(key, tmpl, cur, null, { lang, root })") // 1.36.124: 대상 루트 전달이 계약이다(검수 P1);
6630
6634
  return f13ok && skipOk && f7ok && f11ok && guards;
6631
6635
  } finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
6632
6636
  } }
@@ -7100,7 +7104,9 @@ function integrityCmd(root) {
7100
7104
  // raw 교체는 사용자가 추가한 커스텀 안전 규칙까지 지웠다. 부재/판독불가만 raw 재생성.
7101
7105
  if (_readable && exists(fp)) {
7102
7106
  let cur = ''; try { cur = read(fp); } catch {}
7103
- writeUtf8(fp, managedMerge(key, tmpl, cur, null, { lang }));
7107
+ // 1.36.124 (검수 P1): 대상 루트를 넘기면 보존-포인터 실재 판정이 **cwd 기준**이 된다 —
7108
+ // `integrity check --repair --path <다른 프로젝트>` 를 다른 디렉터리에서 돌리면 정상 포인터를 격하시킨다.
7109
+ writeUtf8(fp, managedMerge(key, tmpl, cur, null, { lang, root }));
7104
7110
  } else {
7105
7111
  writeUtf8(fp, tmpl);
7106
7112
  }
@@ -8332,12 +8338,24 @@ function _p0097AsyncLockOk() {
8332
8338
  } catch { return false; }
8333
8339
  finally { try { fs.rmSync(arena, { recursive: true, force: true }); } catch {} }
8334
8340
  }
8341
+ // 1.36.124 (살아 있는 MCP 통신 실측): selftest 프로브가 `process.env` 를 그대로 상속했다 —
8342
+ // MCP 래퍼는 `LEERNESS_NO_WORKFLOW_GUIDE` 등으로 **안내 출력을 일부러 끄고** 자식을 띄우므로,
8343
+ // "안내가 뜬다" 를 단언하는 프로브가 MCP 경유에서만 실패했다. 그 결과 `leerness_selftest` 가
8344
+ // AI 에게 "설치가 깨졌다" 고 **거짓 보고**했다(343 중 1 실패). 프로브는 억제 변수를 걷어낸 환경에서 돈다.
8345
+ const _PROBE_SUPPRESS_ENV = ['LEERNESS_INTERNAL', 'LEERNESS_NO_BANNER', 'LEERNESS_NO_PROMPT',
8346
+ 'LEERNESS_NO_WORKFLOW_GUIDE', 'LEERNESS_NO_STALE_CHECK', 'LEERNESS_NO_DRIFT_CHECK',
8347
+ 'LEERNESS_NO_HEADLINE', 'LEERNESS_NO_LAZY_WARN'];
8348
+ function _probeEnv(extra) {
8349
+ const e = Object.assign({}, process.env, extra || {});
8350
+ for (const k of _PROBE_SUPPRESS_ENV) delete e[k];
8351
+ return e;
8352
+ }
8335
8353
  function _p0104ReminderOk() {
8336
8354
  const os2 = require('os');
8337
8355
  const cp2 = require('child_process');
8338
8356
  const arena = fs.mkdtempSync(path.join(os2.tmpdir(), 'leerness-p104r-'));
8339
8357
  try {
8340
- const env2 = Object.assign({}, process.env, { LEERNESS_OFFLINE: '1', LEERNESS_NO_AUTO_ROADMAP: '1' });
8358
+ const env2 = _probeEnv({ LEERNESS_OFFLINE: '1', LEERNESS_NO_AUTO_ROADMAP: '1' });
8341
8359
  const mk = (body) => {
8342
8360
  const d = path.join(arena, 'p' + Math.abs(body.length));
8343
8361
  fs.mkdirSync(d, { recursive: true });
@@ -8445,7 +8463,7 @@ function _p0104UsageOk() {
8445
8463
  const arena = fs.mkdtempSync(path.join(os2.tmpdir(), 'leerness-p104-'));
8446
8464
  try {
8447
8465
  const cwdP = path.join(arena, 'CWDPROJ'), tgt = path.join(arena, 'TARGETPROJ');
8448
- const env2 = Object.assign({}, process.env, { LEERNESS_OFFLINE: '1', LEERNESS_NO_AUTO_ROADMAP: '1' });
8466
+ const env2 = _probeEnv({ LEERNESS_OFFLINE: '1', LEERNESS_NO_AUTO_ROADMAP: '1' }); // 억제 변수 제거(위 주석)
8449
8467
  for (const d of [cwdP, tgt]) {
8450
8468
  fs.mkdirSync(d, { recursive: true });
8451
8469
  fs.writeFileSync(path.join(d, 'package.json'), '{"name":"x","version":"0.1.0"}');
@@ -22028,6 +22046,14 @@ function mcpServeCmd(root) {
22028
22046
  const v = parseInt(process.env.LEERNESS_MCP_TIMEOUT_MS || '', 10);
22029
22047
  return (Number.isFinite(v) && v >= 5000 && v <= 3600000) ? v : null;
22030
22048
  })();
22049
+ // 1.36.124 (살아 있는 MCP 통신 실측): `leerness_selftest` 는 **항상** 60초 한도를 넘겨 타임아웃만 돌려줬다.
22050
+ // 실측 116초(우리 repo·빈 임시 프로젝트 둘 다) — 즉 광고된 89개 도구 중 **설치 검증 도구가 MCP 로는 무용**이었다.
22051
+ // 전역 한도를 올리지 않는 이유는 위와 같다(무관한 도구가 서버를 그만큼 얼린다). 그래서 이 명령에만 준다.
22052
+ const MCP_TIMEOUT_LONG_MS = 300000; // 실측 116초 대비 2.5배 여유
22053
+ function _needsLongBudget(cliArgs) {
22054
+ const a = (cliArgs || []).map(String);
22055
+ return a[0] === 'selftest';
22056
+ }
22031
22057
  // bugfix probe 가 도는 경로만 긴 예산을 받는다 — done 전이(task update --status done)와 TodoWrite 왕복(task sync).
22032
22058
  function _needsProbeBudget(cliArgs) {
22033
22059
  const a = (cliArgs || []).map(String);
@@ -22039,7 +22065,8 @@ function mcpServeCmd(root) {
22039
22065
  }
22040
22066
  function _mcpTimeoutFor(cliArgs) {
22041
22067
  return MCP_TIMEOUT_OVERRIDE_MS != null ? MCP_TIMEOUT_OVERRIDE_MS
22042
- : (_needsProbeBudget(cliArgs) ? MCP_TIMEOUT_PROBE_MS : MCP_TIMEOUT_DEFAULT_MS);
22068
+ : (_needsProbeBudget(cliArgs) ? MCP_TIMEOUT_PROBE_MS
22069
+ : (_needsLongBudget(cliArgs) ? MCP_TIMEOUT_LONG_MS : MCP_TIMEOUT_DEFAULT_MS));
22043
22070
  }
22044
22071
  const _shQuote = (s) => (/[\s"']/.test(String(s)) ? `"${String(s).replace(/"/g, '\\"')}"` : String(s));
22045
22072
  function callLeerness(cliArgs) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.36.123",
3
+ "version": "1.36.124",
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
@@ -5138,6 +5138,51 @@ total++;
5138
5138
  if (!ok) failed++;
5139
5139
  }
5140
5140
 
5141
+ // 1.36.124 — MCP 서버를 **실제 JSON-RPC 로** 두드린다. 단위 테스트가 초록이어도 살아 있는 통신은 깨질 수 있다.
5142
+ // 실측으로 두 결함이 나왔다: ① `leerness_selftest` 는 실측 116초인데 MCP 기본 한도가 60초라 **항상 타임아웃**
5143
+ // (광고된 89개 도구 중 설치 검증 도구가 MCP 로는 무용) ② selftest 프로브가 `process.env` 를 상속해
5144
+ // MCP 래퍼가 끄는 안내를 단언 → MCP 경유에서만 1건 실패 = AI 에게 "설치가 깨졌다" 는 **거짓 보고**.
5145
+ total++;
5146
+ {
5147
+ let ok = false; const bad = [];
5148
+ const sb = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-mcp124-'));
5149
+ const ENVm = Object.assign({}, process.env, { TMPDIR: sb, TEMP: sb, TMP: sb, LEERNESS_OFFLINE: '1', LEERNESS_NO_PROMPT: '1' });
5150
+ try {
5151
+ const d = path.join(sb, 'p'); fs.mkdirSync(d, { recursive: true });
5152
+ fs.writeFileSync(path.join(d, 'package.json'), '{"name":"p","version":"0.1.0"}');
5153
+ cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes'], { cwd: d, encoding: 'utf8', timeout: 300000, env: ENVm });
5154
+ const rpc = (id, method, params) => JSON.stringify({ jsonrpc: '2.0', id, method, ...(params ? { params } : {}) }) + '\n';
5155
+ const input = rpc(1, 'initialize', { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'e2e', version: '0' } })
5156
+ + rpc(2, 'tools/list') + rpc(3, 'tools/call', { name: 'leerness_selftest', arguments: { path: d } });
5157
+ const r = cp.spawnSync(process.execPath, [CLI, 'mcp', 'serve', '--path', d],
5158
+ { cwd: d, input, encoding: 'utf8', timeout: 900000, env: ENVm, maxBuffer: 64 * 1024 * 1024 });
5159
+ const msgs = [];
5160
+ for (const line of String(r.stdout || '').split('\n')) { const s = line.trim(); if (s) { try { msgs.push(JSON.parse(s)); } catch { bad.push('비JSON줄'); } } }
5161
+ const by = (i) => msgs.find(m => m.id === i);
5162
+ if (String(r.stderr || '').length !== 0) bad.push(`stderr(${String(r.stderr).length}B)`);
5163
+ const init = by(1); if (!init || init.error || !(init.result && init.result.serverInfo)) bad.push('initialize실패');
5164
+ const tools = by(2) && by(2).result && by(2).result.tools;
5165
+ if (!Array.isArray(tools) || tools.length < 50) bad.push(`tools/list(${Array.isArray(tools) ? tools.length : 'none'})`);
5166
+ else {
5167
+ if (tools.some(t => !t.description || !String(t.description).trim())) bad.push('도구설명없음');
5168
+ if (tools.some(t => !t.inputSchema || typeof t.inputSchema !== 'object')) bad.push('도구스키마없음');
5169
+ }
5170
+ const call = by(3);
5171
+ const txt = call && call.result && Array.isArray(call.result.content) ? String((call.result.content[0] || {}).text || '') : '';
5172
+ // ⚠ content 가 비어 있지 않다는 것만 보면 **타임아웃 문구도 성공**으로 센다(내 첫 프로브가 그랬다).
5173
+ if (!call || call.error) bad.push('tools/call오류');
5174
+ else if (/안에 끝나지 않아 중단/.test(txt)) bad.push('tools/call타임아웃');
5175
+ else if (!/"total"\s*:\s*\d+|전체 \d+건 통과/.test(txt)) bad.push('tools/call결과아님');
5176
+ // MCP 경유 selftest 가 **실패를 보고하면** 그건 거짓 보고다(같은 프로젝트에서 CLI 는 통과한다)
5177
+ else { const m = /"fail"\s*:\s*(\d+)/.exec(txt); if (m && Number(m[1]) > 0) bad.push(`MCP경유_selftest_거짓실패(fail=${m[1]})`); }
5178
+ ok = bad.length === 0;
5179
+ } catch (e) { bad.push('ERR:' + String(e && e.message).slice(0, 80)); }
5180
+ finally { try { fs.rmSync(sb, { recursive: true, force: true }); } catch {} }
5181
+ console.log(ok ? '✓ B(1.36.124) MCP 살아있는 통신: initialize · tools/list(설명·스키마 전수) · tools/call selftest 결과 반환 · stderr 0'
5182
+ : '✗ MCP 살아있는 통신 실패 ' + JSON.stringify(bad.slice(0, 6)));
5183
+ if (!ok) failed++;
5184
+ }
5185
+
5141
5186
  // 1.9.367 회귀 (UR-0025): _mergeEnvLines 모듈 분리 — migrate 가 사용자 .env 값을 key-aware 로 보존 (덮어쓰기 X)
5142
5187
  total++;
5143
5188
  {