leerness 1.9.7 → 1.9.9
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 +58 -0
- package/bin/harness.js +341 -6
- package/package.json +1 -1
- package/scripts/e2e.js +85 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.9 — 2026-05-12
|
|
4
|
+
|
|
5
|
+
**1.9.8 시연 중 자체 도그푸드(dogfood)로 빌드된 패치 — 룰 시스템이 정확히 작동한 증거**.
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- `nonFlagArgs()`의 `withValue` set에 `--trigger`, `--check`, `--set`, `--min-score` 추가. 이전에는 `rule add "..." --trigger every-update` 호출 시 `every-update`가 description 끝에 합쳐져 등록되던 작은 버그.
|
|
9
|
+
|
|
10
|
+
### Demonstrated (자체 도그푸드)
|
|
11
|
+
- 메인 디렉토리에 사용자 자연어 룰 3종 (버전 bump / CHANGELOG 추가 / 배포 안내) 등록
|
|
12
|
+
- `leerness handoff`가 active rules 자동 노출 ✓
|
|
13
|
+
- `leerness rule pause R-0003` → handoff에서 R-0003 사라짐 ✓
|
|
14
|
+
- `leerness release bump --patch` → 1.9.8 → 1.9.9 자동 ✓
|
|
15
|
+
- `leerness release note "..."` → 이 CHANGELOG 항목 자동 작성 ✓
|
|
16
|
+
- `leerness session close`가 룰 검증 (`✓ pass / ⓘ manual / ○ baseline`) 자동 보고 ✓
|
|
17
|
+
|
|
18
|
+
## 1.9.8 — 2026-05-08
|
|
19
|
+
|
|
20
|
+
**자연어 룰 등록 + 매 세션 자동 노출·검증 + 코드로 자동화 가능한 release 명령군**.
|
|
21
|
+
|
|
22
|
+
### Added — User Rules
|
|
23
|
+
|
|
24
|
+
- `.harness/rules.md` — 사용자 정의 영구 룰의 단일 출처. AGENTS.md mandatory read order 10번에 자동 포함.
|
|
25
|
+
- `leerness rule add "<설명>" --trigger every-session|every-update|every-commit|session-start|session-close|pre-publish` — 룰 등록.
|
|
26
|
+
- `leerness rule list / verify / pause <id> / resume <id> / remove <id> / stop / resume-all` — 룰 라이프사이클.
|
|
27
|
+
- `leerness handoff`가 **active rules를 매 세션 시작 시 자동 출력** (사용자 중지 요청 전까지).
|
|
28
|
+
- `leerness session close`가 **활성 룰별 자동 검증 결과 (`✓ pass / ⓘ manual / ⓿ pending / ○ baseline`) 자동 보고**.
|
|
29
|
+
|
|
30
|
+
### Added — 자연어 룰 처리 지시 (AGENTS.md / CLAUDE.md)
|
|
31
|
+
|
|
32
|
+
사용자가 자연어로 "매 X마다 Y를 해줘"라고 말하면 AI 에이전트가 즉시 `leerness rule add` 명령을 호출하도록 매핑 표를 추가:
|
|
33
|
+
|
|
34
|
+
| 자연어 | leerness 명령 |
|
|
35
|
+
|---|---|
|
|
36
|
+
| "매 업데이트마다 버전 bump" | `rule add "버전 patch bump" --trigger every-update` |
|
|
37
|
+
| "매 커밋마다 패치노트" | `rule add "패치노트 추가" --trigger every-commit` |
|
|
38
|
+
| "세션 종료마다 배포" | `rule add "배포" --trigger session-close` |
|
|
39
|
+
| "X 룰 중지/그만" | `rule pause <id>` |
|
|
40
|
+
| "X 룰 제거" | `rule remove <id>` |
|
|
41
|
+
| "모든 룰 중지" | `rule stop` |
|
|
42
|
+
|
|
43
|
+
### Added — release 명령군 (자동화 가능한 룰의 실행 도구)
|
|
44
|
+
|
|
45
|
+
- `leerness release bump [--patch|--minor|--major]` — `package.json#version`과 `.harness/HARNESS_VERSION` 자동 bump.
|
|
46
|
+
- `leerness release note "<내용>"` — CHANGELOG.md에 자동 추가 (같은 버전이면 항목만, 새 버전이면 헤더+항목).
|
|
47
|
+
- `leerness release publish [--dry-run] [--git-push] [--npm-publish]` — npm pack + (선택) git push + (선택) npm publish 통합.
|
|
48
|
+
|
|
49
|
+
### Added — 룰 자동 검증 휴리스틱
|
|
50
|
+
|
|
51
|
+
`session close`가 매번 자동 수행:
|
|
52
|
+
- **version / 버전 / bump 키워드 룰** → `package.json` version이 baseline 캐시 대비 갱신됐는지.
|
|
53
|
+
- **changelog / 패치노트 키워드 룰** → CHANGELOG.md mtime이 갱신됐는지.
|
|
54
|
+
- **test / 테스트 / verify 키워드 룰** → review-evidence.md에 오늘 verify-code 흔적이 있는지.
|
|
55
|
+
- **deploy / 배포 / publish 키워드 룰** → 자동 검증 불가 → `ⓘ manual` (사용자 안내).
|
|
56
|
+
|
|
57
|
+
### Migration
|
|
58
|
+
|
|
59
|
+
기존 1.9.x 사용자는 `npx leerness@latest update . --yes`로 즉시 자동 마이그레이션됩니다. `.harness/rules.md`는 자동 생성됩니다.
|
|
60
|
+
|
|
3
61
|
## 1.9.7 — 2026-05-08
|
|
4
62
|
|
|
5
63
|
코드 검증 자동 실행 + 과거 결정/실수 자동 회수 + TODO 자동 추적의 3종 자동화.
|
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.9';
|
|
10
10
|
const MARK = '<!-- leerness:managed -->';
|
|
11
11
|
const README_START = '<!-- leerness:project-readme:start -->';
|
|
12
12
|
const README_END = '<!-- leerness:project-readme:end -->';
|
|
@@ -56,7 +56,7 @@ function arg(name, def = null) { const i = process.argv.indexOf(name); return i
|
|
|
56
56
|
function has(name) { return process.argv.includes(name); }
|
|
57
57
|
function nonFlagArgs() {
|
|
58
58
|
const out = [];
|
|
59
|
-
const withValue = new Set(['--language','--skills','--path','--status','--progress','--goal','--reason','--next','--target','--token-env','--package','--out','--from','--repo','--id','--note','--evidence','--query','--limit','--action','--agent','--tool','--doc','--command','--capability','--before','--after','--display','--threshold']);
|
|
59
|
+
const withValue = new Set(['--language','--skills','--path','--status','--progress','--goal','--reason','--next','--target','--token-env','--package','--out','--from','--repo','--id','--note','--evidence','--query','--limit','--action','--agent','--tool','--doc','--command','--capability','--before','--after','--display','--threshold','--trigger','--check','--set','--min-score']);
|
|
60
60
|
const a = process.argv.slice(2);
|
|
61
61
|
for (let i = 0; i < a.length; i++) {
|
|
62
62
|
const x = a[i];
|
|
@@ -141,8 +141,8 @@ function coreFiles(root, lang = 'ko', selectedSkills = []) {
|
|
|
141
141
|
const project = detectProjectName(root);
|
|
142
142
|
const skillRows = Object.entries(skillCatalog).map(([k, v]) => `| ${k} | ${v.displayNameKo} | ${v.capabilities.join(', ')} | ${v.lastUpdated} | ${v.verification} |`).join('\n');
|
|
143
143
|
return {
|
|
144
|
-
'AGENTS.md': `${MARK}\n# Leerness Agent Instructions\n\n## Mandatory read order (session start)\n1. .harness/context-routing.md\n2. .harness/session-handoff.md\n3. .harness/current-state.md\n4. .harness/plan.md\n5. .harness/progress-tracker.md\n6. .harness/guideline.md\n7. .harness/protected-files.md\n8. .harness/writeback-policy.md\n9. .harness/anti-lazy-work-policy.md\n\n## Required behavior\n- 작업 시작 시 \`leerness handoff .\`를 실행해 컨텍스트를
|
|
145
|
-
'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`,
|
|
144
|
+
'AGENTS.md': `${MARK}\n# Leerness Agent Instructions\n\n## Mandatory read order (session start)\n1. .harness/context-routing.md\n2. .harness/session-handoff.md\n3. .harness/current-state.md\n4. .harness/plan.md\n5. .harness/progress-tracker.md\n6. .harness/guideline.md\n7. .harness/protected-files.md\n8. .harness/writeback-policy.md\n9. .harness/anti-lazy-work-policy.md\n10. **.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.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`,
|
|
145
|
+
'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`,
|
|
146
146
|
'.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`,
|
|
147
147
|
'.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`,
|
|
148
148
|
'.harness/HARNESS_VERSION': VERSION + '\n',
|
|
@@ -173,6 +173,7 @@ function coreFiles(root, lang = 'ko', selectedSkills = []) {
|
|
|
173
173
|
'.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`),
|
|
174
174
|
'.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`),
|
|
175
175
|
'.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`),
|
|
176
|
+
'.harness/rules.md': _rulesHeader() + '\n',
|
|
176
177
|
'.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`),
|
|
177
178
|
'.harness/leerness-maintenance.md': fm('leerness-maintenance', ['작업 시작','마이그레이션/릴리즈 전'], ['버전 정책 변경'], `# Leerness Maintenance\n\nAI agents should check:\n\n\`\`\`bash\nleerness --version\nleerness self check .\nleerness update --check # 24h 캐시 자동 감지\nleerness update --yes # 새 버전 발견 시 자동 마이그레이션\ncat .harness/HARNESS_VERSION\nnpm view leerness version\n\`\`\`\n`),
|
|
178
179
|
'.harness/language-policy.md': fm('language-policy', ['문서 작성 전'], ['언어 변경'], `# Language Policy\n\nSelected language: ${lang}\n\n모든 Leerness 노트, 스킬 노트, 세션 보고, 작업 목록은 위 언어를 기본으로 사용합니다 (사용자가 다른 언어를 명시 요청 시 예외).\n`),
|
|
@@ -1111,6 +1112,14 @@ function handoff(root) {
|
|
|
1111
1112
|
log('# Session Start Context');
|
|
1112
1113
|
log(`Date: ${today()}`);
|
|
1113
1114
|
log(`Project: ${detectProjectName(root)}`);
|
|
1115
|
+
// 1.9.8: active rules 자동 노출 (매 세션 시작 시 AI에게 보임)
|
|
1116
|
+
const activeRules = readRules(root).filter(r => r.status === 'active');
|
|
1117
|
+
if (activeRules.length) {
|
|
1118
|
+
log('');
|
|
1119
|
+
log('## ⚡ Active User Rules (사용자가 명시 중지/제거 요청 전까지 매 세션 자동 노출)');
|
|
1120
|
+
for (const r of activeRules) log(`- ${r.id} [${r.trigger}] ${r.rule} (lastVerified: ${r.lastVerified || '-'})`);
|
|
1121
|
+
log('');
|
|
1122
|
+
}
|
|
1114
1123
|
log(out);
|
|
1115
1124
|
if (exists(currentStatePath(root))) {
|
|
1116
1125
|
const cs = read(currentStatePath(root)).replace(/Updated: \d{4}-\d{2}-\d{2}/, `Updated: ${today()}`);
|
|
@@ -1183,8 +1192,18 @@ function sessionClose(root) {
|
|
|
1183
1192
|
log(`\n### ${s}`);
|
|
1184
1193
|
log(rowsToList(buckets[s]));
|
|
1185
1194
|
}
|
|
1195
|
+
// 1.9.8: 룰 검증 자동 수행 + 보고
|
|
1196
|
+
const ruleResults = verifyRules(root);
|
|
1197
|
+
log('\n## ⚡ User Rules verification');
|
|
1198
|
+
if (!ruleResults.length) log('- 활성 룰 없음');
|
|
1199
|
+
else {
|
|
1200
|
+
log('| ID | Trigger | Rule | Verified | Note |');
|
|
1201
|
+
log('|---|---|---|---|---|');
|
|
1202
|
+
const ic = { pass: '✓ pass', pending: '⓿ pending', manual: 'ⓘ manual', baseline: '○ baseline' };
|
|
1203
|
+
for (const r of ruleResults) log(`| ${r.id} | ${r.trigger} | ${r.rule.slice(0, 40)} | ${ic[r.verified] || '?'} | ${r.note} |`);
|
|
1204
|
+
}
|
|
1186
1205
|
log('\n## Required final response sections');
|
|
1187
|
-
log('- 완료 작업\n- 진행 중 작업\n- 미완료/예정/대기/보류/차단/드랍 작업\n- 검증 결과\n- 추천 방향\n- 다음 정확한
|
|
1206
|
+
log('- 완료 작업\n- 진행 중 작업\n- 미완료/예정/대기/보류/차단/드랍 작업\n- 검증 결과\n- 추천 방향\n- 다음 정확한 작업\n- ⚡ 활성 룰별 검증 결과');
|
|
1188
1207
|
ok(`session-handoff.md and current-state.md updated`);
|
|
1189
1208
|
}
|
|
1190
1209
|
|
|
@@ -1234,6 +1253,306 @@ function gate(root) {
|
|
|
1234
1253
|
else ok('all gates passed');
|
|
1235
1254
|
}
|
|
1236
1255
|
|
|
1256
|
+
// ===== 1.9.8: User Rules (자연어 등록 + 매 세션 자동 노출/검증) =====
|
|
1257
|
+
function rulesPath(root) { return path.join(root, '.harness/rules.md'); }
|
|
1258
|
+
function rulesArchivePath(root) { return path.join(root, '.harness/rules.archive.md'); }
|
|
1259
|
+
function rulesCachePath(root) { return path.join(root, '.harness/cache/rule-state.json'); }
|
|
1260
|
+
|
|
1261
|
+
function _rulesHeader() {
|
|
1262
|
+
return [
|
|
1263
|
+
'---',
|
|
1264
|
+
'leernessRole: rules',
|
|
1265
|
+
'readWhen:',
|
|
1266
|
+
' - 세션 시작 (handoff)',
|
|
1267
|
+
' - 매 작업 시작 전',
|
|
1268
|
+
' - 매 작업 완료 전',
|
|
1269
|
+
' - 세션 종료 시 (session close)',
|
|
1270
|
+
'updateWhen:',
|
|
1271
|
+
' - 사용자가 자연어로 새 룰 요청',
|
|
1272
|
+
' - 사용자가 룰 중지/제거 요청',
|
|
1273
|
+
'doNotStore:',
|
|
1274
|
+
' - 실제 토큰',
|
|
1275
|
+
' - 비밀번호',
|
|
1276
|
+
' - 운영 쿠키',
|
|
1277
|
+
' - 민감한 개인정보 원문',
|
|
1278
|
+
'---',
|
|
1279
|
+
'<!-- leerness:managed -->',
|
|
1280
|
+
'# User Rules',
|
|
1281
|
+
'',
|
|
1282
|
+
'매 세션·매 작업마다 AI 에이전트가 반드시 따라야 할 사용자 정의 영구 룰.',
|
|
1283
|
+
'사용자가 명시적으로 "중지" / "제거"를 요청하기 전까지 모든 active 룰을 매 세션 자동 노출/검증합니다.',
|
|
1284
|
+
'',
|
|
1285
|
+
'## Active Rules',
|
|
1286
|
+
'',
|
|
1287
|
+
'| ID | Trigger | Rule | Added | Status | Last Verified |',
|
|
1288
|
+
'|---|---|---|---|---|---|'
|
|
1289
|
+
].join('\n');
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
function readRules(root) {
|
|
1293
|
+
const f = rulesPath(root);
|
|
1294
|
+
if (!exists(f)) return [];
|
|
1295
|
+
const rules = [];
|
|
1296
|
+
for (const line of read(f).split('\n')) {
|
|
1297
|
+
if (!/^\| R-\d{4} \|/.test(line)) continue;
|
|
1298
|
+
const cells = line.split('|').slice(1, -1).map(s => s.trim());
|
|
1299
|
+
if (cells.length < 6) continue;
|
|
1300
|
+
rules.push({ id: cells[0], trigger: cells[1], rule: cells[2], added: cells[3], status: cells[4], lastVerified: cells[5] });
|
|
1301
|
+
}
|
|
1302
|
+
return rules;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function writeRules(root, rules) {
|
|
1306
|
+
const body = rules.map(r => `| ${r.id} | ${r.trigger} | ${r.rule} | ${r.added} | ${r.status} | ${r.lastVerified || '-'} |`).join('\n');
|
|
1307
|
+
writeUtf8(rulesPath(root), _rulesHeader() + '\n' + body + (body ? '\n' : ''));
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
function nextRuleId(root) {
|
|
1311
|
+
const rules = readRules(root);
|
|
1312
|
+
let max = 0;
|
|
1313
|
+
for (const r of rules) {
|
|
1314
|
+
const m = r.id.match(/^R-(\d{4})$/);
|
|
1315
|
+
if (m) max = Math.max(max, Number(m[1]));
|
|
1316
|
+
}
|
|
1317
|
+
return `R-${String(max + 1).padStart(4, '0')}`;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
function ruleAdd(root, description) {
|
|
1321
|
+
root = absRoot(root);
|
|
1322
|
+
if (!description) return fail('rule description required (e.g., rule add "매 업데이트마다 버전 bump" --trigger every-update)');
|
|
1323
|
+
if (!exists(rulesPath(root))) writeRules(root, []);
|
|
1324
|
+
const trigger = arg('--trigger', 'every-session');
|
|
1325
|
+
const validTriggers = new Set(['every-session','every-update','every-commit','session-start','session-close','pre-publish']);
|
|
1326
|
+
if (!validTriggers.has(trigger)) {
|
|
1327
|
+
warn(`unknown trigger "${trigger}" — 사용 가능: ${[...validTriggers].join(', ')}. 그대로 등록합니다.`);
|
|
1328
|
+
}
|
|
1329
|
+
const id = nextRuleId(root);
|
|
1330
|
+
const rules = readRules(root);
|
|
1331
|
+
rules.push({ id, trigger, rule: description, added: today(), status: 'active', lastVerified: '-' });
|
|
1332
|
+
writeRules(root, rules);
|
|
1333
|
+
ok(`rule added: ${id} [${trigger}] ${description}`);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
function ruleList(root) {
|
|
1337
|
+
root = absRoot(root);
|
|
1338
|
+
const rules = readRules(root);
|
|
1339
|
+
if (!rules.length) return ok('등록된 룰 없음');
|
|
1340
|
+
log('| ID | Trigger | Rule | Status | Last Verified |');
|
|
1341
|
+
log('|---|---|---|---|---|');
|
|
1342
|
+
for (const r of rules) log(`| ${r.id} | ${r.trigger} | ${r.rule} | ${r.status} | ${r.lastVerified} |`);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
function ruleRemove(root, id) {
|
|
1346
|
+
root = absRoot(root);
|
|
1347
|
+
if (!id) return fail('id required');
|
|
1348
|
+
const rules = readRules(root);
|
|
1349
|
+
const i = rules.findIndex(r => r.id === id);
|
|
1350
|
+
if (i < 0) return fail(`rule not found: ${id}`);
|
|
1351
|
+
const removed = rules.splice(i, 1)[0];
|
|
1352
|
+
writeRules(root, rules);
|
|
1353
|
+
const archive = exists(rulesArchivePath(root)) ? read(rulesArchivePath(root)) : '# Rules archive\n\n| ID | Trigger | Rule | Added | Status | Removed |\n|---|---|---|---|---|---|\n';
|
|
1354
|
+
writeUtf8(rulesArchivePath(root), archive + `| ${removed.id} | ${removed.trigger} | ${removed.rule} | ${removed.added} | removed | ${today()} |\n`);
|
|
1355
|
+
ok(`rule removed: ${id} (보존: .harness/rules.archive.md)`);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
function rulePause(root, id) {
|
|
1359
|
+
root = absRoot(root);
|
|
1360
|
+
if (!id) return fail('id required');
|
|
1361
|
+
const rules = readRules(root);
|
|
1362
|
+
const r = rules.find(x => x.id === id);
|
|
1363
|
+
if (!r) return fail(`rule not found: ${id}`);
|
|
1364
|
+
r.status = 'paused';
|
|
1365
|
+
writeRules(root, rules);
|
|
1366
|
+
ok(`rule paused: ${id}`);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
function ruleResume(root, id) {
|
|
1370
|
+
root = absRoot(root);
|
|
1371
|
+
if (!id) return fail('id required');
|
|
1372
|
+
const rules = readRules(root);
|
|
1373
|
+
const r = rules.find(x => x.id === id);
|
|
1374
|
+
if (!r) return fail(`rule not found: ${id}`);
|
|
1375
|
+
r.status = 'active';
|
|
1376
|
+
writeRules(root, rules);
|
|
1377
|
+
ok(`rule resumed: ${id}`);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
function ruleStop(root) {
|
|
1381
|
+
root = absRoot(root);
|
|
1382
|
+
const rules = readRules(root);
|
|
1383
|
+
let n = 0;
|
|
1384
|
+
for (const r of rules) if (r.status === 'active') { r.status = 'paused'; n++; }
|
|
1385
|
+
writeRules(root, rules);
|
|
1386
|
+
ok(`${n}개 룰 일시 정지 (rule resume <id> 또는 rule resume-all로 재개)`);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
function ruleResumeAll(root) {
|
|
1390
|
+
root = absRoot(root);
|
|
1391
|
+
const rules = readRules(root);
|
|
1392
|
+
let n = 0;
|
|
1393
|
+
for (const r of rules) if (r.status === 'paused') { r.status = 'active'; n++; }
|
|
1394
|
+
writeRules(root, rules);
|
|
1395
|
+
ok(`${n}개 룰 재개`);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
function captureProjectState(root) {
|
|
1399
|
+
const state = { capturedAt: now() };
|
|
1400
|
+
const pkgFile = path.join(root, 'package.json');
|
|
1401
|
+
if (exists(pkgFile)) { try { state.packageVersion = JSON.parse(read(pkgFile)).version; } catch {} }
|
|
1402
|
+
const cl = path.join(root, 'CHANGELOG.md');
|
|
1403
|
+
if (exists(cl)) { try { state.changelogMtime = fs.statSync(cl).mtime.getTime(); state.changelogSize = fs.statSync(cl).size; } catch {} }
|
|
1404
|
+
const hv = path.join(root, '.harness/HARNESS_VERSION');
|
|
1405
|
+
if (exists(hv)) state.harnessVersion = read(hv).trim();
|
|
1406
|
+
return state;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
function verifyRules(root) {
|
|
1410
|
+
root = absRoot(root);
|
|
1411
|
+
const rules = readRules(root);
|
|
1412
|
+
const active = rules.filter(r => r.status === 'active');
|
|
1413
|
+
if (!active.length) return [];
|
|
1414
|
+
let prev = {};
|
|
1415
|
+
if (exists(rulesCachePath(root))) { try { prev = JSON.parse(read(rulesCachePath(root))); } catch {} }
|
|
1416
|
+
const cur = captureProjectState(root);
|
|
1417
|
+
const ev = exists(evidencePath(root)) ? read(evidencePath(root)) : '';
|
|
1418
|
+
const todayStr = today();
|
|
1419
|
+
const results = [];
|
|
1420
|
+
for (const r of active) {
|
|
1421
|
+
let verified = 'manual';
|
|
1422
|
+
let note = '';
|
|
1423
|
+
const rl = r.rule.toLowerCase();
|
|
1424
|
+
if (/version|버전|bump|상승/i.test(rl)) {
|
|
1425
|
+
if (prev.packageVersion && cur.packageVersion && prev.packageVersion !== cur.packageVersion) {
|
|
1426
|
+
verified = 'pass'; note = `${prev.packageVersion} → ${cur.packageVersion}`;
|
|
1427
|
+
} else if (!prev.packageVersion) {
|
|
1428
|
+
verified = 'baseline'; note = `초기 ${cur.packageVersion || '미확인'}`;
|
|
1429
|
+
} else {
|
|
1430
|
+
verified = 'pending'; note = '버전 변경 없음';
|
|
1431
|
+
}
|
|
1432
|
+
} else if (/changelog|패치노트|patch.*note|note.*추가|note.*add/i.test(rl)) {
|
|
1433
|
+
if (prev.changelogMtime && cur.changelogMtime && cur.changelogMtime > prev.changelogMtime) {
|
|
1434
|
+
verified = 'pass'; note = 'CHANGELOG.md 갱신 감지';
|
|
1435
|
+
} else if (!prev.changelogMtime) {
|
|
1436
|
+
verified = 'baseline'; note = '초기 측정';
|
|
1437
|
+
} else {
|
|
1438
|
+
verified = 'pending'; note = 'CHANGELOG.md 변경 없음';
|
|
1439
|
+
}
|
|
1440
|
+
} else if (/test|테스트|verify/i.test(rl)) {
|
|
1441
|
+
const hasTest = new RegExp(`## ${todayStr}.*verify-code|## ${todayStr}.*test`, 'i').test(ev);
|
|
1442
|
+
verified = hasTest ? 'pass' : 'pending';
|
|
1443
|
+
note = hasTest ? '오늘 verify-code 흔적' : '오늘 verify-code 호출 없음';
|
|
1444
|
+
} else if (/deploy|배포|publish|push|release/i.test(rl)) {
|
|
1445
|
+
verified = 'manual'; note = '배포는 사용자 명시 호출 (leerness release publish)';
|
|
1446
|
+
} else {
|
|
1447
|
+
verified = 'manual'; note = '자동 검증 패턴 없음 — 수동 확인';
|
|
1448
|
+
}
|
|
1449
|
+
results.push({ ...r, verified, note });
|
|
1450
|
+
}
|
|
1451
|
+
// lastVerified 갱신 (pass인 경우만)
|
|
1452
|
+
for (const r of rules) {
|
|
1453
|
+
const m = results.find(x => x.id === r.id);
|
|
1454
|
+
if (m && m.verified === 'pass') r.lastVerified = todayStr;
|
|
1455
|
+
}
|
|
1456
|
+
writeRules(root, rules);
|
|
1457
|
+
writeUtf8(rulesCachePath(root), JSON.stringify(cur, null, 2));
|
|
1458
|
+
return results;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
function ruleVerifyCmd(root) {
|
|
1462
|
+
root = absRoot(root);
|
|
1463
|
+
const results = verifyRules(root);
|
|
1464
|
+
if (!results.length) return ok('활성 룰 없음');
|
|
1465
|
+
log('# Rules verification');
|
|
1466
|
+
log('| ID | Trigger | Rule | Verified | Note |');
|
|
1467
|
+
log('|---|---|---|---|---|');
|
|
1468
|
+
const ic = { pass: '✓ pass', pending: '⓿ pending', manual: 'ⓘ manual', baseline: '○ baseline' };
|
|
1469
|
+
for (const r of results) log(`| ${r.id} | ${r.trigger} | ${r.rule.slice(0, 40)} | ${ic[r.verified] || '?'} | ${r.note} |`);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// ===== 1.9.8: release bump / note / publish =====
|
|
1473
|
+
function releaseBump(root) {
|
|
1474
|
+
root = absRoot(root);
|
|
1475
|
+
const kind = has('--major') ? 'major' : (has('--minor') ? 'minor' : 'patch');
|
|
1476
|
+
const pkgFile = path.join(root, 'package.json');
|
|
1477
|
+
if (!exists(pkgFile)) return fail('package.json 없음');
|
|
1478
|
+
let pkg; try { pkg = JSON.parse(read(pkgFile)); } catch (e) { return fail('package.json 파싱 실패: ' + e.message); }
|
|
1479
|
+
const cur = String(pkg.version || '0.0.0');
|
|
1480
|
+
const parts = cur.split('.').map(n => parseInt(n, 10) || 0);
|
|
1481
|
+
const [maj, min, pat] = [parts[0]||0, parts[1]||0, parts[2]||0];
|
|
1482
|
+
let next;
|
|
1483
|
+
if (kind === 'major') next = `${maj + 1}.0.0`;
|
|
1484
|
+
else if (kind === 'minor') next = `${maj}.${min + 1}.0`;
|
|
1485
|
+
else next = `${maj}.${min}.${pat + 1}`;
|
|
1486
|
+
pkg.version = next;
|
|
1487
|
+
writeUtf8(pkgFile, JSON.stringify(pkg, null, 2) + '\n');
|
|
1488
|
+
const hv = path.join(root, '.harness/HARNESS_VERSION');
|
|
1489
|
+
if (exists(hv) && /^\d+\.\d+\.\d+/.test(read(hv).trim())) writeUtf8(hv, next + '\n');
|
|
1490
|
+
ok(`version bumped: ${cur} → ${next} (${kind})`);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function releaseNote(root, text) {
|
|
1494
|
+
root = absRoot(root);
|
|
1495
|
+
if (!text) return fail('note text required (e.g., release note "내용")');
|
|
1496
|
+
const pkgFile = path.join(root, 'package.json');
|
|
1497
|
+
let version = 'unknown';
|
|
1498
|
+
if (exists(pkgFile)) { try { version = JSON.parse(read(pkgFile)).version || 'unknown'; } catch {} }
|
|
1499
|
+
const clFile = path.join(root, 'CHANGELOG.md');
|
|
1500
|
+
const date = today();
|
|
1501
|
+
const headerRe = new RegExp(`^## ${version.replace(/\./g, '\\.')} — `, 'm');
|
|
1502
|
+
if (exists(clFile)) {
|
|
1503
|
+
const cur = read(clFile);
|
|
1504
|
+
if (headerRe.test(cur)) {
|
|
1505
|
+
// 같은 버전 헤더가 있으면 그 바로 아래에 줄 추가
|
|
1506
|
+
const m = cur.match(headerRe);
|
|
1507
|
+
const headerEnd = cur.indexOf('\n', m.index + m[0].length);
|
|
1508
|
+
const insertPos = headerEnd + 1;
|
|
1509
|
+
// 헤더 다음 빈 줄 후 첫 list 시작 찾기
|
|
1510
|
+
const beforeBlock = cur.slice(insertPos);
|
|
1511
|
+
const linesAfter = beforeBlock.split('\n');
|
|
1512
|
+
// 가장 단순: 헤더 다음 줄에 즉시 - text 삽입
|
|
1513
|
+
writeUtf8(clFile, cur.slice(0, insertPos) + `\n- ${text}\n` + cur.slice(insertPos));
|
|
1514
|
+
} else {
|
|
1515
|
+
// 새 버전 헤더 추가 (# Changelog 다음)
|
|
1516
|
+
const top = cur.indexOf('# Changelog');
|
|
1517
|
+
const newBlock = `\n## ${version} — ${date}\n\n- ${text}\n`;
|
|
1518
|
+
if (top >= 0) {
|
|
1519
|
+
const after = cur.indexOf('\n', top) + 1;
|
|
1520
|
+
writeUtf8(clFile, cur.slice(0, after) + newBlock + cur.slice(after));
|
|
1521
|
+
} else {
|
|
1522
|
+
writeUtf8(clFile, `# Changelog\n${newBlock}\n${cur}`);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
} else {
|
|
1526
|
+
writeUtf8(clFile, `# Changelog\n\n## ${version} — ${date}\n\n- ${text}\n`);
|
|
1527
|
+
}
|
|
1528
|
+
ok(`CHANGELOG.md 갱신: [${version}] ${text}`);
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
function releasePublish(root) {
|
|
1532
|
+
root = absRoot(root);
|
|
1533
|
+
const dryRun = has('--dry-run');
|
|
1534
|
+
log('# release publish');
|
|
1535
|
+
log(`Mode: ${dryRun ? 'dry-run' : 'live'}`);
|
|
1536
|
+
const packR = cp.spawnSync('npm', ['pack'], { cwd: root, encoding: 'utf8', shell: true });
|
|
1537
|
+
if (packR.status !== 0) { fail('npm pack 실패'); log(packR.stderr); process.exitCode = 1; return; }
|
|
1538
|
+
ok('npm pack 완료');
|
|
1539
|
+
if (has('--git-push')) {
|
|
1540
|
+
log('git push:');
|
|
1541
|
+
const r1 = cp.spawnSync('git', ['push'], { cwd: root, encoding: 'utf8', shell: true });
|
|
1542
|
+
log(r1.stdout || r1.stderr || '(no output)');
|
|
1543
|
+
const r2 = cp.spawnSync('git', ['push', '--tags'], { cwd: root, encoding: 'utf8', shell: true });
|
|
1544
|
+
log(r2.stdout || r2.stderr || '(no output)');
|
|
1545
|
+
}
|
|
1546
|
+
if (has('--npm-publish')) {
|
|
1547
|
+
const args = dryRun ? ['publish', '--dry-run'] : ['publish', '--access', 'public'];
|
|
1548
|
+
log('npm ' + args.join(' '));
|
|
1549
|
+
const r = cp.spawnSync('npm', args, { cwd: root, encoding: 'utf8', shell: true });
|
|
1550
|
+
log((r.stdout || '').split('\n').slice(-5).join('\n'));
|
|
1551
|
+
if (r.status !== 0) { fail('npm publish 실패'); process.exitCode = 1; return; }
|
|
1552
|
+
}
|
|
1553
|
+
ok('release publish 완료');
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1237
1556
|
// ===== 1.9.7 A: verify-code — npm scripts 자동 감지 + evidence 자동 기록 =====
|
|
1238
1557
|
function verifyCodeCmd(root) {
|
|
1239
1558
|
root = absRoot(root);
|
|
@@ -1754,7 +2073,12 @@ function help() {
|
|
|
1754
2073
|
log(`Leerness v${VERSION}\n\nUsage:\n leerness init [path] [--language auto|ko|en] [--skills recommended|all|a,b]\n leerness migrate [path] [--dry-run] [--force]\n leerness update [path] [--check|--yes|--force|--from <tarball>]\n leerness auto-update install [path]\n leerness status [path]\n leerness verify [path]\n leerness debug [path]\n leerness audit [path]\n leerness check [path]\n leerness scan secrets [path]\n leerness encoding check [path]\n leerness lazy detect [path]\n leerness memory search "query" [--limit 5]\n leerness handoff [path]\n leerness session close [path]\n leerness viewwork install [path]\n leerness viewwork emit [path] [--action a] [--note n] [--agent x] [--tool t]\n leerness route <task-type>\n leerness self check [path]\n leerness readme sync [path]\n leerness consistency check [path]\n leerness consistency merge-design-guide [path]\n leerness plan show|init|add|drop|progress|sync [args]\n leerness task list|add|update|drop|fix-evidence|relink [args]\n leerness skill list|info <name>\n leerness skill learn <id> --doc <url> --command "..." --capability "..." [--note ...]\n leerness skill use <id> [--note ...]\n leerness skill optimize <id> --before "..." --after "..." [--note ...]\n leerness skill remove <id>\n leerness skill consolidate [--threshold 0.3]\n leerness gate [path] # verify+audit+scan+encoding+lazy
|
|
1755
2074
|
leerness verify-code [path] [--build] # npm test/lint/typecheck 자동 실행 + evidence 자동 기록 (1.9.7)
|
|
1756
2075
|
leerness lessons [--query <키>] [--limit N] # 과거 결정/실수 자동 회수 (1.9.7)
|
|
1757
|
-
leerness lazy detect [path] [--auto-track] # --auto-track으로 새 TODO를 progress에 자동 등록 (1.9.7)
|
|
2076
|
+
leerness lazy detect [path] [--auto-track] # --auto-track으로 새 TODO를 progress에 자동 등록 (1.9.7)
|
|
2077
|
+
leerness rule add "<설명>" --trigger every-session|every-update|every-commit|session-start|session-close|pre-publish # 사용자 룰 등록 (1.9.8)
|
|
2078
|
+
leerness rule list|verify|pause <id>|resume <id>|remove <id>|stop|resume-all
|
|
2079
|
+
leerness release bump [--patch|--minor|--major] # package.json 자동 bump (1.9.8)
|
|
2080
|
+
leerness release note "<내용>" # CHANGELOG.md 자동 추가 (1.9.8)
|
|
2081
|
+
leerness release publish [--dry-run] [--git-push] [--npm-publish] # 통합 배포 (1.9.8)\n leerness impact <target> [--all] # 변경 전 영향 분석 (기본 strong, --all로 weak 포함)\n leerness reuse find <query> # 기존 자원 검색 (재귀 안내)\n leerness reuse register <name> --where <p> --kind component|hook|util|api [--note ...]\n leerness ui consistency [path] [--strict] [--fail-on-violation]\n leerness graph [path] [--out <file>] # mermaid 의존성 그래프\n leerness guide [target] # impact + reuse + ui consistency 통합 가이드\n`);
|
|
1758
2082
|
}
|
|
1759
2083
|
|
|
1760
2084
|
async function main() {
|
|
@@ -1795,6 +2119,17 @@ async function main() {
|
|
|
1795
2119
|
if (cmd === 'gate') return gate(args[1] || process.cwd());
|
|
1796
2120
|
if (cmd === 'verify-code') return verifyCodeCmd(args[1] || process.cwd());
|
|
1797
2121
|
if (cmd === 'lessons') return lessonsCmd(arg('--path', process.cwd()));
|
|
2122
|
+
if (cmd === 'rule' && args[1] === 'add') return ruleAdd(arg('--path', process.cwd()), args.slice(2).filter(x => !x.startsWith('-')).join(' '));
|
|
2123
|
+
if (cmd === 'rule' && args[1] === 'list') return ruleList(arg('--path', process.cwd()));
|
|
2124
|
+
if (cmd === 'rule' && args[1] === 'remove') return ruleRemove(arg('--path', process.cwd()), args[2]);
|
|
2125
|
+
if (cmd === 'rule' && args[1] === 'pause') return rulePause(arg('--path', process.cwd()), args[2]);
|
|
2126
|
+
if (cmd === 'rule' && args[1] === 'resume') return ruleResume(arg('--path', process.cwd()), args[2]);
|
|
2127
|
+
if (cmd === 'rule' && args[1] === 'stop') return ruleStop(arg('--path', process.cwd()));
|
|
2128
|
+
if (cmd === 'rule' && args[1] === 'resume-all') return ruleResumeAll(arg('--path', process.cwd()));
|
|
2129
|
+
if (cmd === 'rule' && args[1] === 'verify') return ruleVerifyCmd(arg('--path', process.cwd()));
|
|
2130
|
+
if (cmd === 'release' && args[1] === 'bump') return releaseBump(arg('--path', process.cwd()));
|
|
2131
|
+
if (cmd === 'release' && args[1] === 'note') return releaseNote(arg('--path', process.cwd()), args.slice(2).filter(x => !x.startsWith('-')).join(' '));
|
|
2132
|
+
if (cmd === 'release' && args[1] === 'publish') return releasePublish(arg('--path', process.cwd()));
|
|
1798
2133
|
if (cmd === 'impact') return impactCmd(arg('--path', process.cwd()), args[1]);
|
|
1799
2134
|
if (cmd === 'reuse' && args[1] === 'find') return reuseFind(arg('--path', process.cwd()), args.slice(2).filter(x => !x.startsWith('-')).join(' '));
|
|
1800
2135
|
if (cmd === 'reuse' && args[1] === 'register') return reuseRegister(arg('--path', process.cwd()), args[2]);
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -235,6 +235,91 @@ total++;
|
|
|
235
235
|
if (!(strongOK && weakHint)) failed++;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
// 1.9.8: rule add/list/pause/resume/remove
|
|
239
|
+
total++;
|
|
240
|
+
{
|
|
241
|
+
const r1 = cp.spawnSync(process.execPath, [CLI, 'rule', 'add', '매 업데이트마다 버전 patch bump', '--trigger', 'every-update', '--path', tmp], { encoding: 'utf8' });
|
|
242
|
+
const r2 = cp.spawnSync(process.execPath, [CLI, 'rule', 'add', '매 업데이트마다 패치노트 추가', '--trigger', 'every-update', '--path', tmp], { encoding: 'utf8' });
|
|
243
|
+
const r3 = cp.spawnSync(process.execPath, [CLI, 'rule', 'add', '세션 종료마다 배포', '--trigger', 'session-close', '--path', tmp], { encoding: 'utf8' });
|
|
244
|
+
const rl = cp.spawnSync(process.execPath, [CLI, 'rule', 'list', '--path', tmp], { encoding: 'utf8' });
|
|
245
|
+
const ok = r1.status === 0 && r2.status === 0 && r3.status === 0 && /R-0001/.test(rl.stdout) && /R-0003/.test(rl.stdout);
|
|
246
|
+
console.log(ok ? '✓ B(1.9.8) rule add/list: 3개 등록' : '✗ B(1.9.8) rule add/list 실패');
|
|
247
|
+
if (!ok) failed++;
|
|
248
|
+
}
|
|
249
|
+
total++;
|
|
250
|
+
{
|
|
251
|
+
// pause + handoff에서 paused는 안 보여야 함
|
|
252
|
+
cp.spawnSync(process.execPath, [CLI, 'rule', 'pause', 'R-0001', '--path', tmp], { encoding: 'utf8' });
|
|
253
|
+
const hr = cp.spawnSync(process.execPath, [CLI, 'handoff', tmp], { encoding: 'utf8' });
|
|
254
|
+
const ok = /Active User Rules/.test(hr.stdout) && /R-0002/.test(hr.stdout) && /R-0003/.test(hr.stdout) && !/R-0001 \[/.test(hr.stdout);
|
|
255
|
+
console.log(ok ? '✓ B(1.9.8) handoff: paused 룰 제외, active만 출력' : '✗ B(1.9.8) handoff 출력 실패');
|
|
256
|
+
if (!ok) { failed++; console.log(hr.stdout); }
|
|
257
|
+
cp.spawnSync(process.execPath, [CLI, 'rule', 'resume', 'R-0001', '--path', tmp], { encoding: 'utf8' });
|
|
258
|
+
}
|
|
259
|
+
total++;
|
|
260
|
+
{
|
|
261
|
+
// rule stop / resume-all
|
|
262
|
+
cp.spawnSync(process.execPath, [CLI, 'rule', 'stop', '--path', tmp], { encoding: 'utf8' });
|
|
263
|
+
const rl = cp.spawnSync(process.execPath, [CLI, 'rule', 'list', '--path', tmp], { encoding: 'utf8' });
|
|
264
|
+
const allPaused = (rl.stdout.match(/\| paused \|/g) || []).length >= 3;
|
|
265
|
+
cp.spawnSync(process.execPath, [CLI, 'rule', 'resume-all', '--path', tmp], { encoding: 'utf8' });
|
|
266
|
+
const rl2 = cp.spawnSync(process.execPath, [CLI, 'rule', 'list', '--path', tmp], { encoding: 'utf8' });
|
|
267
|
+
const allActive = (rl2.stdout.match(/\| active \|/g) || []).length >= 3;
|
|
268
|
+
console.log(allPaused && allActive ? '✓ B(1.9.8) rule stop / resume-all: 일괄 전환' : '✗ B(1.9.8) stop/resume-all 실패');
|
|
269
|
+
if (!(allPaused && allActive)) failed++;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 1.9.8: release bump
|
|
273
|
+
total++;
|
|
274
|
+
{
|
|
275
|
+
const tmpR = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-rel-'));
|
|
276
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpR, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore' });
|
|
277
|
+
fs.writeFileSync(path.join(tmpR, 'package.json'), JSON.stringify({ name: 't', version: '1.0.0' }));
|
|
278
|
+
cp.spawnSync(process.execPath, [CLI, 'release', 'bump', '--patch', '--path', tmpR], { encoding: 'utf8' });
|
|
279
|
+
let v = JSON.parse(fs.readFileSync(path.join(tmpR, 'package.json'), 'utf8')).version;
|
|
280
|
+
const okPatch = v === '1.0.1';
|
|
281
|
+
cp.spawnSync(process.execPath, [CLI, 'release', 'bump', '--minor', '--path', tmpR], { encoding: 'utf8' });
|
|
282
|
+
v = JSON.parse(fs.readFileSync(path.join(tmpR, 'package.json'), 'utf8')).version;
|
|
283
|
+
const okMinor = v === '1.1.0';
|
|
284
|
+
cp.spawnSync(process.execPath, [CLI, 'release', 'bump', '--major', '--path', tmpR], { encoding: 'utf8' });
|
|
285
|
+
v = JSON.parse(fs.readFileSync(path.join(tmpR, 'package.json'), 'utf8')).version;
|
|
286
|
+
const okMajor = v === '2.0.0';
|
|
287
|
+
console.log(okPatch && okMinor && okMajor ? '✓ B(1.9.8) release bump: patch/minor/major' : `✗ B(1.9.8) bump 실패 final=${v}`);
|
|
288
|
+
if (!(okPatch && okMinor && okMajor)) failed++;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 1.9.8: release note → CHANGELOG.md 자동 갱신
|
|
292
|
+
total++;
|
|
293
|
+
{
|
|
294
|
+
const tmpN = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-note-'));
|
|
295
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpN, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore' });
|
|
296
|
+
fs.writeFileSync(path.join(tmpN, 'package.json'), JSON.stringify({ name: 't', version: '0.1.0' }));
|
|
297
|
+
cp.spawnSync(process.execPath, [CLI, 'release', 'note', '첫 기능 추가', '--path', tmpN], { encoding: 'utf8' });
|
|
298
|
+
const cl = fs.readFileSync(path.join(tmpN, 'CHANGELOG.md'), 'utf8');
|
|
299
|
+
const ok = /## 0\.1\.0/.test(cl) && /첫 기능 추가/.test(cl);
|
|
300
|
+
console.log(ok ? '✓ B(1.9.8) release note: CHANGELOG 자동 갱신' : '✗ B(1.9.8) release note 실패');
|
|
301
|
+
if (!ok) failed++;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 1.9.8: session close가 rule verification 보고
|
|
305
|
+
total++;
|
|
306
|
+
{
|
|
307
|
+
// tmp는 위에서 rule 3개 등록됨
|
|
308
|
+
// package.json 만들기 + 버전 변경 시뮬 (rule R-0001은 every-update 버전 룰)
|
|
309
|
+
fs.writeFileSync(path.join(tmp, 'package.json'), JSON.stringify({ name: 'tmp-e2e', version: '0.1.0' }));
|
|
310
|
+
// 첫 session close — baseline 캡처
|
|
311
|
+
cp.spawnSync(process.execPath, [CLI, 'session', 'close', tmp], { encoding: 'utf8' });
|
|
312
|
+
// 버전 bump
|
|
313
|
+
cp.spawnSync(process.execPath, [CLI, 'release', 'bump', '--patch', '--path', tmp], { encoding: 'utf8' });
|
|
314
|
+
// CHANGELOG 갱신
|
|
315
|
+
cp.spawnSync(process.execPath, [CLI, 'release', 'note', 'e2e 검증 항목 추가', '--path', tmp], { encoding: 'utf8' });
|
|
316
|
+
// 두 번째 session close — 변경 감지
|
|
317
|
+
const sc = cp.spawnSync(process.execPath, [CLI, 'session', 'close', tmp], { encoding: 'utf8' });
|
|
318
|
+
const ok = /User Rules verification/.test(sc.stdout) && /✓ pass/.test(sc.stdout);
|
|
319
|
+
console.log(ok ? '✓ B(1.9.8) session close: rule 검증 ✓ pass 출력' : `✗ B(1.9.8) session close 검증 실패\n${sc.stdout.split('\n').slice(-15).join('\n')}`);
|
|
320
|
+
if (!ok) failed++;
|
|
321
|
+
}
|
|
322
|
+
|
|
238
323
|
// 1.9.7 A: verify-code — 가짜 package.json + 통과 시나리오
|
|
239
324
|
total++;
|
|
240
325
|
{
|