leerness 1.26.0 → 1.27.0

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,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.27.0 — 2026-06-15 — 🛡️ [안정화/Stable] 보안 수정 안정 minor (개인키 스캔 FN + placeholder FP)
4
+
5
+ **🛡️ 안정화(Stable) minor — 13번째 외부리뷰에서 확인된 보안 수정을 조기 npm 공개.** 직전 minor(1.26.0) 이후 1.26.1 패치 1건이지만, **보안 FN/FP(거짓 "보안 OK" + CI 파손)는 패치 누적을 기다리기보다 조기 공개가 합리적**이라 단독 minor 로 게시. R-0011 정책의 18번째 stable minor.
6
+
7
+ ### 이번 minor 통합 (1.26.1)
8
+ - **🔒 개인키 파일 스캔 FN 차단**: `scan secrets` 가 `.pem`/`.key`/`.crt`/`.p8`/`.pfx` 등을 확장자 allow-list 누락으로 건너뛰어 **커밋된 개인키 미탐 + handoff 가 "보안 OK" 거짓보증**하던 문제 수정(basename 오버라이드). gitignore 된 키는 종전대로 info.
9
+ - **🔒 DB placeholder 오탐(FP) 차단**: `.env.example` 의 `user:password@`·`root:root` 등 교과서 placeholder 를 커밋 시크릿으로 오탐해 `gate`/CI 를 깨뜨리던 문제 수정(valueGroup + placeholder 마커). 진짜 고엔트로피 비밀번호는 계속 탐지(FN=0).
10
+ - **🔧 retro --json NaN 계약**: 비숫자 `--days` 가 plain text 를 `--json` 소비자에게 흘리던 문제 → 숫자 가드 + 클램프(failJson 구조화).
11
+
12
+ ### 잔여 (외부리뷰 백로그)
13
+ - init `--language en` seed 데이터 i18n(= 전체 `.harness/` 템플릿 i18n 필요, 대형) · verify-claim `--test-cmd` no-parse 하드닝(FP 회귀 위험으로 신중) · audit 미초기화 출력 정합(cosmetic) · 진단명령 영어화 Phase 10.
14
+
15
+ ### 검증 (회귀 0)
16
+ - **selftest 246/246** · **E2E 367/367** (개인키 스캔 FN차단 + placeholder FP차단/FN유지 + retro --json 행위 회귀가드 포함) · 게시본 클린룸 재실증.
17
+ - minor(1.27.0) — npm 배포(R-0011 stable) + annotated tag(Stable) + GitHub release(latest).
18
+
19
+ ## 1.26.1 — 2026-06-15 — 13번째 외부리뷰 P2 수정: 개인키파일 스캔 FN + DB placeholder FP + retro --json NaN
20
+
21
+ **🔎 13번째 외부 멀티모델 리뷰(1.26.0 게시본)에서 확인된 P2 3건 수정.** 3 에이전트 클린룸 리뷰 → 맹신 X 양방향 직접 재현으로 진짜만 채택 → 보안 2건 + --json 계약 1건 수정.
22
+
23
+ ### 변경 (확인된 P2 3건)
24
+ - **🔒 개인키 파일 스캔 FN 차단 (보안)**: `scan secrets` 가 `.pem`/`.key`/`.crt`/`.p8`/`.pfx` 등 개인키·인증서 확장자를 스캔 allow-list 누락으로 건너뛰어 **커밋된 개인키를 미탐 + handoff 가 "보안 OK" 거짓보증**하던 문제 → basename 오버라이드(env-family 패턴 미러)로 강제 스캔. (gitignore 된 키는 종전대로 info 강등.)
25
+ - **🔒 DB placeholder 오탐(FP) 차단 (보안/CI)**: `.env.example` 의 `postgres://user:password@`·`root:root`·`yourpassword` 같은 교과서 placeholder 가 커밋 시크릿으로 오탐돼 `gate`/CI 를 깨뜨리던 문제 → DB URI 정규식에 비밀번호 capture group(`valueGroup`) 추가 + placeholder 마커(`root`/`admin`/`user`/`yourpassword` 등 전체-값 정확 일치)로 차단. **진짜 고엔트로피 비밀번호는 계속 탐지(FN=0)**.
26
+ - **🔧 retro --json NaN 크래시 (계약)**: `retro --days <비숫자>` 가 `new Date(Invalid)` throw 로 `--json` 소비자에게 plain text(`✗ Invalid time value`)를 흘리던 문제 → 숫자 가드 + 음수/오버플로 클램프(`failJson` 구조화, insights/round-history 와 일관).
27
+
28
+ ### 검증 (회귀 0)
29
+ - **selftest 245→246** (소스가드) · 행위(맹신 X 양방향: 개인키 .key 탐지 + .crt 무오탐, placeholder 스킵 + 실비번 탐지, retro --json 구조화 + 정상동작) · **E2E 367/367** (신규 행위 회귀가드 1건).
30
+ - patch(1.26.1) — npm 미배포(R-0011, GitHub/CHANGELOG 누적). 잔여 리뷰 발견(audit 미초기화 출력 정합 P2, verify-claim --test-cmd no-parse 하드닝 P3, init en seed 데이터 i18n, 진단명령 영어화 Phase 10)은 백로그.
31
+
3
32
  ## 1.26.0 — 2026-06-15 — 🛡️ [안정화/Stable] i18n 행위가드 + health 진단 영어화 안정 minor
