leerness 1.9.209 → 1.9.210
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 +45 -0
- package/README.md +3 -3
- package/bin/harness.js +217 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.210 — 2026-05-22
|
|
4
|
+
|
|
5
|
+
**⚡ adaptive wakeup interval — 사용자 활동 기반 자동 조절 (사용자 명시).**
|
|
6
|
+
|
|
7
|
+
### 사용자 명시
|
|
8
|
+
> *"wakeup 30분등 긴 시간이 필요없다면 간격을 타이트하게 자동으로 조절하도록"*
|
|
9
|
+
|
|
10
|
+
### 1. `.harness/wakeup-history.json` — fire 이력 누적
|
|
11
|
+
- `_wakeupHistoryPath / _loadWakeupHistory / _writeWakeupHistory` — 파일 I/O
|
|
12
|
+
- `_recordWakeupFire(root, kind)` — kind: 'auto' / 'user-trigger' / 'wakeup-miss'
|
|
13
|
+
- 최근 50개 rotate
|
|
14
|
+
|
|
15
|
+
### 2. `_computeAdaptiveInterval(root)` — 활동량 기반 권장 interval
|
|
16
|
+
- 범위: **600s (10min) ~ 2700s (45min)**, default 1500s
|
|
17
|
+
- 분석 차원:
|
|
18
|
+
- 최근 2시간 user-trigger 빈도 (3+ → 15min, 0 → 35min)
|
|
19
|
+
- 최근 5건 user-trigger 평균 gap (avg < 20min → 단축)
|
|
20
|
+
- pre-wake critical 신호 (1.9.209 통합) → 20min 단축
|
|
21
|
+
- opt-out:
|
|
22
|
+
- env `LEERNESS_FIXED_INTERVAL=1500`
|
|
23
|
+
- `leerness wakeup-interval set 900` (override)
|
|
24
|
+
|
|
25
|
+
### 3. `leerness wakeup-interval <get|set|auto|history|record>` CLI
|
|
26
|
+
- `get` — 권장 interval + stats (--json)
|
|
27
|
+
- `set <secs>` — override 설정
|
|
28
|
+
- `auto` — override 해제 + 자동 계산 복귀
|
|
29
|
+
- `history` — 최근 fire 이력 (--json)
|
|
30
|
+
- `record <kind>` — fire 기록 (테스트/외부 통합용)
|
|
31
|
+
|
|
32
|
+
### 4. 자동 단축 시나리오 검증
|
|
33
|
+
- 신규 워크스페이스 idle → **35min**
|
|
34
|
+
- user-trigger 3회/2h → **10min** (1500 → 600)
|
|
35
|
+
- pre-wake critical + 활동 보통 → **20min**
|
|
36
|
+
|
|
37
|
+
### 5. 누적 회귀 (1.9.200~209) — 모두 유지
|
|
38
|
+
|
|
39
|
+
### 6. stress-v155 — 18/18 PASS
|
|
40
|
+
- 1.9.210 (9) + 성능 (2) + 누적 회귀 (7)
|
|
41
|
+
- 격리 tmp dir 라이프사이클 + override 토글 + rotate to 50
|
|
42
|
+
- 성능: --version cold start avg 440ms · MCP 54 도구 421ms
|
|
43
|
+
|
|
44
|
+
### 7. 자동 release (72 라운드 main-push streak · 33 라운드 npm publish streak)
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
3
48
|
## 1.9.209 — 2026-05-22
|
|
4
49
|
|
|
5
50
|
**🔍 pre-wake sub-agent audit + 깨어남 직후 자동 노출 (사용자 명시).**
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **AI 코딩 에이전트의 거짓 완료·중복·망각·충돌을 막아주는 검수·기억·협업 CLI 하네스.**
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/leerness) [](https://www.npmjs.com/package/leerness) []() []() []() []() []() []() [-brightgreen)]() []() []() []() []() []()
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
╔══════════════════════════════════════════════════════════════╗
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
║ ██║ ██╔══╝ ██╔══╝ ██╔══██╗██║╚██╗██║██╔══╝ ╚════██║ ║
|
|
13
13
|
║ ███████╗███████╗███████╗██║ ██║██║ ╚████║███████╗███████║ ║
|
|
14
14
|
║ ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝ ║
|
|
15
|
-
║ v1.9.
|
|
15
|
+
║ v1.9.210 AI Agent Reliability Harness + Sandbox ║
|
|
16
16
|
║ verify · remember · orchestrate · audit · sandbox · drift ║
|
|
17
|
-
║
|
|
17
|
+
║ ⚡ adaptive wakeup 10~45min (활동 기반 자동) (사용자 명시) ║
|
|
18
18
|
╚══════════════════════════════════════════════════════════════╝
|
|
19
19
|
```
|
|
20
20
|
|
package/bin/harness.js
CHANGED
|
@@ -7,7 +7,7 @@ const cp = require('child_process');
|
|
|
7
7
|
const os = require('os'); // 1.9.178: _publishToNpm 에서 os.tmpdir() 사용 (전역 import)
|
|
8
8
|
const readline = require('readline');
|
|
9
9
|
|
|
10
|
-
const VERSION = '1.9.
|
|
10
|
+
const VERSION = '1.9.210';
|
|
11
11
|
|
|
12
12
|
// 1.9.184: DEP0190 (child_process shell: true) deprecation warning 억제 (사용자 명시).
|
|
13
13
|
// leerness 는 cross-platform PATH resolution 을 위해 shell: true 를 의도적으로 사용 (claude.cmd / ollama.cmd 등 Windows .cmd 처리).
|
|
@@ -2251,6 +2251,119 @@ function _saveAndAppendPreWakeReport(root, audit) {
|
|
|
2251
2251
|
return audit;
|
|
2252
2252
|
}
|
|
2253
2253
|
|
|
2254
|
+
// 1.9.210: adaptive wakeup interval (사용자 명시)
|
|
2255
|
+
// "wakeup 30분등 긴 시간이 필요없다면 간격을 타이트하게 자동으로 조절하도록"
|
|
2256
|
+
// 사용자 활동량 기반 권장 interval 계산 → ScheduleWakeup 시 자동 적용 (R-0001 룰 외 동적 보정)
|
|
2257
|
+
// .harness/wakeup-history.json 에 fire 이력 누적 → 최근 N개 사이 평균 user-trigger gap 분석
|
|
2258
|
+
// opt-out: LEERNESS_FIXED_INTERVAL=1 (env) 또는 leerness wakeup-interval set <secs>
|
|
2259
|
+
function _wakeupHistoryPath(root) { return path.join(root, '.harness', 'wakeup-history.json'); }
|
|
2260
|
+
function _loadWakeupHistory(root) {
|
|
2261
|
+
try {
|
|
2262
|
+
const fp = _wakeupHistoryPath(root);
|
|
2263
|
+
if (!exists(fp)) return { fires: [], override: null };
|
|
2264
|
+
const j = JSON.parse(read(fp));
|
|
2265
|
+
return { fires: Array.isArray(j.fires) ? j.fires : [], override: j.override || null };
|
|
2266
|
+
} catch { return { fires: [], override: null }; }
|
|
2267
|
+
}
|
|
2268
|
+
function _writeWakeupHistory(root, state) {
|
|
2269
|
+
try {
|
|
2270
|
+
mkdirp(path.join(root, '.harness'));
|
|
2271
|
+
writeUtf8(_wakeupHistoryPath(root), JSON.stringify({ ...state, updatedAt: new Date().toISOString() }, null, 2));
|
|
2272
|
+
return true;
|
|
2273
|
+
} catch { return false; }
|
|
2274
|
+
}
|
|
2275
|
+
function _recordWakeupFire(root, kind) {
|
|
2276
|
+
// kind: 'auto' (scheduled), 'user-trigger' (사용자 명시 다음라운드), 'wakeup-miss'
|
|
2277
|
+
const state = _loadWakeupHistory(root);
|
|
2278
|
+
state.fires.push({ at: new Date().toISOString(), kind: kind || 'auto' });
|
|
2279
|
+
// 최근 50개만 유지
|
|
2280
|
+
if (state.fires.length > 50) state.fires = state.fires.slice(-50);
|
|
2281
|
+
_writeWakeupHistory(root, state);
|
|
2282
|
+
return state;
|
|
2283
|
+
}
|
|
2284
|
+
// 권장 interval 계산 (sec)
|
|
2285
|
+
// user-trigger 빈도 + pre-wake critical + 최근 평균 gap 기반
|
|
2286
|
+
// 범위: 600 (10min) ~ 2700 (45min)
|
|
2287
|
+
function _computeAdaptiveInterval(root) {
|
|
2288
|
+
const DEFAULT = 1500; // 25min
|
|
2289
|
+
const MIN = 600; // 10min
|
|
2290
|
+
const MAX = 2700; // 45min
|
|
2291
|
+
|
|
2292
|
+
// 1) opt-out — env or override
|
|
2293
|
+
if (process.env.LEERNESS_FIXED_INTERVAL && process.env.LEERNESS_FIXED_INTERVAL !== '0') {
|
|
2294
|
+
const fixed = Number(process.env.LEERNESS_FIXED_INTERVAL);
|
|
2295
|
+
if (!isNaN(fixed) && fixed >= 60) return { interval: fixed, reason: 'env LEERNESS_FIXED_INTERVAL', adaptive: false };
|
|
2296
|
+
return { interval: DEFAULT, reason: 'env override (default)', adaptive: false };
|
|
2297
|
+
}
|
|
2298
|
+
const state = _loadWakeupHistory(root);
|
|
2299
|
+
if (state.override && Number(state.override) >= 60) {
|
|
2300
|
+
return { interval: Number(state.override), reason: 'wakeup-history.json override', adaptive: false };
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
// 2) 활동량 분석 — 최근 2시간 user-trigger 수
|
|
2304
|
+
const now = Date.now();
|
|
2305
|
+
const recent = state.fires.filter(f => (now - new Date(f.at).getTime()) < 2 * 60 * 60 * 1000);
|
|
2306
|
+
const userTriggers = recent.filter(f => f.kind === 'user-trigger').length;
|
|
2307
|
+
const totalRecent = recent.length;
|
|
2308
|
+
|
|
2309
|
+
// 3) 평균 user-trigger gap (분)
|
|
2310
|
+
const userFires = state.fires.filter(f => f.kind === 'user-trigger').slice(-5);
|
|
2311
|
+
let avgGapMin = null;
|
|
2312
|
+
if (userFires.length >= 2) {
|
|
2313
|
+
let totalGap = 0;
|
|
2314
|
+
for (let i = 1; i < userFires.length; i++) {
|
|
2315
|
+
totalGap += new Date(userFires[i].at).getTime() - new Date(userFires[i - 1].at).getTime();
|
|
2316
|
+
}
|
|
2317
|
+
avgGapMin = Math.floor((totalGap / (userFires.length - 1)) / 60000);
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
// 4) pre-wake critical signal (1.9.209)
|
|
2321
|
+
let criticalSignal = false;
|
|
2322
|
+
try {
|
|
2323
|
+
const pw = _loadPreWakeReport(root);
|
|
2324
|
+
if (pw.reports.length > 0) {
|
|
2325
|
+
const latest = pw.reports[pw.reports.length - 1];
|
|
2326
|
+
if (latest.summary && latest.summary.criticalCount > 0) criticalSignal = true;
|
|
2327
|
+
}
|
|
2328
|
+
} catch {}
|
|
2329
|
+
|
|
2330
|
+
// 5) 계산
|
|
2331
|
+
let interval = DEFAULT;
|
|
2332
|
+
const reasons = [];
|
|
2333
|
+
if (userTriggers >= 3) {
|
|
2334
|
+
interval = Math.max(MIN, 900); // 15min — 활발한 사용자 활동
|
|
2335
|
+
reasons.push(`user-trigger ${userTriggers}회 (2h)`);
|
|
2336
|
+
} else if (userTriggers === 0 && totalRecent <= 1) {
|
|
2337
|
+
interval = Math.min(MAX, 2100); // 35min — 한산
|
|
2338
|
+
reasons.push('user-trigger 없음 (2h)');
|
|
2339
|
+
} else {
|
|
2340
|
+
interval = DEFAULT;
|
|
2341
|
+
reasons.push('default 25min');
|
|
2342
|
+
}
|
|
2343
|
+
// avgGap 기반 미세 조정
|
|
2344
|
+
if (avgGapMin !== null && avgGapMin < 20) {
|
|
2345
|
+
interval = Math.max(MIN, Math.min(interval, avgGapMin * 60));
|
|
2346
|
+
reasons.push(`avg user gap ${avgGapMin}min`);
|
|
2347
|
+
}
|
|
2348
|
+
// critical 신호 시 짧게
|
|
2349
|
+
if (criticalSignal && interval > 1200) {
|
|
2350
|
+
interval = 1200; // 20min
|
|
2351
|
+
reasons.push('pre-wake critical detected');
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
return {
|
|
2355
|
+
interval,
|
|
2356
|
+
reason: reasons.join(' + '),
|
|
2357
|
+
adaptive: true,
|
|
2358
|
+
stats: {
|
|
2359
|
+
recentTotal: totalRecent,
|
|
2360
|
+
userTriggers,
|
|
2361
|
+
avgUserGapMin: avgGapMin,
|
|
2362
|
+
criticalSignal
|
|
2363
|
+
}
|
|
2364
|
+
};
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2254
2367
|
// 1.9.203: 자동 라운드 plan 정리 — 사용자 명시
|
|
2255
2368
|
// "백그라운드에 다음 작업이 시작가능한 예상 시간 + 일어났을때 해야하는 일을 정리"
|
|
2256
2369
|
// 라운드 마무리 시 .harness/auto-resume-plan.json 자동 저장 → 다음 wakeup 시 즉시 실행 가능
|
|
@@ -2661,6 +2774,107 @@ function preWakeAuditCmd(root, sub) {
|
|
|
2661
2774
|
log(dim(` → 깨어난 후: leerness pre-wake-audit --last`));
|
|
2662
2775
|
}
|
|
2663
2776
|
|
|
2777
|
+
// 1.9.210: leerness wakeup-interval <get|set|auto|history|record> — adaptive wakeup interval (사용자 명시)
|
|
2778
|
+
// "wakeup 30분등 긴 시간이 필요없다면 간격을 타이트하게 자동으로 조절하도록"
|
|
2779
|
+
// get → 현재 권장 interval + 분석 stats (--json 가능)
|
|
2780
|
+
// set N → wakeup-history.json override 설정 (초)
|
|
2781
|
+
// auto → override 해제 (자동 계산 모드 복귀)
|
|
2782
|
+
// history → 최근 fire 이력 출력
|
|
2783
|
+
// record <kind> → fire 기록 (user-trigger / auto / wakeup-miss)
|
|
2784
|
+
function wakeupIntervalCmd(root, sub, val) {
|
|
2785
|
+
root = absRoot(root);
|
|
2786
|
+
const isTty = process.stdout && process.stdout.isTTY;
|
|
2787
|
+
const cyan = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
|
|
2788
|
+
const grn = s => isTty ? `\x1b[32m${s}\x1b[0m` : s;
|
|
2789
|
+
const yel = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
|
|
2790
|
+
const dim = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
2791
|
+
|
|
2792
|
+
if (!sub || sub === 'help' || sub === '--help') {
|
|
2793
|
+
log(`# leerness wakeup-interval (1.9.210) — adaptive wakeup 간격`);
|
|
2794
|
+
log('');
|
|
2795
|
+
log(` get → 현재 권장 interval + stats (--json 가능)`);
|
|
2796
|
+
log(` set <secs> → override 설정 (예: leerness wakeup-interval set 900)`);
|
|
2797
|
+
log(` auto → override 해제 + 자동 계산 모드`);
|
|
2798
|
+
log(` history → 최근 fire 이력 출력`);
|
|
2799
|
+
log(` record <kind> → fire 기록 (user-trigger / auto / wakeup-miss)`);
|
|
2800
|
+
log('');
|
|
2801
|
+
log(dim(` opt-out: env LEERNESS_FIXED_INTERVAL=1500 (초) 또는 set <secs>`));
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
if (sub === 'get') {
|
|
2806
|
+
const result = _computeAdaptiveInterval(root);
|
|
2807
|
+
if (has('--json')) { log(JSON.stringify(result, null, 2)); return; }
|
|
2808
|
+
log(cyan(`# leerness wakeup-interval get (1.9.210)`));
|
|
2809
|
+
log(` 📊 권장 interval: ${grn(`${result.interval}s (${Math.round(result.interval/60)}min)`)}`);
|
|
2810
|
+
log(` mode: ${result.adaptive ? grn('adaptive') : yel('fixed')}`);
|
|
2811
|
+
log(` reason: ${result.reason}`);
|
|
2812
|
+
if (result.stats) {
|
|
2813
|
+
log('');
|
|
2814
|
+
log(` 📈 stats (recent 2h):`);
|
|
2815
|
+
log(` • total fires: ${result.stats.recentTotal}`);
|
|
2816
|
+
log(` • user triggers: ${result.stats.userTriggers}`);
|
|
2817
|
+
if (result.stats.avgUserGapMin !== null) log(` • avg user gap: ${result.stats.avgUserGapMin}min`);
|
|
2818
|
+
if (result.stats.criticalSignal) log(yel(` • pre-wake critical 감지`));
|
|
2819
|
+
}
|
|
2820
|
+
log('');
|
|
2821
|
+
log(dim(` → ScheduleWakeup(delaySeconds: ${result.interval}) 권장`));
|
|
2822
|
+
return;
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
if (sub === 'set') {
|
|
2826
|
+
const n = Number(val);
|
|
2827
|
+
if (!val || isNaN(n) || n < 60) { console.error('Usage: leerness wakeup-interval set <secs ≥ 60>'); process.exit(1); }
|
|
2828
|
+
const state = _loadWakeupHistory(root);
|
|
2829
|
+
state.override = n;
|
|
2830
|
+
_writeWakeupHistory(root, state);
|
|
2831
|
+
if (has('--json')) { log(JSON.stringify({ override: n }, null, 2)); return; }
|
|
2832
|
+
log(grn(`✓ wakeup-interval override 설정: ${n}s (${Math.round(n/60)}min)`));
|
|
2833
|
+
log(dim(` 해제: leerness wakeup-interval auto`));
|
|
2834
|
+
return;
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
if (sub === 'auto') {
|
|
2838
|
+
const state = _loadWakeupHistory(root);
|
|
2839
|
+
state.override = null;
|
|
2840
|
+
_writeWakeupHistory(root, state);
|
|
2841
|
+
const result = _computeAdaptiveInterval(root);
|
|
2842
|
+
if (has('--json')) { log(JSON.stringify({ override: null, current: result }, null, 2)); return; }
|
|
2843
|
+
log(grn(`✓ override 해제 — 자동 계산 모드`));
|
|
2844
|
+
log(` 현재 권장: ${result.interval}s (${Math.round(result.interval/60)}min) — ${result.reason}`);
|
|
2845
|
+
return;
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
if (sub === 'history') {
|
|
2849
|
+
const state = _loadWakeupHistory(root);
|
|
2850
|
+
if (has('--json')) { log(JSON.stringify({ override: state.override, fires: state.fires.slice(-20) }, null, 2)); return; }
|
|
2851
|
+
log(cyan(`# leerness wakeup-interval history (1.9.210)`));
|
|
2852
|
+
log(` total fires: ${state.fires.length}${state.override ? ` · override: ${state.override}s` : ''}`);
|
|
2853
|
+
log('');
|
|
2854
|
+
const recent = state.fires.slice(-15);
|
|
2855
|
+
if (recent.length === 0) { log(dim(' (없음)')); return; }
|
|
2856
|
+
for (const f of recent) {
|
|
2857
|
+
const icon = f.kind === 'user-trigger' ? '👆' : (f.kind === 'wakeup-miss' ? '❗' : '⏰');
|
|
2858
|
+
log(` ${icon} ${f.at} ${dim(f.kind)}`);
|
|
2859
|
+
}
|
|
2860
|
+
return;
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
if (sub === 'record') {
|
|
2864
|
+
const kind = val || 'auto';
|
|
2865
|
+
const validKinds = ['auto', 'user-trigger', 'wakeup-miss'];
|
|
2866
|
+
if (!validKinds.includes(kind)) { console.error(`kind must be: ${validKinds.join('/')}`); process.exit(1); }
|
|
2867
|
+
_recordWakeupFire(root, kind);
|
|
2868
|
+
if (has('--json')) { log(JSON.stringify({ recorded: kind, at: new Date().toISOString() }, null, 2)); return; }
|
|
2869
|
+
log(grn(`✓ wakeup fire 기록: ${kind}`));
|
|
2870
|
+
return;
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
console.error(`Unknown subcommand: ${sub}`);
|
|
2874
|
+
console.error(`Run: leerness wakeup-interval help`);
|
|
2875
|
+
process.exit(1);
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2664
2878
|
// 1.9.199: wakeup miss 자동 회복 강화 — last-handoff timestamp 기록 + 25min 기대 interval 정밀 측정
|
|
2665
2879
|
// 사용자 명시 (1.9.198 후속): "22:01에 라운드에 자동진입되지않았어"
|
|
2666
2880
|
// 1.9.196 task-log mtime 기반 detector 는 task-log 가 자주 안 업데이트되면 false positive.
|
|
@@ -16064,6 +16278,8 @@ async function main() {
|
|
|
16064
16278
|
if (cmd === 'constraints') return constraintsCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
|
|
16065
16279
|
// 1.9.209: leerness pre-wake-audit — sleep 전 sub-agent audit (사용자 명시)
|
|
16066
16280
|
if (cmd === 'pre-wake-audit') return preWakeAuditCmd(arg('--path', process.cwd()), args[1]);
|
|
16281
|
+
// 1.9.210: leerness wakeup-interval <get|set|auto|history|record> — adaptive interval (사용자 명시)
|
|
16282
|
+
if (cmd === 'wakeup-interval') return wakeupIntervalCmd(arg('--path', process.cwd()), args[1], args[2]);
|
|
16067
16283
|
if (cmd === 'rule' && args[1] === 'add') return ruleAdd(arg('--path', process.cwd()), args.slice(2).filter(x => !x.startsWith('-')).join(' '));
|
|
16068
16284
|
if (cmd === 'rule' && args[1] === 'list') return ruleList(arg('--path', process.cwd()));
|
|
16069
16285
|
if (cmd === 'rule' && args[1] === 'remove') return ruleRemove(arg('--path', process.cwd()), args[2]);
|