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 +4 -4
- package/bin/leerness.js +35 -8
- package/package.json +1 -1
- package/scripts/e2e.js +45 -0
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.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.
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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.
|
|
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
|
{
|