leerness 1.9.208 → 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 CHANGED
@@ -1,5 +1,95 @@
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
+
48
+ ## 1.9.209 — 2026-05-22
49
+
50
+ **🔍 pre-wake sub-agent audit + 깨어남 직후 자동 노출 (사용자 명시).**
51
+
52
+ ### 사용자 명시
53
+ > *"메인 에이전트가 슬립전에 서브에이전트를 호출해서 미비된 부분이 있는지, 충돌나는 부분이 있는지, 이전에 누락된 작업이 있는지 등등 여러부분을 탐색하면서 필요한 내용을 정리하고 메인 에이전트가 깨어났을때, 서브에이전트가 정리한 내용을 확인해보거나 하는 기능"*
54
+
55
+ ### 1. `.harness/pre-wake-report.json` — sleep 전 audit 누적
56
+ - `_preWakeReportPath/_loadPreWakeReport/_writePreWakeReport` 파일 I/O 헬퍼
57
+ - 최근 10개만 유지 (rotate)
58
+ - 구조: `{ auditedAt, auditVersion, findings: {critical/warning/info}, summary }`
59
+
60
+ ### 2. `_runPreWakeAudit(root)` — sub-agent audit 6영역
61
+ - **missing-user-requests** (1.9.207 통합) — task-log/plan/decisions 매칭 안 된 요청
62
+ - **stale-user-requests** — 7일+ open 요청
63
+ - **stale-in-progress** — 24h+ 진척 없는 task
64
+ - **drift-handoff-stale** — session-handoff.md 5일+ stale
65
+ - **wakeup-missed** — wakeup miss 감지 (1.9.205)
66
+ - **next-action-pending** — next-action queue 대기
67
+ - **auto-resume-plan-ready/stale** — 1.9.203 plan 상태
68
+
69
+ ### 3. `leerness pre-wake-audit` CLI
70
+ - 기본: 새 audit 실행 + 저장
71
+ - `--last` / `show` / `review` — 가장 최근 저장된 audit 표시
72
+ - `--json` — JSON 출력
73
+
74
+ ### 4. handoff 자동 노출 (사용자 명시 "깨어났을때 확인")
75
+ - 헤드라인 14번째 요소: `🔍 pre-wake NC/MW (ageMin)` — critical N건 + warning M건
76
+ - 본문 자동 섹션: `## 🔍 직전 sleep pre-wake-audit (1.9.209, N분 전)`
77
+ · critical/warning 최대 3개씩 노출
78
+ · 4시간 이내 보고서만 (stale 자동 hide)
79
+ - 헤드라인 라벨: `1.9.81/93/113/152/162/192/197/204/207/209`
80
+
81
+ ### 5. 누적 회귀 (1.9.200~208) — 모두 유지
82
+
83
+ ### 6. stress-v154 — 18/18 PASS
84
+ - 1.9.209 (9) + 성능 (2) + 누적 회귀 (7)
85
+ - 격리 tmp dir 라이프사이클 검증 (실행 → 저장 → --last → rotate to 10)
86
+ - 6 findings kind 키워드 검증
87
+ - 성능: --version cold start avg 517ms · MCP 54 도구 574ms
88
+
89
+ ### 7. 자동 release (71 라운드 main-push streak · 32 라운드 npm publish streak)
90
+
91
+ ---
92
+
3
93
  ## 1.9.208 — 2026-05-22
4
94
 
5
95
  **🚦 플랫폼/API 제약 사전 체크 (사용자 명시).**
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **AI 코딩 에이전트의 거짓 완료·중복·망각·충돌을 막아주는 검수·기억·협업 CLI 하네스.**
4
4
 
