leerness 1.36.37 → 1.36.38
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/CHANGELOG.md +8 -0
- package/README.md +4 -4
- package/bin/leerness.js +60 -13
- package/package.json +1 -1
- package/scripts/e2e.js +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.38 — 2026-07-16 — 분석 정직화 — retro 기간필터·신호 정규식·태그 계보·usage 귀속 (codex 4차 #2/#4/#9/#10)
|
|
4
|
+
|
|
5
|
+
- **#2 `retro --days`**: cutoff 를 계산만 하고 미적용 — 주간 회고가 전기간 집계였다. → 날짜 헤딩 섹션 필터(`_filterDatedSections`)를 task-log/evidence/decisions-신호에 적용(결정 블록 수는 "누적" 라벨대로 누적 유지). 실측: days=1 에서 2020 데이터 제외.
|
|
6
|
+
- **#10 신호 정규식**: JS `\b` 가 한글 경계를 못 만들어 한글 fix 신호 전멸(FN) + `pass`가 password/bypass 안에서, `done` 이 undone 안에서 매치(FP). → 한글 리터럴 카운트 + ASCII lookaround 토큰 경계. 실측: "수정 롤백 재발 password bypass undone" → fix 3 · pass 0.
|
|
7
|
+
- **#4 태그 계보**: round-history/milestones 가 `v1.9.*` 하드코드 — 현 계보(v1.36.x) 이력 0 으로 집계하면서 **"모든 마일스톤 달성 (500+)" 허위 축하**까지 출력. → `v*` 전체 semver 수집 + 이력 0 이면 "태그 이력 없음" 정직 표기. 실측: v1.36.36/37 태그 → roundCount 2.
|
|
8
|
+
- **#9 usage 귀속**: `memory status <B>` 처럼 subcommand 대상이 args[2] 인 명령의 사용량이 cwd(A)에 오귀속. → args[2] 대상 해석 추가. 실측: A 0 · B 귀속.
|
|
9
|
+
- **검증**: selftest 309/309(필터/신호/태그/귀속/배선 행위검사), 4건 원 재현 before/after, exit 전파형 게이트 e2e, 클린룸. 이연(codex 4차 잔여): #3 MCP 파라미터 검증 · #5 decisions 이중 소스 · #6 llm-bench 검증 · #7 benchmark 빈 디렉토리 점수 · #8 pulse/health 불일치.
|
|
10
|
+
|
|
3
11
|
## 1.36.37 — 2026-07-16 — managed 파일 --force 병합 — 마이그레이션 보존의 마지막 구멍 (1.9.441 드리프트 경로 실측)
|
|
4
12
|
|
|
5
13
|
도그푸딩에서 발견된 실제 드리프트 세대(1.9.441 — auto-macro 가 쓰는 버전)로 마이그레이션 경로를 전수 재검(11종 마커, 기본→반복→--force): 기본/반복은 유실 0 이었으나 **--force 반복에서 CLAUDE.md 사용자 커스텀 1종 유실** — `writeIfSafe` 에서 force 가 managed 병합(1.36.28 라인-diff 재설계)을 우회해 raw 덮어쓰기했기 때문(1.36.33 `_USER_STATE` 는 .harness 상태 파일만 커버).
|
package/README.md
CHANGED
|
@@ -121,7 +121,7 @@ MIT
|
|
|
121
121
|
<!-- leerness:project-readme:start -->
|
|
122
122
|
## Leerness Project Harness
|
|
123
123
|
|
|
124
|
-
이 프로젝트는 Leerness v1.36.
|
|
124
|
+
이 프로젝트는 Leerness v1.36.38 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
125
125
|
|
|
126
126
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
127
127
|
|
|
@@ -175,7 +175,7 @@ leerness memory restore decision <date|title>
|
|
|
175
175
|
|
|
176
176
|
### MCP server (외부 AI 통합)
|
|
177
177
|
|
|
178
|
-
Leerness v1.36.
|
|
178
|
+
Leerness v1.36.38는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
|
|
179
179
|
|
|
180
180
|
```jsonc
|
|
181
181
|
// 카테고리별
|
|
@@ -196,7 +196,7 @@ Leerness v1.36.37는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
196
196
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
197
197
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) stress-v* 신규 작성 + 누적 회귀 → 4) e2e 219/219 → 5) npm pack + git tag + GitHub release → 6) main 자동 push → 7) session close → 8) 다음 라운드 예약.
|
|
198
198
|
|
|
199
|
-
현재 누적: **70 라운드 (1.9.40 → 1.36.
|
|
199
|
+
현재 누적: **70 라운드 (1.9.40 → 1.36.38)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
200
200
|
|
|
201
201
|
### 성능 가이드
|
|
202
202
|
|
|
@@ -234,6 +234,6 @@ leerness release pack --close --auto-main-push
|
|
|
234
234
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
235
235
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
236
236
|
|
|
237
|
-
Last synced by Leerness v1.36.
|
|
237
|
+
Last synced by Leerness v1.36.38: 2026-07-16
|
|
238
238
|
<!-- leerness:project-readme:end -->
|
|
239
239
|
|
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.38';
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -2134,7 +2134,7 @@ function _computeRoundHistory(root) {
|
|
|
2134
2134
|
};
|
|
2135
2135
|
try {
|
|
2136
2136
|
// git tag --list 'v1.9.*' --sort=creatordate (시간순)
|
|
2137
|
-
const r = cp.spawnSync('git', ['tag', '--list', '
|
|
2137
|
+
const r = cp.spawnSync('git', ['tag', '--list', 'v*', '--sort=creatordate', '--format=%(refname:short)|%(creatordate:iso8601)'], {
|
|
2138
2138
|
cwd: root, encoding: 'utf8', timeout: 5000
|
|
2139
2139
|
});
|
|
2140
2140
|
if (r.status !== 0 || !r.stdout) return result;
|
|
@@ -2179,7 +2179,7 @@ function _computeMilestones(root) {
|
|
|
2179
2179
|
baselineAt: null
|
|
2180
2180
|
};
|
|
2181
2181
|
try {
|
|
2182
|
-
const r = cp.spawnSync('git', ['tag', '--list', '
|
|
2182
|
+
const r = cp.spawnSync('git', ['tag', '--list', 'v*', '--sort=creatordate', '--format=%(refname:short)|%(creatordate:iso8601)'], {
|
|
2183
2183
|
cwd: root, encoding: 'utf8', timeout: 5000
|
|
2184
2184
|
});
|
|
2185
2185
|
if (r.status !== 0 || !r.stdout) return result;
|
|
@@ -2254,7 +2254,7 @@ function milestonesCmd(root) {
|
|
|
2254
2254
|
log(` • ETA: ${data.next.etaDate} (~${data.next.etaDays}일 후, 현재 속도 기준)`);
|
|
2255
2255
|
}
|
|
2256
2256
|
} else {
|
|
2257
|
-
log(gr(` 🎉 모든 마일스톤 달성 (500+)`));
|
|
2257
|
+
log(data && (data.roundCount || data.totalRounds) ? gr(` 🎉 모든 마일스톤 달성 (500+)`) : dm(` (이 저장소 계보의 릴리스 태그 이력 없음)`)); // 1.36.38 (#4): 이력 0 인데 달성 축하 금지
|
|
2258
2258
|
}
|
|
2259
2259
|
}
|
|
2260
2260
|
|
|
@@ -3235,6 +3235,28 @@ function _selfTestCases() {
|
|
|
3235
3235
|
} catch {} finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
3236
3236
|
return salvage && preserve;
|
|
3237
3237
|
} },
|
|
3238
|
+
{ name: '분석 정직화 3종 (1.36.38, codex 4차 #2/#4/#9/#10): retro 기간필터+신호 정규식 / 태그 계보 v* / usage 귀속 — 행위검사', run: () => {
|
|
3239
|
+
// #2: _filterDatedSections — cutoff 이후만
|
|
3240
|
+
const t = '# L\n\n## 2020-01-01\n- ancient fix\n\n## 2099-01-01\n- future 수정\n';
|
|
3241
|
+
const f1 = _filterDatedSections(t, '2026-01-01');
|
|
3242
|
+
const filterOk = !f1.includes('ancient') && f1.includes('future') && _filterDatedSections(t, null) === t;
|
|
3243
|
+
// #10: 한글 리터럴 + ASCII 토큰 경계 (password/bypass/undone FP 차단)
|
|
3244
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_rt_'));
|
|
3245
|
+
let sigOk = false;
|
|
3246
|
+
try {
|
|
3247
|
+
fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true });
|
|
3248
|
+
fs.writeFileSync(path.join(tmp, '.harness', 'task-log.md'), '# T\n\n## 2026-07-16\n- 수정 롤백 재발 password bypass undone\n');
|
|
3249
|
+
const agg = _retroAggregate(tmp);
|
|
3250
|
+
sigOk = agg.fixSignals === 3 && agg.passSignals === 0;
|
|
3251
|
+
} catch {} finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
|
|
3252
|
+
// #4/#9 배선 소스가드
|
|
3253
|
+
const s = read(__filename);
|
|
3254
|
+
const tagOk = s.includes("'--list', 'v*'") && !s.includes("'--list', 'v1.9" + ".*'");
|
|
3255
|
+
const celebOk = (s.match(/이 저장소 계보의 릴리스 태그 이력 없음/g) || []).length >= 2;
|
|
3256
|
+
const usageOk = s.includes("else if (args[2] && !args[2].startsWith('-') && exists(path.join(absRoot(args[2]), '.harness')))");
|
|
3257
|
+
const cutoffWired = s.includes('_retroAggregate(root, cutoff)');
|
|
3258
|
+
return filterOk && sigOk && tagOk && celebOk && usageOk && cutoffWired;
|
|
3259
|
+
} },
|
|
3238
3260
|
{ name: 'debug 렌즈 (1.36.27, obra/superpowers systematic-debugging): 자기질문 6문항 ko/en 락스텝 + affects 유효 + route bugfix 힌트 + 파일매핑 미확장 — 행위검사', run: () => {
|
|
3239
3261
|
const d = LENS_CATALOG.debug;
|
|
3240
3262
|
if (!d) return false;
|
|
@@ -5817,7 +5839,7 @@ function roundHistoryCmd(root) {
|
|
|
5817
5839
|
if (data.nextMilestone != null) {
|
|
5818
5840
|
log(yl(` 🎯 다음 마일스톤: R${data.nextMilestone} (${data.roundsToNextMilestone} 라운드 남음)`));
|
|
5819
5841
|
} else {
|
|
5820
|
-
log(gr(` 🎉 모든 마일스톤 달성 (500+)`));
|
|
5842
|
+
log(data && (data.roundCount || data.totalRounds) ? gr(` 🎉 모든 마일스톤 달성 (500+)`) : dm(` (이 저장소 계보의 릴리스 태그 이력 없음)`)); // 1.36.38 (#4): 이력 0 인데 달성 축하 금지
|
|
5821
5843
|
}
|
|
5822
5844
|
log('');
|
|
5823
5845
|
log(` 최근 10 tags:`);
|
|
@@ -14056,12 +14078,26 @@ function writeSessionCounter(root, c) { writeUtf8(sessionCounterPath(root), JSON
|
|
|
14056
14078
|
|
|
14057
14079
|
// 1.9.324 (UR-0025): _countDatedBlocks / _extractDecisionBlocks → lib/pure-utils.js 로 이동 (순수 메모리 MD 파서, require 사용).
|
|
14058
14080
|
|
|
14059
|
-
|
|
14081
|
+
// 1.36.38 (codex 4차 #2): 날짜 헤딩(## YYYY-MM-DD / ### YYYY-MM-DD …) 섹션 문서를 cutoff 이후만 남김.
|
|
14082
|
+
// 날짜 없는 프리앰블은 유지(헤더/템플릿 — 신호 카운트에 거의 무기여). cutoff 없으면 원문 그대로(무회귀).
|
|
14083
|
+
function _filterDatedSections(text, cutoff) {
|
|
14084
|
+
if (!cutoff || !text) return text || '';
|
|
14085
|
+
const parts = String(text).split(/(?=^#{2,3} \d{4}-\d{2}-\d{2})/m);
|
|
14086
|
+
return parts.filter(p => {
|
|
14087
|
+
const m = p.match(/^#{2,3} (\d{4}-\d{2}-\d{2})/);
|
|
14088
|
+
return !m || m[1] >= cutoff;
|
|
14089
|
+
}).join('');
|
|
14090
|
+
}
|
|
14091
|
+
|
|
14092
|
+
function _retroAggregate(root, cutoff) {
|
|
14060
14093
|
root = absRoot(root);
|
|
14061
14094
|
const rows = readProgressRows(root);
|
|
14062
14095
|
const decisions = exists(decisionsPath(root)) ? read(decisionsPath(root)) : '';
|
|
14063
|
-
|
|
14064
|
-
|
|
14096
|
+
// 1.36.38 (#2): --days 의 cutoff 가 계산만 되고 미적용 → 주간 회고가 전기간 집계였다. 기간 신호(tlog/evidence)에 적용.
|
|
14097
|
+
// decisions 블록 수는 "누적" 라벨 그대로 누적 유지, 신호 카운트용 텍스트만 기간 필터.
|
|
14098
|
+
const tlog = _filterDatedSections(exists(taskLogPath(root)) ? read(taskLogPath(root)) : '', cutoff);
|
|
14099
|
+
const evidence = _filterDatedSections(exists(evidencePath(root)) ? read(evidencePath(root)) : '', cutoff);
|
|
14100
|
+
const _decisionsSig = _filterDatedSections(decisions, cutoff);
|
|
14065
14101
|
const handoff = exists(handoffPath(root)) ? read(handoffPath(root)) : '';
|
|
14066
14102
|
|
|
14067
14103
|
// 1) 작업 상태 분포
|
|
@@ -14103,9 +14139,18 @@ function _retroAggregate(root) {
|
|
|
14103
14139
|
const durations = [];
|
|
14104
14140
|
for (const m of evidence.matchAll(/exit=\d+\s*\((\d+)ms\)/g)) durations.push(parseInt(m[1], 10));
|
|
14105
14141
|
|
|
14106
|
-
// 5) 실패→성공 시그널 — task-log/evidence/decisions에서
|
|
14107
|
-
|
|
14108
|
-
|
|
14142
|
+
// 5) 실패→성공 시그널 — task-log/evidence/decisions(기간 필터본)에서 카운트.
|
|
14143
|
+
// 1.36.38 (codex 4차 #10): JS \b 는 한글 경계를 못 만들어 한글 신호가 전멸(FN)했고, 'pass' 가 password/bypass 안에서,
|
|
14144
|
+
// 'done' 이 undone 안에서 매치(FP)했다. → 한글은 리터럴 카운트, ASCII 는 lookaround 토큰 경계.
|
|
14145
|
+
const _sigText = tlog + evidence + _decisionsSig;
|
|
14146
|
+
const fixSignals = [
|
|
14147
|
+
...(_sigText.match(/(?<![a-z])(fix(?:ed)?|bug|incomplete)(?![a-z])/gi) || []),
|
|
14148
|
+
...(_sigText.match(/수정|롤백|재발/g) || []),
|
|
14149
|
+
];
|
|
14150
|
+
const passSignals = [
|
|
14151
|
+
...(_sigText.match(/✓|통과/g) || []),
|
|
14152
|
+
...(_sigText.match(/(?<![a-z])(pass(?:ed)?|completed|done)(?![a-z])/gi) || []),
|
|
14153
|
+
];
|
|
14109
14154
|
|
|
14110
14155
|
// 6) 룰 활용
|
|
14111
14156
|
const rules = exists(rulesPath(root)) ? readRules(root) : [];
|
|
@@ -14205,7 +14250,7 @@ function retroCmd(root) {
|
|
|
14205
14250
|
if (!Number.isFinite(days)) { failJson(has('--json'), 'invalid_arg', _uiLang(root) === 'en' ? '--days must be a number' : '--days 는 숫자여야 합니다'); return; }
|
|
14206
14251
|
days = Math.max(0, Math.min(days, 36500));
|
|
14207
14252
|
const cutoff = new Date(Date.now() - days * 86400 * 1000).toISOString().slice(0, 10);
|
|
14208
|
-
const agg = _retroAggregate(root);
|
|
14253
|
+
const agg = _retroAggregate(root, cutoff); // 1.36.38 (#2): 기간 필터 적용
|
|
14209
14254
|
// 1.9.16: --json
|
|
14210
14255
|
if (has('--json')) {
|
|
14211
14256
|
log(JSON.stringify({ project: path.basename(root), days, cutoff, summary: _retroOneLine(agg), data: agg }, null, 2));
|
|
@@ -14309,7 +14354,7 @@ function insightsCmd(root) {
|
|
|
14309
14354
|
if (has('--all-apps') || arg('--include', null)) {
|
|
14310
14355
|
return _insightsWorkspace(root);
|
|
14311
14356
|
}
|
|
14312
|
-
const agg = _retroAggregate(root);
|
|
14357
|
+
const agg = _retroAggregate(root); // insights 는 누적 지표 명령 — 기간 필터 없음(retro 만 --days). 1.36.38 광역치환 오적용 교정.
|
|
14313
14358
|
// 1.9.16: --json
|
|
14314
14359
|
if (has('--json')) {
|
|
14315
14360
|
const sc = readSessionCounter(root);
|
|
@@ -21642,6 +21687,8 @@ async function main() {
|
|
|
21642
21687
|
let root;
|
|
21643
21688
|
if (_pathArg) root = absRoot(_pathArg);
|
|
21644
21689
|
else if (args[1] && !args[1].startsWith('-') && exists(path.join(absRoot(args[1]), '.harness'))) root = absRoot(args[1]);
|
|
21690
|
+
// 1.36.38 (codex 4차 #9): subcommand 형(예: memory status <B>)은 대상 경로가 args[2] — 종전엔 cwd(A)에 오귀속됐다.
|
|
21691
|
+
else if (args[2] && !args[2].startsWith('-') && exists(path.join(absRoot(args[2]), '.harness'))) root = absRoot(args[2]);
|
|
21645
21692
|
else root = absRoot(process.cwd());
|
|
21646
21693
|
if (exists(path.join(root, '.harness'))) _bumpUsage(root, cmd);
|
|
21647
21694
|
} catch {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.38",
|
|
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
|
@@ -2166,8 +2166,10 @@ total++;
|
|
|
2166
2166
|
cp.spawnSync(process.execPath, [CLI, 'init', tmpR, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
2167
2167
|
cp.spawnSync(process.execPath, [CLI, 'plan', 'add', '캐시 helper', '--status', 'done', '--path', tmpR], { stdio: 'ignore', timeout: 10000 });
|
|
2168
2168
|
cp.spawnSync(process.execPath, [CLI, 'plan', 'add', '인증 helper', '--status', 'in-progress', '--path', tmpR], { stdio: 'ignore', timeout: 10000 });
|
|
2169
|
-
|
|
2170
|
-
|
|
2169
|
+
// 1.36.38: retro 가 --days 기간필터를 실제 적용하므로 픽스처 날짜는 오늘(창 안) — 고정 과거 날짜는 필터에 걸러지는 게 의도된 동작.
|
|
2170
|
+
const _todayR = new Date().toISOString().slice(0, 10);
|
|
2171
|
+
fs.appendFileSync(path.join(tmpR, '.harness/decisions.md'), `\n### ${_todayR} — 캐시 차등 TTL 결정\n- Reason: ...\n`);
|
|
2172
|
+
fs.appendFileSync(path.join(tmpR, '.harness/review-evidence.md'), `\n## ${_todayR} verify-code\nexit=0 (250ms)\nexit=0 (180ms)\nexit=0 (120ms)\nexit=0 (90ms)\n`);
|
|
2171
2173
|
const r = cp.spawnSync(process.execPath, [CLI, 'retro', tmpR], { encoding: 'utf8', timeout: 15000 });
|
|
2172
2174
|
const ok = r.status === 0 && /한 줄 요약/.test(r.stdout) && /작업 상태 분포/.test(r.stdout) && /다음 우선 작업/.test(r.stdout) && /검증 시간 추세/.test(r.stdout);
|
|
2173
2175
|
console.log(ok ? '✓ B(1.9.13) retro: 한 줄 요약 + 다음 우선 작업 + 검증 시간 추세' : '✗ retro 실패');
|