leerness 1.36.78 → 1.36.79

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,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.36.79 — 2026-07-28 — 실 프로젝트 60개 도그푸딩: P1 5건 (합성 헌트 10회가 못 본 결함)
4
+
5
+ 사용자의 **실제 프로젝트 60개**(v0.1.40 ~ 1.36.77)를 5개 렌즈로 병렬 감사(읽기 전용 + temp 복사본 실험). 합성 픽스처 기반 codex 헌트 10라운드가 한 번도 못 본 P1 5건이 나왔다 — 실데이터에서만 발화하는 조건들이었다.
6
+
7
+ - **P1 progress-tracker 행 무단 삭제 (실 데이터 손실)**: ID 접두를 `T|M|D` 로만 인정해 `R-`/`UR-` 등 다른 접두 행이 파싱에서 탈락 → 재작성 시 **사용자 이력이 조용히 삭제**됐다(실측: auto-Trading 9행 → 1행, 942B, exit 0 무경고). migrate 만이 아니라 `task add` 등 모든 쓰기 경로 공통. 접두 일반화 + **클래스 가드**(디스크 행 수 > 쓰려는 행 수면 쓰기 중단) — 규칙 하나가 아니라 불변식으로 차단.
8
+ - **P1 `handoff --json` 이 사람용 drift 배너로 오염**: stale 프로젝트에서만 발화해(실 프로젝트 27/37 해당) JSON.parse 가 실패, MCP/AI 소비자가 전부 깨졌다 → `--json` 이면 배너 억제(텍스트 모드는 유지).
9
+ - **P1 `parent detect <path>` 가 positional 경로 무시**: cwd 를 분석해 자신만만한 오답을 exit 0 으로 반환(positional path 잔존 클래스의 미소진 지점).
10
+ - **P1 tech 언어 오판**: devDependency 1개짜리 package.json 이 **205개 .py 파일을 이겨** Python 모노레포를 "javascript" 로 라벨링 → 실제 소스 파일 수를 1순위 근거로 재정렬 + 스캔 깊이 2→3(3단 레이아웃 누락 해소). 진짜 JS 프로젝트는 무회귀.
11
+ - **P1 audit 이 파괴된 설치를 healthy 로 보고**: 56개 중 44개가 사라진 설치에 `healthy:true·failures:0`(같은 디렉토리에서 integrity 는 `ok:false` — 두 판정 모순) → integrity 의 관리문서 부재/손상 신호를 audit 이 공유(단일 진실). 3건 이상 부재는 차단 등급.
12
+ - (문서) `doctor` 는 설계상 CLI 설치 진단이라 경로 인자가 없다 — usage 2곳의 잘못된 `doctor [path]` 광고 정정.
13
+ - **(R-0001 검수 21회전이 제 수정에서 P1급 5건을 더 차단)**: ① 클래스 가드의 기준선이 틀렸다 — "변이 후 행 수" 비교는 upsert 가 행을 추가하면 손실을 상쇄해 무력화(실측) → **"디스크에 파서가 못 읽는 행-모양 라인이 있는가"**로 재설계. ② `allowShrink` boolean 우회가 가드를 통째로 꺼 dedup 중 손상 행까지 삭제 → **expectedShrink 수량제**로. ③ 접두 전면 개방(`[A-Z]{1,3}`)이 무관한 참조표(`| API-123 | owner |`)를 task 로 흡수 → **등록 네임스페이스(T/M/D/R/UR/F/P)** 한정. ④ 표 **뒤 사용자 콘텐츠**(참조표·메모·푸터)가 재작성 때 삭제되던 것 → 후행 보존. ⑤ audit 이 integrity 임계값을 재구현해 판정이 갈림 → **단일 판정기 공유**. ⑥ tech 디렉토리당 400개 컷이 집계를 편향(1501 JS < 500 PY) → 상한 5000 + 전체 예산 20000. ⑦ parent 가 맨 상대경로를 무시하고 없는 경로도 성공 → 위치인자 파싱 + 존재 검증.
14
+ - 검증: P1 5건 + 검수 7건 재현→수정→재확인 실측(무회귀 포함), selftest 334, e2e +1(10 단언).
15
+
3
16
  ## 1.36.78 — 2026-07-28 — codex 10차 홀리스틱 헌트: 오케스트레이션/분석 명령군 7건 수정 (병렬 적대검증 8건 중 7 CONFIRMED·1 REFUTED)
4
17
 
5
18
  표면 회전으로 team/agents·constraints/intent·contract·milestones 를 헌트. 8건 보고를 8개 독립 에이전트로 병렬 재현(워크플로) → 7 CONFIRMED 수정, 1 REFUTED(pulse).
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.78 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
125
+ 이 프로젝트는 Leerness v1.36.79 하네스를 사용합니다. 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.78는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **88개 도구**를 노출:
179
+ Leerness v1.36.79는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **88개 도구**를 노출:
180
180
 
