leerness 1.9.6 → 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 +83 -0
- package/bin/harness.js +493 -10
- package/package.json +1 -1
- package/scripts/e2e.js +173 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,88 @@
|
|
|
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
|
+
|
|
61
|
+
## 1.9.7 — 2026-05-08
|
|
62
|
+
|
|
63
|
+
코드 검증 자동 실행 + 과거 결정/실수 자동 회수 + TODO 자동 추적의 3종 자동화.
|
|
64
|
+
|
|
65
|
+
### Added — A. `leerness verify-code [path] [--build]`
|
|
66
|
+
|
|
67
|
+
`package.json#scripts`에서 `test` / `lint` / `typecheck` (또는 `tsc`) / (선택) `build`를 자동 감지해 차례로 실행. 결과는 모두 `review-evidence.md`에 자동 누적 (`Command/Tasks/exit/duration/tail`). 실패 시 `process.exit(1)` + progress의 in-progress row를 `incomplete`로 표시 권장 안내.
|
|
68
|
+
|
|
69
|
+
- `tsconfig.json`이 있고 `typecheck` script가 없으면 `npx tsc --noEmit` 자동 호출.
|
|
70
|
+
- 5분 timeout 내장 (장기 실행 방지).
|
|
71
|
+
|
|
72
|
+
### Added — B. `leerness lessons [--query <키>] [--limit N]`
|
|
73
|
+
|
|
74
|
+
`decisions.md`의 모든 `### 블록`, `review-evidence.md`의 실패 표지(`✗ / fail / 롤백 / incomplete / bug / 버그 / warning`) 블록, `task-log.md`의 실패 키워드 라인, `session-handoff.md`의 Incomplete 섹션을 통합 추출. `--query`로 키워드 필터.
|
|
75
|
+
|
|
76
|
+
- `leerness guide [target]`이 자동으로 lessons 섹션을 추가 (target 이름을 query로 사용).
|
|
77
|
+
|
|
78
|
+
### Added — C. `lazy detect --auto-track` + `.harness/known-todos.json`
|
|
79
|
+
|
|
80
|
+
새 TODO/FIXME/XXX의 `(file, line, text)` 위치 캡처. `known-todos.json`에 acknowledged 기록을 비교해 매번 같은 false positive를 줄이고, 새로 발견된 것만 노출. `--auto-track`으로 `progress-tracker.md`에 `T-XXXX requested`로 자동 등록 + known-todos.json에도 자동 추가.
|
|
81
|
+
|
|
82
|
+
### Migration
|
|
83
|
+
|
|
84
|
+
기존 1.9.x 사용자는 `npx leerness@latest update . --yes`로 즉시 자동 마이그레이션됩니다.
|
|
85
|
+
|
|
3
86
|
## 1.9.6 — 2026-05-08
|
|
4
87
|
|
|
5
88
|
1.9.5 후 발견된 한 가지 한계 (옛 link 손실 자동 복구 부재)를 패치.
|
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`),
|
|
@@ -1003,7 +1004,13 @@ function lazyDetect(root) {
|
|
|
1003
1004
|
const bq = (pre.match(/(?<!\\)`/g) || []).length;
|
|
1004
1005
|
return (sq % 2 === 1) || (dq % 2 === 1) || (bq % 2 === 1);
|
|
1005
1006
|
}
|
|
1007
|
+
// 1.9.7 C: TODO 자동 추적 강화 — 위치+텍스트 캡처, known-todos 비교, --auto-track 등록
|
|
1008
|
+
const knownPath = path.join(root, '.harness/known-todos.json');
|
|
1009
|
+
let knownList = [];
|
|
1010
|
+
if (exists(knownPath)) { try { knownList = JSON.parse(read(knownPath)); } catch {} }
|
|
1011
|
+
const knownSet = new Set(knownList.map(k => `${k.file}:${k.line}:${k.text}`));
|
|
1006
1012
|
let todoCount = 0;
|
|
1013
|
+
const newTodos = [];
|
|
1007
1014
|
const cliSelf = path.resolve(__filename);
|
|
1008
1015
|
for (const file of walk(root)) {
|
|
1009
1016
|
const ext = path.extname(file).toLowerCase();
|
|
@@ -1014,17 +1021,39 @@ function lazyDetect(root) {
|
|
|
1014
1021
|
let text; try { text = read(file); } catch { continue; }
|
|
1015
1022
|
const lines = text.split('\n');
|
|
1016
1023
|
const tre = /\bTODO\b|\bFIXME\b|\bXXX\b/g;
|
|
1017
|
-
for (
|
|
1024
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1018
1025
|
tre.lastIndex = 0;
|
|
1019
1026
|
let m;
|
|
1020
|
-
while ((m = tre.exec(
|
|
1021
|
-
if (
|
|
1027
|
+
while ((m = tre.exec(lines[i]))) {
|
|
1028
|
+
if (isInsideQuote(lines[i], m.index)) continue;
|
|
1029
|
+
todoCount++;
|
|
1030
|
+
const txt = lines[i].trim().slice(0, 120);
|
|
1031
|
+
const fileRel = rel(root, file);
|
|
1032
|
+
const key = `${fileRel}:${i + 1}:${txt}`;
|
|
1033
|
+
if (!knownSet.has(key)) newTodos.push({ file: fileRel, line: i + 1, text: txt });
|
|
1022
1034
|
}
|
|
1023
1035
|
}
|
|
1024
1036
|
}
|
|
1025
1037
|
if (todoCount > 0) {
|
|
1026
1038
|
const hasTodoTask = rows.some(r => /TODO|FIXME|XXX/.test(r.request) || /TODO|FIXME|XXX/i.test(r.evidence));
|
|
1027
|
-
if (!hasTodoTask) {
|
|
1039
|
+
if (!hasTodoTask) {
|
|
1040
|
+
issues++;
|
|
1041
|
+
warn(`code has ${todoCount} TODO/FIXME/XXX (new: ${newTodos.length}) but no progress-tracker entry tracks them`);
|
|
1042
|
+
// 새 TODO 처음 5개 표시
|
|
1043
|
+
newTodos.slice(0, 5).forEach(t => log(` ${t.file}:${t.line} ${t.text}`));
|
|
1044
|
+
if (has('--auto-track') && newTodos.length) {
|
|
1045
|
+
for (const t of newTodos) {
|
|
1046
|
+
const id = nextId(root, 'T');
|
|
1047
|
+
upsertProgress(root, { id, status: 'requested', request: `TODO ${t.file}:${t.line}`, evidence: 'auto-tracked', nextAction: t.text.slice(0, 80) });
|
|
1048
|
+
}
|
|
1049
|
+
// known-todos에 추가 — 다음 detect에서 재카운트 안 하도록
|
|
1050
|
+
const merged = [...knownList, ...newTodos.map(t => ({ ...t, ackAt: now() }))];
|
|
1051
|
+
writeUtf8(knownPath, JSON.stringify(merged, null, 2) + '\n');
|
|
1052
|
+
ok(`${newTodos.length}개 TODO를 progress-tracker에 자동 등록 + known-todos.json 갱신`);
|
|
1053
|
+
} else if (newTodos.length) {
|
|
1054
|
+
log(` 💡 자동 등록: leerness lazy detect --auto-track`);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1028
1057
|
}
|
|
1029
1058
|
const blockers = rows.filter(r => r.status === 'blocked');
|
|
1030
1059
|
for (const b of blockers) if (b.nextAction === '없음' || /다음 액션 작성/.test(b.nextAction)) { issues++; warn(`blocker without nextAction: ${b.id}`); }
|
|
@@ -1083,6 +1112,14 @@ function handoff(root) {
|
|
|
1083
1112
|
log('# Session Start Context');
|
|
1084
1113
|
log(`Date: ${today()}`);
|
|
1085
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
|
+
}
|
|
1086
1123
|
log(out);
|
|
1087
1124
|
if (exists(currentStatePath(root))) {
|
|
1088
1125
|
const cs = read(currentStatePath(root)).replace(/Updated: \d{4}-\d{2}-\d{2}/, `Updated: ${today()}`);
|
|
@@ -1155,8 +1192,18 @@ function sessionClose(root) {
|
|
|
1155
1192
|
log(`\n### ${s}`);
|
|
1156
1193
|
log(rowsToList(buckets[s]));
|
|
1157
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
|
+
}
|
|
1158
1205
|
log('\n## Required final response sections');
|
|
1159
|
-
log('- 완료 작업\n- 진행 중 작업\n- 미완료/예정/대기/보류/차단/드랍 작업\n- 검증 결과\n- 추천 방향\n- 다음 정확한
|
|
1206
|
+
log('- 완료 작업\n- 진행 중 작업\n- 미완료/예정/대기/보류/차단/드랍 작업\n- 검증 결과\n- 추천 방향\n- 다음 정확한 작업\n- ⚡ 활성 룰별 검증 결과');
|
|
1160
1207
|
ok(`session-handoff.md and current-state.md updated`);
|
|
1161
1208
|
}
|
|
1162
1209
|
|
|
@@ -1206,6 +1253,414 @@ function gate(root) {
|
|
|
1206
1253
|
else ok('all gates passed');
|
|
1207
1254
|
}
|
|
1208
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
|
+
|
|
1556
|
+
// ===== 1.9.7 A: verify-code — npm scripts 자동 감지 + evidence 자동 기록 =====
|
|
1557
|
+
function verifyCodeCmd(root) {
|
|
1558
|
+
root = absRoot(root);
|
|
1559
|
+
const pkgFile = path.join(root, 'package.json');
|
|
1560
|
+
if (!exists(pkgFile)) return fail('package.json 없음 — Node 프로젝트 위치에서 실행하세요.');
|
|
1561
|
+
let pkg;
|
|
1562
|
+
try { pkg = JSON.parse(read(pkgFile)); } catch (e) { return fail('package.json 파싱 실패: ' + e.message); }
|
|
1563
|
+
const scripts = pkg.scripts || {};
|
|
1564
|
+
const tasks = [];
|
|
1565
|
+
if (scripts.test) tasks.push({ name: 'test', cmd: 'npm test' });
|
|
1566
|
+
else if (scripts['test:smoke']) tasks.push({ name: 'test', cmd: 'npm run test:smoke' });
|
|
1567
|
+
if (scripts.lint) tasks.push({ name: 'lint', cmd: 'npm run lint' });
|
|
1568
|
+
if (scripts.typecheck) tasks.push({ name: 'typecheck', cmd: 'npm run typecheck' });
|
|
1569
|
+
else if (scripts.tsc) tasks.push({ name: 'typecheck', cmd: 'npm run tsc' });
|
|
1570
|
+
else if (exists(path.join(root, 'tsconfig.json'))) tasks.push({ name: 'typecheck', cmd: 'npx --yes tsc --noEmit', optional: true });
|
|
1571
|
+
if (has('--build') && scripts.build) tasks.push({ name: 'build', cmd: 'npm run build' });
|
|
1572
|
+
if (!tasks.length) {
|
|
1573
|
+
warn('실행할 검증 task 없음 (package.json#scripts에 test/lint/typecheck 추가하세요)');
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
log(`# verify-code (${tasks.length}개)`);
|
|
1577
|
+
let failedCnt = 0;
|
|
1578
|
+
const results = [];
|
|
1579
|
+
for (const t of tasks) {
|
|
1580
|
+
log(`\n## ${t.name}: ${t.cmd}`);
|
|
1581
|
+
const start = Date.now();
|
|
1582
|
+
const r = cp.spawnSync(t.cmd, [], { cwd: root, encoding: 'utf8', shell: true, timeout: 5 * 60 * 1000 });
|
|
1583
|
+
const dur = Date.now() - start;
|
|
1584
|
+
if (r.status === 0) ok(`${t.name} passed (${dur}ms)`);
|
|
1585
|
+
else if (t.optional && r.status === 127) warn(`${t.name} 스킵 (${t.cmd} 없음)`);
|
|
1586
|
+
else { fail(`${t.name} failed (exit ${r.status}, ${dur}ms)`); failedCnt++; }
|
|
1587
|
+
const tail = (r.stdout || '').split('\n').slice(-8).join('\n').slice(0, 400);
|
|
1588
|
+
results.push({ name: t.name, cmd: t.cmd, exit: r.status, durMs: dur, tail });
|
|
1589
|
+
}
|
|
1590
|
+
const evBlock = [
|
|
1591
|
+
``,
|
|
1592
|
+
`## ${now().slice(0, 16)} verify-code (자동)`,
|
|
1593
|
+
`Command: leerness verify-code`,
|
|
1594
|
+
`Tasks: ${tasks.map(t => t.name).join(', ')}`,
|
|
1595
|
+
...results.map(r => `- ${r.name}: exit=${r.exit} (${r.durMs}ms) — \`${r.cmd}\``),
|
|
1596
|
+
`Tail:`,
|
|
1597
|
+
'```',
|
|
1598
|
+
results.map(r => `[${r.name}]\n${r.tail}`).join('\n---\n').slice(0, 1500),
|
|
1599
|
+
'```'
|
|
1600
|
+
].join('\n');
|
|
1601
|
+
append(evidencePath(root), evBlock + '\n');
|
|
1602
|
+
ok(`evidence 기록: .harness/review-evidence.md`);
|
|
1603
|
+
if (failedCnt) { process.exitCode = 1; warn(`${failedCnt}개 task 실패 — progress의 해당 row를 incomplete로 표시하세요.`); }
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
// ===== 1.9.7 B: lessons — 과거 결정/실수 자동 회수 =====
|
|
1607
|
+
function lessonsCmd(root) {
|
|
1608
|
+
root = absRoot(root);
|
|
1609
|
+
const query = arg('--query', null);
|
|
1610
|
+
const limit = parseInt(arg('--limit', '10'), 10);
|
|
1611
|
+
const decisions = exists(decisionsPath(root)) ? read(decisionsPath(root)) : '';
|
|
1612
|
+
const evidence = exists(evidencePath(root)) ? read(evidencePath(root)) : '';
|
|
1613
|
+
const tlog = exists(taskLogPath(root)) ? read(taskLogPath(root)) : '';
|
|
1614
|
+
const handoff = exists(handoffPath(root)) ? read(handoffPath(root)) : '';
|
|
1615
|
+
const lessons = [];
|
|
1616
|
+
// decisions: ### 블록 전체
|
|
1617
|
+
for (const block of decisions.split(/\n(?=### )/)) {
|
|
1618
|
+
if (!block.startsWith('### ')) continue;
|
|
1619
|
+
const m = block.match(/^### (.+)$/m);
|
|
1620
|
+
if (!m) continue;
|
|
1621
|
+
lessons.push({ source: 'decisions.md', title: m[1].trim(), block });
|
|
1622
|
+
}
|
|
1623
|
+
// evidence: ## 블록 중 실패/롤백/버그 표지가 있는 것
|
|
1624
|
+
for (const block of evidence.split(/\n(?=## )/)) {
|
|
1625
|
+
if (!block.startsWith('## ')) continue;
|
|
1626
|
+
if (/✗|\bfail(ed)?\b|롤백|재발|incomplete|\bbug\b|버그|warning/i.test(block)) {
|
|
1627
|
+
const m = block.match(/^## (.+)$/m);
|
|
1628
|
+
if (m) lessons.push({ source: 'review-evidence.md', title: m[1].trim(), block });
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
// task-log: 실패 키워드 라인
|
|
1632
|
+
for (const line of tlog.split('\n')) {
|
|
1633
|
+
if (line.length > 4 && /✗|\bfail|롤백|재발|incomplete|버그/i.test(line)) {
|
|
1634
|
+
lessons.push({ source: 'task-log.md', title: line.replace(/^[-*]\s*/, '').slice(0, 80), block: line });
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
// handoff: 미완료/블로커 항목
|
|
1638
|
+
if (handoff) {
|
|
1639
|
+
const incompleteSec = handoff.match(/## Incomplete[\s\S]*?(?=\n## |$)/);
|
|
1640
|
+
if (incompleteSec && incompleteSec[0].split('\n').slice(1).some(l => /^- (?!없음)/.test(l))) {
|
|
1641
|
+
lessons.push({ source: 'session-handoff.md', title: 'Incomplete / Blocked from last session', block: incompleteSec[0] });
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
let filtered = lessons;
|
|
1645
|
+
if (query) {
|
|
1646
|
+
const q = new RegExp(escapeRegex(query), 'i');
|
|
1647
|
+
filtered = lessons.filter(l => q.test(l.title) || q.test(l.block));
|
|
1648
|
+
}
|
|
1649
|
+
log(`# Lessons${query ? ` — query="${query}"` : ''}`);
|
|
1650
|
+
if (!filtered.length) {
|
|
1651
|
+
if (query) ok(`"${query}" 관련 과거 lessons 없음`);
|
|
1652
|
+
else ok('과거 lessons 없음 (decisions/evidence가 비어있거나 실패 표지 없음)');
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
log(`총 ${filtered.length}건 발견:`);
|
|
1656
|
+
for (const l of filtered.slice(0, limit)) {
|
|
1657
|
+
log(`\n[${l.source}] ${l.title}`);
|
|
1658
|
+
const preview = l.block.replace(/\n+/g, ' ').replace(/\s{2,}/g, ' ').slice(0, 240);
|
|
1659
|
+
log(` ${preview}${l.block.length > 240 ? '…' : ''}`);
|
|
1660
|
+
}
|
|
1661
|
+
if (filtered.length > limit) log(`\n💡 ${filtered.length - limit}개 더 있음 — --limit ${filtered.length}`);
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1209
1664
|
// ===== 1.9.3: Causal / reuse / consistency =====
|
|
1210
1665
|
const CODE_EXT = new Set(['.js','.ts','.jsx','.tsx','.mjs','.cjs','.css','.scss','.sass','.less','.html','.htm','.vue','.svelte','.md','.json','.py','.rb','.go','.rs','.java','.kt','.swift','.cs','.php']);
|
|
1211
1666
|
function* walkCode(root, base = root, depth = 0, extras = null) {
|
|
@@ -1432,6 +1887,13 @@ function guideCmd(root, target) {
|
|
|
1432
1887
|
log('');
|
|
1433
1888
|
log('## 3. UI consistency — 디자인 토큰 일치');
|
|
1434
1889
|
uiConsistency(root);
|
|
1890
|
+
log('');
|
|
1891
|
+
log('## 4. Lessons — 과거 결정/실수 회수 (1.9.7)');
|
|
1892
|
+
if (q) {
|
|
1893
|
+
// lessonsCmd가 arg('--query')를 읽으므로 임시로 push
|
|
1894
|
+
if (!process.argv.includes('--query')) { process.argv.push('--query', q); }
|
|
1895
|
+
lessonsCmd(root);
|
|
1896
|
+
} else log('(target/--query 없음 — lessons 검색 스킵)');
|
|
1435
1897
|
log('\n💡 다음 단계: 위 결과를 바탕으로 작업 계획을 plan/progress에 기록 후 진행하세요.');
|
|
1436
1898
|
}
|
|
1437
1899
|
|
|
@@ -1608,7 +2070,15 @@ function viewworkInstall(root) {
|
|
|
1608
2070
|
}
|
|
1609
2071
|
|
|
1610
2072
|
function help() {
|
|
1611
|
-
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
|
|
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
|
|
2074
|
+
leerness verify-code [path] [--build] # npm test/lint/typecheck 자동 실행 + evidence 자동 기록 (1.9.7)
|
|
2075
|
+
leerness lessons [--query <키>] [--limit N] # 과거 결정/실수 자동 회수 (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`);
|
|
1612
2082
|
}
|
|
1613
2083
|
|
|
1614
2084
|
async function main() {
|
|
@@ -1647,6 +2117,19 @@ async function main() {
|
|
|
1647
2117
|
if (cmd === 'skill' && args[1] === 'remove') return skillRemove(absRoot(arg('--path', process.cwd())), args[2]);
|
|
1648
2118
|
if (cmd === 'skill' && args[1] === 'consolidate') return skillConsolidate(absRoot(arg('--path', process.cwd())));
|
|
1649
2119
|
if (cmd === 'gate') return gate(args[1] || process.cwd());
|
|
2120
|
+
if (cmd === 'verify-code') return verifyCodeCmd(args[1] || process.cwd());
|
|
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()));
|
|
1650
2133
|
if (cmd === 'impact') return impactCmd(arg('--path', process.cwd()), args[1]);
|
|
1651
2134
|
if (cmd === 'reuse' && args[1] === 'find') return reuseFind(arg('--path', process.cwd()), args.slice(2).filter(x => !x.startsWith('-')).join(' '));
|
|
1652
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,179 @@ 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
|
+
|
|
323
|
+
// 1.9.7 A: verify-code — 가짜 package.json + 통과 시나리오
|
|
324
|
+
total++;
|
|
325
|
+
{
|
|
326
|
+
const tmpV = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-vc-'));
|
|
327
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpV, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore' });
|
|
328
|
+
fs.writeFileSync(path.join(tmpV, 'package.json'), JSON.stringify({ name: 't', version: '0.0.1', scripts: { test: 'node -e "console.log(\\"OK\\");process.exit(0)"' } }));
|
|
329
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'verify-code', tmpV], { encoding: 'utf8' });
|
|
330
|
+
const ev = fs.readFileSync(path.join(tmpV, '.harness/review-evidence.md'), 'utf8');
|
|
331
|
+
const ok = r.status === 0 && /test passed/.test(r.stdout) && /verify-code \(자동\)/.test(ev);
|
|
332
|
+
console.log(ok ? '✓ B(1.9.7-A) verify-code: 통과 + evidence 자동 기록' : `✗ A 실패\n${r.stdout}`);
|
|
333
|
+
if (!ok) failed++;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// 1.9.7 A: verify-code — 실패 시나리오
|
|
337
|
+
total++;
|
|
338
|
+
{
|
|
339
|
+
const tmpV2 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-vc2-'));
|
|
340
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpV2, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore' });
|
|
341
|
+
fs.writeFileSync(path.join(tmpV2, 'package.json'), JSON.stringify({ name: 't', version: '0.0.1', scripts: { test: 'node -e "process.exit(2)"' } }));
|
|
342
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'verify-code', tmpV2], { encoding: 'utf8' });
|
|
343
|
+
const ok = r.status === 1 && /test failed/.test(r.stdout);
|
|
344
|
+
console.log(ok ? '✓ B(1.9.7-A) verify-code: 실패 시 exit=1' : `✗ A2 실패 status=${r.status}`);
|
|
345
|
+
if (!ok) failed++;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// 1.9.7 B: lessons — decisions/evidence에 시드 후 query로 회수
|
|
349
|
+
total++;
|
|
350
|
+
{
|
|
351
|
+
fs.appendFileSync(path.join(tmp, '.harness/decisions.md'), `\n### 2026-05-08 — Decision: 캐시 차등 TTL 도입\n- Reason: 단일 5분 TTL이 daily 데이터에 비효율\n- Impact: open-meteo 응답 캐시 적중률 ↑\n`);
|
|
352
|
+
fs.appendFileSync(path.join(tmp, '.harness/review-evidence.md'), `\n## 2026-05-08 e2e\n✗ 캐시 키 불안정 — 좌표 정규화 부재 (롤백 후 fix)\n`);
|
|
353
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'lessons', '--query', '캐시', '--path', tmp], { encoding: 'utf8' });
|
|
354
|
+
const ok = r.status === 0 && /Lessons.*query="캐시"/.test(r.stdout) && /decisions\.md/.test(r.stdout) && /review-evidence\.md/.test(r.stdout);
|
|
355
|
+
console.log(ok ? '✓ B(1.9.7-B) lessons: decisions+evidence 회수' : `✗ B 실패\n${r.stdout}`);
|
|
356
|
+
if (!ok) failed++;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// 1.9.7 B: guide가 lessons를 자동 통합
|
|
360
|
+
total++;
|
|
361
|
+
{
|
|
362
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'guide', 'src/components/Card.html', '--path', tmp], { encoding: 'utf8' });
|
|
363
|
+
const ok = r.status === 0 && /## 4\. Lessons/.test(r.stdout);
|
|
364
|
+
console.log(ok ? '✓ B(1.9.7-B) guide: lessons 섹션 자동 추가' : '✗ B guide 통합 실패');
|
|
365
|
+
if (!ok) failed++;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// 1.9.7 C: lazy detect --auto-track
|
|
369
|
+
total++;
|
|
370
|
+
{
|
|
371
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-c-'));
|
|
372
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore' });
|
|
373
|
+
// 의도된 진짜 TODO (주석)
|
|
374
|
+
fs.mkdirSync(path.join(tmpC, 'src'), { recursive: true });
|
|
375
|
+
fs.writeFileSync(path.join(tmpC, 'src/a.js'), `// TODO: 추적해야 할 미완료 작업\nfunction foo() {}\n`);
|
|
376
|
+
// review-evidence에 npm test 키워드 추가 (lazy detect의 다른 신호 우회)
|
|
377
|
+
fs.appendFileSync(path.join(tmpC, '.harness/review-evidence.md'), '\n## seed\nCommand: npm test\n');
|
|
378
|
+
// session close로 handoff 채우기
|
|
379
|
+
cp.spawnSync(process.execPath, [CLI, 'session', 'close', tmpC], { stdio: 'ignore' });
|
|
380
|
+
// --auto-track 실행
|
|
381
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'lazy', 'detect', tmpC, '--auto-track'], { encoding: 'utf8' });
|
|
382
|
+
const tracker = fs.readFileSync(path.join(tmpC, '.harness/progress-tracker.md'), 'utf8');
|
|
383
|
+
const known = fs.existsSync(path.join(tmpC, '.harness/known-todos.json')) ? JSON.parse(fs.readFileSync(path.join(tmpC, '.harness/known-todos.json'), 'utf8')) : [];
|
|
384
|
+
const ok = /TODO src\/a\.js:1/.test(tracker) && /auto-tracked/.test(tracker) && known.length === 1;
|
|
385
|
+
console.log(ok ? '✓ B(1.9.7-C) lazy detect --auto-track: 자동 등록 + known-todos.json' : `✗ C 실패\nTracker:\n${tracker.split('\n').filter(l=>l.startsWith('| T-')).slice(-3).join('\n')}\nKnown: ${JSON.stringify(known)}`);
|
|
386
|
+
if (!ok) failed++;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// 1.9.7 C: 같은 TODO 재실행 시 known-todos가 적용되어 newTodos 0
|
|
390
|
+
total++;
|
|
391
|
+
{
|
|
392
|
+
// 위 시나리오 이어서 — known-todos가 있으므로 새 TODO=0이어야 함
|
|
393
|
+
// 별도 새 tmp로 재현 (tmpC는 위에서 자동 등록됐으니 same dir에서 다시 호출)
|
|
394
|
+
// 위 tmpC는 already auto-tracked
|
|
395
|
+
const tmpC2 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-c2-'));
|
|
396
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC2, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore' });
|
|
397
|
+
fs.mkdirSync(path.join(tmpC2, 'src'), { recursive: true });
|
|
398
|
+
fs.writeFileSync(path.join(tmpC2, 'src/a.js'), `// TODO: 추적된 항목\n`);
|
|
399
|
+
// known-todos.json에 미리 등록
|
|
400
|
+
fs.writeFileSync(path.join(tmpC2, '.harness/known-todos.json'), JSON.stringify([{ file: 'src/a.js', line: 1, text: '// TODO: 추적된 항목', ackAt: '2026-05-08T00:00:00Z' }]));
|
|
401
|
+
fs.appendFileSync(path.join(tmpC2, '.harness/review-evidence.md'), '\n## seed\nCommand: npm test\n');
|
|
402
|
+
cp.spawnSync(process.execPath, [CLI, 'session', 'close', tmpC2], { stdio: 'ignore' });
|
|
403
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'lazy', 'detect', tmpC2], { encoding: 'utf8' });
|
|
404
|
+
// newTodos가 0이므로 "new: 0" 또는 TODO 카운트가 1이지만 progress 추적에 자동 등록 안 됨
|
|
405
|
+
// 핵심: TODO 1개 잡혀도 known이라 새 항목 노출 X
|
|
406
|
+
const ok = /new: 0\b/.test(r.stdout) || !/💡 자동 등록/.test(r.stdout);
|
|
407
|
+
console.log(ok ? '✓ B(1.9.7-C) known-todos: 재카운트 회피' : `✗ C2 실패\n${r.stdout}`);
|
|
408
|
+
if (!ok) failed++;
|
|
409
|
+
}
|
|
410
|
+
|
|
238
411
|
// 1.9.6 회귀: task relink — 인위적 link 손실 → 자동 매칭 제안 + --apply 자동 복구
|
|
239
412
|
total++;
|
|
240
413
|
{
|