leerness 1.35.6 → 1.35.8

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,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.35.8 — 2026-07-03 — GPT5.5평가 잔여 제안 채택: 문서 drift 가드 + 전용 스캐너 병행 레시피 + 테스트 티어 안내 (UR-0017)
4
+
5
+ **GPT 5.5 Pro 평가 후속 라운드** — 1.35.7 에서 확정버그 3건을 수정했고, 이번엔 잔여 개선제안 중 저위험·고정직성 항목을 채택. 나머지(e2e 3-tier 분리 / minimal lazy-create / AST 검증기)는 UR-0014~0016 으로 백로그 등록.
6
+
7
+ ### 채택 (제안⑤⑥ + ③문서파트)
8
+ - **문서 숫자 drift 자동가드 (제안⑤)**: selftest 가 README managed 섹션의 MCP 도구 수(`**N개 도구**`)를 `lib/mcp-tools` 실제 정의 수와 대조 — 사이트가 85↔86 으로 drift 했던 계열의 재발을 릴리스 게이트에서 차단.
9
+ - **전용 스캐너 병행 레시피 (제안⑥)**: README "Guidance vs enforcement" 에 gitleaks-action + leerness scan secrets 병행 워크플로 스니펫 추가. `scan secrets` human 출력에도 포지셔닝 정직 고지 1줄(편의 가드 — CI급 보장은 전용 스캐너 병행, ko/en) — README 만으론 CLI 사용자에게 닿지 않던 갭.
10
+ - **테스트 티어 안내 (제안③ 문서파트)**: README Maturity 에 npm test = 풀 릴리스 게이트(10분+, 의도된 설계) vs test:fast = 개발 루프(1분 미만) 명시. 스크립트 분리 자체는 UR-0014 별도 라운드.
11
+
12
+ ### 검증
13
+ - selftest **268** (신규: README 도구 수 정합 + 레시피/고지/티어 문서 존재). full e2e (scan secrets 출력 1줄 추가 — 기존 단언과 충돌 없음 확인).
14
+
15
+ ## 1.35.7 — 2026-07-03 — 19th GPT5.5pro 평가: verify-claim declared-pass 부풀림 게이팅 + spec 리포터 파싱 + json ok/reasons (UR-0013)
16
+
17
+ **웹 GPT 5.5 Pro 확장의 독립 평가보고서(1.35.6 게시본 대상) 검토 라운드**. 맹신 X 재현 결과 보고서의 헤드라인 버그가 **3경로 전부 실재** — 원인 분석의 소스 위치까지 정확했음. 검토 중 추가 발견 2건(자기모순 출력 + Node v26 파서 FN)도 함께 수정.
18
+
19
+ ### 확정 버그 (게시본 1.35.6 재현)
20
+ - **declared-pass mismatch 미게이팅 (P1)**: evidence 주장 "3/3 passed" vs 실행 "2/2" 를 감지·표시("⚠ 불일치")하고도 human/`--json`/`--all` 3경로 모두 **exit 0** — 부풀린 완료 주장이 플래그십을 통과. 게다가 human 최종요약은 불일치 출력 직후 "✓ evidence 주장이 실제 파일·테스트·실행 결과와 일치" **자기모순 표기**.
21
+ - **Node v26 파서 FN (추가발견)**: 신형 Node 는 비-TTY 도 spec 리포터가 기본(`ℹ pass 2`) → 기존 5패턴 전부 미매치 → `parsed=null` 로 mismatch 검사 자체가 조용히 스킵.
22
+
23
+ ### 수정
24
+ - **부풀림 게이팅 (`_declaredPassMismatch`)**: 실행 pass < 주장 pass 면 human `overallFail` + `--json` exit + collect(`--all`/`gate --claims`) `declared-pass-mismatch` reason 3경로 FAIL. **방향성 게이트** — 실행 ≥ 주장(테스트가 늘어난 정상 흐름)은 무벌점(testCountMatch 의 "실측≥주장" 규칙과 동일 철학, 1.18.1식 거짓차단 FP 회피). 완화: `--lenient`. 요약 라벨도 "✗ FAIL (주장이 실행 결과보다 부풀려짐)" 로 자기모순 제거.
25
+ - **`_parseTestStdout` 순수 추출 + spec 리포터 패턴 6**: 기존 5패턴(비율/jest/mocha/tap/pytest) 원형 유지 + node:test spec 리포터(`pass N`/`fail N` 라인-전체 앵커, denom = pass+fail) 신규 — 선두 비단어문자만 허용해 타 러너 프로즈 오탐 차단(테스트명 "pass 2 (1 ms)" 미매치 확인).
26
+ - **단건 `--json` top-level `ok`/`reasons` (평가 개선제안 ②)**: CI 가 verdict 필드를 재조합할 필요 없이 collect 경로와 동일 어휘. exit 는 reasons 기반 단일 판정으로 통합 — ok↔exit 불일치 원천 차단(기존 게이트 동일).
27
+
28
+ ### 검증
29
+ - selftest **267** (신규: 파서 5+스펙 행위 + 게이팅 3경로 와이어 + json ok/reasons). full e2e **382** (신규: 부풀림 exit 1 human/json/--all + spec 파싱 + 정직주장/실행≥주장 무벌점 FP 가드).
30
+
3
31
  ## 1.35.6 — 2026-07-02 — 18th 위임실증: codex-leerness 준수 라이브 검증 + dispatch harness 브리프 + bench stdin hang 수정
4
32
 
5
33
  **위임 실증 라운드 (사용자 명시: "codex가 leerness를 참조하고 작업을 진행하는지 확인 후 고도화")**. 라이브 재현: leerness init 된 임시 프로젝트에 위임 룰(codex 구현/Claude 검수) 등록 → **leerness 를 전혀 언급하지 않은** 순수 코딩 태스크를 dispatch 레시피 그대로 codex(0.141)에 위임.
