leerness 1.9.316 → 1.9.317

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,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.317 — 2026-06-04 — UR-0051: 텔레메트리 분리 (설치리뷰)
4
+
5
+ **📊 내부 auto-call 이 usage 통계를 오염시켜 거짓 skill 추천을 유발하던 결함 수정.**
6
+
7
+ ### 배경 (설치리뷰)
8
+ "텔레메트리 오염 — 내부 auto-call 이 사용자명령 집계 → 거짓 skill 추천 + task add ~550ms". leerness 가 자기 자신을 spawn 하는 내부 auto-call(`task add` → `review-request`, MCP `callLeerness`, session close/handoff/drift 자동 보조 호출 등 ~20곳)이 dispatch 의 usage 카운터에 **사용자 명령처럼 집계** → `skill suggest`(명령 빈도 기반)가 거짓 후보 추천.
9
+
10
+ ### 구현 (UR-0051)
11
+ 1. **`LEERNESS_INTERNAL=1` 마커**: 모든 내부 self-spawn(`cp.spawnSync(process.execPath, [__filename, …])`)의 env 에 마킹 (~20곳). 공통 env 프래그먼트 일괄 적용 + env 없던 3곳(task add/session close 자동) 보강.
12
+ 2. **usage 집계 가드**: dispatch 의 `_bumpUsage` 호출 조건에 `process.env.LEERNESS_INTERNAL !== '1'` 추가 — 내부 호출은 집계 제외.
13
+ 3. selftest 64→65 · e2e 261→262.
14
+
15
+ ### 검증
16
+ - **selftest 65/65 PASS** · **E2E 262/262 PASS** (회귀 0 — ~20곳 마킹에도 모든 auto-call 정상).
17
+ - 실측: `task add` 1회 → usage `{task:1}` (이전엔 `review-request` 도 집계) · 일반 drift 1→2 집계, **`LEERNESS_INTERNAL=1` drift → 미집계(2 유지)**.
18
+ - 범위: usage(skill 추천) 오염 제거. task add 지연(~550ms, review-request spawn 자체)은 별도 — 후속 캐싱/비동기화 검토 여지.
19
+
3
20
  ## 1.9.316 — 2026-06-04 — 🐛 drift 'session close 누락' 영구 오발화 버그 (자가 발견)
4
21
 
