leerness 1.9.39 → 1.9.41

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,76 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.41 — 2026-05-19
4
+
5
+ **디스크 마이그레이션 ↔ AI 컨텍스트 인지 갭 차단 — 맞춤형 차분 마이그레이션**.
6
+
7
+ 사용자 통찰: 같은 채팅 세션에서 leerness를 latest로 migrate해도, AI 에이전트는 이전 청크의 마인드셋으로 계속 작업하여 신규 도구(release pack, drift check 등)를 자동으로 호출하지 않는 패턴 발견. migrate는 파일만 업데이트, AI에겐 "새 도구가 들어왔다"는 신호 전달 부재.
8
+
9
+ ### Added
10
+
11
+ - **`leerness whats-new [--from V] [--to V] [--json]`** 신규 명령 — CHANGELOG.md를 자동 파싱하여 두 버전 사이의 차분 추출:
12
+ - 신규 명령 (`leerness X` 패턴), 신규 플래그 (`--xxx`), 신규 파일 (`.harness/*.md`) 자동 분류
13
+ - 각 버전의 헤드라인 (`**...**` 또는 첫 라인) 추출
14
+ - AI 가독 권장 행동 자동 출력
15
+ - **`migrate` 후 stdout에 AI must re-read 차분 자동 출력** — migrate 직전 이전 버전을 캡처 (`_previousVersion`) → CHANGELOG 차분 추출 → 신규 명령/파일을 stdout에 즉시 표시:
16
+ - "이전 청크의 기억 무효 — 새 도구 우선 시도" 명시
17
+ - 같은 세션 내 AI 인-컨텍스트에 신규 도구 인지 주입
18
+ - **`migration-report.md`에 "🤖 AI must re-read" 섹션 영구 기록** — 신규 명령/플래그/파일 + 버전별 헤드라인 + 권장 행동
19
+ - **`handoff`가 fresh migration-report (24h 내) 시 자동 알림** — "🆕 최근 N시간 전 migrate 차분" 블록 자동 표시. 같은 세션 내 매 handoff 호출이 AI에게 신규 도구 재안내.
20
+
21
+ ### 발견된 시스템 결함 (이번 라운드 해결)
22
+ - ❌ **before 1.9.41**: migrate가 파일만 업데이트, AI 마인드셋 stale 유지 → 신규 도구 자동 호출 X
23
+ - ✅ **1.9.41 이후**: migrate 직후 stdout + migration-report.md + handoff 모두 신규 도구를 AI 가독 포맷으로 노출 → "잊을 수 없는" 차분 안내
24
+
25
+ ### 자기 검증
26
+ - 의도적으로 root를 1.9.37로 되돌림 → `leerness migrate .` 호출 → **AI must re-read 차분 자동 stdout 출력**:
27
+ - `📌 신규 명령: leerness release pack`
28
+ - 1.9.38/1.9.39/1.9.40 버전별 헤드라인 자동 추출
29
+ - 권장 행동 4단계 (--help, 신규 파일 재독, 인스트럭션 재독, whats-new --json)
30
+
31
+ ### e2e: 186/186 PASS (1.9.40 182 + 신규 4)
32
+
33
+ ### 정책
34
+ - ✅ 차분 안내는 **AI 가독 포맷** (`**📌**`, `` `leerness X` `` 등 마크다운)
35
+ - ✅ 같은 세션 내 다양한 채널 (stdout + report + handoff)로 *반복 노출* → 청크 stale 방지
36
+ - ✅ 추출은 CHANGELOG.md 파싱 — 새 라운드 마다 자동 갱신
37
+
38
+ ## 1.9.40 — 2026-05-19
39
+
40
+ **dogfooding gap 차단 — `leerness release pack` 통합 명령 + audit README mismatch 자동 감지**.
41
+
42
+ 세션 메타-감사(`_reports/SESSION_LEERNESS_USAGE_AUDIT.md`)에서 발견한 1.9.40 후보 4건을 모두 통합. 메인 에이전트가 "라운드 마감 = e2e/pack"으로만 끝내고 leerness 자체 마감을 잊는 패턴을 도구로 차단.
43
+
44
+ ### Added
45
+
46
+ - **`leerness release pack [path]`** 신규 명령 — 라운드 마감 통합 워크플로:
47
+ - `--dry-run` — 시뮬레이션 모드
48
+ - `--task-add "<title>"` — progress-tracker에 라운드 마감 task 자동 등록
49
+ - `--parent-migrate` — 부모 워크스페이스(`..`)의 `.harness`도 함께 latest로 migrate (dogfooding gap 차단)
50
+ - `--close` — `session close` 자동 실행
51
+ - `--no-readme-sync` — README 자동 동기화 스킵 (기본은 적용)
52
+ - 사용 예: `leerness release pack . --task-add "1.9.41 X 통합" --parent-migrate --close`
53
+ - **`syncReadme` 자동 갱신 강화**:
54
+ - `package.json#version` 또는 `.harness/HARNESS_VERSION` 기반 README의 version 배지 자동 갱신
55
+ - `scripts/e2e.js`의 `total++` 카운트 기반 e2e 배지 추세 반영
56
+
57
+ ### Fixed (audit 강화)
58
+
59
+ - **`leerness audit`에 README ↔ package.json version mismatch 자동 감지** — dogfooding gap의 가장 흔한 패턴 자동 차단:
60
+ - `audit`: warning 출력
61
+ - `audit --fix`: README 배지 자동 갱신
62
+ - 메타 감사에서 발견한 "leerness-pkg는 1.9.40인데 README는 1.9.38" 같은 stale 사전 차단
63
+
64
+ ### 정책
65
+ - ✅ `release pack`은 npm 호출 외엔 `.harness`만 갱신 (사용자 메모리 보존)
66
+ - ✅ `--parent-migrate`는 명시 플래그 필요 (자동 부모 변경 없음)
67
+ - ✅ README mismatch는 warning만 (failures가 아님 — 사용자 차단 X)
68
+
69
+ ### 실측
70
+ - 메타 감사에서 발견한 4 후보 모두 통합
71
+ - e2e: 182/182 PASS (1.9.39 178 + 신규 4)
72
+ - 자체 검증: leerness-pkg에 `release pack --dry-run --task-add` 호출 → task T-0001 자동 등록
73
+
3
74
  ## 1.9.39 — 2026-05-19
4
75
 
