leerness 1.9.417 → 1.9.419

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,48 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.419 — 2026-06-07 — 엔트리 파일명 변경: bin/harness.js → bin/leerness.js (네이밍 일관성, UR-0126)
4
+
5
+ **📛 패키지 엔트리 파일명을 `bin/harness.js` → `bin/leerness.js` 로 변경 — 명령어(`leerness`)와 파일명 일치.**
6
+
7
+ ### 배경
8
+ 파일명 `harness.js` 는 초기 잔재로, 명령어/패키지명(`leerness`)과 불일치했습니다. 3-에이전트 실현가능성 조사 결과 **EASY/안전**(명령어는 이미 `leerness`, 내부 참조는 `__filename`/`path.resolve` 기반이라 자동 대응)으로 판정되어 실행.
9
+
10
+ ### 변경
11
+ - `git mv bin/harness.js bin/leerness.js` (히스토리 보존).
12
+ - `package.json`: `bin.leerness` / `main` / npm scripts(test/test:fast/prepack) → `bin/leerness.js`.
13
+ - `scripts/e2e.js`(35건)·`scripts/smoke.js`·`.github/workflows/ci.yml`·`docs/PUBLISH_PRECHECK.md` 경로 참조 갱신.
14
+ - 내부 self-invocation/진단(`harnessPath: __filename`)·selftest(`read(__filename)`)는 런타임 자동 — 무변경.
15
+ - next-action 제안 문자열 1건 `node ./bin/harness.js whats-new` → `leerness whats-new`.
16
+
17
+ ### 사용자 영향
18
+ **없음** — 설치/실행은 `leerness` 명령(불변). 파일을 직접 호출하지 않음. 기존 워크스페이스(.harness)·상태도 불변.
19
+
20
+ ### 검증 (회귀 0)
21
+ - **selftest 164→165 PASS** (신규 구조 가드: bin 파일=leerness.js + package.json bin/main 일치).
22
+ - **E2E 418→419 PASS** (CLI 경로 변경 후 전체 스모크).
23
+
24
+ ### 참고 (조사 결과 — 다음 라운드)
25
+ - harness.js 무거움(21k줄) → UR-0125 점진 모듈화(handoff 등 DI 추출).
26
+ - `.harness`→`.leerness` 폴더명 → UR-0127 **직접 변경 비권장**(기존 .leerness 상태 substrate 충돌) — 상태기판 격리(대안 A) 권장.
27
+
28
+ ## 1.9.418 — 2026-06-07 — health 보안 정직화 + status "healthy" 의미 명시 (9번째 외부평가 Codex P2, UR-0121 잔여)
29
+
30
+ **🩺 `health`/`status` 의 "healthy" 가 프로젝트 안전을 뜻하는 듯 오해되던 것(Codex P2) 보강 — 정직성 일관 적용.**
31
+
32
+ ### 배경
33
+ Codex: 같은 상태에서 `gate` 는 실패하는데 `health --json`/`status --json` 은 `healthy:true`. `health` 의 보안 점검은 `.env`/`.gitignore` 만 보고 하드코딩 시크릿을 무시(1.9.415 handoff 와 동일 근본원인), `status` 의 `healthy` 는 설치 파일 존재만 의미하나 라벨이 오해를 유발.
34
+
35
+ ### 수정
36
+ - **health 보안 점검 정직화**: `_collectSecretFindings`(1.9.415 공유 헬퍼)로 **커밋 대상 하드코딩 시크릿**을 반영 → `checks.security.committedSecrets`/`critical` + `issues` + 최상위 `healthy` 에 전파. 시크릿이 있으면 `health` 가 `healthy:false`. `.env` 없어도 소스 스캔(기존엔 .env 없으면 보안 점검 스킵).
37
+ - **status 의미 명시**: `--json` 에 `scope:"install"` + `healthyMeaning`("설치 파일 존재 여부 — 프로젝트 안전은 gate/scan secrets 사용") 추가. `healthy` 의미(설치 완료)는 보존(비파괴).
38
+
39
+ ### 검증 (회귀 0)
40
+ - **selftest 163→164 PASS** (health `_collectSecretFindings` 와이어 + status scope/meaning).
41
+ - **E2E 417→418 PASS** (시크릿 시 health healthy:false + committedSecrets + status scope:install + 클린 회귀).
42
+
43
+ ### 9번째 외부평가 — 확정 발견 전부 소진
44
+ UR-0121(handoff/scan/encoding/contract + **health/status 라벨**) ✅ · UR-0122(add류) ✅ · UR-0123(contract field) ✅. 잔여는 전략 항목 UR-0124(init 프로파일/명령 계층화/handoff 속도/--compact) + team positional path(소소).
45
+
3
46
  ## 1.9.417 — 2026-06-07 — contract verify field 검증 범용화: ## Fields 섹션 불릿 인식 (9번째 외부평가, UR-0123)
4
47
 
5
48
  **🔧 `contract verify` 의 필드 계약 검증이 `tick.` 프리픽스 전용으로 하드코딩돼 범용 spec 에서 무력화되던 것(Opus 발견) 보강.**
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > **AI 코딩 에이전트의 거짓 완료·중복·망각·충돌을 막아주는 검수·기억·협업 CLI 하네스.**
4
4
  > **A CLI harness that stops AI coding agents from faking completion, duplicating work, forgetting context, and colliding.**
5
5
 