@@ -12,7 +40,7 @@
12
40
  - **agents bench codex stdin hang (확인, 실버그)**: `codex exec` 는 stdin 이 열린 파이프면 `Reading additional input from stdin...` 에서 EOF 를 무한 대기(라이브 재현 — 40바이트 출력으로 8분+ hang). bench 의 `cp.spawn(..., { shell: true })` 는 기본 stdio pipe 로 stdin 을 닫지 않아 codex 가 항상 타임아웃으로 왜곡됨 → `stdio: ['ignore','pipe','pipe']`. dispatch 안내에도 비대화형 spawn 시 stdin 닫기 경고 추가.
13
41
 
14
42
  ### 검증
15
- - selftest **266** (신규: `_harnessBrief` 내용/셸-안전 + dispatch 접두 와이어 + bench stdio 가드). full e2e (dispatch 회귀 무영향기존 테스트는 플래그/거부 단언). patchnpm 미배포(R-0011).
43
+ - selftest **266** (신규: `_harnessBrief` 내용/셸-안전 + dispatch 접두 와이어 + bench stdio 가드). full e2e **381/381** (flag-bleed 테스트는 의도 보존형으로 갱신 기본: 브리프 태스크 비오염, --raw: 1.9.435 원형). **npm 배포됨** (사용자 지시, automation token) 게시본 재실증: selftest 266 + dispatch 브리프/--raw 행위 확인.
16
44
 
17
45
  ## 1.35.5 — 2026-06-27 — 17th 버그헌트: scan .json5/.jsonc FN + verify-claim git 매칭 정밀도
18
46
 
package/README.md CHANGED
@@ -81,12 +81,22 @@ The generated workflow is production-grade: it **pins the leerness version** (re
81
81
 
82
82
  Then make that check **required** in GitHub branch protection. Now a PR that skips verification (or whose claims fail) **cannot merge** — the gate runs independently of the agent, returns a non-zero exit code, and blocks. That is the difference between a guideline and a guardrail. For exact per-claim enforcement, run `leerness gate --claims` — it adds a 6th check that runs `verify-claim` on **every** completed task and fails the gate if any "done" task's evidence doesn't match reality (the default 5-check gate already blocks false-done via heuristics; `--claims` makes it precise).
83
83
 
84
+ For secrets, pair the gate with a **dedicated scanner** in the same workflow — leerness's `scan secrets` is a convenience guard (the same signal your agent sees locally), not a hard guarantee:
85
+
86
+ ```yaml
87
+ # add to .github/workflows/leerness-gate.yml (or a separate job)
88
+ - uses: gitleaks/gitleaks-action@v2 # dedicated scanner — the hard-guarantee layer
89
+ - run: npx leerness@<pinned-version> scan secrets . --json # convenience layer — same check your agent runs locally
90
+ ```
91
+
84
92
  ---
85
93
 
86
94
  ## Maturity — and why trying it is still cheap
87
95
 
88
96
  Be honest with yourself before you depend on this: leerness is **early and largely solo-maintained**, developed mostly through autonomous AI rounds — so its own `selftest` + e2e suites are the primary quality signal, and external adoption is still small. Don't make it load-bearing on faith: **pin a version**, and treat the differentiated slice — `verify-claim` + the CI `gate` as a required check — as the part worth relying on.
89
97
 
98
+ (Contributor note: `npm test` is the full release gate — selftest + the entire e2e suite, **10+ minutes by design**. For a quick dev loop use `npm run test:fast` — selftest + smoke, under a minute.)
99
+
90
100
  The asymmetry is what makes a trial reasonable anyway: MIT, **0 runtime dependencies**, offline-first, and all state is plain files in *your* repo. Lock-in is near zero — if it doesn't earn its place, remove the tool and your `task`/`decision`/`lesson` files stay. (For secret scanning specifically, mature dedicated tools like gitleaks/trufflehog exist — use those if you need a hard guarantee; leerness's `scan secrets` is a convenience guard, not a replacement.)
91
101
 
92
102
  ---
@@ -115,7 +125,7 @@ MIT
115
125
  <!-- leerness:project-readme:start -->
116
126
  ## Leerness Project Harness
117
127
 
118
- 이 프로젝트는 Leerness v1.35.6 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
128
+ 이 프로젝트는 Leerness v1.35.8 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
119
129
 
120
130
  ### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
121
131
 
@@ -169,7 +179,7 @@ leerness memory restore decision <date|title>
169
179
 
170
180
  ### MCP server (외부 AI 통합)
171
181
 
172
- Leerness v1.35.6는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
182
+ Leerness v1.35.8는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
173
183
 
174
184
  ```jsonc
175
185
  // 카테고리별
@@ -190,7 +200,7 @@ Leerness v1.35.6는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code
190
200
  `<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
191
201
  1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) stress-v* 신규 작성 + 누적 회귀 → 4) e2e 219/219 → 5) npm pack + git tag + GitHub release → 6) main 자동 push (1.9.140+) → 7) session close → 8) 다음 라운드 예약.
192
202
 
193
- 현재 누적: **70 라운드 (1.9.40 → 1.35.6)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
203
+ 현재 누적: **70 라운드 (1.9.40 → 1.35.8)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
194
204
 
195
205
  ### 성능 가이드 (1.9.140 측정)
196
206
 
@@ -228,6 +238,6 @@ leerness release pack --close --auto-main-push
228
238
  - `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
229
239
  - `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
230
240
 
231
- Last synced by Leerness v1.35.6: 2026-07-03
241
+ Last synced by Leerness v1.35.8: 2026-07-03
232
242
  <!-- leerness:project-readme:end -->
233
243
 
package/bin/leerness.js CHANGED
@@ -32,7 +32,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
32
32
  // 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