5
- [![npm](https://img.shields.io/badge/npm-leerness-blue)](https://www.npmjs.com/package/leerness) [![version](https://img.shields.io/badge/version-1.9.208-green)]() [![tests](https://img.shields.io/badge/e2e-217%2F217-success)]() [![stress](https://img.shields.io/badge/stress--v153-18%2F18-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-54-brightgreen)]() [![rounds](https://img.shields.io/badge/autonomous--rounds-138-blueviolet)]() [![main-push](https://img.shields.io/badge/release--main--push-70_rounds-success)]() [![goal-matrix](https://img.shields.io/badge/목적_매트릭스-98%2F100_(5축)-brightgreen)]() [![constraints](https://img.shields.io/badge/constraints-API_사전체크_6plat-success)]() [![perf](https://img.shields.io/badge/cold_start-483ms-success)]() [![install](https://img.shields.io/badge/npm_i_leerness-즉시_사용_가능-success)]() [![sandbox](https://img.shields.io/badge/runCommandSafe-cwd_jail%2Benv_scrub-success)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
5
+ [![npm](https://img.shields.io/badge/npm-leerness-blue)](https://www.npmjs.com/package/leerness) [![version](https://img.shields.io/badge/version-1.9.210-green)]() [![tests](https://img.shields.io/badge/e2e-217%2F217-success)]() [![stress](https://img.shields.io/badge/stress--v155-18%2F18-success)]() [![mcp](https://img.shields.io/badge/MCP--tools-54-brightgreen)]() [![rounds](https://img.shields.io/badge/autonomous--rounds-140-blueviolet)]() [![main-push](https://img.shields.io/badge/release--main--push-72_rounds-success)]() [![goal-matrix](https://img.shields.io/badge/목적_매트릭스-98%2F100_(5축)-brightgreen)]() [![adaptive](https://img.shields.io/badge/wakeup-adaptive_10~45min-success)]() [![perf](https://img.shields.io/badge/cold_start-440ms-success)]() [![install](https://img.shields.io/badge/npm_i_leerness-즉시_사용_가능-success)]() [![sandbox](https://img.shields.io/badge/runCommandSafe-cwd_jail%2Benv_scrub-success)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
6
6
 
7
7
  ```
8
8
  ╔══════════════════════════════════════════════════════════════╗
@@ -12,9 +12,9 @@
12
12
  ║ ██║ ██╔══╝ ██╔══╝ ██╔══██╗██║╚██╗██║██╔══╝ ╚════██║ ║
13
13
  ║ ███████╗███████╗███████╗██║ ██║██║ ╚████║███████╗███████║ ║
14
14
  ║ ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝ ║
15
- ║ v1.9.208 AI Agent Reliability Harness + Sandbox ║
15
+ ║ v1.9.210 AI Agent Reliability Harness + Sandbox ║
16
16
  ║ verify · remember · orchestrate · audit · sandbox · drift ║
17
- 🚦 플랫폼/API 제약 사전 체크 6종 (사용자 명시)
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.208';
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 처리).
@@ -2111,6 +2111,259 @@ function _checkRequestConstraints(root, text) {
2111
2111
  return { matched, suggestions, totalPlatforms: Object.keys(catalog.platforms).length };
2112
2112
  }
2113
2113
 
2114
+ // 1.9.209: pre-wake sub-agent audit (사용자 명시)
2115
+ // "메인 에이전트가 슬립전에 서브에이전트를 호출해서 미비된 부분이 있는지, 충돌나는 부분이 있는지,
2116
+ // 이전에 누락된 작업이 있는지 등등 여러부분을 탐색하면서 필요한 내용을 정리하고
2117
+ // 메인 에이전트가 깨어났을때, 서브에이전트가 정리한 내용을 확인해보거나 하는 기능"
2118
+ // .harness/pre-wake-report.json 에 sleep 전 audit 결과 누적 (최근 10개 유지) →
2119
+ // 다음 handoff 에서 자동 노출 → 깨어남 직후 메인 에이전트가 검토 가능
2120
+ // audit 영역: 미답 요청 / 24h+ 무진척 task / drift 신호 / feature_graph 충돌 / 플랫폼 제약 누락
2121
+ function _preWakeReportPath(root) { return path.join(root, '.harness', 'pre-wake-report.json'); }
2122
+ function _loadPreWakeReport(root) {
2123
+ try {
2124
+ const fp = _preWakeReportPath(root);
2125
+ if (!exists(fp)) return { reports: [] };
2126
+ const j = JSON.parse(read(fp));
2127
+ return { reports: Array.isArray(j.reports) ? j.reports : [] };
2128
+ } catch { return { reports: [] }; }
2129
+ }
2130
+ function _writePreWakeReport(root, reports) {
2131
+ try {
2132
+ mkdirp(path.join(root, '.harness'));
2133
+ writeUtf8(_preWakeReportPath(root), JSON.stringify({ reports, updatedAt: new Date().toISOString() }, null, 2));
2134
+ return true;
2135
+ } catch { return false; }
2136
+ }
2137
+ // 실제 audit 실행 — sub-agent 역할 (단일 프로세스에서 sync로 다양한 신호 수집)
2138
+ function _runPreWakeAudit(root) {
2139
+ const audit = {
2140
+ auditedAt: new Date().toISOString(),
2141
+ auditVersion: VERSION,
2142
+ findings: { critical: [], warning: [], info: [] }
2143
+ };
2144
+ // 1) 미답 사용자 요청 (1.9.207)
2145
+ try {
2146
+ const reqAudit = _auditUserRequests(root);
2147
+ if (reqAudit.missing && reqAudit.missing.length > 0) {
2148
+ audit.findings.critical.push({
2149
+ kind: 'missing-user-requests',
2150
+ count: reqAudit.missing.length,
2151
+ detail: `${reqAudit.missing.length}건 사용자 요청이 task-log/plan/decisions 에 매칭 안 됨`,
2152
+ items: reqAudit.missing.slice(0, 5).map(r => ({ id: r.id, text: r.text.slice(0, 80) }))
2153
+ });
2154
+ }
2155
+ if (reqAudit.stale && reqAudit.stale.length > 0) {
2156
+ audit.findings.warning.push({
2157
+ kind: 'stale-user-requests',
2158
+ count: reqAudit.stale.length,
2159
+ detail: `${reqAudit.stale.length}건 사용자 요청이 7일+ open 상태`
2160
+ });
2161
+ }
2162
+ } catch (e) { audit.findings.info.push({ kind: 'audit-error', area: 'user-requests', error: String(e.message || e) }); }
2163
+
2164
+ // 2) 24h+ 무진척 task — progress-tracker 검사
2165
+ try {
2166
+ const ptPath = path.join(root, '.harness', 'progress-tracker.md');
2167
+ if (exists(ptPath)) {
2168
+ const content = read(ptPath);
2169
+ const stat = fs.statSync(ptPath);
2170
+ const ageMin = Math.floor((Date.now() - stat.mtimeMs) / 60000);
2171
+ // in-progress 태그가 있는데 24h+ 갱신 없음
2172
+ const inProgressCount = (content.match(/status:\s*in-progress/gi) || []).length;
2173
+ if (inProgressCount > 0 && ageMin > 24 * 60) {
2174
+ audit.findings.warning.push({
2175
+ kind: 'stale-in-progress',
2176
+ count: inProgressCount,
2177
+ detail: `${inProgressCount}건 in-progress task, ${Math.floor(ageMin/60)}h 갱신 없음`,
2178
+ ageMin
2179
+ });
2180
+ }
2181
+ }
2182
+ } catch {}
2183
+
2184
+ // 3) drift 신호 — session-handoff/progress-tracker stale 검사
2185
+ try {
2186
+ const handoffPath = path.join(root, '.harness', 'session-handoff.md');
2187
+ if (exists(handoffPath)) {
2188
+ const stat = fs.statSync(handoffPath);
2189
+ const ageDay = (Date.now() - stat.mtimeMs) / 86400000;
2190
+ if (ageDay > 5) {
2191
+ audit.findings.warning.push({
2192
+ kind: 'drift-handoff-stale',
2193
+ ageDay: Math.round(ageDay * 10) / 10,
2194
+ detail: `session-handoff.md ${ageDay.toFixed(1)}일 stale — drift 위험`
2195
+ });
2196
+ }
2197
+ }
2198
+ } catch {}
2199
+
2200
+ // 4) 다음 라운드 plan 적용 가능 여부 (1.9.203)
2201
+ try {
2202
+ const plan = _loadAutoResumePlan(root);
2203
+ if (!plan) {
2204
+ audit.findings.info.push({ kind: 'no-auto-resume-plan', detail: 'auto-resume-plan.json 없음 — 다음 라운드 진입 시 manual context' });
2205
+ } else if (plan.ageMin > 12 * 60) {
2206
+ audit.findings.warning.push({ kind: 'auto-resume-plan-stale', detail: `plan ${plan.ageMin}분 전 작성 — outdated 가능` });
2207
+ } else {
2208
+ audit.findings.info.push({ kind: 'auto-resume-plan-ready', detail: `nextRoundVersion=${plan.nextRoundVersion}, focus=${(plan.focus||'').slice(0,50)}` });
2209
+ }
2210
+ } catch {}
2211
+
2212
+ // 5) Active wakeup 상태 (1.9.205)
2213
+ try {
2214
+ const wkStat = _analyzeWakeupStatus(root);
2215
+ if (wkStat.hasWakeup && wkStat.kind === 'missed') {
2216
+ audit.findings.critical.push({
2217
+ kind: 'wakeup-missed',
2218
+ deltaMin: wkStat.deltaMin,
2219
+ detail: `wakeup ${wkStat.deltaMin}분 지남 — 다음 라운드 진입 늦음`
2220
+ });
2221
+ }
2222
+ } catch {}
2223
+
2224
+ // 6) next-action queue 잔여 (1.9.201)
2225
+ try {
2226
+ const q = _loadNextActionQueue(root);
2227
+ if (q && q.pending && q.pending.length > 0) {
2228
+ audit.findings.info.push({
2229
+ kind: 'next-action-pending',
2230
+ count: q.pending.length,
2231
+ detail: `${q.pending.length}건 next-action 대기 — leerness next-action take 로 즉시 처리`
2232
+ });
2233
+ }
2234
+ } catch {}
2235
+
2236
+ // 7) 통합 요약
2237
+ audit.summary = {
2238
+ criticalCount: audit.findings.critical.length,
2239
+ warningCount: audit.findings.warning.length,
2240
+ infoCount: audit.findings.info.length,
2241
+ needsAttention: audit.findings.critical.length > 0 || audit.findings.warning.length >= 2
2242
+ };
2243
+ return audit;
2244
+ }
2245
+ function _saveAndAppendPreWakeReport(root, audit) {
2246
+ const state = _loadPreWakeReport(root);
2247
+ state.reports.push(audit);
2248
+ // 최근 10개만 유지
2249
+ if (state.reports.length > 10) state.reports = state.reports.slice(-10);
2250
+ _writePreWakeReport(root, state.reports);
2251
+ return audit;
2252
+ }
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
+
2114
2367
  // 1.9.203: 자동 라운드 plan 정리 — 사용자 명시
2115
2368
  // "백그라운드에 다음 작업이 시작가능한 예상 시간 + 일어났을때 해야하는 일을 정리"
2116
2369
  // 라운드 마무리 시 .harness/auto-resume-plan.json 자동 저장 → 다음 wakeup 시 즉시 실행 가능
@@ -2434,6 +2687,194 @@ function constraintsCmd(root, sub, ...rest) {
2434
2687
  process.exit(1);
2435
2688
  }
2436
2689
 
2690
+ // 1.9.209: leerness pre-wake-audit — sleep 전 sub-agent audit CLI (사용자 명시)
2691
+ // "메인 에이전트가 슬립전에 서브에이전트를 호출해서 미비된 부분이 있는지, 충돌나는 부분이 있는지,
2692
+ // 이전에 누락된 작업이 있는지 등등 여러부분을 탐색하면서 필요한 내용을 정리"
2693
+ // 기본: 새 audit 실행 + 저장 + 출력
2694
+ // --last: 최근 저장된 audit 표시 (사용자 깨어났을 때 검토용)
2695
+ // --json: JSON 출력
2696
+ function preWakeAuditCmd(root, sub) {
2697
+ root = absRoot(root);
2698
+ const isTty = process.stdout && process.stdout.isTTY;
2699
+ const cyan = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
2700
+ const grn = s => isTty ? `\x1b[32m${s}\x1b[0m` : s;
2701
+ const yel = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
2702
+ const red = s => isTty ? `\x1b[31m${s}\x1b[0m` : s;
2703
+ const dim = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
2704
+
2705
+ // --last 또는 review/show — 가장 최근 audit 표시 (실행 안 함)
2706
+ if (has('--last') || sub === 'show' || sub === 'review') {
2707
+ const state = _loadPreWakeReport(root);
2708
+ if (state.reports.length === 0) {
2709
+ log(cyan(`# leerness pre-wake-audit (1.9.209) — review`));
2710
+ log(dim(` (저장된 audit 없음 — leerness pre-wake-audit 으로 새 실행)`));
2711
+ return;
2712
+ }
2713
+ const latest = state.reports[state.reports.length - 1];
2714
+ if (has('--json')) { log(JSON.stringify(latest, null, 2)); return; }
2715
+ log(cyan(`# leerness pre-wake-audit (1.9.209) — review (latest)`));
2716
+ log(dim(` audited at: ${latest.auditedAt} (version ${latest.auditVersion})`));
2717
+ log('');
2718
+ const sum = latest.summary || {};
2719
+ log(` 📊 ${red(`critical ${sum.criticalCount || 0}`)} · ${yel(`warning ${sum.warningCount || 0}`)} · info ${sum.infoCount || 0}`);
2720
+ if (sum.needsAttention) log(yel(` ⚠ needsAttention=true — 메인 에이전트 검토 필요`));
2721
+ log('');
2722
+ if (latest.findings.critical && latest.findings.critical.length) {
2723
+ log(red(`## 🚨 critical (${latest.findings.critical.length})`));
2724
+ latest.findings.critical.forEach(f => log(` - [${f.kind}] ${f.detail}`));
2725
+ log('');
2726
+ }
2727
+ if (latest.findings.warning && latest.findings.warning.length) {
2728
+ log(yel(`## ⚠ warning (${latest.findings.warning.length})`));
2729
+ latest.findings.warning.forEach(f => log(` - [${f.kind}] ${f.detail}`));
2730
+ log('');
2731
+ }
2732
+ if (latest.findings.info && latest.findings.info.length) {
2733
+ log(dim(`## ℹ info (${latest.findings.info.length})`));
2734
+ latest.findings.info.forEach(f => log(dim(` - [${f.kind}] ${f.detail}`)));
2735
+ }
2736
+ return;
2737
+ }
2738
+
2739
+ // 기본: 새 audit 실행 + 저장
2740
+ const audit = _runPreWakeAudit(root);
2741
+ _saveAndAppendPreWakeReport(root, audit);
2742
+
2743
+ if (has('--json')) { log(JSON.stringify(audit, null, 2)); return; }
2744
+ log(cyan(`# leerness pre-wake-audit (1.9.209) — sleep 전 sub-agent audit`));
2745
+ log(dim(` audited at: ${audit.auditedAt}`));
2746
+ log('');
2747
+ const sum = audit.summary;
2748
+ log(` 📊 ${red(`critical ${sum.criticalCount}`)} · ${yel(`warning ${sum.warningCount}`)} · info ${sum.infoCount}`);
2749
+ if (sum.needsAttention) {
2750
+ log(yel(` ⚠ needsAttention=true — 깨어난 후 메인 에이전트 검토 필수`));
2751
+ } else {
2752
+ log(grn(` ✓ 잠재 이슈 없음 — sleep 안전`));
2753
+ }
2754
+ log('');
2755
+ if (audit.findings.critical.length) {
2756
+ log(red(`## 🚨 critical (${audit.findings.critical.length})`));
2757
+ audit.findings.critical.forEach(f => {
2758
+ log(` - [${f.kind}] ${f.detail}`);
2759
+ if (f.items) f.items.forEach(i => log(dim(` • ${i.id}: ${i.text}`)));
2760
+ });
2761
+ log('');
2762
+ }
2763
+ if (audit.findings.warning.length) {
2764
+ log(yel(`## ⚠ warning (${audit.findings.warning.length})`));
2765
+ audit.findings.warning.forEach(f => log(` - [${f.kind}] ${f.detail}`));
2766
+ log('');
2767
+ }
2768
+ if (audit.findings.info.length) {
2769
+ log(dim(`## ℹ info (${audit.findings.info.length})`));
2770
+ audit.findings.info.forEach(f => log(dim(` - [${f.kind}] ${f.detail}`)));
2771
+ log('');
2772
+ }
2773
+ log(dim(` → 저장: .harness/pre-wake-report.json (최근 10개 유지)`));
2774
+ log(dim(` → 깨어난 후: leerness pre-wake-audit --last`));
2775
+ }
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
+
2437
2878
  // 1.9.199: wakeup miss 자동 회복 강화 — last-handoff timestamp 기록 + 25min 기대 interval 정밀 측정
2438
2879
  // 사용자 명시 (1.9.198 후속): "22:01에 라운드에 자동진입되지않았어"
2439
2880
  // 1.9.196 task-log mtime 기반 detector 는 task-log 가 자주 안 업데이트되면 false positive.
@@ -4477,13 +4918,52 @@ function handoff(root) {
4477
4918
  parts.push(`📥 요청 ${audit.open} (tracked)`);
4478
4919
  }
4479
4920
  } catch {}
4921
+ // 14) 1.9.209: pre-wake-audit 최근 보고서 (사용자 명시) — 깨어남 직후 자동 노출
4922
+ try {
4923
+ const pwState = _loadPreWakeReport(root);
4924
+ if (pwState.reports.length > 0) {
4925
+ const latest = pwState.reports[pwState.reports.length - 1];
4926
+ const sum = latest.summary || {};
4927
+ if (sum.criticalCount > 0 || sum.warningCount > 0) {
4928
+ const ageMin = Math.floor((Date.now() - new Date(latest.auditedAt).getTime()) / 60000);
4929
+ parts.push(`🔍 pre-wake ${sum.criticalCount}C/${sum.warningCount}W (${ageMin}m)`);
4930
+ }
4931
+ }
4932
+ } catch {}
4480
4933
  if (parts.length) {
4481
4934
  const isTty = process.stdout && process.stdout.isTTY;
4482
4935
  const cy = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
4483
- log(cy(`📊 헤드라인 (1.9.81/93/113/152/162/192/197/204/207): ${parts.join(' · ')}`));
4936
+ log(cy(`📊 헤드라인 (1.9.81/93/113/152/162/192/197/204/207/209): ${parts.join(' · ')}`));
4484
4937
  }
4485
4938
  } catch {}
4486
4939
  }
4940
+
4941
+ // 1.9.209: pre-wake-audit 본문 자동 노출 (handoff 마지막 부분, 사용자 명시)
4942
+ // "메인 에이전트가 깨어났을때, 서브에이전트가 정리한 내용을 확인"
4943
+ try {
4944
+ const pwState = _loadPreWakeReport(root);
4945
+ if (pwState.reports.length > 0) {
4946
+ const latest = pwState.reports[pwState.reports.length - 1];
4947
+ const ageMin = Math.floor((Date.now() - new Date(latest.auditedAt).getTime()) / 60000);
4948
+ // 4시간 이내 보고서만 자동 노출 (오래된 건 stale)
4949
+ if (ageMin < 240 && (latest.summary.criticalCount > 0 || latest.summary.warningCount > 0)) {
4950
+ const isTty = process.stdout && process.stdout.isTTY;
4951
+ const cy2 = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
4952
+ const rd2 = s => isTty ? `\x1b[31m${s}\x1b[0m` : s;
4953
+ const yl2 = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
4954
+ const dm2 = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
4955
+ log('');
4956
+ log(cy2(`## 🔍 직전 sleep pre-wake-audit (1.9.209, ${ageMin}분 전)`));
4957
+ if (latest.findings.critical && latest.findings.critical.length) {
4958
+ latest.findings.critical.slice(0, 3).forEach(f => log(rd2(` 🚨 [${f.kind}] ${f.detail}`)));
4959
+ }
4960
+ if (latest.findings.warning && latest.findings.warning.length) {
4961
+ latest.findings.warning.slice(0, 3).forEach(f => log(yl2(` ⚠ [${f.kind}] ${f.detail}`)));
4962
+ }
4963
+ log(dm2(` → 상세: leerness pre-wake-audit --last`));
4964
+ }
4965
+ }
4966
+ } catch {}
4487
4967
  // 1.9.8: active rules 자동 노출 (매 세션 시작 시 AI에게 보임)
4488
4968
  const activeRules = readRules(root).filter(r => r.status === 'active');
4489
4969
  if (activeRules.length) {
@@ -15796,6 +16276,10 @@ async function main() {
15796
16276
  if (cmd === 'requests') return requestsCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
15797
16277
  // 1.9.208: leerness constraints <list|check|add> — 플랫폼/API 제약 사전 체크 (사용자 명시)
15798
16278
  if (cmd === 'constraints') return constraintsCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
16279
+ // 1.9.209: leerness pre-wake-audit — sleep 전 sub-agent audit (사용자 명시)
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]);
15799
16283
  if (cmd === 'rule' && args[1] === 'add') return ruleAdd(arg('--path', process.cwd()), args.slice(2).filter(x => !x.startsWith('-')).join(' '));
15800
16284
  if (cmd === 'rule' && args[1] === 'list') return ruleList(arg('--path', process.cwd()));
15801
16285
  if (cmd === 'rule' && args[1] === 'remove') return ruleRemove(arg('--path', process.cwd()), args[2]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.208",
3
+ "version": "1.9.210",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",