leerness 1.9.7 → 1.9.10
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 +94 -0
- package/bin/harness.js +504 -8
- package/package.json +1 -1
- package/scripts/e2e.js +126 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,99 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.10 — 2026-05-12
|
|
4
|
+
|
|
5
|
+
**leerness-skillpack 분리 + release publish 강화 (git remote 자동 감지 + GitHub Release + gh-pages 배포)**.
|
|
6
|
+
|
|
7
|
+
### Changed — 스킬 카탈로그 동적 로드
|
|
8
|
+
|
|
9
|
+
- `leerness-skillpack`이 npm에 별도 패키지로 분리됨. leerness 본 패키지는 `_tryLoadSkillpack()`으로 다음 순서로 동적 로드:
|
|
10
|
+
1. `require('leerness-skillpack/catalog.json')` 시도
|
|
11
|
+
2. `<cwd>/node_modules/leerness-skillpack/catalog.json` 탐색
|
|
12
|
+
3. `npm root -g`의 `leerness-skillpack/catalog.json` 탐색
|
|
13
|
+
4. `LEERNESS_SKILLPACK_PATH` 환경변수 경로
|
|
14
|
+
5. 모두 실패 시 leerness 본 패키지의 내장 fallback (1.9.x 호환 유지)
|
|
15
|
+
- `leerness init` 출력에 `Skill catalog source: skillpack v1.0.0 | builtin (fallback)` 안내.
|
|
16
|
+
- `leerness skill list` 헤더에 카탈로그 출처 + 출처 컬럼에 `skillpack` / `builtin` / `user` 표시.
|
|
17
|
+
|
|
18
|
+
### Added — release publish 강화
|
|
19
|
+
|
|
20
|
+
- `detectGitRemote(root)`: 현재 디렉토리의 `git remote -v origin` 자동 감지 + GitHub owner/repo 추출.
|
|
21
|
+
- `leerness release publish` 신규 플래그:
|
|
22
|
+
- `--auto` — remote 있으면 자동 `git push` (편의)
|
|
23
|
+
- `--gh-release` — gh CLI로 GitHub Release 자동 생성 (`v<version>` 태그 + 자동 노트 + tarball 첨부)
|
|
24
|
+
- `--gh-pages` — `gh-pages` branch에 정적 파일 자동 배포 (orphan 또는 기존 branch). 기본 소스는 `roadmap.html`, `--gh-pages-src <file>` 또는 `--roadmap <file>`로 지정.
|
|
25
|
+
- `--pack` — npm pack만 명시적 실행
|
|
26
|
+
- `gh-pages` 배포는 임시 git worktree로 처리해 현재 작업 트리에 영향 없음. 배포 후 `https://<owner>.github.io/<repo>/` URL 안내.
|
|
27
|
+
|
|
28
|
+
### Migration
|
|
29
|
+
|
|
30
|
+
기존 1.9.x 사용자는 `npx leerness@latest update . --yes`로 즉시 자동 마이그레이션됩니다. leerness-skillpack은 선택 설치:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install leerness-skillpack # 본 카탈로그 사용
|
|
34
|
+
# 또는 그대로 두면 leerness 내장 fallback이 동작 (기존과 동일)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 1.9.9 — 2026-05-12
|
|
38
|
+
|
|
39
|
+
- 1.9.9 빌드 + GitHub 배포
|
|
40
|
+
|
|
41
|
+
**1.9.8 시연 중 자체 도그푸드(dogfood)로 빌드된 패치 — 룰 시스템이 정확히 작동한 증거**.
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
- `nonFlagArgs()`의 `withValue` set에 `--trigger`, `--check`, `--set`, `--min-score` 추가. 이전에는 `rule add "..." --trigger every-update` 호출 시 `every-update`가 description 끝에 합쳐져 등록되던 작은 버그.
|
|
45
|
+
|
|
46
|
+
### Demonstrated (자체 도그푸드)
|
|
47
|
+
- 메인 디렉토리에 사용자 자연어 룰 3종 (버전 bump / CHANGELOG 추가 / 배포 안내) 등록
|
|
48
|
+
- `leerness handoff`가 active rules 자동 노출 ✓
|
|
49
|
+
- `leerness rule pause R-0003` → handoff에서 R-0003 사라짐 ✓
|
|
50
|
+
- `leerness release bump --patch` → 1.9.8 → 1.9.9 자동 ✓
|
|
51
|
+
- `leerness release note "..."` → 이 CHANGELOG 항목 자동 작성 ✓
|
|
52
|
+
- `leerness session close`가 룰 검증 (`✓ pass / ⓘ manual / ○ baseline`) 자동 보고 ✓
|
|
53
|
+
|
|
54
|
+
## 1.9.8 — 2026-05-08
|
|
55
|
+
|
|
56
|
+
**자연어 룰 등록 + 매 세션 자동 노출·검증 + 코드로 자동화 가능한 release 명령군**.
|
|
57
|
+
|
|
58
|
+
### Added — User Rules
|
|
59
|
+
|
|
60
|
+
- `.harness/rules.md` — 사용자 정의 영구 룰의 단일 출처. AGENTS.md mandatory read order 10번에 자동 포함.
|
|
61
|
+
- `leerness rule add "<설명>" --trigger every-session|every-update|every-commit|session-start|session-close|pre-publish` — 룰 등록.
|
|
62
|
+
- `leerness rule list / verify / pause <id> / resume <id> / remove <id> / stop / resume-all` — 룰 라이프사이클.
|
|
63
|
+
- `leerness handoff`가 **active rules를 매 세션 시작 시 자동 출력** (사용자 중지 요청 전까지).
|
|
64
|
+
- `leerness session close`가 **활성 룰별 자동 검증 결과 (`✓ pass / ⓘ manual / ⓿ pending / ○ baseline`) 자동 보고**.
|
|
65
|
+
|
|
66
|
+
### Added — 자연어 룰 처리 지시 (AGENTS.md / CLAUDE.md)
|
|
67
|
+
|
|
68
|
+
사용자가 자연어로 "매 X마다 Y를 해줘"라고 말하면 AI 에이전트가 즉시 `leerness rule add` 명령을 호출하도록 매핑 표를 추가:
|
|
69
|
+
|
|
70
|
+
| 자연어 | leerness 명령 |
|
|
71
|
+
|---|---|
|
|
72
|
+
| "매 업데이트마다 버전 bump" | `rule add "버전 patch bump" --trigger every-update` |
|
|
73
|
+
| "매 커밋마다 패치노트" | `rule add "패치노트 추가" --trigger every-commit` |
|
|
74
|
+
| "세션 종료마다 배포" | `rule add "배포" --trigger session-close` |
|
|
75
|
+
| "X 룰 중지/그만" | `rule pause <id>` |
|
|
76
|
+
| "X 룰 제거" | `rule remove <id>` |
|
|
77
|
+
| "모든 룰 중지" | `rule stop` |
|
|
78
|
+
|
|
79
|
+
### Added — release 명령군 (자동화 가능한 룰의 실행 도구)
|
|
80
|
+
|
|
81
|
+
- `leerness release bump [--patch|--minor|--major]` — `package.json#version`과 `.harness/HARNESS_VERSION` 자동 bump.
|
|
82
|
+
- `leerness release note "<내용>"` — CHANGELOG.md에 자동 추가 (같은 버전이면 항목만, 새 버전이면 헤더+항목).
|
|
83
|
+
- `leerness release publish [--dry-run] [--git-push] [--npm-publish]` — npm pack + (선택) git push + (선택) npm publish 통합.
|
|
84
|
+
|
|
85
|
+
### Added — 룰 자동 검증 휴리스틱
|
|
86
|
+
|
|
87
|
+
`session close`가 매번 자동 수행:
|
|
88
|
+
- **version / 버전 / bump 키워드 룰** → `package.json` version이 baseline 캐시 대비 갱신됐는지.
|
|
89
|
+
- **changelog / 패치노트 키워드 룰** → CHANGELOG.md mtime이 갱신됐는지.
|
|
90
|
+
- **test / 테스트 / verify 키워드 룰** → review-evidence.md에 오늘 verify-code 흔적이 있는지.
|
|
91
|
+
- **deploy / 배포 / publish 키워드 룰** → 자동 검증 불가 → `ⓘ manual` (사용자 안내).
|
|
92
|
+
|
|
93
|
+
### Migration
|
|
94
|
+
|
|
95
|
+
기존 1.9.x 사용자는 `npx leerness@latest update . --yes`로 즉시 자동 마이그레이션됩니다. `.harness/rules.md`는 자동 생성됩니다.
|
|
96
|
+
|
|
3
97
|
## 1.9.7 — 2026-05-08
|
|
4
98
|
|
|
5
99
|
코드 검증 자동 실행 + 과거 결정/실수 자동 회수 + TODO 자동 추적의 3종 자동화.
|
package/bin/harness.js
CHANGED
|
@@ -6,12 +6,64 @@ 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.10';
|
|
10
10
|
const MARK = '<!-- leerness:managed -->';
|
|
11
11
|
const README_START = '<!-- leerness:project-readme:start -->';
|
|
12
12
|
const README_END = '<!-- leerness:project-readme:end -->';
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
// 1.9.10: leerness-skillpack 동적 로드 (선택). 없으면 BUILTIN 사용.
|
|
15
|
+
function _tryLoadSkillpack() {
|
|
16
|
+
// 1) 정상 require resolution
|
|
17
|
+
try { return { src: 'require', data: require('leerness-skillpack/catalog.json') }; } catch {}
|
|
18
|
+
// 2) cwd/node_modules
|
|
19
|
+
try {
|
|
20
|
+
const f = path.join(process.cwd(), 'node_modules/leerness-skillpack/catalog.json');
|
|
21
|
+
if (fs.existsSync(f)) return { src: 'cwd', data: JSON.parse(fs.readFileSync(f, 'utf8')) };
|
|
22
|
+
} catch {}
|
|
23
|
+
// 3) npm global root
|
|
24
|
+
try {
|
|
25
|
+
const root = cp.execSync('npm root -g', { encoding: 'utf8', timeout: 4000 }).trim();
|
|
26
|
+
const f = path.join(root, 'leerness-skillpack/catalog.json');
|
|
27
|
+
if (fs.existsSync(f)) return { src: 'global', data: JSON.parse(fs.readFileSync(f, 'utf8')) };
|
|
28
|
+
} catch {}
|
|
29
|
+
// 4) 환경변수 명시 경로
|
|
30
|
+
if (process.env.LEERNESS_SKILLPACK_PATH) {
|
|
31
|
+
try {
|
|
32
|
+
const f = path.resolve(process.env.LEERNESS_SKILLPACK_PATH);
|
|
33
|
+
const target = f.endsWith('.json') ? f : path.join(f, 'catalog.json');
|
|
34
|
+
if (fs.existsSync(target)) return { src: 'env', data: JSON.parse(fs.readFileSync(target, 'utf8')) };
|
|
35
|
+
} catch {}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let SKILLPACK_SOURCE = 'builtin';
|
|
41
|
+
let SKILLPACK_META = null;
|
|
42
|
+
function _loadSkillCatalog() {
|
|
43
|
+
const sp = _tryLoadSkillpack();
|
|
44
|
+
if (sp && sp.data && Array.isArray(sp.data.skills)) {
|
|
45
|
+
SKILLPACK_SOURCE = sp.src;
|
|
46
|
+
SKILLPACK_META = { name: sp.data.name, version: sp.data.version };
|
|
47
|
+
const out = {};
|
|
48
|
+
for (const s of sp.data.skills) {
|
|
49
|
+
out[s.id] = {
|
|
50
|
+
displayNameKo: s.displayNameKo,
|
|
51
|
+
version: s.version,
|
|
52
|
+
lastUpdated: s.lastUpdated,
|
|
53
|
+
verification: s.verification,
|
|
54
|
+
capabilities: s.capabilities,
|
|
55
|
+
_source: 'skillpack'
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
SKILLPACK_SOURCE = 'builtin';
|
|
61
|
+
const out = {};
|
|
62
|
+
for (const [k, v] of Object.entries(BUILTIN_CATALOG)) out[k] = { ...v, _source: 'builtin' };
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const BUILTIN_CATALOG = {
|
|
15
67
|
'office': { displayNameKo: '마이크로소프트 오피스 자동화 스킬 라이브러리', version: '1.0.0', lastUpdated: '2026-05-08', verification: 'passed', capabilities: ['Word/Excel/PowerPoint 문서 자동화', '템플릿 기반 문서 생성', '표/차트/요약 문서화', '민감정보 제외 규칙 적용'] },
|
|
16
68
|
'commerce-api': { displayNameKo: '커머스 API 연동 스킬 라이브러리', version: '1.0.0', lastUpdated: '2026-05-08', verification: 'passed', capabilities: ['쿠팡·롯데온·스마트스토어 API 연동 설계', '주문/상품/매출 동기화', '환경변수 기반 인증 분리', '레이트리밋/재시도/오류 처리'] },
|
|
17
69
|
'crawling': { displayNameKo: '크롤링·브라우저 자동화 스킬 라이브러리', version: '1.0.0', lastUpdated: '2026-05-08', verification: 'passed', capabilities: ['Playwright 기반 자동화', '다운로드/로그인 세션 처리', '스크린샷 기반 실패 진단', '약관/권한/차단 위험 점검'] },
|
|
@@ -22,6 +74,9 @@ const skillCatalog = {
|
|
|
22
74
|
'feature-implementation': { displayNameKo: '기능 구현 표준 스킬', version: '1.0.0', lastUpdated: '2026-05-08', verification: 'passed', capabilities: ['feature-contracts 작성', '재사용 우선 검사', '테스트 증거 수집', '핸드오프 트리거'] }
|
|
23
75
|
};
|
|
24
76
|
|
|
77
|
+
// 1.9.10: skillCatalog는 skillpack 우선, fallback builtin. _loadSkillCatalog 호출은 BUILTIN_CATALOG 정의 후.
|
|
78
|
+
const skillCatalog = _loadSkillCatalog();
|
|
79
|
+
|
|
25
80
|
const routes = {
|
|
26
81
|
planning: { read: ['.harness/plan.md','.harness/progress-tracker.md','.harness/project-brief.md','.harness/current-state.md','.harness/guideline.md'], update: ['.harness/plan.md','.harness/progress-tracker.md','.harness/current-state.md','.harness/session-handoff.md'] },
|
|
27
82
|
feature: { read: ['.harness/plan.md','.harness/current-state.md','.harness/architecture.md','.harness/context-map.md','.harness/feature-contracts.md','.harness/skills/feature-implementation/README.md','.harness/reuse-map.md'], update: ['.harness/progress-tracker.md','.harness/feature-contracts.md','.harness/current-state.md','.harness/task-log.md','.harness/session-handoff.md'] },
|
|
@@ -56,7 +111,7 @@ function arg(name, def = null) { const i = process.argv.indexOf(name); return i
|
|
|
56
111
|
function has(name) { return process.argv.includes(name); }
|
|
57
112
|
function nonFlagArgs() {
|
|
58
113
|
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']);
|
|
114
|
+
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
115
|
const a = process.argv.slice(2);
|
|
61
116
|
for (let i = 0; i < a.length; i++) {
|
|
62
117
|
const x = a[i];
|
|
@@ -141,8 +196,8 @@ function coreFiles(root, lang = 'ko', selectedSkills = []) {
|
|
|
141
196
|
const project = detectProjectName(root);
|
|
142
197
|
const skillRows = Object.entries(skillCatalog).map(([k, v]) => `| ${k} | ${v.displayNameKo} | ${v.capabilities.join(', ')} | ${v.lastUpdated} | ${v.verification} |`).join('\n');
|
|
143
198
|
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`,
|
|
199
|
+
'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`,
|
|
200
|
+
'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
201
|
'.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
202
|
'.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
203
|
'.harness/HARNESS_VERSION': VERSION + '\n',
|
|
@@ -173,6 +228,7 @@ function coreFiles(root, lang = 'ko', selectedSkills = []) {
|
|
|
173
228
|
'.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
229
|
'.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
230
|
'.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`),
|
|
231
|
+
'.harness/rules.md': _rulesHeader() + '\n',
|
|
176
232
|
'.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
233
|
'.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
234
|
'.harness/language-policy.md': fm('language-policy', ['문서 작성 전'], ['언어 변경'], `# Language Policy\n\nSelected language: ${lang}\n\n모든 Leerness 노트, 스킬 노트, 세션 보고, 작업 목록은 위 언어를 기본으로 사용합니다 (사용자가 다른 언어를 명시 요청 시 예외).\n`),
|
|
@@ -334,6 +390,9 @@ async function install(root, opts = {}) {
|
|
|
334
390
|
log(`Target: ${root}`);
|
|
335
391
|
log(`Language: ${lang}`);
|
|
336
392
|
log(`Skills: ${skills.length ? skills.join(', ') : 'none'}`);
|
|
393
|
+
// 1.9.10: 스킬 카탈로그 출처 안내
|
|
394
|
+
if (SKILLPACK_SOURCE === 'builtin') log(`Skill catalog source: builtin (leerness-skillpack 미설치 — \`npm i leerness-skillpack\`로 확장 가능)`);
|
|
395
|
+
else log(`Skill catalog source: ${SKILLPACK_SOURCE} (leerness-skillpack${SKILLPACK_META ? ` v${SKILLPACK_META.version}` : ''})`);
|
|
337
396
|
const files = coreFiles(root, lang, skills);
|
|
338
397
|
const backup = createBackup(root, opts.force ? 'force' : (opts.migration ? 'migration' : 'init'), files, opts.dry);
|
|
339
398
|
if (opts.dry) {
|
|
@@ -429,7 +488,8 @@ function saveUserSkill(root, id, data) {
|
|
|
429
488
|
|
|
430
489
|
function listAllSkills(root) {
|
|
431
490
|
const out = {};
|
|
432
|
-
|
|
491
|
+
// 1.9.10: skillCatalog의 _source('skillpack' 또는 'builtin')를 보존
|
|
492
|
+
for (const [k, v] of Object.entries(skillCatalog)) out[k] = { ...v, _source: v._source || 'builtin' };
|
|
433
493
|
if (root) {
|
|
434
494
|
const dir = userSkillsDir(root);
|
|
435
495
|
if (exists(dir)) {
|
|
@@ -447,6 +507,8 @@ function listAllSkills(root) {
|
|
|
447
507
|
|
|
448
508
|
function skillList(root) {
|
|
449
509
|
const all = listAllSkills(root);
|
|
510
|
+
if (SKILLPACK_SOURCE !== 'builtin') log(`# skillpack 출처: ${SKILLPACK_SOURCE}${SKILLPACK_META ? ` (${SKILLPACK_META.name} v${SKILLPACK_META.version})` : ''}`);
|
|
511
|
+
else log('# skillpack 미설치 — builtin fallback 사용 (leerness 본 패키지 내장 카탈로그)');
|
|
450
512
|
log('| ID | 한글명 | 출처 | 능력(요약) | 사용횟수 | 최종 |');
|
|
451
513
|
log('|---|---|---|---|---|---|');
|
|
452
514
|
for (const [id, v] of Object.entries(all)) {
|
|
@@ -1111,6 +1173,14 @@ function handoff(root) {
|
|
|
1111
1173
|
log('# Session Start Context');
|
|
1112
1174
|
log(`Date: ${today()}`);
|
|
1113
1175
|
log(`Project: ${detectProjectName(root)}`);
|
|
1176
|
+
// 1.9.8: active rules 자동 노출 (매 세션 시작 시 AI에게 보임)
|
|
1177
|
+
const activeRules = readRules(root).filter(r => r.status === 'active');
|
|
1178
|
+
if (activeRules.length) {
|
|
1179
|
+
log('');
|
|
1180
|
+
log('## ⚡ Active User Rules (사용자가 명시 중지/제거 요청 전까지 매 세션 자동 노출)');
|
|
1181
|
+
for (const r of activeRules) log(`- ${r.id} [${r.trigger}] ${r.rule} (lastVerified: ${r.lastVerified || '-'})`);
|
|
1182
|
+
log('');
|
|
1183
|
+
}
|
|
1114
1184
|
log(out);
|
|
1115
1185
|
if (exists(currentStatePath(root))) {
|
|
1116
1186
|
const cs = read(currentStatePath(root)).replace(/Updated: \d{4}-\d{2}-\d{2}/, `Updated: ${today()}`);
|
|
@@ -1183,8 +1253,18 @@ function sessionClose(root) {
|
|
|
1183
1253
|
log(`\n### ${s}`);
|
|
1184
1254
|
log(rowsToList(buckets[s]));
|
|
1185
1255
|
}
|
|
1256
|
+
// 1.9.8: 룰 검증 자동 수행 + 보고
|
|
1257
|
+
const ruleResults = verifyRules(root);
|
|
1258
|
+
log('\n## ⚡ User Rules verification');
|
|
1259
|
+
if (!ruleResults.length) log('- 활성 룰 없음');
|
|
1260
|
+
else {
|
|
1261
|
+
log('| ID | Trigger | Rule | Verified | Note |');
|
|
1262
|
+
log('|---|---|---|---|---|');
|
|
1263
|
+
const ic = { pass: '✓ pass', pending: '⓿ pending', manual: 'ⓘ manual', baseline: '○ baseline' };
|
|
1264
|
+
for (const r of ruleResults) log(`| ${r.id} | ${r.trigger} | ${r.rule.slice(0, 40)} | ${ic[r.verified] || '?'} | ${r.note} |`);
|
|
1265
|
+
}
|
|
1186
1266
|
log('\n## Required final response sections');
|
|
1187
|
-
log('- 완료 작업\n- 진행 중 작업\n- 미완료/예정/대기/보류/차단/드랍 작업\n- 검증 결과\n- 추천 방향\n- 다음 정확한
|
|
1267
|
+
log('- 완료 작업\n- 진행 중 작업\n- 미완료/예정/대기/보류/차단/드랍 작업\n- 검증 결과\n- 추천 방향\n- 다음 정확한 작업\n- ⚡ 활성 룰별 검증 결과');
|
|
1188
1268
|
ok(`session-handoff.md and current-state.md updated`);
|
|
1189
1269
|
}
|
|
1190
1270
|
|
|
@@ -1234,6 +1314,406 @@ function gate(root) {
|
|
|
1234
1314
|
else ok('all gates passed');
|
|
1235
1315
|
}
|
|
1236
1316
|
|
|
1317
|
+
// ===== 1.9.8: User Rules (자연어 등록 + 매 세션 자동 노출/검증) =====
|
|
1318
|
+
function rulesPath(root) { return path.join(root, '.harness/rules.md'); }
|
|
1319
|
+
function rulesArchivePath(root) { return path.join(root, '.harness/rules.archive.md'); }
|
|
1320
|
+
function rulesCachePath(root) { return path.join(root, '.harness/cache/rule-state.json'); }
|
|
1321
|
+
|
|
1322
|
+
function _rulesHeader() {
|
|
1323
|
+
return [
|
|
1324
|
+
'---',
|
|
1325
|
+
'leernessRole: rules',
|
|
1326
|
+
'readWhen:',
|
|
1327
|
+
' - 세션 시작 (handoff)',
|
|
1328
|
+
' - 매 작업 시작 전',
|
|
1329
|
+
' - 매 작업 완료 전',
|
|
1330
|
+
' - 세션 종료 시 (session close)',
|
|
1331
|
+
'updateWhen:',
|
|
1332
|
+
' - 사용자가 자연어로 새 룰 요청',
|
|
1333
|
+
' - 사용자가 룰 중지/제거 요청',
|
|
1334
|
+
'doNotStore:',
|
|
1335
|
+
' - 실제 토큰',
|
|
1336
|
+
' - 비밀번호',
|
|
1337
|
+
' - 운영 쿠키',
|
|
1338
|
+
' - 민감한 개인정보 원문',
|
|
1339
|
+
'---',
|
|
1340
|
+
'<!-- leerness:managed -->',
|
|
1341
|
+
'# User Rules',
|
|
1342
|
+
'',
|
|
1343
|
+
'매 세션·매 작업마다 AI 에이전트가 반드시 따라야 할 사용자 정의 영구 룰.',
|
|
1344
|
+
'사용자가 명시적으로 "중지" / "제거"를 요청하기 전까지 모든 active 룰을 매 세션 자동 노출/검증합니다.',
|
|
1345
|
+
'',
|
|
1346
|
+
'## Active Rules',
|
|
1347
|
+
'',
|
|
1348
|
+
'| ID | Trigger | Rule | Added | Status | Last Verified |',
|
|
1349
|
+
'|---|---|---|---|---|---|'
|
|
1350
|
+
].join('\n');
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
function readRules(root) {
|
|
1354
|
+
const f = rulesPath(root);
|
|
1355
|
+
if (!exists(f)) return [];
|
|
1356
|
+
const rules = [];
|
|
1357
|
+
for (const line of read(f).split('\n')) {
|
|
1358
|
+
if (!/^\| R-\d{4} \|/.test(line)) continue;
|
|
1359
|
+
const cells = line.split('|').slice(1, -1).map(s => s.trim());
|
|
1360
|
+
if (cells.length < 6) continue;
|
|
1361
|
+
rules.push({ id: cells[0], trigger: cells[1], rule: cells[2], added: cells[3], status: cells[4], lastVerified: cells[5] });
|
|
1362
|
+
}
|
|
1363
|
+
return rules;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
function writeRules(root, rules) {
|
|
1367
|
+
const body = rules.map(r => `| ${r.id} | ${r.trigger} | ${r.rule} | ${r.added} | ${r.status} | ${r.lastVerified || '-'} |`).join('\n');
|
|
1368
|
+
writeUtf8(rulesPath(root), _rulesHeader() + '\n' + body + (body ? '\n' : ''));
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
function nextRuleId(root) {
|
|
1372
|
+
const rules = readRules(root);
|
|
1373
|
+
let max = 0;
|
|
1374
|
+
for (const r of rules) {
|
|
1375
|
+
const m = r.id.match(/^R-(\d{4})$/);
|
|
1376
|
+
if (m) max = Math.max(max, Number(m[1]));
|
|
1377
|
+
}
|
|
1378
|
+
return `R-${String(max + 1).padStart(4, '0')}`;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
function ruleAdd(root, description) {
|
|
1382
|
+
root = absRoot(root);
|
|
1383
|
+
if (!description) return fail('rule description required (e.g., rule add "매 업데이트마다 버전 bump" --trigger every-update)');
|
|
1384
|
+
if (!exists(rulesPath(root))) writeRules(root, []);
|
|
1385
|
+
const trigger = arg('--trigger', 'every-session');
|
|
1386
|
+
const validTriggers = new Set(['every-session','every-update','every-commit','session-start','session-close','pre-publish']);
|
|
1387
|
+
if (!validTriggers.has(trigger)) {
|
|
1388
|
+
warn(`unknown trigger "${trigger}" — 사용 가능: ${[...validTriggers].join(', ')}. 그대로 등록합니다.`);
|
|
1389
|
+
}
|
|
1390
|
+
const id = nextRuleId(root);
|
|
1391
|
+
const rules = readRules(root);
|
|
1392
|
+
rules.push({ id, trigger, rule: description, added: today(), status: 'active', lastVerified: '-' });
|
|
1393
|
+
writeRules(root, rules);
|
|
1394
|
+
ok(`rule added: ${id} [${trigger}] ${description}`);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
function ruleList(root) {
|
|
1398
|
+
root = absRoot(root);
|
|
1399
|
+
const rules = readRules(root);
|
|
1400
|
+
if (!rules.length) return ok('등록된 룰 없음');
|
|
1401
|
+
log('| ID | Trigger | Rule | Status | Last Verified |');
|
|
1402
|
+
log('|---|---|---|---|---|');
|
|
1403
|
+
for (const r of rules) log(`| ${r.id} | ${r.trigger} | ${r.rule} | ${r.status} | ${r.lastVerified} |`);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function ruleRemove(root, id) {
|
|
1407
|
+
root = absRoot(root);
|
|
1408
|
+
if (!id) return fail('id required');
|
|
1409
|
+
const rules = readRules(root);
|
|
1410
|
+
const i = rules.findIndex(r => r.id === id);
|
|
1411
|
+
if (i < 0) return fail(`rule not found: ${id}`);
|
|
1412
|
+
const removed = rules.splice(i, 1)[0];
|
|
1413
|
+
writeRules(root, rules);
|
|
1414
|
+
const archive = exists(rulesArchivePath(root)) ? read(rulesArchivePath(root)) : '# Rules archive\n\n| ID | Trigger | Rule | Added | Status | Removed |\n|---|---|---|---|---|---|\n';
|
|
1415
|
+
writeUtf8(rulesArchivePath(root), archive + `| ${removed.id} | ${removed.trigger} | ${removed.rule} | ${removed.added} | removed | ${today()} |\n`);
|
|
1416
|
+
ok(`rule removed: ${id} (보존: .harness/rules.archive.md)`);
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
function rulePause(root, id) {
|
|
1420
|
+
root = absRoot(root);
|
|
1421
|
+
if (!id) return fail('id required');
|
|
1422
|
+
const rules = readRules(root);
|
|
1423
|
+
const r = rules.find(x => x.id === id);
|
|
1424
|
+
if (!r) return fail(`rule not found: ${id}`);
|
|
1425
|
+
r.status = 'paused';
|
|
1426
|
+
writeRules(root, rules);
|
|
1427
|
+
ok(`rule paused: ${id}`);
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
function ruleResume(root, id) {
|
|
1431
|
+
root = absRoot(root);
|
|
1432
|
+
if (!id) return fail('id required');
|
|
1433
|
+
const rules = readRules(root);
|
|
1434
|
+
const r = rules.find(x => x.id === id);
|
|
1435
|
+
if (!r) return fail(`rule not found: ${id}`);
|
|
1436
|
+
r.status = 'active';
|
|
1437
|
+
writeRules(root, rules);
|
|
1438
|
+
ok(`rule resumed: ${id}`);
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
function ruleStop(root) {
|
|
1442
|
+
root = absRoot(root);
|
|
1443
|
+
const rules = readRules(root);
|
|
1444
|
+
let n = 0;
|
|
1445
|
+
for (const r of rules) if (r.status === 'active') { r.status = 'paused'; n++; }
|
|
1446
|
+
writeRules(root, rules);
|
|
1447
|
+
ok(`${n}개 룰 일시 정지 (rule resume <id> 또는 rule resume-all로 재개)`);
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
function ruleResumeAll(root) {
|
|
1451
|
+
root = absRoot(root);
|
|
1452
|
+
const rules = readRules(root);
|
|
1453
|
+
let n = 0;
|
|
1454
|
+
for (const r of rules) if (r.status === 'paused') { r.status = 'active'; n++; }
|
|
1455
|
+
writeRules(root, rules);
|
|
1456
|
+
ok(`${n}개 룰 재개`);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
function captureProjectState(root) {
|
|
1460
|
+
const state = { capturedAt: now() };
|
|
1461
|
+
const pkgFile = path.join(root, 'package.json');
|
|
1462
|
+
if (exists(pkgFile)) { try { state.packageVersion = JSON.parse(read(pkgFile)).version; } catch {} }
|
|
1463
|
+
const cl = path.join(root, 'CHANGELOG.md');
|
|
1464
|
+
if (exists(cl)) { try { state.changelogMtime = fs.statSync(cl).mtime.getTime(); state.changelogSize = fs.statSync(cl).size; } catch {} }
|
|
1465
|
+
const hv = path.join(root, '.harness/HARNESS_VERSION');
|
|
1466
|
+
if (exists(hv)) state.harnessVersion = read(hv).trim();
|
|
1467
|
+
return state;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
function verifyRules(root) {
|
|
1471
|
+
root = absRoot(root);
|
|
1472
|
+
const rules = readRules(root);
|
|
1473
|
+
const active = rules.filter(r => r.status === 'active');
|
|
1474
|
+
if (!active.length) return [];
|
|
1475
|
+
let prev = {};
|
|
1476
|
+
if (exists(rulesCachePath(root))) { try { prev = JSON.parse(read(rulesCachePath(root))); } catch {} }
|
|
1477
|
+
const cur = captureProjectState(root);
|
|
1478
|
+
const ev = exists(evidencePath(root)) ? read(evidencePath(root)) : '';
|
|
1479
|
+
const todayStr = today();
|
|
1480
|
+
const results = [];
|
|
1481
|
+
for (const r of active) {
|
|
1482
|
+
let verified = 'manual';
|
|
1483
|
+
let note = '';
|
|
1484
|
+
const rl = r.rule.toLowerCase();
|
|
1485
|
+
if (/version|버전|bump|상승/i.test(rl)) {
|
|
1486
|
+
if (prev.packageVersion && cur.packageVersion && prev.packageVersion !== cur.packageVersion) {
|
|
1487
|
+
verified = 'pass'; note = `${prev.packageVersion} → ${cur.packageVersion}`;
|
|
1488
|
+
} else if (!prev.packageVersion) {
|
|
1489
|
+
verified = 'baseline'; note = `초기 ${cur.packageVersion || '미확인'}`;
|
|
1490
|
+
} else {
|
|
1491
|
+
verified = 'pending'; note = '버전 변경 없음';
|
|
1492
|
+
}
|
|
1493
|
+
} else if (/changelog|패치노트|patch.*note|note.*추가|note.*add/i.test(rl)) {
|
|
1494
|
+
if (prev.changelogMtime && cur.changelogMtime && cur.changelogMtime > prev.changelogMtime) {
|
|
1495
|
+
verified = 'pass'; note = 'CHANGELOG.md 갱신 감지';
|
|
1496
|
+
} else if (!prev.changelogMtime) {
|
|
1497
|
+
verified = 'baseline'; note = '초기 측정';
|
|
1498
|
+
} else {
|
|
1499
|
+
verified = 'pending'; note = 'CHANGELOG.md 변경 없음';
|
|
1500
|
+
}
|
|
1501
|
+
} else if (/test|테스트|verify/i.test(rl)) {
|
|
1502
|
+
const hasTest = new RegExp(`## ${todayStr}.*verify-code|## ${todayStr}.*test`, 'i').test(ev);
|
|
1503
|
+
verified = hasTest ? 'pass' : 'pending';
|
|
1504
|
+
note = hasTest ? '오늘 verify-code 흔적' : '오늘 verify-code 호출 없음';
|
|
1505
|
+
} else if (/deploy|배포|publish|push|release/i.test(rl)) {
|
|
1506
|
+
verified = 'manual'; note = '배포는 사용자 명시 호출 (leerness release publish)';
|
|
1507
|
+
} else {
|
|
1508
|
+
verified = 'manual'; note = '자동 검증 패턴 없음 — 수동 확인';
|
|
1509
|
+
}
|
|
1510
|
+
results.push({ ...r, verified, note });
|
|
1511
|
+
}
|
|
1512
|
+
// lastVerified 갱신 (pass인 경우만)
|
|
1513
|
+
for (const r of rules) {
|
|
1514
|
+
const m = results.find(x => x.id === r.id);
|
|
1515
|
+
if (m && m.verified === 'pass') r.lastVerified = todayStr;
|
|
1516
|
+
}
|
|
1517
|
+
writeRules(root, rules);
|
|
1518
|
+
writeUtf8(rulesCachePath(root), JSON.stringify(cur, null, 2));
|
|
1519
|
+
return results;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
function ruleVerifyCmd(root) {
|
|
1523
|
+
root = absRoot(root);
|
|
1524
|
+
const results = verifyRules(root);
|
|
1525
|
+
if (!results.length) return ok('활성 룰 없음');
|
|
1526
|
+
log('# Rules verification');
|
|
1527
|
+
log('| ID | Trigger | Rule | Verified | Note |');
|
|
1528
|
+
log('|---|---|---|---|---|');
|
|
1529
|
+
const ic = { pass: '✓ pass', pending: '⓿ pending', manual: 'ⓘ manual', baseline: '○ baseline' };
|
|
1530
|
+
for (const r of results) log(`| ${r.id} | ${r.trigger} | ${r.rule.slice(0, 40)} | ${ic[r.verified] || '?'} | ${r.note} |`);
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// ===== 1.9.8: release bump / note / publish =====
|
|
1534
|
+
function releaseBump(root) {
|
|
1535
|
+
root = absRoot(root);
|
|
1536
|
+
const kind = has('--major') ? 'major' : (has('--minor') ? 'minor' : 'patch');
|
|
1537
|
+
const pkgFile = path.join(root, 'package.json');
|
|
1538
|
+
if (!exists(pkgFile)) return fail('package.json 없음');
|
|
1539
|
+
let pkg; try { pkg = JSON.parse(read(pkgFile)); } catch (e) { return fail('package.json 파싱 실패: ' + e.message); }
|
|
1540
|
+
const cur = String(pkg.version || '0.0.0');
|
|
1541
|
+
const parts = cur.split('.').map(n => parseInt(n, 10) || 0);
|
|
1542
|
+
const [maj, min, pat] = [parts[0]||0, parts[1]||0, parts[2]||0];
|
|
1543
|
+
let next;
|
|
1544
|
+
if (kind === 'major') next = `${maj + 1}.0.0`;
|
|
1545
|
+
else if (kind === 'minor') next = `${maj}.${min + 1}.0`;
|
|
1546
|
+
else next = `${maj}.${min}.${pat + 1}`;
|
|
1547
|
+
pkg.version = next;
|
|
1548
|
+
writeUtf8(pkgFile, JSON.stringify(pkg, null, 2) + '\n');
|
|
1549
|
+
const hv = path.join(root, '.harness/HARNESS_VERSION');
|
|
1550
|
+
if (exists(hv) && /^\d+\.\d+\.\d+/.test(read(hv).trim())) writeUtf8(hv, next + '\n');
|
|
1551
|
+
ok(`version bumped: ${cur} → ${next} (${kind})`);
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
function releaseNote(root, text) {
|
|
1555
|
+
root = absRoot(root);
|
|
1556
|
+
if (!text) return fail('note text required (e.g., release note "내용")');
|
|
1557
|
+
const pkgFile = path.join(root, 'package.json');
|
|
1558
|
+
let version = 'unknown';
|
|
1559
|
+
if (exists(pkgFile)) { try { version = JSON.parse(read(pkgFile)).version || 'unknown'; } catch {} }
|
|
1560
|
+
const clFile = path.join(root, 'CHANGELOG.md');
|
|
1561
|
+
const date = today();
|
|
1562
|
+
const headerRe = new RegExp(`^## ${version.replace(/\./g, '\\.')} — `, 'm');
|
|
1563
|
+
if (exists(clFile)) {
|
|
1564
|
+
const cur = read(clFile);
|
|
1565
|
+
if (headerRe.test(cur)) {
|
|
1566
|
+
// 같은 버전 헤더가 있으면 그 바로 아래에 줄 추가
|
|
1567
|
+
const m = cur.match(headerRe);
|
|
1568
|
+
const headerEnd = cur.indexOf('\n', m.index + m[0].length);
|
|
1569
|
+
const insertPos = headerEnd + 1;
|
|
1570
|
+
// 헤더 다음 빈 줄 후 첫 list 시작 찾기
|
|
1571
|
+
const beforeBlock = cur.slice(insertPos);
|
|
1572
|
+
const linesAfter = beforeBlock.split('\n');
|
|
1573
|
+
// 가장 단순: 헤더 다음 줄에 즉시 - text 삽입
|
|
1574
|
+
writeUtf8(clFile, cur.slice(0, insertPos) + `\n- ${text}\n` + cur.slice(insertPos));
|
|
1575
|
+
} else {
|
|
1576
|
+
// 새 버전 헤더 추가 (# Changelog 다음)
|
|
1577
|
+
const top = cur.indexOf('# Changelog');
|
|
1578
|
+
const newBlock = `\n## ${version} — ${date}\n\n- ${text}\n`;
|
|
1579
|
+
if (top >= 0) {
|
|
1580
|
+
const after = cur.indexOf('\n', top) + 1;
|
|
1581
|
+
writeUtf8(clFile, cur.slice(0, after) + newBlock + cur.slice(after));
|
|
1582
|
+
} else {
|
|
1583
|
+
writeUtf8(clFile, `# Changelog\n${newBlock}\n${cur}`);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
} else {
|
|
1587
|
+
writeUtf8(clFile, `# Changelog\n\n## ${version} — ${date}\n\n- ${text}\n`);
|
|
1588
|
+
}
|
|
1589
|
+
ok(`CHANGELOG.md 갱신: [${version}] ${text}`);
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// 1.9.10: git remote 자동 감지 + gh-release + gh-pages 배포
|
|
1593
|
+
function detectGitRemote(root) {
|
|
1594
|
+
const r = cp.spawnSync('git', ['remote', 'get-url', 'origin'], { cwd: root, encoding: 'utf8', shell: true });
|
|
1595
|
+
if (r.status !== 0) return null;
|
|
1596
|
+
const url = (r.stdout || '').trim();
|
|
1597
|
+
if (!url) return null;
|
|
1598
|
+
// owner/repo 추출
|
|
1599
|
+
const m = url.match(/github\.com[:/]([^/]+)\/([^/.]+)(?:\.git)?/);
|
|
1600
|
+
return { url, host: m ? 'github' : 'unknown', owner: m ? m[1] : null, repo: m ? m[2] : null };
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
function getCurrentVersion(root) {
|
|
1604
|
+
const pkgF = path.join(root, 'package.json');
|
|
1605
|
+
if (!exists(pkgF)) return null;
|
|
1606
|
+
try { return JSON.parse(read(pkgF)).version || null; } catch { return null; }
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
function deployGhPages(root, sourceFile) {
|
|
1610
|
+
const remote = detectGitRemote(root);
|
|
1611
|
+
if (!remote || remote.host !== 'github') { fail('GitHub remote가 없습니다 — gh-pages 배포 불가'); process.exitCode = 1; return; }
|
|
1612
|
+
const src = path.resolve(root, sourceFile);
|
|
1613
|
+
if (!exists(src)) { fail(`소스 파일 없음: ${src}`); process.exitCode = 1; return; }
|
|
1614
|
+
log(`# gh-pages deploy`);
|
|
1615
|
+
log(`Source: ${rel(root, src)}`);
|
|
1616
|
+
log(`Target: gh-pages branch of ${remote.owner}/${remote.repo}`);
|
|
1617
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1618
|
+
const wt = path.join(root, '.harness/cache', `ghpages-${stamp}`);
|
|
1619
|
+
mkdirp(path.dirname(wt));
|
|
1620
|
+
// worktree (기존 gh-pages 있으면 fetch, 없으면 orphan)
|
|
1621
|
+
const fetchR = cp.spawnSync('git', ['fetch', 'origin', 'gh-pages'], { cwd: root, encoding: 'utf8', shell: true });
|
|
1622
|
+
const hasBranch = fetchR.status === 0;
|
|
1623
|
+
let wtArgs;
|
|
1624
|
+
if (hasBranch) wtArgs = ['worktree', 'add', wt, 'origin/gh-pages'];
|
|
1625
|
+
else wtArgs = ['worktree', 'add', '--orphan', '-b', 'gh-pages', wt];
|
|
1626
|
+
const wtR = cp.spawnSync('git', wtArgs, { cwd: root, encoding: 'utf8', shell: true });
|
|
1627
|
+
if (wtR.status !== 0) { fail('worktree 생성 실패: ' + (wtR.stderr || '').slice(0, 200)); process.exitCode = 1; return; }
|
|
1628
|
+
try {
|
|
1629
|
+
// orphan인 경우 초기화
|
|
1630
|
+
if (!hasBranch) {
|
|
1631
|
+
cp.spawnSync('git', ['rm', '-rf', '.'], { cwd: wt, encoding: 'utf8', shell: true });
|
|
1632
|
+
}
|
|
1633
|
+
// 소스 복사 (index.html로 이름 변경)
|
|
1634
|
+
const destName = path.basename(src) === 'index.html' ? 'index.html' : 'index.html';
|
|
1635
|
+
fs.copyFileSync(src, path.join(wt, destName));
|
|
1636
|
+
// 원본 파일명도 보존
|
|
1637
|
+
if (path.basename(src) !== 'index.html') fs.copyFileSync(src, path.join(wt, path.basename(src)));
|
|
1638
|
+
cp.spawnSync('git', ['add', '-A'], { cwd: wt, encoding: 'utf8' });
|
|
1639
|
+
const commit = cp.spawnSync('git', ['commit', '-m', `deploy: ${path.basename(src)} ${stamp}`], { cwd: wt, encoding: 'utf8' });
|
|
1640
|
+
if (commit.status !== 0 && !/nothing to commit/.test(commit.stdout || '')) {
|
|
1641
|
+
fail('commit 실패: ' + (commit.stdout || commit.stderr || '').slice(0, 200));
|
|
1642
|
+
process.exitCode = 1;
|
|
1643
|
+
} else {
|
|
1644
|
+
const pushR = cp.spawnSync('git', ['push', 'origin', 'gh-pages'], { cwd: wt, encoding: 'utf8' });
|
|
1645
|
+
if (pushR.status !== 0) { fail('push 실패: ' + (pushR.stderr || '').slice(0, 200)); process.exitCode = 1; }
|
|
1646
|
+
else ok(`gh-pages push 완료 → https://${remote.owner}.github.io/${remote.repo}/`);
|
|
1647
|
+
}
|
|
1648
|
+
} finally {
|
|
1649
|
+
cp.spawnSync('git', ['worktree', 'remove', '--force', wt], { cwd: root, encoding: 'utf8', shell: true });
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
function releasePublish(root) {
|
|
1654
|
+
root = absRoot(root);
|
|
1655
|
+
const dryRun = has('--dry-run');
|
|
1656
|
+
log('# release publish');
|
|
1657
|
+
log(`Mode: ${dryRun ? 'dry-run' : 'live'}`);
|
|
1658
|
+
|
|
1659
|
+
// 1. git remote 자동 감지 (1.9.10)
|
|
1660
|
+
const remote = detectGitRemote(root);
|
|
1661
|
+
if (remote) log(`Git remote (origin): ${remote.host === 'github' ? `${remote.owner}/${remote.repo}` : remote.url}`);
|
|
1662
|
+
else log('Git remote: 없음');
|
|
1663
|
+
|
|
1664
|
+
// 2. npm pack (필요한 경우 — pack-only도 의미 있음)
|
|
1665
|
+
if (has('--pack') || has('--npm-publish') || (!has('--git-push') && !has('--gh-release') && !has('--gh-pages'))) {
|
|
1666
|
+
const packR = cp.spawnSync('npm', ['pack'], { cwd: root, encoding: 'utf8', shell: true });
|
|
1667
|
+
if (packR.status !== 0) { fail('npm pack 실패'); log(packR.stderr); process.exitCode = 1; return; }
|
|
1668
|
+
ok('npm pack 완료');
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// 3. git push (--git-push 또는 --auto + remote 있을 때)
|
|
1672
|
+
if (has('--git-push') || (has('--auto') && remote)) {
|
|
1673
|
+
log('git push:');
|
|
1674
|
+
const r1 = cp.spawnSync('git', ['push'], { cwd: root, encoding: 'utf8', shell: true });
|
|
1675
|
+
log((r1.stdout || r1.stderr || '').slice(-200) || '(no output)');
|
|
1676
|
+
const r2 = cp.spawnSync('git', ['push', '--tags'], { cwd: root, encoding: 'utf8', shell: true });
|
|
1677
|
+
log((r2.stdout || r2.stderr || '').slice(-200) || '(no output)');
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// 4. GitHub Release (--gh-release, gh CLI 사용)
|
|
1681
|
+
if (has('--gh-release')) {
|
|
1682
|
+
if (!remote || remote.host !== 'github') { warn('--gh-release: GitHub remote 없음 — 스킵'); }
|
|
1683
|
+
else {
|
|
1684
|
+
const v = getCurrentVersion(root);
|
|
1685
|
+
if (!v) { warn('--gh-release: package.json#version 없음 — 스킵'); }
|
|
1686
|
+
else {
|
|
1687
|
+
const tag = `v${v}`;
|
|
1688
|
+
const ghArgs = ['release', 'create', tag, '--generate-notes', '--title', `${remote.repo} ${tag}`];
|
|
1689
|
+
const tarball = path.join(root, `${JSON.parse(read(path.join(root, 'package.json'))).name}-${v}.tgz`);
|
|
1690
|
+
if (exists(tarball)) ghArgs.push(tarball);
|
|
1691
|
+
log(`gh ${ghArgs.join(' ')}`);
|
|
1692
|
+
const ghR = cp.spawnSync('gh', ghArgs, { cwd: root, encoding: 'utf8', shell: true });
|
|
1693
|
+
log((ghR.stdout || ghR.stderr || '').slice(-300) || '(no output)');
|
|
1694
|
+
if (ghR.status !== 0) warn('gh release 생성 실패 (이미 존재할 수 있음)');
|
|
1695
|
+
else ok(`GitHub Release 생성: ${tag}`);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// 5. gh-pages 배포 (--gh-pages)
|
|
1701
|
+
if (has('--gh-pages')) {
|
|
1702
|
+
const src = arg('--gh-pages-src', null) || arg('--roadmap', null) || 'roadmap.html';
|
|
1703
|
+
deployGhPages(root, src);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// 6. npm publish (--npm-publish)
|
|
1707
|
+
if (has('--npm-publish')) {
|
|
1708
|
+
const args = dryRun ? ['publish', '--dry-run'] : ['publish', '--access', 'public'];
|
|
1709
|
+
log('npm ' + args.join(' '));
|
|
1710
|
+
const r = cp.spawnSync('npm', args, { cwd: root, encoding: 'utf8', shell: true });
|
|
1711
|
+
log((r.stdout || '').split('\n').slice(-5).join('\n'));
|
|
1712
|
+
if (r.status !== 0) { fail('npm publish 실패'); process.exitCode = 1; return; }
|
|
1713
|
+
}
|
|
1714
|
+
ok('release publish 완료');
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1237
1717
|
// ===== 1.9.7 A: verify-code — npm scripts 자동 감지 + evidence 자동 기록 =====
|
|
1238
1718
|
function verifyCodeCmd(root) {
|
|
1239
1719
|
root = absRoot(root);
|
|
@@ -1754,7 +2234,12 @@ function help() {
|
|
|
1754
2234
|
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
2235
|
leerness verify-code [path] [--build] # npm test/lint/typecheck 자동 실행 + evidence 자동 기록 (1.9.7)
|
|
1756
2236
|
leerness lessons [--query <키>] [--limit N] # 과거 결정/실수 자동 회수 (1.9.7)
|
|
1757
|
-
leerness lazy detect [path] [--auto-track] # --auto-track으로 새 TODO를 progress에 자동 등록 (1.9.7)
|
|
2237
|
+
leerness lazy detect [path] [--auto-track] # --auto-track으로 새 TODO를 progress에 자동 등록 (1.9.7)
|
|
2238
|
+
leerness rule add "<설명>" --trigger every-session|every-update|every-commit|session-start|session-close|pre-publish # 사용자 룰 등록 (1.9.8)
|
|
2239
|
+
leerness rule list|verify|pause <id>|resume <id>|remove <id>|stop|resume-all
|
|
2240
|
+
leerness release bump [--patch|--minor|--major] # package.json 자동 bump (1.9.8)
|
|
2241
|
+
leerness release note "<내용>" # CHANGELOG.md 자동 추가 (1.9.8)
|
|
2242
|
+
leerness release publish [--dry-run] [--pack] [--git-push] [--gh-release] [--gh-pages] [--gh-pages-src file] [--npm-publish] [--auto] # 통합 배포 (1.9.8 + 1.9.10)\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
2243
|
}
|
|
1759
2244
|
|
|
1760
2245
|
async function main() {
|
|
@@ -1795,6 +2280,17 @@ async function main() {
|
|
|
1795
2280
|
if (cmd === 'gate') return gate(args[1] || process.cwd());
|
|
1796
2281
|
if (cmd === 'verify-code') return verifyCodeCmd(args[1] || process.cwd());
|
|
1797
2282
|
if (cmd === 'lessons') return lessonsCmd(arg('--path', process.cwd()));
|
|
2283
|
+
if (cmd === 'rule' && args[1] === 'add') return ruleAdd(arg('--path', process.cwd()), args.slice(2).filter(x => !x.startsWith('-')).join(' '));
|
|
2284
|
+
if (cmd === 'rule' && args[1] === 'list') return ruleList(arg('--path', process.cwd()));
|
|
2285
|
+
if (cmd === 'rule' && args[1] === 'remove') return ruleRemove(arg('--path', process.cwd()), args[2]);
|
|
2286
|
+
if (cmd === 'rule' && args[1] === 'pause') return rulePause(arg('--path', process.cwd()), args[2]);
|
|
2287
|
+
if (cmd === 'rule' && args[1] === 'resume') return ruleResume(arg('--path', process.cwd()), args[2]);
|
|
2288
|
+
if (cmd === 'rule' && args[1] === 'stop') return ruleStop(arg('--path', process.cwd()));
|
|
2289
|
+
if (cmd === 'rule' && args[1] === 'resume-all') return ruleResumeAll(arg('--path', process.cwd()));
|
|
2290
|
+
if (cmd === 'rule' && args[1] === 'verify') return ruleVerifyCmd(arg('--path', process.cwd()));
|
|
2291
|
+
if (cmd === 'release' && args[1] === 'bump') return releaseBump(args[2] || arg('--path', process.cwd()));
|
|
2292
|
+
if (cmd === 'release' && args[1] === 'note') return releaseNote(arg('--path', process.cwd()), args.slice(2).filter(x => !x.startsWith('-')).join(' '));
|
|
2293
|
+
if (cmd === 'release' && args[1] === 'publish') return releasePublish(args[2] || arg('--path', process.cwd()));
|
|
1798
2294
|
if (cmd === 'impact') return impactCmd(arg('--path', process.cwd()), args[1]);
|
|
1799
2295
|
if (cmd === 'reuse' && args[1] === 'find') return reuseFind(arg('--path', process.cwd()), args.slice(2).filter(x => !x.startsWith('-')).join(' '));
|
|
1800
2296
|
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,132 @@ total++;
|
|
|
235
235
|
if (!(strongOK && weakHint)) failed++;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
// 1.9.10 A: skillpack 동적 로드 (LEERNESS_SKILLPACK_PATH로 시뮬)
|
|
239
|
+
total++;
|
|
240
|
+
{
|
|
241
|
+
const skillpackDir = path.resolve(__dirname, '..', '..', 'leerness-skillpack');
|
|
242
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'skill', 'list', '--path', tmp], {
|
|
243
|
+
encoding: 'utf8',
|
|
244
|
+
env: Object.assign({}, process.env, { LEERNESS_SKILLPACK_PATH: skillpackDir })
|
|
245
|
+
});
|
|
246
|
+
const ok = r.status === 0 && /skillpack 출처: env/.test(r.stdout) && /\| skillpack \|/.test(r.stdout);
|
|
247
|
+
console.log(ok ? '✓ B(1.9.10) skillpack 동적 로드 (env path)' : '✗ skillpack 로드 실패');
|
|
248
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 800)); }
|
|
249
|
+
}
|
|
250
|
+
// 1.9.10 A: skillpack 없을 때 builtin fallback
|
|
251
|
+
total++;
|
|
252
|
+
{
|
|
253
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'skill', 'list', '--path', tmp], {
|
|
254
|
+
encoding: 'utf8',
|
|
255
|
+
env: Object.assign({}, process.env, { LEERNESS_SKILLPACK_PATH: '' })
|
|
256
|
+
});
|
|
257
|
+
const ok = r.status === 0 && /builtin fallback/.test(r.stdout) && /\| builtin \|/.test(r.stdout);
|
|
258
|
+
console.log(ok ? '✓ B(1.9.10) builtin fallback (skillpack 없을 때)' : '✗ builtin fallback 실패');
|
|
259
|
+
if (!ok) failed++;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 1.9.10 B: detectGitRemote (가짜 git remote 시뮬은 어려움 — 실제 git 명령으로 확인)
|
|
263
|
+
total++;
|
|
264
|
+
{
|
|
265
|
+
// tmp는 git init이 없음 → detectGitRemote는 null → publish 호출 시 'Git remote: 없음' 출력
|
|
266
|
+
// 시뮬: tmp에 git init + remote add
|
|
267
|
+
cp.spawnSync('git', ['init'], { cwd: tmp, encoding: 'utf8', shell: true });
|
|
268
|
+
cp.spawnSync('git', ['remote', 'add', 'origin', 'https://github.com/test/repo.git'], { cwd: tmp, encoding: 'utf8', shell: true });
|
|
269
|
+
// package.json도 필요
|
|
270
|
+
if (!fs.existsSync(path.join(tmp, 'package.json'))) {
|
|
271
|
+
fs.writeFileSync(path.join(tmp, 'package.json'), JSON.stringify({ name: 'e2e-test', version: '0.1.0' }));
|
|
272
|
+
}
|
|
273
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'release', 'publish', tmp, '--dry-run'], { encoding: 'utf8' });
|
|
274
|
+
const ok = /Git remote \(origin\): test\/repo/.test(r.stdout);
|
|
275
|
+
console.log(ok ? '✓ B(1.9.10) detectGitRemote: github owner/repo 추출' : `✗ remote 감지 실패\n${r.stdout.slice(0, 500)}`);
|
|
276
|
+
if (!ok) failed++;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 1.9.8: rule add/list/pause/resume/remove
|
|
280
|
+
total++;
|
|
281
|
+
{
|
|
282
|
+
const r1 = cp.spawnSync(process.execPath, [CLI, 'rule', 'add', '매 업데이트마다 버전 patch bump', '--trigger', 'every-update', '--path', tmp], { encoding: 'utf8' });
|
|
283
|
+
const r2 = cp.spawnSync(process.execPath, [CLI, 'rule', 'add', '매 업데이트마다 패치노트 추가', '--trigger', 'every-update', '--path', tmp], { encoding: 'utf8' });
|
|
284
|
+
const r3 = cp.spawnSync(process.execPath, [CLI, 'rule', 'add', '세션 종료마다 배포', '--trigger', 'session-close', '--path', tmp], { encoding: 'utf8' });
|
|
285
|
+
const rl = cp.spawnSync(process.execPath, [CLI, 'rule', 'list', '--path', tmp], { encoding: 'utf8' });
|
|
286
|
+
const ok = r1.status === 0 && r2.status === 0 && r3.status === 0 && /R-0001/.test(rl.stdout) && /R-0003/.test(rl.stdout);
|
|
287
|
+
console.log(ok ? '✓ B(1.9.8) rule add/list: 3개 등록' : '✗ B(1.9.8) rule add/list 실패');
|
|
288
|
+
if (!ok) failed++;
|
|
289
|
+
}
|
|
290
|
+
total++;
|
|
291
|
+
{
|
|
292
|
+
// pause + handoff에서 paused는 안 보여야 함
|
|
293
|
+
cp.spawnSync(process.execPath, [CLI, 'rule', 'pause', 'R-0001', '--path', tmp], { encoding: 'utf8' });
|
|
294
|
+
const hr = cp.spawnSync(process.execPath, [CLI, 'handoff', tmp], { encoding: 'utf8' });
|
|
295
|
+
const ok = /Active User Rules/.test(hr.stdout) && /R-0002/.test(hr.stdout) && /R-0003/.test(hr.stdout) && !/R-0001 \[/.test(hr.stdout);
|
|
296
|
+
console.log(ok ? '✓ B(1.9.8) handoff: paused 룰 제외, active만 출력' : '✗ B(1.9.8) handoff 출력 실패');
|
|
297
|
+
if (!ok) { failed++; console.log(hr.stdout); }
|
|
298
|
+
cp.spawnSync(process.execPath, [CLI, 'rule', 'resume', 'R-0001', '--path', tmp], { encoding: 'utf8' });
|
|
299
|
+
}
|
|
300
|
+
total++;
|
|
301
|
+
{
|
|
302
|
+
// rule stop / resume-all
|
|
303
|
+
cp.spawnSync(process.execPath, [CLI, 'rule', 'stop', '--path', tmp], { encoding: 'utf8' });
|
|
304
|
+
const rl = cp.spawnSync(process.execPath, [CLI, 'rule', 'list', '--path', tmp], { encoding: 'utf8' });
|
|
305
|
+
const allPaused = (rl.stdout.match(/\| paused \|/g) || []).length >= 3;
|
|
306
|
+
cp.spawnSync(process.execPath, [CLI, 'rule', 'resume-all', '--path', tmp], { encoding: 'utf8' });
|
|
307
|
+
const rl2 = cp.spawnSync(process.execPath, [CLI, 'rule', 'list', '--path', tmp], { encoding: 'utf8' });
|
|
308
|
+
const allActive = (rl2.stdout.match(/\| active \|/g) || []).length >= 3;
|
|
309
|
+
console.log(allPaused && allActive ? '✓ B(1.9.8) rule stop / resume-all: 일괄 전환' : '✗ B(1.9.8) stop/resume-all 실패');
|
|
310
|
+
if (!(allPaused && allActive)) failed++;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// 1.9.8: release bump
|
|
314
|
+
total++;
|
|
315
|
+
{
|
|
316
|
+
const tmpR = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-rel-'));
|
|
317
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpR, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore' });
|
|
318
|
+
fs.writeFileSync(path.join(tmpR, 'package.json'), JSON.stringify({ name: 't', version: '1.0.0' }));
|
|
319
|
+
cp.spawnSync(process.execPath, [CLI, 'release', 'bump', '--patch', '--path', tmpR], { encoding: 'utf8' });
|
|
320
|
+
let v = JSON.parse(fs.readFileSync(path.join(tmpR, 'package.json'), 'utf8')).version;
|
|
321
|
+
const okPatch = v === '1.0.1';
|
|
322
|
+
cp.spawnSync(process.execPath, [CLI, 'release', 'bump', '--minor', '--path', tmpR], { encoding: 'utf8' });
|
|
323
|
+
v = JSON.parse(fs.readFileSync(path.join(tmpR, 'package.json'), 'utf8')).version;
|
|
324
|
+
const okMinor = v === '1.1.0';
|
|
325
|
+
cp.spawnSync(process.execPath, [CLI, 'release', 'bump', '--major', '--path', tmpR], { encoding: 'utf8' });
|
|
326
|
+
v = JSON.parse(fs.readFileSync(path.join(tmpR, 'package.json'), 'utf8')).version;
|
|
327
|
+
const okMajor = v === '2.0.0';
|
|
328
|
+
console.log(okPatch && okMinor && okMajor ? '✓ B(1.9.8) release bump: patch/minor/major' : `✗ B(1.9.8) bump 실패 final=${v}`);
|
|
329
|
+
if (!(okPatch && okMinor && okMajor)) failed++;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// 1.9.8: release note → CHANGELOG.md 자동 갱신
|
|
333
|
+
total++;
|
|
334
|
+
{
|
|
335
|
+
const tmpN = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-note-'));
|
|
336
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpN, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore' });
|
|
337
|
+
fs.writeFileSync(path.join(tmpN, 'package.json'), JSON.stringify({ name: 't', version: '0.1.0' }));
|
|
338
|
+
cp.spawnSync(process.execPath, [CLI, 'release', 'note', '첫 기능 추가', '--path', tmpN], { encoding: 'utf8' });
|
|
339
|
+
const cl = fs.readFileSync(path.join(tmpN, 'CHANGELOG.md'), 'utf8');
|
|
340
|
+
const ok = /## 0\.1\.0/.test(cl) && /첫 기능 추가/.test(cl);
|
|
341
|
+
console.log(ok ? '✓ B(1.9.8) release note: CHANGELOG 자동 갱신' : '✗ B(1.9.8) release note 실패');
|
|
342
|
+
if (!ok) failed++;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// 1.9.8: session close가 rule verification 보고
|
|
346
|
+
total++;
|
|
347
|
+
{
|
|
348
|
+
// tmp는 위에서 rule 3개 등록됨
|
|
349
|
+
// package.json 만들기 + 버전 변경 시뮬 (rule R-0001은 every-update 버전 룰)
|
|
350
|
+
fs.writeFileSync(path.join(tmp, 'package.json'), JSON.stringify({ name: 'tmp-e2e', version: '0.1.0' }));
|
|
351
|
+
// 첫 session close — baseline 캡처
|
|
352
|
+
cp.spawnSync(process.execPath, [CLI, 'session', 'close', tmp], { encoding: 'utf8' });
|
|
353
|
+
// 버전 bump
|
|
354
|
+
cp.spawnSync(process.execPath, [CLI, 'release', 'bump', '--patch', '--path', tmp], { encoding: 'utf8' });
|
|
355
|
+
// CHANGELOG 갱신
|
|
356
|
+
cp.spawnSync(process.execPath, [CLI, 'release', 'note', 'e2e 검증 항목 추가', '--path', tmp], { encoding: 'utf8' });
|
|
357
|
+
// 두 번째 session close — 변경 감지
|
|
358
|
+
const sc = cp.spawnSync(process.execPath, [CLI, 'session', 'close', tmp], { encoding: 'utf8' });
|
|
359
|
+
const ok = /User Rules verification/.test(sc.stdout) && /✓ pass/.test(sc.stdout);
|
|
360
|
+
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')}`);
|
|
361
|
+
if (!ok) failed++;
|
|
362
|
+
}
|
|
363
|
+
|
|
238
364
|
// 1.9.7 A: verify-code — 가짜 package.json + 통과 시나리오
|
|
239
365
|
total++;
|
|
240
366
|
{
|