leerness 1.9.13 → 1.9.16
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 +53 -0
- package/bin/harness.js +336 -35
- package/package.json +1 -1
- package/scripts/e2e.js +173 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.16 — 2026-05-13
|
|
4
|
+
|
|
5
|
+
**brainstorm 워크스페이스 통합 + 3 명령 JSON export + session close 워크스페이스 안내**.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`leerness brainstorm "<주제>" --all-apps` / `--include`**: retro/insights에 이어 brainstorm도 다수 프로젝트 통합 검색. 프로젝트별 결과 요약 + 워크스페이스 총합.
|
|
10
|
+
- **`--json` 옵션** (retro / insights / brainstorm, 단일/워크스페이스 모두): JSON으로 export. CI/대시보드 연동 가능.
|
|
11
|
+
- **session close 끝에 워크스페이스 안내**: `_apps/*/` 또는 부모 `_apps/*/`에 다른 leerness 프로젝트가 있으면 `🌐 워크스페이스에 N개 — leerness retro --all-apps` 자동 안내.
|
|
12
|
+
|
|
13
|
+
### Migration
|
|
14
|
+
```bash
|
|
15
|
+
npx leerness@latest update . --yes
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 1.9.15 — 2026-05-13
|
|
19
|
+
|
|
20
|
+
**브레인스토밍 출처 표시 + 워크스페이스 통합 회고/통찰**.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **brainstorm 매치 위치 표시**: 모든 결과에 `.harness/<file>:<line>` 형식의 파일 경로 + 줄 번호. task 결과는 매치된 필드(request/evidence/nextAction)도 함께 표시.
|
|
25
|
+
- **`leerness retro --all-apps`**: 현재 디렉토리 + `_apps/*` (또는 부모 `_apps/*`)의 모든 leerness 프로젝트를 통합 회고. 프로젝트별 한 줄 요약 + 다음 우선 작업 + top 스킬 + 워크스페이스 총합 (task / done % / 결정 / 스킬 / 사용 / 최적화 / pass-fix 비율).
|
|
26
|
+
- **`leerness retro --include <p1,p2,...>`**: 명시 경로 통합 회고. 쉼표 구분 다중 경로 지원.
|
|
27
|
+
- **`leerness insights --all-apps`** / **`--include`**: 통합 통계를 표 형식으로 출력 + 안정성 평가 + 최적화 권장.
|
|
28
|
+
|
|
29
|
+
### Migration
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx leerness@latest update . --yes
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
기존 명령은 모두 호환. `--all-apps` / `--include`는 선택 옵션.
|
|
36
|
+
|
|
37
|
+
## 1.9.14 — 2026-05-13
|
|
38
|
+
|
|
39
|
+
**1.9.13의 retro/brainstorm 정확도 4건 fix** (city-insights 대형 프로젝트 운영 중 발견).
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- **A. decisions Template 카운트 오류**: init이 만드는 `decisions.md`의 `### YYYY-MM-DD — Decision` 템플릿 예시가 실제 결정으로 잘못 카운트되던 문제. `_extractDecisionBlocks()` 헬퍼가 코드블록(```...```) 안의 ### 와 `### (Template|템플릿)` 시작 블록을 자동 제외.
|
|
44
|
+
- **B. brainstorm 토큰 매칭 부정확**: 단순 substring 매치로 인해 무관한 task가 잡히던 문제. **유니코드 word boundary** (`(?<![\p{L}\p{N}_])…(?![\p{L}\p{N}_])`) 기반 토큰 매칭으로 변경. 다중 토큰 (예: `"API rate limit"`)은 **모두** 매치되어야 결과로 표시.
|
|
45
|
+
- **C. retro 다음 우선 작업이 planned 미포함**: in-progress/blocked가 비어있으면 "(없음)"으로 표시되던 문제. 우선순위 가중치 (in-progress=0, blocked/waiting/on-hold/incomplete=1, planned/requested=2)로 정렬해 planned도 포함.
|
|
46
|
+
- **D. decisions.md 템플릿 형식**: init 디폴트가 실 결정과 동일한 `### YYYY-MM-DD — Decision` 형식이라 retro 카운트와 충돌. 템플릿을 **명시적 ```` ```md ```` 코드블록**으로 감싸 표시. retro/brainstorm/lessons가 일관되게 무시.
|
|
47
|
+
|
|
48
|
+
### Migration
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx leerness@latest update . --yes
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
기존 프로젝트의 decisions.md는 그대로 두면 자동으로 정확히 카운트됩니다 (코드블록 처리는 양쪽 모두 동작).
|
|
55
|
+
|
|
3
56
|
## 1.9.13 — 2026-05-13
|
|
4
57
|
|
|
5
58
|
**회고·통찰·브레인스토밍** — 누적된 leerness 데이터에서 자동으로 패턴/추세/주제별 자원을 추출.
|
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.16';
|
|
10
10
|
const MARK = '<!-- leerness:managed -->';
|
|
11
11
|
const README_START = '<!-- leerness:project-readme:start -->';
|
|
12
12
|
const README_END = '<!-- leerness:project-readme:end -->';
|
|
@@ -113,7 +113,7 @@ function arg(name, def = null) { const i = process.argv.indexOf(name); return i
|
|
|
113
113
|
function has(name) { return process.argv.includes(name); }
|
|
114
114
|
function nonFlagArgs() {
|
|
115
115
|
const out = [];
|
|
116
|
-
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']);
|
|
116
|
+
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','--include','--days','--gh-pages-src','--roadmap']);
|
|
117
117
|
const a = process.argv.slice(2);
|
|
118
118
|
for (let i = 0; i < a.length; i++) {
|
|
119
119
|
const x = a[i];
|
|
@@ -218,7 +218,7 @@ function coreFiles(root, lang = 'ko', selectedSkills = []) {
|
|
|
218
218
|
'.harness/protected-files.md': fm('protected-files', ['파일 삭제/정리/마이그레이션 전'], ['보호 대상 변경'], `# Protected Files\n\nAI agents must not delete or reset these files without explicit user approval.\n\n- .harness/\n- .harness/skills/\n- .harness/library/\n- AGENTS.md\n- CLAUDE.md\n- .cursor/rules/leerness.mdc\n- .github/copilot-instructions.md\n- .claude/commands/\n- .claude/skills/\n- README.md Leerness managed section\n\nUse merge, archive, or deprecated markers instead of deletion.\n`),
|
|
219
219
|
'.harness/architecture.md': fm('architecture', ['기능 구현','리팩토링','마이그레이션'], ['구조 변경'], `# Architecture\n\n## Overview\n- 실제 구조를 기록하세요.\n\n## Data Flow\n-\n\n## External Dependencies\n-\n`),
|
|
220
220
|
'.harness/context-map.md': fm('context-map', ['관련 파일 탐색','기능 구현 전'], ['파일 구조 변경'], `# Context Map\n\n| Area | Files | Notes |\n|---|---|---|\n| App | src/** | 실제 경로로 업데이트 |\n| Tests | tests/** | 검증 경로 |\n`),
|
|
221
|
-
'.harness/decisions.md': fm('decisions', ['설계 결정 확인'], ['중요 결정 발생'], `# Decisions\n\n## Template\n### ${today()} — Decision
|
|
221
|
+
'.harness/decisions.md': fm('decisions', ['설계 결정 확인'], ['중요 결정 발생'], `# Decisions\n\n## Template (예시 — 실제 결정은 아래 코드블록 밖에 추가)\n\n\`\`\`md\n### ${today()} — Decision 제목\n- Decision:\n- Reason:\n- Alternatives:\n- Impact:\n\`\`\`\n`),
|
|
222
222
|
'.harness/task-log.md': fm('task-log', ['작업 이력 확인'], ['모든 의미 있는 작업 후'], `# Task Log\n\n## ${today()}\n- Leerness v${VERSION} initialized.\n`),
|
|
223
223
|
'.harness/guardrails.md': fm('guardrails', ['모든 작업 전','보안/권한/리팩토링 전'], ['금지 규칙 변경'], `# Guardrails\n\n- 토큰/키/비밀번호를 저장하지 않습니다. 환경변수 이름만 기록합니다.\n- 요청 없는 대규모 리팩토링을 하지 않습니다 (5개 이상 파일 변경 시 사용자 사전 승인).\n- API/DB/환경변수 변경은 영향 범위를 task-log에 기록합니다.\n- Leerness 보호 파일/관리 섹션을 삭제하지 않습니다.\n- 한글 인코딩은 BOM 없는 UTF-8을 유지합니다.\n- destructive Git 작업(\`git reset --hard\`, \`git push --force\` 등)은 사용자 명시 승인 후에만 수행합니다.\n`),
|
|
224
224
|
'.harness/design-system.md': fm('design-system', ['UI 변경','컴포넌트 추가','designguide 병합'], ['디자인 기준 변경','재사용 패턴 발견'], `# Design System\n\n## Canonical File\n이 파일은 designguide.md, design-guide.md와 같은 디자인 가이드의 기준 파일입니다.\n\n## Tokens\n| Token | Value | Notes |\n|---|---|---|\n| color.primary | (실제 값으로 업데이트) | |\n| color.surface | | |\n| spacing.unit | | |\n| typography.body | | |\n\n## Reusable Patterns\n| Pattern | Where | Reuse Rule |\n|---|---|---|\n`),
|
|
@@ -1297,6 +1297,22 @@ function sessionClose(root) {
|
|
|
1297
1297
|
const left = 5 - (sc.count % 5);
|
|
1298
1298
|
log(` 💡 ${left}세션 후 자동 깊은 회고 — \`leerness retro\`로 즉시 실행 가능`);
|
|
1299
1299
|
}
|
|
1300
|
+
// 1.9.16: 워크스페이스 안내 (다른 leerness 프로젝트가 있으면)
|
|
1301
|
+
try {
|
|
1302
|
+
const wsCands = [path.resolve(root, '_apps'), path.resolve(root, '..', '_apps')];
|
|
1303
|
+
let wsCount = 0;
|
|
1304
|
+
for (const base of wsCands) {
|
|
1305
|
+
if (!exists(base)) continue;
|
|
1306
|
+
try { if (!fs.statSync(base).isDirectory()) continue; } catch { continue; }
|
|
1307
|
+
for (const e of fs.readdirSync(base)) {
|
|
1308
|
+
try {
|
|
1309
|
+
const p = path.join(base, e);
|
|
1310
|
+
if (fs.statSync(p).isDirectory() && exists(path.join(p, '.harness')) && p !== root) wsCount++;
|
|
1311
|
+
} catch {}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
if (wsCount > 0) log(` 🌐 워크스페이스에 ${wsCount}개 다른 leerness 프로젝트 — \`leerness retro --all-apps\`로 통합 회고`);
|
|
1315
|
+
} catch {}
|
|
1300
1316
|
} catch (e) {
|
|
1301
1317
|
warn('retro 요약 실패: ' + (e && e.message ? e.message : e));
|
|
1302
1318
|
}
|
|
@@ -1356,6 +1372,15 @@ function readSessionCounter(root) {
|
|
|
1356
1372
|
}
|
|
1357
1373
|
function writeSessionCounter(root, c) { writeUtf8(sessionCounterPath(root), JSON.stringify(c, null, 2) + '\n'); }
|
|
1358
1374
|
|
|
1375
|
+
// 1.9.14 A/D: 결정 블록 추출 — 코드 블록 안의 ### + Template 제외
|
|
1376
|
+
function _extractDecisionBlocks(text) {
|
|
1377
|
+
// 줄 시작의 ```부터 줄 시작의 ```까지를 코드블록으로 인식 (인라인 백틱 무시)
|
|
1378
|
+
const cleaned = String(text || '').replace(/^```[^\n]*\n[\s\S]*?\n```\s*$/gm, '');
|
|
1379
|
+
return cleaned.split(/\n(?=### )/).filter(b =>
|
|
1380
|
+
b.startsWith('### ') && !/^### (Template|템플릿)\b/.test(b.trim())
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1359
1384
|
function _retroAggregate(root) {
|
|
1360
1385
|
root = absRoot(root);
|
|
1361
1386
|
const rows = readProgressRows(root);
|
|
@@ -1369,8 +1394,8 @@ function _retroAggregate(root) {
|
|
|
1369
1394
|
for (const s of STATUSES) statusCounts[s] = 0;
|
|
1370
1395
|
for (const r of rows) if (statusCounts[r.status] != null) statusCounts[r.status]++;
|
|
1371
1396
|
|
|
1372
|
-
// 2) 결정 블록 수
|
|
1373
|
-
const decisionBlocks = decisions
|
|
1397
|
+
// 2) 결정 블록 수 (1.9.14: 코드블록/Template 제외)
|
|
1398
|
+
const decisionBlocks = _extractDecisionBlocks(decisions);
|
|
1374
1399
|
// recent decisions (날짜로 정렬 시 가장 최근)
|
|
1375
1400
|
const recentDecisions = decisionBlocks.slice(-5).map(b => {
|
|
1376
1401
|
const t = (b.match(/^### (.+)$/m) || [, ''])[1];
|
|
@@ -1412,9 +1437,10 @@ function _retroAggregate(root) {
|
|
|
1412
1437
|
const activeRules = rules.filter(r => r.status === 'active');
|
|
1413
1438
|
const verifiedRules = rules.filter(r => r.lastVerified && r.lastVerified !== '-');
|
|
1414
1439
|
|
|
1415
|
-
// 7)
|
|
1416
|
-
const
|
|
1417
|
-
|
|
1440
|
+
// 7) 다음 우선 작업 — 우선순위: in-progress > blocked/waiting/on-hold/incomplete > planned/requested (1.9.14 C)
|
|
1441
|
+
const _priority = { 'in-progress': 0, 'blocked': 1, 'waiting': 1, 'on-hold': 1, 'incomplete': 1, 'planned': 2, 'requested': 2 };
|
|
1442
|
+
const focusNext = rows.filter(r => _priority[r.status] != null)
|
|
1443
|
+
.sort((a, b) => (_priority[a.status] || 9) - (_priority[b.status] || 9));
|
|
1418
1444
|
|
|
1419
1445
|
return {
|
|
1420
1446
|
statusCounts,
|
|
@@ -1456,11 +1482,49 @@ function _retroOneLine(agg) {
|
|
|
1456
1482
|
return parts.join(' · ');
|
|
1457
1483
|
}
|
|
1458
1484
|
|
|
1485
|
+
// 1.9.15: --all-apps / --include 경로 모음
|
|
1486
|
+
function _collectWorkspacePaths(rootBase) {
|
|
1487
|
+
const set = new Set();
|
|
1488
|
+
if (exists(path.join(rootBase, '.harness'))) set.add(rootBase);
|
|
1489
|
+
if (has('--all-apps')) {
|
|
1490
|
+
const baseCandidates = [path.resolve(rootBase, '_apps'), path.resolve(rootBase, '..', '_apps')];
|
|
1491
|
+
for (const base of baseCandidates) {
|
|
1492
|
+
if (!exists(base)) continue;
|
|
1493
|
+
let st; try { st = fs.statSync(base); } catch { continue; }
|
|
1494
|
+
if (!st.isDirectory()) continue;
|
|
1495
|
+
for (const e of fs.readdirSync(base)) {
|
|
1496
|
+
const p = path.join(base, e);
|
|
1497
|
+
try {
|
|
1498
|
+
if (fs.statSync(p).isDirectory() && exists(path.join(p, '.harness'))) set.add(p);
|
|
1499
|
+
} catch {}
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
const include = arg('--include', null);
|
|
1504
|
+
if (include) {
|
|
1505
|
+
for (const p of String(include).split(',')) {
|
|
1506
|
+
const abs = path.resolve(p.trim());
|
|
1507
|
+
if (exists(path.join(abs, '.harness'))) set.add(abs);
|
|
1508
|
+
else warn(`--include 무시: ${abs} (.harness 없음)`);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
return Array.from(set);
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1459
1514
|
function retroCmd(root) {
|
|
1460
1515
|
root = absRoot(root);
|
|
1516
|
+
// 1.9.15: --all-apps / --include 통합 모드
|
|
1517
|
+
if (has('--all-apps') || arg('--include', null)) {
|
|
1518
|
+
return _retroWorkspace(root);
|
|
1519
|
+
}
|
|
1461
1520
|
const days = parseInt(arg('--days', '7'), 10);
|
|
1462
1521
|
const cutoff = new Date(Date.now() - days * 86400 * 1000).toISOString().slice(0, 10);
|
|
1463
1522
|
const agg = _retroAggregate(root);
|
|
1523
|
+
// 1.9.16: --json
|
|
1524
|
+
if (has('--json')) {
|
|
1525
|
+
log(JSON.stringify({ project: path.basename(root), days, cutoff, summary: _retroOneLine(agg), data: agg }, null, 2));
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1464
1528
|
log(`# 회고 (retro) — 최근 ${days}일 (since ${cutoff})`);
|
|
1465
1529
|
log(`\n📈 한 줄 요약: ${_retroOneLine(agg)}`);
|
|
1466
1530
|
|
|
@@ -1506,9 +1570,66 @@ function retroCmd(root) {
|
|
|
1506
1570
|
log(` 4. \`leerness brainstorm <주제>\`로 누적 데이터 기반 컨텍스트 적재`);
|
|
1507
1571
|
}
|
|
1508
1572
|
|
|
1573
|
+
// 1.9.15: 워크스페이스 통합 retro (다수 프로젝트 묶음 회고)
|
|
1574
|
+
function _retroWorkspace(rootBase) {
|
|
1575
|
+
const paths = _collectWorkspacePaths(rootBase);
|
|
1576
|
+
if (!paths.length) return fail('대상 프로젝트 없음. --include <path1,path2> 또는 --all-apps 사용 필요.');
|
|
1577
|
+
// 1.9.16: --json
|
|
1578
|
+
if (has('--json')) {
|
|
1579
|
+
const projects = paths.map(p => {
|
|
1580
|
+
const a = _retroAggregate(p);
|
|
1581
|
+
return { project: path.basename(p), path: p, summary: _retroOneLine(a), data: a };
|
|
1582
|
+
});
|
|
1583
|
+
const totals = projects.reduce((t, p) => ({
|
|
1584
|
+
tasks: t.tasks + p.data.totalTasks, done: t.done + p.data.doneCount,
|
|
1585
|
+
decisions: t.decisions + p.data.decisionBlocks, skills: t.skills + p.data.skillUsage.length,
|
|
1586
|
+
usage: t.usage + p.data.totalSkillUsage, opts: t.opts + p.data.totalOptimizations,
|
|
1587
|
+
activeRules: t.activeRules + p.data.activeRules, pass: t.pass + p.data.passSignals, fix: t.fix + p.data.fixSignals
|
|
1588
|
+
}), { tasks: 0, done: 0, decisions: 0, skills: 0, usage: 0, opts: 0, activeRules: 0, pass: 0, fix: 0 });
|
|
1589
|
+
log(JSON.stringify({ projects, totals, projectCount: paths.length }, null, 2));
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
log(`# Cross-project retro — ${paths.length}개 프로젝트`);
|
|
1593
|
+
const totals = { tasks: 0, done: 0, decisions: 0, skills: 0, totalSkillUsage: 0, totalOpts: 0, activeRules: 0, fixSig: 0, passSig: 0 };
|
|
1594
|
+
for (const p of paths) {
|
|
1595
|
+
const agg = _retroAggregate(p);
|
|
1596
|
+
const name = path.basename(p);
|
|
1597
|
+
log(`\n## ${name}`);
|
|
1598
|
+
log(` 📈 ${_retroOneLine(agg)}`);
|
|
1599
|
+
const f = agg.focusNext[0];
|
|
1600
|
+
log(` 🎯 다음 우선: ${f ? `${f.id} [${f.status}] ${f.request.slice(0, 50)}` : '(없음)'}`);
|
|
1601
|
+
log(` 📚 top 스킬: ${agg.skillUsage.length ? agg.skillUsage[0].id + ' (' + agg.skillUsage[0].count + '회)' : '(없음)'}`);
|
|
1602
|
+
totals.tasks += agg.totalTasks;
|
|
1603
|
+
totals.done += agg.doneCount;
|
|
1604
|
+
totals.decisions += agg.decisionBlocks;
|
|
1605
|
+
totals.skills += agg.skillUsage.length;
|
|
1606
|
+
totals.totalSkillUsage += agg.totalSkillUsage;
|
|
1607
|
+
totals.totalOpts += agg.totalOptimizations;
|
|
1608
|
+
totals.activeRules += agg.activeRules;
|
|
1609
|
+
totals.fixSig += agg.fixSignals;
|
|
1610
|
+
totals.passSig += agg.passSignals;
|
|
1611
|
+
}
|
|
1612
|
+
log(`\n## 📊 워크스페이스 총합 (${paths.length} 프로젝트)`);
|
|
1613
|
+
log(` - 누적 task: ${totals.tasks}${totals.tasks ? ` (done ${totals.done} = ${Math.round(totals.done / totals.tasks * 100)}%)` : ''}`);
|
|
1614
|
+
log(` - 누적 결정: ${totals.decisions}건`);
|
|
1615
|
+
log(` - 스킬: ${totals.skills}종 / 사용 ${totals.totalSkillUsage}회 / 최적화 ${totals.totalOpts}건`);
|
|
1616
|
+
log(` - 활성 룰: ${totals.activeRules}건`);
|
|
1617
|
+
log(` - 시그널: pass ${totals.passSig} · fix ${totals.fixSig}${totals.passSig + totals.fixSig > 0 ? ` (비율 ${totals.fixSig ? (totals.passSig / totals.fixSig).toFixed(2) : '∞'})` : ''}`);
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1509
1620
|
function insightsCmd(root) {
|
|
1510
1621
|
root = absRoot(root);
|
|
1622
|
+
// 1.9.15: --all-apps / --include 통합 모드
|
|
1623
|
+
if (has('--all-apps') || arg('--include', null)) {
|
|
1624
|
+
return _insightsWorkspace(root);
|
|
1625
|
+
}
|
|
1511
1626
|
const agg = _retroAggregate(root);
|
|
1627
|
+
// 1.9.16: --json
|
|
1628
|
+
if (has('--json')) {
|
|
1629
|
+
const sc = readSessionCounter(root);
|
|
1630
|
+
log(JSON.stringify({ project: path.basename(root), sessionCount: sc.count, lastCloseAt: sc.lastCloseAt, data: agg }, null, 2));
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1512
1633
|
const sc = readSessionCounter(root);
|
|
1513
1634
|
log(`# Insights — 누적 통계`);
|
|
1514
1635
|
log(`\n## 📊 핵심 지표`);
|
|
@@ -1543,22 +1664,182 @@ function insightsCmd(root) {
|
|
|
1543
1664
|
if (agg.statusCounts.blocked > 0) log(` - blocked 작업 ${agg.statusCounts.blocked}건 — \`leerness lessons --query "blocked"\`로 과거 패턴 회수`);
|
|
1544
1665
|
}
|
|
1545
1666
|
|
|
1667
|
+
function _insightsWorkspace(rootBase) {
|
|
1668
|
+
const paths = _collectWorkspacePaths(rootBase);
|
|
1669
|
+
if (!paths.length) return fail('대상 프로젝트 없음. --include 또는 --all-apps 사용.');
|
|
1670
|
+
// 1.9.16: --json
|
|
1671
|
+
if (has('--json')) {
|
|
1672
|
+
const projects = paths.map(p => ({ project: path.basename(p), path: p, data: _retroAggregate(p) }));
|
|
1673
|
+
log(JSON.stringify({ projects, projectCount: paths.length }, null, 2));
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
log(`# Workspace Insights — ${paths.length}개 프로젝트`);
|
|
1677
|
+
log(`\n| Project | Task | Done % | Decisions | Skills | Usage | Opts | Pass/Fix |`);
|
|
1678
|
+
log(`|---|---|---|---|---|---|---|---|`);
|
|
1679
|
+
const totals = { tasks: 0, done: 0, decisions: 0, skills: 0, usage: 0, opts: 0, pass: 0, fix: 0 };
|
|
1680
|
+
for (const p of paths) {
|
|
1681
|
+
const a = _retroAggregate(p);
|
|
1682
|
+
const donePct = a.totalTasks ? Math.round(a.doneCount / a.totalTasks * 100) : 0;
|
|
1683
|
+
const pf = a.fixSignals ? (a.passSignals / a.fixSignals).toFixed(1) : '∞';
|
|
1684
|
+
log(`| ${path.basename(p)} | ${a.totalTasks} | ${donePct}% | ${a.decisionBlocks} | ${a.skillUsage.length} | ${a.totalSkillUsage} | ${a.totalOptimizations} | ${a.passSignals}/${a.fixSignals} (${pf}) |`);
|
|
1685
|
+
totals.tasks += a.totalTasks; totals.done += a.doneCount; totals.decisions += a.decisionBlocks;
|
|
1686
|
+
totals.skills += a.skillUsage.length; totals.usage += a.totalSkillUsage; totals.opts += a.totalOptimizations;
|
|
1687
|
+
totals.pass += a.passSignals; totals.fix += a.fixSignals;
|
|
1688
|
+
}
|
|
1689
|
+
const tpf = totals.fix ? (totals.pass / totals.fix).toFixed(1) : '∞';
|
|
1690
|
+
const tDonePct = totals.tasks ? Math.round(totals.done / totals.tasks * 100) : 0;
|
|
1691
|
+
log(`| **TOTAL** | **${totals.tasks}** | **${tDonePct}%** | **${totals.decisions}** | **${totals.skills}** | **${totals.usage}** | **${totals.opts}** | **${totals.pass}/${totals.fix} (${tpf})** |`);
|
|
1692
|
+
log(`\n## 📈 평가`);
|
|
1693
|
+
if (totals.pass > totals.fix * 3) log(` - 안정성: 우수 (pass÷fix = ${tpf})`);
|
|
1694
|
+
else if (totals.pass > totals.fix) log(` - 안정성: 보통 (pass÷fix = ${tpf})`);
|
|
1695
|
+
else if (totals.fix > 0) log(` - 안정성: 주의 (fix가 pass보다 많음) — verify-code 자동화 검토`);
|
|
1696
|
+
if (totals.opts === 0) log(` - 최적화 누적 없음 — \`leerness skill optimize\` 활용 권장`);
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
// 1.9.16: brainstorm 핵심 로직 분리 — 단일 프로젝트 결과 반환
|
|
1700
|
+
function _brainstormFor(root, topic) {
|
|
1701
|
+
function _escUnicode(s) { return String(s).replace(/[\\^$*+?.()|[\]{}]/g, '\\$&'); }
|
|
1702
|
+
const tokens = String(topic).split(/\s+/).filter(t => t.length >= 2);
|
|
1703
|
+
const wordRes = tokens.map(t => new RegExp(`(?<![\\p{L}\\p{N}_])${_escUnicode(t)}(?![\\p{L}\\p{N}_])`, 'iu'));
|
|
1704
|
+
function matches(text) { return wordRes.every(re => re.test(text)); }
|
|
1705
|
+
const hits = { decisions: [], skills: [], tasks: [], rules: [], evidence: [], lessons: [] };
|
|
1706
|
+
const dec = exists(decisionsPath(root)) ? read(decisionsPath(root)) : '';
|
|
1707
|
+
const decLines = dec.split('\n');
|
|
1708
|
+
for (const b of _extractDecisionBlocks(dec)) {
|
|
1709
|
+
if (matches(b)) {
|
|
1710
|
+
const t = (b.match(/^### (.+)$/m) || [, ''])[1];
|
|
1711
|
+
const lineIdx = decLines.findIndex(line => line === `### ${t}`);
|
|
1712
|
+
const lineNo = lineIdx >= 0 ? lineIdx + 1 : 0;
|
|
1713
|
+
hits.decisions.push({ title: t, preview: b.slice(0, 200).replace(/\n+/g, ' '), line: lineNo });
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
const skillsDir = path.join(root, '.harness/skills');
|
|
1717
|
+
if (exists(skillsDir)) {
|
|
1718
|
+
for (const id of fs.readdirSync(skillsDir)) {
|
|
1719
|
+
const f = path.join(skillsDir, id, 'skill.json');
|
|
1720
|
+
if (!exists(f)) continue;
|
|
1721
|
+
try {
|
|
1722
|
+
const s = JSON.parse(read(f));
|
|
1723
|
+
if (matches(JSON.stringify(s))) hits.skills.push({ id, displayNameKo: s.displayNameKo, capabilities: s.capabilities, usage: s.usage });
|
|
1724
|
+
} catch {}
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
const rows = readProgressRows(root);
|
|
1728
|
+
const progressText = exists(progressPath(root)) ? read(progressPath(root)) : '';
|
|
1729
|
+
for (const r of rows) {
|
|
1730
|
+
const fields = [];
|
|
1731
|
+
if (matches(r.request)) fields.push('request');
|
|
1732
|
+
if (matches(r.evidence)) fields.push('evidence');
|
|
1733
|
+
if (matches(r.nextAction)) fields.push('nextAction');
|
|
1734
|
+
if (fields.length) {
|
|
1735
|
+
const idx = progressText.indexOf(`| ${r.id} |`);
|
|
1736
|
+
const lineNo = idx >= 0 ? progressText.slice(0, idx).split('\n').length : 0;
|
|
1737
|
+
hits.tasks.push({ ...r, _fields: fields, line: lineNo });
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
if (exists(rulesPath(root))) {
|
|
1741
|
+
const rulesText = read(rulesPath(root));
|
|
1742
|
+
for (const r of readRules(root)) {
|
|
1743
|
+
if (matches(r.rule)) {
|
|
1744
|
+
const idx = rulesText.indexOf(`| ${r.id} |`);
|
|
1745
|
+
const lineNo = idx >= 0 ? rulesText.slice(0, idx).split('\n').length : 0;
|
|
1746
|
+
hits.rules.push({ ...r, line: lineNo });
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
const ev = exists(evidencePath(root)) ? read(evidencePath(root)) : '';
|
|
1751
|
+
for (const block of ev.split(/\n(?=## )/)) {
|
|
1752
|
+
if (!block.startsWith('## ')) continue;
|
|
1753
|
+
if (matches(block)) {
|
|
1754
|
+
const t = (block.match(/^## (.+)$/m) || [, ''])[1];
|
|
1755
|
+
const idx = ev.indexOf(block);
|
|
1756
|
+
const lineNo = idx >= 0 ? ev.slice(0, idx).split('\n').length : 0;
|
|
1757
|
+
hits.evidence.push({ title: t.trim(), preview: block.slice(0, 200).replace(/\n+/g, ' '), line: lineNo });
|
|
1758
|
+
if (/✗|fail|롤백|incomplete|버그/i.test(block)) hits.lessons.push({ title: t.trim(), line: lineNo });
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
return hits;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
function _brainstormTotal(h) { return h.decisions.length + h.skills.length + h.tasks.length + h.rules.length + h.evidence.length; }
|
|
1765
|
+
|
|
1766
|
+
// 1.9.16: 워크스페이스 통합 brainstorm
|
|
1767
|
+
function _brainstormWorkspace(rootBase, topic) {
|
|
1768
|
+
const paths = _collectWorkspacePaths(rootBase);
|
|
1769
|
+
if (!paths.length) return fail('대상 프로젝트 없음. --include 또는 --all-apps 사용.');
|
|
1770
|
+
if (has('--json')) {
|
|
1771
|
+
const result = paths.map(p => ({ project: path.basename(p), path: p, hits: _brainstormFor(p, topic) }));
|
|
1772
|
+
log(JSON.stringify({ topic, projects: result, total: result.reduce((a, b) => a + _brainstormTotal(b.hits), 0) }, null, 2));
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
log(`# Cross-project Brainstorm — "${topic}" — ${paths.length}개 프로젝트`);
|
|
1776
|
+
let grandTotal = 0;
|
|
1777
|
+
for (const p of paths) {
|
|
1778
|
+
const h = _brainstormFor(p, topic);
|
|
1779
|
+
const n = _brainstormTotal(h);
|
|
1780
|
+
grandTotal += n;
|
|
1781
|
+
if (n === 0) continue;
|
|
1782
|
+
log(`\n## ${path.basename(p)} (${n}건)`);
|
|
1783
|
+
if (h.decisions.length) {
|
|
1784
|
+
log(` 🧠 결정 (${h.decisions.length})`);
|
|
1785
|
+
h.decisions.slice(0, 3).forEach(d => log(` - decisions.md:${d.line || '?'} — ${d.title}`));
|
|
1786
|
+
}
|
|
1787
|
+
if (h.skills.length) {
|
|
1788
|
+
log(` 📚 스킬 (${h.skills.length})`);
|
|
1789
|
+
h.skills.slice(0, 3).forEach(s => log(` - ${s.id} (${s.displayNameKo}) · 사용 ${s.usage?.count || 0}회`));
|
|
1790
|
+
}
|
|
1791
|
+
if (h.tasks.length) {
|
|
1792
|
+
log(` 📌 task (${h.tasks.length})`);
|
|
1793
|
+
h.tasks.slice(0, 3).forEach(t => log(` - progress-tracker.md:${t.line || '?'} — ${t.id} [${t.status}] ${t.request.slice(0, 50)} (matched: ${t._fields.join('+')})`));
|
|
1794
|
+
}
|
|
1795
|
+
if (h.rules.length) {
|
|
1796
|
+
log(` ⚡ 룰 (${h.rules.length})`);
|
|
1797
|
+
h.rules.slice(0, 3).forEach(r => log(` - rules.md:${r.line || '?'} — ${r.id} [${r.trigger}]`));
|
|
1798
|
+
}
|
|
1799
|
+
if (h.evidence.length) {
|
|
1800
|
+
log(` 🧪 evidence (${h.evidence.length})`);
|
|
1801
|
+
h.evidence.slice(0, 3).forEach(e => log(` - review-evidence.md:${e.line || '?'} — ${e.title}`));
|
|
1802
|
+
}
|
|
1803
|
+
if (h.lessons.length) {
|
|
1804
|
+
log(` ⚠ 과거 실패/롤백 (${h.lessons.length})`);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
log(`\n## 📊 워크스페이스 총합: ${grandTotal}건 매치 (${paths.length} 프로젝트)`);
|
|
1808
|
+
if (grandTotal === 0) log(` ⓘ 어느 프로젝트에서도 "${topic}" 관련 자원 없음 — 새 영역. 첫 결정/스킬을 기록하면 다음 brainstorm이 풍부해짐.`);
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1546
1811
|
function brainstormCmd(root, topic) {
|
|
1547
1812
|
root = absRoot(root);
|
|
1548
1813
|
if (!topic) return fail('topic required (e.g., brainstorm "API rate limit")');
|
|
1814
|
+
// 1.9.16: --all-apps / --include 통합 모드
|
|
1815
|
+
if (has('--all-apps') || arg('--include', null)) {
|
|
1816
|
+
return _brainstormWorkspace(root, topic);
|
|
1817
|
+
}
|
|
1818
|
+
// 1.9.16: --json 단일 프로젝트
|
|
1819
|
+
if (has('--json')) {
|
|
1820
|
+
const h = _brainstormFor(root, topic);
|
|
1821
|
+
log(JSON.stringify({ topic, project: path.basename(root), hits: h, total: _brainstormTotal(h) }, null, 2));
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1549
1824
|
log(`# Brainstorm — "${topic}"`);
|
|
1550
1825
|
log(`\n누적된 leerness 데이터에서 주제 관련 자원을 회수합니다.`);
|
|
1551
1826
|
|
|
1552
|
-
|
|
1827
|
+
// 1.9.14 B: 토큰 기반 매칭 — unicode word boundary. unicode 모드에서 하이픈은 escape 불필요.
|
|
1828
|
+
function _escUnicode(s) { return String(s).replace(/[\\^$*+?.()|[\]{}]/g, '\\$&'); }
|
|
1829
|
+
const tokens = String(topic).split(/\s+/).filter(t => t.length >= 2);
|
|
1830
|
+
const wordRes = tokens.map(t => new RegExp(`(?<![\\p{L}\\p{N}_])${_escUnicode(t)}(?![\\p{L}\\p{N}_])`, 'iu'));
|
|
1831
|
+
function matches(text) { return wordRes.every(re => re.test(text)); }
|
|
1553
1832
|
const hits = { decisions: [], skills: [], tasks: [], rules: [], evidence: [], lessons: [] };
|
|
1554
1833
|
|
|
1555
|
-
// decisions
|
|
1834
|
+
// decisions (1.9.14: 코드블록/Template 제외, 1.9.15: 라인 번호)
|
|
1556
1835
|
const dec = exists(decisionsPath(root)) ? read(decisionsPath(root)) : '';
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
if (
|
|
1836
|
+
const decLines = dec.split('\n');
|
|
1837
|
+
for (const b of _extractDecisionBlocks(dec)) {
|
|
1838
|
+
if (matches(b)) {
|
|
1560
1839
|
const t = (b.match(/^### (.+)$/m) || [, ''])[1];
|
|
1561
|
-
|
|
1840
|
+
const lineIdx = decLines.findIndex(line => line === `### ${t}`);
|
|
1841
|
+
const lineNo = lineIdx >= 0 ? lineIdx + 1 : 0;
|
|
1842
|
+
hits.decisions.push({ title: t, preview: b.slice(0, 200).replace(/\n+/g, ' '), line: lineNo });
|
|
1562
1843
|
}
|
|
1563
1844
|
}
|
|
1564
1845
|
// skills
|
|
@@ -1570,54 +1851,75 @@ function brainstormCmd(root, topic) {
|
|
|
1570
1851
|
try {
|
|
1571
1852
|
const s = JSON.parse(read(f));
|
|
1572
1853
|
const text = JSON.stringify(s);
|
|
1573
|
-
if (
|
|
1854
|
+
if (matches(text)) hits.skills.push({ id, displayNameKo: s.displayNameKo, capabilities: s.capabilities, usage: s.usage });
|
|
1574
1855
|
} catch {}
|
|
1575
1856
|
}
|
|
1576
1857
|
}
|
|
1577
|
-
// tasks
|
|
1858
|
+
// tasks (1.9.14: token 매칭, 1.9.15: 매치 필드 + 라인 번호)
|
|
1578
1859
|
const rows = readProgressRows(root);
|
|
1579
|
-
|
|
1580
|
-
|
|
1860
|
+
const progressText = exists(progressPath(root)) ? read(progressPath(root)) : '';
|
|
1861
|
+
for (const r of rows) {
|
|
1862
|
+
const fields = [];
|
|
1863
|
+
if (matches(r.request)) fields.push('request');
|
|
1864
|
+
if (matches(r.evidence)) fields.push('evidence');
|
|
1865
|
+
if (matches(r.nextAction)) fields.push('nextAction');
|
|
1866
|
+
if (fields.length) {
|
|
1867
|
+
const idx = progressText.indexOf(`| ${r.id} |`);
|
|
1868
|
+
const lineNo = idx >= 0 ? progressText.slice(0, idx).split('\n').length : 0;
|
|
1869
|
+
hits.tasks.push({ ...r, _fields: fields, line: lineNo });
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
// rules (1.9.15: 라인 번호)
|
|
1581
1873
|
if (exists(rulesPath(root))) {
|
|
1582
|
-
|
|
1874
|
+
const rulesText = read(rulesPath(root));
|
|
1875
|
+
for (const r of readRules(root)) {
|
|
1876
|
+
if (matches(r.rule)) {
|
|
1877
|
+
const idx = rulesText.indexOf(`| ${r.id} |`);
|
|
1878
|
+
const lineNo = idx >= 0 ? rulesText.slice(0, idx).split('\n').length : 0;
|
|
1879
|
+
hits.rules.push({ ...r, line: lineNo });
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1583
1882
|
}
|
|
1584
|
-
// evidence — lessons 키워드 (fail/롤백/incomplete) 동반
|
|
1883
|
+
// evidence — lessons 키워드 (fail/롤백/incomplete) 동반 (1.9.15: 라인 번호)
|
|
1585
1884
|
const ev = exists(evidencePath(root)) ? read(evidencePath(root)) : '';
|
|
1586
1885
|
for (const block of ev.split(/\n(?=## )/)) {
|
|
1587
1886
|
if (!block.startsWith('## ')) continue;
|
|
1588
|
-
if (
|
|
1887
|
+
if (matches(block)) {
|
|
1589
1888
|
const t = (block.match(/^## (.+)$/m) || [, ''])[1];
|
|
1590
|
-
|
|
1591
|
-
|
|
1889
|
+
const idx = ev.indexOf(block);
|
|
1890
|
+
const lineNo = idx >= 0 ? ev.slice(0, idx).split('\n').length : 0;
|
|
1891
|
+
hits.evidence.push({ title: t.trim(), preview: block.slice(0, 200).replace(/\n+/g, ' '), line: lineNo });
|
|
1892
|
+
if (/✗|fail|롤백|incomplete|버그/i.test(block)) hits.lessons.push({ title: t.trim(), line: lineNo });
|
|
1592
1893
|
}
|
|
1593
1894
|
}
|
|
1594
1895
|
|
|
1595
1896
|
const total = hits.decisions.length + hits.skills.length + hits.tasks.length + hits.rules.length + hits.evidence.length;
|
|
1596
1897
|
log(`\n📦 총 ${total}건 발견 (decisions ${hits.decisions.length} · skills ${hits.skills.length} · tasks ${hits.tasks.length} · rules ${hits.rules.length} · evidence ${hits.evidence.length})`);
|
|
1597
1898
|
|
|
1899
|
+
// 1.9.15: 모든 출력에 출처 파일:라인 표시
|
|
1598
1900
|
if (hits.decisions.length) {
|
|
1599
1901
|
log(`\n## 🧠 관련 결정 (${hits.decisions.length})`);
|
|
1600
|
-
hits.decisions.slice(0, 5).forEach(d => log(` - ${d.title}`));
|
|
1902
|
+
hits.decisions.slice(0, 5).forEach(d => log(` - .harness/decisions.md:${d.line || '?'} — ${d.title}`));
|
|
1601
1903
|
}
|
|
1602
1904
|
if (hits.skills.length) {
|
|
1603
1905
|
log(`\n## 📚 관련 스킬 (${hits.skills.length}) — 시작 전 \`skill info <id>\` 권장`);
|
|
1604
|
-
hits.skills.forEach(s => log(` - ${s.id} (${s.displayNameKo}) · 사용 ${s.usage?.count || 0}회 · cap ${(s.capabilities || []).length}`));
|
|
1906
|
+
hits.skills.forEach(s => log(` - .harness/skills/${s.id}/skill.json — ${s.id} (${s.displayNameKo}) · 사용 ${s.usage?.count || 0}회 · cap ${(s.capabilities || []).length}`));
|
|
1605
1907
|
}
|
|
1606
1908
|
if (hits.tasks.length) {
|
|
1607
1909
|
log(`\n## 📌 관련 과거 task (${hits.tasks.length})`);
|
|
1608
|
-
hits.tasks.slice(0, 5).forEach(t => log(` - ${t.id} [${t.status}] ${t.request}`));
|
|
1910
|
+
hits.tasks.slice(0, 5).forEach(t => log(` - .harness/progress-tracker.md:${t.line || '?'} — ${t.id} [${t.status}] ${t.request} (matched: ${t._fields.join('+')})`));
|
|
1609
1911
|
}
|
|
1610
1912
|
if (hits.rules.length) {
|
|
1611
1913
|
log(`\n## ⚡ 관련 룰 (${hits.rules.length})`);
|
|
1612
|
-
hits.rules.forEach(r => log(` - ${r.id} [${r.trigger}] ${r.rule}`));
|
|
1914
|
+
hits.rules.forEach(r => log(` - .harness/rules.md:${r.line || '?'} — ${r.id} [${r.trigger}] ${r.rule}`));
|
|
1613
1915
|
}
|
|
1614
1916
|
if (hits.evidence.length) {
|
|
1615
1917
|
log(`\n## 🧪 관련 검증 기록 (${hits.evidence.length})`);
|
|
1616
|
-
hits.evidence.slice(0, 5).forEach(e => log(` - ${e.title}`));
|
|
1918
|
+
hits.evidence.slice(0, 5).forEach(e => log(` - .harness/review-evidence.md:${e.line || '?'} — ${e.title}`));
|
|
1617
1919
|
}
|
|
1618
1920
|
if (hits.lessons.length) {
|
|
1619
1921
|
log(`\n## ⚠ 같은 주제 과거 실패/롤백 (${hits.lessons.length}) — 같은 실수 방지`);
|
|
1620
|
-
hits.lessons.slice(0, 5).forEach(l => log(` - ${l.title}`));
|
|
1922
|
+
hits.lessons.slice(0, 5).forEach(l => log(` - .harness/review-evidence.md:${l.line || '?'} — ${l.title}`));
|
|
1621
1923
|
}
|
|
1622
1924
|
|
|
1623
1925
|
log(`\n## 💡 시작 전 권장 액션`);
|
|
@@ -2434,9 +2736,8 @@ function lessonsCmd(root) {
|
|
|
2434
2736
|
const tlog = exists(taskLogPath(root)) ? read(taskLogPath(root)) : '';
|
|
2435
2737
|
const handoff = exists(handoffPath(root)) ? read(handoffPath(root)) : '';
|
|
2436
2738
|
const lessons = [];
|
|
2437
|
-
// decisions: ### 블록 전체
|
|
2438
|
-
for (const block of decisions
|
|
2439
|
-
if (!block.startsWith('### ')) continue;
|
|
2739
|
+
// decisions: ### 블록 전체 (1.9.14: 코드블록/Template 제외)
|
|
2740
|
+
for (const block of _extractDecisionBlocks(decisions)) {
|
|
2440
2741
|
const m = block.match(/^### (.+)$/m);
|
|
2441
2742
|
if (!m) continue;
|
|
2442
2743
|
lessons.push({ source: 'decisions.md', title: m[1].trim(), block });
|
|
@@ -2895,9 +3196,9 @@ function viewworkInstall(root) {
|
|
|
2895
3196
|
|
|
2896
3197
|
function help() {
|
|
2897
3198
|
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
|
|
2898
|
-
leerness retro [path] [--days 7]
|
|
2899
|
-
leerness insights [path]
|
|
2900
|
-
leerness brainstorm "<주제>"
|
|
3199
|
+
leerness retro [path] [--days 7] [--all-apps] [--include p1,p2] [--json] # 회고 (1.9.13~1.9.16)
|
|
3200
|
+
leerness insights [path] [--all-apps] [--include p1,p2] [--json] # 누적 통계 (1.9.13~1.9.16)
|
|
3201
|
+
leerness brainstorm "<주제>" [--all-apps] [--include p1,p2] [--json] # 브레인스토밍 (1.9.13~1.9.16)
|
|
2901
3202
|
leerness roadmap [path] [--out file.html] # 좌→우 수평 트리 + 상하 중앙정렬 + 화이트보드 (1.9.11)
|
|
2902
3203
|
leerness roadmap auto on|off|status [--on-every-change] [--out file.html] # 자동 갱신 (1.9.12, install/session-close 기본 ON)
|
|
2903
3204
|
leerness verify-code [path] [--build] # npm test/lint/typecheck 자동 실행 + evidence 자동 기록 (1.9.7)
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -237,6 +237,179 @@ total++;
|
|
|
237
237
|
if (!(strongOK && weakHint)) failed++;
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
// 1.9.16 회귀: brainstorm --all-apps / --json / session close 워크스페이스 안내
|
|
241
|
+
total++;
|
|
242
|
+
{
|
|
243
|
+
// brainstorm --all-apps
|
|
244
|
+
const tmpA = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-bsa-'));
|
|
245
|
+
const tmpB = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-bsb-'));
|
|
246
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpA, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
247
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpB, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
248
|
+
fs.appendFileSync(path.join(tmpA, '.harness/decisions.md'), '\n### 2026-05-13 — 캐시 정책\n- Reason: rate limit\n');
|
|
249
|
+
fs.appendFileSync(path.join(tmpB, '.harness/decisions.md'), '\n### 2026-05-13 — 캐시 분산\n- Reason: 확장성\n');
|
|
250
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'brainstorm', '캐시', '--include', `${tmpA},${tmpB}`], { encoding: 'utf8', timeout: 15000, cwd: os.tmpdir() });
|
|
251
|
+
const ok = r.status === 0 && /Cross-project Brainstorm — "캐시" — 2개/.test(r.stdout) && /워크스페이스 총합: 2건/.test(r.stdout);
|
|
252
|
+
console.log(ok ? '✓ B(1.9.16) brainstorm --include 통합' : '✗ brainstorm --include 실패');
|
|
253
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 600)); }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
total++;
|
|
257
|
+
{
|
|
258
|
+
// --json 단일 brainstorm
|
|
259
|
+
const tmpJ = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-json-'));
|
|
260
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpJ, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
261
|
+
fs.appendFileSync(path.join(tmpJ, '.harness/decisions.md'), '\n### 2026-05-13 — JSON 결정\n- ...\n');
|
|
262
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'brainstorm', 'JSON', '--json', '--path', tmpJ], { encoding: 'utf8', timeout: 15000 });
|
|
263
|
+
let parsed = null;
|
|
264
|
+
try { parsed = JSON.parse(r.stdout); } catch {}
|
|
265
|
+
const ok = r.status === 0 && parsed && parsed.topic === 'JSON' && parsed.total >= 1;
|
|
266
|
+
console.log(ok ? '✓ B(1.9.16) brainstorm --json 단일' : `✗ brainstorm --json 실패\n${r.stdout.slice(0, 300)}`);
|
|
267
|
+
if (!ok) failed++;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
total++;
|
|
271
|
+
{
|
|
272
|
+
// retro --json
|
|
273
|
+
const tmpR = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-rj-'));
|
|
274
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpR, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
275
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'retro', tmpR, '--json'], { encoding: 'utf8', timeout: 15000 });
|
|
276
|
+
let parsed = null;
|
|
277
|
+
try { parsed = JSON.parse(r.stdout); } catch {}
|
|
278
|
+
const ok = r.status === 0 && parsed && parsed.summary && parsed.data;
|
|
279
|
+
console.log(ok ? '✓ B(1.9.16) retro --json' : '✗ retro --json 실패');
|
|
280
|
+
if (!ok) failed++;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
total++;
|
|
284
|
+
{
|
|
285
|
+
// insights --json (workspace)
|
|
286
|
+
const tmpA = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-iwsa-'));
|
|
287
|
+
const tmpB = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-iwsb-'));
|
|
288
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpA, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
289
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpB, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
290
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'insights', '--include', `${tmpA},${tmpB}`, '--json'], { encoding: 'utf8', timeout: 15000, cwd: os.tmpdir() });
|
|
291
|
+
let parsed = null;
|
|
292
|
+
try { parsed = JSON.parse(r.stdout); } catch {}
|
|
293
|
+
const ok = r.status === 0 && parsed && parsed.projectCount === 2 && Array.isArray(parsed.projects);
|
|
294
|
+
console.log(ok ? '✓ B(1.9.16) insights --include --json' : '✗ insights --json 실패');
|
|
295
|
+
if (!ok) failed++;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
total++;
|
|
299
|
+
{
|
|
300
|
+
// session close 끝에 워크스페이스 안내 (다른 leerness 프로젝트 시뮬)
|
|
301
|
+
const wsRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-ws-'));
|
|
302
|
+
fs.mkdirSync(path.join(wsRoot, '_apps'), { recursive: true });
|
|
303
|
+
const proj = path.join(wsRoot, 'main');
|
|
304
|
+
const other = path.join(wsRoot, '_apps', 'other');
|
|
305
|
+
cp.spawnSync(process.execPath, [CLI, 'init', proj, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
306
|
+
cp.spawnSync(process.execPath, [CLI, 'init', other, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
307
|
+
// _apps가 proj와 같은 부모 디렉토리에 있어야 감지
|
|
308
|
+
// 우리 케이스는 wsRoot/main과 wsRoot/_apps/other → main에서 ../_apps 검색하면 발견
|
|
309
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'session', 'close', proj], { encoding: 'utf8', timeout: 15000 });
|
|
310
|
+
const ok = /워크스페이스에 \d+개 다른 leerness 프로젝트/.test(r.stdout);
|
|
311
|
+
console.log(ok ? '✓ B(1.9.16) session close 워크스페이스 안내' : '✗ session close 안내 실패');
|
|
312
|
+
if (!ok) { failed++; console.log(r.stdout.slice(-500)); }
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// 1.9.15 회귀: brainstorm 라인번호 / --all-apps / --include
|
|
316
|
+
total++;
|
|
317
|
+
{
|
|
318
|
+
const tmpL = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-line-'));
|
|
319
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpL, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
320
|
+
fs.appendFileSync(path.join(tmpL, '.harness/decisions.md'), '\n### 2026-05-13 — 캐시 정책 결정\n- Reason: rate limit 회피\n');
|
|
321
|
+
cp.spawnSync(process.execPath, [CLI, 'plan', 'add', '캐시 helper 구현', '--path', tmpL], { stdio: 'ignore', timeout: 10000 });
|
|
322
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'brainstorm', '캐시', '--path', tmpL], { encoding: 'utf8', timeout: 15000 });
|
|
323
|
+
const ok = /\.harness\/decisions\.md:\d+/.test(r.stdout) && /\.harness\/progress-tracker\.md:\d+/.test(r.stdout) && /matched: request/.test(r.stdout);
|
|
324
|
+
console.log(ok ? '✓ B(1.9.15) brainstorm: 파일:라인 + 매치 필드 표시' : `✗ 1.9.15 brainstorm 위치 실패\n${r.stdout.slice(0,500)}`);
|
|
325
|
+
if (!ok) failed++;
|
|
326
|
+
}
|
|
327
|
+
total++;
|
|
328
|
+
{
|
|
329
|
+
// --include 다중 경로 통합
|
|
330
|
+
const tmpA = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-wsA-'));
|
|
331
|
+
const tmpB = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-wsB-'));
|
|
332
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpA, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
333
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpB, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
334
|
+
cp.spawnSync(process.execPath, [CLI, 'plan', 'add', 'A 작업', '--status', 'done', '--path', tmpA], { stdio: 'ignore', timeout: 10000 });
|
|
335
|
+
cp.spawnSync(process.execPath, [CLI, 'plan', 'add', 'B 작업', '--status', 'planned', '--path', tmpB], { stdio: 'ignore', timeout: 10000 });
|
|
336
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'retro', '--include', `${tmpA},${tmpB}`], { encoding: 'utf8', timeout: 15000, cwd: os.tmpdir() });
|
|
337
|
+
const ok = r.status === 0 && /Cross-project retro — 2개 프로젝트/.test(r.stdout) && /워크스페이스 총합/.test(r.stdout);
|
|
338
|
+
console.log(ok ? '✓ B(1.9.15) retro --include: 2개 통합' : '✗ 1.9.15 retro --include 실패');
|
|
339
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 600)); }
|
|
340
|
+
}
|
|
341
|
+
total++;
|
|
342
|
+
{
|
|
343
|
+
// insights --include
|
|
344
|
+
const tmpA = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-iA-'));
|
|
345
|
+
const tmpB = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-iB-'));
|
|
346
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpA, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
347
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpB, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
348
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'insights', '--include', `${tmpA},${tmpB}`], { encoding: 'utf8', timeout: 15000, cwd: os.tmpdir() });
|
|
349
|
+
const ok = r.status === 0 && /Workspace Insights — 2개/.test(r.stdout) && /TOTAL/.test(r.stdout);
|
|
350
|
+
console.log(ok ? '✓ B(1.9.15) insights --include: 표 형식 통합' : '✗ 1.9.15 insights --include 실패');
|
|
351
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 600)); }
|
|
352
|
+
}
|
|
353
|
+
total++;
|
|
354
|
+
{
|
|
355
|
+
// 잘못된 --include 경로 — warn 출력 + .harness 있는 것만 처리
|
|
356
|
+
const tmpA = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-bad-'));
|
|
357
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpA, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
358
|
+
const bad = '/tmp/nonexistent-leerness-' + Date.now();
|
|
359
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'retro', '--include', `${tmpA},${bad}`], { encoding: 'utf8', timeout: 15000, cwd: os.tmpdir() });
|
|
360
|
+
const ok = r.status === 0 && /--include 무시/.test(r.stdout) && /Cross-project retro — 1개/.test(r.stdout);
|
|
361
|
+
console.log(ok ? '✓ B(1.9.15) --include 잘못된 경로 graceful skip' : '✗ 1.9.15 bad path 처리 실패');
|
|
362
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// 1.9.14 회귀: A(Template 제외) / B(word boundary) / C(planned 포함) / D(코드블록 템플릿)
|
|
366
|
+
total++;
|
|
367
|
+
{
|
|
368
|
+
// A: init 직후 decisions.md의 Template이 결정으로 카운트되지 않아야 함
|
|
369
|
+
const tmpA = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-A-'));
|
|
370
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpA, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
371
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'insights', tmpA], { encoding: 'utf8', timeout: 15000 });
|
|
372
|
+
const ok = r.status === 0 && /누적 결정 \(decisions\.md\): 0건/.test(r.stdout);
|
|
373
|
+
console.log(ok ? '✓ B(1.9.14-A) Template 제외: 누적 결정 0건' : `✗ A 실패\n${r.stdout.slice(0, 500)}`);
|
|
374
|
+
if (!ok) failed++;
|
|
375
|
+
}
|
|
376
|
+
total++;
|
|
377
|
+
{
|
|
378
|
+
// B: brainstorm 토큰 매칭 — "API"는 매치, "AP"는 부분 매치라 안 잡힘
|
|
379
|
+
const tmpB = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-B-'));
|
|
380
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpB, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
381
|
+
fs.appendFileSync(path.join(tmpB, '.harness/decisions.md'), '\n### 2026-05-13 — API rate limit 정책\n- Reason: ...\n');
|
|
382
|
+
// "limit" 매치
|
|
383
|
+
const r1 = cp.spawnSync(process.execPath, [CLI, 'brainstorm', 'limit', '--path', tmpB], { encoding: 'utf8', timeout: 15000 });
|
|
384
|
+
// "lim" 부분 매치 — 매치되면 안 됨
|
|
385
|
+
const r2 = cp.spawnSync(process.execPath, [CLI, 'brainstorm', 'lim', '--path', tmpB], { encoding: 'utf8', timeout: 15000 });
|
|
386
|
+
const ok = /총 1건/.test(r1.stdout) && /총 0건/.test(r2.stdout);
|
|
387
|
+
console.log(ok ? '✓ B(1.9.14-B) brainstorm word boundary: limit 매치 / lim 부분매치 안 잡힘' : `✗ B 실패\n${r1.stdout.slice(0, 200)}\n${r2.stdout.slice(0, 200)}`);
|
|
388
|
+
if (!ok) failed++;
|
|
389
|
+
}
|
|
390
|
+
total++;
|
|
391
|
+
{
|
|
392
|
+
// C: retro 다음 우선 작업에 planned 포함
|
|
393
|
+
const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-C-'));
|
|
394
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
395
|
+
// 모든 task 제거 후 planned만 추가
|
|
396
|
+
fs.writeFileSync(path.join(tmpC, '.harness/progress-tracker.md'), `# Progress Tracker\nStatus values: requested, planned, in-progress, waiting, on-hold, blocked, incomplete, done, dropped\n\n| ID | Status | Request | Evidence | Next Action | Updated |\n|---|---|---|---|---|---|\n| T-0001 | planned | 미래 작업 | plan:M-0001 | 시작 예정 | 2026-05-13 |\n`);
|
|
397
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'retro', tmpC], { encoding: 'utf8', timeout: 15000 });
|
|
398
|
+
const ok = r.status === 0 && /T-0001 \[planned\]/.test(r.stdout) && !/없음 — 새 plan add 권장/.test(r.stdout);
|
|
399
|
+
console.log(ok ? '✓ B(1.9.14-C) retro 다음 우선 작업에 planned 포함' : '✗ C 실패');
|
|
400
|
+
if (!ok) { failed++; console.log(r.stdout.slice(0, 600)); }
|
|
401
|
+
}
|
|
402
|
+
total++;
|
|
403
|
+
{
|
|
404
|
+
// D: init decisions.md가 ```md 코드블록으로 감싸짐
|
|
405
|
+
const tmpD = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-D-'));
|
|
406
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmpD, '--yes', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
|
|
407
|
+
const dec = fs.readFileSync(path.join(tmpD, '.harness/decisions.md'), 'utf8');
|
|
408
|
+
const ok = /```md\n### \d{4}-\d{2}-\d{2} — Decision/.test(dec) && /^## Template/m.test(dec);
|
|
409
|
+
console.log(ok ? '✓ B(1.9.14-D) decisions.md template 코드블록 감싸짐' : '✗ D 실패');
|
|
410
|
+
if (!ok) { failed++; console.log(dec.slice(0, 400)); }
|
|
411
|
+
}
|
|
412
|
+
|
|
240
413
|
// 1.9.13: retro / insights / brainstorm
|
|
241
414
|
total++;
|
|
242
415
|
{
|