leerness 1.36.105 → 1.36.106
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 +33 -4
- package/lib/diagnostics.js +20 -1
- package/package.json +1 -1
- package/scripts/e2e.js +51 -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.106 하네스를 사용합니다. 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.106는 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.105는 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.106 릴리스 태그 이력** (수백 라운드) · _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.106: 2026-08-07
|
|
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.106';
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -3307,13 +3307,36 @@ function pathSetupCmd(root, opts = {}) {
|
|
|
3307
3307
|
// 실측 비용: init 1330ms · 초기화된 디렉토리 복사 78ms(17배 저렴) · CLI 1회 기동 434ms.
|
|
3308
3308
|
// → 언어별로 **한 번만** init 하고 이후는 복사해서 쓴다. 각 호출자는 자기 사본을 받으므로 상호 오염이 없다.
|
|
3309
3309
|
const _stFixtureCache = new Map();
|
|
3310
|
+
// 1.36.106 (T-0090): 캐시된 base 는 호출자가 지울 수 없어 **프로세스마다 언어 수만큼 남았다**(실측 누적 3,190개).
|
|
3311
|
+
// selftest/doctor 는 CI 에서 반복 실행되는 명령이라 그대로 두면 사용자 tmpdir 이 무한히 자란다.
|
|
3312
|
+
// 프로세스 종료 시 한 번에 치운다 — 캐시의 수명이 곧 프로세스 수명이므로 exit 훅이 정확한 자리다.
|
|
3313
|
+
let _stCleanupHooked = false;
|
|
3314
|
+
function _stSweepFixtures() {
|
|
3315
|
+
for (const b of _stFixtureCache.values()) {
|
|
3316
|
+
if (!b) continue;
|
|
3317
|
+
try { fs.rmSync(b, { recursive: true, force: true }); } catch { /* 정리 실패는 판정과 무관 */ }
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
function _stHookCleanup() {
|
|
3321
|
+
if (_stCleanupHooked) return;
|
|
3322
|
+
_stCleanupHooked = true;
|
|
3323
|
+
process.on('exit', _stSweepFixtures);
|
|
3324
|
+
// codex 검수 MEDIUM#1: Ctrl+C 는 'exit' 리스너를 거치지 않는다(SIGINT 리스너가 없으면 기본 종료가 즉시 죽인다).
|
|
3325
|
+
// CI 에서 중단되는 일이 드물지 않으므로 SIGINT 도 잡아 치우고 표준 종료코드로 나간다.
|
|
3326
|
+
// 이 훅은 selftest/doctor 경로에서만 등록된다(대화형 prompt 가 있는 명령에는 걸리지 않는다).
|
|
3327
|
+
process.on('SIGINT', () => { _stSweepFixtures(); process.exit(130); });
|
|
3328
|
+
}
|
|
3310
3329
|
function _selftestFixture(lang) {
|
|
3311
3330
|
const key = lang || 'ko';
|
|
3312
3331
|
if (!_stFixtureCache.has(key)) {
|
|
3332
|
+
_stHookCleanup();
|
|
3313
3333
|
const base = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_stfx_' + key + '_'));
|
|
3314
3334
|
const args = [__filename, 'init', base, '--yes', '--no-env', '--no-stale-check'];
|
|
3315
3335
|
if (key !== 'ko') args.push('--language', key);
|
|
3316
3336
|
const r = cp.spawnSync(process.execPath, args, { encoding: 'utf8', timeout: 120000, maxBuffer: 16 * 1024 * 1024 });
|
|
3337
|
+
// codex 검수 MEDIUM#2: init 실패 시 캐시에 null 을 넣는데, 그러면 **이미 만든 base 가 훅의 시야에서 사라진다**.
|
|
3338
|
+
// 실패 경로야말로 정리가 필요한 자리다 — 그 자리에서 바로 지운다.
|
|
3339
|
+
if (r.status !== 0) { try { fs.rmSync(base, { recursive: true, force: true }); } catch {} }
|
|
3317
3340
|
_stFixtureCache.set(key, r.status === 0 ? base : null);
|
|
3318
3341
|
}
|
|
3319
3342
|
const base = _stFixtureCache.get(key);
|
|
@@ -4266,13 +4289,19 @@ function _selfTestCases() {
|
|
|
4266
4289
|
const encJson = src.includes('function encodingCheck(root, opts = {})');
|
|
4267
4290
|
const contractExit = src.includes('if (!okJson) process.exitCode = 1;');
|
|
4268
4291
|
let behav = false;
|
|
4269
|
-
|
|
4292
|
+
// 1.36.106 (T-0090): 대조군 디렉토리를 익명으로 만들어 **정리할 수 없었다** — 실측 tmpdir 누적 4,574개.
|
|
4293
|
+
// selftest 는 CI 친화 명령으로 광고되므로 커밋마다 쌓인다. 이름을 붙여 finally 에서 함께 지운다.
|
|
4294
|
+
// codex 검수 LOW#3: 둘을 try 밖에서 만들면 **두 번째 생성이 실패할 때 첫 번째가 남는다**(ENOSPC 등).
|
|
4295
|
+
// 생성을 try 안으로 넣어 어느 쪽이 실패해도 finally 가 만들어진 것만 정확히 치우게 한다.
|
|
4296
|
+
let tmp = null, cleanDir = null;
|
|
4270
4297
|
try {
|
|
4298
|
+
tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_sec_'));
|
|
4299
|
+
cleanDir = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_clean_'));
|
|
4271
4300
|
fs.writeFileSync(path.join(tmp, 'c.js'), 'module.exports={apiKey:"sk-test-1234567890abcdefghijklmnopqrstuvwxyz"};');
|
|
4272
4301
|
const r = _collectSecretFindings(tmp);
|
|
4273
|
-
const clean = _collectSecretFindings(
|
|
4302
|
+
const clean = _collectSecretFindings(cleanDir);
|
|
4274
4303
|
behav = r.committed.length >= 1 && clean.committed.length === 0;
|
|
4275
|
-
} catch {} finally { try { fs.rmSync(
|
|
4304
|
+
} catch {} finally { for (const _d of [tmp, cleanDir]) { if (_d) { try { fs.rmSync(_d, { recursive: true, force: true }); } catch {} } } }
|
|
4276
4305
|
return handoffWired && scanJson && encJson && contractExit && behav;
|
|
4277
4306
|
} },
|
|
4278
4307
|
{ name: '9라운드 (UR-0119/0120): team review(메인 검수) — _composeTeamPlan reviewStep + handoff 검수필요 + team add 와이어 (1.9.414)', run: () => { const m = require('../lib/pure-utils'); const on = m._composeTeamPlan({ id: 't', members: ['a', 'b'], personas: ['security'] }, '점검'); const off = m._composeTeamPlan({ id: 't', members: ['a'], review: false }, '점검'); const planOk = on.review === true && !!on.reviewStep && on.reviewStep.suggestedCommand.includes('verify-claim') && off.review === false && !off.reviewStep; const rem = m._teamHandoffReminders([{ id: 'r', schedule: 'every-session', status: 'active', members: ['a'], review: true }]); const remOk = rem.length === 1 && rem[0].includes('검수필요'); const teamSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'team.js')); const wired = teamSrc.includes("review: !has('--no-review')") && teamSrc.includes('메인 검수 (필수)'); return planOk && remOk && wired; } },
|
package/lib/diagnostics.js
CHANGED
|
@@ -27,11 +27,24 @@ function doctorCmd(opts = {}, deps = {}) {
|
|
|
27
27
|
let shell = null, psVersion = null;
|
|
28
28
|
try { const ctx = _detectShellCtx(); shell = ctx.shell; psVersion = ctx.psVersion; } catch {}
|
|
29
29
|
const mcpCount = (() => { try { return _mcpToolCount(); } catch { return null; } })();
|
|
30
|
+
// 1.36.106 (T-0090): selftest/doctor 가 임시 디렉토리를 흘려 왔다(실행당 8/3개). 새는 자리는 고쳤지만
|
|
31
|
+
// **이미 쌓인 것**은 사용자 디스크다 — 조용히 지우지 않고 사실만 알린다(삭제는 사용자 판단).
|
|
32
|
+
// 비용은 readdir 1회이고 stat 은 하지 않는다. 실패는 진단 실패가 아니므로 통째로 삼킨다.
|
|
33
|
+
// codex 검수 LOW#4/#5: 처음엔 readdir 이름만 봤다 — 그래서 (a) `leerness-note.txt` 같은 **파일**을
|
|
34
|
+
// "임시 디렉토리" 로 세고, (b) Windows(대소문자 무시 파일시스템)에서 `__LEERNESS_*` 를 놓쳤다.
|
|
35
|
+
// withFileTypes 로 디렉토리만 세고, 접두 비교는 대소문자를 무시한다.
|
|
36
|
+
let tmpLeftover = null;
|
|
37
|
+
try {
|
|
38
|
+
const tdir = require('os').tmpdir();
|
|
39
|
+
const ents = require('fs').readdirSync(tdir, { withFileTypes: true });
|
|
40
|
+
const n = ents.filter(e => e.isDirectory() && /^(__leerness_|leerness-)/i.test(e.name)).length;
|
|
41
|
+
if (n > 200) tmpLeftover = { dir: tdir, count: n };
|
|
42
|
+
} catch { tmpLeftover = null; }
|
|
30
43
|
const report = {
|
|
31
44
|
version: VERSION, node: process.version, platform: process.platform + '/' + process.arch,
|
|
32
45
|
runningFrom: harnessPath, mcpTools: mcpCount,
|
|
33
46
|
selftest: { pass, total, ok: failNames.length === 0, failed: failNames },
|
|
34
|
-
shell, psVersion, healthy: failNames.length === 0
|
|
47
|
+
shell, psVersion, tmpLeftover, healthy: failNames.length === 0
|
|
35
48
|
};
|
|
36
49
|
if (json) { process.stdout.write(JSON.stringify(report, null, 2) + '\n'); if (!report.healthy) process.exitCode = 1; return report; }
|
|
37
50
|
const isTty = process.stdout && process.stdout.isTTY;
|
|
@@ -45,6 +58,12 @@ function doctorCmd(opts = {}, deps = {}) {
|
|
|
45
58
|
log(` ${report.selftest.ok ? gr('✓') : rd('✗')} selftest: ${pass}/${total} ${t('통과', 'passed')}${report.selftest.ok ? '' : t(` (${total - pass}건 실패)`, ` (${total - pass} failed)`)}`);
|
|
46
59
|
if (!report.selftest.ok) report.selftest.failed.slice(0, 5).forEach(n => log(rd(` ✗ ${n}`)));
|
|
47
60
|
log(` ${gr('✓')} ${t('셸', 'shell')}: ${shell || 'unknown'}${psVersion && shell === 'powershell' ? ` (PowerShell ${psVersion})` : ''}`);
|
|
61
|
+
// 남은 임시 디렉토리는 '문제 감지'(healthy)로 올리지 않는다 — 동작에 지장이 없고, 과거 버전이 남긴 흔적이다.
|
|
62
|
+
// 지우는 명령을 대신 실행하지도 않는다: 경로를 알려주고 사용자가 판단한다.
|
|
63
|
+
if (tmpLeftover) {
|
|
64
|
+
log(` ${dm('ⓘ')} ${t(`임시 디렉토리 잔존 ${tmpLeftover.count}개 (1.36.106 이전 버전이 남긴 것 — 새 실행은 흘리지 않습니다)`, `${tmpLeftover.count} leftover temp dirs (created by versions before 1.36.106 — current runs leak none)`)}`);
|
|
65
|
+
log(` ${dm(tmpLeftover.dir)} ${dm(t('— 필요하면 직접 정리하세요(leerness 는 지우지 않습니다)', '— remove them yourself if you want (leerness will not)'))}`);
|
|
66
|
+
}
|
|
48
67
|
log('');
|
|
49
68
|
log(report.healthy ? gr(t(' ✓ leerness 설치 정상', ' ✓ leerness install OK')) : rd(t(' ✗ 문제 감지 — 재설치: npm i -g leerness@latest · 진단: leerness which', ' ✗ problem detected — reinstall: npm i -g leerness@latest · diagnose: leerness which')));
|
|
50
69
|
if (!report.healthy) process.exitCode = 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.106",
|
|
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
|
@@ -10035,6 +10035,57 @@ total++;
|
|
|
10035
10035
|
if (!ok) failed++;
|
|
10036
10036
|
}
|
|
10037
10037
|
|
|
10038
|
+
// ── 1.36.106 (T-0090): selftest/doctor 가 임시 디렉토리를 흘렸다 — 실행당 8개/3개, 실측 누적 83,135개.
|
|
10039
|
+
// selftest 는 "CI 친화" 로 광고되는 명령이라 커밋마다 쌓인다. 정적으로는 안 보이고 **실행 전후 증가분**으로만 잡힌다.
|
|
10040
|
+
// 대조군: 사용자 명령(handoff/audit)은 원래 0이어야 하고, 여기서도 0이어야 한다(회귀 감시).
|
|
10041
|
+
{
|
|
10042
|
+
total++;
|
|
10043
|
+
let ok = false; const dbg = {};
|
|
10044
|
+
const proj = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-tl106-'));
|
|
10045
|
+
// 자식의 TMPDIR 를 격리한다 — 이 블록은 임시 디렉토리를 세므로 전체 스위트의 다른 블록이 만드는
|
|
10046
|
+
// 임시 디렉토리에 오염되면 판정이 흔들린다(그리고 파괴적 변이 시험이 실제 tmpdir 을 지우는 것도 막는다).
|
|
10047
|
+
const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-tlbox-'));
|
|
10048
|
+
try {
|
|
10049
|
+
fs.writeFileSync(path.join(proj, 'package.json'), '{"name":"p","version":"0.1.0"}');
|
|
10050
|
+
const ENV = Object.assign({}, process.env, { TMPDIR: sandbox, TEMP: sandbox, TMP: sandbox });
|
|
10051
|
+
const R = (a) => cp.spawnSync(process.execPath, [CLI, ...a], { cwd: proj, encoding: 'utf8', timeout: 300000, env: ENV });
|
|
10052
|
+
R(['init', proj, '--yes']);
|
|
10053
|
+
const count = () => { try { return fs.readdirSync(sandbox).filter(x => /^(__leerness_|leerness-)/.test(x)).length; } catch { return -1; } };
|
|
10054
|
+
const delta = (a) => { const b = count(); R(a); return count() - b; };
|
|
10055
|
+
dbg.selftest = delta(['selftest']);
|
|
10056
|
+
dbg.doctor = delta(['doctor']);
|
|
10057
|
+
dbg.handoff = delta(['handoff', '.']); // 대조군 — 원래 0
|
|
10058
|
+
dbg.audit = delta(['audit']); // 대조군 — 원래 0
|
|
10059
|
+
// 반복해도 누적되지 않는가 (한 번만 0 이고 두 번째부터 새면 의미 없다)
|
|
10060
|
+
dbg.selftest2 = delta(['selftest']);
|
|
10061
|
+
// 잔존 보고는 **잔존이 있을 때만** 판별력이 있다 — 주변 상태에 기대지 말고 조건을 직접 만들고 직접 치운다.
|
|
10062
|
+
for (let i = 0; i < 250; i++) { try { fs.mkdirSync(path.join(sandbox, '__leerness_probe_' + i)); } catch {} }
|
|
10063
|
+
let dj = null; try { dj = JSON.parse(((R(['doctor', '--json']).stdout) || '').trim()); } catch {}
|
|
10064
|
+
const beforeReport = count();
|
|
10065
|
+
R(['doctor']);
|
|
10066
|
+
dbg.doctorReadOnly = count() - beforeReport === 0; // doctor 는 지우지 않는다(사용자 디스크)
|
|
10067
|
+
dbg.reportsLeftover = !!dj && !!dj.tmpLeftover && dj.tmpLeftover.count >= 250;
|
|
10068
|
+
dbg.healthyUnaffected = !!dj && dj.healthy === true; // 잔존은 '문제 감지' 가 아니다
|
|
10069
|
+
// codex 검수 LOW#4: **파일**을 "임시 디렉토리" 로 세면 안 된다 (leerness-note.txt 210개는 잔존 0)
|
|
10070
|
+
const fbox = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-fbox-'));
|
|
10071
|
+
for (let i = 0; i < 210; i++) fs.writeFileSync(path.join(fbox, 'leerness-user-note-' + i + '.txt'), 'x');
|
|
10072
|
+
let fj = null; try { fj = JSON.parse(((cp.spawnSync(process.execPath, [CLI, 'doctor', '--json'], { cwd: proj, encoding: 'utf8', timeout: 300000, env: Object.assign({}, process.env, { TMPDIR: fbox, TEMP: fbox, TMP: fbox }) }).stdout) || '').trim()); } catch {}
|
|
10073
|
+
dbg.filesNotCounted = !!fj && fj.tmpLeftover === null;
|
|
10074
|
+
// codex 검수 LOW#5: Windows 는 대소문자를 구분하지 않는다 — 대문자 접두도 잡아야 한다
|
|
10075
|
+
const cbox = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-cbox-'));
|
|
10076
|
+
for (let i = 0; i < 210; i++) fs.mkdirSync(path.join(cbox, '__LEERNESS_probe_' + i));
|
|
10077
|
+
let cj = null; try { cj = JSON.parse(((cp.spawnSync(process.execPath, [CLI, 'doctor', '--json'], { cwd: proj, encoding: 'utf8', timeout: 300000, env: Object.assign({}, process.env, { TMPDIR: cbox, TEMP: cbox, TMP: cbox }) }).stdout) || '').trim()); } catch {}
|
|
10078
|
+
dbg.caseInsensitive = !!cj && !!cj.tmpLeftover && cj.tmpLeftover.count >= 210;
|
|
10079
|
+
for (const _b of [fbox, cbox]) { try { fs.rmSync(_b, { recursive: true, force: true }); } catch {} }
|
|
10080
|
+
ok = dbg.selftest === 0 && dbg.doctor === 0 && dbg.selftest2 === 0
|
|
10081
|
+
&& dbg.handoff === 0 && dbg.audit === 0 && dbg.doctorReadOnly && dbg.reportsLeftover && dbg.healthyUnaffected
|
|
10082
|
+
&& dbg.filesNotCounted && dbg.caseInsensitive;
|
|
10083
|
+
} catch (e) { dbg.err = String(e && e.message).slice(0, 160); } finally { for (const _p of [proj, sandbox]) { try { fs.rmSync(_p, { recursive: true, force: true }); } catch {} } }
|
|
10084
|
+
console.log(ok ? '✓ I(1.36.106/T-0090) selftest·doctor 임시 디렉토리 누수 0 (반복 포함) + 사용자 명령 대조군 0 + 잔존은 보고만'
|
|
10085
|
+
: '✗ T-0090 임시 디렉토리 누수 ' + JSON.stringify(dbg));
|
|
10086
|
+
if (!ok) failed++;
|
|
10087
|
+
}
|
|
10088
|
+
|
|
10038
10089
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
10039
10090
|
if (failed > 0) process.exit(1);
|
|
10040
10091
|
|