181
181
  ```jsonc
182
182
  // 카테고리별
@@ -197,7 +197,7 @@ Leerness v1.36.78는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
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.78 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
200
+ 현재 누적: **v1.9.x → 1.36.79 릴리스 태그 이력** (수백 라운드) · _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.78: 2026-07-28
238
+ Last synced by Leerness v1.36.79: 2026-07-28
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.78';
37
+ const VERSION = '1.36.79';
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') 시 호스트 프로세스 오염.
@@ -5880,6 +5880,41 @@ const _INTEGRITY_DOCS = [
5880
5880
  '.harness/consistency-policy.md', '.harness/testing-strategy.md', '.harness/review-checklist.md',
5881
5881
  '.harness/release-checklist.md', '.harness/session-close-policy.md', '.harness/plan-progress-boundary.md',
5882
5882
  ];
5883
+ // 1.36.79 (도그푸딩 P1-E): integrity 의 "관리 문서 부재/손상" 판정을 audit 도 공유하도록 경량 추출.
5884
+ // audit 이 존재-여부만 보고 healthy 를 선언해 integrity 와 모순되던 것 해소(두 명령 단일 진실).
5885
+ // (검수 #5) 임계값·H1 판정을 재구현하면 두 명령이 갈린다(실측: 10KB→520B 문서를 integrity 는 truncated, audit 은 무탐).
5886
+ // integrityCmd 와 **동일한 단일 판정기**를 쓴다 — 여기가 유일한 구현이고 integrityCmd 도 이 함수를 호출한다.
5887
+ function _integrityFindings(root) {
5888
+ root = absRoot(root);
5889
+ if (!exists(path.join(root, '.harness'))) return [];
5890
+ let minimal = false;
5891
+ try { minimal = !!JSON.parse(read(path.join(root, '.harness', 'manifest.json'))).minimal; } catch {}
5892
+ const lang = detectLanguageValue(root, 'auto');
5893
+ const templates = coreFiles(root, lang, [], { minimal });
5894
+ const out = [];
5895
+ for (const key of _INTEGRITY_DOCS.filter(k => typeof templates[k] === 'string')) {
5896
+ const tmpl = templates[key];
5897
+ const fp = path.join(root, key);
5898
+ let kind = null;
5899
+ if (!exists(fp)) kind = 'missing';
5900
+ else {
5901
+ let cur = ''; try { cur = read(fp); } catch { kind = 'unreadable'; }
5902
+ if (!kind) {
5903
+ // 코드펜스 안의 `# 주석`을 H1 로 오인하지 않도록 펜스 밖 라인만 검사 (integrityCmd 와 동일 규칙)
5904
+ let _fence = false;
5905
+ const _hasH1 = cur.split('\n').some(l => {
5906
+ const t = l.trim();
5907
+ if (t.startsWith('```')) { _fence = !_fence; return false; }
5908
+ return !_fence && /^# \S/.test(t);
5909
+ });
5910
+ if (!_hasH1) kind = 'h1_lost';
5911
+ else if (cur.length < tmpl.length * 0.4) kind = 'truncated';
5912
+ }
5913
+ }
5914
+ if (kind) out.push({ file: key, kind, templateBytes: Buffer.byteLength(tmpl, 'utf8') });
5915
+ }
5916
+ return out;
5917
+ }
5883
5918
  function integrityCmd(root) {
5884
5919
  root = absRoot(root);
5885
5920
  const json = has('--json');
@@ -8541,7 +8576,7 @@ function _autoFixIdempotency(root) {
8541
8576
  kept.push(r);
8542
8577
  }
8543
8578
  if (removedExact > 0 || droppedSameText > 0) {
8544
- writeProgressRows(root, header, kept);
8579
+ writeProgressRows(root, header, kept, { expectedShrink: removedExact }); // 1.36.79 (검수 #1): 제거한 만큼만 축소 허용 — 손상 행은 여전히 차단
8545
8580
  fixed.push({ kind: 'task-duplicate-request', action: 'deduped', removedExact, droppedSameText });
8546
8581
  }
8547
8582
  }
@@ -9210,11 +9245,19 @@ function nextId(root, prefix) {
9210
9245
  return `${prefix}-${String(max + 1).padStart(4, '0')}`;
9211
9246
  }
9212
9247
 