5
22
  **🔧 session close 직후에도 drift 가 `session close 누락`(13일)을 보고하던 버그 수정 — 4라운드 연속 관찰된 leerness 자체 정확성 결함.**
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.316-green)]() [![tests](https://img.shields.io/badge/e2e-261%2F261-success)]() [![selftest](https://img.shields.io/badge/selftest-64-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-83-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.317-green)]() [![tests](https://img.shields.io/badge/e2e-262%2F262-success)]() [![selftest](https://img.shields.io/badge/selftest-65-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-83-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.316 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
474
+ 이 프로젝트는 Leerness v1.9.317 하네스를 사용합니다. 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.316는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **83개 도구**를 노출:
528
+ Leerness v1.9.317는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **83개 도구**를 노출:
529
529
 
530
530
  ```jsonc
531
531
  // 카테고리별
@@ -546,7 +546,7 @@ Leerness v1.9.316는 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.316)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
549
+ 현재 누적: **70 라운드 (1.9.40 → 1.9.317)** · 매 라운드 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.316: 2026-06-04
587
+ Last synced by Leerness v1.9.317: 2026-06-04
588
588
  <!-- leerness:project-readme:end -->
589
589
 
package/bin/harness.js CHANGED
@@ -14,7 +14,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
14
14
  // 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
15
15
  const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST } = require('../lib/catalogs');
16
16
 
17
- const VERSION = '1.9.316';
17
+ const VERSION = '1.9.317';
18
18
 
19
19
  // 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
20
20
  // 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
@@ -2517,7 +2517,7 @@ function agentModeCmd(root, sub) {
2517
2517
  const yl = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
2518
2518
  const dm = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
2519
2519
  function spawnChild(args, opts) {
2520
- return cp.spawnSync(process.execPath, [__filename, ...args], Object.assign({ encoding: 'utf8', cwd: root, env: { ...process.env, LEERNESS_NO_BANNER: '1', LEERNESS_NO_PROMPT: '1' } }, opts || {}));
2520
+ return cp.spawnSync(process.execPath, [__filename, ...args], Object.assign({ encoding: 'utf8', cwd: root, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_BANNER: '1', LEERNESS_NO_PROMPT: '1' } }, opts || {}));
2521
2521
  }
2522
2522
  if (!sub || sub === 'help') {
2523
2523
  log(cy(`# leerness agent-mode (1.9.239) — 자율 모드 전용 통합 명령`));
@@ -3115,6 +3115,7 @@ function _selfTestCases() {
3115
3115
  { name: 'PowerShell 감지: pwsh7(channel/Documents\\PowerShell/install) + ps5.1 영구경로 과경고 안함 (UR-0052 설치리뷰 1.9.314)', run: () => { const f = _detectPwshFromEnv; const pwsh7a = f({ POWERSHELL_DISTRIBUTION_CHANNEL: 'MSI:Windows 10' }).version === '7'; const pwsh7b = f({ PSModulePath: 'C:\\Users\\me\\Documents\\PowerShell\\Modules' }).version === '7'; const pwsh7c = f({ PSModulePath: 'C:\\Program Files\\PowerShell\\7\\Modules' }).version === '7'; const noFalsePs5 = f({ PSModulePath: 'C:\\Users\\me\\Documents\\WindowsPowerShell\\Modules' }).isPowerShell === false; const cmdSys = f({ PSModulePath: 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\Modules' }).isPowerShell === false; const empty = f({}).isPowerShell === false; const src = read(__filename); const wired = src.includes('const fromEnv = _detectPwshFromEnv()') && src.includes('const pwshEnv = _detectPwshFromEnv()'); return pwsh7a && pwsh7b && pwsh7c && noFalsePs5 && cmdSys && empty && wired; } },
3116
3116
  { name: 'doc/surface 정합: doctor 명령 + stale MCP 카운트 동적화(commands/banner) (UR-0054 설치리뷰 1.9.315)', run: () => { const src = read(__filename); const doctorOk = typeof doctorCmd === 'function' && /cmd === 'doctor'/.test(src) && /# leerness doctor/.test(src); const dynCount = /MCP 도구: \$\{_mcpToolCount\(\)\}/.test(src) && /외부 AI 통합 \(MCP \$\{_mcpToolCount\(\)\} 도구\)/.test(src); return doctorOk && dynCount; } },
3117
3117
  { name: 'drift 마커 버그: session-handoff 프론트매터는 ^--- 일 때만 + drift 최신 Last generated (1.9.316)', run: () => { const src = read(__filename); const writeFix = src.includes('if (/^---\\r?\\n/.test(cur))') && src.includes('writeUtf8(handoffPath(root), frontmatter + block)'); const readFix = src.includes('matchAll(/Last generated') && src.includes('allGen[allGen.length - 1]'); return writeFix && readFix; } },
3118
+ { name: '텔레메트리 분리: 내부 auto-call(LEERNESS_INTERNAL) usage 집계 제외 + 주요 spawn 마킹 (UR-0051 설치리뷰 1.9.317)', run: () => { const src = read(__filename); const guard = src.includes("process.env.LEERNESS_INTERNAL !== '1'"); const marked = (src.match(/LEERNESS_INTERNAL: '1'/g) || []).length >= 10; const reviewMarked = /'review-request'[\s\S]{0,200}LEERNESS_INTERNAL: '1'/.test(src); return guard && marked && reviewMarked; } },
3118
3119
  { name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
3119
3120
  ];
3120
3121
  }
@@ -5890,7 +5891,7 @@ async function nextActionCmd(root, sub, ...rest) {
5890
5891
  state.queue.splice(n, 1);
5891
5892
  _writeNextActionQueue(root, state.queue);
5892
5893
  // leerness task add 호출
5893
- const taskResult = cp.spawnSync(process.execPath, [__filename, 'task', 'add', taskTitle, '--path', root], { encoding: 'utf8', timeout: 8000 });
5894
+ const taskResult = cp.spawnSync(process.execPath, [__filename, 'task', 'add', taskTitle, '--path', root], { encoding: 'utf8', timeout: 8000, env: { ...process.env, LEERNESS_INTERNAL: '1' } });
5894
5895
  if (taskResult.status === 0) {
5895
5896
  const m = (taskResult.stdout || '').match(/T-\d{4}/);
5896
5897
  log(` ✓ task 추가: ${m ? m[0] : '?'} — "${taskTitle}"`);
@@ -6411,7 +6412,7 @@ function taskAdd(root, text) {
6411
6412
  const t0 = Date.now();
6412
6413
  const r = cp.spawnSync(process.execPath, [__filename, 'review-request', text, '--path', absRoot(root), '--json'], {
6413
6414
  encoding: 'utf8', timeout: 15000,
6414
- env: { ...process.env, LEERNESS_NO_BANNER: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' }
6415
+ env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_BANNER: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' }
6415
6416
  });
6416
6417
  if (r.status === 0 && r.stdout) {
6417
6418
  const j = JSON.parse(r.stdout);
@@ -7918,7 +7919,7 @@ function handoff(root) {
7918
7919
  // 1) drift level (가벼운 check)
7919
7920
  try {
7920
7921
  const r = cp.spawnSync(process.execPath, [__filename, 'drift', 'check', root, '--json'],
7921
- { encoding: 'utf8', timeout: 8000, env: { ...process.env, LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '0' } });
7922
+ { encoding: 'utf8', timeout: 8000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '0' } });
7922
7923
  const j = JSON.parse(r.stdout.trim());
7923
7924
  if (j.level) parts.push(`drift ${j.level.replace(/^[^\w]+/, '')} (${j.score})`);
7924
7925
  } catch {}
@@ -8516,7 +8517,7 @@ function handoff(root) {
8516
8517
  if (!has('--no-brainstorm-hits') && !has('--quiet') && process.env.LEERNESS_NO_BRAINSTORM_HITS !== '1') {
8517
8518
  try {
8518
8519
  const r = cp.spawnSync(process.execPath, [__filename, 'brainstorm', keyword, '--path', root, '--json'],
8519
- { encoding: 'utf8', timeout: 8000, env: { ...process.env, LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' } });
8520
+ { encoding: 'utf8', timeout: 8000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' } });
8520
8521
  const bj = JSON.parse(r.stdout);
8521
8522
  const hits = bj.hits || {};
8522
8523
  const items = [];
@@ -9019,7 +9020,7 @@ function handoff(root) {
9019
9020
  if (process.env.LEERNESS_AUTO_SECURITY_FIX === '1') {
9020
9021
  try {
9021
9022
  const r = cp.spawnSync(process.execPath, [__filename, 'audit', root, '--fix'],
9022
- { encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' } });
9023
+ { encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' } });
9023
9024
  if (r.status === 0) {
9024
9025
  log(dim(` ✓ 자동 회복 (LEERNESS_AUTO_SECURITY_FIX=1): audit --fix 완료`));
9025
9026
  } else {
@@ -9264,7 +9265,7 @@ function handoffCmd(root) {
9264
9265
  if (has('--auto-recover') && sevStale) {
9265
9266
  log(dim(` 🔧 --auto-recover 활성 — session close 자동 실행 중...`));
9266
9267
  try {
9267
- const r = cp.spawnSync(process.execPath, [__filename, 'session', 'close', absR0], { encoding: 'utf8', timeout: 60000 });
9268
+ const r = cp.spawnSync(process.execPath, [__filename, 'session', 'close', absR0], { encoding: 'utf8', timeout: 60000, env: { ...process.env, LEERNESS_INTERNAL: '1' } });
9268
9269
  if (r.status === 0) {
9269
9270
  log(dim(` ✓ session close 자동 완료 (다음 라운드부터 healthy)`));
9270
9271
  const s2 = _readUsageStats(absR0);
@@ -12302,7 +12303,7 @@ function sessionClose(root, opts = {}) {
12302
12303
  // 1) skill suggest
12303
12304
  try {
12304
12305
  const r = cp.spawnSync(process.execPath, [__filename, 'skill', 'suggest', '--path', root, '--min', '3', '--json'],
12305
- { encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' } });
12306
+ { encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' } });
12306
12307
  const j = JSON.parse(r.stdout);
12307
12308
  if (j.candidates && j.candidates.length) {
12308
12309
  log(dim(' 📌 신규 skill 후보 (Hermes-style 자동 학습):'));
@@ -12313,7 +12314,7 @@ function sessionClose(root, opts = {}) {
12313
12314
  // 2) drift check
12314
12315
  try {
12315
12316
  const r = cp.spawnSync(process.execPath, [__filename, 'drift', 'check', root, '--json'],
12316
- { encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '0' } });
12317
+ { encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '0' } });
12317
12318
  const j = JSON.parse(r.stdout.trim());
12318
12319
  if (j.level) {
12319
12320
  log(dim(` 🩺 drift 상태: ${j.level} ${j.score}/200`));
@@ -15740,7 +15741,7 @@ function driftCheckCmd(root, opts = {}) {
15740
15741
  log(`🔒 --auto-fix 활성 (1.9.82) — 보안 신호 회복: audit --fix 자동 실행 중...`);
15741
15742
  try {
15742
15743
  const r = cp.spawnSync(process.execPath, [__filename, 'audit', root, '--fix'],
15743
- { encoding: 'utf8', timeout: 30000, env: { ...process.env, LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' } });
15744
+ { encoding: 'utf8', timeout: 30000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' } });
15744
15745
  if (r.status === 0) {
15745
15746
  log(`✓ audit --fix 완료 — .gitignore + .env.example 동기화`);
15746
15747
  // 재검사 (보안 신호 회복 확인)
@@ -15853,7 +15854,7 @@ function driftCheckCmd(root, opts = {}) {
15853
15854
  log('');
15854
15855
  log(`🔧 --auto-fix 활성 — session close 자동 실행 중...`);
15855
15856
  try {
15856
- const r = cp.spawnSync(process.execPath, [__filename, 'session', 'close', root], { encoding: 'utf8', timeout: 60000 });
15857
+ const r = cp.spawnSync(process.execPath, [__filename, 'session', 'close', root], { encoding: 'utf8', timeout: 60000, env: { ...process.env, LEERNESS_INTERNAL: '1' } });
15857
15858
  if (r.status === 0) {
15858
15859
  log(`✓ session close 자동 완료`);
15859
15860
  // autoResolved 카운트
@@ -16134,7 +16135,7 @@ function skillPublishCmd(root) {
16134
16135
  if (!has('--no-security-check') && !has('--force')) {
16135
16136
  try {
16136
16137
  const r = cp.spawnSync(process.execPath, [__filename, 'health', root, '--json'],
16137
- { encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '0' } });
16138
+ { encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '0' } });
16138
16139
  const j = JSON.parse(r.stdout.trim());
16139
16140
  if (j.issues && j.issues.length > 0) {
16140
16141
  log(`🚨 보안 사전 점검 (1.9.98): ${j.issues.length}건 issue 발견`);
@@ -16217,11 +16218,11 @@ const BENCHMARK_SCENARIOS = {
16217
16218
  setup: (dir) => {
16218
16219
  // 빈 evidence로 done task 생성
16219
16220
  cp.spawnSync(process.execPath, [__filename, 'task', 'add', '거짓 완료된 작업', '--status', 'done', '--evidence', '', '--path', dir],
16220
- { encoding: 'utf8', timeout: 10000, env: { ...process.env, LEERNESS_NO_PROMPT: '1' } });
16221
+ { encoding: 'utf8', timeout: 10000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_PROMPT: '1' } });
16221
16222
  },
16222
16223
  measure: (dir) => {
16223
16224
  const r = cp.spawnSync(process.execPath, [__filename, 'lazy', 'detect', dir],
16224
- { encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_NO_DRIFT_CHECK: '1' } });
16225
+ { encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_DRIFT_CHECK: '1' } });
16225
16226
  const detected = /✗ |found.*issue|증거 없는|empty/.test(r.stdout);
16226
16227
  return { detected, exit: r.status, sample: r.stdout.slice(0, 200) };
16227
16228
  }
@@ -16236,7 +16237,7 @@ const BENCHMARK_SCENARIOS = {
16236
16237
  measure: (dir) => {
16237
16238
  const r = cp.spawnSync(process.execPath, [__filename, 'contract', 'verify',
16238
16239
  path.join(dir, 'mismatch-spec.md'), path.join(dir, 'mismatch-impl.js'), '--json'],
16239
- { encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_NO_DRIFT_CHECK: '1' } });
16240
+ { encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_DRIFT_CHECK: '1' } });
16240
16241
  let j = null;
16241
16242
  try { j = JSON.parse(r.stdout); } catch {}
16242
16243
  const detected = j && j.missingFunctions && j.missingFunctions.includes('missingFn');
@@ -16256,7 +16257,7 @@ const BENCHMARK_SCENARIOS = {
16256
16257
  },
16257
16258
  measure: (dir) => {
16258
16259
  const r = cp.spawnSync(process.execPath, [__filename, 'drift', 'check', dir, '--json'],
16259
- { encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_NO_DRIFT_CHECK: '0' } });
16260
+ { encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_DRIFT_CHECK: '0' } });
16260
16261
  let j = null;
16261
16262
  try { j = JSON.parse(r.stdout.trim()); } catch {}
16262
16263
  const detected = j && (j.level === '🔴 critical' || j.level === '🟠 attention');
@@ -16275,7 +16276,7 @@ const BENCHMARK_SCENARIOS = {
16275
16276
  measure: (dir) => {
16276
16277
  const r = cp.spawnSync(process.execPath, [__filename, 'skill', 'install',
16277
16278
  path.join(dir, 'bom-test.md'), '--path', dir],
16278
- { encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_NO_PROMPT: '1' } });
16279
+ { encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_PROMPT: '1' } });
16279
16280
  const f = path.join(dir, '.harness', 'skills', 'bom-test', 'SKILL.md');
16280
16281
  return { detected: r.status === 0 && exists(f), sample: r.stdout.slice(0, 200) };
16281
16282
  }
@@ -16312,7 +16313,7 @@ async function _benchmarkMeasure(root, task) {
16312
16313
  const t1 = Date.now();
16313
16314
  cp.spawnSync(process.execPath, [__filename, 'audit', root, '--fix'], {
16314
16315
  encoding: 'utf8', timeout: 15000,
16315
- env: { ...process.env, LEERNESS_NO_BANNER: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' }
16316
+ env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_BANNER: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' }
16316
16317
  });
16317
16318
  const verifyTime = Date.now() - t1;
16318
16319
  results.push({
@@ -16782,7 +16783,7 @@ function mcpServeCmd(root) {
16782
16783
  const r = cp.spawnSync(process.execPath, [__filename, ...cliArgs], {
16783
16784
  encoding: 'utf8',
16784
16785
  timeout: 60000,
16785
- env: { ...process.env, LEERNESS_NO_BANNER: '1', LEERNESS_NO_STALE_CHECK: '1', LEERNESS_NO_DRIFT_CHECK: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_WORKFLOW_GUIDE: '1' }
16786
+ env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_BANNER: '1', LEERNESS_NO_STALE_CHECK: '1', LEERNESS_NO_DRIFT_CHECK: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_WORKFLOW_GUIDE: '1' }
16786
16787
  });
16787
16788
  return { ok: r.status === 0, exit: r.status, stdout: r.stdout || '', stderr: r.stderr || '' };
16788
16789
  }
@@ -18917,7 +18918,7 @@ async function _agentRepl(root, opts) {
18917
18918
  try {
18918
18919
  const hf = cp.spawnSync(process.execPath, [__filename, 'handoff', root, '--compact', '--no-drift-check', '--no-headline'], {
18919
18920
  encoding: 'utf8', timeout: 8000,
18920
- env: { ...process.env, LEERNESS_NO_BANNER: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1', LEERNESS_NO_LESSONS: '1' }
18921
+ env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_BANNER: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1', LEERNESS_NO_LESSONS: '1' }
18921
18922
  });
18922
18923
  if (hf.status === 0 && hf.stdout) {
18923
18924
  const preview = hf.stdout.split('\n').slice(0, 4).map(l => l.replace(/^\s+/, '')).filter(Boolean).slice(0, 3).join(' · ');
@@ -19659,7 +19660,7 @@ async function agentCmd(root, taskArg) {
19659
19660
  log(`permission mode: ${perms.mode || 'basic'}`);
19660
19661
  // handoff 자동 회수 (compact 모드)
19661
19662
  try {
19662
- const hf = cp.spawnSync(process.execPath, [__filename, 'handoff', root, '--compact', '--no-drift-check'], { encoding: 'utf8', timeout: 10000, env: { ...process.env, LEERNESS_NO_BANNER: '1', LEERNESS_NO_PROMPT: '1' } });
19663
+ const hf = cp.spawnSync(process.execPath, [__filename, 'handoff', root, '--compact', '--no-drift-check'], { encoding: 'utf8', timeout: 10000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_BANNER: '1', LEERNESS_NO_PROMPT: '1' } });
19663
19664
  if (hf.status === 0 && hf.stdout) {
19664
19665
  const preview = hf.stdout.split('\n').slice(0, 6).join('\n');
19665
19666
  log('\n[handoff context (preview)]\n' + preview);
@@ -19886,7 +19887,7 @@ async function incidentHandleCmd(root, id) {
19886
19887
  const keywords = String(err).toLowerCase().match(/[\w가-힣]{4,}/g) || [];
19887
19888
  if (keywords.length) {
19888
19889
  const r = cp.spawnSync(process.execPath, [__filename, 'lessons', '--path', root, '--query', keywords[0], '--limit', '3'],
19889
- { encoding: 'utf8', timeout: 8000, env: { ...process.env, LEERNESS_NO_PROMPT: '1' } });
19890
+ { encoding: 'utf8', timeout: 8000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_PROMPT: '1' } });
19890
19891
  if (r.status === 0 && /총 \d+건 발견/.test(r.stdout)) {
19891
19892
  const block = r.stdout.split('\n').slice(0, 12).join('\n');
19892
19893
  log(`\n📚 관련 lessons:\n${block}`);
@@ -20027,7 +20028,7 @@ function healthCmd(root) {
20027
20028
  // 1) drift level
20028
20029
  try {
20029
20030
  const r = cp.spawnSync(process.execPath, [__filename, 'drift', 'check', root, '--json'],
20030
- { encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '0' } });
20031
+ { encoding: 'utf8', timeout: 15000, env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '0' } });
20031
20032
  const j = JSON.parse(r.stdout.trim());
20032
20033
  out.checks.drift = { level: j.level, score: j.score, firedCount: (j.fired || []).length };
20033
20034
  } catch { out.checks.drift = { error: 'drift check 실패' }; }
@@ -21152,7 +21153,7 @@ function reviewRequestCmd(root, request) {
21152
21153
  try {
21153
21154
  const r = cp.spawnSync(process.execPath, [__filename, 'brainstorm', text, '--path', root, '--json'], {
21154
21155
  encoding: 'utf8', timeout: 12000,
21155
- env: { ...process.env, LEERNESS_NO_BANNER: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' }
21156
+ env: { ...process.env, LEERNESS_INTERNAL: '1', LEERNESS_NO_BANNER: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' }
21156
21157
  });
21157
21158
  if (r.stdout) {
21158
21159
  const j = JSON.parse(r.stdout);
@@ -21540,7 +21541,8 @@ async function main() {
21540
21541
  }
21541
21542
  if (has('--help') || has('-h')) return help();
21542
21543
  // 1.9.38 (B): 사용 통계 카운터 — usage stats 명령 자체와 비차단 경로는 제외
21543
- if (cmd !== 'usage' && cmd !== 'init' && cmd !== 'migrate' && cmd !== '--version' && cmd !== '--help') {
21544
+ // 1.9.317 (UR-0051, 설치리뷰): 내부 auto-call(LEERNESS_INTERNAL=1) usage 집계 제외 텔레메트리 오염(거짓 skill 추천) 방지.
21545
+ if (process.env.LEERNESS_INTERNAL !== '1' && cmd !== 'usage' && cmd !== 'init' && cmd !== 'migrate' && cmd !== '--version' && cmd !== '--help') {
21544
21546
  try {
21545
21547
  const root = absRoot(arg('--path', args[1] && !args[1].startsWith('-') ? args[1] : process.cwd()));
21546
21548
  if (exists(path.join(root, '.harness'))) _bumpUsage(root, cmd);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.316",
3
+ "version": "1.9.317",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -3752,5 +3752,31 @@ total++;
3752
3752
  if (!ok) failed++;
3753
3753
  }
3754
3754
 
3755
+ // 1.9.317 회귀 (UR-0051 설치리뷰): 텔레메트리 분리 — 내부 auto-call(LEERNESS_INTERNAL)이 usage 집계 오염 안 함
3756
+ total++;
3757
+ {
3758
+ let ok = false;
3759
+ try {
3760
+ const td = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-telem-'));
3761
+ cp.spawnSync(process.execPath, [CLI, 'init', td, '--yes', '--language', 'ko', '--skills', 'recommended'], { encoding: 'utf8', timeout: 30000 });
3762
+ const usagePath = path.join(td, '.harness', 'cache', 'usage-stats.json');
3763
+ const cmds = () => { try { return JSON.parse(fs.readFileSync(usagePath, 'utf8')).commands || {}; } catch { return {}; } };
3764
+ // task add → 내부 review-request auto-call 이 usage 오염 안 함 (task 만 집계, review-request 없음)
3765
+ cp.spawnSync(process.execPath, [CLI, 'task', 'add', '텔레메트리 테스트', '--path', td], { encoding: 'utf8', timeout: 20000 });
3766
+ const afterTask = cmds();
3767
+ const noPollution = afterTask.task >= 1 && !afterTask['review-request'];
3768
+ // LEERNESS_INTERNAL=1 호출은 미집계 (일반 호출은 집계)
3769
+ cp.spawnSync(process.execPath, [CLI, 'drift', 'check', '--path', td], { encoding: 'utf8', timeout: 20000 });
3770
+ const d1 = cmds().drift || 0;
3771
+ cp.spawnSync(process.execPath, [CLI, 'drift', 'check', '--path', td], { encoding: 'utf8', timeout: 20000, env: { ...process.env, LEERNESS_INTERNAL: '1' } });
3772
+ const d2 = cmds().drift || 0;
3773
+ const internalSkip = d1 >= 1 && d2 === d1; // 일반=증가, INTERNAL=증가없음
3774
+ ok = noPollution && internalSkip;
3775
+ fs.rmSync(td, { recursive: true, force: true });
3776
+ } catch {}
3777
+ console.log(ok ? '✓ B(1.9.317) 텔레메트리 분리: task add review-request 오염 없음 + LEERNESS_INTERNAL 미집계 (UR-0051)' : '✗ 텔레메트리 분리 실패');
3778
+ if (!ok) failed++;
3779
+ }
3780
+
3755
3781
  console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
3756
3782
  if (failed > 0) process.exit(1);