33
33
  const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST, _DEFAULT_PLATFORM_CONSTRAINTS, _DEFAULT_DOMAIN_CATALOG, _TOOL_CATALOG, _LSP_LANG_PATTERNS, OPTIMISM_PATTERNS, BUILT_IN_PERSONAS, STRINGS, BUILTIN_CATALOG, ROADMAP_STATUS_LABEL, ROADMAP_STATUS_COLOR, SECRET_PATTERNS, MERGE_OVERWRITE_FILES, MINIMAL_SKIP_KEYS, REQUIRED_WORKSPACE_FILES, KEYWORD_STOPWORDS, SKILL_CATALOG_PRESETS } = require('../lib/catalogs'); // 1.9.344/368/369 (UR-0025): catalog 분리 · 1.11.4 (UR-0007): _TOOL_CATALOG
34
34
 
35
- const VERSION = '1.35.6';
35
+ const VERSION = '1.35.8';
36
36
 
37
37
  // 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
38
38
  // 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
@@ -2885,11 +2885,42 @@ function _selfTestCases() {
2885
2885
  const stdinFixed = agentsSrc.includes("{ shell: true, stdio: ['ignore', 'pipe', 'pipe'] }");
2886
2886
  return briefOk && wired && stdinFixed;
2887
2887
  } },
2888
+ { name: '19th GPT5.5평가 (UR-0013): _parseTestStdout 러너 인식(jest/tap/spec/비율) + declared-pass 부풀림 3경로 게이팅 + json ok/reasons (1.35.7)', run: () => {
2889
+ // (1) 파서 행위: 기존 4형식 보존 + spec 리포터(패턴 6) 신규 — denom = pass + fail.
2890
+ const p = _parseTestStdout;
2891
+ const tap = p('# tests 2\n# pass 2\n# fail 0');
2892
+ const spec = p('ℹ tests 2\nℹ suites 0\nℹ pass 2\nℹ fail 0\n');
2893
+ const specFail = p('ℹ pass 3\nℹ fail 1\n');
2894
+ const jest = p('Tests: 5 passed, 6 total');
2895
+ const ratio = p('12/12 passed');
2896
+ const parseOk = tap && tap.num === 2 && tap.denom === 2 && spec && spec.num === 2 && spec.denom === 2
2897
+ && specFail && specFail.num === 3 && specFail.denom === 4 && jest && jest.num === 5 && jest.denom === 6
2898
+ && ratio && ratio.num === 12 && p('no test output here') === null && p(' ✓ pass 2 (1 ms) extra') === null;
2899
+ // (2) 게이팅 와이어: 3경로(human overallFail / json reasons / collect reasons) + 방향성(실행 pass < 주장 pass 만 FAIL).
2900
+ const src = read(__filename);
2901
+ const gateWired = /overallFail = [^\n]*_declaredPassMismatch/.test(src)
2902
+ && (src.match(/reasons\.push\('declared-pass-mismatch'\)/g) || []).length === 2
2903
+ && src.includes('runResult.parsed.num < declaredPass.num');
2904
+ // (3) 단건 --json top-level ok/reasons — exit 는 reasons 기반 단일 판정.
2905
+ const jsonOk = src.includes('out.ok = out.reasons.length === 0') && src.includes('if (!out.ok) return process.exit(1)');
2906
+ return parseOk && gateWired && jsonOk;
2907
+ } },
2908
+ { name: '19th GPT5.5평가 제안⑤⑥ (UR-0017): README MCP 도구 수 ↔ lib/mcp-tools 정합(문서 drift 가드) + 전용 스캐너 병행 레시피/포지셔닝/테스트 티어 문서화 (1.35.8)', run: () => {
2909
+ // (⑤) 문서 숫자 drift: README managed 섹션의 "**N개 도구**" 가 실제 MCP 도구 정의 수와 일치해야 함 (사이트 85↔86 drift 재발 방지 계열).
2910
+ const readme = read(path.join(__dirname, '..', 'README.md'));
2911
+ const m = readme.match(/\*\*(\d+)개 도구\*\*/);
2912
+ const tools = require('../lib/mcp-tools').length;
2913
+ const managedOk = !!m && parseInt(m[1], 10) === tools;
2914
+ // (⑥) 전용 스캐너 병행 레시피(README) + scan secrets 출력 포지셔닝 고지 + 테스트 티어 안내.
2915
+ const recipeOk = /gitleaks\/gitleaks-action/.test(readme) && /test:fast/.test(readme);
2916
+ const advisoryOk = /pair with a dedicated scanner \(gitleaks\/trufflehog\)/.test(read(__filename));
2917
+ return managedOk && recipeOk && advisoryOk;
2918
+ } },
2888
2919
  { name: 'honesty-check: AI 인식론적 정직성 3차원 + MCP/CLI/strict 통합 (사용자명시 1.9.305)', run: () => { const h = _epistemicHonestyCheck; const d1 = h('이 기능은 항상 정상 동작합니다').findings.some(f => f.dim === 'pretend-knowledge'); const d2 = h('아마 될 것 같습니다. 구현 완료했습니다').findings.some(f => f.dim === 'premature-judgment'); const d3 = h('이 API 의 rate limit 은 초당 5회입니다').findings.some(f => f.dim === 'no-info-gathering'); const clean = h('src/api.js 수정, 12/12 통과 (Exit: 0)').ok === true; const src = read(__filename); const wired = require('../lib/mcp-tools').some(t => t.name === 'leerness_honesty_check') && /if \(cmd === 'honesty-check'\)/.test(src) && /honestyFindings = _epistemicHonestyCheck/.test(src); return d1 && d2 && d3 && clean && wired; } },
