leerness 1.36.37 → 1.36.39
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 +17 -0
- package/README.md +4 -4
- package/bin/leerness.js +135 -30
- package/package.json +1 -1
- package/scripts/e2e.js +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.39 — 2026-07-16 — 판정 정직화 배치 B — codex 4차 헌트 10건 전건 처분 완결 (#3/#5/#6/#7/#8)
|
|
4
|
+
|
|
5
|
+
- **#3 MCP 파라미터 검증**: `arguments: null`/비객체가 args 접근 throw → stdin 캐치가 -32700 "Parse error"(id null)로 오보 + `{"title":{...}}` 같은 비문자열이 `[object Object]` 로 영속. → 비객체 arguments/스키마 타입 위반/필수 누락은 **-32602 + 원 id**, 파싱과 핸들링 캐치 분리(-32603 은 원 id 유지). 원 재현 2종 실측 + 정상 호출 무회귀.
|
|
6
|
+
- **#5 decisions 이중 소스**: retro/insights 가 md 를 직접 읽어 `decision list`/`memory status`(canonical json)와 불일치 → canonical `_loadDecisions` 단일 소스(신호 텍스트·recent 목록·카운트 전부, cutoff 는 date 필드). 실측: md 수동 훼손 후에도 list=retro=2 일치.
|
|
7
|
+
- **#6 llm-bench record**: -1/NaN 점수·무제한 필드 무검증 저장 → 유한·비음수 점수 필수(`invalid_score`), 필드 200자 상한 + 파이프/개행 셀 이스케이프.
|
|
8
|
+
- **#7 benchmark**: 미초기화 빈 디렉토리에 **497/600** — 4개 차원이 상수·2개는 높은 바닥값 → .harness 없으면 측정 거부(`harness_missing`), 점수 응답에 `scoreBasis`(measuredDims/constantDims/시뮬 명시) 라벨 — 설계치를 실측으로 오독 방지([[lesson-tool-overstates-own-maturity]] 계열).
|
|
9
|
+
- **#8 pulse↔health 모순**: pulse 가 계산하지 않는 security/health/driftScore 를 'unknown' 상태값처럼 노출 → null + `notComputed` 명시(계산은 `leerness health`).
|
|
10
|
+
- **검증**: selftest 310/310, 5건 원 재현 before/after + 무회귀, exit 전파형 게이트 e2e, 클린룸. **codex 4차 10건 전건 처분(수정 10 · DEFER 0).**
|
|
11
|
+
|
|
12
|
+
## 1.36.38 — 2026-07-16 — 분석 정직화 — retro 기간필터·신호 정규식·태그 계보·usage 귀속 (codex 4차 #2/#4/#9/#10)
|
|
13
|
+
|
|
14
|
+
- **#2 `retro --days`**: cutoff 를 계산만 하고 미적용 — 주간 회고가 전기간 집계였다. → 날짜 헤딩 섹션 필터(`_filterDatedSections`)를 task-log/evidence/decisions-신호에 적용(결정 블록 수는 "누적" 라벨대로 누적 유지). 실측: days=1 에서 2020 데이터 제외.
|
|
15
|
+
- **#10 신호 정규식**: JS `\b` 가 한글 경계를 못 만들어 한글 fix 신호 전멸(FN) + `pass`가 password/bypass 안에서, `done` 이 undone 안에서 매치(FP). → 한글 리터럴 카운트 + ASCII lookaround 토큰 경계. 실측: "수정 롤백 재발 password bypass undone" → fix 3 · pass 0.
|
|
16
|
+
- **#4 태그 계보**: round-history/milestones 가 `v1.9.*` 하드코드 — 현 계보(v1.36.x) 이력 0 으로 집계하면서 **"모든 마일스톤 달성 (500+)" 허위 축하**까지 출력. → `v*` 전체 semver 수집 + 이력 0 이면 "태그 이력 없음" 정직 표기. 실측: v1.36.36/37 태그 → roundCount 2.
|
|
17
|
+
- **#9 usage 귀속**: `memory status <B>` 처럼 subcommand 대상이 args[2] 인 명령의 사용량이 cwd(A)에 오귀속. → args[2] 대상 해석 추가. 실측: A 0 · B 귀속.
|
|
18
|
+
- **검증**: selftest 309/309(필터/신호/태그/귀속/배선 행위검사), 4건 원 재현 before/after, exit 전파형 게이트 e2e, 클린룸. 이연(codex 4차 잔여): #3 MCP 파라미터 검증 · #5 decisions 이중 소스 · #6 llm-bench 검증 · #7 benchmark 빈 디렉토리 점수 · #8 pulse/health 불일치.
|
|
19
|
+
|
|
3
20
|
## 1.36.37 — 2026-07-16 — managed 파일 --force 병합 — 마이그레이션 보존의 마지막 구멍 (1.9.441 드리프트 경로 실측)
|
|
4
21
|
|
|
5
22
|
도그푸딩에서 발견된 실제 드리프트 세대(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.39 하네스를 사용합니다. 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.39는 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.39)** · 매 라운드 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.39: 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.39';
|
|
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,43 @@ 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
|
+
} },
|
|
3260
|
+
{ name: '판정 정직화 배치B (1.36.39, codex 4차 #3/#5/#6/#7/#8): MCP -32602 검증 + retro canonical decisions + llm-bench 검증 + benchmark 정직 라벨 + pulse notComputed', run: () => {
|
|
3261
|
+
const s = read(__filename);
|
|
3262
|
+
// #3: arguments 비객체 -32602 + 스키마 타입 검증 + 파싱/핸들링 분리
|
|
3263
|
+
const mcpOk = s.includes("code: -32602, message: 'Invalid params: arguments must be an object'")
|
|
3264
|
+
&& s.includes("must be a string") && s.includes("catch (e) { send({ jsonrpc: '2.0', id: (req && req.id != null) ? req.id : null");
|
|
3265
|
+
// #5: retro 가 canonical _loadDecisions 사용 (행위: json 에 2건 → retro 도 2)
|
|
3266
|
+
const declOk = s.includes('const _decsCanon = (() => { try { return _loadDecisions(root) || []; }');
|
|
3267
|
+
// #6: llm-bench 점수 검증 + 셀 이스케이프
|
|
3268
|
+
const benchRecOk = s.includes("'invalid_score'") && s.includes('const _bcell = v =>');
|
|
3269
|
+
// #7: 미초기화 측정 거부 + 점수 근거 라벨
|
|
3270
|
+
const benchOk = s.includes("failJson(has('--json'), 'harness_missing', `측정 불가") && s.includes("constantDims: ['autoVerify', 'workspace', 'contextKeep']");
|
|
3271
|
+
// #8: pulse notComputed
|
|
3272
|
+
const pulseOk = s.includes("notComputed: ['security', 'health', 'driftScore']");
|
|
3273
|
+
return mcpOk && declOk && benchRecOk && benchOk && pulseOk;
|
|
3274
|
+
} },
|
|
3238
3275
|
{ name: 'debug 렌즈 (1.36.27, obra/superpowers systematic-debugging): 자기질문 6문항 ko/en 락스텝 + affects 유효 + route bugfix 힌트 + 파일매핑 미확장 — 행위검사', run: () => {
|
|
3239
3276
|
const d = LENS_CATALOG.debug;
|
|
3240
3277
|
if (!d) return false;
|
|
@@ -5304,9 +5341,12 @@ function pulseCmd(root) {
|
|
|
5304
5341
|
roundCount: 0,
|
|
5305
5342
|
mcpTools: 0,
|
|
5306
5343
|
memorySurface: 'T0/D0/R0/P0/L0',
|
|
5307
|
-
|
|
5308
|
-
health:
|
|
5344
|
+
// 1.36.39 (codex 4차 #8): pulse 는 이 3종을 계산하지 않는다 — 상태값처럼 보이던 'unknown' placeholder 가
|
|
5345
|
+
// health --json(healthy:true)과 모순돼 보였음. null + notComputed 로 명시(계산은 leerness health).
|
|
5346
|
+
security: null,
|
|
5347
|
+
health: null,
|
|
5309
5348
|
driftScore: null,
|
|
5349
|
+
notComputed: ['security', 'health', 'driftScore'],
|
|
5310
5350
|
nextMilestone: null,
|
|
5311
5351
|
etaDays: null,
|
|
5312
5352
|
abnormalShutdown: 'none'
|
|
@@ -5817,7 +5857,7 @@ function roundHistoryCmd(root) {
|
|
|
5817
5857
|
if (data.nextMilestone != null) {
|
|
5818
5858
|
log(yl(` 🎯 다음 마일스톤: R${data.nextMilestone} (${data.roundsToNextMilestone} 라운드 남음)`));
|
|
5819
5859
|
} else {
|
|
5820
|
-
log(gr(` 🎉 모든 마일스톤 달성 (500+)`));
|
|
5860
|
+
log(data && (data.roundCount || data.totalRounds) ? gr(` 🎉 모든 마일스톤 달성 (500+)`) : dm(` (이 저장소 계보의 릴리스 태그 이력 없음)`)); // 1.36.38 (#4): 이력 0 인데 달성 축하 금지
|
|
5821
5861
|
}
|
|
5822
5862
|
log('');
|
|
5823
5863
|
log(` 최근 10 tags:`);
|
|
@@ -13965,9 +14005,13 @@ function llmBenchRecordCmd(root) {
|
|
|
13965
14005
|
const tokens = arg('--tokens', null);
|
|
13966
14006
|
const model = arg('--model', 'unknown');
|
|
13967
14007
|
if (!score) { fail('--score 필요'); return process.exit(1); }
|
|
14008
|
+
// 1.36.39 (codex 4차 #6): 무의미 데이터 무검증 저장 — 유한/비음수 점수 + 필드 길이 상한 + 셀 파이프 이스케이프.
|
|
14009
|
+
const scoreN = Number(score);
|
|
14010
|
+
if (!Number.isFinite(scoreN) || scoreN < 0) { failJson(has('--json'), 'invalid_score', `--score 는 0 이상의 유한 숫자여야 합니다 (받음: ${score})`); return; }
|
|
14011
|
+
const _bcell = v => String(v == null ? '' : v).slice(0, 200).replace(/\|/g, '\\|').replace(/\r?\n/g, ' ');
|
|
13968
14012
|
const histFile = path.join(root, '.harness', 'llm-bench-history.md');
|
|
13969
14013
|
if (!exists(path.dirname(histFile))) fs.mkdirSync(path.dirname(histFile), { recursive: true });
|
|
13970
|
-
const row = `| ${today()} | ${model} | ${label} | ${
|
|
14014
|
+
const row = `| ${today()} | ${_bcell(model)} | ${_bcell(label)} | ${scoreN} | ${_bcell(tokens) || '?'} |\n`;
|
|
13971
14015
|
if (!exists(histFile)) {
|
|
13972
14016
|
writeUtf8(histFile, `# LLM Bench History\n\n| Date | Model | Label | Score | Tokens |\n|---|---|---|---:|---:|\n${row}`);
|
|
13973
14017
|
} else {
|
|
@@ -14056,12 +14100,31 @@ function writeSessionCounter(root, c) { writeUtf8(sessionCounterPath(root), JSON
|
|
|
14056
14100
|
|
|
14057
14101
|
// 1.9.324 (UR-0025): _countDatedBlocks / _extractDecisionBlocks → lib/pure-utils.js 로 이동 (순수 메모리 MD 파서, require 사용).
|
|
14058
14102
|
|
|
14059
|
-
|
|
14103
|
+
// 1.36.38 (codex 4차 #2): 날짜 헤딩(## YYYY-MM-DD / ### YYYY-MM-DD …) 섹션 문서를 cutoff 이후만 남김.
|
|
14104
|
+
// 날짜 없는 프리앰블은 유지(헤더/템플릿 — 신호 카운트에 거의 무기여). cutoff 없으면 원문 그대로(무회귀).
|
|
14105
|
+
function _filterDatedSections(text, cutoff) {
|
|
14106
|
+
if (!cutoff || !text) return text || '';
|
|
14107
|
+
const parts = String(text).split(/(?=^#{2,3} \d{4}-\d{2}-\d{2})/m);
|
|
14108
|
+
return parts.filter(p => {
|
|
14109
|
+
const m = p.match(/^#{2,3} (\d{4}-\d{2}-\d{2})/);
|
|
14110
|
+
return !m || m[1] >= cutoff;
|
|
14111
|
+
}).join('');
|
|
14112
|
+
}
|
|
14113
|
+
|
|
14114
|
+
function _retroAggregate(root, cutoff) {
|
|
14060
14115
|
root = absRoot(root);
|
|
14061
14116
|
const rows = readProgressRows(root);
|
|
14062
14117
|
const decisions = exists(decisionsPath(root)) ? read(decisionsPath(root)) : '';
|
|
14063
|
-
|
|
14064
|
-
|
|
14118
|
+
// 1.36.38 (#2): --days 의 cutoff 가 계산만 되고 미적용 → 주간 회고가 전기간 집계였다. 기간 신호(tlog/evidence)에 적용.
|
|
14119
|
+
// decisions 블록 수는 "누적" 라벨 그대로 누적 유지, 신호 카운트용 텍스트만 기간 필터.
|
|
14120
|
+
const tlog = _filterDatedSections(exists(taskLogPath(root)) ? read(taskLogPath(root)) : '', cutoff);
|
|
14121
|
+
const evidence = _filterDatedSections(exists(evidencePath(root)) ? read(evidencePath(root)) : '', cutoff);
|
|
14122
|
+
// 1.36.39 (codex 4차 #5): decisions 는 canonical decisions.json 단일 소스 — md 직접 읽기는 decision list/memory status 와
|
|
14123
|
+
// 불일치를 만들었다(md 수동 편집/프로젝션 중단 시). 신호 텍스트도 canonical 객체에서 합성 + cutoff 는 date 필드로.
|
|
14124
|
+
const _decsCanon = (() => { try { return _loadDecisions(root) || []; } catch { return []; } })();
|
|
14125
|
+
const _decisionsSig = _decsCanon
|
|
14126
|
+
.filter(d => !cutoff || !d.date || String(d.date).slice(0, 10) >= cutoff)
|
|
14127
|
+
.map(d => [d.decision, d.title, d.reason, d.impact].filter(Boolean).join(' ')).join('\n');
|
|
14065
14128
|
const handoff = exists(handoffPath(root)) ? read(handoffPath(root)) : '';
|
|
14066
14129
|
|
|
14067
14130
|
// 1) 작업 상태 분포
|
|
@@ -14069,13 +14132,12 @@ function _retroAggregate(root) {
|
|
|
14069
14132
|
for (const s of STATUSES) statusCounts[s] = 0;
|
|
14070
14133
|
for (const r of rows) if (statusCounts[r.status] != null) statusCounts[r.status]++;
|
|
14071
14134
|
|
|
14072
|
-
// 2) 결정
|
|
14073
|
-
const decisionBlocks =
|
|
14074
|
-
|
|
14075
|
-
|
|
14076
|
-
|
|
14077
|
-
|
|
14078
|
-
}).reverse();
|
|
14135
|
+
// 2) 결정 수 — 1.36.39 (#5): canonical decisions.json 기준 (decision list/memory status 와 항상 일치)
|
|
14136
|
+
const decisionBlocks = _decsCanon;
|
|
14137
|
+
const recentDecisions = _decsCanon.slice(-5).map(d => ({
|
|
14138
|
+
title: String(d.title || d.decision || '').trim(),
|
|
14139
|
+
block: [d.decision, d.reason].filter(Boolean).join(' — ').slice(0, 200)
|
|
14140
|
+
})).reverse();
|
|
14079
14141
|
|
|
14080
14142
|
// 3) 스킬 활용
|
|
14081
14143
|
const skillsDir = path.join(root, '.harness/skills');
|
|
@@ -14103,9 +14165,18 @@ function _retroAggregate(root) {
|
|
|
14103
14165
|
const durations = [];
|
|
14104
14166
|
for (const m of evidence.matchAll(/exit=\d+\s*\((\d+)ms\)/g)) durations.push(parseInt(m[1], 10));
|
|
14105
14167
|
|
|
14106
|
-
// 5) 실패→성공 시그널 — task-log/evidence/decisions에서
|
|
14107
|
-
|
|
14108
|
-
|
|
14168
|
+
// 5) 실패→성공 시그널 — task-log/evidence/decisions(기간 필터본)에서 카운트.
|
|
14169
|
+
// 1.36.38 (codex 4차 #10): JS \b 는 한글 경계를 못 만들어 한글 신호가 전멸(FN)했고, 'pass' 가 password/bypass 안에서,
|
|
14170
|
+
// 'done' 이 undone 안에서 매치(FP)했다. → 한글은 리터럴 카운트, ASCII 는 lookaround 토큰 경계.
|
|
14171
|
+
const _sigText = tlog + evidence + _decisionsSig;
|
|
14172
|
+
const fixSignals = [
|
|
14173
|
+
...(_sigText.match(/(?<![a-z])(fix(?:ed)?|bug|incomplete)(?![a-z])/gi) || []),
|
|
14174
|
+
...(_sigText.match(/수정|롤백|재발/g) || []),
|
|
14175
|
+
];
|
|
14176
|
+
const passSignals = [
|
|
14177
|
+
...(_sigText.match(/✓|통과/g) || []),
|
|
14178
|
+
...(_sigText.match(/(?<![a-z])(pass(?:ed)?|completed|done)(?![a-z])/gi) || []),
|
|
14179
|
+
];
|
|
14109
14180
|
|
|
14110
14181
|
// 6) 룰 활용
|
|
14111
14182
|
const rules = exists(rulesPath(root)) ? readRules(root) : [];
|
|
@@ -14205,7 +14276,7 @@ function retroCmd(root) {
|
|
|
14205
14276
|
if (!Number.isFinite(days)) { failJson(has('--json'), 'invalid_arg', _uiLang(root) === 'en' ? '--days must be a number' : '--days 는 숫자여야 합니다'); return; }
|
|
14206
14277
|
days = Math.max(0, Math.min(days, 36500));
|
|
14207
14278
|
const cutoff = new Date(Date.now() - days * 86400 * 1000).toISOString().slice(0, 10);
|
|
14208
|
-
const agg = _retroAggregate(root);
|
|
14279
|
+
const agg = _retroAggregate(root, cutoff); // 1.36.38 (#2): 기간 필터 적용
|
|
14209
14280
|
// 1.9.16: --json
|
|
14210
14281
|
if (has('--json')) {
|
|
14211
14282
|
log(JSON.stringify({ project: path.basename(root), days, cutoff, summary: _retroOneLine(agg), data: agg }, null, 2));
|
|
@@ -14309,7 +14380,7 @@ function insightsCmd(root) {
|
|
|
14309
14380
|
if (has('--all-apps') || arg('--include', null)) {
|
|
14310
14381
|
return _insightsWorkspace(root);
|
|
14311
14382
|
}
|
|
14312
|
-
const agg = _retroAggregate(root);
|
|
14383
|
+
const agg = _retroAggregate(root); // insights 는 누적 지표 명령 — 기간 필터 없음(retro 만 --days). 1.36.38 광역치환 오적용 교정.
|
|
14313
14384
|
// 1.9.16: --json
|
|
14314
14385
|
if (has('--json')) {
|
|
14315
14386
|
const sc = readSessionCounter(root);
|
|
@@ -17066,6 +17137,8 @@ function benchmarkCmd(root) {
|
|
|
17066
17137
|
log(`💡 verify-claim/audit 오버헤드는 일반적으로 검수 1회당 200~500ms (실 CLI 호출 대비 1-10%)`);
|
|
17067
17138
|
});
|
|
17068
17139
|
}
|
|
17140
|
+
// 1.36.39 (codex 4차 #7): 미초기화 디렉토리(증거 0)에 497/600 을 주던 것 — 측정 대상이 아니므로 거부.
|
|
17141
|
+
if (!exists(path.join(root, '.harness'))) { failJson(has('--json'), 'harness_missing', `측정 불가: ${root} 에 .harness 없음 — leerness init 후 사용`); return; }
|
|
17069
17142
|
const rows = readProgressRows(root);
|
|
17070
17143
|
const done = rows.filter(r => r.status === 'done').length;
|
|
17071
17144
|
const totalTasks = rows.length;
|
|
@@ -17101,6 +17174,12 @@ function benchmarkCmd(root) {
|
|
|
17101
17174
|
project: detectProjectName(root),
|
|
17102
17175
|
measured: { totalTasks, done, reuseLines, usage: usage.commands, driftLevel: usage.drift },
|
|
17103
17176
|
leernessScore: score, total,
|
|
17177
|
+
// 1.36.39 (#7 정직화): 어떤 차원이 측정 기반이고 어떤 게 설계 상수/정성 추정인지 명시 — 점수를 실측으로 오독 방지.
|
|
17178
|
+
scoreBasis: {
|
|
17179
|
+
measuredDims: ['multiAgent', 'reuse', 'bugDetect'],
|
|
17180
|
+
constantDims: ['autoVerify', 'workspace', 'contextKeep'],
|
|
17181
|
+
note: 'constantDims 는 도구 능력 설계치(이 워크스페이스의 측정값 아님) · compareSimulated 는 정성 추정'
|
|
17182
|
+
},
|
|
17104
17183
|
compareSimulated: vsTools
|
|
17105
17184
|
}, null, 2));
|
|
17106
17185
|
return;
|
|
@@ -17753,7 +17832,31 @@ function mcpServeCmd(root) {
|
|
|
17753
17832
|
} else if (req.method === 'tools/list') {
|
|
17754
17833
|
send({ jsonrpc: '2.0', id, result: { tools: TOOLS } });
|
|
17755
17834
|
} else if (req.method === 'tools/call') {
|
|
17756
|
-
const
|
|
17835
|
+
const name = (req.params || {}).name;
|
|
17836
|
+
// 1.36.39 (codex 4차 #3): arguments 가 null/비객체면 기본 {} 디폴트가 안 먹어 args.path 접근이 throw →
|
|
17837
|
+
// stdin 캐치가 -32700 "Parse error"(id null)로 오보했다. 유효 JSON 의 잘못된 파라미터는 -32602 + 원 id.
|
|
17838
|
+
const _rawArgs = (req.params || {}).arguments;
|
|
17839
|
+
if (_rawArgs != null && (typeof _rawArgs !== 'object' || Array.isArray(_rawArgs))) {
|
|
17840
|
+
return send({ jsonrpc: '2.0', id, error: { code: -32602, message: 'Invalid params: arguments must be an object' } });
|
|
17841
|
+
}
|
|
17842
|
+
const args = _rawArgs || {};
|
|
17843
|
+
// 1.36.39 (#3): 도구 inputSchema 기반 타입 검증 — {"title":{...}} 같은 비문자열이 "[object Object]" 로 영속되던 것 차단.
|
|
17844
|
+
{
|
|
17845
|
+
const _td = TOOLS.find(t => t.name === name);
|
|
17846
|
+
const _props = _td && _td.inputSchema && _td.inputSchema.properties || null;
|
|
17847
|
+
if (_props) {
|
|
17848
|
+
for (const [k, spec] of Object.entries(_props)) {
|
|
17849
|
+
const v = args[k];
|
|
17850
|
+
if (v === undefined || v === null) continue;
|
|
17851
|
+
if (spec.type === 'string' && typeof v !== 'string') return send({ jsonrpc: '2.0', id, error: { code: -32602, message: `Invalid params: '${k}' must be a string` } });
|
|
17852
|
+
if (spec.type === 'number' && typeof v !== 'number') return send({ jsonrpc: '2.0', id, error: { code: -32602, message: `Invalid params: '${k}' must be a number` } });
|
|
17853
|
+
if (spec.type === 'boolean' && typeof v !== 'boolean') return send({ jsonrpc: '2.0', id, error: { code: -32602, message: `Invalid params: '${k}' must be a boolean` } });
|
|
17854
|
+
}
|
|
17855
|
+
for (const rk of (_td.inputSchema.required || [])) {
|
|
17856
|
+
if (args[rk] === undefined || args[rk] === null) return send({ jsonrpc: '2.0', id, error: { code: -32602, message: `Invalid params: missing required '${rk}'` } });
|
|
17857
|
+
}
|
|
17858
|
+
}
|
|
17859
|
+
}
|
|
17757
17860
|
// 1.12.2 (14th 버그헌트, UR-0181): MCP 서버는 generic 설계 — cwd 에서 실행 후 각 호출의 path 로 대상 프로젝트 지정(의도된 기능, e2e 가 검증). 즉 path-타게팅 자체는 취약점 아님(신뢰된 로컬 MCP). policy 도 대상-프로젝트별로 path 에서 로드(올바름).
|
|
17758
17861
|
// 진짜 버그는 unknown tool 도 _bumpMcpUsage 가 임의 경로에 무조건 쓰던 것 → 사용 통계를 unknown-tool 검증 "후"로 이동. (프로젝트-scoped 가둠은 opt-in 플래그로 별도 검토 — UR backlog)
|
|
17759
17862
|
const targetPath = args.path || root;
|
|
@@ -17809,12 +17912,12 @@ function mcpServeCmd(root) {
|
|
|
17809
17912
|
const line = buf.slice(0, nl).trim();
|
|
17810
17913
|
buf = buf.slice(nl + 1);
|
|
17811
17914
|
if (!line) continue;
|
|
17812
|
-
|
|
17813
|
-
|
|
17814
|
-
|
|
17815
|
-
|
|
17816
|
-
|
|
17817
|
-
}
|
|
17915
|
+
// 1.36.39 (#3): 파싱과 핸들링 분리 — 핸들러 내부 예외가 -32700 "Parse error"(id null)로 오보되지 않게.
|
|
17916
|
+
let req = null;
|
|
17917
|
+
try { req = JSON.parse(line); }
|
|
17918
|
+
catch (e) { send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error: ' + e.message } }); continue; }
|
|
17919
|
+
try { handleRequest(req); }
|
|
17920
|
+
catch (e) { send({ jsonrpc: '2.0', id: (req && req.id != null) ? req.id : null, error: { code: -32603, message: 'Internal error: ' + e.message } }); }
|
|
17818
17921
|
}
|
|
17819
17922
|
});
|
|
17820
17923
|
process.stdin.on('end', () => process.exit(0));
|
|
@@ -21642,6 +21745,8 @@ async function main() {
|
|
|
21642
21745
|
let root;
|
|
21643
21746
|
if (_pathArg) root = absRoot(_pathArg);
|
|
21644
21747
|
else if (args[1] && !args[1].startsWith('-') && exists(path.join(absRoot(args[1]), '.harness'))) root = absRoot(args[1]);
|
|
21748
|
+
// 1.36.38 (codex 4차 #9): subcommand 형(예: memory status <B>)은 대상 경로가 args[2] — 종전엔 cwd(A)에 오귀속됐다.
|
|
21749
|
+
else if (args[2] && !args[2].startsWith('-') && exists(path.join(absRoot(args[2]), '.harness'))) root = absRoot(args[2]);
|
|
21645
21750
|
else root = absRoot(process.cwd());
|
|
21646
21751
|
if (exists(path.join(root, '.harness'))) _bumpUsage(root, cmd);
|
|
21647
21752
|
} catch {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.39",
|
|
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 실패');
|