4
33
 
5
34
  **🛡️ 안정화(Stable) minor — i18n 레이어 견고성 검증·가드 + health 진단 영어화를 npm 공개.** 직전 minor(1.25.0) 이후 누적된 패치 2건(1.25.1 + 1.25.2)을 검증·통합해 배포. R-0011 정책의 17번째 stable minor. 한국어 우선 기본은 그대로.
package/README.md CHANGED
@@ -104,7 +104,7 @@ MIT
104
104
  <!-- leerness:project-readme:start -->
105
105
  ## Leerness Project Harness
106
106
 
107
- 이 프로젝트는 Leerness v1.26.0 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
107
+ 이 프로젝트는 Leerness v1.27.0 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
108
108
 
109
109
  ### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
110
110
 
@@ -158,7 +158,7 @@ leerness memory restore decision <date|title>
158
158
 
159
159
  ### MCP server (외부 AI 통합)
160
160
 
161
- Leerness v1.26.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
161
+ Leerness v1.27.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
162
162
 
163
163
  ```jsonc
164
164
  // 카테고리별
@@ -179,7 +179,7 @@ Leerness v1.26.0는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code
179
179
  `<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
180
180
  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) 다음 라운드 예약.
181
181
 
182
- 현재 누적: **70 라운드 (1.9.40 → 1.26.0)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
182
+ 현재 누적: **70 라운드 (1.9.40 → 1.27.0)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
183
183
 
184
184
  ### 성능 가이드 (1.9.140 측정)
185
185
 
@@ -217,6 +217,6 @@ leerness release pack --close --auto-main-push
217
217
  - `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
218
218
  - `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
219
219
 
220
- Last synced by Leerness v1.26.0: 2026-06-15
220
+ Last synced by Leerness v1.27.0: 2026-06-15
221
221
  <!-- leerness:project-readme:end -->
222
222
 
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.26.0';
35
+ const VERSION = '1.27.0';
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') 시 호스트 프로세스 오염.
@@ -3810,6 +3810,16 @@ function _selfTestCases() {
3810
3810
  const renderEn = bin.includes('quality self-question lenses (v${VERSION})') && bin.includes("t('페르소나', 'persona')");
3811
3811
  return enFields && koVerbatim && renderEn;
3812
3812
  } },
3813
+ { name: '13번째 외부리뷰 P2 수정 (1.26.1): 개인키파일 스캔 + DB placeholder valueGroup + retro NaN 가드 (소스 가드)', run: () => {
3814
+ const bin = read(__filename);
3815
+ const cat = read(path.join(path.dirname(__filename), '..', 'lib', 'catalogs.js'));
3816
+ const pu = read(path.join(path.dirname(__filename), '..', 'lib', 'pure-utils.js'));
3817
+ const keyFile = bin.includes('const isKeyFile =') && bin.includes('!isKeyFile) continue;');
3818
+ const dbVg = /DB connection string[^\n]*valueGroup: 1/.test(cat);
3819
+ const phPlaceholder = pu.includes('root|admin|user|username|yourpassword');
3820
+ const retroGuard = bin.includes("failJson(has('--json'), 'invalid_arg'") && bin.includes('Math.min(days, 36500)');
3821
+ return keyFile && dbVg && phPlaceholder && retroGuard;
3822
+ } },
3813
3823
  { name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
3814
3824
  ];
3815
3825
  }