2889
2920
  { name: 'exit code 일관성: fail()→exitCode 1 행위 + unknown 명령 안내 (UR-0045 / CV-5 행위화 1.9.366)', run: () => { if (typeof fail !== 'function') return false; const saved = process.exitCode; const _w = process.stdout.write; let setOk = false; try { process.stdout.write = () => true; process.exitCode = 0; fail('selftest probe'); setOk = process.exitCode === 1; } finally { process.stdout.write = _w; process.exitCode = saved; } const src = read(__filename); const dispatchOk = /알 수 없는 명령: \$\{cmd\}/.test(src); return setOk && dispatchOk; } },
2890
2921
  { name: 'brief: 프로젝트 청사진 set/show/export + README 개요 섹션 (UR-0055 사용자명시 1.9.307)', run: () => { const src = read(__filename); const fnOk = typeof briefCmd === 'function' && typeof _loadBrief === 'function' && typeof _briefBlueprint === 'function' && _BRIEF_FIELDS.length === 10; const b = { project: 'X', intro: 'i', purpose: 'p', problem: '', features: ['f1', 'f2'], stack: ['s'], architecture: '', users: [], success: [], nonGoals: [], currentState: '' }; const bp = _briefBlueprint(b, VERSION); const bpOk = /Blueprint/.test(bp) && /소개 \(Intro\)/.test(bp) && /f1/.test(bp) && /신규 프로젝트 시작 가이드/.test(bp); const rb = _briefReadmeBlock(b); const rbOk = rb.includes(BRIEF_START) && rb.includes(BRIEF_END) && /프로젝트 개요/.test(rb) && /\*\*목적\*\*/.test(rb); return fnOk && bpOk && rbOk && /if \(cmd === 'brief'\)/.test(src); } },
