leerness 1.9.37 → 1.9.39
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 +74 -0
- package/README.md +246 -340
- package/bin/harness.js +316 -32
- package/package.json +1 -1
- package/scripts/e2e.js +131 -0
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.
|
|
9
|
+
const VERSION = '1.9.39';
|
|
10
10
|
const MARK = '<!-- leerness:managed -->';
|
|
11
11
|
const README_START = '<!-- leerness:project-readme:start -->';
|
|
12
12
|
const README_END = '<!-- leerness:project-readme:end -->';
|
|
@@ -198,8 +198,8 @@ function coreFiles(root, lang = 'ko', selectedSkills = []) {
|
|
|
198
198
|
const project = detectProjectName(root);
|
|
199
199
|
const skillRows = Object.entries(skillCatalog).map(([k, v]) => `| ${k} | ${v.displayNameKo} | ${v.capabilities.join(', ')} | ${v.lastUpdated} | ${v.verification} |`).join('\n');
|
|
200
200
|
return {
|
|
201
|
-
'AGENTS.md': `${MARK}\n# Leerness Agent Instructions\n\n## Mandatory read order (session start)\n1. .harness/context-routing.md\
|
|
202
|
-
'CLAUDE.md': `${MARK}\n# Claude Code Instructions\n\nFollow AGENTS.md. Always run \`leerness handoff .\` at the start and \`leerness session close .\` before ending a session.\n\nProtected files must not be deleted. Read .harness/anti-lazy-work-policy.md before claiming completion.\n\n## 자연어 영구 룰 (1.9.8)\n사용자가 "매 X마다 Y를 해줘" 같은 자연어 룰을 말하면 즉시 \`leerness rule add "Y" --trigger every-X\`로 등록하세요. 등록된 룰은 매 세션 \`handoff\`가 자동 출력하고, \`session close\`가 자동 검증해 보고합니다. 사용자가 "중지" / "그만" / "끄기"를 명시할 때만 \`rule pause/remove\`를 호출합니다.\n\n자세한 매핑은 AGENTS.md의 "자연어 룰 처리" 표를 참고하세요.\n`,
|
|
201
|
+
'AGENTS.md': `${MARK}\n# Leerness Agent Instructions\n\n## ⭐ 매 세션 첫 행동 (1.9.39+)\n**반드시 \`.harness/session-workflow.md\`를 먼저 읽고 6단계 워크플로를 따른다**: 요청분석→계획→분배→sub-agent작업→종합검증→마감. 라운드 길이/복잡도 무관, drift 방지를 위해 모든 작업에 동일 흐름 유지.\n\n## Mandatory read order (session start)\n1. **.harness/session-workflow.md** (1.9.39+ 6단계 워크플로 — 최우선)\n2. .harness/context-routing.md\n3. .harness/session-handoff.md\n4. .harness/current-state.md\n5. .harness/plan.md\n6. .harness/progress-tracker.md\n7. .harness/guideline.md\n8. .harness/protected-files.md\n9. .harness/writeback-policy.md\n10. .harness/anti-lazy-work-policy.md\n11. **.harness/rules.md** (사용자 정의 영구 룰 — 매 세션 반드시 따름)\n\n## Required behavior\n- 작업 시작 시 \`leerness handoff .\`를 실행해 컨텍스트를 적재합니다 (handoff가 active rules를 자동 출력).\n- 작업 분류는 \`leerness route <task-type>\`로 확인합니다 (planning, feature, bugfix, refactor, research, consistency, release, migration, session-start, session-close, harness-maintenance).\n- 보호 파일/관리 섹션을 삭제하지 않습니다. 머지·아카이브·deprecated 표시를 사용합니다.\n- 의미 있는 변경 후 progress-tracker, current-state, task-log, session-handoff를 갱신합니다.\n- 완료 선언 전 \`leerness check .\` 또는 \`leerness lazy detect .\`로 자기검증합니다.\n- 변경 전 secret/encoding 가드: \`leerness scan secrets .\`, \`leerness encoding check .\`.\n- 같은 기능 중복 생성 전 design-system.md, consistency-policy.md, reuse-map.md를 확인합니다.\n- 매 세션 종료 시 \`leerness session close .\`로 9개 카테고리(완료/진행중/미완료/예정/대기/보류/차단/드랍/검증) + **활성 룰 검증 결과**를 보고합니다.\n- 업데이트는 \`leerness update --check\` (감지) → \`leerness update --yes\` (자동 마이그레이션).\n\n## 자연어 회고/통찰/브레인스토밍 (1.9.13)\n사용자가 자연어로 회고/통찰/브레인스토밍을 요청하면 즉시 leerness 명령으로 호출합니다.\n\n| 사용자 발화 (자연어) | 즉시 실행할 명령 |\n|---|---|\n| "회고해줘 / 돌아보자 / 정리해줘" | \`leerness retro\` |\n| "최근 N일 회고" | \`leerness retro --days N\` |\n| "통계 / 누적 지표 / insights" | \`leerness insights\` |\n| "X에 대해 브레인스토밍 / X 관련 자료 / X 시작 전 검토" | \`leerness brainstorm "X"\` |\n\nsession close가 매번 자동으로 한 줄 요약을 출력하고, 5세션마다 자동 깊은 회고를 실행합니다. 사용자가 명시 요청 시 즉시 호출.\n\n## 자연어 룰 처리 (1.9.8)\n사용자가 자연어로 영구 룰을 요청하면 즉시 leerness rule 명령으로 등록합니다.\n\n| 사용자 발화 (자연어) | 즉시 실행할 명령 |\n|---|---|\n| "매 업데이트마다 버전 bump해줘" | \`leerness rule add "버전을 patch로 bump" --trigger every-update\` |\n| "매 커밋마다 패치노트 추가해줘" | \`leerness rule add "패치노트 추가" --trigger every-commit\` |\n| "세션 종료마다 배포해줘" | \`leerness rule add "배포 (release publish)" --trigger session-close\` |\n| "X 룰 중지/그만/끄기" | \`leerness rule pause <ID>\` (해당 룰 ID는 list로 확인) |\n| "X 룰 제거/삭제" | \`leerness rule remove <ID>\` |\n| "모든 룰 중지" | \`leerness rule stop\` |\n| "룰 다시 켜줘" | \`leerness rule resume-all\` 또는 \`leerness rule resume <ID>\` |\n\n룰을 등록한 후 사용자에게 등록 결과(ID + trigger + 설명)를 보고하고, 그 이후 매 세션마다 자동 적용합니다. 사용자가 "중지" 또는 "제거"를 명시적으로 말하기 전까지는 룰을 비활성화하지 않습니다.\n\n## 룰 자동 적용 (1.9.8)\nleerness가 자동 검증 가능한 trigger:\n- **every-update / version bump 키워드 룰**: package.json의 version이 갱신됐는지 검사 (handoff/session close가 baseline 캐시와 비교).\n- **CHANGELOG / 패치노트 키워드 룰**: CHANGELOG.md의 mtime이 갱신됐는지 검사.\n- **test / 테스트 / verify 키워드 룰**: review-evidence.md에 오늘 verify-code 흔적이 있는지 검사.\n- **배포 / publish / push 키워드 룰**: 자동 검증 불가 → 사용자에게 release publish 명령을 안내.\n\n자동 검증 가능한 룰의 실행은 \`leerness release bump\`, \`leerness release note "..."\`, \`leerness release publish\`를 사용해 자동화합니다.\n`,
|
|
202
|
+
'CLAUDE.md': `${MARK}\n# Claude Code Instructions\n\nFollow AGENTS.md. Always run \`leerness handoff .\` at the start and \`leerness session close .\` before ending a session.\n\n**⭐ 매 세션 첫 행동 (1.9.39+)**: \`.harness/session-workflow.md\`의 6단계 워크플로(요청분석→계획→분배→sub-agent→종합검증→마감)를 따라야 함. drift critical 시 \`leerness drift check --auto-fix\`로 자동 회복.\n\nProtected files must not be deleted. Read .harness/anti-lazy-work-policy.md before claiming completion.\n\n## 자연어 영구 룰 (1.9.8)\n사용자가 "매 X마다 Y를 해줘" 같은 자연어 룰을 말하면 즉시 \`leerness rule add "Y" --trigger every-X\`로 등록하세요. 등록된 룰은 매 세션 \`handoff\`가 자동 출력하고, \`session close\`가 자동 검증해 보고합니다. 사용자가 "중지" / "그만" / "끄기"를 명시할 때만 \`rule pause/remove\`를 호출합니다.\n\n자세한 매핑은 AGENTS.md의 "자연어 룰 처리" 표를 참고하세요.\n`,
|
|
203
203
|
'.cursor/rules/leerness.mdc': `${MARK}\n---\nalwaysApply: true\n---\nFollow AGENTS.md and .harness/context-routing.md.\nRun: \`leerness handoff .\` at session start.\nRun: \`leerness session close .\` at session end.\nPreserve Leerness protected files.\n`,
|
|
204
204
|
'.github/copilot-instructions.md': `${MARK}\n# Copilot Instructions\n\nUse AGENTS.md and .harness/ as project memory.\nDo not remove protected Leerness files.\nBefore completion, ensure plan.md, progress-tracker.md, current-state.md, session-handoff.md are updated.\n`,
|
|
205
205
|
'.harness/HARNESS_VERSION': VERSION + '\n',
|
|
@@ -229,6 +229,81 @@ function coreFiles(root, lang = 'ko', selectedSkills = []) {
|
|
|
229
229
|
'.harness/review-checklist.md': fm('review-checklist', ['PR/리뷰 전'], ['리뷰 기준 변경'], `# Review Checklist\n\n- [ ] 계획과 정렬되어 있는가\n- [ ] progress-tracker가 갱신되었는가\n- [ ] 보호 파일을 삭제하지 않았는가\n- [ ] 디자인/기능 재사용을 확인했는가\n- [ ] 시크릿이 코드에 들어가지 않았는가 (\`leerness scan secrets\`)\n- [ ] 한글 인코딩 OK (\`leerness encoding check\`)\n- [ ] 게으름 평가 통과 (\`leerness lazy detect\`)\n`),
|
|
230
230
|
'.harness/release-checklist.md': fm('release-checklist', ['배포 전'], ['배포 조건/환경변수/롤백 변경'], `# Release Checklist\n\n- [ ] \`leerness verify .\`\n- [ ] \`leerness audit .\`\n- [ ] \`leerness scan secrets .\`\n- [ ] \`leerness encoding check .\`\n- [ ] 프로젝트 typecheck/lint/test\n- [ ] 환경변수 (.env.example) 동기화\n- [ ] 롤백 방법 확인\n- [ ] CHANGELOG 갱신\n`),
|
|
231
231
|
'.harness/session-close-policy.md': fm('session-close-policy', ['세션 종료 전'], ['세션 종료 형식 변경'], `# Session Close Policy\n\nEvery session must list:\n- Completed\n- In progress\n- Incomplete\n- Planned\n- Waiting\n- On hold\n- Blocked\n- Dropped\n- Verification (commands run, results)\n- Recommended next direction\n- Next exact step\n\n\`leerness session close\`가 위 9개 카테고리를 자동 추출하고, session-handoff.md에 다음 세션을 위한 인수인계 블록을 자동 작성합니다.\n`),
|
|
232
|
+
'.harness/session-workflow.md': fm('session-workflow', ['세션 시작','새 사용자 요청 도착','복잡한 작업 분배 전'], ['워크플로 단계 변경'], `# Session Workflow — AI 하네스 엔지니어링 6단계
|
|
233
|
+
|
|
234
|
+
> **매 세션 시작 시 메인 에이전트는 이 문서를 먼저 읽고 6단계를 그대로 따른다.**
|
|
235
|
+
> 라운드 길이/복잡도 무관, 단순 작업도 동일 흐름 유지 — 그래야 drift 안 됨.
|
|
236
|
+
|
|
237
|
+
## Step 1. 요청 분석 + 환경 확인
|
|
238
|
+
\`\`\`bash
|
|
239
|
+
leerness handoff . # 컨텍스트 적재 + drift 자동 경고
|
|
240
|
+
leerness drift check . # 4 신호 + 4단계 레벨
|
|
241
|
+
\`\`\`
|
|
242
|
+
- 사용자 요청을 5W1H로 분해. 모호하면 명확화 질문 (autonomous 모드 제외).
|
|
243
|
+
- drift critical 시 \`leerness session close .\` 또는 \`drift check --auto-fix\` 우선 실행.
|
|
244
|
+
|
|
245
|
+
## Step 2. 계획 수립
|
|
246
|
+
- 작업이 3 step 이상 → TodoWrite 또는 \`leerness plan add\` 사용.
|
|
247
|
+
- 신규 capability → \`leerness reuse-map\` / \`reuse find <query>\`로 기존 자원 우선 검색.
|
|
248
|
+
- 다중 모듈 → 통합 사양 사전 정의 (예: TICK_SPEC.md).
|
|
249
|
+
|
|
250
|
+
## Step 3. 업무 분배 — sub-agent 매핑
|
|
251
|
+
\`\`\`bash
|
|
252
|
+
leerness agents list # ready CLI 확인
|
|
253
|
+
leerness agents quota # 한도 확인
|
|
254
|
+
leerness agents dispatch "<task>" --to <id> # 작업 유형 추천 자동
|
|
255
|
+
\`\`\`
|
|
256
|
+
- 작업 유형별 최적 sub-agent:
|
|
257
|
+
- 텍스트/번역/분석 → claude (1.7× 빠름)
|
|
258
|
+
- 깊은 코드 추론 → codex (가장 상세)
|
|
259
|
+
- 파일 직접 수정 → gemini --yolo (정확)
|
|
260
|
+
- 보안 리뷰 → \`leerness review --persona security\`
|
|
261
|
+
- **충돌 방지 규칙 (필수)**:
|
|
262
|
+
- 각 sub-agent에 *자신만 수정할 파일 경로* 명시
|
|
263
|
+
- mtime 검증 결과 보고 의무화 (동시 쓰기는 last-writer-wins 위험)
|
|
264
|
+
- 사양 사전 정의 → \`leerness contract verify\`로 사후 검증
|
|
265
|
+
|
|
266
|
+
## Step 4. sub-agent 작업 + 개별 자체 검증
|
|
267
|
+
- 각 sub-agent가 자기 모듈 자체 테스트 통과 후 보고.
|
|
268
|
+
- 보고 형식: 라인 수, 테스트 N/N PASS, 발견 이슈, mtime 검증 결과.
|
|
269
|
+
|
|
270
|
+
## Step 5. 종합 검증
|
|
271
|
+
\`\`\`bash
|
|
272
|
+
leerness contract verify SPEC.md src/<mod>.js # 명세 ↔ 구현 일치
|
|
273
|
+
leerness verify-claim T-XXX --run-tests --strict-claims
|
|
274
|
+
leerness review <file> --persona security,performance,ux
|
|
275
|
+
\`\`\`
|
|
276
|
+
- 메인이 직접 통합 시나리오 작성 + 실행 (independent 검증).
|
|
277
|
+
- Sub-agent 검수 vs 메인 검수 결과 *교차 일치* 확인.
|
|
278
|
+
|
|
279
|
+
## Step 6. 세션 마감 + 인계
|
|
280
|
+
\`\`\`bash
|
|
281
|
+
leerness session close . # handoff/current-state/task-log 자동 갱신
|
|
282
|
+
leerness audit . --fix # 누락 메타 자동 보강
|
|
283
|
+
leerness usage stats . # 이번 세션 명령 카운트 확인
|
|
284
|
+
\`\`\`
|
|
285
|
+
- session close가 누락되면 다음 세션 시작 시 drift critical 발생.
|
|
286
|
+
- 자동 회복 옵션: \`drift check --auto-fix\` (critical 시 session close 자동 실행).
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## 빠른 체크리스트
|
|
291
|
+
|
|
292
|
+
세션 끝나기 전 다음이 모두 ✓이어야 한다:
|
|
293
|
+
- [ ] plan/progress-tracker에 이번 라운드 task 등록됨 (또는 task sync)
|
|
294
|
+
- [ ] 모든 done 항목에 evidence 첨부됨 (verify-claim PASS)
|
|
295
|
+
- [ ] sub-agent 사용 시 contract verify PASS
|
|
296
|
+
- [ ] drift 점수 ≤ 30 (attention 이하)
|
|
297
|
+
- [ ] session close 호출됨
|
|
298
|
+
|
|
299
|
+
## Anti-pattern (drift 신호)
|
|
300
|
+
|
|
301
|
+
- ⚠ "작업 끝났으니 보고만 하고 끝" → session close 누락 → 다음 세션 drift critical
|
|
302
|
+
- ⚠ "TodoWrite만 갱신하고 leerness 안 씀" → \`task sync --from\` 또는 \`task add\` 필수
|
|
303
|
+
- ⚠ sub-agent 분배 시 파일 경로 미명시 → 동시 쓰기 충돌
|
|
304
|
+
- ⚠ "테스트 돌렸으니 PASS" 자기 보고만 → verify-claim --run-tests 미실행
|
|
305
|
+
- ⚠ contract verify 생략 → 사양 불일치 BUG가 사용자에게 노출
|
|
306
|
+
`),
|
|
232
307
|
'.harness/anti-lazy-work-policy.md': fm('anti-lazy-work-policy', ['완료 선언 전'], ['게으른 작업 방지 기준 변경'], `# Anti Lazy Work Policy\n\n## Rules\n1. **증거 없는 완료 금지**: \"완료\"를 선언하려면 progress-tracker의 evidence 컬럼에 명령 출력/테스트 결과/스크린샷 경로 등이 있어야 합니다.\n2. **빈 핸드오프 금지**: 세션 종료 시 session-handoff.md의 Completed/In Progress/Next Exact Step이 모두 비어 있으면 close가 \"insufficient\" 상태로 표시됩니다.\n3. **부분 구현 자기보고**: 완전 구현이 아니면 status를 \`incomplete\`로, Next Exact Step에 \"무엇을 추가해야 끝나는지\" 한 줄을 적습니다.\n4. **검증 기록**: typecheck/lint/test 결과를 review-evidence.md에 누적 기록합니다.\n5. **TODO 표지**: 코드에 \`TODO\`/\`FIXME\`/\`XXX\`를 새로 도입하면 progress-tracker에 동일 ID로 추적합니다.\n6. **거짓 완료 자동 감지**: \`leerness lazy detect\`는 다음을 자동 점검합니다.\n - progress-tracker에 done인데 evidence가 비어있는 row\n - session-handoff의 Completed가 비어있고 Next Exact Step도 비어있음\n - 코드에 새 TODO/FIXME 추가 + progress-tracker에 추적 항목 없음\n - test 명령 실행 흔적 없음 (review-evidence.md 또는 task-log.md에 명령 기록)\n`),
|
|
233
308
|
'.harness/rules.md': _rulesHeader() + '\n',
|
|
234
309
|
'.harness/session-handoff.md': fm('session-handoff', ['세션 시작','다음 작업 이어받기'], ['세션 종료'], `# Session Handoff\n\nLast generated: (자동)\n\n## Completed\n-\n\n## In Progress\n-\n\n## Incomplete / Waiting / On Hold / Blocked\n-\n\n## Dropped\n-\n\n## Verification\n-\n\n## Recommended Direction\n-\n\n## Next Exact Step\n-\n`),
|
|
@@ -1302,6 +1377,24 @@ function handoff(root) {
|
|
|
1302
1377
|
const cs = read(currentStatePath(root)).replace(/Updated: \d{4}-\d{2}-\d{2}/, `Updated: ${today()}`);
|
|
1303
1378
|
writeUtf8(currentStatePath(root), cs);
|
|
1304
1379
|
}
|
|
1380
|
+
// 1.9.39: handoff 출력 끝에 6단계 워크플로 가이드 자동 표시 (메인 에이전트가 매 세션 인지)
|
|
1381
|
+
if (!has('--no-workflow-guide') && !has('--compact') && process.env.LEERNESS_NO_WORKFLOW_GUIDE !== '1') {
|
|
1382
|
+
const isTty = process.stdout && process.stdout.isTTY;
|
|
1383
|
+
const cy = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
|
|
1384
|
+
const b = s => isTty ? `\x1b[1m${s}\x1b[0m` : s;
|
|
1385
|
+
const d = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
1386
|
+
log('');
|
|
1387
|
+
log(cy('## 🛠 세션 워크플로 6단계 (1.9.39+, AI 하네스 엔지니어링)'));
|
|
1388
|
+
log(d(' 상세: ') + cy('.harness/session-workflow.md'));
|
|
1389
|
+
log(` 1. ${b('요청 분석')} handoff(이미 완료) · drift check · 모호하면 명확화`);
|
|
1390
|
+
log(` 2. ${b('계획 수립')} plan add / TodoWrite · reuse-map으로 기존 자원 우선`);
|
|
1391
|
+
log(` 3. ${b('업무 분배')} agents list/recommend · 작업유형별 sub-agent 매핑`);
|
|
1392
|
+
log(` 4. ${b('sub-agent 작업')} 파일 경로 격리 · mtime 검증 의무 · 자체 테스트`);
|
|
1393
|
+
log(` 5. ${b('종합 검증')} contract verify · verify-claim --run-tests · review --persona`);
|
|
1394
|
+
log(` 6. ${b('세션 마감')} session close · audit --fix · usage stats`);
|
|
1395
|
+
log(d(' 끄려면: --no-workflow-guide 또는 LEERNESS_NO_WORKFLOW_GUIDE=1'));
|
|
1396
|
+
log('');
|
|
1397
|
+
}
|
|
1305
1398
|
ok('handoff loaded; current-state updated');
|
|
1306
1399
|
}
|
|
1307
1400
|
|
|
@@ -1433,36 +1526,94 @@ function handoffCmd(root) {
|
|
|
1433
1526
|
return _handoffWorkspace(absRoot(root));
|
|
1434
1527
|
}
|
|
1435
1528
|
// 1.9.37: drift 자동 경고 (메인 에이전트가 leerness를 점점 안 쓰는 현상 감지)
|
|
1529
|
+
// 1.9.38 (A): drift 임계 시 .harness/agent-reminders.md 자동 생성 — 메인 에이전트 프롬프트에 표시되도록.
|
|
1530
|
+
// 1.9.38 (D): skip 횟수 학습 — --no-drift-check 빈도 ≥5 시 임계 완화 (1d → 2d).
|
|
1436
1531
|
const absR0 = absRoot(root || process.cwd());
|
|
1437
|
-
if (exists(path.join(absR0, '.harness')) &&
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1532
|
+
if (exists(path.join(absR0, '.harness')) && process.env.LEERNESS_NO_DRIFT_CHECK !== '1') {
|
|
1533
|
+
// skip 카운트
|
|
1534
|
+
if (has('--no-drift-check')) {
|
|
1535
|
+
try {
|
|
1536
|
+
const stats = _readUsageStats(absR0);
|
|
1537
|
+
stats.drift = stats.drift || {};
|
|
1538
|
+
stats.drift.skipped = (stats.drift.skipped || 0) + 1;
|
|
1539
|
+
const p = _usageStatsPath(absR0);
|
|
1540
|
+
mkdirp(path.dirname(p));
|
|
1541
|
+
writeUtf8(p, JSON.stringify(stats, null, 2) + '\n');
|
|
1542
|
+
} catch {}
|
|
1543
|
+
} else {
|
|
1544
|
+
try {
|
|
1545
|
+
const isTty = process.stdout && process.stdout.isTTY;
|
|
1546
|
+
const yel = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
|
|
1547
|
+
const red = s => isTty ? `\x1b[31m${s}\x1b[0m` : s;
|
|
1548
|
+
const dim = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
1549
|
+
// 1.9.38 (D): 학습된 임계 (skip 빈도 높으면 임계 완화)
|
|
1550
|
+
const stats = _readUsageStats(absR0);
|
|
1551
|
+
const skipCount = (stats.drift && stats.drift.skipped) || 0;
|
|
1552
|
+
const threshold = skipCount >= 5 ? 4 : 2; // 5회 이상 끄면 2일 → 4일로 완화
|
|
1553
|
+
// 간이 drift 계산
|
|
1554
|
+
const now = Date.now();
|
|
1555
|
+
const shPath = handoffPath(absR0);
|
|
1556
|
+
let shAge = null;
|
|
1557
|
+
if (exists(shPath)) {
|
|
1558
|
+
const m = read(shPath).match(/Last generated:\s*([\d\-T:.Z]+)/);
|
|
1559
|
+
if (m) shAge = (now - new Date(m[1]).getTime()) / 86400000;
|
|
1560
|
+
}
|
|
1561
|
+
const rows = readProgressRows(absR0);
|
|
1562
|
+
let ptAge = null;
|
|
1563
|
+
if (rows.length) {
|
|
1564
|
+
const dates = rows.map(r => (r.updated || '').match(/\d{4}-\d{2}-\d{2}/)).filter(Boolean).map(m => m[0]).sort();
|
|
1565
|
+
if (dates.length) ptAge = (now - new Date(dates[dates.length - 1]).getTime()) / 86400000;
|
|
1566
|
+
}
|
|
1567
|
+
const sevStale = (shAge !== null && shAge > 3) || (ptAge !== null && ptAge > 3);
|
|
1568
|
+
if ((shAge !== null && shAge > threshold) || (ptAge !== null && ptAge > threshold)) {
|
|
1569
|
+
log('');
|
|
1570
|
+
log(yel(' ⚠ leerness drift 감지 — 메타파일이 stale합니다'));
|
|
1571
|
+
if (shAge !== null && shAge > threshold) log(dim(` session-handoff.md: ${shAge.toFixed(1)}일 stale`));
|
|
1572
|
+
if (ptAge !== null && ptAge > threshold) log(dim(` progress-tracker: ${ptAge.toFixed(1)}일 stale`));
|
|
1573
|
+
log(dim(` → 권장: ${red('leerness session close .')} 또는 ${red('leerness drift check .')} 로 상세 보기`));
|
|
1574
|
+
if (skipCount >= 5) log(dim(` (학습: skip ${skipCount}회 누적 → 임계 ${threshold}일로 완화)`));
|
|
1575
|
+
// 1.9.39: --auto-recover — drift 감지 시 inline 자동 회복
|
|
1576
|
+
if (has('--auto-recover') && sevStale) {
|
|
1577
|
+
log(dim(` 🔧 --auto-recover 활성 — session close 자동 실행 중...`));
|
|
1578
|
+
try {
|
|
1579
|
+
const r = cp.spawnSync(process.execPath, [__filename, 'session', 'close', absR0], { encoding: 'utf8', timeout: 60000 });
|
|
1580
|
+
if (r.status === 0) {
|
|
1581
|
+
log(dim(` ✓ session close 자동 완료 (다음 라운드부터 healthy)`));
|
|
1582
|
+
const s2 = _readUsageStats(absR0);
|
|
1583
|
+
s2.drift = s2.drift || {};
|
|
1584
|
+
s2.drift.autoResolved = (s2.drift.autoResolved || 0) + 1;
|
|
1585
|
+
writeUtf8(_usageStatsPath(absR0), JSON.stringify(s2, null, 2) + '\n');
|
|
1586
|
+
} else {
|
|
1587
|
+
log(dim(` ⚠ auto-recover 실패 (exit ${r.status})`));
|
|
1588
|
+
}
|
|
1589
|
+
} catch (e) {
|
|
1590
|
+
log(dim(` ⚠ auto-recover 오류: ${e.message}`));
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
log('');
|
|
1594
|
+
// 1.9.38 (A): critical 시 .harness/agent-reminders.md 자동 생성 — 다음 세션 시작 시 메인 에이전트가 읽도록.
|
|
1595
|
+
if (sevStale) {
|
|
1596
|
+
try {
|
|
1597
|
+
const remPath = path.join(absR0, '.harness', 'agent-reminders.md');
|
|
1598
|
+
const body = `<!-- leerness:managed:auto -->\n# 🔔 메인 에이전트용 자동 reminder\n\n_생성: ${new Date().toISOString()}_\n\n## drift critical 감지\n현재 워크스페이스의 메타파일이 매우 stale합니다. 이번 라운드 작업 끝에 반드시 다음 명령을 호출하세요:\n\n\`\`\`bash\nleerness session close .\n\`\`\`\n\n또는 상세 점검:\n\`\`\`bash\nleerness drift check .\n\`\`\`\n\nstale 신호:\n${shAge !== null ? `- session-handoff.md: ${shAge.toFixed(1)}일 stale\n` : ''}${ptAge !== null ? `- progress-tracker: ${ptAge.toFixed(1)}일 stale\n` : ''}\n\n_이 파일은 leerness 1.9.38+가 자동 갱신합니다. session close 후 자동 삭제.\n_사용자가 이 파일을 보고 메인 에이전트에 reminder 전달 가능._\n`;
|
|
1599
|
+
writeUtf8(remPath, body);
|
|
1600
|
+
} catch {}
|
|
1601
|
+
} else {
|
|
1602
|
+
// attention 등급으로 회복했으면 reminder 파일 삭제
|
|
1603
|
+
try {
|
|
1604
|
+
const remPath = path.join(absR0, '.harness', 'agent-reminders.md');
|
|
1605
|
+
if (exists(remPath)) fs.unlinkSync(remPath);
|
|
1606
|
+
} catch {}
|
|
1607
|
+
}
|
|
1608
|
+
} else {
|
|
1609
|
+
// healthy → reminder 파일 자동 청소
|
|
1610
|
+
try {
|
|
1611
|
+
const remPath = path.join(absR0, '.harness', 'agent-reminders.md');
|
|
1612
|
+
if (exists(remPath)) fs.unlinkSync(remPath);
|
|
1613
|
+
} catch {}
|
|
1614
|
+
}
|
|
1615
|
+
} catch {}
|
|
1616
|
+
}
|
|
1466
1617
|
}
|
|
1467
1618
|
// 1.9.35 개선 #1: .harness 부재 시 즉시 경고 (자동 init 권장)
|
|
1468
1619
|
// 사용자가 신규 디렉토리에서 handoff 호출 시 sub-agent 작업이 길을 잃지 않도록.
|
|
@@ -5335,6 +5486,44 @@ function driftCheckCmd(root, opts = {}) {
|
|
|
5335
5486
|
else if (totalScore >= 50) level = '🟡 warning';
|
|
5336
5487
|
else if (totalScore >= 20) level = '🟠 attention';
|
|
5337
5488
|
|
|
5489
|
+
// 1.9.38 (D): drift critical 등급은 누적 카운트 (학습 신호)
|
|
5490
|
+
try {
|
|
5491
|
+
if (level === '🔴 critical') {
|
|
5492
|
+
const stats = _readUsageStats(root);
|
|
5493
|
+
stats.drift = stats.drift || {};
|
|
5494
|
+
stats.drift.criticalSeen = (stats.drift.criticalSeen || 0) + 1;
|
|
5495
|
+
const p = _usageStatsPath(root);
|
|
5496
|
+
mkdirp(path.dirname(p));
|
|
5497
|
+
writeUtf8(p, JSON.stringify(stats, null, 2) + '\n');
|
|
5498
|
+
}
|
|
5499
|
+
} catch {}
|
|
5500
|
+
// 1.9.39: --auto-fix — critical 시 session close 자동 실행
|
|
5501
|
+
const autoFix = has('--auto-fix');
|
|
5502
|
+
if (autoFix && level === '🔴 critical') {
|
|
5503
|
+
log('');
|
|
5504
|
+
log(`🔧 --auto-fix 활성 — session close 자동 실행 중...`);
|
|
5505
|
+
try {
|
|
5506
|
+
const r = cp.spawnSync(process.execPath, [__filename, 'session', 'close', root], { encoding: 'utf8', timeout: 60000 });
|
|
5507
|
+
if (r.status === 0) {
|
|
5508
|
+
log(`✓ session close 자동 완료`);
|
|
5509
|
+
// autoResolved 카운트
|
|
5510
|
+
const stats = _readUsageStats(root);
|
|
5511
|
+
stats.drift = stats.drift || {};
|
|
5512
|
+
stats.drift.autoResolved = (stats.drift.autoResolved || 0) + 1;
|
|
5513
|
+
const p = _usageStatsPath(root);
|
|
5514
|
+
mkdirp(path.dirname(p));
|
|
5515
|
+
writeUtf8(p, JSON.stringify(stats, null, 2) + '\n');
|
|
5516
|
+
// 재검사
|
|
5517
|
+
log('');
|
|
5518
|
+
log(`재검사 중...`);
|
|
5519
|
+
return driftCheckCmd(root); // 재귀 1회 (auto-fix 없이)
|
|
5520
|
+
} else {
|
|
5521
|
+
log(`⚠ session close 실패 (exit ${r.status}) — 수동 실행 필요`);
|
|
5522
|
+
}
|
|
5523
|
+
} catch (e) {
|
|
5524
|
+
log(`⚠ auto-fix 오류: ${e.message}`);
|
|
5525
|
+
}
|
|
5526
|
+
}
|
|
5338
5527
|
if (has('--json')) {
|
|
5339
5528
|
log(JSON.stringify({ root, score: totalScore, level, signals, fired, appsZeroTask }, null, 2));
|
|
5340
5529
|
return;
|
|
@@ -5366,6 +5555,92 @@ function driftCheckCmd(root, opts = {}) {
|
|
|
5366
5555
|
if (level === '🔴 critical') process.exitCode = 1;
|
|
5367
5556
|
}
|
|
5368
5557
|
|
|
5558
|
+
// 1.9.38: 사용 통계 (cumulative count, command별)
|
|
5559
|
+
function _usageStatsPath(root) { return path.join(absRoot(root), '.harness', 'cache', 'usage-stats.json'); }
|
|
5560
|
+
function _readUsageStats(root) {
|
|
5561
|
+
const p = _usageStatsPath(root);
|
|
5562
|
+
if (!exists(p)) return { commands: {}, drift: { criticalSeen: 0, skipped: 0, autoResolved: 0 }, since: today() };
|
|
5563
|
+
try { return JSON.parse(read(p)); } catch { return { commands: {}, drift: {}, since: today() }; }
|
|
5564
|
+
}
|
|
5565
|
+
function _bumpUsage(root, cmdName) {
|
|
5566
|
+
// 가벼운 카운터 — 명령 실행마다 호출 (sync write로 작은 파일)
|
|
5567
|
+
try {
|
|
5568
|
+
const stats = _readUsageStats(root);
|
|
5569
|
+
if (!stats.commands) stats.commands = {};
|
|
5570
|
+
stats.commands[cmdName] = (stats.commands[cmdName] || 0) + 1;
|
|
5571
|
+
stats.lastCommand = cmdName;
|
|
5572
|
+
stats.lastAt = new Date().toISOString();
|
|
5573
|
+
if (!stats.since) stats.since = today();
|
|
5574
|
+
const p = _usageStatsPath(root);
|
|
5575
|
+
mkdirp(path.dirname(p));
|
|
5576
|
+
writeUtf8(p, JSON.stringify(stats, null, 2) + '\n');
|
|
5577
|
+
} catch {}
|
|
5578
|
+
}
|
|
5579
|
+
|
|
5580
|
+
function usageStatsCmd(root) {
|
|
5581
|
+
root = absRoot(root || process.cwd());
|
|
5582
|
+
const stats = _readUsageStats(root);
|
|
5583
|
+
if (has('--json')) { log(JSON.stringify(stats, null, 2)); return; }
|
|
5584
|
+
log(`# leerness usage stats (1.9.38)`);
|
|
5585
|
+
log(`since: ${stats.since || '(unknown)'} · last: ${stats.lastAt || '(none)'}`);
|
|
5586
|
+
log('');
|
|
5587
|
+
const entries = Object.entries(stats.commands || {}).sort((a, b) => b[1] - a[1]);
|
|
5588
|
+
if (!entries.length) {
|
|
5589
|
+
log(' (사용 기록 없음)');
|
|
5590
|
+
return;
|
|
5591
|
+
}
|
|
5592
|
+
log(`| 명령 | 호출 수 |`);
|
|
5593
|
+
log(`|---|---:|`);
|
|
5594
|
+
for (const [cmd, n] of entries.slice(0, 30)) log(`| ${cmd} | ${n} |`);
|
|
5595
|
+
const total = entries.reduce((s, [, n]) => s + n, 0);
|
|
5596
|
+
log('');
|
|
5597
|
+
log(`총 ${total} 회 호출 · 종류 ${entries.length} 가지`);
|
|
5598
|
+
if (stats.drift) {
|
|
5599
|
+
log('');
|
|
5600
|
+
log(`drift 통계: critical 발견 ${stats.drift.criticalSeen || 0} · skip ${stats.drift.skipped || 0} · 자동 해소 ${stats.drift.autoResolved || 0}`);
|
|
5601
|
+
if ((stats.drift.skipped || 0) > 5) {
|
|
5602
|
+
log(`💡 drift 경고 ${stats.drift.skipped}회 스킵 → 1.9.38 학습: 임계 자동 완화 (--no-drift-check 빈도 ≥5)`);
|
|
5603
|
+
}
|
|
5604
|
+
}
|
|
5605
|
+
}
|
|
5606
|
+
|
|
5607
|
+
// 1.9.38: task sync — TodoWrite/외부 JSON에서 leerness task로 mirror
|
|
5608
|
+
function taskSyncCmd(root) {
|
|
5609
|
+
root = absRoot(root || process.cwd());
|
|
5610
|
+
const file = arg('--from', null);
|
|
5611
|
+
if (!file) {
|
|
5612
|
+
fail('사용법: leerness task sync --from <todo.json>\n 파일 형식: [{"content":"...","status":"completed|in_progress|pending","activeForm":"..."}]');
|
|
5613
|
+
return process.exit(1);
|
|
5614
|
+
}
|
|
5615
|
+
const full = path.resolve(file);
|
|
5616
|
+
if (!exists(full)) { fail(`파일 없음: ${full}`); return process.exit(1); }
|
|
5617
|
+
let todos;
|
|
5618
|
+
try { todos = JSON.parse(read(full)); }
|
|
5619
|
+
catch (e) { fail(`JSON 파싱 실패: ${e.message}`); return process.exit(1); }
|
|
5620
|
+
if (!Array.isArray(todos)) { fail('JSON 최상위는 배열이어야 함'); return process.exit(1); }
|
|
5621
|
+
let imported = 0, updated = 0;
|
|
5622
|
+
for (const t of todos) {
|
|
5623
|
+
if (!t || !t.content) continue;
|
|
5624
|
+
const status = t.status === 'completed' ? 'done' : t.status === 'in_progress' ? 'in-progress' : 'planned';
|
|
5625
|
+
// 이미 같은 request 있는지
|
|
5626
|
+
const existing = readProgressRows(root).find(r => r.request === t.content);
|
|
5627
|
+
if (existing) {
|
|
5628
|
+
if (existing.status !== status) {
|
|
5629
|
+
upsertProgress(root, { id: existing.id, status });
|
|
5630
|
+
updated++;
|
|
5631
|
+
}
|
|
5632
|
+
} else {
|
|
5633
|
+
const id = nextId(root, 'T');
|
|
5634
|
+
upsertProgress(root, { id, status, request: t.content, evidence: 'todowrite-sync', nextAction: t.activeForm || '다음 액션' });
|
|
5635
|
+
imported++;
|
|
5636
|
+
}
|
|
5637
|
+
}
|
|
5638
|
+
log(`# leerness task sync (1.9.38)`);
|
|
5639
|
+
log(`from: ${full}`);
|
|
5640
|
+
log(`imported: ${imported} · updated: ${updated} · total in source: ${todos.length}`);
|
|
5641
|
+
if (has('--json')) log(JSON.stringify({ imported, updated, total: todos.length }, null, 2));
|
|
5642
|
+
}
|
|
5643
|
+
|
|
5369
5644
|
// 1.9.35 개선 #3: contract verify <spec.md> <impl.js>
|
|
5370
5645
|
// 사양 문서(spec.md)에 명시된 함수 이름이 실제 module.exports에 모두 있는지 검사.
|
|
5371
5646
|
// 사용 예: leerness contract verify TICK_SPEC.md src/format.js
|
|
@@ -5533,6 +5808,13 @@ async function main() {
|
|
|
5533
5808
|
return log(VERSION);
|
|
5534
5809
|
}
|
|
5535
5810
|
if (has('--help') || has('-h')) return help();
|
|
5811
|
+
// 1.9.38 (B): 사용 통계 카운터 — usage stats 명령 자체와 비차단 경로는 제외
|
|
5812
|
+
if (cmd !== 'usage' && cmd !== 'init' && cmd !== 'migrate' && cmd !== '--version' && cmd !== '--help') {
|
|
5813
|
+
try {
|
|
5814
|
+
const root = absRoot(arg('--path', args[1] && !args[1].startsWith('-') ? args[1] : process.cwd()));
|
|
5815
|
+
if (exists(path.join(root, '.harness'))) _bumpUsage(root, cmd);
|
|
5816
|
+
} catch {}
|
|
5817
|
+
}
|
|
5536
5818
|
if (cmd === 'init') return await install(args[1] || process.cwd(), { force:false, dry:false, migration:false });
|
|
5537
5819
|
if (cmd === 'migrate') return await install(args[1] || process.cwd(), { force:has('--force'), dry:has('--dry-run'), migration:true });
|
|
5538
5820
|
if (cmd === 'update') return await updateCmd(args[1] || process.cwd(), { checkOnly: has('--check'), yes: has('--yes'), force: has('--force') });
|
|
@@ -5559,6 +5841,7 @@ async function main() {
|
|
|
5559
5841
|
if (cmd === 'agents') return agentsCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
|
|
5560
5842
|
if (cmd === 'contract' && args[1] === 'verify') return contractVerifyCmd(args[2], args[3]);
|
|
5561
5843
|
if (cmd === 'drift' && (args[1] === 'check' || !args[1])) return driftCheckCmd(args[2] || arg('--path', process.cwd()));
|
|
5844
|
+
if (cmd === 'usage' && (args[1] === 'stats' || !args[1])) return usageStatsCmd(args[2] || arg('--path', process.cwd()));
|
|
5562
5845
|
if (cmd === 'reuse' && args[1] === 'autodetect') return reuseAutodetectCmd(args[2] || arg('--path', process.cwd()));
|
|
5563
5846
|
if (cmd === 'setup-agents' || cmd === 'setup' && args[1] === 'agents') return await setupAgentsCmd(args[1] && args[1] !== 'agents' ? args[1] : (args[2] || process.cwd()));
|
|
5564
5847
|
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; }
|
|
@@ -5623,6 +5906,7 @@ async function main() {
|
|
|
5623
5906
|
if (sub==='drop') return taskDrop(root, args[2]);
|
|
5624
5907
|
if (sub==='fix-evidence') return taskFixEvidence(root);
|
|
5625
5908
|
if (sub==='relink') return taskRelink(root);
|
|
5909
|
+
if (sub==='sync') return taskSyncCmd(root);
|
|
5626
5910
|
}
|
|
5627
5911
|
return help();
|
|
5628
5912
|
}
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -950,6 +950,137 @@ total++;
|
|
|
950
950
|
if (!ok) { failed++; console.log(r.stdout.slice(0, 800)); }
|
|
951
951
|
}
|
|
952
952
|
|
|
953
|
+
// 1.9.39 회귀: session workflow 가이드 + auto-fix + auto-recover
|
|
954
|
+
total++;
|
|
955
|
+
{
|
|
956
|
+
// handoff 끝에 워크플로 6단계 가이드 자동 표시
|
|
957
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-wf-'));
|
|
958
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
959
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'handoff', tmpC, '--no-drift-check'], { encoding: 'utf8', timeout: 15000 });
|
|
960
|
+
const ok = r.status === 0
|
|
961
|
+
&& /세션 워크플로 6단계/.test(r.stdout)
|
|
962
|
+
&& /1\. 요청 분석/.test(r.stdout)
|
|
963
|
+
&& /6\. 세션 마감/.test(r.stdout);
|
|
964
|
+
console.log(ok ? '✓ B(1.9.39) handoff: 6단계 워크플로 가이드 자동 표시' : `✗ workflow guide 실패`);
|
|
965
|
+
if (!ok) { failed++; console.log(r.stdout.slice(-500)); }
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
total++;
|
|
969
|
+
{
|
|
970
|
+
// session-workflow.md 파일이 init 시 생성
|
|
971
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-wf2-'));
|
|
972
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
973
|
+
const wfFile = path.join(tmpC, '.harness', 'session-workflow.md');
|
|
974
|
+
const ok = fs.existsSync(wfFile)
|
|
975
|
+
&& /6단계/.test(fs.readFileSync(wfFile, 'utf8'))
|
|
976
|
+
&& /sub-agent/.test(fs.readFileSync(wfFile, 'utf8'));
|
|
977
|
+
console.log(ok ? '✓ B(1.9.39) .harness/session-workflow.md init 시 자동 생성' : `✗ workflow 파일 실패`);
|
|
978
|
+
if (!ok) { failed++; if (fs.existsSync(wfFile)) console.log(fs.readFileSync(wfFile, 'utf8').slice(0, 300)); else console.log('파일 없음'); }
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
total++;
|
|
982
|
+
{
|
|
983
|
+
// AGENTS.md / CLAUDE.md에 session-workflow.md 참조 포함
|
|
984
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-wf3-'));
|
|
985
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
986
|
+
const agentsBody = fs.readFileSync(path.join(tmpC, 'AGENTS.md'), 'utf8');
|
|
987
|
+
const claudeBody = fs.readFileSync(path.join(tmpC, 'CLAUDE.md'), 'utf8');
|
|
988
|
+
const ok = /session-workflow\.md/.test(agentsBody) && /session-workflow\.md/.test(claudeBody);
|
|
989
|
+
console.log(ok ? '✓ B(1.9.39) AGENTS/CLAUDE 템플릿에 session-workflow.md 참조' : `✗ 인스트럭션 통합 실패`);
|
|
990
|
+
if (!ok) { failed++; }
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
total++;
|
|
994
|
+
{
|
|
995
|
+
// drift check --auto-fix: critical 시 session close 자동 실행 (시뮬은 어려우니 옵션 인식만)
|
|
996
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-af-'));
|
|
997
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
998
|
+
// --auto-fix 플래그 인식 (healthy 상태에서도 명령 자체는 정상 종료)
|
|
999
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'drift', 'check', tmpC, '--auto-fix'], { encoding: 'utf8', timeout: 30000 });
|
|
1000
|
+
const ok = r.status === 0
|
|
1001
|
+
&& /leerness drift check/.test(r.stdout)
|
|
1002
|
+
&& /(healthy|attention|warning|critical)/.test(r.stdout);
|
|
1003
|
+
console.log(ok ? '✓ B(1.9.39) drift check --auto-fix 옵션 인식 + healthy fallthrough' : `✗ --auto-fix 실패`);
|
|
1004
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// 1.9.38 회귀: usage stats, task sync, drift reminder, drift skip learning
|
|
1008
|
+
total++;
|
|
1009
|
+
{
|
|
1010
|
+
// B. usage stats: 빈 상태 + 호출 후 카운터 증가
|
|
1011
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-usage-'));
|
|
1012
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
1013
|
+
// 카운터 자극: status, handoff 호출
|
|
1014
|
+
cp.spawnSync(process.execPath, [CLI, 'status', tmpC], { stdio: 'ignore', timeout: 10000 });
|
|
1015
|
+
cp.spawnSync(process.execPath, [CLI, 'handoff', tmpC, '--compact', '--no-drift-check'], { stdio: 'ignore', timeout: 10000 });
|
|
1016
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'usage', 'stats', tmpC, '--json'], { encoding: 'utf8', timeout: 10000 });
|
|
1017
|
+
let parsed = null;
|
|
1018
|
+
try { parsed = JSON.parse(r.stdout); } catch {}
|
|
1019
|
+
const ok = parsed
|
|
1020
|
+
&& parsed.commands
|
|
1021
|
+
&& (parsed.commands.status >= 1 || parsed.commands.handoff >= 1)
|
|
1022
|
+
&& parsed.drift
|
|
1023
|
+
&& typeof parsed.drift.skipped === 'number';
|
|
1024
|
+
console.log(ok ? '✓ B(1.9.38) usage stats: 명령 카운터 누적 + drift 통계 구조' : `✗ usage stats 실패`);
|
|
1025
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
total++;
|
|
1029
|
+
{
|
|
1030
|
+
// C. task sync — TodoWrite JSON 임포트
|
|
1031
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-tasksync-'));
|
|
1032
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
1033
|
+
const todoFile = path.join(tmpC, 'todo.json');
|
|
1034
|
+
fs.writeFileSync(todoFile, JSON.stringify([
|
|
1035
|
+
{ content: 'sync 테스트 작업 A', status: 'completed', activeForm: 'syncA' },
|
|
1036
|
+
{ content: 'sync 테스트 작업 B', status: 'in_progress', activeForm: 'syncB' },
|
|
1037
|
+
{ content: 'sync 테스트 작업 C', status: 'pending', activeForm: 'syncC' }
|
|
1038
|
+
]), 'utf8');
|
|
1039
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'task', 'sync', '--from', todoFile, '--path', tmpC, '--json'], { encoding: 'utf8', timeout: 15000 });
|
|
1040
|
+
let parsed = null;
|
|
1041
|
+
try { parsed = JSON.parse(r.stdout.split('\n').filter(l => l.startsWith('{')).pop() || '{}'); } catch {}
|
|
1042
|
+
const ok = r.status === 0 && /imported: 3/.test(r.stdout);
|
|
1043
|
+
console.log(ok ? '✓ B(1.9.38) task sync: 3개 TodoWrite → progress-tracker import' : `✗ task sync 실패`);
|
|
1044
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
total++;
|
|
1048
|
+
{
|
|
1049
|
+
// A. drift reminder 파일 자동 생성 (인공 stale 시뮬)
|
|
1050
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-rem-'));
|
|
1051
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
1052
|
+
// session-handoff.md를 5일 전으로
|
|
1053
|
+
const shPath = path.join(tmpC, '.harness', 'session-handoff.md');
|
|
1054
|
+
if (fs.existsSync(shPath)) {
|
|
1055
|
+
const oldDate = new Date(Date.now() - 5 * 86400000).toISOString();
|
|
1056
|
+
let body = fs.readFileSync(shPath, 'utf8');
|
|
1057
|
+
body = body.replace(/Last generated:.*/, `Last generated: ${oldDate}`);
|
|
1058
|
+
if (!/Last generated:/.test(body)) body = `Last generated: ${oldDate}\n` + body;
|
|
1059
|
+
fs.writeFileSync(shPath, body, 'utf8');
|
|
1060
|
+
}
|
|
1061
|
+
// handoff 호출 → reminder 자동 생성
|
|
1062
|
+
cp.spawnSync(process.execPath, [CLI, 'handoff', tmpC, '--compact'], { encoding: 'utf8', timeout: 10000 });
|
|
1063
|
+
const remPath = path.join(tmpC, '.harness', 'agent-reminders.md');
|
|
1064
|
+
const ok = fs.existsSync(remPath) && /drift critical/.test(fs.readFileSync(remPath, 'utf8'));
|
|
1065
|
+
console.log(ok ? '✓ B(1.9.38) drift critical → agent-reminders.md 자동 생성' : `✗ reminder 파일 실패`);
|
|
1066
|
+
if (!ok) { failed++; if (fs.existsSync(remPath)) console.log(fs.readFileSync(remPath, 'utf8').slice(0, 400)); else console.log('(reminder 파일 없음)'); }
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
total++;
|
|
1070
|
+
{
|
|
1071
|
+
// D. drift 학습 — --no-drift-check 5회 호출 후 임계 완화
|
|
1072
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-learn-'));
|
|
1073
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
1074
|
+
// 5회 --no-drift-check 호출
|
|
1075
|
+
for (let i = 0; i < 5; i++) {
|
|
1076
|
+
cp.spawnSync(process.execPath, [CLI, 'handoff', tmpC, '--compact', '--no-drift-check'], { stdio: 'ignore', timeout: 10000 });
|
|
1077
|
+
}
|
|
1078
|
+
const stats = JSON.parse(fs.readFileSync(path.join(tmpC, '.harness', 'cache', 'usage-stats.json'), 'utf8'));
|
|
1079
|
+
const ok = stats.drift && stats.drift.skipped >= 5;
|
|
1080
|
+
console.log(ok ? '✓ B(1.9.38) drift 학습: --no-drift-check 5회 누적 (skipped≥5)' : `✗ drift 학습 실패`);
|
|
1081
|
+
if (!ok) { failed++; console.log(JSON.stringify(stats.drift || {})); }
|
|
1082
|
+
}
|
|
1083
|
+
|
|
953
1084
|
// 1.9.37 회귀: drift detection
|
|
954
1085
|
total++;
|
|
955
1086
|
{
|