6
- [![npm](https://img.shields.io/badge/npm-leerness-blue)](https://www.npmjs.com/package/leerness) [![version](https://img.shields.io/badge/version-1.9.417-green)]() [![tests](https://img.shields.io/badge/e2e-356%2F356-success)]() [![selftest](https://img.shields.io/badge/selftest-163-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-85-brightgreen)]() [![providers](https://img.shields.io/badge/AI_providers-10-brightgreen)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
6
+ [![npm](https://img.shields.io/badge/npm-leerness-blue)](https://www.npmjs.com/package/leerness) [![version](https://img.shields.io/badge/version-1.9.419-green)]() [![tests](https://img.shields.io/badge/e2e-357%2F357-success)]() [![selftest](https://img.shields.io/badge/selftest-165-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-85-brightgreen)]() [![providers](https://img.shields.io/badge/AI_providers-10-brightgreen)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
7
7
 
8
8
  ```
9
9
  ╔══════════════════════════════════════════════════════════════╗
@@ -471,7 +471,7 @@ MIT — © leerness contributors
471
471
  <!-- leerness:project-readme:start -->
472
472
  ## Leerness Project Harness
473
473
 
474
- 이 프로젝트는 Leerness v1.9.417 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
474
+ 이 프로젝트는 Leerness v1.9.419 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
475
475
 
476
476
  ### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
477
477
 
@@ -525,7 +525,7 @@ leerness memory restore decision <date|title>
525
525
 
526
526
  ### MCP server (외부 AI 통합)
527
527
 
528
- Leerness v1.9.417는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
528
+ Leerness v1.9.419는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **85개 도구**를 노출:
529
529
 
530
530
  ```jsonc
531
531
  // 카테고리별
@@ -546,7 +546,7 @@ Leerness v1.9.417는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
546
546
  `<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
547
547
  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) 다음 라운드 예약.
548
548
 
549
- 현재 누적: **70 라운드 (1.9.40 → 1.9.417)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
549
+ 현재 누적: **70 라운드 (1.9.40 → 1.9.419)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
550
550
 
551
551
  ### 성능 가이드 (1.9.140 측정)
552
552
 
@@ -584,6 +584,6 @@ leerness release pack --close --auto-main-push
584
584
  - `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
585
585
  - `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
586
586
 
587
- Last synced by Leerness v1.9.417: 2026-06-07
587
+ Last synced by Leerness v1.9.419: 2026-06-07
588
588
  <!-- leerness:project-readme:end -->
589
589
 
@@ -31,7 +31,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
31
31
  // 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
32
32
  const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST, _DEFAULT_PLATFORM_CONSTRAINTS, _DEFAULT_DOMAIN_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 분리 (MERGE_OVERWRITE_FILES/MINIMAL_SKIP_KEYS 포함)
33
33
 
34
- const VERSION = '1.9.417';
34
+ const VERSION = '1.9.419';
35
35
 
36
36
  // 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
37
37
  // 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
@@ -3025,6 +3025,12 @@ function _selfTestCases() {
3025
3025
  { name: '8번째 버그헌트 (UR-0115): lazy detect --auto-track 단일 RMW 배치(O(T×N)→O(N+T)) (1.9.411)', run: () => { const src = read(__filename); const batched = src.includes("8번째 버그헌트, UR-0115") && /has\('--auto-track'\)[\s\S]{0,500}?_withLock\(progressPath\(root\), \(\) => \{[\s\S]{0,1200}?writeProgressRows/.test(src); const noPerTodoUpsert = !/for \(const t of newTodos\) \{\s*const id = nextId\(root, 'T'\);/.test(src); return batched && noPerTodoUpsert; } },
3026
3026
  { name: '6번째 외부평가 Opus P1 (UR-0100): list-family(decision/feature/plan/runs/team list) positional path 지원 (조용한 cwd 오독 차단) (1.9.412)', run: () => { const src = read(__filename); const L = '_resolveRoot('; const decOk = src.includes("decisionListCmd(absRoot(" + L + "args[2]))"); const planOk = src.includes("planListCmd(absRoot(" + L + "args[2]))"); const featOk = src.includes("featureListCmd(absRoot(" + L + "args[2]))"); const runsOk = src.includes("runsListCmd(absRoot(" + L + "args[2]))"); const teamOk = src.includes(L + "args[1] === 'list' ? args[2] : null)"); return decOk && planOk && featOk && runsOk && teamOk; } },
3027
3027
  { name: '6번째 외부평가 codex P2 (UR-0101): action 명령(task/decision/rule/lesson add) --json 구조화 출력 (1.9.413)', run: () => { const src = read(__filename); const taskJ = src.includes("log(JSON.stringify({ ok: true, id, status: arg('--status', 'requested'), request: text }))"); const decJ = src.includes("log(JSON.stringify({ ok: true, title }))"); const lesJ = src.includes("log(JSON.stringify({ ok: true, text, tag: tag || null }))"); const ruleJ = src.includes("skipped: !!result.skip"); return taskJ && decJ && lesJ && ruleJ; } },
3028
+ { name: '9th 외부평가 Codex P2 (UR-0121 잔여): health 보안 정직화(커밋 시크릿 반영) + status scope:install (1.9.418)', run: () => {
3029
+ const src = read(__filename);
3030
+ const healthWired = src.includes('_collectSecretFindings(root)') && src.includes('committedSecrets') && src.includes('커밋 대상 하드코딩 시크릿');
3031
+ const statusScope = src.includes("scope: 'install'") && src.includes('healthyMeaning');
3032
+ return healthWired && statusScope && typeof healthCmd === 'function' && typeof status === 'function';
3033
+ } },
3028
3034
  { name: '9th 외부평가 Opus (UR-0123): contract field 범용화 — ## Fields 섹션 불릿 인식(tick. 회귀 보존) (1.9.417)', run: () => {
3029
3035
  const m = require('../lib/pure-utils');
3030
3036
  const r1 = m._parseContractSpec('# S\n\n## Fields\n- userId\n- expiresAt: string\n');
@@ -3066,6 +3072,7 @@ function _selfTestCases() {
3066
3072
  return handoffWired && scanJson && encJson && contractExit && behav;
3067
3073
  } },
3068
3074
  { name: '9라운드 (UR-0119/0120): team review(메인 검수) — _composeTeamPlan reviewStep + handoff 검수필요 + team add 와이어 (1.9.414)', run: () => { const m = require('../lib/pure-utils'); const on = m._composeTeamPlan({ id: 't', members: ['a', 'b'], personas: ['security'] }, '점검'); const off = m._composeTeamPlan({ id: 't', members: ['a'], review: false }, '점검'); const planOk = on.review === true && !!on.reviewStep && on.reviewStep.suggestedCommand.includes('verify-claim') && off.review === false && !off.reviewStep; const rem = m._teamHandoffReminders([{ id: 'r', schedule: 'every-session', status: 'active', members: ['a'], review: true }]); const remOk = rem.length === 1 && rem[0].includes('검수필요'); const teamSrc = read(path.join(path.dirname(__filename), '..', 'lib', 'team.js')); const wired = teamSrc.includes("review: !has('--no-review')") && teamSrc.includes('메인 검수 (필수)'); return planOk && remOk && wired; } },
3075
+ { name: '파일명 변경 (UR-0126): bin 파일=leerness.js + package.json bin/main 일치 (1.9.419)', run: () => { const okName = path.basename(__filename) === 'leerness.js'; let pkg; try { pkg = require('../package.json'); } catch { return false; } const okBin = pkg && pkg.bin && pkg.bin.leerness === 'bin/leerness.js' && pkg.main === 'bin/leerness.js'; return okName && okBin; } },
3069
3076
  { name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
3070
3077
  ];
3071
3078
  }
@@ -5703,7 +5710,7 @@ function _suggestNextActions(root, latestRow, keyword) {
5703
5710
  // 6) CHANGELOG / README 버전 일치 확인 (1.9.X bump 후 자주 누락되는 단계)
5704
5711
  try {
5705
5712
  if (/(\d\.\d\.\d+)/.test(taskText) || /(bump|version|release)/i.test(taskText)) {
5706
- actions.push({ icon: '📝', title: `버전 변동 task — CHANGELOG.md / README.md 갱신 확인`, command: `node ./bin/harness.js whats-new .` });
5713
+ actions.push({ icon: '📝', title: `버전 변동 task — CHANGELOG.md / README.md 갱신 확인`, command: `leerness whats-new .` });
5707
5714
  }
5708
5715
  } catch {}
5709
5716
 
@@ -6741,7 +6748,8 @@ function status(root) {
6741
6748
  const files = Object.keys(coreFiles(root, lang, [], { minimal: isMinimal }));
6742
6749
  const missing = files.filter(f => !exists(path.join(root,f)));
6743
6750
  // 1.9.384 (5번째 외부평가/UR-0085): --json 일관성 — AI 에이전트용 구조화 출력.
6744
- if (has('--json')) { log(JSON.stringify({ version: ver, language: lang, minimal: isMinimal, total: files.length, present: files.length - missing.length, missing, healthy: missing.length === 0 }, null, 2)); return; }
6751
+ // 1.9.418 (9th 외부평가 Codex P2): healthy 의미를 명시(설치 파일 존재 프로젝트 안전). 프로젝트 안전은 gate/scan secrets 사용.
6752
+ if (has('--json')) { log(JSON.stringify({ version: ver, language: lang, minimal: isMinimal, scope: 'install', total: files.length, present: files.length - missing.length, missing, healthy: missing.length === 0, healthyMeaning: '설치 파일 존재 여부(프로젝트 안전 아님 — 보안/품질은 leerness gate / scan secrets 사용)' }, null, 2)); return; }
6745
6753
  log(`Leerness: ${ver}${isMinimal ? ' (minimal)' : ''}`);
6746
6754
  log(`Files: ${files.length - missing.length}/${files.length}`);
6747
6755
  if (missing.length) missing.forEach(x => warn('missing: ' + x));
@@ -19381,26 +19389,29 @@ function healthCmd(root) {
19381
19389
  const j = JSON.parse(r.stdout.trim());
19382
19390
  out.checks.drift = { level: j.level, score: j.score, firedCount: (j.fired || []).length };
19383
19391
  } catch { out.checks.drift = { error: 'drift check 실패' }; }
19384
- // 2) 보안 상태 (env + .gitignore)
19392
+ // 2) 보안 상태 (1.9.418, 9th 외부평가 Codex P2): .env/.gitignore + **실제 하드코딩 시크릿 스캔**.
19393
+ // 기존엔 .env 가 .gitignore 에 있으면 critical:false 라 커밋된 하드코딩 시크릿이 있어도 health 가 healthy:true 였음(false-OK).
19394
+ // handoff/scan secrets 와 동일하게 _collectSecretFindings 로 커밋 대상 시크릿을 반영(정직성).
19385
19395
  try {
19396
+ const sec = _collectSecretFindings(root);
19397
+ const committedSecrets = sec.committed.length;
19386
19398
  const envPath = path.join(root, '.env');
19387
- if (exists(envPath)) {
19399
+ const hasDotEnv = exists(envPath);
19400
+ const s = { hasDotEnv, committedSecrets };
19401
+ if (hasDotEnv) {
19388
19402
  const d = envDiff(root);
19389
19403
  const giText = exists(path.join(root, '.gitignore')) ? read(path.join(root, '.gitignore')) : '';
19390
19404
  const giLines = giText.split('\n').map(l => l.trim());
19391
19405
  const envInGi = giLines.includes('.env') || giLines.includes('/.env');
19392
19406
  const SECRET_PATTERNS = ['.env', '.env.local', '.env.production', '.env.*.local', '*.pem', 'credentials.json'];
19393
- const missingSecrets = SECRET_PATTERNS.filter(p => !giLines.some(l => l === p || l === '/' + p));
19394
- out.checks.security = {
19395
- hasDotEnv: true,
19396
- envInGitignore: envInGi,
19397
- envExampleMissing: d.inEnvOnly,
19398
- gitignoreMissingSecrets: missingSecrets,
19399
- critical: !envInGi
19400
- };
19407
+ s.envInGitignore = envInGi;
19408
+ s.envExampleMissing = d.inEnvOnly;
19409
+ s.gitignoreMissingSecrets = SECRET_PATTERNS.filter(p => !giLines.some(l => l === p || l === '/' + p));
19410
+ s.critical = !envInGi || committedSecrets > 0;
19401
19411
  } else {
19402
- out.checks.security = { hasDotEnv: false, ok: true };
19412
+ s.critical = committedSecrets > 0;
19403
19413
  }
19414
+ out.checks.security = s;
19404
19415
  } catch { out.checks.security = { error: '보안 점검 실패' }; }
19405
19416
  // 3) skill 수 + skill query 누적
19406
19417
  try {
@@ -19643,7 +19654,8 @@ function healthCmd(root) {
19643
19654
  // 6) issues 요약 (사용자 글로벌 룰 가시화)
19644
19655
  const issues = [];
19645
19656
  if (out.checks.drift?.level && !/healthy/.test(out.checks.drift.level)) issues.push(`drift ${out.checks.drift.level}`);
19646
- if (out.checks.security?.critical) issues.push('🚨 .env가 .gitignore에 누락 (보안 CRITICAL)');
19657
+ if (out.checks.security?.committedSecrets > 0) issues.push(`🚨 커밋 대상 하드코딩 시크릿 ${out.checks.security.committedSecrets}건 (보안 CRITICAL)`); // 1.9.418 (9th 외부평가 Codex P2)
19658
+ if (out.checks.security?.hasDotEnv && out.checks.security?.envInGitignore === false) issues.push('🚨 .env가 .gitignore에 누락 (보안 CRITICAL)');
19647
19659
  if (out.checks.security?.envExampleMissing?.length) issues.push(`.env→.env.example 누락 ${out.checks.security.envExampleMissing.length}건`);
19648
19660
  if (out.checks.security?.gitignoreMissingSecrets?.length) issues.push(`.gitignore 시크릿 누락 ${out.checks.security.gitignoreMissingSecrets.length}건`);
19649
19661
  out.issues = issues;
@@ -1,93 +1,93 @@
1
- # Publish Pre-check (leerness 1.9.0)
2
-
3
- > ⚠ **Owner 권한 필수**: npm `leerness`의 메인테이너는 `gytlrgpfl <gytlrgpfl96@gmail.com>` 입니다. 이 계정으로 로그인되어 있거나 collaborator로 등록되어 있어야 publish가 통과합니다. 권한이 없으면 `E403 Forbidden`이 떨어집니다.
4
-
5
- ## 1. 메타데이터 보강 (선택)
6
-
7
- `package.json`의 다음 필드는 비어 있거나 일반적입니다. 사용자 정보로 채우는 것을 권장합니다.
8
-
9
- ```json
10
- {
11
- "author": "leerness contributors",
12
- "repository": { /* 비어있음 */ },
13
- "bugs": { /* 비어있음 */ },
14
- "homepage": ""
15
- }
16
- ```
17
-
18
- 예:
19
-
20
- ```json
21
- "author": "Your Name <you@example.com>",
22
- "repository": { "type": "git", "url": "git+https://github.com/<user>/leerness.git" },
23
- "bugs": { "url": "https://github.com/<user>/leerness/issues" },
24
- "homepage": "https://github.com/<user>/leerness#readme"
25
- ```
26
-
27
- publish 필수는 아니지만, npm 페이지 신뢰도와 사용자 경험을 위해 채워두면 좋습니다.
28
-
29
- ## 2. 권한 확인
30
-
31
- ```bash
32
- npm whoami
33
- # → 응답이 leerness의 owner인지 확인
34
- npm owner ls leerness
35
- # → 자신의 계정이 목록에 있는지 확인
36
- ```
37
-
38
- 권한이 없으면 collaborator 추가를 owner에게 요청해야 합니다.
39
-
40
- ## 3. 로컬 검증
41
-
42
- ```bash
43
- node ./bin/harness.js --version # → 1.9.0
44
- npm pack --dry-run # 패키지 내용 미리보기
45
- node ./scripts/e2e.js # 30+개 시나리오 통과
46
- ```
47
-
48
- ## 4. 1.8.0 → 1.9.0 자동 업그레이드 시연 (선택)
49
-
50
- ```bash
51
- mkdir /tmp/lr-old; cd /tmp/lr-old
52
- npx -y leerness@1.8.0 init . --language ko --skills recommended
53
- LEERNESS_OFFLINE=1 npx -y -p /path/to/leerness-1.9.0.tgz leerness update . --yes
54
- cat .harness/HARNESS_VERSION # → 1.9.0
55
- ```
56
-
57
- ## 5. publish dry-run
58
-
59
- ```bash
60
- npm publish --dry-run
61
- # 또는
62
- npm publish leerness-1.9.0.tgz --dry-run
63
- ```
64
-
65
- 확인:
66
- - `package.json#files`와 실제 tarball 내용 일치
67
- - 총 크기 (≈30~40 kB)
68
- - bin → harness.js 매핑
69
-
70
- ## 6. 실 publish
71
-
72
- ```bash
73
- npm login # 권한 있는 계정으로
74
- npm publish --access public
75
- ```
76
-
77
- ## 7. publish 후 검증
78
-
79
- ```bash
80
- npm view leerness version # → 1.9.0
81
- npx -y leerness@latest --version # → 1.9.0
82
- mkdir test-install && cd test-install
83
- npx -y leerness@latest init . --yes --language ko --skills recommended
84
- npx -y leerness@latest verify .
85
- ```
86
-
87
- ## 8. 롤백
88
-
89
- 24시간 이내라면 `npm unpublish leerness@1.9.0` 가능. 이후엔 1.9.1 패치 또는 deprecate.
90
-
91
- ```bash
92
- npm deprecate leerness@1.9.0 "Use 1.9.1+"
93
- ```
1
+ # Publish Pre-check (leerness 1.9.0)
2
+
3
+ > ⚠ **Owner 권한 필수**: npm `leerness`의 메인테이너는 `gytlrgpfl <gytlrgpfl96@gmail.com>` 입니다. 이 계정으로 로그인되어 있거나 collaborator로 등록되어 있어야 publish가 통과합니다. 권한이 없으면 `E403 Forbidden`이 떨어집니다.
4
+
5
+ ## 1. 메타데이터 보강 (선택)
6
+
7
+ `package.json`의 다음 필드는 비어 있거나 일반적입니다. 사용자 정보로 채우는 것을 권장합니다.
8
+
9
+ ```json
10
+ {
11
+ "author": "leerness contributors",
12
+ "repository": { /* 비어있음 */ },
13
+ "bugs": { /* 비어있음 */ },
14
+ "homepage": ""
15
+ }
16
+ ```
17
+
18
+ 예:
19
+
20
+ ```json
21
+ "author": "Your Name <you@example.com>",
22
+ "repository": { "type": "git", "url": "git+https://github.com/<user>/leerness.git" },
23
+ "bugs": { "url": "https://github.com/<user>/leerness/issues" },
24
+ "homepage": "https://github.com/<user>/leerness#readme"
25
+ ```
26
+
27
+ publish 필수는 아니지만, npm 페이지 신뢰도와 사용자 경험을 위해 채워두면 좋습니다.
28
+
29
+ ## 2. 권한 확인
30
+
31
+ ```bash
32
+ npm whoami
33
+ # → 응답이 leerness의 owner인지 확인
34
+ npm owner ls leerness
35
+ # → 자신의 계정이 목록에 있는지 확인
36
+ ```
37
+
38
+ 권한이 없으면 collaborator 추가를 owner에게 요청해야 합니다.
39
+
40
+ ## 3. 로컬 검증
41
+
42
+ ```bash
43
+ node ./bin/leerness.js --version # → 1.9.0
44
+ npm pack --dry-run # 패키지 내용 미리보기
45
+ node ./scripts/e2e.js # 30+개 시나리오 통과
46
+ ```
47
+
48
+ ## 4. 1.8.0 → 1.9.0 자동 업그레이드 시연 (선택)
49
+
50
+ ```bash
51
+ mkdir /tmp/lr-old; cd /tmp/lr-old
52
+ npx -y leerness@1.8.0 init . --language ko --skills recommended
53
+ LEERNESS_OFFLINE=1 npx -y -p /path/to/leerness-1.9.0.tgz leerness update . --yes
54
+ cat .harness/HARNESS_VERSION # → 1.9.0
55
+ ```
56
+
57
+ ## 5. publish dry-run
58
+
59
+ ```bash
60
+ npm publish --dry-run
61
+ # 또는
62
+ npm publish leerness-1.9.0.tgz --dry-run
63
+ ```
64
+
65
+ 확인:
66
+ - `package.json#files`와 실제 tarball 내용 일치
67
+ - 총 크기 (≈30~40 kB)
68
+ - bin → leerness.js 매핑
69
+
70
+ ## 6. 실 publish
71
+
72
+ ```bash
73
+ npm login # 권한 있는 계정으로
74
+ npm publish --access public
75
+ ```
76
+
77
+ ## 7. publish 후 검증
78
+
79
+ ```bash
80
+ npm view leerness version # → 1.9.0
81
+ npx -y leerness@latest --version # → 1.9.0
82
+ mkdir test-install && cd test-install
83
+ npx -y leerness@latest init . --yes --language ko --skills recommended
84
+ npx -y leerness@latest verify .
85
+ ```
86
+
87
+ ## 8. 롤백
88
+
89
+ 24시간 이내라면 `npm unpublish leerness@1.9.0` 가능. 이후엔 1.9.1 패치 또는 deprecate.
90
+
91
+ ```bash
92
+ npm deprecate leerness@1.9.0 "Use 1.9.1+"
93
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.417",
3
+ "version": "1.9.419",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
@@ -28,13 +28,13 @@
28
28
  "license": "MIT",
29
29
  "author": "leerness contributors",
30
30
  "type": "commonjs",
31
- "main": "bin/harness.js",
31
+ "main": "bin/leerness.js",
32
32
  "preferGlobal": true,
33
33
  "engines": {
34
34
  "node": ">=18"
35
35
  },
36
36
  "bin": {
37
- "leerness": "bin/harness.js"
37
+ "leerness": "bin/leerness.js"
38
38
  },
39
39
  "files": [
40
40
  "bin",
@@ -47,10 +47,10 @@
47
47
  "LICENSE"
48
48
  ],
49
49
  "scripts": {
50
- "test": "node ./bin/harness.js --version && node ./bin/harness.js selftest && node ./scripts/e2e.js",
51
- "test:fast": "node ./bin/harness.js selftest && node ./scripts/smoke.js",
50
+ "test": "node ./bin/leerness.js --version && node ./bin/leerness.js selftest && node ./scripts/e2e.js",
51
+ "test:fast": "node ./bin/leerness.js selftest && node ./scripts/smoke.js",
52
52
  "test:smoke": "node ./scripts/e2e.js",
53
- "prepack": "node ./bin/harness.js readme sync . && node ./bin/harness.js --version"
53
+ "prepack": "node ./bin/leerness.js readme sync . && node ./bin/leerness.js --version"
54
54
  },
55
55
  "publishConfig": {
56
56
  "access": "public"
package/scripts/e2e.js CHANGED
@@ -11,7 +11,7 @@ process.env.LEERNESS_OFFLINE = process.env.LEERNESS_OFFLINE || '1';
11
11
  // 1.9.284 (UR-0029): e2e 속도 — 기본 roadmap.html(70KB HTML) 자동 생성 OFF (roadmap 전용 테스트 블록만 일시 ON).
12
12
  // 대부분의 init/session 테스트는 roadmap 을 검증하지 않으므로 생성 비용 제거 → 5분 내 완료.
13
13
  process.env.LEERNESS_NO_AUTO_ROADMAP = '1';
14
- const CLI = path.resolve(__dirname, '..', 'bin', 'harness.js');
14
+ const CLI = path.resolve(__dirname, '..', 'bin', 'leerness.js');
15
15
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-e2e-'));
16
16
  let failed = 0; let total = 0;
17
17
  const _e2eStart = Date.now(); // 1.9.284 (UR-0029): 총 소요시간 투명성
@@ -3081,7 +3081,7 @@ total++;
3081
3081
  {
3082
3082
  let ok = false;
3083
3083
  try {
3084
- const h = require(path.resolve(__dirname, '..', 'bin', 'harness.js'));
3084
+ const h = require(path.resolve(__dirname, '..', 'bin', 'leerness.js'));
3085
3085
  const q = h._shellQuoteArg('a; rm -rf / && echo $(whoami)');
3086
3086
  const win = process.platform === 'win32';
3087
3087
  // 따옴표로 감싸 메타문자가 단일 리터럴 인자가 됨 (POSIX 단일/Windows 이중)
@@ -3104,7 +3104,7 @@ total++;
3104
3104
  "const survived=process.listeners('warning').includes(L);" +
3105
3105
  "const polluted=(process.env.NODE_OPTIONS||'')!==o;" +
3106
3106
  "process.exit(survived&&!polluted?0:1);";
3107
- const harnessPath = path.resolve(__dirname, '..', 'bin', 'harness.js');
3107
+ const harnessPath = path.resolve(__dirname, '..', 'bin', 'leerness.js');
3108
3108
  const r = cp.spawnSync(process.execPath, ['-e', probe, harnessPath], { encoding: 'utf8', timeout: 20000 });
3109
3109
  ok = r.status === 0;
3110
3110
  } catch {}
@@ -3118,14 +3118,14 @@ total++;
3118
3118
  let ok = false;
3119
3119
  try {
3120
3120
  const reg = require(path.resolve(__dirname, '..', 'lib', 'agent-registry.js'));
3121
- const h = require(path.resolve(__dirname, '..', 'bin', 'harness.js'));
3121
+ const h = require(path.resolve(__dirname, '..', 'bin', 'leerness.js'));
3122
3122
  const dataOk = Array.isArray(reg.EXTERNAL_AGENTS) && reg.EXTERNAL_AGENTS.length === 10 &&
3123
3123
  reg.EXTERNAL_AGENTS.every(a => a.id && a.bin && a.envFlag) &&
3124
3124
  reg.AGENT_SLASH_COMMANDS && Object.keys(reg.AGENT_SLASH_COMMANDS).length === 9;
3125
3125
  // harness 가 모듈을 단일출처로 사용 (같은 객체 참조)
3126
3126
  const singleSource = h.AGENT_SLASH_COMMANDS === reg.AGENT_SLASH_COMMANDS;
3127
3127
  // harness.js 소스에 인라인 정의가 더 이상 없음 (모듈로 이동 완료)
3128
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
3128
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
3129
3129
  const movedOut = !/const EXTERNAL_AGENTS = \[/.test(harnessSrc) && /require\('\.\.\/lib\/agent-registry'\)/.test(harnessSrc);
3130
3130
  ok = dataOk && singleSource && movedOut;
3131
3131
  } catch {}
@@ -3143,7 +3143,7 @@ total++;
3143
3143
  const structOk = r.status === 0 && j.schemaVersion === 1 && !!j.version && !!j.project && ('currentTask' in j) &&
3144
3144
  j.openRequests && typeof j.openRequests.count === 'number' && Array.isArray(j.recentDecisions) &&
3145
3145
  Array.isArray(j.activeRules) && Array.isArray(j.nextActions) && j.memory && typeof j.memory.rulesActive === 'number';
3146
- const h = require(path.resolve(__dirname, '..', 'bin', 'harness.js'));
3146
+ const h = require(path.resolve(__dirname, '..', 'bin', 'leerness.js'));
3147
3147
  const mcpOk = h._mcpToolCount && h._mcpToolCount() >= 80;
3148
3148
  ok = structOk && mcpOk;
3149
3149
  } catch {}
@@ -3188,7 +3188,7 @@ total++;
3188
3188
  reg._PROVIDER_MODEL_CATALOG && Object.keys(reg._PROVIDER_MODEL_CATALOG).length === 10 &&
3189
3189
  reg._ROLE_ALIASES && Object.keys(reg._ROLE_ALIASES).length >= 14 &&
3190
3190
  reg._AGENT_ROLE_PROMPTS && reg._AGENT_ROLE_PROMPTS.actor;
3191
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
3191
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
3192
3192
  const movedOut = !/const ROLE_CATALOG = \{/.test(harnessSrc) && /require\('\.\.\/lib\/role-catalog'\)/.test(harnessSrc);
3193
3193
  // roles 명령이 여전히 동작 (모듈 require 후) — 회귀 방지
3194
3194
  const rr = cp.spawnSync(process.execPath, [CLI, 'roles', 'list', '--path', tmp, '--json'], { cwd: tmp, encoding: 'utf8', timeout: 20000 });
@@ -3210,7 +3210,7 @@ total++;
3210
3210
  reg.ADAPTERS && Object.keys(reg.ADAPTERS).length === 9 &&
3211
3211
  Array.isArray(reg.REUSE_CATEGORIES) && reg.REUSE_CATEGORIES.length === 15 &&
3212
3212
  Array.isArray(reg.REUSE_CHECKLIST) && reg.REUSE_CHECKLIST.length === 6;
3213
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
3213
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
3214
3214
  const movedOut = !/const CAPABILITY_SURFACE = \{/.test(harnessSrc) && !/const ADAPTERS = \{/.test(harnessSrc) && /require\('\.\.\/lib\/catalogs'\)/.test(harnessSrc);
3215
3215
  // 소비 명령 회귀: capabilities + reuse-check (카탈로그 require 후 동작)
3216
3216
  const cap = cp.spawnSync(process.execPath, [CLI, 'capabilities', '--json'], { cwd: tmp, encoding: 'utf8', timeout: 20000 });
@@ -3253,12 +3253,12 @@ total++;
3253
3253
  try {
3254
3254
  const T = require(path.resolve(__dirname, '..', 'lib', 'mcp-tools.js'));
3255
3255
  const dataOk = Array.isArray(T) && T.length >= 81 && T.every(t => t.name && t.description && t.inputSchema) && T[0].name === 'leerness_handoff';
3256
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
3256
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
3257
3257
  const movedOut = !/const TOOLS = \[/.test(harnessSrc) && /require\('\.\.\/lib\/mcp-tools'\)/.test(harnessSrc);
3258
3258
  // tools/list(라이브 MCP) == 모듈 length (단일출처 일치)
3259
3259
  const ml = cp.spawnSync(process.execPath, [CLI, 'mcp', 'serve'], { cwd: tmp, encoding: 'utf8', timeout: 15000, input: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }) + '\n' });
3260
3260
  const live = JSON.parse(ml.stdout.split('\n').filter(Boolean)[0]).result.tools.length;
3261
- const h = require(path.resolve(__dirname, '..', 'bin', 'harness.js'));
3261
+ const h = require(path.resolve(__dirname, '..', 'bin', 'leerness.js'));
3262
3262
  const singleSource = live === T.length && h._mcpToolCount() === T.length;
3263
3263
  ok = dataOk && movedOut && singleSource;
3264
3264
  } catch {}
@@ -3325,7 +3325,7 @@ total++;
3325
3325
  {
3326
3326
  let ok = false;
3327
3327
  try {
3328
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
3328
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
3329
3329
  // (1) 소스: cp.exec 템플릿 제거 + execFile args 배열(view pkg version) + argList 인용
3330
3330
  const srcOk = /'view', pkg, 'version'/.test(harnessSrc) && // 1.9.360(CV-2/UR-0077): cmd.exe /d /s /c npm view (args 배열) 형태
3331
3331
  !/cp\.exec\(.npm view \$\{pkg\}/.test(harnessSrc) &&
@@ -3442,7 +3442,7 @@ total++;
3442
3442
  a._evidenceQuality('src/api.js 수정, 12/12 통과 (Exit: 0)').ok === true &&
3443
3443
  a._shellGuardAnalyze('a && b', { shell: 'powershell', psVersion: '5' }).issues.some(i => i.rule === 'ps5-chain') &&
3444
3444
  a._claimFileInGit('src/api.js', new Set(['src/api.js'])) === true;
3445
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
3445
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
3446
3446
  const movedOut = !/function _evidenceQuality\(evidence\) \{/.test(harnessSrc) && !/function _shellGuardAnalyze\(cmd, ctx\) \{/.test(harnessSrc) && /require\('\.\.\/lib\/analyzers'\)/.test(harnessSrc);
3447
3447
  // 소비 명령 회귀: shell-guard (_shellGuardAnalyze 사용)
3448
3448
  const sg = cp.spawnSync(process.execPath, [CLI, 'shell-guard', 'a && b', '--json'], { cwd: tmp, encoding: 'utf8', timeout: 20000 });
@@ -3787,7 +3787,7 @@ total++;
3787
3787
  const work = m._htmlToText('<p>a <b>b</b></p>') === 'a b'
3788
3788
  && m._extractTitle('<title>T &amp; U</title>') === 'T & U'
3789
3789
  && m._extractLinks('<a href="/x">x</a><a href="https://o.com/y">y</a>', 'https://h.com/').length === 1; // same-domain only
3790
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
3790
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
3791
3791
  const movedOut = !/function _htmlToText\(html\) \{/.test(harnessSrc) && harnessSrc.includes('_htmlToText, _extractTitle, _extractLinks') && /require\('\.\.\/lib\/pure-utils'\)/.test(harnessSrc); // 1.9.324: import 순서 비의존(이후 import 추가 허용)
3792
3792
  const r = cp.spawnSync(process.execPath, [CLI, 'api-skill'], { encoding: 'utf8', timeout: 15000 }); // 소비 명령 로드
3793
3793
  const cmdOk = /api-skill/.test(r.stdout || '');
@@ -3900,7 +3900,7 @@ total++;
3900
3900
  const fnOk = typeof m._countDatedBlocks === 'function' && typeof m._extractDecisionBlocks === 'function';
3901
3901
  const work = m._countDatedBlocks('```md\n### 2026-01-01 — T\n```\n### 2026-06-05 — R\n') === 1
3902
3902
  && m._extractDecisionBlocks('### 2026-06-05 — A\n- Decision: x\n').length === 1;
3903
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
3903
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
3904
3904
  // 1.9.325: import 순서 비의존 — pure-utils 구조분해 블록을 추출해 이름 포함 확인(이후 import 추가 허용)
3905
3905
  const _puImport = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
3906
3906
  const movedOut = !/function _countDatedBlocks\(/.test(harnessSrc) && !/function _compareSemver\(/.test(harnessSrc)
@@ -3928,7 +3928,7 @@ total++;
3928
3928
  const work = m._classifyIntent('정확히 그것만').intent === 'precise'
3929
3929
  && m._classifyIntent('전체 다양한 기능').intent === 'broad'
3930
3930
  && m._classifyIntent('로그인 구현').intent === 'default';
3931
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
3931
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
3932
3932
  // import 순서 비의존: pure-utils 구조분해 블록 추출 후 이름 포함 확인
3933
3933
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
3934
3934
  const movedOut = !/function _classifyIntent\(/.test(harnessSrc) && _puImp.includes('_classifyIntent');
@@ -3955,7 +3955,7 @@ total++;
3955
3955
  && m._detectPwshFromEnv({ POWERSHELL_DISTRIBUTION_CHANNEL: 'X' }).version === '7'
3956
3956
  && m._detectPwshFromEnv({}).isPowerShell === false
3957
3957
  && /^['"]/.test(m._shellQuoteArg('a b'));
3958
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
3958
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
3959
3959
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0]; // import 순서 비의존
3960
3960
  const movedOut = !/function _sanitizeFences\(/.test(harnessSrc) && !/function _shellQuoteArg\(/.test(harnessSrc) && !/function _detectPwshFromEnv\(/.test(harnessSrc)
3961
3961
  && _puImp.includes('_sanitizeFences') && _puImp.includes('_shellQuoteArg') && _puImp.includes('_detectPwshFromEnv');
@@ -3978,7 +3978,7 @@ total++;
3978
3978
  const work = m._formatLocal('2026-06-05T01:13:00.000Z', { tz: 'Asia/Seoul' }) === '2026-06-05 10:13 KST' // UTC→KST +9h
3979
3979
  && m._formatLocal('2026-06-05T01:13:00.000Z', { tz: 'Asia/Seoul', dateOnly: true }) === '2026-06-05'
3980
3980
  && m._formatLocal('') === '?' && typeof m._getLocalTz() === 'string';
3981
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
3981
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
3982
3982
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0]; // import 순서 비의존
3983
3983
  const movedOut = !/function _formatLocal\(/.test(harnessSrc) && !/function _getLocalTz\(/.test(harnessSrc)
3984
3984
  && _puImp.includes('_getLocalTz') && _puImp.includes('_formatLocal');
@@ -3997,7 +3997,7 @@ total++;
3997
3997
  const work = typeof m._truncate === 'function' && typeof m._splitList === 'function'
3998
3998
  && m._truncate('hello world', 8) === 'hello w…' && m._truncate('hi', 8) === 'hi'
3999
3999
  && JSON.stringify(m._splitList('a, b ,c,')) === '["a","b","c"]';
4000
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4000
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4001
4001
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0]; // import 순서 비의존
4002
4002
  const movedOut = !/function _truncate\(/.test(harnessSrc) && !/function _splitList\(/.test(harnessSrc)
4003
4003
  && _puImp.includes('_truncate') && _puImp.includes('_splitList');
@@ -4017,7 +4017,7 @@ total++;
4017
4017
  && m._roadmapMapStatus('REQUESTED') === 'planned' && m._roadmapMapStatus('done') === 'done'
4018
4018
  && m._roadmapParseMilestones('### M-0001. 로그인\nStatus: in-progress\nProgress: 40%')[0].progress === 40
4019
4019
  && m._roadmapParseTokens('| color | #fff |').color === '#fff';
4020
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4020
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4021
4021
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0]; // import 순서 비의존
4022
4022
  const movedOut = !/function _roadmapMapStatus\(/.test(harnessSrc) && !/function _roadmapParseMilestones\(/.test(harnessSrc) && !/function _roadmapParseTokens\(/.test(harnessSrc)
4023
4023
  && _puImp.includes('_roadmapMapStatus') && _puImp.includes('_roadmapParseMilestones') && _puImp.includes('_roadmapParseTokens');
@@ -4035,7 +4035,7 @@ total++;
4035
4035
  const m = require(path.resolve(__dirname, '..', 'lib', 'pure-utils.js'));
4036
4036
  const cfgOk = Array.isArray(m._BRIEF_FIELDS) && m._BRIEF_FIELDS.length === 10 && m._BRIEF_FIELDS[0].key === 'intro';
4037
4037
  const work = m._briefFilled({ intro: 'x', features: ['a'] }) === 2 && m._briefFilled({}) === 0;
4038
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4038
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4039
4039
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0]; // import 순서 비의존
4040
4040
  const movedOut = !/const _BRIEF_FIELDS = \[/.test(harnessSrc) && !/function _briefFilled\(/.test(harnessSrc)
4041
4041
  && _puImp.includes('_BRIEF_FIELDS') && _puImp.includes('_briefFilled');
@@ -4062,7 +4062,7 @@ total++;
4062
4062
  const work = typeof m._briefReadmeBlock === 'function' && typeof m._briefBlueprint === 'function'
4063
4063
  && m._briefReadmeBlock(b).includes(m.BRIEF_START) && /f1/.test(m._briefReadmeBlock(b))
4064
4064
  && /Blueprint/.test(m._briefBlueprint(b, '9.9.9')) && /leerness v9\.9\.9/.test(m._briefBlueprint(b, '9.9.9')); // VERSION 주입
4065
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4065
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4066
4066
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0]; // import 순서 비의존
4067
4067
  const movedOut = !/function _briefReadmeBlock\(/.test(harnessSrc) && !/function _briefBlueprint\(/.test(harnessSrc) && !/^const BRIEF_START =/m.test(harnessSrc)
4068
4068
  && _puImp.includes('_briefReadmeBlock') && _puImp.includes('_briefBlueprint') && _puImp.includes('BRIEF_START');
@@ -4088,7 +4088,7 @@ total++;
4088
4088
  const m = require(path.resolve(__dirname, '..', 'lib', 'pure-utils.js'));
4089
4089
  const r = m._parseLessonEntries('### 2026-06-05\n- Lesson: A\n- Tag: t\n\n### 2026-06-04\n- Lesson: B');
4090
4090
  const work = typeof m._parseLessonEntries === 'function' && r.length === 2 && r[0].text === 'A' && r[0].tag === 't' && r[1].tag === null;
4091
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4091
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4092
4092
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0]; // import 순서 비의존
4093
4093
  const movedOut = !/function _parseLessonEntries\(/.test(harnessSrc) && _puImp.includes('_parseLessonEntries');
4094
4094
  // 소비 명령 회귀: lesson save + list --json
@@ -4114,7 +4114,7 @@ total++;
4114
4114
  const catOk = c._DEFAULT_PLATFORM_CONSTRAINTS && Object.keys(c._DEFAULT_PLATFORM_CONSTRAINTS.platforms).length === 6;
4115
4115
  const r = m._matchConstraints(c._DEFAULT_PLATFORM_CONSTRAINTS, 'stripe 결제');
4116
4116
  const work = catOk && r.matched.length === 1 && r.matched[0].platform === 'stripe' && r.totalPlatforms === 6 && m._matchConstraints(null, 'x').matched.length === 0;
4117
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4117
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4118
4118
  // 1.9.334: catalogs import 블록 추출 후 이름 포함 확인(순서/추가 비의존 — 이후 import 추가 허용)
4119
4119
  const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0];
4120
4120
  const movedOut = !/const _DEFAULT_PLATFORM_CONSTRAINTS = \{/.test(harnessSrc) && harnessSrc.includes('_matchConstraints(_loadPlatformConstraints(root), text)')
@@ -4141,7 +4141,7 @@ total++;
4141
4141
  const catOk = c._DEFAULT_DOMAIN_CATALOG && Object.keys(c._DEFAULT_DOMAIN_CATALOG.domains).length === 5;
4142
4142
  const r = m._matchDomain(c._DEFAULT_DOMAIN_CATALOG, 'unity 게임');
4143
4143
  const work = catOk && typeof m._matchDomain === 'function' && r.domain === 'game' && Array.isArray(r.components) && m._matchDomain(null, 'x').domain === null;
4144
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4144
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4145
4145
  const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
4146
4146
  const movedOut = !/const _DEFAULT_DOMAIN_CATALOG = \{/.test(harnessSrc) && harnessSrc.includes('_matchDomain(_loadDomainCatalog(root), text)')
4147
4147
  && _catImp.includes('_DEFAULT_DOMAIN_CATALOG');
@@ -4168,7 +4168,7 @@ total++;
4168
4168
  const langOk = m._detectLspLang('x.py') === 'python' && m._detectLspLang('y.rs') === 'rust' && m._detectLspLang('z.txt') === 'javascript';
4169
4169
  const sy = m._matchLspSymbols(c._LSP_LANG_PATTERNS, 'def foo():\n pass\nclass Bar:\n pass', 'python');
4170
4170
  const work = catOk && langOk && typeof m._matchLspSymbols === 'function' && sy.length === 2 && sy[0].name === 'foo' && sy[0].kind === 'function' && sy[1].name === 'Bar' && m._matchLspSymbols(null, 'x', 'javascript').length === 0;
4171
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4171
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4172
4172
  const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
4173
4173
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
4174
4174
  const movedOut = !/const _LSP_LANG_PATTERNS = \{/.test(harnessSrc) && !/function _detectLspLang\(/.test(harnessSrc)
@@ -4199,7 +4199,7 @@ total++;
4199
4199
  const work = catOk && typeof m._detectOptimism === 'function' && sus.some(s => s.kind === 'API' && s.severity === 'high') && conf < 0.5
4200
4200
  && m._computeConfidence(c.OPTIMISM_PATTERNS, '그냥 정리함', 'x') === 1 && m._detectOptimism(null, ev, 'x').length === 0
4201
4201
  && m._extractUrlClaims('POST /a/b').length === 1 && m._verifyUrlClaim({ path: '/a/b' }, 'has /a/b') === true;
4202
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4202
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4203
4203
  const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
4204
4204
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
4205
4205
  const movedOut = !/const OPTIMISM_PATTERNS = \[/.test(harnessSrc) && !/function _extractUrlClaims\(/.test(harnessSrc)
@@ -4228,7 +4228,7 @@ total++;
4228
4228
  const catOk = c.BUILT_IN_PERSONAS && Object.keys(c.BUILT_IN_PERSONAS).length === 5 && c.BUILT_IN_PERSONAS.security && typeof c.BUILT_IN_PERSONAS.security.body === 'string';
4229
4229
  const sm = m._personaSummaries(c.BUILT_IN_PERSONAS);
4230
4230
  const work = catOk && typeof m._personaSummaries === 'function' && Array.isArray(sm) && sm.length === 5 && sm[0].id === 'security' && sm[0].body === undefined && m._personaSummaries(null).length === 0;
4231
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4231
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4232
4232
  const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
4233
4233
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
4234
4234
  const movedOut = !/const BUILT_IN_PERSONAS = \{/.test(harnessSrc) && _catImp.includes('BUILT_IN_PERSONAS') && _puImp.includes('_personaSummaries');
@@ -4262,7 +4262,7 @@ total++;
4262
4262
  && m._translate(c.STRINGS, 'no.such.key', 'en') === 'no.such.key'
4263
4263
  && m._translate(null, 'x', 'ko') === 'x'
4264
4264
  && m._translate({ k: { ko: '케이' } }, 'k', 'en') === '케이';
4265
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4265
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4266
4266
  const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
4267
4267
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
4268
4268
  // _t 박막: 인라인 STRINGS 정의 제거 + import + _translate(STRINGS,..) 호출
@@ -4378,7 +4378,7 @@ total++;
4378
4378
  const work = catOk && typeof m._withBuiltinSource === 'function' && Object.keys(out).length === 9
4379
4379
  && Object.values(out).every(v => v._source === 'builtin') && Array.isArray(out.office.capabilities)
4380
4380
  && Object.keys(m._withBuiltinSource(null)).length === 0;
4381
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4381
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4382
4382
  const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
4383
4383
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
4384
4384
  const movedOut = !/const BUILTIN_CATALOG = \{/.test(harnessSrc) && _catImp.includes('BUILTIN_CATALOG') && _puImp.includes('_withBuiltinSource')
@@ -4405,7 +4405,7 @@ total++;
4405
4405
  const mapsOk = c.ROADMAP_STATUS_LABEL && c.ROADMAP_STATUS_COLOR
4406
4406
  && Object.keys(c.ROADMAP_STATUS_LABEL).length === 11 && Object.keys(c.ROADMAP_STATUS_COLOR).length === 11
4407
4407
  && c.ROADMAP_STATUS_LABEL.done === '완료' && c.ROADMAP_STATUS_COLOR.done === '#16a34a' && c.ROADMAP_STATUS_COLOR.skill === '#8b5cf6';
4408
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4408
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4409
4409
  const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
4410
4410
  const movedOut = !/const ROADMAP_STATUS_LABEL = \{/.test(harnessSrc) && !/const ROADMAP_STATUS_COLOR = \{/.test(harnessSrc)
4411
4411
  && _catImp.includes('ROADMAP_STATUS_LABEL') && _catImp.includes('ROADMAP_STATUS_COLOR');
@@ -4433,7 +4433,7 @@ total++;
4433
4433
  const hit = (s) => c.SECRET_PATTERNS.some(p => { p.re.lastIndex = 0; return p.re.test(s); });
4434
4434
  const catOk = Array.isArray(c.SECRET_PATTERNS) && c.SECRET_PATTERNS.length === 20
4435
4435
  && hit('AKIA' + 'ABCD1234EFGH5678') && hit('sk-' + 'ant-api03-' + A + '_' + A) && !hit('const u = "john' + '_doe_2024";');
4436
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4436
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4437
4437
  const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
4438
4438
  // 모듈 레벨 정의 제거(블록 지역 .env 배열은 보존) + import
4439
4439
  const movedOut = !/const SECRET_PATTERNS = \[\r?\n\s*\{ name:/.test(harnessSrc) && _catImp.includes('SECRET_PATTERNS')
@@ -4467,7 +4467,7 @@ total++;
4467
4467
  const catOk = c.SKILL_CATALOG_PRESETS && Object.keys(c.SKILL_CATALOG_PRESETS).length === 2
4468
4468
  && c.SKILL_CATALOG_PRESETS.vercel && c.SKILL_CATALOG_PRESETS.vercel.owner === 'vercel-labs'
4469
4469
  && c.SKILL_CATALOG_PRESETS.anthropic && c.SKILL_CATALOG_PRESETS.anthropic.repo === 'skills';
4470
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4470
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4471
4471
  const _catImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/catalogs'\)/) || [''])[0]; // import 순서/추가 비의존
4472
4472
  const movedOut = !/const SKILL_CATALOG_PRESETS = \{/.test(harnessSrc) && _catImp.includes('SKILL_CATALOG_PRESETS');
4473
4473
  // 소비 회귀: skill discover 가 preset 목록을 catalog 에서 노출 (네트워크 없이 unknown preset → 사용가능 목록)
@@ -4493,7 +4493,7 @@ total++;
4493
4493
  && m._esc('&<>"\'') === '&amp;&lt;&gt;&quot;&#39;'
4494
4494
  && m._esc('<script>x</script>') === '&lt;script&gt;x&lt;/script&gt;'
4495
4495
  && m._esc(null) === '' && m._esc(undefined) === '' && m._esc(42) === '42';
4496
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4496
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4497
4497
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
4498
4498
  const movedOut = !/function _esc\(/.test(harnessSrc) && _puImp.includes('_esc');
4499
4499
  // 소비 회귀: roadmap.html 이 악성 task 제목을 이스케이프 (인젝션 방지)
@@ -4521,7 +4521,7 @@ total++;
4521
4521
  const pureOk = typeof m._roadmapTokenStyles === 'function' && out.startsWith(':root {')
4522
4522
  && out.includes('--lr-primary: #2563eb') && out.includes('--lr-surface: #fff') && out.includes('--lr-custom: #abc')
4523
4523
  && out.includes('--lr-card-bg') && out.includes('--lr-page-bg') && m._roadmapTokenStyles(null, null).startsWith(':root {');
4524
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4524
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4525
4525
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
4526
4526
  const movedOut = !/function _roadmapTokenStyles\(/.test(harnessSrc) && _puImp.includes('_roadmapTokenStyles');
4527
4527
  // 소비 회귀: roadmap.html 이 :root CSS 변수 주입
@@ -4548,7 +4548,7 @@ total++;
4548
4548
  const pureOk = typeof m._parseSkillMd === 'function' && r.meta.name === 's1' && r.meta.description === 'd1' && r.body === 'body'
4549
4549
  && m._parseSkillMd('---\nname: b\n---\nx').meta.name === 'b'
4550
4550
  && Object.keys(m._parseSkillMd('plain').meta).length === 0 && m._parseSkillMd(null).body === '';
4551
- const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'harness.js'), 'utf8');
4551
+ const harnessSrc = fs.readFileSync(path.resolve(__dirname, '..', 'bin', 'leerness.js'), 'utf8');
4552
4552
  const _puImp = (harnessSrc.match(/const \{[\s\S]*?\} = require\('\.\.\/lib\/pure-utils'\)/) || [''])[0];
4553
4553
  const movedOut = !/function _parseSkillMd\(/.test(harnessSrc) && _puImp.includes('_parseSkillMd');
4554
4554
  // 소비 회귀: skill install 이 BOM 포함 SKILL.md 를 정상 설치 (frontmatter name 파싱)
@@ -6008,5 +6008,31 @@ total++;
6008
6008
  if (!ok) failed++;
6009
6009
  }
6010
6010
 
6011
+ // 1.9.418 회귀 (9th 외부평가 Codex P2, UR-0121 잔여): health 보안 정직화 + status scope
6012
+ total++;
6013
+ {
6014
+ let ok = false;
6015
+ try {
6016
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-hlabel-'));
6017
+ cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
6018
+ fs.writeFileSync(path.join(d, 'leak.js'), 'module.exports={apiKey:"sk-test-1234567890abcdefghijklmnopqrstuvwxyz"};');
6019
+ // (1) 시크릿 있으면 health healthy:false + committedSecrets>0
6020
+ const hj = cp.spawnSync(process.execPath, [CLI, 'health', d, '--json'], { encoding: 'utf8', timeout: 20000 });
6021
+ let secOk = false; try { const j = JSON.parse(hj.stdout); secOk = j.healthy === false && j.checks.security.critical === true && j.checks.security.committedSecrets >= 1; } catch {}
6022
+ // (2) status scope:install + healthyMeaning
6023
+ const sj = cp.spawnSync(process.execPath, [CLI, 'status', d, '--json'], { encoding: 'utf8', timeout: 15000 });
6024
+ let scopeOk = false; try { const j = JSON.parse(sj.stdout); scopeOk = j.scope === 'install' && typeof j.healthyMeaning === 'string' && j.healthyMeaning.length > 0; } catch {}
6025
+ // (3) 회귀: 클린 워크스페이스 health healthy:true
6026
+ const dc = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-hclean-'));
6027
+ cp.spawnSync(process.execPath, [CLI, 'init', dc, '--yes', '--language', 'ko'], { encoding: 'utf8', timeout: 30000 });
6028
+ const hc = cp.spawnSync(process.execPath, [CLI, 'health', dc, '--json'], { encoding: 'utf8', timeout: 20000 });
6029
+ let cleanOk = false; try { const j = JSON.parse(hc.stdout); cleanOk = j.checks.security.committedSecrets === 0; } catch {}
6030
+ fs.rmSync(d, { recursive: true, force: true }); fs.rmSync(dc, { recursive: true, force: true });
6031
+ ok = secOk && scopeOk && cleanOk;
6032
+ } catch {}
6033
+ console.log(ok ? '✓ B(1.9.418) 9th외부평가: health 보안 정직화(커밋 시크릿→healthy:false) + status scope:install (UR-0121 잔여)' : '✗ health/status 라벨 실패');
6034
+ if (!ok) failed++;
6035
+ }
6036
+
6011
6037
  console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
6012
6038
  if (failed > 0) process.exit(1);
package/scripts/smoke.js CHANGED
@@ -12,7 +12,7 @@ const path = require('path');
12
12
  const cp = require('child_process');
13
13
 
14
14
  process.env.LEERNESS_OFFLINE = process.env.LEERNESS_OFFLINE || '1';
15
- const CLI = path.resolve(__dirname, '..', 'bin', 'harness.js');
15
+ const CLI = path.resolve(__dirname, '..', 'bin', 'leerness.js');
16
16
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-smoke-'));
17
17
  let failed = 0, total = 0;
18
18
  const t0 = Date.now();