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 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.37 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
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.37는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
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.37)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
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.37: 2026-07-15
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';
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', 'v1.9.*', '--sort=creatordate', '--format=%(refname:short)|%(creatordate:iso8601)'], {
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', 'v1.9.*', '--sort=creatordate', '--format=%(refname:short)|%(creatordate:iso8601)'], {
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
- security: 'unknown',
5308
- health: 'unknown',
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} | ${score} | ${tokens || '?'} |\n`;
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
- function _retroAggregate(root) {
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
- const tlog = exists(taskLogPath(root)) ? read(taskLogPath(root)) : '';
14064
- const evidence = exists(evidencePath(root)) ? read(evidencePath(root)) : '';
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) 결정 블록 (1.9.14: 코드블록/Template 제외)
14073
- const decisionBlocks = _extractDecisionBlocks(decisions);
14074
- // recent decisions (날짜로 정렬 시 가장 최근)
14075
- const recentDecisions = decisionBlocks.slice(-5).map(b => {
14076
- const t = (b.match(/^### (.+)$/m) || [, ''])[1];
14077
- return { title: t.trim(), block: b.slice(0, 200) };
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에서 "롤백" / "fail" / "재발" / "fix" / "수정" 등의 동시 등장 카운트
14107
- const fixSignals = (tlog + evidence + decisions).match(/\b(fix|fixed|수정|롤백|재발|incomplete|bug)\b/gi) || [];
14108
- const passSignals = (tlog + evidence + decisions).match(/(?:✓|pass(?:ed)?|통과|completed|done)/gi) || [];
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 { name, arguments: args = {} } = req.params || {};
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
- try {
17813
- const req = JSON.parse(line);
17814
- handleRequest(req);
17815
- } catch (e) {
17816
- send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error: ' + e.message } });
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.37",
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
- fs.appendFileSync(path.join(tmpR, '.harness/decisions.md'), `\n### 2026-05-13 캐시 차등 TTL 결정\n- Reason: ...\n`);
2170
- fs.appendFileSync(path.join(tmpR, '.harness/review-evidence.md'), `\n## 2026-05-13 verify-code\nexit=0 (250ms)\nexit=0 (180ms)\nexit=0 (120ms)\nexit=0 (90ms)\n`);
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 실패');