@@ -7843,7 +7853,9 @@ function _collectSecretFindings(root) {
7843
7853
  const ext = path.extname(file).toLowerCase();
7844
7854
  // 1.9.386 (UR-0087): env-family(.env / .env.local / .env.production …) basename 강제 포함.
7845
7855
  const isEnvFamily = /^\.env(\.|$)/.test(path.basename(file));
7846
- if (!SCAN_TEXT_EXT.has(ext) && !isEnvFamily) continue;
7856
+ // 1.26.1 (13번째 외부리뷰 P2): 개인키/인증서 파일(.pem/.key/.crt/.p8 …) 확장자 allow-list 에 없어 스캔 누락 → 커밋된 개인키 미탐 + handoff 'OK' 거짓보증. basename 으로 강제 포함('Generic private key' 정규식이 실제로 돌도록).
7857
+ const isKeyFile = /\.(?:pem|key|crt|cer|der|p8|p12|pfx|pkcs8|ppk|asc|gpg|keystore|jks)$/i.test(path.basename(file));
7858
+ if (!SCAN_TEXT_EXT.has(ext) && !isEnvFamily && !isKeyFile) continue;
7847
7859
  let text;
7848
7860
  // 1.12.5 (15th 버그헌트 P2, UR-0019): stat-before-read — 1MB 초과 파일은 읽지 않고 건너뜀(이전엔 read 후 검사라 대형 파일 통째 로드).
7849
7861
  try { if (fs.statSync(file).size > 1024 * 1024) continue; } catch { continue; }
@@ -12509,7 +12521,10 @@ function retroCmd(root) {
12509
12521
  if (has('--all-apps') || arg('--include', null)) {
12510
12522
  return _retroWorkspace(root);
12511
12523
  }
12512
- const days = parseInt(arg('--days', '7'), 10);
12524
+ // 1.26.1 (13번째 외부리뷰 P2): 비숫자 --days → NaN → new Date(Invalid) throw 로 --json 소비자에 plain text 누출. 숫자 가드 + 음수/오버플로 클램프(insights/round-history 와 일관).
12525
+ let days = parseInt(arg('--days', '7'), 10);
12526
+ if (!Number.isFinite(days)) { failJson(has('--json'), 'invalid_arg', _uiLang(root) === 'en' ? '--days must be a number' : '--days 는 숫자여야 합니다'); return; }
12527
+ days = Math.max(0, Math.min(days, 36500));
12513
12528
  const cutoff = new Date(Date.now() - days * 86400 * 1000).toISOString().slice(0, 10);
12514
12529
  const agg = _retroAggregate(root);
12515
12530
  // 1.9.16: --json
package/lib/catalogs.js CHANGED
@@ -383,7 +383,7 @@ const SECRET_PATTERNS = [
383
383
  // 1.9.350 (UR-0060 외부리뷰 3모델): 누락 패턴 보강
384
384
  { name: 'GitLab PAT', re: /\bglpat-[A-Za-z0-9_-]{20,}\b/g },
385
385
  { name: 'JWT', re: /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
386
- { name: 'DB connection string (embedded password)', re: /\b(?:postgres|postgresql|mysql|mongodb(?:\+srv)?|redis|amqp):\/\/[^:\s/@]+:[^@\s/]+@/gi },
386
+ { name: 'DB connection string (embedded password)', re: /\b(?:postgres|postgresql|mysql|mongodb(?:\+srv)?|redis|amqp):\/\/[^:\s/@]+:([^@\s/]+)@/gi, valueGroup: 1 }, // 1.26.1 (13번째 외부리뷰 P2): valueGroup=비밀번호 → .env.example 의 placeholder(user:password 등) 오탐 차단
387
387
  { name: 'SendGrid API key', re: /\bSG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}\b/g },
388
388
  { name: 'AWS Secret Access Key (context)', re: /\baws[^\n]{0,40}?(?:secret_access_key|secret_key|secret)[^\n]{0,12}?["']?[A-Za-z0-9/+]{40}["']?/gi },
389
389
  { name: 'Hardcoded Bearer token', re: /\bBearer\s+[A-Za-z0-9_\-.=]{20,}/g },
package/lib/pure-utils.js CHANGED
@@ -642,7 +642,8 @@ function _isPlaceholderSecret(value) {
642
642
  let v = String(value).trim().replace(/^["']|["']$/g, '').trim().toLowerCase();
643
643
  if (!v) return true;
644
644
  // 전체가 placeholder 토큰
645
- if (/^(?:x{3,}|\*{3,}|\.{3,}|-+|0+|1234567890?|12345678|abc123|secret|password|passwd|changeme|change[-_]me|replace[-_]?me|placeholder|example|examples?|sample|dummy|test|testing|foo|bar|baz|tbd|todo|none|null|undefined|nil|empty|redacted|hidden|value|string|here)$/.test(v)) return true;
645
+ // 1.26.1 (13번째 외부리뷰 P2): DB URI 등 placeholder 자격증명(user:password / root:root / yourpassword …) 추가 — 전체-값 정확 일치만 매칭하므로 실키(길고 고엔트로피)에는 FN 영향 0.
646
+ if (/^(?:x{3,}|\*{3,}|\.{3,}|-+|0+|1234567890?|12345678|abc123|secret|password|passwd|pass|changeme|change[-_]me|changeit|replace[-_]?me|placeholder|example|examples?|sample|dummy|test|testing|foo|bar|baz|tbd|todo|none|null|undefined|nil|empty|redacted|hidden|value|string|here|root|admin|user|username|yourpassword|your[-_]?password|mypassword)$/.test(v)) return true;
646
647
  // 1.9.405 (8번째 버그헌트 회귀수정, UR-0109): placeholder 단어 신호를 entropy 가드보다 먼저 검사.
647
648
  // 1.9.401 회귀: 긴 서술형 placeholder('your-super-secret-api-key-example-value')가 고엔트로피(영숫자24+ & 고유12+)를 넘어 실키로 오탐(FP).
648
649
  // → placeholder 마커 단어가 있으면 entropy 가드 무시하고 placeholder 로 판정. 실키 prefix(sk-/AKIA 등)는 마커보다 우선(FN 방지).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.26.0",
3
+ "version": "1.27.0",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -6237,5 +6237,38 @@ total++;
6237
6237
  if (!ok) failed++;
6238
6238
  }
6239
6239
 
6240
+ // 1.26.1 (13번째 외부리뷰 P2 회귀가드): 개인키파일 스캔 FN + DB placeholder FP + retro --json NaN 행위 가드.
6241
+ total++;
6242
+ {
6243
+ let ok = false;
6244
+ try {
6245
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-rev13-'));
6246
+ cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes'], { encoding: 'utf8', timeout: 30000 });
6247
+ const out = (r) => (r.stdout || '') + (r.stderr || '');
6248
+ // #4: 커밋된 개인키 파일(.key, gitignore 미포함)은 잡혀야 함(FN 차단)
6249
+ fs.writeFileSync(path.join(d, 'server.key'), '-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA1234567890abcdefghij\n-----END RSA PRIVATE KEY-----\n');
6250
+ const keyScan = cp.spawnSync(process.execPath, [CLI, 'scan', 'secrets', d], { encoding: 'utf8', timeout: 15000 });
6251
+ const keyCaught = keyScan.status === 1 && /Generic private key/.test(out(keyScan));
6252
+ fs.unlinkSync(path.join(d, 'server.key'));
6253
+ // #5: .env.example 의 placeholder DB URI 는 오탐 X (FP 차단) / 진짜 비번은 잡힘(FN 유지)
6254
+ fs.writeFileSync(path.join(d, '.env.example'), 'A=postgres://user:password@h:5432/db\nB=mysql://root:root@h/db\n');
6255
+ const phScan = cp.spawnSync(process.execPath, [CLI, 'scan', 'secrets', d], { encoding: 'utf8', timeout: 15000 });
6256
+ const noFp = phScan.status === 0 && !/DB connection string/.test(out(phScan));
6257
+ fs.unlinkSync(path.join(d, '.env.example'));
6258
+ fs.writeFileSync(path.join(d, 'real.env'), 'D=postgres://admin:Xk9zQ2mP7rL4wT@prod.example.com:5432/main\n');
6259
+ const realScan = cp.spawnSync(process.execPath, [CLI, 'scan', 'secrets', d], { encoding: 'utf8', timeout: 15000 });
6260
+ const realCaught = realScan.status === 1 && /DB connection string/.test(out(realScan));
6261
+ fs.unlinkSync(path.join(d, 'real.env'));
6262
+ // #1: retro --days 비숫자 --json 은 구조화 JSON(plain text 누출 X)
6263
+ const rj = cp.spawnSync(process.execPath, [CLI, 'retro', d, '--days', 'xyz', '--json'], { encoding: 'utf8', timeout: 15000 });
6264
+ let retroJsonOk = false;
6265
+ try { const j = JSON.parse(rj.stdout); retroJsonOk = j && (j.error || j.code) && rj.status === 1; } catch {}
6266
+ fs.rmSync(d, { recursive: true, force: true });
6267
+ ok = keyCaught && noFp && realCaught && retroJsonOk;
6268
+ } catch {}
6269
+ console.log(ok ? '✓ B(1.26.1) 13th 외부리뷰: 개인키파일 스캔(FN차단) + DB placeholder(FP차단/FN유지) + retro --json NaN 구조화' : '✗ 13th 외부리뷰 P2 회귀가드 실패');
6270
+ if (!ok) failed++;
6271
+ }
6272
+
6240
6273
  console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
6241
6274
  if (failed > 0) process.exit(1);