leerness 1.36.121 → 1.36.122
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 +5 -2
- package/lib/audit.js +3 -1
- package/lib/pure-utils.js +12 -2
- package/package.json +1 -1
- package/scripts/e2e.js +67 -2
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.122 하네스를 사용합니다. 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.122는 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.121는 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.122 릴리스 태그 이력** (수백 라운드) · _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.122: 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.
|
|
37
|
+
const VERSION = '1.36.122';
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -1043,7 +1043,10 @@ function managedMerge(file, next, previous, archiveDir, mergeOpts = {}) {
|
|
|
1043
1043
|
// 다른 디렉토리에서 실행하면 "전체 원본 백업: <경로>" 가 존재하지 않는 곳을 가리킨다(검수 재현).
|
|
1044
1044
|
// 그 문장은 "삭제하지 않았다" 는 약속의 근거이므로 반드시 프로젝트 기준이어야 한다.
|
|
1045
1045
|
const _base = mergeOpts && mergeOpts.root ? mergeOpts.root : process.cwd();
|
|
1046
|
-
|
|
1046
|
+
// 1.36.122 (자기 저장소 도그푸딩): 백업을 만들지 않는 실행(`adapter` 등)은 여기서 일반 경로를 **채워** 넘겼고,
|
|
1047
|
+
// 그러면 이전 마이그레이션이 적어 둔 **구체 백업 경로가 덮여** 사라졌다(우리 repo 에서 실측).
|
|
1048
|
+
// 백업이 없으면 `null` 을 넘겨, 이전 문서의 포인터를 그대로 두게 한다(보존 블록의 정보 손실 방지).
|
|
1049
|
+
const archiveRel = archiveDir ? path.relative(_base, archiveDir).replace(/\\/g, '/') : null;
|
|
1047
1050
|
return _managedMerge(file, next, previous, archiveRel, MERGE_OVERWRITE_FILES, mergeOpts); // 1.36.60: altTemplate/lang 전달
|
|
1048
1051
|
}
|
|
1049
1052
|
|
package/lib/audit.js
CHANGED
|
@@ -95,7 +95,9 @@ function _collectGuardInputs(root) {
|
|
|
95
95
|
// CMD 스텝의 `REM`·`::` 도 주석이다(검수 재현: `REM debug: node .\bin\phantom.js` 한 줄로 제품 진입점이 러너가 됐다).
|
|
96
96
|
// ⚠ `--`·`;` 는 여기서 다루는 파일 종류(YAML·Makefile·셸·JS)에서 주석이 아니다 — 오히려 여러 줄 명령의
|
|
97
97
|
// 연속 인자(`--file=scripts/x.js`)를 지워 파일 참조를 잃는다. 주석 표기는 실제로 쓰이는 것만 둔다.
|
|
98
|
-
|
|
98
|
+
// ⚠ 판정기(`_dropCommentLines`)와 **같은 표기**여야 한다 — `*`(블록 주석 이어짐)를 한쪽에만 두면
|
|
99
|
+
// 두 단계의 입력이 갈린다(검수 P3: 정합성 주장이 사실이 아니었다).
|
|
100
|
+
const _noComment = (s) => String(s).split('\n').filter(l => !/^\s*(#|\/\/|\*|::|@?REM\s)/i.test(l)).join('\n');
|
|
99
101
|
// 설명 필드(`- name: archive index.js`)는 실행 증거가 아니다 — bare 진입점 매처에도 같은 규칙을 건다.
|
|
100
102
|
// 단 값이 **산문일 때만** 설명으로 본다(matrix 변수명이 `name` 일 수 있다 — 판정기와 같은 규칙).
|
|
101
103
|
const _noDescriptive = (s) => String(s).split('\n').filter(l => {
|
package/lib/pure-utils.js
CHANGED
|
@@ -993,7 +993,15 @@ function _managedMerge(file, next, previous, archiveRel, overwriteSet, opts = {}
|
|
|
993
993
|
custom.push(raw.replace(/\\`\\`\\`/g, '```')); // 구 형식에서 이스케이프된 펜스 복원
|
|
994
994
|
}
|
|
995
995
|
if (!custom.length) return next;
|
|
996
|
-
|
|
996
|
+
// 1.36.122 (자기 저장소 도그푸딩 실측): 새 백업을 만들지 않는 실행(`adapter` 등)이 `archiveRel` 없이 들어오면
|
|
997
|
+
// 이 안내가 **구체 백업 경로를 일반 경로로 덮어썼다** — 보존이 목적인 블록에서의 정보 손실이다.
|
|
998
|
+
// (우리 repo 에서 `.harness/archive/leerness-1.36.100-2026-08-05T…` → `.harness/archive` 로 격하되는 걸 확인했다.)
|
|
999
|
+
// 이전 문서가 이미 구체 경로를 가리키면 그대로 둔다.
|
|
1000
|
+
let ar = archiveRel;
|
|
1001
|
+
if (!ar) {
|
|
1002
|
+
const _prevAr = /(?:전체 원본 백업|Full original backup):\s*`([^`]+)`/.exec(String(previous || ''));
|
|
1003
|
+
ar = (_prevAr && _prevAr[1]) || '.harness/archive';
|
|
1004
|
+
}
|
|
997
1005
|
// 1.36.60 (검수): 보존 래퍼 문구도 프로젝트 언어를 따른다 — en 프로젝트에 한글 래퍼가 새 한글원을 만들던 것 해소
|
|
998
1006
|
let note = opts.lang === 'en'
|
|
999
1007
|
? `> Custom user/project content carried over from the previous version — re-carried automatically on every migration. Full original backup: \`${ar}\``
|
|
@@ -1287,7 +1295,9 @@ function _detectOrphanGuards(input) {
|
|
|
1287
1295
|
// ⚠ 기준 디렉터리가 다르다: **CI 설정의 명령은 프로젝트 루트에서 실행**된다(`run: node ./scripts/x.js`).
|
|
1288
1296
|
// 설정 파일 위치(`.github/workflows`) 기준으로 풀면 `.github/workflows/scripts/x.js` 가 돼 실제 호출을 놓친다(검수 재현).
|
|
1289
1297
|
// 코드 파일 안의 `require('./x')` 는 그 파일 기준이 맞다.
|
|
1290
|
-
|
|
1298
|
+
// 러너 자신의 경로도 정규화한다 — `node scripts/./a/run.js` 로 수집된 러너는 `r.file` 에 dot-segment 가
|
|
1299
|
+
// 남아 있어 해석 결과가 후보와 어긋났다(검수 재현: 실제로 도는 가드가 고아로 보고됨).
|
|
1300
|
+
const dir = _isConfigRunner(r.file) ? [] : _normPath(String(r.file || '')).split('/').slice(0, -1);
|
|
1291
1301
|
// 원문을 먼저 정규화한다 — 실제 JS 는 `.\\check.js`(backslash 2개)로 저장되고, 한 번만 치환하면
|
|
1292
1302
|
// `.//check.js` 가 남아 아래 정규식에 안 걸린다(검수 재현: 실제로 도는 가드가 고아로 보고됐다).
|
|
1293
1303
|
const body = _normPath(_dropCommentLines(String(r.text || '')));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.122",
|
|
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
|
@@ -5091,7 +5091,8 @@ total++;
|
|
|
5091
5091
|
// 1.36.120: 잡음을 **열거**하는 대신 stderr 를 통째로 0 으로 고정한다. audit 이 `--json` 에서 stdout 만 막고
|
|
5092
5092
|
// stderr 는 열어 둬서 사람용 경고가 샜고(309B), 그 문구가 **프로젝트 상태에 따라** 달라져
|
|
5093
5093
|
// 같은 코드로 게이트가 통과/실패로 갈렸다(실측). 열거는 유계가 아니다 — 원인 단계에서 막았으니 여기선 0을 요구한다.
|
|
5094
|
-
|
|
5094
|
+
// ⚠ `trim()===''` 은 개행만 있는 출력을 통과시킨다 — 문구가 "0 바이트" 면 검사도 0 바이트여야 한다(검수 P3).
|
|
5095
|
+
const noiseFree = se.length === 0;
|
|
5095
5096
|
ok = pureStdout && payloadOk && noiseFree && r.status === 0;
|
|
5096
5097
|
} catch {}
|
|
5097
5098
|
console.log(ok ? '✓ B(1.36.88) selftest --json 계약: 리포 루트 cwd stdout 순수 JSON + stderr 잡음 0' : '✗ selftest --json 계약 실패 (stdout 오염 또는 stderr 잡음)');
|
|
@@ -5121,7 +5122,8 @@ total++;
|
|
|
5121
5122
|
if (!out.trim()) { bad.push(`${c}:출력없음`); continue; }
|
|
5122
5123
|
let j = null; try { j = JSON.parse(out.trim()); } catch {}
|
|
5123
5124
|
if (!j) { bad.push(`${c}:stdout이JSON아님`); continue; }
|
|
5124
|
-
|
|
5125
|
+
// 문구가 "0 바이트" 면 검사도 0 바이트여야 한다 — `trim()===''` 은 개행만 있는 출력을 통과시킨다(검수 P3).
|
|
5126
|
+
if (err.length !== 0) bad.push(`${c}:stderr잡음(${err.length}B:${err.trim().split('\n')[0].slice(0, 40)})`);
|
|
5125
5127
|
}
|
|
5126
5128
|
// 판별력 — 이 검사가 실제로 무언가를 실행했는지(전부 건너뛰면 공허하게 통과한다)
|
|
5127
5129
|
if (bad.length === 0 && CMDS.length < 10) bad.push('대상이_너무적음');
|
|
@@ -5169,6 +5171,48 @@ total++;
|
|
|
5169
5171
|
if (!ok) failed++;
|
|
5170
5172
|
}
|
|
5171
5173
|
|
|
5174
|
+
// 1.36.122 (자기 저장소 도그푸딩): 보존 블록의 **백업 포인터**가 재실행에 덮여 사라졌다.
|
|
5175
|
+
// `adapter` 처럼 새 백업을 만들지 않는 실행이 일반 경로(`.harness/archive`)를 채워 넘겨서,
|
|
5176
|
+
// 이전 마이그레이션이 적어 둔 구체 경로(`.harness/archive/leerness-1.36.100-…`)를 덮었다.
|
|
5177
|
+
// 그 문장은 "삭제하지 않았다" 는 약속의 근거이므로 격하는 곧 정보 손실이다. 세 방향을 모두 잰다.
|
|
5178
|
+
total++;
|
|
5179
|
+
{
|
|
5180
|
+
let ok = false; const bad = [];
|
|
5181
|
+
try {
|
|
5182
|
+
const PUx = require(path.resolve(path.dirname(CLI), '..', 'lib', 'pure-utils'));
|
|
5183
|
+
const next = '# T\nmanaged\n';
|
|
5184
|
+
const prev = '# T\nmanaged\nCUSTOM-LINE\n';
|
|
5185
|
+
const ptr = (s) => { const m = /(?:전체 원본 백업|Full original backup):\s*`([^`]+)`/.exec(s); return m && m[1]; };
|
|
5186
|
+
// ① 이전 포인터가 없고 새 백업도 없으면 기본값
|
|
5187
|
+
if (ptr(PUx._managedMerge('CLAUDE.md', next, prev, null, null)) !== '.harness/archive') bad.push('기본값아님');
|
|
5188
|
+
// ② 이전 포인터가 **구체 경로**면 새 백업이 없을 때 그대로 둔다(격하 금지)
|
|
5189
|
+
const prev2 = prev + '\n> 전체 원본 백업: `.harness/archive/leerness-1.2.3-STAMP`\n';
|
|
5190
|
+
if (ptr(PUx._managedMerge('CLAUDE.md', next, prev2, null, null)) !== '.harness/archive/leerness-1.2.3-STAMP') bad.push('구체경로가_격하됨');
|
|
5191
|
+
// ③ 새 백업이 있으면 그쪽으로 갱신(보존이 과해 최신 백업을 못 가리키면 안 된다)
|
|
5192
|
+
if (ptr(PUx._managedMerge('CLAUDE.md', next, prev2, '.harness/archive/NEW', null)) !== '.harness/archive/NEW') bad.push('새백업으로_갱신안됨');
|
|
5193
|
+
// ④ 실제 CLI 경로 — 새 백업 없이 adapter 를 두 번 돌려도 포인터가 유지돼야 한다
|
|
5194
|
+
const sb = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-ptr122-'));
|
|
5195
|
+
try {
|
|
5196
|
+
const d = path.join(sb, 'p'); fs.mkdirSync(d, { recursive: true });
|
|
5197
|
+
fs.writeFileSync(path.join(d, 'package.json'), '{"name":"p","version":"0.1.0"}');
|
|
5198
|
+
const ENVp = Object.assign({}, process.env, { TMPDIR: sb, TEMP: sb, TMP: sb, LEERNESS_NO_PROMPT: '1' });
|
|
5199
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes'], { cwd: d, encoding: 'utf8', timeout: 300000, env: ENVp });
|
|
5200
|
+
const cf = path.join(d, 'CLAUDE.md');
|
|
5201
|
+
// 사용자 커스텀 + 구체 백업 포인터를 가진 보존 블록을 만든다
|
|
5202
|
+
fs.writeFileSync(cf, fs.readFileSync(cf, 'utf8')
|
|
5203
|
+
+ '\n<!-- leerness:migration-preserved -->\n## Preserved previous content\n\n> 이전 버전에서 이어진 사용자/프로젝트 커스텀 내용 — 마이그레이션마다 자동 이월됩니다. 전체 원본 백업: `.harness/archive/leerness-9.9.9-STAMP`\n\nUSER-KEEP-LINE\n');
|
|
5204
|
+
cp.spawnSync(process.execPath, [CLI, 'adapter', 'claude', '--path', d], { cwd: d, encoding: 'utf8', timeout: 300000, env: ENVp });
|
|
5205
|
+
const after2 = fs.readFileSync(cf, 'utf8');
|
|
5206
|
+
if (!/leerness-9\.9\.9-STAMP/.test(after2)) bad.push('CLI경로에서_포인터_격하');
|
|
5207
|
+
if (!after2.includes('USER-KEEP-LINE')) bad.push('CLI경로에서_사용자라인_유실');
|
|
5208
|
+
} finally { try { fs.rmSync(sb, { recursive: true, force: true }); } catch {} }
|
|
5209
|
+
ok = bad.length === 0;
|
|
5210
|
+
} catch (e) { bad.push('ERR:' + String(e && e.message).slice(0, 80)); }
|
|
5211
|
+
console.log(ok ? '✓ B(1.36.122) 보존 블록 백업 포인터: 새 백업 없으면 기존 구체 경로 유지 · 있으면 갱신 · 없으면 기본값'
|
|
5212
|
+
: '✗ 보존 블록 백업 포인터 실패 ' + JSON.stringify(bad));
|
|
5213
|
+
if (!ok) failed++;
|
|
5214
|
+
}
|
|
5215
|
+
|
|
5172
5216
|
// 1.9.369 회귀 (UR-0025): MINIMAL_SKIP_KEYS/_parseSkillsValue 분리 — init --minimal 비핵심 스킵+코어 유지, --skills recommended 설치
|
|
5173
5217
|
total++;
|
|
5174
5218
|
{
|
|
@@ -11976,6 +12020,27 @@ total++;
|
|
|
11976
12020
|
scriptFiles: [],
|
|
11977
12021
|
});
|
|
11978
12022
|
if (!cmt.orphanScripts.includes('check:leaf')) bad.push('판정기가_CMD주석을_실행으로');
|
|
12023
|
+
// (검수) 러너 **자신의 경로**에 dot-segment 가 있으면 해석 결과가 후보와 어긋났다 — 실제로 도는 가드가 고아로.
|
|
12024
|
+
const ddot = path.join(sb4, 'dotnest');
|
|
12025
|
+
fs.mkdirSync(path.join(ddot, 'scripts', 'a'), { recursive: true });
|
|
12026
|
+
fs.mkdirSync(path.join(ddot, '.github', 'workflows'), { recursive: true });
|
|
12027
|
+
fs.writeFileSync(path.join(ddot, 'package.json'), JSON.stringify({ name: 'd', version: '0.1.0', scripts: { test: 'node scripts/./a/run.js' } }));
|
|
12028
|
+
fs.writeFileSync(path.join(ddot, '.github', 'workflows', 'ci.yml'), 'jobs:\n a:\n steps:\n - run: npm test\n');
|
|
12029
|
+
fs.writeFileSync(path.join(ddot, 'scripts', 'a', 'run.js'), 'require("./check.js");\n');
|
|
12030
|
+
fs.writeFileSync(path.join(ddot, 'scripts', 'a', 'check.js'), '// guard\n');
|
|
12031
|
+
const dotRes = PU._detectOrphanGuards(AUD._collectGuardInputs(ddot));
|
|
12032
|
+
if (dotRes.orphanFiles.includes('scripts/a/check.js')) bad.push('러너경로_dot세그먼트_오탐');
|
|
12033
|
+
// (검수 P3) 수집기와 판정기의 **주석 표기가 같아야** 한다 — `*`(블록 주석 이어짐)를 한쪽에만 두면 입력이 갈린다.
|
|
12034
|
+
const dstar = path.join(sb4, 'star');
|
|
12035
|
+
fs.mkdirSync(path.join(dstar, 'scripts'), { recursive: true });
|
|
12036
|
+
fs.mkdirSync(path.join(dstar, '.github', 'workflows'), { recursive: true });
|
|
12037
|
+
fs.writeFileSync(path.join(dstar, 'package.json'), JSON.stringify({ name: 's', version: '0.1.0', scripts: { test: 'node t.js', 'check:leaf': 'node l.js' } }));
|
|
12038
|
+
fs.writeFileSync(path.join(dstar, '.github', 'workflows', 'ci.yml'),
|
|
12039
|
+
'jobs:\n a:\n steps:\n - run: |\n /*\n * node scripts/ghost.js\n */\n npm test\n');
|
|
12040
|
+
fs.writeFileSync(path.join(dstar, 'scripts', 'ghost.js'), 'sh("npm run check:leaf")\n');
|
|
12041
|
+
const starInp = AUD._collectGuardInputs(dstar);
|
|
12042
|
+
if (starInp.runners.some(r => /ghost\.js$/.test(r.file))) bad.push('블록주석에서_러너_수집');
|
|
12043
|
+
if (!PU._detectOrphanGuards(starInp).orphanScripts.includes('check:leaf')) bad.push('블록주석이_고아를_덮음');
|
|
11979
12044
|
// (자체 헌트) Windows CI 는 `node scripts\x\check.js` 로 적는다. 후보 경로는 `/` 표기라
|
|
11980
12045
|
// 전체 경로가 안 맞고 basename 경계 판정도 앞의 `\` 때문에 걸러져 **정상 배선이 고아로 보고**됐다.
|
|
11981
12046
|
for (const [label, ref] of [['posix', 'scripts/x/check.js'], ['win', 'scripts\\x\\check.js'], ['win-dot', '.\\scripts\\x\\check.js']]) {
|