9248
+ // 1.36.79 (도그푸딩 P1-A, 실 프로젝트 데이터 손실): ID 접두를 T/M/D 로만 인정해 R-/UR- 등 다른 접두 행이
9249
+ // 파싱에서 탈락 → writeProgressRows 가 파싱된 행만으로 파일을 재작성하며 **사용자 이력을 조용히 삭제**했다
9250
+ // (실측: auto-Trading 9행 → 1행, 942B 소실, exit 0 무경고). migrate 만이 아니라 task add 등 모든 쓰기 경로 공통.
9251
+ // 접두를 일반화(대문자 1~3자)해 왕복 보존.
9252
+ // (검수 #2) 다만 `[A-Z]{1,3}` 전면 개방은 과했다 — 사용자가 트래커에 붙여 둔 무관한 표(`| API-123 | owner | ... |`)까지
9253
+ // task 로 흡수해 task list 에 노출·정규화됐다(실측). leerness 가 실제 발급하는 **등록 네임스페이스만** 인정한다.
9254
+ const _PROGRESS_ID_NS = ['T', 'M', 'D', 'R', 'UR', 'F', 'P'];
9255
+ const _PROGRESS_ROW_RE = new RegExp('^\\|\\s*(?:' + _PROGRESS_ID_NS.join('|') + ')-\\d{3,}\\s*\\|');
9213
9256
  function readProgressRows(root) {
9214
9257
  const text = exists(progressPath(root)) ? read(progressPath(root)) : '';
9215
9258
  const rows = [];
9216
9259
  for (const line of text.split('\n')) {
9217
- if (!/^\| (?:T|M|D)-\d{4,} \|/.test(line)) continue;
9260
+ if (!_PROGRESS_ROW_RE.test(line)) continue;
9218
9261
  // 1.9.399 (7번째 버그헌트 P1-A, UR-0104): 비이스케이프 파이프에서만 분리 + 셀 복원 — 사용자 텍스트의 '|'(이스케이프됨)이 컬럼을 깨지 않음.
9219
9262
  const cells = line.split(/(?<!\\)\|/).slice(1, -1).map(s => _cellUnescape(s).trim());
9220
9263
  if (cells.length < 6) continue;
@@ -9245,11 +9288,54 @@ function progressHeader(root) {
9245
9288
  }
9246
9289
  return text.slice(0, text.indexOf('\n', idx)).trimEnd();
9247
9290
  }
9248
- function writeProgressRows(root, header, rows) {
9291
+ function writeProgressRows(root, header, rows, opts = {}) {
9292
+ // 1.36.79 (도그푸딩 P1-A 클래스 가드): 파서가 놓친 행이 재작성으로 사라지는 사고를 규칙 하나가 아니라 **불변식**으로 차단 —
9293
+ // 디스크의 행-모양 라인 수가 쓰려는 행 수보다 많으면 쓰기를 중단한다(향후 파서 강화/CRLF/셀부족 등 같은 클래스 전부 포착).
9294
+ // 단 **의도적 행 감소**(audit --fix 의 완전중복 dedup)는 opts.allowShrink 로 명시 옵트인 — 오탐으로 정당한 정리를 막지 않는다.
9295
+ // 판정 기준은 "변이 후 행 수"가 아니라 **디스크에 파서가 못 읽는 행-모양 라인이 있는가** — upsert 가 행을 추가하면
9296
+ // 개수 비교는 손실을 상쇄해 무력화된다(실측: 디스크 2 vs 쓰기 2 로 통과, 셀부족 행 소실).
9297
+ // (검수 #1) 의도적 축소는 boolean 우회가 아니라 **예상 감소량**으로 — allowShrink:true 는 가드를 통째로 꺼서
9298
+ // dedup 중 손상 행까지 조용히 삭제했다. expectedShrink 만큼만 허용하고 손상 행 존재는 여전히 차단한다.
9299
+ try {
9300
+ const _cur = exists(progressPath(root)) ? read(progressPath(root)) : '';
9301
+ if (_cur) {
9302
+ let _shaped = 0, _parsable = 0;
9303
+ for (const line of _cur.split('\n')) {
9304
+ if (!_PROGRESS_ROW_RE.test(line)) continue;
9305
+ _shaped++;
9306
+ if (line.split(/(?<!\\)\|/).slice(1, -1).length >= 6) _parsable++;
9307
+ }
9308
+ if (_parsable < _shaped) {
9309
+ const _msg = `progress-tracker.md 행 손실 위험 — 행-모양 ${_shaped}건 중 ${_parsable}건만 파싱 가능(셀 부족/형식 이상). ` +
9310
+ `쓰면 나머지 ${_shaped - _parsable}행이 사라지므로 중단합니다(데이터 보호): ${progressPath(root)} 의 해당 행을 6칸으로 고치거나 .harness/archive 백업 복원 후 재시도`;
9311
+ throw Object.assign(new Error(_msg), { code: 'E_PROGRESS_ROW_LOSS' });
9312
+ }
9313
+ const _expect = Number.isInteger(opts.expectedShrink) ? Math.max(0, opts.expectedShrink) : 0;
9314
+ if (_parsable - _expect > rows.length) {
9315
+ const _msg = `progress-tracker.md 행 손실 위험 — 파싱 ${_parsable}행 − 예상감소 ${_expect} > 쓰려는 ${rows.length}행. 쓰기 중단(데이터 보호): ${progressPath(root)}`;
9316
+ throw Object.assign(new Error(_msg), { code: 'E_PROGRESS_ROW_LOSS' });
9317
+ }
9318
+ }
9319
+ } catch (e) { if (e && e.code === 'E_PROGRESS_ROW_LOSS') throw e; }
9249
9320
  // 1.9.399 (7번째 버그헌트 P1-A, UR-0104): 셀 콘텐츠 안전화 — 개행→공백, '|'→'\|'. 사용자 텍스트의 파이프/개행이 표 행을 손상/주입 못 함.
9321
+ // 1.36.79 (검수 #2 후반): 표 **뒤에 사용자가 쓴 내용**(참조표·메모·`Status values:` 푸터)이 재작성 때 통째로 사라지던 것 —
9322
+ // 마지막 task 행 이후의 비-행 콘텐츠를 그대로 이어붙여 보존한다.
9323
+ let _suffix = '';
9324
+ try {
9325
+ const _cur0 = exists(progressPath(root)) ? read(progressPath(root)) : '';
9326
+ if (_cur0) {
9327
+ const _lines = _cur0.split('\n');
9328
+ let _lastRow = -1;
9329
+ for (let i = 0; i < _lines.length; i++) if (_PROGRESS_ROW_RE.test(_lines[i])) _lastRow = i;
9330
+ if (_lastRow >= 0) {
9331
+ const _tail = _lines.slice(_lastRow + 1).join('\n').replace(/^\n+/, '');
9332
+ if (_tail.trim()) _suffix = '\n' + _tail.replace(/\s+$/, '') + '\n';
9333
+ }
9334
+ }
9335
+ } catch {}
9250
9336
  const composed = header + '\n' +
9251
9337
  rows.map(r => `| ${_cellSafe(r.id)} | ${_cellSafe(r.status)} | ${_cellSafe(r.request)} | ${_cellSafe(r.evidence)} | ${_cellSafe(r.nextAction)} | ${_cellSafe(r.updated)} |`).join('\n') +
9252
- (rows.length ? '\n' : '');
9338
+ (rows.length ? '\n' : '') + _suffix;
9253
9339
  writeUtf8(progressPath(root), composed);
9254
9340
  }
9255
9341
  function upsertProgress(root, row) {
@@ -10295,7 +10381,7 @@ function debug(root) {
10295
10381
 
10296
10382
  const _audit = require('../lib/audit');
10297
10383
  // 1.9.421 (UR-0025/UR-0125 큰 핸들러 모듈화 6번째): audit → lib/audit.js (DI 위임, thin wrapper)
10298
- function audit(root, opts = {}) { return _audit.audit(root, opts, { VERSION, arg, has, planPath, readProgressRows, currentStatePath, handoffPath, envDiff, _readFeatureGraph, _matchAPISkills, _listAPISkills, _collectSecretFindings }); }
10384
+ function audit(root, opts = {}) { return _audit.audit(root, opts, { VERSION, arg, has, planPath, readProgressRows, currentStatePath, handoffPath, envDiff, _readFeatureGraph, _matchAPISkills, _listAPISkills, _collectSecretFindings, _integrityFindings }); } // 1.36.79: integrity 신호 공유
10299
10385
 
10300
10386
  // 1.9.312 (UR-0050, 설치리뷰 3중수렴): secret 스캐너 현대 키 패턴 보강.
10301
10387
  // 배경: 기존 OpenAI 패턴 `sk-[A-Za-z0-9]{32,}` 은 하이픈에서 끊겨 sk-proj-/sk-svcacct- (modern 프로젝트/서비스 키)를 놓침.
@@ -12440,7 +12526,10 @@ function handoffCmd(root) {
12440
12526
  // 1.9.38 (A): drift 임계 시 .harness/agent-reminders.md 자동 생성 — 메인 에이전트 프롬프트에 표시되도록.
12441
12527
  // 1.9.38 (D): skip 횟수 학습 — --no-drift-check 빈도 ≥5 시 임계 완화 (1d → 2d).
12442
12528
  const absR0 = absRoot(root || process.cwd());
12443
- if (exists(path.join(absR0, '.harness')) && process.env.LEERNESS_NO_DRIFT_CHECK !== '1') {
12529
+ // 1.36.79 (도그푸딩 P1-B, 실 프로젝트 27/37 해당): 사람용 drift 배너가 --json 모드에서도 stdout 에 찍혀
12530
+ // handoff --json 등의 응답을 통째로 파싱 불가로 만들었다(실측: JSON.parse 실패). stale 프로젝트에서만 발화해
12531
+ // 합성 픽스처 헌트가 못 봤다. --json 이면 배너 억제(구조화 필드/텍스트 모드로만 노출).
12532
+ if (exists(path.join(absR0, '.harness')) && process.env.LEERNESS_NO_DRIFT_CHECK !== '1' && !has('--json')) {
12444
12533
  // skip 카운트
12445
12534
  if (has('--no-drift-check')) {
12446
12535
  try {
@@ -23006,7 +23095,7 @@ SETUP & UPDATE
23006
23095
 
23007
23096
  STATUS & DIAGNOSTICS
23008
23097
  status [path] Install status (files present)
23009
- health [path] | doctor [path] Install/state diagnostics
23098
+ health [path] | doctor [--json] health: project state · doctor: CLI install/environment (no path arg)
23010
23099
  verify [path] Required-file verification
23011
23100
  which [--json] Resolve current binary/version (npm cache conflicts)
23012
23101
  selftest [--json] Core-function integrity self-check (CI-friendly)
@@ -23093,7 +23182,7 @@ function help() {
23093
23182
  leerness rule add "<설명>" --trigger every-session|every-update|every-commit|session-start|session-close|pre-publish # 사용자 룰 등록 (1.9.8)
23094
23183
  leerness rule list|verify|pause <id>|resume <id>|remove <id>|stop|resume-all
23095
23184
  leerness context [path] [--json] # 에이전트 온보딩 컨텍스트 (1.9.292, UR-0065)
23096
- leerness health [path] [--json] · leerness doctor [path] # 설치/상태 진단
23185
+ leerness health [path] [--json] · leerness doctor [--json] # health: 프로젝트 상태 · doctor: CLI 설치/환경(경로 인자 없음)
23097
23186
  leerness pulse [path] · leerness milestones [path] · leerness round-history [path] # 종합 지표
23098
23187
  leerness decision add "<title>" [--reason ...] | decision list|drop <id> # 결정 메모리 (.harness, canonical JSON)
23099
23188
  leerness lesson save "<text>" [--tag t] | lesson list|drop <date|text> # 교훈 메모리
@@ -23421,7 +23510,19 @@ async function main() {
23421
23510
  if (cmd === 'wakeup-interval') return wakeupIntervalCmd(arg('--path', process.cwd()), args[1], args[2]);
23422
23511
  // 1.9.211: leerness workspace-dir <get|guide> — 현재 워크스페이스 디렉토리 / AI 참조 가이드 (사용자 명시)
23423
23512
  if (cmd === 'workspace-dir') return workspaceDirCmd(arg('--path', process.cwd()), args[1]);
23424
- if (cmd === 'parent') return parentCmd(arg('--path', process.cwd()), args[1]);
23513
+ // 1.36.79 (도그푸딩 P1-C): positional 경로(`parent detect <path>`)를 무시하고 cwd 를 분석해 자신만만한 오답(exit 0) 내던 것
23514
+ // — positional path 잔존 클래스(1.36.33)의 미소진 지점. --path 우선, 없으면 sub 다음 positional.
23515
+ // (검수 #3) _taskPositionalPath 는 './' 접두 등 "경로처럼 보이는" 값만 인정해 `parent detect parent\child` 같은 맨 상대경로를
23516
+ // 무시하고 cwd 를 분석했다. parent 는 하위명령 뒤 첫 non-flag 를 경로로 받고, 존재하는 디렉토리인지 먼저 검증한다.
23517
+ if (cmd === 'parent') {
23518
+ const _pPos = args.slice(2).find(a => a && !a.startsWith('-'));
23519
+ const _pRoot = arg('--path', null) || _pPos || process.cwd();
23520
+ const _pAbs = path.resolve(_pRoot);
23521
+ if (_pPos || arg('--path', null)) {
23522
+ if (!exists(_pAbs) || !fs.statSync(_pAbs).isDirectory()) { failJson(has('--json'), 'path_not_found', `경로 없음 또는 디렉토리 아님: ${_pAbs}`); return; }
23523
+ }
23524
+ return parentCmd(_pAbs, args[1]);
23525
+ }
23425
23526
  // 1.9.211: leerness migrate-workspace-dir — .harness → .leerness 마이그레이션 (사용자 명시)
23426
23527
  if (cmd === 'migrate-workspace-dir') return migrateWorkspaceDirCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd()); // 1.36.33: positional path 무시 → cwd 오염 (1.36.21/24 계열 잔존 — 실제로 이 버그로 소스 저장소가 오염될 뻔)
23427
23528
  // 1.9.212: leerness idempotency audit — 멱등성 위반 탐지 (사용자 명시)
package/lib/audit.js CHANGED
@@ -34,6 +34,25 @@ function audit(root, opts = {}, deps = {}) {
34
34
  process.exitCode = 1;
35
35
  return;
36
36
  }
37
+ // 1.36.79 (도그푸딩 P1-E, 실 프로젝트): .harness/AGENTS.md 존재만 보고 통과시켜 **56개 중 44개가 사라진 설치**를
38
+ // healthy:true·failures:0 으로 보고했다(같은 디렉토리에서 integrity check 는 ok:false — 두 판정이 모순).
39
+ // integrity 가 이미 계산하는 관리 문서 부재/손상 신호를 audit 에도 반영해 "가장 먼저 부르는 명령"이 진실을 말하게 한다.
40
+ try {
41
+ if (typeof deps._integrityFindings === 'function') {
42
+ const _integ = deps._integrityFindings(root) || [];
43
+ // (검수 #5) integrity 와 동일 kind 집합 — truncated 도 포함해 두 명령의 판정이 갈리지 않게(단, 차단 등급은 missing 기준)
44
+ const _missing = _integ.filter(f => f && ['missing', 'h1_lost', 'truncated', 'unreadable'].includes(f.kind));
45
+ if (_missing.length) {
46
+ // 코어 문서 다수 부재 = 설치 손상(차단). 소수는 경고.
47
+ const _sev = _missing.filter(f => f.kind === 'missing').length >= 3 ? 'fail' : 'warn';
48
+ if (_sev === 'fail') { failures++; fail(`관리 문서 ${_missing.length}건 부재/손상 — 설치 불완전 (leerness init 또는 integrity check --repair)`); }
49
+ else { warnings++; warn(`관리 문서 ${_missing.length}건 부재/손상 (leerness integrity check --repair)`); }
50
+ _finding('managed_docs_incomplete', _sev, 'managed policy docs missing/damaged (install incomplete)', { count: _missing.length, files: _missing.slice(0, 12).map(f => f.file || f.name), kinds: [...new Set(_missing.map(f => f.kind))] });
51
+ } else {
52
+ ok('관리 문서 무결성 OK (integrity)');
53
+ }
54
+ }
55
+ } catch {}
37
56
  // 1.36.1 (클린룸 리뷰 FN): .harness/*.json 상태파일 JSON 무결성 — 깨진 JSON 을 그레이스풀 폴백(빈 상태)이 "healthy" 로 감추던 false-negative 차단.
38
57
  // 손상 파일을 warning + corrupted_state_json finding 으로 표면화(--strict 시 failure 로 승격). 비-크래시(헬퍼가 파서 예외를 흡수).
39
58
  try {
@@ -58,18 +58,22 @@ function _envKeys(root) {
58
58
 
59
59
  function _shallowExts(root) {
60
60
  const counts = {};
61
+ let _extBudget = 20000; // 1.36.79 (검수 #4): 전체 예산 — 거대 저장소에서도 공정 집계 + 상한 보장
61
62
  const scan = (dir, depth) => {
62
63
  let entries = [];
63
64
  try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
64
- for (const e of entries.slice(0, 400)) {
65
+ // 1.36.79 (검수 #4): 디렉토리당 400개 컷이 큰 플랫 디렉토리에서 **집계를 편향**시켰다(1501 JS vs 500 PY 인데 Python 1위).
66
+ // 순위가 실제 파일 수에 기반하려면 세는 범위가 공정해야 한다 — 상한을 크게(디렉토리당 5000, 전체 예산 20000) 두고 공정 집계.
67
+ for (const e of entries.slice(0, 5000)) {
68
+ if (_extBudget <= 0) return;
65
69
  if (e.name.startsWith('.') || e.name === 'node_modules' || e.name === 'dist' || e.name === 'build') continue;
66
70
  const p = path.join(dir, e.name);
67
71
  if (e.isDirectory()) { if (depth > 0) scan(p, depth - 1); continue; }
68
72
  const ext = path.extname(e.name).toLowerCase();
69
- if (EXT_LANG[ext]) counts[EXT_LANG[ext]] = (counts[EXT_LANG[ext]] || 0) + 1;
73
+ if (EXT_LANG[ext]) { counts[EXT_LANG[ext]] = (counts[EXT_LANG[ext]] || 0) + 1; _extBudget--; }
70
74
  }
71
75
  };
72
- scan(absRoot(root), 2);
76
+ scan(absRoot(root), 3); // 1.36.79 (도그푸딩 P1-D): depth 2→3 — src/pkg/mod 3단 레이아웃의 소스가 통째로 안 세어지던 것
73
77
  return counts;
74
78
  }
75
79
 
@@ -84,6 +88,11 @@ function detectTechProfile(root) {
84
88
  }
85
89
  const extCounts = _shallowExts(root);
86
90
  for (const [lang, n] of Object.entries(extCounts)) if (n >= 3) addL(lang, `${n} source files`);
91
+ // 1.36.79 (도그푸딩 P1-D, 실 프로젝트): 마커 파일 존재만으로 순서가 정해져 **devDependency 1개짜리 package.json 이
92
+ // 205개 .py 파일을 이겨** Python 모노레포가 "javascript" 로 라벨링됐다(실측). 실제 소스 파일 수를 1순위 근거로
93
+ // 재정렬 — 파일이 많은 언어가 앞, 동수면 기존(마커) 순서 유지. 마커-only(0 파일)는 뒤로.
94
+ languages.sort((a, b) => (extCounts[b.id] || 0) - (extCounts[a.id] || 0));
95
+ for (const l of languages) { const n = extCounts[l.id] || 0; if (n >= 3 && !/source files/.test(l.evidence)) l.evidence = `${l.evidence} · ${n} source files`; }
87
96
  // deps 수집 — 1.36.54 (#5): 모노레포 대응, 루트 + 얕은 하위(2단계) 매니페스트 병합 (vendor 류 제외)
88
97
  let deps = {};
89
98
  let reqTxt = '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.36.78",
3
+ "version": "1.36.79",
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
@@ -7619,5 +7619,92 @@ total++;
7619
7619
  if (!ok) failed++;
7620
7620
  }
7621
7621
 
7622
+ // 1.36.79 (실 프로젝트 60개 도그푸딩 P1 5건): progress 행 보존 · handoff --json 순수 · parent path · tech 언어 순위 · audit=integrity 정합
7623
+ total++;
7624
+ {
7625
+ let ok = false;
7626
+ const _d = [];
7627
+ try {
7628
+ // P1-A: 비-T/M/D 행(R-/UR-)이 쓰기 경로에서 소실되면 안 된다 + 클래스 가드
7629
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-dog-a-')); _d.push(d);
7630
+ cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--minimal', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
7631
+ const pt = path.join(d, '.harness', 'progress-tracker.md');
7632
+ fs.writeFileSync(pt, fs.readFileSync(pt, 'utf8').trimEnd() + '\n| R-0001 | done | 사용자 이력 | 증거 | - | 2026-05-07 |\n| UR-0002 | done | 다른 요청 | 증거2 | - | 2026-05-07 |\n');
7633
+ const rowsOf = () => (fs.readFileSync(pt, 'utf8').match(/^\|\s*[A-Z]{1,3}-\d{3,}\s*\|/gm) || []).length;
7634
+ const beforeRows = rowsOf();
7635
+ cp.spawnSync(process.execPath, [CLI, 'task', 'add', 'brand new', '--path', d], { encoding: 'utf8', timeout: 20000 });
7636
+ const rowOk = rowsOf() === beforeRows + 1
7637
+ && fs.readFileSync(pt, 'utf8').includes('R-0001') && fs.readFileSync(pt, 'utf8').includes('UR-0002');
7638
+ // 클래스 가드: 파서가 못 읽는 행-모양 라인(셀 부족)이 있으면 쓰기 중단 + 원본 보존 (자체 재설계 후 실동작)
7639
+ const dG = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-dog-a2-')); _d.push(dG);
7640
+ cp.spawnSync(process.execPath, [CLI, 'init', dG, '--yes', '--minimal', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
7641
+ const ptG = path.join(dG, '.harness', 'progress-tracker.md');
7642
+ fs.appendFileSync(ptG, '| R-0007 | done | 셀부족행 |\n');
7643
+ const beforeG = fs.readFileSync(ptG, 'utf8');
7644
+ const guardR = cp.spawnSync(process.execPath, [CLI, 'task', 'add', 'x', '--path', dG], { encoding: 'utf8', timeout: 20000 });
7645
+ const guardOk = guardR.status === 1 && fs.readFileSync(ptG, 'utf8') === beforeG;
7646
+ // (검수 #2) 무관한 참조표는 task 로 흡수되지 않고, 표 뒤 사용자 콘텐츠는 재작성에도 보존
7647
+ const dS = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-dog-a3-')); _d.push(dS);
7648
+ cp.spawnSync(process.execPath, [CLI, 'init', dS, '--yes', '--minimal', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
7649
+ const ptS = path.join(dS, '.harness', 'progress-tracker.md');
7650
+ fs.appendFileSync(ptS, '\n## 참고표\n\n| Code | Owner | Note |\n|---|---|---|\n| API-123 | owner | 무관 참조 |\n');
7651
+ let absorbed = true;
7652
+ try { absorbed = (JSON.parse(cp.spawnSync(process.execPath, [CLI, 'task', 'list', '--path', dS, '--json'], { encoding: 'utf8', timeout: 20000 }).stdout).tasks || []).some(t => t.id === 'API-123'); } catch {}
7653
+ cp.spawnSync(process.execPath, [CLI, 'task', 'add', 'x', '--path', dS], { encoding: 'utf8', timeout: 20000 });
7654
+ const suffixOk = !absorbed && fs.readFileSync(ptS, 'utf8').includes('| API-123 | owner |') && fs.readFileSync(ptS, 'utf8').includes('## 참고표');
7655
+ // P1-B: stale 프로젝트에서도 handoff --json 은 순수 JSON (사람용 drift 배너 미오염)
7656
+ const d2 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-dog-b-')); _d.push(d2);
7657
+ cp.spawnSync(process.execPath, [CLI, 'init', d2, '--yes', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
7658
+ const hp = path.join(d2, '.harness', 'session-handoff.md');
7659
+ if (fs.existsSync(hp)) fs.writeFileSync(hp, fs.readFileSync(hp, 'utf8').replace(/Last generated:.*/, 'Last generated: 2026-01-01T00:00:00.000Z'));
7660
+ const pt2 = path.join(d2, '.harness', 'progress-tracker.md');
7661
+ if (fs.existsSync(pt2)) fs.writeFileSync(pt2, fs.readFileSync(pt2, 'utf8').replace(/2026-\d\d-\d\d/g, '2026-01-01'));
7662
+ const hj = cp.spawnSync(process.execPath, [CLI, 'handoff', d2, '--json'], { encoding: 'utf8', timeout: 40000, maxBuffer: 32 * 1024 * 1024 });
7663
+ let jsonPure = false; try { JSON.parse(hj.stdout); jsonPure = true; } catch {}
7664
+ // 텍스트 모드에서는 배너 유지(무회귀)
7665
+ const ht = cp.spawnSync(process.execPath, [CLI, 'handoff', d2], { encoding: 'utf8', timeout: 40000, maxBuffer: 32 * 1024 * 1024 });
7666
+ const bannerKept = /drift 감지|drift detected/.test(ht.stdout);
7667
+ // P1-C: parent detect 가 positional path 를 존중
7668
+ const d3 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-dog-c-')); _d.push(d3);
7669
+ cp.spawnSync(process.execPath, [CLI, 'init', d3, '--yes', '--minimal', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
7670
+ let parentOk = false;
7671
+ try { const j = JSON.parse(cp.spawnSync(process.execPath, [CLI, 'parent', 'detect', d3, '--json'], { encoding: 'utf8', timeout: 20000, cwd: path.dirname(path.dirname(CLI)) }).stdout); parentOk = String(j.root || j.child || '').includes(path.basename(d3)); } catch {}
7672
+ // P1-D: 소스 파일 수가 마커 파일보다 우선 (Python 모노레포 오라벨 방지) + 진짜 JS 무회귀
7673
+ const d4 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-dog-d-')); _d.push(d4);
7674
+ cp.spawnSync(process.execPath, [CLI, 'init', d4, '--yes', '--minimal', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
7675
+ fs.mkdirSync(path.join(d4, 'src'), { recursive: true });
7676
+ for (let i = 0; i < 30; i++) fs.writeFileSync(path.join(d4, 'src', `m${i}.py`), 'def f():\n return 1\n');
7677
+ fs.writeFileSync(path.join(d4, 'requirements.txt'), 'requests\n');
7678
+ fs.writeFileSync(path.join(d4, 'package.json'), JSON.stringify({ name: 'x', devDependencies: { tailwindcss: '^3' } }));
7679
+ let techOk = false;
7680
+ try { techOk = JSON.parse(cp.spawnSync(process.execPath, [CLI, 'tech', '--json', '--path', d4], { encoding: 'utf8', timeout: 25000 }).stdout).current.languages[0].id === 'python'; } catch {}
7681
+ const d5 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-dog-d2-')); _d.push(d5);
7682
+ cp.spawnSync(process.execPath, [CLI, 'init', d5, '--yes', '--minimal', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
7683
+ fs.mkdirSync(path.join(d5, 'src'), { recursive: true });
7684
+ for (let i = 0; i < 20; i++) fs.writeFileSync(path.join(d5, 'src', `m${i}.js`), 'export const a = 1;\n');
7685
+ fs.writeFileSync(path.join(d5, 'package.json'), JSON.stringify({ name: 'js', dependencies: { react: '^18' } }));
7686
+ let jsOk = false;
7687
+ try { jsOk = JSON.parse(cp.spawnSync(process.execPath, [CLI, 'tech', '--json', '--path', d5], { encoding: 'utf8', timeout: 25000 }).stdout).current.languages[0].id === 'javascript'; } catch {}
7688
+ // P1-E: 코어 문서가 대량 사라진 설치를 audit 이 healthy 로 보고하면 안 된다(integrity 와 정합)
7689
+ const d6 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-dog-e-')); _d.push(d6);
7690
+ cp.spawnSync(process.execPath, [CLI, 'init', d6, '--yes', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
7691
+ const hd = path.join(d6, '.harness');
7692
+ const keep = new Set(['progress-tracker.md', 'HARNESS_VERSION', 'plan.md']);
7693
+ for (const f of fs.readdirSync(hd)) { if (!keep.has(f)) { try { fs.rmSync(path.join(hd, f), { recursive: true, force: true }); } catch {} } }
7694
+ let auditOk = false;
7695
+ try {
7696
+ const au = JSON.parse(cp.spawnSync(process.execPath, [CLI, 'audit', d6, '--json'], { encoding: 'utf8', timeout: 30000 }).stdout);
7697
+ const ic = JSON.parse(cp.spawnSync(process.execPath, [CLI, 'integrity', 'check', d6, '--json'], { encoding: 'utf8', timeout: 30000 }).stdout);
7698
+ auditOk = au.healthy === false && au.failures >= 1 && ic.ok === false; // 두 명령 판정 일치
7699
+ } catch {}
7700
+ // (검수 #3) parent 없는 경로 거부
7701
+ const parentBadOk = cp.spawnSync(process.execPath, [CLI, 'parent', 'detect', 'C:/no/such/dir', '--json'], { encoding: 'utf8', timeout: 20000 }).status === 1;
7702
+ ok = rowOk && guardOk && suffixOk && jsonPure && bannerKept && parentOk && parentBadOk && techOk && jsOk && auditOk;
7703
+ if (!ok) console.log(` [dogfood 디버그] row=${rowOk} guard=${guardOk} suffix=${suffixOk} json=${jsonPure} banner=${bannerKept} parent=${parentOk} parentBad=${parentBadOk} tech=${techOk} js=${jsOk} audit=${auditOk}`);
7704
+ } catch (e) {} finally { _d.forEach(x => { try { fs.rmSync(x, { recursive: true, force: true }); } catch {} }); }
7705
+ console.log(ok ? '✓ B(1.36.79) 도그푸딩 P1×5: progress 행 보존 · handoff --json 순수(텍스트 배너 유지) · parent path · tech 파일수 우선 · audit=integrity 정합' : '✗ 도그푸딩 P1 수정 실패');
7706
+ if (!ok) failed++;
7707
+ }
7708
+
7622
7709
  console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
7623
7710
  if (failed > 0) process.exit(1);