5
76
  **AI 하네스 엔지니어링 6단계 워크플로 자동 유도 + drift 자동 회복**.
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.39-green)]() [![tests](https://img.shields.io/badge/e2e-178%2F178-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.40-green)]() [![tests](https://img.shields.io/badge/e2e-138%2F138-success)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
6
6
 
7
7
  ```
8
8
  ╔══════════════════════════════════════════════════════════════╗
@@ -12,7 +12,7 @@
12
12
  ║ ██║ ██╔══╝ ██╔══╝ ██╔══██╗██║╚██╗██║██╔══╝ ╚════██║ ║
13
13
  ║ ███████╗███████╗███████╗██║ ██║██║ ╚████║███████╗███████║ ║
14
14
  ║ ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝ ║
15
- ║ v1.9.38 AI Agent Reliability Harness
15
+ ║ v1.9.41 AI Agent Reliability Harness
16
16
  ║ verify · remember · orchestrate · audit · prevent drift ║
17
17
  ╚══════════════════════════════════════════════════════════════╝
18
18
  ```
@@ -116,22 +116,24 @@ npx leerness session close .
116
116
 
117
117
  `leerness-bench` 28 프로젝트 124 task 측정 결과:
118
118
 
119
- | 카테고리 | 적용 | 미적용 | 개선 |
120
- |---|---:|---:|---:|
121
- | 다중 에이전트 효율 | 100/100 | 3/100 | **+97** |
122
- | 자동 검수 (verify-claim) | 98/100 | 0/100 | +98 |
123
- | 재사용 인식 | 100/100 | 0/100 | +100 |
124
- | 워크스페이스 가시성 | 99/100 | 0/100 | +99 |
125
- | 자동 BUG 감지 | 100/100 | 0/100 | +100 |
126
- | 컨텍스트 유지 | 100/100 | 0/100 | +100 |
127
- | **종합** | **597/600 (99%)** | 3/600 (0.5%) | **+594** |
119
+ | 카테고리 | 적용 | 미적용 | 개선 |
120
+ | ------------------------ | ----------------------: | -----------: | -------------: |
121
+ | 다중 에이전트 효율 | 100/100 | 3/100 | **+97** |
122
+ | 자동 검수 (verify-claim) | 98/100 | 0/100 | +98 |
123
+ | 재사용 인식 | 100/100 | 0/100 | +100 |
124
+ | 워크스페이스 가시성 | 99/100 | 0/100 | +99 |
125
+ | 자동 BUG 감지 | 100/100 | 0/100 | +100 |
126
+ | 컨텍스트 유지 | 100/100 | 0/100 | +100 |
127
+ | **종합** | **597/600 (99%)** | 3/600 (0.5%) | **+594** |
128
128
 
129
129
  ### 실제 작업 시간 절감
130
+
130
131
  - **수동 검수 90s → 자동 1.5s** (`verify-claim --run-tests`)
131
132
  - **워크스페이스 28 프로젝트 1 명령** (vs 112 개별 명령)
132
133
  - **컨텍스트 적재 500자** (`handoff --compact`, AI 토큰 비용 90% 절감)
133
134
 
134
135
  ### 실제 BUG 자동 발견 사례
136
+
135
137
  - **거짓 완료**: 5건 (모두 verify-claim에서 evidence 누락 감지)
136
138
  - **사양 불일치**: rpg-replay에서 `tick.damage` vs `tick.amount` 필드명 충돌 자동 감지 (contract verify)
137
139
  - **보안 위험**: `contract verify`가 require()로 임의 코드 실행 → 정적 분석으로 즉시 수정 (1.9.36)
@@ -141,26 +143,27 @@ npx leerness session close .
141
143
 
142
144
  ## 어떤 함정을 어떻게 막나요?
143
145
 
144
- | AI와 일할 때 함정 | leerness 도구 | 효과 |
145
- |---|---|---|
146
- | "완료했습니다"인데 코드 변경이 없음 | `verify-claim --run-tests` | 증거 파일 + 테스트 실제 실행 검증 |
147
- | "API 호출 완료"인데 URL 코드 없음 | `optimism-check` | 10 카테고리 패턴 + URL 매핑 + 신뢰도 점수 |
148
- | 같은 함수를 여러 프로젝트에 중복 | `reuse-map --strict-elements` | 함수명 fuzzy 중복 감지 |
149
- | 다음 세션이 컨텍스트 잃음 | `handoff` 3채널 자동 적재 | 500자 압축 (`--compact`) |
150
- | 표면적 코드 리뷰 (도메인 깊이 부족) | `review --persona security,performance,ux` | 도메인 sub-agent 자동 부여 |
151
- | 외부 AI CLI 자동 호출 위험 | `agents list/dispatch/quota` | 환경변수 활성화 + 명시적 분배 |
152
- | npx 캐시로 옛 버전 실행 | `_warnIfStale` 자동 (1.9.33+) | npm latest 자동 비교 + 경고 |
153
- | 멀티 sub-agent 파일 충돌 | `agents dispatch` 안내 + 경로 격리 | last-writer-wins 위험 사전 차단 |
154
- | sub-agent마다 사양 해석 다름 | **`contract verify`** | 명세 ↔ 구현 함수/필드 자동 검사 |
155
- | 신규 모듈 capability 미등록 | **`reuse autodetect`** | `module.exports` 정적 분석 + 자동 등록 |
156
- | 라운드 길어지며 메인이 leerness 잊음 | **`drift check`** + `agent-reminders.md` 자동 | 4 신호 + 자동 reminder + 학습 |
157
- | TodoWrite ↔ progress-tracker 분리 | **`task sync --from`** (1.9.38) | TodoWrite JSON 자동 import |
146
+ | AI와 일할 때 함정 | leerness 도구 | 효과 |
147
+ | ------------------------------------ | ------------------------------------------------------- | ----------------------------------------- |
148
+ | "완료했습니다"인데 코드 변경이 없음 | `verify-claim --run-tests` | 증거 파일 + 테스트 실제 실행 검증 |
149
+ | "API 호출 완료"인데 URL 코드 없음 | `optimism-check` | 10 카테고리 패턴 + URL 매핑 + 신뢰도 점수 |
150
+ | 같은 함수를 여러 프로젝트에 중복 | `reuse-map --strict-elements` | 함수명 fuzzy 중복 감지 |
151
+ | 다음 세션이 컨텍스트 잃음 | `handoff` 3채널 자동 적재 | 500자 압축 (`--compact`) |
152
+ | 표면적 코드 리뷰 (도메인 깊이 부족) | `review --persona security,performance,ux` | 도메인 sub-agent 자동 부여 |
153
+ | 외부 AI CLI 자동 호출 위험 | `agents list/dispatch/quota` | 환경변수 활성화 + 명시적 분배 |
154
+ | npx 캐시로 옛 버전 실행 | `_warnIfStale` 자동 (1.9.33+) | npm latest 자동 비교 + 경고 |
155
+ | 멀티 sub-agent 파일 충돌 | `agents dispatch` 안내 + 경로 격리 | last-writer-wins 위험 사전 차단 |
156
+ | sub-agent마다 사양 해석 다름 | **`contract verify`** | 명세 ↔ 구현 함수/필드 자동 검사 |
157
+ | 신규 모듈 capability 미등록 | **`reuse autodetect`** | `module.exports` 정적 분석 + 자동 등록 |
158
+ | 라운드 길어지며 메인이 leerness 잊음 | **`drift check`** + `agent-reminders.md` 자동 | 4 신호 + 자동 reminder + 학습 |
159
+ | TodoWrite ↔ progress-tracker 분리 | **`task sync --from`** (1.9.38) | TodoWrite JSON 자동 import |
158
160
 
159
161
  ---
160
162
 
161
163
  ## 핵심 명령
162
164
 
163
165
  ### 일상
166
+
164
167
  ```bash
165
168
  leerness init [path] [--language ko|en] # 신규 프로젝트 (방향키 multi-select)
166
169
  leerness handoff [path] [--compact] # 컨텍스트 적재 (drift 자동 경고)
@@ -173,6 +176,7 @@ leerness usage stats [path] # 명령별 누적 카운터
173
176
  ```
174
177
 
175
178
  ### 워크스페이스 (멀티 프로젝트)
179
+
176
180
  ```bash
177
181
  leerness handoff --all-apps # 전 프로젝트 통합 뷰
178
182
  leerness reuse-map --all-apps --strict-elements
@@ -185,6 +189,7 @@ leerness task sync --from <todo.json> # TodoWrite import
185
189
  ```
186
190
 
187
191
  ### 외부 AI CLI · 멀티 에이전트
192
+
188
193
  ```bash
189
194
  leerness setup-agents # 인터랙티브 활성화 + 자동 설치
190
195
  leerness agents list / check / quota # 상태/한도
@@ -193,12 +198,14 @@ leerness agents bench "<task>" # 3 CLI 동시 호출 + 비교표
193
198
  ```
194
199
 
195
200
  ### 페르소나·리뷰
201
+
196
202
  ```bash
197
203
  leerness persona list / show <id> / add <id>
198
204
  leerness review <file> --persona security,performance,ux
199
205
  ```
200
206
 
201
207
  ### 보안·인코딩
208
+
202
209
  ```bash
203
210
  leerness scan secrets . # AWS/GitHub/OpenAI/Anthropic/Google/Slack/PEM
204
211
  leerness encoding check . # BOM/UTF-16/한글 라운드트립
@@ -207,6 +214,7 @@ leerness gate [path] # verify + audit + scan + encoding + lazy 통합
207
214
  ```
208
215
 
209
216
  ### 버전 관리
217
+
210
218
  ```bash
211
219
  leerness update --check # 24h 캐시로 새 버전 감지
212
220
  leerness update --yes # 자동 마이그레이션 + 검증
@@ -217,6 +225,7 @@ leerness update --yes # 자동 마이그레이션 + 검증
217
225
  ## 사용 시나리오
218
226
 
219
227
  ### 시나리오 1: AI에게 작업 시키고 거짓 완료 검증
228
+
220
229
  ```bash
221
230
  # AI에게 작업 지시 후
222
231
  leerness verify-claim T-0042 --run-tests --strict-claims
@@ -225,6 +234,7 @@ leerness verify-claim T-0042 --run-tests --strict-claims
225
234
  ```
226
235
 
227
236
  ### 시나리오 2: 멀티 AI 에이전트 협업
237
+
228
238
  ```bash
229
239
  # 1) 외부 CLI 활성화
230
240
  leerness setup-agents . # 방향키로 claude/codex/gemini 선택
@@ -241,6 +251,7 @@ leerness agents dispatch "파일 생성" --to gemini --write
241
251
  ```
242
252
 
243
253
  ### 시나리오 3: 라운드가 길어지며 drift 감지
254
+
244
255
  ```bash
245
256
  # 며칠 후 새 세션 시작
246
257
  leerness handoff .
@@ -254,6 +265,7 @@ leerness session close .
254
265
  ```
255
266
 
256
267
  ### 시나리오 4: TodoWrite ↔ leerness 동기화
268
+
257
269
  ```bash
258
270
  # Claude Code의 TodoWrite를 JSON으로 export 후
259
271
  leerness task sync --from /path/to/todos.json
@@ -291,22 +303,23 @@ AGENTS.md · CLAUDE.md
291
303
 
292
304
  ## 환경변수
293
305
 
294
- | 변수 | 효과 |
295
- |---|---|
296
- | `LEERNESS_OFFLINE=1` | npm 호출 스킵 (오프라인) |
297
- | `LEERNESS_OLLAMA_BASE_URL` | orchestrate opt-in (1.9.22+) |
298
- | `LEERNESS_ENABLE_CLAUDE/CODEX/GEMINI/COPILOT` | 외부 CLI 활성화 (1.9.30+) |
299
- | `LEERNESS_NO_BANNER` | ASCII 배너 스킵 (1.9.32+) |
300
- | `LEERNESS_NO_PROMPT` | readline prompt 비활성 (1.9.32+) |
301
- | `LEERNESS_NO_STALE_CHECK` | npx 옛 버전 경고 끄기 (1.9.33+) |
302
- | `LEERNESS_NO_INTERACTIVE` | 방향키 multi-select 비활성 (1.9.34+) |
303
- | `LEERNESS_NO_DRIFT_CHECK` | drift 자동 경고 끄기 (1.9.37+) |
306
+ | 변수 | 효과 |
307
+ | ----------------------------------------------- | ------------------------------------ |
308
+ | `LEERNESS_OFFLINE=1` | npm 호출 스킵 (오프라인) |
309
+ | `LEERNESS_OLLAMA_BASE_URL` | orchestrate opt-in (1.9.22+) |
310
+ | `LEERNESS_ENABLE_CLAUDE/CODEX/GEMINI/COPILOT` | 외부 CLI 활성화 (1.9.30+) |
311
+ | `LEERNESS_NO_BANNER` | ASCII 배너 스킵 (1.9.32+) |
312
+ | `LEERNESS_NO_PROMPT` | readline prompt 비활성 (1.9.32+) |
313
+ | `LEERNESS_NO_STALE_CHECK` | npx 옛 버전 경고 끄기 (1.9.33+) |
314
+ | `LEERNESS_NO_INTERACTIVE` | 방향키 multi-select 비활성 (1.9.34+) |
315
+ | `LEERNESS_NO_DRIFT_CHECK` | drift 자동 경고 끄기 (1.9.37+) |
304
316
 
305
317
  ---
306
318
 
307
319
  ## Claude Code / Cursor / Copilot 통합
308
320
 
309
321
  설치 시 자동 등록:
322
+
310
323
  - `.claude/commands/{handoff, session-close, audit, lazy-detect, update}.md`
311
324
  - `.claude/skills/leerness.md` (스킬 정의)
312
325
  - `.claude/settings.local.json` (SessionStart hook = `update --check`)
@@ -317,15 +330,15 @@ AGENTS.md · CLAUDE.md
317
330
 
318
331
  ## 자연어 트리거
319
332
 
320
- | 사용자 발화 | 자동 실행 |
321
- |---|---|
322
- | "회고해줘 / 돌아보자" | `leerness retro` |
323
- | "최근 N일 회고" | `leerness retro --days N` |
324
- | "통계 / 누적 지표" | `leerness insights` |
325
- | "X 관련 자료 / X 시작 전 검토" | `leerness brainstorm "X"` |
326
- | "매 X마다 Y를 해줘" | `leerness rule add "Y" --trigger every-X` |
327
- | "외부 CLI 설정" | `leerness setup-agents` |
328
- | "drift 점검 / leerness를 잘 쓰고 있나?" | `leerness drift check` |
333
+ | 사용자 발화 | 자동 실행 |
334
+ | --------------------------------------- | ------------------------------------------- |
335
+ | "회고해줘 / 돌아보자" | `leerness retro` |
336
+ | "최근 N일 회고" | `leerness retro --days N` |
337
+ | "통계 / 누적 지표" | `leerness insights` |
338
+ | "X 관련 자료 / X 시작 전 검토" | `leerness brainstorm "X"` |
339
+ | "매 X마다 Y를 해줘" | `leerness rule add "Y" --trigger every-X` |
340
+ | "외부 CLI 설정" | `leerness setup-agents` |
341
+ | "drift 점검 / leerness를 잘 쓰고 있나?" | `leerness drift check` |
329
342
 
330
343
  `AGENTS.md`에 자동 등록 — AI 에이전트가 자연어를 명령으로 변환.
331
344
 
@@ -334,6 +347,7 @@ AGENTS.md · CLAUDE.md
334
347
  ## 설치 시 함정 주의
335
348
 
336
349
  ### `@latest` 명시 권장
350
+
337
351
  ```bash
338
352
  # ❌ npx 캐시로 옛 버전 실행 가능
339
353
  npx leerness init .
@@ -380,6 +394,8 @@ npm test # = node ./scripts/e2e.js
380
394
 
381
395
  ## 변경 이력 (최근)
382
396
 
397
+ - **1.9.41** — 디스크↔AI 컨텍스트 인지 갭 차단: `leerness whats-new` 명령 (CHANGELOG 자동 차분 추출) · `migrate` 후 stdout에 AI must re-read 차분 자동 출력 · `migration-report.md`에 신규 명령/파일 영구 기록 · `handoff`가 fresh migrate(24h 내) 시 자동 알림.
398
+ - **1.9.40** — dogfooding gap 차단: `leerness release pack` 통합 명령 (라운드 마감 자동화 — npm pack + parent migrate + task add + close + readme sync) · `audit`에 README ↔ package.json version mismatch 자동 감지 + `--fix`로 자동 갱신.
383
399
  - **1.9.39** — AI 하네스 엔지니어링 6단계 워크플로 자동 유도 (`session-workflow.md` + handoff 끝 가이드 + AGENTS/CLAUDE 인스트럭션 통합) · `drift check --auto-fix` · `handoff --auto-recover` (critical 시 session close 자동 실행).
384
400
  - **1.9.38** — drift 자동 reminder (`agent-reminders.md`) · `usage stats` 명령 · `task sync --from <todo.json>` · drift 임계 학습 (skip ≥5 → 임계 완화).
385
401
  - **1.9.37** — `leerness drift check` (4 신호 + 4단계 레벨) — 라운드 길어지며 메인이 leerness 잊는 현상 자동 감지.
@@ -412,3 +428,34 @@ MIT — © leerness contributors
412
428
 
413
429
  > **AI 에이전트가 신뢰할 수 있게 일하도록 만드는 도구.**
414
430
  > 사용자 동의 없이 외부 LLM/API/CLI를 자동 호출하지 않습니다.
431
+
432
+ <!-- leerness:project-readme:start -->
433
+ ## Leerness Project Harness
434
+
435
+ 이 프로젝트는 Leerness v1.9.40 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
436
+
437
+ ### Core Commands
438
+
439
+ ```bash
440
+ leerness handoff . # 세션 시작 컨텍스트 자동 로드
441
+ leerness status . # 설치 상태
442
+ leerness verify . # 필수 파일 검증
443
+ leerness audit . # 일관성·계획-진행 정렬 감사
444
+ leerness scan secrets . # 시크릿 패턴 스캔
445
+ leerness encoding check . # UTF-8 / BOM / CRLF 검사
446
+ leerness lazy detect . # 게으름 방지 자동 평가
447
+ leerness memory search "키" # 결정/이력 검색
448
+ leerness session close . # 세션 종료 + handoff 자동 작성
449
+ leerness update . # 자동 버전 감지 + 마이그레이션
450
+ ```
451
+
452
+ ### Planning Files
453
+
454
+ - `.harness/plan.md`: 전체 목표, milestone, 제외/드랍 범위
455
+ - `.harness/progress-tracker.md`: 요청 단위 상태와 증거
456
+ - `.harness/current-state.md`: 지금 이어서 할 작업
457
+ - `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
458
+
459
+ Last synced by Leerness v1.9.40: 2026-05-19
460
+ <!-- leerness:project-readme:end -->
461
+
package/bin/harness.js CHANGED
@@ -6,7 +6,7 @@ const path = require('path');
6
6
  const cp = require('child_process');
7
7
  const readline = require('readline');
8
8
 
9
- const VERSION = '1.9.39';
9
+ const VERSION = '1.9.41';
10
10
  const MARK = '<!-- leerness:managed -->';
11
11
  const README_START = '<!-- leerness:project-readme:start -->';
12
12
  const README_END = '<!-- leerness:project-readme:end -->';
@@ -411,16 +411,77 @@ function mergeLinesFile(p, lines) {
411
411
  writeUtf8(p, next);
412
412
  }
413
413
 
414
- function writeMigrationReport(root, backup, actions) {
414
+ function writeMigrationReport(root, backup, actions, opts = {}) {
415
415
  const p = path.join(root, '.harness/migration-report.md');
416
416
  const rows = actions.map(a => `| ${a.file} | ${a.action} |`).join('\n');
417
- writeUtf8(p, `# Leerness Migration Report\n\nVersion: ${VERSION}\nDate: ${now()}\nBackup: ${rel(root, backup.archiveDir)}\n\n## Policy\n\n- Existing harness, skill, and instruction files are backed up before migration.\n- Project memory files are preserved by default.\n- Managed instruction files are merged with previous content instead of being blindly overwritten.\n- .env.example/.gitignore are line-merged only.\n\n## Backed Up Candidates\n\n${backup.candidates.map(x => '- ' + x).join('\n')}\n\n## File Actions\n\n| File | Action |\n|---|---|\n${rows}\n`);
417
+ // 1.9.41: AI must re-read 섹션 migrate가 추가/변경한 파일을 AI 가독 포맷으로 추출
418
+ // fromV가 명시되면 CHANGELOG 차분 포함
419
+ let aiReadBlock = '';
420
+ try {
421
+ const fromV = opts.fromV || (backup && backup.previousVersion) || null;
422
+ if (fromV && fromV !== VERSION) {
423
+ const changelogPath = path.join(__dirname, '..', 'CHANGELOG.md');
424
+ const cl = exists(changelogPath) ? read(changelogPath) : (exists(path.join(root, 'CHANGELOG.md')) ? read(path.join(root, 'CHANGELOG.md')) : '');
425
+ if (cl) {
426
+ const diff = _parseChangelogBetween(cl, fromV, VERSION);
427
+ const allCommands = new Set(), allFlags = new Set(), allFiles = new Set();
428
+ for (const v of diff) {
429
+ v.newCommands.forEach(c => allCommands.add(c));
430
+ v.newFlags.forEach(f => allFlags.add(f));
431
+ v.newFiles.forEach(f => allFiles.add(f));
432
+ }
433
+ if (diff.length) {
434
+ aiReadBlock = `\n## 🤖 AI must re-read (1.9.41 차분 안내)\n\n`;
435
+ aiReadBlock += `이 migrate는 ${fromV} → ${VERSION} 점프입니다. 메인 AI 에이전트는 다음을 인지하고 우선 활용:\n\n`;
436
+ if (allCommands.size) aiReadBlock += `**📌 신규 명령** (이전엔 없던 것):\n${[...allCommands].map(c => `- \`leerness ${c}\``).join('\n')}\n\n`;
437
+ if (allFlags.size) aiReadBlock += `**🚩 신규 플래그**:\n${[...allFlags].map(f => `- \`${f}\``).join('\n')}\n\n`;
438
+ if (allFiles.size) aiReadBlock += `**📄 신규/변경 파일** (반드시 재독):\n${[...allFiles].map(f => `- \`${f}\``).join('\n')}\n\n`;
439
+ aiReadBlock += `**버전별 헤드라인**:\n`;
440
+ for (const v of diff) {
441
+ const firstLine = (v.body.match(/^\*\*([^*]+)\*\*/) || [])[1]
442
+ || (v.body.split('\n').find(l => l.trim() && !l.startsWith('##')) || '').trim().slice(0, 120);
443
+ aiReadBlock += `- ${v.version} — ${firstLine || '(no headline)'}\n`;
444
+ }
445
+ aiReadBlock += `\n**권장 행동**:\n1. 위 신규 명령을 \`--help\`로 확인\n2. \`AGENTS.md\` / \`CLAUDE.md\` / \`.harness/session-workflow.md\` 재독 (다음 \`leerness handoff\` 호출 시 자동 안내)\n3. 이전 청크의 기억 무효 — 새 도구 우선 시도\n4. 상세: \`leerness whats-new --from ${fromV}\`\n`;
446
+ }
447
+ }
448
+ }
449
+ } catch {}
450
+ writeUtf8(p, `# Leerness Migration Report\n\nVersion: ${VERSION}\nDate: ${now()}\nBackup: ${rel(root, backup.archiveDir)}\n${opts.fromV ? `Previous: ${opts.fromV}\n` : ''}${aiReadBlock}\n## Policy\n\n- Existing harness, skill, and instruction files are backed up before migration.\n- Project memory files are preserved by default.\n- Managed instruction files are merged with previous content instead of being blindly overwritten.\n- .env.example/.gitignore are line-merged only.\n\n## Backed Up Candidates\n\n${backup.candidates.map(x => '- ' + x).join('\n')}\n\n## File Actions\n\n| File | Action |\n|---|---|\n${rows}\n`);
418
451
  }
419
452
 
420
453
  function syncReadme(root) {
421
454
  const p = path.join(root, 'README.md');
422
455
  const existing = exists(p) ? read(p) : '';
423
- writeUtf8(p, mergeReadmeSection(existing, managedReadmeBlock(detectProjectName(root))));
456
+ // 1.9.40: 자체 README도 동기화 — version 배지, e2e 카운트, package.json#version 일관성
457
+ let updated = mergeReadmeSection(existing, managedReadmeBlock(detectProjectName(root)));
458
+ try {
459
+ // package.json#version 또는 .harness/HARNESS_VERSION을 참조하여 README 배지 자동 갱신
460
+ const pkgPath = path.join(root, 'package.json');
461
+ let v = null;
462
+ if (exists(pkgPath)) {
463
+ try { v = JSON.parse(read(pkgPath)).version; } catch {}
464
+ }
465
+ if (!v) {
466
+ const hv = path.join(root, '.harness', 'HARNESS_VERSION');
467
+ if (exists(hv)) v = parseHarnessVersion(read(hv)).base;
468
+ }
469
+ if (v && /^\d+\.\d+\.\d+/.test(v)) {
470
+ // version 배지
471
+ updated = updated.replace(/badge\/version-[\d.]+-(green|blue|red)/g, `badge/version-${v}-green`);
472
+ }
473
+ // e2e 배지: scripts/e2e.js의 출력 "E2E result: N/N passed" 추정 (직접 grep)
474
+ const e2ePath = path.join(root, 'scripts', 'e2e.js');
475
+ if (exists(e2ePath)) {
476
+ // total++ 횟수 카운트 — 정확하진 않지만 추세 반영
477
+ const body = read(e2ePath);
478
+ const total = (body.match(/^total\+\+;/gm) || []).length;
479
+ if (total > 0) {
480
+ updated = updated.replace(/badge\/e2e-(\d+)%2F(\d+)-success/g, `badge/e2e-${total}%2F${total}-success`);
481
+ }
482
+ }
483
+ } catch {}
484
+ if (updated !== existing) writeUtf8(p, updated);
424
485
  ok('README.md Leerness section synced');
425
486
  }
426
487
 
@@ -489,6 +550,14 @@ async function resolveInstallOptions(root, opts = {}) {
489
550
 
490
551
  async function install(root, opts = {}) {
491
552
  root = absRoot(root); mkdirp(root);
553
+ // 1.9.41: migrate 직전 이전 버전 캡처 — 차분 안내에 사용
554
+ try {
555
+ const hv = path.join(root, '.harness', 'HARNESS_VERSION');
556
+ if (exists(hv) && !opts._previousVersion) {
557
+ const parsed = parseHarnessVersion(read(hv));
558
+ opts._previousVersion = parsed.base || parsed.plus || null;
559
+ }
560
+ } catch {}
492
561
  // 1.9.32: init 시 ASCII 배너 + 빠른 시작 가이드 (migrate는 quiet)
493
562
  if (!opts.migration && !has('--no-banner')) _banner({ quickStart: !opts.dry });
494
563
  // 1.9.33: npx 캐시로 옛 버전이 실행될 때 경고 (migrate/--no-stale-check 시 스킵)
@@ -560,7 +629,23 @@ async function install(root, opts = {}) {
560
629
  ]);
561
630
  syncReadme(root);
562
631
  installSkills(root, skills);
563
- writeMigrationReport(root, backup, actions);
632
+ // 1.9.41: migrate 시 이전 버전을 미리 캡처해 차분 안내에 사용
633
+ writeMigrationReport(root, backup, actions, { fromV: opts._previousVersion || null });
634
+ // 1.9.41: migrate 후 (= 점프인 경우) 차분 안내를 stdout에 즉시 출력 — AI 컨텍스트에 새 도구 주입
635
+ if (opts.migration && opts._previousVersion && opts._previousVersion !== VERSION) {
636
+ try {
637
+ const reportPath = path.join(root, '.harness', 'migration-report.md');
638
+ if (exists(reportPath)) {
639
+ const rep = read(reportPath);
640
+ const aiBlock = rep.match(/## 🤖 AI must re-read[\s\S]*?(?=\n## )/);
641
+ if (aiBlock) {
642
+ log('');
643
+ log(aiBlock[0].trim());
644
+ log('');
645
+ }
646
+ }
647
+ } catch {}
648
+ }
564
649
  // 1.9.1 P7: 디폴트 M-0001이 plan에 있고 progress에 row가 없으면 자동 추가
565
650
  try {
566
651
  const planText = exists(planPath(root)) ? read(planPath(root)) : '';
@@ -1096,6 +1181,26 @@ function audit(root) {
1096
1181
  }
1097
1182
  else ok('current-state.md fresh');
1098
1183
  }
1184
+ // 1.9.40: README의 version 배지 ↔ package.json#version mismatch 감지 (도구 만드는 자가 자기 도구 stale하는 dogfooding gap 차단)
1185
+ try {
1186
+ const readmePath = path.join(root, 'README.md');
1187
+ const pkgPath = path.join(root, 'package.json');
1188
+ if (exists(readmePath) && exists(pkgPath)) {
1189
+ const readmeText = read(readmePath);
1190
+ const pkg = JSON.parse(read(pkgPath));
1191
+ const m = readmeText.match(/badge\/version-(\d+\.\d+\.\d+)/);
1192
+ if (pkg.version && m && m[1] !== pkg.version) {
1193
+ warnings++;
1194
+ warn(`README.md version badge mismatch: README=${m[1]} vs package.json=${pkg.version} (run: leerness readme sync)`);
1195
+ if (fix) {
1196
+ const updated = readmeText.replace(/badge\/version-[\d.]+-(green|blue|red)/g, `badge/version-${pkg.version}-green`);
1197
+ writeUtf8(readmePath, updated);
1198
+ ok(' ↳ fixed: README.md version 배지 갱신');
1199
+ fixed++;
1200
+ }
1201
+ }
1202
+ }
1203
+ } catch {}
1099
1204
  log(`Audit summary: warnings=${warnings} failures=${failures}${fix ? ` fixed=${fixed}` : ''}`);
1100
1205
  if (failures) process.exitCode = 1;
1101
1206
  }
@@ -1377,6 +1482,29 @@ function handoff(root) {
1377
1482
  const cs = read(currentStatePath(root)).replace(/Updated: \d{4}-\d{2}-\d{2}/, `Updated: ${today()}`);
1378
1483
  writeUtf8(currentStatePath(root), cs);
1379
1484
  }
1485
+ // 1.9.41: 최근 migrate 차분 알림 — migration-report.md가 24h 내면 "AI must re-read" 블록 자동 표시
1486
+ // 같은 채팅 세션의 AI 청크가 이전 버전 마인드셋이어도 새 도구를 즉시 인지하도록.
1487
+ if (!has('--no-workflow-guide') && !has('--compact')) {
1488
+ try {
1489
+ const reportPath = path.join(root, '.harness', 'migration-report.md');
1490
+ if (exists(reportPath)) {
1491
+ const stat = fs.statSync(reportPath);
1492
+ const ageHr = (Date.now() - stat.mtimeMs) / 3600000;
1493
+ if (ageHr < 24) {
1494
+ const rep = read(reportPath);
1495
+ const aiBlock = rep.match(/## 🤖 AI must re-read[\s\S]*?(?=\n## )/);
1496
+ if (aiBlock) {
1497
+ const isTty = process.stdout && process.stdout.isTTY;
1498
+ const yel = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
1499
+ log('');
1500
+ log(yel(`## 🆕 최근 ${ageHr.toFixed(1)}시간 전 migrate 차분 — AI 에이전트는 신규 도구 우선 시도`));
1501
+ log(aiBlock[0].trim());
1502
+ log('');
1503
+ }
1504
+ }
1505
+ }
1506
+ } catch {}
1507
+ }
1380
1508
  // 1.9.39: handoff 출력 끝에 6단계 워크플로 가이드 자동 표시 (메인 에이전트가 매 세션 인지)
1381
1509
  if (!has('--no-workflow-guide') && !has('--compact') && process.env.LEERNESS_NO_WORKFLOW_GUIDE !== '1') {
1382
1510
  const isTty = process.stdout && process.stdout.isTTY;
@@ -4817,6 +4945,79 @@ function deployGhPages(root, sourceFile) {
4817
4945
  }
4818
4946
  }
4819
4947
 
4948
+ // 1.9.40: release pack — 가벼운 통합 명령 (npm pack + self-host migrate + auto task + close + readme sync)
4949
+ // 메타 감사에서 발견한 "라운드 마감 = pack" 패턴을 leerness 워크플로로 흡수.
4950
+ async function releasePackCmd(root) {
4951
+ root = absRoot(root || process.cwd());
4952
+ const dryRun = has('--dry-run');
4953
+ const parentMigrate = has('--parent-migrate');
4954
+ const close = has('--close');
4955
+ const readmeSync = !has('--no-readme-sync');
4956
+ const taskTitle = arg('--task-add', null);
4957
+ log(`# leerness release pack (1.9.40)`);
4958
+ log(`mode: ${dryRun ? 'dry-run' : 'live'} · parent-migrate: ${parentMigrate} · close: ${close} · readme-sync: ${readmeSync}`);
4959
+ log('');
4960
+
4961
+ // 1. README 동기화 (배지/카운트)
4962
+ if (readmeSync) {
4963
+ try { syncReadme(root); ok('readme sync 적용'); } catch (e) { warn('readme sync skip: ' + e.message); }
4964
+ }
4965
+
4966
+ // 2. npm pack
4967
+ if (!dryRun) {
4968
+ const r = cp.spawnSync('npm', ['pack'], { cwd: root, encoding: 'utf8', shell: true });
4969
+ if (r.status !== 0) { fail('npm pack 실패'); log(r.stderr); process.exitCode = 1; return; }
4970
+ const tarMatch = (r.stdout || '').match(/[^\s]+\.tgz/);
4971
+ if (tarMatch) ok(`npm pack → ${tarMatch[0]}`);
4972
+ else ok('npm pack 완료');
4973
+ } else {
4974
+ log(' (dry-run) npm pack 스킵');
4975
+ }
4976
+
4977
+ // 3. 부모 워크스페이스 self-host migrate (dogfooding gap 차단)
4978
+ if (parentMigrate) {
4979
+ const parent = path.resolve(root, '..');
4980
+ if (exists(path.join(parent, '.harness'))) {
4981
+ log(`\n[parent self-host migrate] ${parent}`);
4982
+ if (!dryRun) {
4983
+ try {
4984
+ await install(parent, { force: false, dry: false, migration: true, nonInteractive: true });
4985
+ ok('parent migrate 완료');
4986
+ } catch (e) { warn('parent migrate 실패: ' + e.message); }
4987
+ } else {
4988
+ log(` (dry-run) ${parent} migrate 스킵`);
4989
+ }
4990
+ } else {
4991
+ log(' (parent에 .harness 없음 — migrate 스킵)');
4992
+ }
4993
+ }
4994
+
4995
+ // 4. 자동 task add — 매 release 라운드가 progress-tracker에 흔적 남도록
4996
+ if (taskTitle) {
4997
+ const v = getCurrentVersion(root) || VERSION;
4998
+ const id = nextId(root, 'T');
4999
+ upsertProgress(root, {
5000
+ id,
5001
+ status: 'done',
5002
+ request: taskTitle,
5003
+ evidence: `release pack ${v} · ${new Date().toISOString().slice(0, 10)}`,
5004
+ nextAction: '다음 라운드 후보 검토'
5005
+ });
5006
+ ok(`task added: ${id} · ${taskTitle}`);
5007
+ }
5008
+
5009
+ // 5. session close
5010
+ if (close) {
5011
+ log('\n[session close]');
5012
+ try {
5013
+ const r = sessionClose(root);
5014
+ ok('session close 호출됨');
5015
+ } catch (e) { warn('session close 실패: ' + e.message); }
5016
+ }
5017
+
5018
+ log('\n✅ release pack 완료');
5019
+ }
5020
+
4820
5021
  function releasePublish(root) {
4821
5022
  root = absRoot(root);
4822
5023
  const dryRun = has('--dry-run');
@@ -5577,6 +5778,107 @@ function _bumpUsage(root, cmdName) {
5577
5778
  } catch {}
5578
5779
  }
5579
5780
 
5781
+ // 1.9.41: CHANGELOG.md를 파싱하여 from → to 사이 버전 차분 추출
5782
+ // 반환: [{ version, date, body, newCommands, newFlags, newFiles }]
5783
+ function _parseChangelogBetween(changelogText, fromV, toV) {
5784
+ // ## 1.9.X — YYYY-MM-DD 헤더 사이의 텍스트 추출
5785
+ const sections = [];
5786
+ const re = /^## (\d+\.\d+\.\d+)(?:\s+—\s+(\d{4}-\d{2}-\d{2}))?\s*\n([\s\S]*?)(?=^## \d+\.\d+\.\d+|$)/gm;
5787
+ let m;
5788
+ while ((m = re.exec(changelogText)) !== null) {
5789
+ sections.push({ version: m[1], date: m[2] || null, body: m[3].trim() });
5790
+ }
5791
+ // from < V <= to 만 (fromV 자체는 이미 적용된 버전이므로 제외)
5792
+ const ranged = sections.filter(s => {
5793
+ const cmp = (v1, v2) => {
5794
+ const a = v1.split('.').map(Number), b = v2.split('.').map(Number);
5795
+ for (let i = 0; i < 3; i++) { if (a[i] !== b[i]) return a[i] - b[i]; }
5796
+ return 0;
5797
+ };
5798
+ return cmp(s.version, fromV) > 0 && cmp(s.version, toV) <= 0;
5799
+ });
5800
+ // 각 섹션에서 신규 명령/플래그/파일 추출
5801
+ for (const s of ranged) {
5802
+ s.newCommands = [];
5803
+ s.newFlags = [];
5804
+ s.newFiles = [];
5805
+ // `leerness X [...]` 또는 backtick에 싸인 leerness 명령
5806
+ for (const cm of s.body.matchAll(/`leerness\s+([a-z][\w-]*(?:\s+[a-z][\w-]*)?)/g)) {
5807
+ const cmd = cm[1].trim();
5808
+ if (!s.newCommands.includes(cmd)) s.newCommands.push(cmd);
5809
+ }
5810
+ // `--xxx` 플래그
5811
+ for (const fm of s.body.matchAll(/`(--[a-z][\w-]*)`/g)) {
5812
+ if (!s.newFlags.includes(fm[1])) s.newFlags.push(fm[1]);
5813
+ }
5814
+ // .harness/X.md 같은 신규 파일
5815
+ for (const ff of s.body.matchAll(/`(\.harness\/[\w./-]+\.(?:md|json|jsonl))`/g)) {
5816
+ if (!s.newFiles.includes(ff[1])) s.newFiles.push(ff[1]);
5817
+ }
5818
+ }
5819
+ return ranged;
5820
+ }
5821
+
5822
+ // 1.9.41: leerness whats-new [--from V] — 현재 워크스페이스 버전 → leerness latest 차분
5823
+ function whatsNewCmd(root) {
5824
+ root = absRoot(root || process.cwd());
5825
+ const fromV = arg('--from', null) || (function () {
5826
+ const hv = path.join(root, '.harness', 'HARNESS_VERSION');
5827
+ if (exists(hv)) { try { return parseHarnessVersion(read(hv)).base || parseHarnessVersion(read(hv)).plus; } catch { return null; } }
5828
+ return null;
5829
+ })();
5830
+ const toV = arg('--to', null) || VERSION;
5831
+ if (!fromV) {
5832
+ fail('현재 버전을 파악할 수 없습니다. --from <version> 명시');
5833
+ return process.exit(1);
5834
+ }
5835
+ // CHANGELOG.md — 우선 root, 없으면 leerness-pkg 자체
5836
+ let changelogPath = path.join(root, 'CHANGELOG.md');
5837
+ if (!exists(changelogPath)) changelogPath = path.join(__dirname, '..', 'CHANGELOG.md');
5838
+ if (!exists(changelogPath)) {
5839
+ fail('CHANGELOG.md 없음');
5840
+ return process.exit(1);
5841
+ }
5842
+ const diff = _parseChangelogBetween(read(changelogPath), fromV, toV);
5843
+ if (has('--json')) { log(JSON.stringify({ from: fromV, to: toV, versions: diff }, null, 2)); return; }
5844
+ if (!diff.length) {
5845
+ log(`# leerness whats-new (1.9.41)`);
5846
+ log(`현재 ${fromV} ↔ 대상 ${toV}: 새 항목 없음 (또는 CHANGELOG에 기록 안 됨)`);
5847
+ return;
5848
+ }
5849
+ log(`# leerness whats-new (1.9.41)`);
5850
+ log(`현재 워크스페이스 버전: ${fromV} → 대상: ${toV}`);
5851
+ log(`범위: ${diff.length}개 버전 (${diff[0].version} → ${diff[diff.length - 1].version})`);
5852
+ log('');
5853
+ // AI 가독 요약 — 각 버전당 한 줄 + 신규 명령/플래그/파일
5854
+ log(`## 🆕 신규 명령·플래그·파일 (AI 에이전트는 다음 명령을 우선 시도)`);
5855
+ const allCommands = new Set();
5856
+ const allFlags = new Set();
5857
+ const allFiles = new Set();
5858
+ for (const v of diff) {
5859
+ v.newCommands.forEach(c => allCommands.add(c));
5860
+ v.newFlags.forEach(f => allFlags.add(f));
5861
+ v.newFiles.forEach(f => allFiles.add(f));
5862
+ }
5863
+ if (allCommands.size) log(` 📌 신규 명령: ${[...allCommands].join(', ')}`);
5864
+ if (allFlags.size) log(` 🚩 신규 플래그: ${[...allFlags].join(', ')}`);
5865
+ if (allFiles.size) log(` 📄 신규 파일: ${[...allFiles].join(', ')}`);
5866
+ log('');
5867
+ log(`## 📜 버전별 헤드라인`);
5868
+ for (const v of diff) {
5869
+ // body 첫 줄(또는 strong header) 추출
5870
+ const firstLine = (v.body.match(/^\*\*([^*]+)\*\*/) || [])[1]
5871
+ || (v.body.split('\n').find(l => l.trim() && !l.startsWith('##')) || '').trim().slice(0, 120);
5872
+ log(` • ${v.version}${v.date ? ` (${v.date})` : ''} — ${firstLine || '(no headline)'}`);
5873
+ }
5874
+ log('');
5875
+ log(`## 💡 권장 행동`);
5876
+ log(` 1. 위 신규 명령들을 시도해 보세요 (예: \`leerness <명령> --help\`)`);
5877
+ log(` 2. 신규 파일들을 읽어 보세요 (예: \`cat .harness/session-workflow.md\`)`);
5878
+ log(` 3. AGENTS.md/CLAUDE.md 재독 — migrate가 인스트럭션을 업데이트했을 수 있음`);
5879
+ log(` 4. 상세: \`cat CHANGELOG.md\` 또는 \`leerness whats-new --json\``);
5880
+ }
5881
+
5580
5882
  function usageStatsCmd(root) {
5581
5883
  root = absRoot(root || process.cwd());
5582
5884
  const stats = _readUsageStats(root);
@@ -5842,6 +6144,7 @@ async function main() {
5842
6144
  if (cmd === 'contract' && args[1] === 'verify') return contractVerifyCmd(args[2], args[3]);
5843
6145
  if (cmd === 'drift' && (args[1] === 'check' || !args[1])) return driftCheckCmd(args[2] || arg('--path', process.cwd()));
5844
6146
  if (cmd === 'usage' && (args[1] === 'stats' || !args[1])) return usageStatsCmd(args[2] || arg('--path', process.cwd()));
6147
+ if (cmd === 'whats-new') return whatsNewCmd(args[1] || arg('--path', process.cwd()));
5845
6148
  if (cmd === 'reuse' && args[1] === 'autodetect') return reuseAutodetectCmd(args[2] || arg('--path', process.cwd()));
5846
6149
  if (cmd === 'setup-agents' || cmd === 'setup' && args[1] === 'agents') return await setupAgentsCmd(args[1] && args[1] !== 'agents' ? args[1] : (args[2] || process.cwd()));
5847
6150
  if (cmd === 'session' && args[1] === 'close') { const r = sessionClose(args[2] || process.cwd()); viewworkEmit(args[2] || process.cwd(), { action: 'task', tool: 'session-close', note: 'session close' }); return r; }
@@ -5880,6 +6183,7 @@ async function main() {
5880
6183
  if (cmd === 'release' && args[1] === 'bump') return releaseBump(args[2] || arg('--path', process.cwd()));
5881
6184
  if (cmd === 'release' && args[1] === 'note') return releaseNote(arg('--path', process.cwd()), args.slice(2).filter(x => !x.startsWith('-')).join(' '));
5882
6185
  if (cmd === 'release' && args[1] === 'publish') return releasePublish(args[2] || arg('--path', process.cwd()));
6186
+ if (cmd === 'release' && args[1] === 'pack') return await releasePackCmd(args[2] || arg('--path', process.cwd()));
5883
6187
  if (cmd === 'impact') return impactCmd(arg('--path', process.cwd()), args[1]);
5884
6188
  if (cmd === 'reuse' && args[1] === 'find') return reuseFind(arg('--path', process.cwd()), args.slice(2).filter(x => !x.startsWith('-')).join(' '));
5885
6189
  if (cmd === 'reuse' && args[1] === 'register') return reuseRegister(arg('--path', process.cwd()), args[2]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.39",
3
+ "version": "1.9.41",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -950,6 +950,126 @@ total++;
950
950
  if (!ok) { failed++; console.log(r.stdout.slice(0, 800)); }
951
951
  }
952
952
 
953
+ // 1.9.41 회귀: whats-new 명령 + migrate 차분 AI must re-read + handoff fresh 알림
954
+ total++;
955
+ {
956
+ // whats-new --from 큰 점프 → 신규 명령 추출
957
+ const r = cp.spawnSync(process.execPath, [CLI, 'whats-new', '--from', '1.9.33', '--json'], { encoding: 'utf8', timeout: 15000 });
958
+ let parsed = null;
959
+ try { parsed = JSON.parse(r.stdout); } catch {}
960
+ const ok = parsed
961
+ && parsed.from === '1.9.33'
962
+ && Array.isArray(parsed.versions)
963
+ && parsed.versions.length >= 5
964
+ && parsed.versions.some(v => v.newCommands && v.newCommands.length > 0);
965
+ console.log(ok ? '✓ B(1.9.41) whats-new --from 1.9.33: 5+ 버전 차분 + 신규 명령 추출' : `✗ whats-new 실패`);
966
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
967
+ }
968
+
969
+ total++;
970
+ {
971
+ // migrate가 fromV가 있는 경우 AI must re-read 블록을 stdout에 출력
972
+ const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-mig-'));
973
+ // 1.9.30 표시로 init한 척 (HARNESS_VERSION 직접 작성)
974
+ fs.mkdirSync(path.join(tmpC, '.harness'), { recursive: true });
975
+ fs.writeFileSync(path.join(tmpC, '.harness', 'HARNESS_VERSION'), 'leerness@1.9.36\n', 'utf8');
976
+ const r = cp.spawnSync(process.execPath, [CLI, 'migrate', tmpC, '--yes', '--no-banner', '--no-stale-check'], { encoding: 'utf8', timeout: 60000 });
977
+ const ok = r.status === 0
978
+ && /AI must re-read/.test(r.stdout)
979
+ && /1\.9\.36 → 1\.9\.4[01]/.test(r.stdout)
980
+ && /신규 명령/.test(r.stdout);
981
+ console.log(ok ? '✓ B(1.9.41) migrate stdout: AI must re-read 차분 자동 출력' : `✗ migrate 차분 출력 실패`);
982
+ if (!ok) { failed++; console.log(r.stdout.slice(-800)); }
983
+ }
984
+
985
+ total++;
986
+ {
987
+ // migration-report.md에 AI must re-read 섹션 영구 기록
988
+ const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-mig2-'));
989
+ fs.mkdirSync(path.join(tmpC, '.harness'), { recursive: true });
990
+ fs.writeFileSync(path.join(tmpC, '.harness', 'HARNESS_VERSION'), 'leerness@1.9.30\n', 'utf8');
991
+ cp.spawnSync(process.execPath, [CLI, 'migrate', tmpC, '--yes', '--no-banner', '--no-stale-check'], { stdio: 'ignore', timeout: 60000 });
992
+ const reportPath = path.join(tmpC, '.harness', 'migration-report.md');
993
+ const body = fs.existsSync(reportPath) ? fs.readFileSync(reportPath, 'utf8') : '';
994
+ const ok = /## 🤖 AI must re-read/.test(body) && /Previous: 1\.9\.30/.test(body);
995
+ console.log(ok ? '✓ B(1.9.41) migration-report.md: AI must re-read 섹션 + Previous 버전 기록' : `✗ report 기록 실패`);
996
+ if (!ok) { failed++; console.log(body.slice(0, 600)); }
997
+ }
998
+
999
+ total++;
1000
+ {
1001
+ // handoff가 fresh migration-report (24h 내) 시 자동 알림
1002
+ const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-fresh-'));
1003
+ fs.mkdirSync(path.join(tmpC, '.harness'), { recursive: true });
1004
+ fs.writeFileSync(path.join(tmpC, '.harness', 'HARNESS_VERSION'), 'leerness@1.9.30\n', 'utf8');
1005
+ cp.spawnSync(process.execPath, [CLI, 'migrate', tmpC, '--yes', '--no-banner', '--no-stale-check'], { stdio: 'ignore', timeout: 60000 });
1006
+ const r = cp.spawnSync(process.execPath, [CLI, 'handoff', tmpC, '--no-drift-check'], { encoding: 'utf8', timeout: 15000 });
1007
+ const ok = r.status === 0
1008
+ && /최근.*시간 전 migrate 차분|AI must re-read/.test(r.stdout);
1009
+ console.log(ok ? '✓ B(1.9.41) handoff: 최근 migrate 차분 자동 표시 (24h 내)' : `✗ handoff 차분 알림 실패`);
1010
+ if (!ok) { failed++; console.log(r.stdout.slice(-500)); }
1011
+ }
1012
+
1013
+ // 1.9.40 회귀: release pack 통합 명령 + audit README mismatch 감지
1014
+ total++;
1015
+ {
1016
+ // release pack --dry-run --task-add: 자동 task 등록
1017
+ const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-rp-'));
1018
+ // init 후 가벼운 package.json 흉내 (release pack은 npm pack 시도하므로 dry-run으로 우회)
1019
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
1020
+ fs.writeFileSync(path.join(tmpC, 'package.json'), JSON.stringify({ name: 'rp-test', version: '0.0.1' }), 'utf8');
1021
+ const r = cp.spawnSync(process.execPath, [CLI, 'release', 'pack', tmpC, '--dry-run', '--task-add', '1.9.40 e2e 검증', '--no-readme-sync'], { encoding: 'utf8', timeout: 30000 });
1022
+ const ok = r.status === 0
1023
+ && /release pack \(1\.9\.40\)/.test(r.stdout)
1024
+ && /task added/.test(r.stdout)
1025
+ && /dry-run/.test(r.stdout);
1026
+ console.log(ok ? '✓ B(1.9.40) release pack: --dry-run + --task-add 자동 등록' : `✗ release pack 실패`);
1027
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
1028
+ }
1029
+
1030
+ total++;
1031
+ {
1032
+ // release pack --parent-migrate (인공 parent .harness 생성)
1033
+ const tmpParent = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-rp-parent-'));
1034
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpParent, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
1035
+ const tmpChild = path.join(tmpParent, 'child');
1036
+ fs.mkdirSync(tmpChild, { recursive: true });
1037
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpChild, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
1038
+ fs.writeFileSync(path.join(tmpChild, 'package.json'), JSON.stringify({ name: 'rp-child', version: '0.0.1' }), 'utf8');
1039
+ const r = cp.spawnSync(process.execPath, [CLI, 'release', 'pack', tmpChild, '--dry-run', '--parent-migrate', '--no-readme-sync'], { encoding: 'utf8', timeout: 30000 });
1040
+ const ok = r.status === 0 && /parent self-host migrate/.test(r.stdout);
1041
+ console.log(ok ? '✓ B(1.9.40) release pack --parent-migrate: 부모 .harness 자동 감지' : `✗ parent-migrate 실패`);
1042
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
1043
+ }
1044
+
1045
+ total++;
1046
+ {
1047
+ // audit README version mismatch 감지
1048
+ const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-mm-'));
1049
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
1050
+ // package.json v1.0.0 + README의 version 배지는 v0.5.0 → mismatch
1051
+ fs.writeFileSync(path.join(tmpC, 'package.json'), JSON.stringify({ name: 't', version: '1.0.0' }), 'utf8');
1052
+ fs.writeFileSync(path.join(tmpC, 'README.md'), '# Test\n[![version](https://img.shields.io/badge/version-0.5.0-green)]()\n', 'utf8');
1053
+ const r = cp.spawnSync(process.execPath, [CLI, 'audit', tmpC], { encoding: 'utf8', timeout: 15000 });
1054
+ const ok = r.status === 0 && /version badge mismatch.*0\.5\.0.*1\.0\.0/.test(r.stdout);
1055
+ console.log(ok ? '✓ B(1.9.40) audit: README ↔ package.json version mismatch 감지' : `✗ README mismatch 실패`);
1056
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
1057
+ }
1058
+
1059
+ total++;
1060
+ {
1061
+ // audit --fix가 README 자동 갱신
1062
+ const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-mm2-'));
1063
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
1064
+ fs.writeFileSync(path.join(tmpC, 'package.json'), JSON.stringify({ name: 't', version: '2.0.0' }), 'utf8');
1065
+ fs.writeFileSync(path.join(tmpC, 'README.md'), '# Test\n[![version](https://img.shields.io/badge/version-0.5.0-green)]()\n', 'utf8');
1066
+ cp.spawnSync(process.execPath, [CLI, 'audit', tmpC, '--fix'], { stdio: 'ignore', timeout: 15000 });
1067
+ const after = fs.readFileSync(path.join(tmpC, 'README.md'), 'utf8');
1068
+ const ok = /badge\/version-2\.0\.0-green/.test(after);
1069
+ console.log(ok ? '✓ B(1.9.40) audit --fix: README version 배지 자동 갱신' : `✗ --fix README 실패`);
1070
+ if (!ok) { failed++; console.log(after.slice(0, 300)); }
1071
+ }
1072
+
953
1073
  // 1.9.39 회귀: session workflow 가이드 + auto-fix + auto-recover
954
1074
  total++;
955
1075
  {
@@ -1302,7 +1422,7 @@ total++;
1302
1422
  && /███████╗/.test(r.stdout)
1303
1423
  && /verify · reuse-map/.test(r.stdout)
1304
1424
  && /한국어 우선 AI 개발 하네스/.test(r.stdout)
1305
- && /v1\.9\.3\d/.test(r.stdout);
1425
+ && /v1\.9\.\d+/.test(r.stdout);
1306
1426
  console.log(ok ? '✓ B(1.9.34) 배너 색상 + ASCII + 한국어' : `✗ 배너 색상 실패`);
1307
1427
  if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
1308
1428
  }