leerness 1.9.315 → 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 +35 -0
- package/README.md +5 -5
- package/bin/harness.js +41 -30
- package/package.json +1 -1
- package/scripts/e2e.js +50 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
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
|
+
|
|
20
|
+
## 1.9.316 — 2026-06-04 — 🐛 drift 'session close 누락' 영구 오발화 버그 (자가 발견)
|
|
21
|
+
|
|
22
|
+
**🔧 session close 직후에도 drift 가 `session close 누락`(13일)을 보고하던 버그 수정 — 4라운드 연속 관찰된 leerness 자체 정확성 결함.**
|
|
23
|
+
|
|
24
|
+
### 근본 원인 (자가 발견)
|
|
25
|
+
- drift 의 `session close 누락` 신호는 `session-handoff.md` 의 `Last generated:` 타임스탬프로 나이를 계산.
|
|
26
|
+
- `sessionClose` 가 파일 재작성 시 프론트매터 보존 로직 `cur.indexOf('\n---\n', 4)` 가 **본문의 `---`(수평선/구분자)** 를 프론트매터 종료로 오인 → **구 블록 전체(구 `Last generated`)를 보존**하고 그 뒤에 새 블록을 append.
|
|
27
|
+
- 결과: `session-handoff.md` 에 `Last generated:` 가 **2개 누적**(구 + 신). drift 의 `.match()` 는 **첫(=구) 매치**를 읽어 → 매 session close 후에도 13일 stale → 영구 오발화.
|
|
28
|
+
|
|
29
|
+
### 구현 (2중 수정)
|
|
30
|
+
1. **근본 (write)**: 프론트매터는 파일이 **`^---` 로 시작할 때만** 추출 — 본문 `---` 오인 차단. 프론트매터 없으면 깨끗이 덮어써 단일 블록 유지(기존 손상 파일도 다음 close 에 self-heal).
|
|
31
|
+
2. **방어 (read)**: drift 가 `matchAll` 로 **최신(마지막) `Last generated`** 사용 — 혹시 모를 중복에도 freshest 반영.
|
|
32
|
+
3. selftest 63→64 · e2e 260→261.
|
|
33
|
+
|
|
34
|
+
### 검증
|
|
35
|
+
- **selftest 64/64 PASS** · **E2E 261/261 PASS** (회귀 0).
|
|
36
|
+
- 실측: 손상 파일(구 timestamp + 본문 `---`) → session close → `Last generated` **1개**(신선), drift `session close 누락` **age 0.00d 클리어** · 프론트매터 파일 3회 연속 close → 누적 0(프론트매터 보존).
|
|
37
|
+
|
|
3
38
|
## 1.9.315 — 2026-06-04 — UR-0054(부분): doc/surface 정합 + doctor 명령 (설치리뷰)
|
|
4
39
|
|
|
5
40
|
**📋 stale 현재상태 표시 수정 + `leerness doctor` 설치 진단 명령 추가 (Codex#2·Sonnet#2·Opus#2 공통 지적).**
|
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.315는 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) — 자율 모드 전용 통합 명령`));
|
|
@@ -3114,6 +3114,8 @@ function _selfTestCases() {
|
|
|
3114
3114
|
{ name: 'MCP notification 준수: id없는 요청 무응답 가드 + ping {} (UR-0049 설치리뷰 1.9.313)', run: () => { const src = read(__filename); const guard = src.includes("const isNotification = !('id' in req)") && src.includes("req.method.startsWith('notifications/')") && src.includes('if (isNotification) return;'); const ping = src.includes("req.method === 'ping'") && /ping[\s\S]{0,140}result: \{\} \}/.test(src); return guard && ping; } },
|
|
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
|
+
{ 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; } },
|
|
3117
3119
|
{ name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
|
|
3118
3120
|
];
|
|
3119
3121
|
}
|
|
@@ -5889,7 +5891,7 @@ async function nextActionCmd(root, sub, ...rest) {
|
|
|
5889
5891
|
state.queue.splice(n, 1);
|
|
5890
5892
|
_writeNextActionQueue(root, state.queue);
|
|
5891
5893
|
// leerness task add 호출
|
|
5892
|
-
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' } });
|
|
5893
5895
|
if (taskResult.status === 0) {
|
|
5894
5896
|
const m = (taskResult.stdout || '').match(/T-\d{4}/);
|
|
5895
5897
|
log(` ✓ task 추가: ${m ? m[0] : '?'} — "${taskTitle}"`);
|
|
@@ -6410,7 +6412,7 @@ function taskAdd(root, text) {
|
|
|
6410
6412
|
const t0 = Date.now();
|
|
6411
6413
|
const r = cp.spawnSync(process.execPath, [__filename, 'review-request', text, '--path', absRoot(root), '--json'], {
|
|
6412
6414
|
encoding: 'utf8', timeout: 15000,
|
|
6413
|
-
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' }
|
|
6414
6416
|
});
|
|
6415
6417
|
if (r.status === 0 && r.stdout) {
|
|
6416
6418
|
const j = JSON.parse(r.stdout);
|
|
@@ -7917,7 +7919,7 @@ function handoff(root) {
|
|
|
7917
7919
|
// 1) drift level (가벼운 check)
|
|
7918
7920
|
try {
|
|
7919
7921
|
const r = cp.spawnSync(process.execPath, [__filename, 'drift', 'check', root, '--json'],
|
|
7920
|
-
{ 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' } });
|
|
7921
7923
|
const j = JSON.parse(r.stdout.trim());
|
|
7922
7924
|
if (j.level) parts.push(`drift ${j.level.replace(/^[^\w]+/, '')} (${j.score})`);
|
|
7923
7925
|
} catch {}
|
|
@@ -8515,7 +8517,7 @@ function handoff(root) {
|
|
|
8515
8517
|
if (!has('--no-brainstorm-hits') && !has('--quiet') && process.env.LEERNESS_NO_BRAINSTORM_HITS !== '1') {
|
|
8516
8518
|
try {
|
|
8517
8519
|
const r = cp.spawnSync(process.execPath, [__filename, 'brainstorm', keyword, '--path', root, '--json'],
|
|
8518
|
-
{ 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' } });
|
|
8519
8521
|
const bj = JSON.parse(r.stdout);
|
|
8520
8522
|
const hits = bj.hits || {};
|
|
8521
8523
|
const items = [];
|
|
@@ -9018,7 +9020,7 @@ function handoff(root) {
|
|
|
9018
9020
|
if (process.env.LEERNESS_AUTO_SECURITY_FIX === '1') {
|
|
9019
9021
|
try {
|
|
9020
9022
|
const r = cp.spawnSync(process.execPath, [__filename, 'audit', root, '--fix'],
|
|
9021
|
-
{ 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' } });
|
|
9022
9024
|
if (r.status === 0) {
|
|
9023
9025
|
log(dim(` ✓ 자동 회복 (LEERNESS_AUTO_SECURITY_FIX=1): audit --fix 완료`));
|
|
9024
9026
|
} else {
|
|
@@ -9263,7 +9265,7 @@ function handoffCmd(root) {
|
|
|
9263
9265
|
if (has('--auto-recover') && sevStale) {
|
|
9264
9266
|
log(dim(` 🔧 --auto-recover 활성 — session close 자동 실행 중...`));
|
|
9265
9267
|
try {
|
|
9266
|
-
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' } });
|
|
9267
9269
|
if (r.status === 0) {
|
|
9268
9270
|
log(dim(` ✓ session close 자동 완료 (다음 라운드부터 healthy)`));
|
|
9269
9271
|
const s2 = _readUsageStats(absR0);
|
|
@@ -12246,9 +12248,15 @@ function sessionClose(root, opts = {}) {
|
|
|
12246
12248
|
``
|
|
12247
12249
|
].join('\n');
|
|
12248
12250
|
const cur = exists(handoffPath(root)) ? read(handoffPath(root)) : '';
|
|
12249
|
-
|
|
12250
|
-
|
|
12251
|
-
|
|
12251
|
+
// 1.9.316 (drift 마커 버그): 프론트매터는 파일이 '---' 로 시작할 때만 추출.
|
|
12252
|
+
// 이전: 본문의 '---'(수평선/구분자)을 프론트매터 종료로 오인 → 구 블록(구 'Last generated')을 보존 →
|
|
12253
|
+
// session-handoff.md 에 'Last generated' 중복 누적 → drift 가 첫(=구) 매치를 읽어 'session close 누락' 영구 오발화.
|
|
12254
|
+
let frontmatter = '';
|
|
12255
|
+
if (/^---\r?\n/.test(cur)) {
|
|
12256
|
+
const fmEnd = cur.indexOf('\n---\n', 4);
|
|
12257
|
+
if (fmEnd > 0) frontmatter = cur.slice(0, fmEnd + 5) + MARK + '\n';
|
|
12258
|
+
}
|
|
12259
|
+
writeUtf8(handoffPath(root), frontmatter + block);
|
|
12252
12260
|
|
|
12253
12261
|
if (exists(currentStatePath(root))) {
|
|
12254
12262
|
let cs = read(currentStatePath(root));
|
|
@@ -12295,7 +12303,7 @@ function sessionClose(root, opts = {}) {
|
|
|
12295
12303
|
// 1) skill suggest
|
|
12296
12304
|
try {
|
|
12297
12305
|
const r = cp.spawnSync(process.execPath, [__filename, 'skill', 'suggest', '--path', root, '--min', '3', '--json'],
|
|
12298
|
-
{ 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' } });
|
|
12299
12307
|
const j = JSON.parse(r.stdout);
|
|
12300
12308
|
if (j.candidates && j.candidates.length) {
|
|
12301
12309
|
log(dim(' 📌 신규 skill 후보 (Hermes-style 자동 학습):'));
|
|
@@ -12306,7 +12314,7 @@ function sessionClose(root, opts = {}) {
|
|
|
12306
12314
|
// 2) drift check
|
|
12307
12315
|
try {
|
|
12308
12316
|
const r = cp.spawnSync(process.execPath, [__filename, 'drift', 'check', root, '--json'],
|
|
12309
|
-
{ 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' } });
|
|
12310
12318
|
const j = JSON.parse(r.stdout.trim());
|
|
12311
12319
|
if (j.level) {
|
|
12312
12320
|
log(dim(` 🩺 drift 상태: ${j.level} ${j.score}/200`));
|
|
@@ -15588,7 +15596,9 @@ function driftCheckCmd(root, opts = {}) {
|
|
|
15588
15596
|
const shPath = handoffPath(root);
|
|
15589
15597
|
if (exists(shPath)) {
|
|
15590
15598
|
const txt = read(shPath);
|
|
15591
|
-
|
|
15599
|
+
// 1.9.316 (drift 마커 버그): 최신(마지막) 'Last generated' 사용 — 구 블록 중복 시 첫(구) 매치를 읽던 오발화 방어.
|
|
15600
|
+
const allGen = [...txt.matchAll(/Last generated:\s*([\d\-T:.Z]+)/g)];
|
|
15601
|
+
const m = allGen.length ? allGen[allGen.length - 1] : null;
|
|
15592
15602
|
let ageDays;
|
|
15593
15603
|
if (m) {
|
|
15594
15604
|
ageDays = (now - new Date(m[1]).getTime()) / 86400000;
|
|
@@ -15731,7 +15741,7 @@ function driftCheckCmd(root, opts = {}) {
|
|
|
15731
15741
|
log(`🔒 --auto-fix 활성 (1.9.82) — 보안 신호 회복: audit --fix 자동 실행 중...`);
|
|
15732
15742
|
try {
|
|
15733
15743
|
const r = cp.spawnSync(process.execPath, [__filename, 'audit', root, '--fix'],
|
|
15734
|
-
{ 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' } });
|
|
15735
15745
|
if (r.status === 0) {
|
|
15736
15746
|
log(`✓ audit --fix 완료 — .gitignore + .env.example 동기화`);
|
|
15737
15747
|
// 재검사 (보안 신호 회복 확인)
|
|
@@ -15844,7 +15854,7 @@ function driftCheckCmd(root, opts = {}) {
|
|
|
15844
15854
|
log('');
|
|
15845
15855
|
log(`🔧 --auto-fix 활성 — session close 자동 실행 중...`);
|
|
15846
15856
|
try {
|
|
15847
|
-
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' } });
|
|
15848
15858
|
if (r.status === 0) {
|
|
15849
15859
|
log(`✓ session close 자동 완료`);
|
|
15850
15860
|
// autoResolved 카운트
|
|
@@ -16125,7 +16135,7 @@ function skillPublishCmd(root) {
|
|
|
16125
16135
|
if (!has('--no-security-check') && !has('--force')) {
|
|
16126
16136
|
try {
|
|
16127
16137
|
const r = cp.spawnSync(process.execPath, [__filename, 'health', root, '--json'],
|
|
16128
|
-
{ 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' } });
|
|
16129
16139
|
const j = JSON.parse(r.stdout.trim());
|
|
16130
16140
|
if (j.issues && j.issues.length > 0) {
|
|
16131
16141
|
log(`🚨 보안 사전 점검 (1.9.98): ${j.issues.length}건 issue 발견`);
|
|
@@ -16208,11 +16218,11 @@ const BENCHMARK_SCENARIOS = {
|
|
|
16208
16218
|
setup: (dir) => {
|
|
16209
16219
|
// 빈 evidence로 done task 생성
|
|
16210
16220
|
cp.spawnSync(process.execPath, [__filename, 'task', 'add', '거짓 완료된 작업', '--status', 'done', '--evidence', '', '--path', dir],
|
|
16211
|
-
{ 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' } });
|
|
16212
16222
|
},
|
|
16213
16223
|
measure: (dir) => {
|
|
16214
16224
|
const r = cp.spawnSync(process.execPath, [__filename, 'lazy', 'detect', dir],
|
|
16215
|
-
{ 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' } });
|
|
16216
16226
|
const detected = /✗ |found.*issue|증거 없는|empty/.test(r.stdout);
|
|
16217
16227
|
return { detected, exit: r.status, sample: r.stdout.slice(0, 200) };
|
|
16218
16228
|
}
|
|
@@ -16227,7 +16237,7 @@ const BENCHMARK_SCENARIOS = {
|
|
|
16227
16237
|
measure: (dir) => {
|
|
16228
16238
|
const r = cp.spawnSync(process.execPath, [__filename, 'contract', 'verify',
|
|
16229
16239
|
path.join(dir, 'mismatch-spec.md'), path.join(dir, 'mismatch-impl.js'), '--json'],
|
|
16230
|
-
{ 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' } });
|
|
16231
16241
|
let j = null;
|
|
16232
16242
|
try { j = JSON.parse(r.stdout); } catch {}
|
|
16233
16243
|
const detected = j && j.missingFunctions && j.missingFunctions.includes('missingFn');
|
|
@@ -16247,7 +16257,7 @@ const BENCHMARK_SCENARIOS = {
|
|
|
16247
16257
|
},
|
|
16248
16258
|
measure: (dir) => {
|
|
16249
16259
|
const r = cp.spawnSync(process.execPath, [__filename, 'drift', 'check', dir, '--json'],
|
|
16250
|
-
{ 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' } });
|
|
16251
16261
|
let j = null;
|
|
16252
16262
|
try { j = JSON.parse(r.stdout.trim()); } catch {}
|
|
16253
16263
|
const detected = j && (j.level === '🔴 critical' || j.level === '🟠 attention');
|
|
@@ -16266,7 +16276,7 @@ const BENCHMARK_SCENARIOS = {
|
|
|
16266
16276
|
measure: (dir) => {
|
|
16267
16277
|
const r = cp.spawnSync(process.execPath, [__filename, 'skill', 'install',
|
|
16268
16278
|
path.join(dir, 'bom-test.md'), '--path', dir],
|
|
16269
|
-
{ 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' } });
|
|
16270
16280
|
const f = path.join(dir, '.harness', 'skills', 'bom-test', 'SKILL.md');
|
|
16271
16281
|
return { detected: r.status === 0 && exists(f), sample: r.stdout.slice(0, 200) };
|
|
16272
16282
|
}
|
|
@@ -16303,7 +16313,7 @@ async function _benchmarkMeasure(root, task) {
|
|
|
16303
16313
|
const t1 = Date.now();
|
|
16304
16314
|
cp.spawnSync(process.execPath, [__filename, 'audit', root, '--fix'], {
|
|
16305
16315
|
encoding: 'utf8', timeout: 15000,
|
|
16306
|
-
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' }
|
|
16307
16317
|
});
|
|
16308
16318
|
const verifyTime = Date.now() - t1;
|
|
16309
16319
|
results.push({
|
|
@@ -16773,7 +16783,7 @@ function mcpServeCmd(root) {
|
|
|
16773
16783
|
const r = cp.spawnSync(process.execPath, [__filename, ...cliArgs], {
|
|
16774
16784
|
encoding: 'utf8',
|
|
16775
16785
|
timeout: 60000,
|
|
16776
|
-
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' }
|
|
16777
16787
|
});
|
|
16778
16788
|
return { ok: r.status === 0, exit: r.status, stdout: r.stdout || '', stderr: r.stderr || '' };
|
|
16779
16789
|
}
|
|
@@ -18908,7 +18918,7 @@ async function _agentRepl(root, opts) {
|
|
|
18908
18918
|
try {
|
|
18909
18919
|
const hf = cp.spawnSync(process.execPath, [__filename, 'handoff', root, '--compact', '--no-drift-check', '--no-headline'], {
|
|
18910
18920
|
encoding: 'utf8', timeout: 8000,
|
|
18911
|
-
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' }
|
|
18912
18922
|
});
|
|
18913
18923
|
if (hf.status === 0 && hf.stdout) {
|
|
18914
18924
|
const preview = hf.stdout.split('\n').slice(0, 4).map(l => l.replace(/^\s+/, '')).filter(Boolean).slice(0, 3).join(' · ');
|
|
@@ -19650,7 +19660,7 @@ async function agentCmd(root, taskArg) {
|
|
|
19650
19660
|
log(`permission mode: ${perms.mode || 'basic'}`);
|
|
19651
19661
|
// handoff 자동 회수 (compact 모드)
|
|
19652
19662
|
try {
|
|
19653
|
-
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' } });
|
|
19654
19664
|
if (hf.status === 0 && hf.stdout) {
|
|
19655
19665
|
const preview = hf.stdout.split('\n').slice(0, 6).join('\n');
|
|
19656
19666
|
log('\n[handoff context (preview)]\n' + preview);
|
|
@@ -19877,7 +19887,7 @@ async function incidentHandleCmd(root, id) {
|
|
|
19877
19887
|
const keywords = String(err).toLowerCase().match(/[\w가-힣]{4,}/g) || [];
|
|
19878
19888
|
if (keywords.length) {
|
|
19879
19889
|
const r = cp.spawnSync(process.execPath, [__filename, 'lessons', '--path', root, '--query', keywords[0], '--limit', '3'],
|
|
19880
|
-
{ 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' } });
|
|
19881
19891
|
if (r.status === 0 && /총 \d+건 발견/.test(r.stdout)) {
|
|
19882
19892
|
const block = r.stdout.split('\n').slice(0, 12).join('\n');
|
|
19883
19893
|
log(`\n📚 관련 lessons:\n${block}`);
|
|
@@ -20018,7 +20028,7 @@ function healthCmd(root) {
|
|
|
20018
20028
|
// 1) drift level
|
|
20019
20029
|
try {
|
|
20020
20030
|
const r = cp.spawnSync(process.execPath, [__filename, 'drift', 'check', root, '--json'],
|
|
20021
|
-
{ 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' } });
|
|
20022
20032
|
const j = JSON.parse(r.stdout.trim());
|
|
20023
20033
|
out.checks.drift = { level: j.level, score: j.score, firedCount: (j.fired || []).length };
|
|
20024
20034
|
} catch { out.checks.drift = { error: 'drift check 실패' }; }
|
|
@@ -21143,7 +21153,7 @@ function reviewRequestCmd(root, request) {
|
|
|
21143
21153
|
try {
|
|
21144
21154
|
const r = cp.spawnSync(process.execPath, [__filename, 'brainstorm', text, '--path', root, '--json'], {
|
|
21145
21155
|
encoding: 'utf8', timeout: 12000,
|
|
21146
|
-
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' }
|
|
21147
21157
|
});
|
|
21148
21158
|
if (r.stdout) {
|
|
21149
21159
|
const j = JSON.parse(r.stdout);
|
|
@@ -21531,7 +21541,8 @@ async function main() {
|
|
|
21531
21541
|
}
|
|
21532
21542
|
if (has('--help') || has('-h')) return help();
|
|
21533
21543
|
// 1.9.38 (B): 사용 통계 카운터 — usage stats 명령 자체와 비차단 경로는 제외
|
|
21534
|
-
|
|
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') {
|
|
21535
21546
|
try {
|
|
21536
21547
|
const root = absRoot(arg('--path', args[1] && !args[1].startsWith('-') ? args[1] : process.cwd()));
|
|
21537
21548
|
if (exists(path.join(root, '.harness'))) _bumpUsage(root, cmd);
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -3728,5 +3728,55 @@ total++;
|
|
|
3728
3728
|
if (!ok) failed++;
|
|
3729
3729
|
}
|
|
3730
3730
|
|
|
3731
|
+
// 1.9.316 회귀 (drift 마커 버그): session-handoff 'Last generated' 중복 누적 방지 + drift 'session close 누락' 클리어
|
|
3732
|
+
// 근본: sessionClose 프론트매터 추출이 본문 '---' 를 오인 → 구 블록 보존 → 첫(구) Last generated 를 drift 가 읽어 영구 오발화
|
|
3733
|
+
total++;
|
|
3734
|
+
{
|
|
3735
|
+
let ok = false;
|
|
3736
|
+
try {
|
|
3737
|
+
const dd = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-driftmark-'));
|
|
3738
|
+
cp.spawnSync(process.execPath, [CLI, 'init', dd, '--yes', '--language', 'ko', '--skills', 'recommended'], { encoding: 'utf8', timeout: 30000 });
|
|
3739
|
+
const shp = path.join(dd, '.harness', 'session-handoff.md');
|
|
3740
|
+
// 손상 시뮬: 구 timestamp + 본문 '---' (프론트매터 없는 파일)
|
|
3741
|
+
fs.writeFileSync(shp, '# Session Handoff\n\nLast generated: 2026-01-01T00:00:00.000Z\n\n## Completed\n- old\n\n---\n## x\n');
|
|
3742
|
+
cp.spawnSync(process.execPath, [CLI, 'session', 'close', dd], { encoding: 'utf8', timeout: 30000 });
|
|
3743
|
+
const after = fs.readFileSync(shp, 'utf8');
|
|
3744
|
+
const genCount = (after.match(/Last generated:/g) || []).length; // 단일화
|
|
3745
|
+
const noOld = !after.includes('2026-01-01'); // 구 timestamp 제거
|
|
3746
|
+
const dr = cp.spawnSync(process.execPath, [CLI, 'drift', 'check', dd, '--json'], { encoding: 'utf8', timeout: 20000 });
|
|
3747
|
+
let driftOk = false; try { const j = JSON.parse(dr.stdout); const sig = (j.signals || []).find(x => x.label && x.label.includes('session close')); driftOk = sig && sig.ageDays <= sig.threshold; } catch {}
|
|
3748
|
+
ok = genCount === 1 && noOld && driftOk;
|
|
3749
|
+
fs.rmSync(dd, { recursive: true, force: true });
|
|
3750
|
+
} catch {}
|
|
3751
|
+
console.log(ok ? '✓ B(1.9.316) drift 마커: session-handoff Last generated 단일화 + session close 누락 신호 클리어' : '✗ drift 마커 실패');
|
|
3752
|
+
if (!ok) failed++;
|
|
3753
|
+
}
|
|
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
|
+
|
|
3731
3781
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
3732
3782
|
if (failed > 0) process.exit(1);
|