2891
2922
  { name: 'brief 2단계: update --direction 이력 + MCP leerness_brief + context 통합 (UR-0055 1.9.308)', run: () => { const src = read(__filename); const b = { project: 'X', intro: '', purpose: '', problem: '', features: [], stack: [], architecture: '', users: [], success: [], nonGoals: [], currentState: '', directionHistory: ['2026-06-04: 확대'] }; const bpOk = /개발 방향 이력/.test(_briefBlueprint(b, VERSION)) && /최근 개발 방향 변경/.test(_briefReadmeBlock(b)); const histWired = /sub === 'update'/.test(src) && /brief\.directionHistory \|\| \[\]\), `\$\{today\(\)\}/.test(src); const mcpOk = require('../lib/mcp-tools').some(t => t.name === 'leerness_brief'); const ctxOk = /brief: \{ intro:/.test(src); return bpOk && histWired && mcpOk && ctxOk; } },
2892
- { name: 'verify-claim: done 주장 evidence 기본강제 + --lenient + MCP/json 도달 (UR-0048 설치리뷰 critical 1.9.309)', run: () => { const src = read(__filename); const def = /const mustHaveEvidence = !has\('--lenient'\) && \(isDoneClaim \|\| has\('--require-evidence'\)\)/.test(src); const threshold = /has\('--require-evidence'\) \? evq\.ok : \(evq\.hasFile \|\| evq\.hasTest \|\| evq\.hasLog\)/.test(src); const jsonWired = /evidenceComplete:/.test(src) && /!evidenceQualityOk[^\n]*\) return process\.exit\(1\)/.test(src); const mcpLenient = !!require('../lib/mcp-tools').find(t => t.name === 'leerness_verify_claim').inputSchema.properties.lenient; return def && threshold && jsonWired && mcpLenient; } },
2923
+ { name: 'verify-claim: done 주장 evidence 기본강제 + --lenient + MCP/json 도달 (UR-0048 설치리뷰 critical 1.9.309)', run: () => { const src = read(__filename); const def = /const mustHaveEvidence = !has\('--lenient'\) && \(isDoneClaim \|\| has\('--require-evidence'\)\)/.test(src); const threshold = /has\('--require-evidence'\) \? evq\.ok : \(evq\.hasFile \|\| evq\.hasTest \|\| evq\.hasLog\)/.test(src); const jsonWired = /evidenceComplete:/.test(src) && /if \(!evidenceQualityOk\) out\.reasons\.push\('evidence-incomplete'\)/.test(src) && /if \(!out\.ok\) return process\.exit\(1\)/.test(src); /* 1.35.7: reasons 기반 단일 exit 재배선(의도 동일 — evidence 게이트가 json exit 도달) */ const mcpLenient = !!require('../lib/mcp-tools').find(t => t.name === 'leerness_verify_claim').inputSchema.properties.lenient; return def && threshold && jsonWired && mcpLenient; } },
2893
2924
  { name: '입력 스키마 검증: task status/rule trigger 무효값 거부 + every-round 보존 (UR-0046 설치리뷰 1.9.310)', run: () => { const src = read(__filename); const sets = TASK_STATUSES.has('done') && TASK_STATUSES.has('in-progress') && !TASK_STATUSES.has('nonsense') && RULE_TRIGGERS.has('every-round') && RULE_TRIGGERS.has('every-update') && !RULE_TRIGGERS.has('not-a-trigger'); const helper = typeof _validateChoice === 'function' && _validateChoice('done', TASK_STATUSES, 'x') === true; const wired = /_validateChoice\(arg\('--status', null\), TASK_STATUSES/.test(src) && /_validateChoice\(trigger, RULE_TRIGGERS/.test(src); return sets && helper && wired; } },
2894
2925
  { name: 'init 가드: 미초기화 write 차단 + 다중마커 판별 + --force 우회 (UR-0047 설치리뷰 1.9.311)', run: () => { const src = read(__filename); const fnOk = typeof _isInitialized === 'function' && typeof _requireInit === 'function'; const _fix = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_initfix_')); let liveOk = false; try { fs.writeFileSync(path.join(_fix, 'AGENTS.md'), 'x'); liveOk = _isInitialized(_fix) === true; } finally { try { fs.rmSync(_fix, { recursive: true, force: true }); } catch {} } const emptyOk = _isInitialized(path.join(os.tmpdir(), '__leerness_noinit_marker__')) === false; const wired = ["task add", "task update", "plan add", "decision add", "rule add", "lesson save", "brief set"].every(l => src.includes(`_requireInit(root, '${l}')`)) && !src.includes("_requireInit(root, 'state " + "start')"); const force = /if \(_isInitialized\(root\) \|\| has\('--force'\)\) return true/.test(src); return fnOk && liveOk && emptyOk && wired && force; } },
2895
2926
  { name: 'secret 스캐너 현대 키: OpenAI proj/svcacct·Anthropic api03(_)·GitHub 변종·Stripe·npm 검출 + 오탐 가드 (UR-0050 설치리뷰 1.9.312)', run: () => { const hit = (s) => SECRET_PATTERNS.some(p => { p.re.lastIndex = 0; return p.re.test(s); }); const named = (s, nm) => SECRET_PATTERNS.some(p => { p.re.lastIndex = 0; return p.re.test(s) && p.name === nm; }); const A = 'A'.repeat(40); const projKey = 'sk-' + 'proj-' + A + '_' + A; const svcKey = 'sk-' + 'svcacct-' + A; const antKey = 'sk-' + 'ant-api03-' + A + '_' + A; const ghoKey = 'gho_' + 'a1B2'.repeat(9); const stripeKey = 'sk_' + 'live_' + A; const npmKey = 'npm_' + 'a1B2'.repeat(9); const asiaKey = 'ASIA' + 'ABCD1234EFGH5678'; const legacy = 'sk-' + A; const hits = hit(projKey) && hit(svcKey) && hit(antKey) && hit(ghoKey) && hit(stripeKey) && hit(npmKey) && hit(asiaKey) && hit(legacy); const names = named(projKey, 'OpenAI project/service key') && named(antKey, 'Anthropic API key') && named(stripeKey, 'Stripe secret key') && named(npmKey, 'npm token'); const clean = !hit('const userName = "john' + '_doe_2024";') && !hit('https://example.com/path/to/page'); return hits && names && clean; } },
@@ -8218,6 +8249,10 @@ function scanSecrets(root, opts = {}) {
8218
8249
  if (ignored.length) {
8219
8250
  log(` ⓘ gitignored ${ignored.length}건 (커밋 제외 — 설계상 안전 보관, 비실패): ${[...new Set(ignored.map(f => f.file))].join(', ')}`);
8220
8251
  }
8252
+ // 1.35.8 (UR-0017, GPT5.5평가 제안⑥): 포지셔닝 정직 고지 — 편의 가드임을 출력에서도 명시(README 만으론 CLI 사용자에게 안 닿음).
8253
+ log(_uiLang(root) === 'en'
8254
+ ? ` ⓘ positioning: convenience guard for agent workflows — for CI-grade guarantees pair with a dedicated scanner (gitleaks/trufflehog). Recipe: README "Guidance vs enforcement".`
8255
+ : ` ⓘ 포지셔닝: 에이전트 워크플로용 편의 가드 — CI급 보장이 필요하면 전용 스캐너(gitleaks/trufflehog)와 병행하세요. 레시피: README "Guidance vs enforcement".`);
8221
8256
  }
8222
8257
 
8223
8258
  function encodingCheck(root, opts = {}) {
@@ -10438,6 +10473,37 @@ function _vcImplIsEmpty(body) {
10438
10473
  return residue === ''; // 의미 토큰이 하나도 안 남으면 스텁
10439
10474
  }
10440
10475
 
10476
+ // 1.35.7 (UR-0013, GPT5.5pro 평가): --run-tests stdout 파서 순수 추출 — selftest 단위검증 가능 + spec 리포터 인식.
10477
+ // 패턴 1~5는 1.9.20/UR-0045 원형 그대로, 6(node:test spec 리포터 "pass N"/"fail N" 라인)만 신규.
10478
+ // 추가발견 배경: Node v26 은 비-TTY 에서도 spec 리포터가 기본 → TAP "# pass N" 이 없어 parsed=null,
10479
+ // declared-pass 불일치 검사가 조용히 스킵됐음. 라인-전체 앵커(선두 비단어문자만 허용)로 타 러너 프로즈 오탐 차단.
10480
+ function _parseTestStdout(out) {
10481
+ out = String(out || '');
10482
+ // 1) X/Y passing|passed|pass|통과
10483
+ const m = out.match(/(\d+)\s*\/\s*(\d+)\s*(?:passed|통과|pass|passing)/i);
10484
+ if (m) return { num: parseInt(m[1], 10), denom: parseInt(m[2], 10) };
10485
+ // 2) jest: "Tests: N passed, M total" — 통과 + 총
10486
+ const m2 = out.match(/Tests?:\s*(?:\d+\s*failed,\s*)?(\d+)\s*passed(?:,\s*(\d+)\s*total)?/i);
10487
+ if (m2) return { num: parseInt(m2[1], 10), denom: parseInt(m2[2] || m2[1], 10) };
10488
+ // 3) mocha: "N passing" — 단독 패턴이면 total = passing
10489
+ const m3 = out.match(/^\s*(\d+)\s+passing\b/im);
10490
+ if (m3) return { num: parseInt(m3[1], 10), denom: parseInt(m3[1], 10) };
10491
+ // 4) tap: "# pass N"
10492
+ const m4 = out.match(/#\s*pass\s+(\d+)/i);
10493
+ if (m4) return { num: parseInt(m4[1], 10), denom: parseInt(m4[1], 10) };
10494
+ // 5) pytest: "N passed in 0.12s" (UR-0045 — 파이썬 러너 출력 인식)
10495
+ const m5 = out.match(/(\d+)\s+passed\b/i);
10496
+ if (m5) return { num: parseInt(m5[1], 10), denom: parseInt(m5[1], 10) };
10497
+ // 6) node:test spec 리포터: "ℹ pass 2" / "ℹ fail 0" — denom = pass + fail (fail 라인 없으면 0)
10498
+ const mp6 = out.match(/^[^\w\r\n]*pass\s+(\d+)\s*$/im);
10499
+ if (mp6) {
10500
+ const mf6 = out.match(/^[^\w\r\n]*fail\s+(\d+)\s*$/im);
10501
+ const p6 = parseInt(mp6[1], 10);
10502
+ return { num: p6, denom: p6 + (mf6 ? parseInt(mf6[1], 10) : 0) };
10503
+ }
10504
+ return null;
10505
+ }
10506
+
10441
10507
  function verifyClaimCmd(root, taskId, opts = {}) {
10442
10508
  root = absRoot(root);
10443
10509
  const _j = has('--json'); // 1.9.400 (7번째 버그헌트 P1-B, UR-0105): --json 에러도 구조화
@@ -10578,31 +10644,8 @@ function verifyClaimCmd(root, taskId, opts = {}) {
10578
10644
  runResult = { skipped: true, reason: `테스트 명령 차단(${r.error}) — '${testCmd}' 실행 불가 (불일치 판정 아님). leerness permissions set extended 또는 allowList 에 추가 권장` };
10579
10645
  } else {
10580
10646
  const out = (r.stdout || '') + (r.stderr || '');
10581
- // 1.9.20: 파싱 패턴 확장한국어 + jest/mocha/tap/vitest
10582
- let parsed = null;
10583
- // 1) X/Y passing|passed|pass|통과
10584
- let m = out.match(/(\d+)\s*\/\s*(\d+)\s*(?:passed|통과|pass|passing)/i);
10585
- if (m) parsed = { num: parseInt(m[1], 10), denom: parseInt(m[2], 10) };
10586
- // 2) jest: "Tests: N passed, M total" — 통과 + 총
10587
- if (!parsed) {
10588
- const m2 = out.match(/Tests?:\s*(?:\d+\s*failed,\s*)?(\d+)\s*passed(?:,\s*(\d+)\s*total)?/i);
10589
- if (m2) parsed = { num: parseInt(m2[1], 10), denom: parseInt(m2[2] || m2[1], 10) };
10590
- }
10591
- // 3) mocha: "N passing" — 단독 패턴이면 total = passing
10592
- if (!parsed) {
10593
- const m3 = out.match(/^\s*(\d+)\s+passing\b/im);
10594
- if (m3) parsed = { num: parseInt(m3[1], 10), denom: parseInt(m3[1], 10) };
10595
- }
10596
- // 4) tap: "# pass N" 또는 "ok N"
10597
- if (!parsed) {
10598
- const m4 = out.match(/#\s*pass\s+(\d+)/i);
10599
- if (m4) parsed = { num: parseInt(m4[1], 10), denom: parseInt(m4[1], 10) };
10600
- }
10601
- // 5) pytest: "N passed in 0.12s" (UR-0045 — 파이썬 러너 출력 인식)
10602
- if (!parsed) {
10603
- const m5 = out.match(/(\d+)\s+passed\b/i);
10604
- if (m5) parsed = { num: parseInt(m5[1], 10), denom: parseInt(m5[1], 10) };
10605
- }
10647
+ // 1.9.20 패턴 1~5 + 1.35.7 spec 리포터(패턴 6)_parseTestStdout 순수 함수로 추출 (UR-0013, selftest 단위검증)
10648
+ const parsed = _parseTestStdout(out);
10606
10649
  runResult = {
10607
10650
  skipped: false,
10608
10651
  cmd: testCmd,
@@ -10642,6 +10685,11 @@ function verifyClaimCmd(root, taskId, opts = {}) {
10642
10685
  const gitStrongMismatch = gitApplicable && claimedInGit.length === 0; // 변경 있는데 주장 파일이 git 변경에 전무
10643
10686
  // 1.11.2 (UR-0175): git strongMismatch 는 기본 게이트에서 제외(advisory) — 커밋 후 검증(정상 흐름)에서 커밋된 파일이 working-tree 변경에 없어 false-fail 하므로. --strict-claims 시에만 FAIL 기여. 기본 게이트는 신뢰도 높은 optimism(claimsConsistent)이 담당.
10644
10687
  const gitClaimOk = !(has('--strict-claims') && gitStrongMismatch);
10688
+ // 1.35.7 (UR-0013, GPT5.5pro 평가 확정버그): declared-pass 부풀림 게이팅 — 주장 비율(예: 3/3)의 pass 가 실행 pass(예: 2)보다 크면 FAIL.
10689
+ // 이전: 3경로(human overallFail / --json exit / collect reasons) 모두 불일치를 감지·표시만 하고 exit 0 — 부풀린 주장이 플래그십을 통과 + 최종요약은 "일치 ✓" 자기모순.
10690
+ // 주장 개수("N개")는 이미 게이팅(testCountMatch)되는데 비율 주장("N/M")만 안 되던 내부 비일관성 해소.
10691
+ // 방향성 게이트(실행 pass ≥ 주장 pass 면 통과) — 테스트가 늘어난 정상 흐름을 벌하지 않음(testCountMatch 의 실측≥주장 규칙과 동일 철학). 완화: --lenient.
10692
+ const _declaredPassMismatch = !lenient && !!(runResult && !runResult.skipped && runResult.parsed && declaredPass && runResult.parsed.num < declaredPass.num);
10645
10693
 
10646
10694
  // 1.33.2 (verify-claim --all): 집계 모드는 렌더/exit 없이 verdict 만 반환. 게이팅 부울은 아래 --json/human 경로와 동일 계산을 공유(분기 없음).
10647
10695
  if (opts.collect) {
@@ -10656,6 +10704,7 @@ function verifyClaimCmd(root, taskId, opts = {}) {
10656
10704
  if (claimsChecked && stubFiles.length > 0) reasons.push('stub-impl');
10657
10705
  if (has('--strict-claims') && testLinkOk === false) reasons.push('test-impl-unlinked');
10658
10706
  if (_runFail) reasons.push('tests-failed');
10707
+ if (_declaredPassMismatch) reasons.push('declared-pass-mismatch'); // 1.35.7 (UR-0013): --all/gate --claims 도 부풀림 게이팅
10659
10708
  return { id: taskId, request: row.request, status: row.status, ok: reasons.length === 0, reasons };
10660
10709
  }
10661
10710
 
@@ -10688,10 +10737,21 @@ function verifyClaimCmd(root, taskId, opts = {}) {
10688
10737
  out.verdict.declaredPassMatches = (runResult.parsed.num === declaredPass.num && runResult.parsed.denom === declaredPass.denom);
10689
10738
  }
10690
10739
  }
10740
+ // 1.35.7 (UR-0013 GPT5.5평가 개선②): top-level ok/reasons — CI 가 verdict 필드를 재조합하지 않아도 되도록 collect 경로와 동일 어휘.
10741
+ // exit 는 reasons 기반 단일 판정(ok↔exit 불일치 원천 차단) — 기존 게이트(1.11.2/1.17.3/1.17.4) 동일 + declared-pass-mismatch 추가.
10742
+ out.reasons = [];
10743
+ if (!filesAllExist) out.reasons.push('files-missing');
10744
+ if (out.verdict.testCountMatch === false) out.reasons.push('test-count-short');
10745
+ if (!evidenceQualityOk) out.reasons.push('evidence-incomplete');
10746
+ if (claimsChecked && !strictOk) out.reasons.push('optimism/honesty');
10747
+ if (!gitClaimOk) out.reasons.push('git-mismatch');
10748
+ if (claimsChecked && stubFiles.length > 0) out.reasons.push('stub-impl');
10749
+ if (has('--strict-claims') && testLinkOk === false) out.reasons.push('test-impl-unlinked');
10750
+ if (runResult && !runResult.skipped && !runResult.allPassed) out.reasons.push('tests-failed');
10751
+ if (_declaredPassMismatch) out.reasons.push('declared-pass-mismatch');
10752
+ out.ok = out.reasons.length === 0;
10691
10753
  log(JSON.stringify(out, null, 2));
10692
- if (runResult && !runResult.skipped && !runResult.allPassed) return process.exit(1);
10693
- // 1.11.2 (UR-0175): --json 도 optimism+git 게이팅 — 머신 경로가 허위완료를 통과시키지 않도록(human 경로와 동일).
10694
- if (!filesAllExist || out.verdict.testCountMatch === false || !evidenceQualityOk || (claimsChecked && !strictOk) || !gitClaimOk || (claimsChecked && stubFiles.length > 0) || (has('--strict-claims') && testLinkOk === false)) return process.exit(1); // 1.17.3 (UR-0046): 스텁(done 기본) + 테스트 미연결(strict). 1.17.4 (UR-0047): testCountMatch null(측정불가)은 미기여
10754
+ if (!out.ok) return process.exit(1);
10695
10755
  return;
10696
10756
  }
10697
10757
 
@@ -10746,7 +10806,7 @@ function verifyClaimCmd(root, taskId, opts = {}) {
10746
10806
  if (runResult && !runResult.skipped) {
10747
10807
  // 1.27.1 (13번째 외부리뷰 #3): exit 0 인데 테스트 비율을 못 파싱한 경우(예: 비-테스트 --test-cmd)를 '✓ all passed' 로 거짓표기하지 않음 — '실행됨, 테스트 수 미확인' 으로 정직 표기(판정/exit 불변 → 이색 테스트러너 FP 없음).
10748
10808
  log(` - ${runResult.cmd || 'npm test'} ${t('실행', 'run')}: ${runTestsOk ? (runResult.parsed ? '✓ all passed' : t('✓ 실행됨 (exit 0) — 테스트 수 미확인', '✓ ran (exit 0) — test count unconfirmed')) : '✗ FAIL'}`);
10749
- if (declaredPass) log(` - ${t('주장과 실행 결과 일치', 'claimed matches run')}: ${declaredPassMatchesActual ? '✓ pass' : t('⚠ 다름', '⚠ differs')}`);
10809
+ if (declaredPass) log(` - ${t('주장과 실행 결과 일치', 'claimed matches run')}: ${declaredPassMatchesActual ? '✓ pass' : (_declaredPassMismatch ? t('✗ FAIL (주장이 실행 결과보다 부풀려짐)', '✗ FAIL (claim inflated vs run)') : t('⚠ 다름 (실행 ≥ 주장 — pass)', '⚠ differs (run ≥ claimed — pass)'))}`); // 1.35.7 (UR-0013): 자기모순 표기 제거 — 부풀림은 FAIL 라벨
10750
10810
  }
10751
10811
  // 1.11.2 (UR-0175): optimism+정직성 — done 주장은 기본 게이팅(claimsChecked). 완화: --lenient.
10752
10812
  if (claimsChecked) {
@@ -10784,7 +10844,7 @@ function verifyClaimCmd(root, taskId, opts = {}) {
10784
10844
  } else if (testLinkOk === true && claimsChecked) {
10785
10845
  log(` - ${t('테스트-구현 연결', 'test-impl link')}: ${t('✓ pass (테스트가 구현을 참조)', '✓ pass (test references the impl)')}`);
10786
10846
  }
10787
- const overallFail = !allFilesOk || !testOk || (runResult && !runResult.skipped && !runTestsOk) || (claimsChecked && !strictOk) || !evidenceQualityOk || !gitClaimOk || (claimsChecked && stubFiles.length > 0) || (has('--strict-claims') && testLinkOk === false);
10847
+ const overallFail = !allFilesOk || !testOk || (runResult && !runResult.skipped && !runTestsOk) || (claimsChecked && !strictOk) || !evidenceQualityOk || !gitClaimOk || (claimsChecked && stubFiles.length > 0) || (has('--strict-claims') && testLinkOk === false) || _declaredPassMismatch; // 1.35.7 (UR-0013): declared-pass 부풀림 게이팅
10788
10848
  // 1.9.287: 정직한 한계 고지 — 테스트 통과 ≠ 의미적 구현 정확성
10789
10849
  if (claimsChecked || mustHaveEvidence) {
10790
10850
  log('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.35.6",
3
+ "version": "1.35.8",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -6818,5 +6818,43 @@ total++;
6818
6818
  if (!ok) failed++;
6819
6819
  }
6820
6820
 
6821
+ // 1.35.7 (UR-0013, GPT5.5pro 평가): declared-pass 부풀림 게이팅 — 주장(3/3) vs 실행(2/2) → exit 1 (human/--json/--all 3경로) + spec 리포터 파싱(패턴 6) + 실행≥주장 무벌점.
6822
+ total++;
6823
+ {
6824
+ let ok = false;
6825
+ try {
6826
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-dpm-'));
6827
+ cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
6828
+ fs.mkdirSync(path.join(d, 'src'), { recursive: true });
6829
+ fs.writeFileSync(path.join(d, 'src', 'x.js'), 'module.exports = { f: () => 1 };\n');
6830
+ fs.writeFileSync(path.join(d, 'x.test.js'), 'test();\ntest();\n');
6831
+ cp.spawnSync(process.execPath, [CLI, 'task', 'add', 'dpm', '--path', d], { encoding: 'utf8', timeout: 15000 });
6832
+ cp.spawnSync(process.execPath, [CLI, 'task', 'update', 'T-0002', '--status', 'done', '--evidence', 'src/x.js implemented, x.test.js added; 3/3 passed', '--path', d], { encoding: 'utf8', timeout: 15000 });
6833
+ // (1) 부풀림(주장 3/3, 실행 2/2 — jest 형식 echo): human exit 1 + FAIL 라벨
6834
+ const r1 = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0002', '--run-tests', '--test-cmd', 'echo Tests: 2 passed, 2 total', '--path', d], { encoding: 'utf8', timeout: 20000 });
6835
+ const inflatedFails = r1.status === 1 && /부풀려짐|inflated/.test(r1.stdout);
6836
+ // (2) --json: top-level ok=false + reasons 에 declared-pass-mismatch + verdict 필드 유지
6837
+ const r2 = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0002', '--run-tests', '--test-cmd', 'echo Tests: 2 passed, 2 total', '--json', '--path', d], { encoding: 'utf8', timeout: 20000 });
6838
+ let jsonOk = false; try { const j = JSON.parse(r2.stdout); jsonOk = r2.status === 1 && j.ok === false && j.reasons.includes('declared-pass-mismatch') && j.verdict.declaredPassMatches === false; } catch {}
6839
+ // (3) --all collect: 동일 reason 으로 집계 실패
6840
+ const r3 = cp.spawnSync(process.execPath, [CLI, 'verify-claim', '--all', '--run-tests', '--test-cmd', 'echo Tests: 2 passed, 2 total', '--json', '--path', d], { encoding: 'utf8', timeout: 20000 });
6841
+ let allOk = false; try { const j3 = JSON.parse(r3.stdout); const e3 = (j3.results || []).find(x => x.id === 'T-0002'); allOk = r3.status === 1 && e3 && e3.ok === false && e3.reasons.includes('declared-pass-mismatch'); } catch {}
6842
+ // (4) spec 리포터 파싱(신규 패턴 6): 라인-전체 "pass 2" → parsed 2/2 → 부풀림 감지 exit 1
6843
+ const r4 = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0002', '--run-tests', '--test-cmd', 'echo pass 2', '--path', d], { encoding: 'utf8', timeout: 20000 });
6844
+ const specParsed = r4.status === 1 && /2\/2/.test(r4.stdout);
6845
+ // (5) 정직 주장(2/2 = 실행) → exit 0 (FP 없음) / (6) 실행이 주장보다 많음(주장 2/2, 실행 3/3) → 무벌점 exit 0
6846
+ cp.spawnSync(process.execPath, [CLI, 'task', 'update', 'T-0002', '--status', 'done', '--evidence', 'src/x.js implemented, x.test.js added; 2/2 passed', '--path', d], { encoding: 'utf8', timeout: 15000 });
6847
+ const r5 = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0002', '--run-tests', '--test-cmd', 'echo Tests: 2 passed, 2 total', '--path', d], { encoding: 'utf8', timeout: 20000 });
6848
+ const r6 = cp.spawnSync(process.execPath, [CLI, 'verify-claim', 'T-0002', '--run-tests', '--test-cmd', 'echo Tests: 3 passed, 3 total', '--path', d], { encoding: 'utf8', timeout: 20000 });
6849
+ const truthfulOk = r5.status === 0;
6850
+ const growthOk = r6.status === 0;
6851
+ fs.rmSync(d, { recursive: true, force: true });
6852
+ ok = inflatedFails && jsonOk && allOk && specParsed && truthfulOk && growthOk;
6853
+ if (!ok) console.log(` [dpm 디버그] inflated=${inflatedFails} json=${jsonOk} all=${allOk} spec=${specParsed} truthful=${truthfulOk} growth=${growthOk}`);
6854
+ } catch {}
6855
+ console.log(ok ? '✓ B(1.35.7) UR-0013: declared-pass 부풀림 게이팅(human/json/--all) + spec 리포터 파싱 + 실행≥주장 무벌점' : '✗ declared-pass mismatch 게이팅 실패');
6856
+ if (!ok) failed++;
6857
+ }
6858
+
6821
6859
  console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
6822
6860
  if (failed > 0) process.exit(1);