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 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.37 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
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.37는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
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.37)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
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.37: 2026-07-15
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';
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', '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,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
- function _retroAggregate(root) {
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
- const tlog = exists(taskLogPath(root)) ? read(taskLogPath(root)) : '';
14064
- const evidence = exists(evidencePath(root)) ? read(evidencePath(root)) : '';
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에서 "롤백" / "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) || [];
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.37",
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
- 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 실패');