leerness 1.36.122 → 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 +4 -4
- package/bin/leerness.js +62 -15
- package/package.json +1 -1
- package/scripts/e2e.js +104 -1
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.
|
|
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.
|
|
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.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.
|
|
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.
|
|
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.
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -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,20 @@ function managedMerge(file, next, previous, archiveDir, mergeOpts = {}) {
|
|
|
1046
1061
|
// 1.36.122 (자기 저장소 도그푸딩): 백업을 만들지 않는 실행(`adapter` 등)은 여기서 일반 경로를 **채워** 넘겼고,
|
|
1047
1062
|
// 그러면 이전 마이그레이션이 적어 둔 **구체 백업 경로가 덮여** 사라졌다(우리 repo 에서 실측).
|
|
1048
1063
|
// 백업이 없으면 `null` 을 넘겨, 이전 문서의 포인터를 그대로 두게 한다(보존 블록의 정보 손실 방지).
|
|
1049
|
-
|
|
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
|
+
// 절대 경로 포인터도 있다(구버전이 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';
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1050
1078
|
return _managedMerge(file, next, previous, archiveRel, MERGE_OVERWRITE_FILES, mergeOpts); // 1.36.60: altTemplate/lang 전달
|
|
1051
1079
|
}
|
|
1052
1080
|
|
|
@@ -1358,7 +1386,7 @@ async function install(root, opts = {}) {
|
|
|
1358
1386
|
if (!exists(p)) { _noop = false; if (process.env.LEERNESS_DEBUG_NOOP) log(`[noop-debug] missing: ${f}`); break; }
|
|
1359
1387
|
if (managedOverwrite.has(f)) {
|
|
1360
1388
|
let cur = ''; try { cur = read(p); } catch { _noop = false; break; }
|
|
1361
|
-
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 전달
|
|
1362
1390
|
if (_normVolatile(f, wouldBe) !== _normVolatile(f, cur)) { _noop = false; if (process.env.LEERNESS_DEBUG_NOOP) log(`[noop-debug] differs: ${f}`); break; }
|
|
1363
1391
|
}
|
|
1364
1392
|
// 비-managed 는 preserve(불변) 경로 — 존재하면 변경 없음
|
|
@@ -1592,13 +1620,7 @@ async function install(root, opts = {}) {
|
|
|
1592
1620
|
// 1.36.115: 구버전은 스킬을 `.claude/skills/leerness.md`(평면)로 깔았고 그 형식은 발견되지 않는다.
|
|
1593
1621
|
// 새 위치로 옮겨 심었지만 **옛 파일은 지우지 않는다** — 사용자가 손댔을 수 있고, 이 저장소의 계약은 false-DROP 이 버그다.
|
|
1594
1622
|
// 대신 사본 2벌이 생겼다는 사실과 어느 쪽이 실제로 읽히는지 알린다(안 그러면 옛 파일을 고치고 반영 안 돼 혼란).
|
|
1595
|
-
|
|
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 {}
|
|
1623
|
+
_warnLegacySkillFile(root);
|
|
1602
1624
|
if (!has('--no-auto-update') && !opts.minimal) {
|
|
1603
1625
|
try { autoUpdateInstall(root); } catch (e) { warn('auto-update hook install skipped: ' + (e && e.message)); }
|
|
1604
1626
|
} else if (opts.minimal) {
|
|
@@ -6608,7 +6630,7 @@ function _selfTestCases() {
|
|
|
6608
6630
|
const guards = /if \(level === '🔴 critical'\) process\.exitCode = 1;\s*\n\s*log\(JSON\.stringify/.test(dr)
|
|
6609
6631
|
&& s.includes("writeIfSafe(root, '.claude/commands/update.md', _updContent")
|
|
6610
6632
|
&& s.includes('force: !!opts.force, dry: false, migration: true')
|
|
6611
|
-
&& 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);
|
|
6612
6634
|
return f13ok && skipOk && f7ok && f11ok && guards;
|
|
6613
6635
|
} finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
6614
6636
|
} }
|
|
@@ -7082,7 +7104,9 @@ function integrityCmd(root) {
|
|
|
7082
7104
|
// raw 교체는 사용자가 추가한 커스텀 안전 규칙까지 지웠다. 부재/판독불가만 raw 재생성.
|
|
7083
7105
|
if (_readable && exists(fp)) {
|
|
7084
7106
|
let cur = ''; try { cur = read(fp); } catch {}
|
|
7085
|
-
|
|
7107
|
+
// 1.36.124 (검수 P1): 대상 루트를 안 넘기면 보존-포인터 실재 판정이 **cwd 기준**이 된다 —
|
|
7108
|
+
// `integrity check --repair --path <다른 프로젝트>` 를 다른 디렉터리에서 돌리면 정상 포인터를 격하시킨다.
|
|
7109
|
+
writeUtf8(fp, managedMerge(key, tmpl, cur, null, { lang, root }));
|
|
7086
7110
|
} else {
|
|
7087
7111
|
writeUtf8(fp, tmpl);
|
|
7088
7112
|
}
|
|
@@ -8314,12 +8338,24 @@ function _p0097AsyncLockOk() {
|
|
|
8314
8338
|
} catch { return false; }
|
|
8315
8339
|
finally { try { fs.rmSync(arena, { recursive: true, force: true }); } catch {} }
|
|
8316
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
|
+
}
|
|
8317
8353
|
function _p0104ReminderOk() {
|
|
8318
8354
|
const os2 = require('os');
|
|
8319
8355
|
const cp2 = require('child_process');
|
|
8320
8356
|
const arena = fs.mkdtempSync(path.join(os2.tmpdir(), 'leerness-p104r-'));
|
|
8321
8357
|
try {
|
|
8322
|
-
const env2 =
|
|
8358
|
+
const env2 = _probeEnv({ LEERNESS_OFFLINE: '1', LEERNESS_NO_AUTO_ROADMAP: '1' });
|
|
8323
8359
|
const mk = (body) => {
|
|
8324
8360
|
const d = path.join(arena, 'p' + Math.abs(body.length));
|
|
8325
8361
|
fs.mkdirSync(d, { recursive: true });
|
|
@@ -8427,7 +8463,7 @@ function _p0104UsageOk() {
|
|
|
8427
8463
|
const arena = fs.mkdtempSync(path.join(os2.tmpdir(), 'leerness-p104-'));
|
|
8428
8464
|
try {
|
|
8429
8465
|
const cwdP = path.join(arena, 'CWDPROJ'), tgt = path.join(arena, 'TARGETPROJ');
|
|
8430
|
-
const env2 =
|
|
8466
|
+
const env2 = _probeEnv({ LEERNESS_OFFLINE: '1', LEERNESS_NO_AUTO_ROADMAP: '1' }); // 억제 변수 제거(위 주석)
|
|
8431
8467
|
for (const d of [cwdP, tgt]) {
|
|
8432
8468
|
fs.mkdirSync(d, { recursive: true });
|
|
8433
8469
|
fs.writeFileSync(path.join(d, 'package.json'), '{"name":"x","version":"0.1.0"}');
|
|
@@ -22010,6 +22046,14 @@ function mcpServeCmd(root) {
|
|
|
22010
22046
|
const v = parseInt(process.env.LEERNESS_MCP_TIMEOUT_MS || '', 10);
|
|
22011
22047
|
return (Number.isFinite(v) && v >= 5000 && v <= 3600000) ? v : null;
|
|
22012
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
|
+
}
|
|
22013
22057
|
// bugfix probe 가 도는 경로만 긴 예산을 받는다 — done 전이(task update --status done)와 TodoWrite 왕복(task sync).
|
|
22014
22058
|
function _needsProbeBudget(cliArgs) {
|
|
22015
22059
|
const a = (cliArgs || []).map(String);
|
|
@@ -22021,7 +22065,8 @@ function mcpServeCmd(root) {
|
|
|
22021
22065
|
}
|
|
22022
22066
|
function _mcpTimeoutFor(cliArgs) {
|
|
22023
22067
|
return MCP_TIMEOUT_OVERRIDE_MS != null ? MCP_TIMEOUT_OVERRIDE_MS
|
|
22024
|
-
: (_needsProbeBudget(cliArgs) ? MCP_TIMEOUT_PROBE_MS
|
|
22068
|
+
: (_needsProbeBudget(cliArgs) ? MCP_TIMEOUT_PROBE_MS
|
|
22069
|
+
: (_needsLongBudget(cliArgs) ? MCP_TIMEOUT_LONG_MS : MCP_TIMEOUT_DEFAULT_MS));
|
|
22025
22070
|
}
|
|
22026
22071
|
const _shQuote = (s) => (/[\s"']/.test(String(s)) ? `"${String(s).replace(/"/g, '\\"')}"` : String(s));
|
|
22027
22072
|
function callLeerness(cliArgs) {
|
|
@@ -23943,6 +23988,8 @@ function adapterCmd(root, tool, opts = {}) {
|
|
|
23943
23988
|
// 주장은 **파일 단위**로 한다 — 우리가 쓴 파일만 이름을 대고 말한다.
|
|
23944
23989
|
_wrote.forEach(r => log(` ℹ ${r.file} 에 MCP 등록 — 이 파일을 읽는 도구는 leerness MCP verb(state_show/start/record/verify/handoff)를 직접 호출 가능`));
|
|
23945
23990
|
_kept.forEach(r => log(` ℹ ${r.file} 의 기존 leerness 항목을 그대로 두었습니다 — 그 항목이 leerness MCP 를 띄우는지는 확인하지 않았습니다`));
|
|
23991
|
+
// 스킬을 까는 어댑터면 구 평면 파일도 같이 알린다(1.36.123: `init` 에만 있어 이 경로는 조용했다)
|
|
23992
|
+
if (a.keys.some(k => String(k).includes('.claude/skills/'))) _warnLegacySkillFile(root);
|
|
23946
23993
|
// 보존은 안전하지만 **막다른 길이면 안 된다** — 우리 것이 망가졌을 때 되돌릴 방법을 알려 준다(자체 헌트).
|
|
23947
23994
|
if (_kept.length || _brk.length) log(` → 표준 설정으로 되돌리려면 그 파일의 "leerness" 항목을 지우고 이 명령을 다시 실행하세요`);
|
|
23948
23995
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
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
|
@@ -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('대상이_너무적음');
|
|
@@ -5135,6 +5138,51 @@ total++;
|
|
|
5135
5138
|
if (!ok) failed++;
|
|
5136
5139
|
}
|
|
5137
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
|
+
|
|
5138
5186
|
// 1.9.367 회귀 (UR-0025): _mergeEnvLines 모듈 분리 — migrate 가 사용자 .env 값을 key-aware 로 보존 (덮어쓰기 X)
|
|
5139
5187
|
total++;
|
|
5140
5188
|
{
|
|
@@ -5201,10 +5249,29 @@ total++;
|
|
|
5201
5249
|
// 사용자 커스텀 + 구체 백업 포인터를 가진 보존 블록을 만든다
|
|
5202
5250
|
fs.writeFileSync(cf, fs.readFileSync(cf, 'utf8')
|
|
5203
5251
|
+ '\n<!-- leerness:migration-preserved -->\n## Preserved previous content\n\n> 이전 버전에서 이어진 사용자/프로젝트 커스텀 내용 — 마이그레이션마다 자동 이월됩니다. 전체 원본 백업: `.harness/archive/leerness-9.9.9-STAMP`\n\nUSER-KEEP-LINE\n');
|
|
5204
|
-
|
|
5252
|
+
// ⚠ adapter 가 **실제로 무언가 했는지**부터 확인한다 — no-op/실패여도 미리 심은 포인터와 사용자 줄은
|
|
5253
|
+
// 그대로 남아 이 단언이 공허하게 통과한다(검수 P2). 아카이브도 실재해야 유지가 정당하다.
|
|
5254
|
+
fs.mkdirSync(path.join(d, '.harness', 'archive', 'leerness-9.9.9-STAMP'), { recursive: true });
|
|
5255
|
+
fs.rmSync(path.join(d, '.mcp.json'), { force: true });
|
|
5256
|
+
const rAd = cp.spawnSync(process.execPath, [CLI, 'adapter', 'claude', '--path', d], { cwd: d, encoding: 'utf8', timeout: 300000, env: ENVp });
|
|
5257
|
+
if (rAd.status !== 0) bad.push(`adapter실패(exit=${rAd.status})`);
|
|
5258
|
+
if (!fs.existsSync(path.join(d, '.mcp.json'))) bad.push('adapter가_아무것도_안함(공허)');
|
|
5205
5259
|
const after2 = fs.readFileSync(cf, 'utf8');
|
|
5206
5260
|
if (!/leerness-9\.9\.9-STAMP/.test(after2)) bad.push('CLI경로에서_포인터_격하');
|
|
5207
5261
|
if (!after2.includes('USER-KEEP-LINE')) bad.push('CLI경로에서_사용자라인_유실');
|
|
5262
|
+
// ⚠ 위 픽스처는 아카이브가 **실재**한다(위에서 만든다). 실재하지 않으면 그 문장은 거짓 약속이므로
|
|
5263
|
+
// 일반 경로로 되돌려야 한다 — 유지가 과하면 없어진 백업을 영구 고착시킨다(자체 헌트가 잡은 자리).
|
|
5264
|
+
const dgone = path.join(sb, 'gone'); fs.mkdirSync(dgone, { recursive: true });
|
|
5265
|
+
fs.writeFileSync(path.join(dgone, 'package.json'), '{"name":"g","version":"0.1.0"}');
|
|
5266
|
+
cp.spawnSync(process.execPath, [CLI, 'init', dgone, '--yes'], { cwd: dgone, encoding: 'utf8', timeout: 300000, env: ENVp });
|
|
5267
|
+
const gf = path.join(dgone, 'CLAUDE.md');
|
|
5268
|
+
fs.writeFileSync(gf, fs.readFileSync(gf, 'utf8')
|
|
5269
|
+
+ '\n<!-- leerness:migration-preserved -->\n## Preserved previous content\n\n> 이전 버전에서 이어진 사용자/프로젝트 커스텀 내용 — 마이그레이션마다 자동 이월됩니다. 전체 원본 백업: `.harness/archive/GONE-9.9.9`\n\nUSER-KEEP-LINE\n');
|
|
5270
|
+
cp.spawnSync(process.execPath, [CLI, 'adapter', 'claude', '--path', dgone], { cwd: dgone, encoding: 'utf8', timeout: 300000, env: ENVp });
|
|
5271
|
+
const gAfter = fs.readFileSync(gf, 'utf8');
|
|
5272
|
+
if (/GONE-9\.9\.9/.test(gAfter)) bad.push('없어진_아카이브를_계속_가리킴');
|
|
5273
|
+
if (!/전체 원본 백업: `\.harness\/archive`/.test(gAfter)) bad.push('없어진_아카이브인데_일반경로로_안내림');
|
|
5274
|
+
if (!gAfter.includes('USER-KEEP-LINE')) bad.push('격하시_사용자라인_유실');
|
|
5208
5275
|
} finally { try { fs.rmSync(sb, { recursive: true, force: true }); } catch {} }
|
|
5209
5276
|
ok = bad.length === 0;
|
|
5210
5277
|
} catch (e) { bad.push('ERR:' + String(e && e.message).slice(0, 80)); }
|
|
@@ -11284,6 +11351,22 @@ total++;
|
|
|
11284
11351
|
}
|
|
11285
11352
|
// 평면 파일을 새로 만들지 않는다(구버전 잔재는 별개 — 지우지 않되 새로 만들지도 않는다)
|
|
11286
11353
|
if (fs.existsSync(path.join(d, '.claude', 'skills', 'leerness.md'))) bad.push('skill:평면파일을새로만듦');
|
|
11354
|
+
// (자기 저장소 도그푸딩) 구 평면 잔재 경고가 `init` 에만 있었다 — 도구 배선의 문서화된 진입점인
|
|
11355
|
+
// `adapter` 로 들어온 사용자는 사본 2벌을 갖고도 아무 말을 못 들었다(우리 repo 가 그 상태였다).
|
|
11356
|
+
{
|
|
11357
|
+
const dleg = mk();
|
|
11358
|
+
fs.rmSync(path.join(dleg, '.claude', 'skills', 'leerness'), { recursive: true, force: true });
|
|
11359
|
+
fs.writeFileSync(path.join(dleg, '.claude', 'skills', 'leerness.md'), '---\nname: leerness\n---\nold\n');
|
|
11360
|
+
const rleg = cp.spawnSync(process.execPath, [CLI, 'adapter', 'claude', '--path', dleg], { cwd: dleg, encoding: 'utf8', timeout: 300000, env: ENV });
|
|
11361
|
+
const oleg = String(rleg.stdout || '') + String(rleg.stderr || '');
|
|
11362
|
+
if (!fs.existsSync(path.join(dleg, '.claude', 'skills', 'leerness', 'SKILL.md'))) bad.push('skill:adapter가_새경로_미생성');
|
|
11363
|
+
if (!/구 스킬 파일/.test(oleg)) bad.push('skill:adapter가_구파일_미경고');
|
|
11364
|
+
if (!fs.existsSync(path.join(dleg, '.claude', 'skills', 'leerness.md'))) bad.push('skill:구파일을_지움(false-DROP)');
|
|
11365
|
+
// 판별력 — 잔재가 없으면 그 경고가 뜨면 안 된다
|
|
11366
|
+
const dclean = mk();
|
|
11367
|
+
const rcl = cp.spawnSync(process.execPath, [CLI, 'adapter', 'claude', '--path', dclean], { cwd: dclean, encoding: 'utf8', timeout: 300000, env: ENV });
|
|
11368
|
+
if (/구 스킬 파일/.test(String(rcl.stdout || '') + String(rcl.stderr || ''))) bad.push('skill:잔재없는데_경고');
|
|
11369
|
+
}
|
|
11287
11370
|
// ⚠ `init` 만 밟으면 안 된다. 실제로 `lib/catalogs.js` 의 어댑터 목록이 옛 평면 경로를 유지하고 있어
|
|
11288
11371
|
// `adapter claude` 가 이 수정을 되돌리고 있었다(1.36.116 에서 발견) — 가드가 밟지 않은 경로였다.
|
|
11289
11372
|
const da = mk(['--minimal']);
|
|
@@ -12041,6 +12124,26 @@ total++;
|
|
|
12041
12124
|
const starInp = AUD._collectGuardInputs(dstar);
|
|
12042
12125
|
if (starInp.runners.some(r => /ghost\.js$/.test(r.file))) bad.push('블록주석에서_러너_수집');
|
|
12043
12126
|
if (!PU._detectOrphanGuards(starInp).orphanScripts.includes('check:leaf')) bad.push('블록주석이_고아를_덮음');
|
|
12127
|
+
// ⚠ 위는 **수집기만** 잰다 — 판정기의 `*` 처리를 지워도 통과한다(검수 P3: 수집기가 이미 걸러 놨으니까).
|
|
12128
|
+
// 판정기 쪽은 러너를 **직접 주입해** 따로 잰다: 주석 줄에만 이름이 있으면 고아여야 한다.
|
|
12129
|
+
const starDet = PU._detectOrphanGuards({
|
|
12130
|
+
packageScripts: { test: 'node t.js', 'check:leaf': 'node l.js' },
|
|
12131
|
+
runners: [{ file: '.github/workflows/ci.yml', viaScript: null, viaScripts: [], unconditional: true,
|
|
12132
|
+
text: 'jobs:\n a:\n steps:\n - run: npm test\n' },
|
|
12133
|
+
{ file: 'scripts/x.js', viaScript: 'test', viaScripts: ['test'],
|
|
12134
|
+
text: '/*\n * npm run check:leaf\n */\nconsole.log(1);\n' }],
|
|
12135
|
+
scriptFiles: [],
|
|
12136
|
+
});
|
|
12137
|
+
if (!starDet.orphanScripts.includes('check:leaf')) bad.push('판정기가_블록주석을_실행으로');
|
|
12138
|
+
// 판별력 — 주석이 아닌 실제 호출은 여전히 덮여야 한다
|
|
12139
|
+
const starLive = PU._detectOrphanGuards({
|
|
12140
|
+
packageScripts: { test: 'node t.js', 'check:leaf': 'node l.js' },
|
|
12141
|
+
runners: [{ file: '.github/workflows/ci.yml', viaScript: null, viaScripts: [], unconditional: true,
|
|
12142
|
+
text: 'jobs:\n a:\n steps:\n - run: npm test\n' },
|
|
12143
|
+
{ file: 'scripts/x.js', viaScript: 'test', viaScripts: ['test'], text: 'sh("npm run check:leaf");\n' }],
|
|
12144
|
+
scriptFiles: [],
|
|
12145
|
+
});
|
|
12146
|
+
if (starLive.orphanScripts.length) bad.push('판정기_주석필터가_실제호출까지_지움');
|
|
12044
12147
|
// (자체 헌트) Windows CI 는 `node scripts\x\check.js` 로 적는다. 후보 경로는 `/` 표기라
|
|
12045
12148
|
// 전체 경로가 안 맞고 basename 경계 판정도 앞의 `\` 때문에 걸러져 **정상 배선이 고아로 보고**됐다.
|
|
12046
12149
|
for (const [label, ref] of [['posix', 'scripts/x/check.js'], ['win', 'scripts\\x\\check.js'], ['win-dot', '.\\scripts\\x\\check.js']]) {
|