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 +17 -0
- package/README.md +5 -5
- package/bin/harness.js +28 -26
- package/package.json +1 -1
- package/scripts/e2e.js +26 -0
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
|
-
[](https://www.npmjs.com/package/leerness) [](https://www.npmjs.com/package/leerness) []() []() []() []() []() []()
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
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);
|