leerness 1.9.13 → 1.9.18
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 +97 -0
- package/bin/harness.js +693 -37
- package/package.json +1 -1
- package/scripts/e2e.js +328 -0
package/bin/harness.js
CHANGED
|
@@ -6,7 +6,7 @@ const path = require('path');
|
|
|
6
6
|
const cp = require('child_process');
|
|
7
7
|
const readline = require('readline');
|
|
8
8
|
|
|
9
|
-
const VERSION = '1.9.
|
|
9
|
+
const VERSION = '1.9.18';
|
|
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','--since']);
|
|
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`),
|
|
@@ -1200,6 +1200,359 @@ function handoff(root) {
|
|
|
1200
1200
|
ok('handoff loaded; current-state updated');
|
|
1201
1201
|
}
|
|
1202
1202
|
|
|
1203
|
+
// 1.9.18: --since 파서 ("24h", "3d", "1w", "30m") → cutoff ISO date
|
|
1204
|
+
function _parseSince(s) {
|
|
1205
|
+
if (!s) return null;
|
|
1206
|
+
const m = String(s).match(/^(\d+(?:\.\d+)?)\s*([mhdw])$/i);
|
|
1207
|
+
if (!m) return null;
|
|
1208
|
+
const n = parseFloat(m[1]);
|
|
1209
|
+
const unit = m[2].toLowerCase();
|
|
1210
|
+
const ms = unit === 'm' ? n * 60 * 1000
|
|
1211
|
+
: unit === 'h' ? n * 3600 * 1000
|
|
1212
|
+
: unit === 'd' ? n * 86400 * 1000
|
|
1213
|
+
: /* w */ n * 7 * 86400 * 1000;
|
|
1214
|
+
const cutoff = new Date(Date.now() - ms);
|
|
1215
|
+
return cutoff.toISOString().slice(0, 10); // YYYY-MM-DD
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// 1.9.17→1.9.18: 워크스페이스 통합 handoff — 4개 agent 동시 작업 시 메인 agent용 한 줄 요약
|
|
1219
|
+
// 1.9.18: --since <duration> 추가, 최근 수정된 T-row 강조 (🆕 마크 + 별도 섹션)
|
|
1220
|
+
function _handoffWorkspace(rootBase) {
|
|
1221
|
+
const paths = _collectWorkspacePaths(rootBase);
|
|
1222
|
+
if (!paths.length) return fail('대상 프로젝트 없음. --include 또는 --all-apps 사용.');
|
|
1223
|
+
const sinceArg = arg('--since', null);
|
|
1224
|
+
const sinceCutoff = sinceArg ? _parseSince(sinceArg) : null;
|
|
1225
|
+
if (sinceArg && !sinceCutoff) { fail(`--since 형식 오류: "${sinceArg}" (예: 24h, 3d, 1w)`); return process.exit(1); }
|
|
1226
|
+
|
|
1227
|
+
function isRecent(row) {
|
|
1228
|
+
if (!sinceCutoff || !row.updated) return false;
|
|
1229
|
+
return row.updated >= sinceCutoff;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
if (has('--json')) {
|
|
1233
|
+
const projects = paths.map(p => {
|
|
1234
|
+
const rows = readProgressRows(p);
|
|
1235
|
+
const buckets = {};
|
|
1236
|
+
for (const r of rows) (buckets[r.status] || (buckets[r.status] = [])).push(r);
|
|
1237
|
+
const activeRules = readRules(p).filter(r => r.status === 'active').length;
|
|
1238
|
+
const recent = sinceCutoff ? rows.filter(isRecent) : [];
|
|
1239
|
+
return {
|
|
1240
|
+
project: path.basename(p),
|
|
1241
|
+
path: p,
|
|
1242
|
+
total: rows.length,
|
|
1243
|
+
done: (buckets['done'] || []).length,
|
|
1244
|
+
inProgress: (buckets['in-progress'] || []).length,
|
|
1245
|
+
planned: (buckets['planned'] || []).length,
|
|
1246
|
+
blocked: (buckets['blocked'] || []).length,
|
|
1247
|
+
activeRules,
|
|
1248
|
+
nextAction: (buckets['in-progress']?.[0]?.nextAction) || (buckets['planned']?.[0]?.nextAction) || (buckets['requested']?.[0]?.nextAction) || null,
|
|
1249
|
+
recent: recent.map(r => ({ id: r.id, status: r.status, request: r.request, updated: r.updated }))
|
|
1250
|
+
};
|
|
1251
|
+
});
|
|
1252
|
+
log(JSON.stringify({ workspace: path.basename(rootBase), since: sinceCutoff, projects, totals: {
|
|
1253
|
+
tasks: projects.reduce((a, b) => a + b.total, 0),
|
|
1254
|
+
done: projects.reduce((a, b) => a + b.done, 0),
|
|
1255
|
+
inProgress: projects.reduce((a, b) => a + b.inProgress, 0),
|
|
1256
|
+
blocked: projects.reduce((a, b) => a + b.blocked, 0),
|
|
1257
|
+
recent: projects.reduce((a, b) => a + (b.recent?.length || 0), 0)
|
|
1258
|
+
} }, null, 2));
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
log(`# Workspace Handoff — ${paths.length}개 프로젝트 (1.9.18)`);
|
|
1262
|
+
log(`Date: ${today()}`);
|
|
1263
|
+
if (sinceCutoff) log(`Filter: since ${sinceArg} (${sinceCutoff} 이후 수정된 항목 🆕 강조)`);
|
|
1264
|
+
log('');
|
|
1265
|
+
log('## 프로젝트별 진행 상태');
|
|
1266
|
+
let totalDone = 0, totalTasks = 0, totalWIP = 0, totalBlocked = 0, totalRecent = 0;
|
|
1267
|
+
const allRecent = [];
|
|
1268
|
+
for (const p of paths) {
|
|
1269
|
+
const rows = readProgressRows(p);
|
|
1270
|
+
const buckets = {};
|
|
1271
|
+
for (const r of rows) (buckets[r.status] || (buckets[r.status] = [])).push(r);
|
|
1272
|
+
const done = (buckets['done'] || []).length;
|
|
1273
|
+
const wip = (buckets['in-progress'] || []).length;
|
|
1274
|
+
const planned = (buckets['planned'] || []).length;
|
|
1275
|
+
const blocked = (buckets['blocked'] || []).length;
|
|
1276
|
+
const recent = sinceCutoff ? rows.filter(isRecent) : [];
|
|
1277
|
+
totalDone += done; totalTasks += rows.length; totalWIP += wip; totalBlocked += blocked; totalRecent += recent.length;
|
|
1278
|
+
for (const r of recent) allRecent.push({ project: path.basename(p), ...r });
|
|
1279
|
+
const nx = (buckets['in-progress']?.[0]) || (buckets['planned']?.[0]) || null;
|
|
1280
|
+
const pct = rows.length ? Math.round(done / rows.length * 100) : 0;
|
|
1281
|
+
const recentBadge = recent.length ? ` · 🆕 ${recent.length}` : '';
|
|
1282
|
+
log(` ${path.basename(p)}: ${done}/${rows.length} (${pct}%) · WIP ${wip} · planned ${planned}${blocked ? ` · 🚫 blocked ${blocked}` : ''}${recentBadge}`);
|
|
1283
|
+
if (nx) log(` └ 다음: ${nx.id} [${nx.status}] ${nx.nextAction || nx.request}`);
|
|
1284
|
+
}
|
|
1285
|
+
// 1.9.18: --since 모드일 때 최근 추가/수정 섹션
|
|
1286
|
+
if (sinceCutoff) {
|
|
1287
|
+
log('');
|
|
1288
|
+
log(`## 🆕 최근 변경 (${sinceArg} 내, ${totalRecent}건)`);
|
|
1289
|
+
if (!totalRecent) log(` (없음) — ${sinceCutoff} 이후 progress-tracker 업데이트 없음`);
|
|
1290
|
+
else {
|
|
1291
|
+
for (const r of allRecent) log(` - ${r.project}/${r.id} [${r.status}] ${r.request} (updated ${r.updated})`);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
log('');
|
|
1295
|
+
log(`## 📊 워크스페이스 총합`);
|
|
1296
|
+
log(` - 누적 task: ${totalTasks} (done ${totalDone}, ${totalTasks ? Math.round(totalDone / totalTasks * 100) : 0}%)`);
|
|
1297
|
+
log(` - 진행중 (WIP): ${totalWIP} · 차단: ${totalBlocked}${sinceCutoff ? ` · 🆕 최근 ${totalRecent}` : ''}`);
|
|
1298
|
+
if (totalBlocked > 0) log(` - ⚠ ${totalBlocked}건이 blocked — 우선 처리 검토`);
|
|
1299
|
+
log('');
|
|
1300
|
+
log(`## 💡 멀티에이전트 오케스트레이션 권장`);
|
|
1301
|
+
log(` - 각 프로젝트의 "다음" 작업을 sub-agent 1명씩 병렬 진행 가능`);
|
|
1302
|
+
log(` - 새 패턴 추가 시 \`leerness reuse-map --all-apps\`로 중복 감지${sinceCutoff ? '' : ' / `--since 24h`로 최근 변경 추적'}`);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function handoffCmd(root) {
|
|
1306
|
+
// 1.9.17: --all-apps / --include 통합 모드
|
|
1307
|
+
if (has('--all-apps') || arg('--include', null)) {
|
|
1308
|
+
return _handoffWorkspace(absRoot(root));
|
|
1309
|
+
}
|
|
1310
|
+
return handoff(root);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// 1.9.17: 워크스페이스 통합 reuse-map — Capability 중복 자동 감지
|
|
1314
|
+
// 1.9.18: element에서 함수명 추출, notes에서 depends-on 추출
|
|
1315
|
+
function _extractFunctionName(element) {
|
|
1316
|
+
// "src/build.js (escapeHtml)" → "escapeHtml"
|
|
1317
|
+
// "src/openMeteo.js (fetchBatch)" → "fetchBatch"
|
|
1318
|
+
// "src/cities.js" → null
|
|
1319
|
+
const m = String(element).match(/\(([A-Za-z_$][\w$]*)\s*\)?\s*$/);
|
|
1320
|
+
return m ? m[1] : null;
|
|
1321
|
+
}
|
|
1322
|
+
function _extractFilePath(element) {
|
|
1323
|
+
// "src/build.js (escapeHtml)" → "src/build.js"
|
|
1324
|
+
// "src/cities.js" → "src/cities.js"
|
|
1325
|
+
const m = String(element).match(/^([^\s(]+)/);
|
|
1326
|
+
return m ? m[1] : null;
|
|
1327
|
+
}
|
|
1328
|
+
function _extractDependsOn(notes) {
|
|
1329
|
+
// notes 컬럼에서 "depends-on: A, B" 또는 "depends: A" 패턴 추출
|
|
1330
|
+
const m = String(notes).match(/depends(?:-on)?:\s*([^|]+?)(?:\s*\)|$)/i);
|
|
1331
|
+
if (!m) return [];
|
|
1332
|
+
return m[1].split(/[,;]/).map(s => s.trim()).filter(Boolean);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
function _readReuseMap(root) {
|
|
1336
|
+
const p = path.join(root, '.harness', 'reuse-map.md');
|
|
1337
|
+
if (!exists(p)) return [];
|
|
1338
|
+
const txt = read(p);
|
|
1339
|
+
const lines = txt.split('\n');
|
|
1340
|
+
const out = [];
|
|
1341
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1342
|
+
const l = lines[i].trim();
|
|
1343
|
+
// skip header + separator + empty
|
|
1344
|
+
if (!l.startsWith('|') || l.startsWith('|--') || /^\|\s*Capability\s*\|/i.test(l)) continue;
|
|
1345
|
+
const cells = l.split('|').map(c => c.trim()).filter((_, idx, arr) => idx !== 0 && idx !== arr.length - 1);
|
|
1346
|
+
if (cells.length < 2 || !cells[0]) continue;
|
|
1347
|
+
const notes = cells[3] || '';
|
|
1348
|
+
out.push({
|
|
1349
|
+
capability: cells[0],
|
|
1350
|
+
element: cells[1] || '',
|
|
1351
|
+
method: cells[2] || '',
|
|
1352
|
+
notes,
|
|
1353
|
+
line: i + 1,
|
|
1354
|
+
// 1.9.18: 파생 필드
|
|
1355
|
+
functionName: _extractFunctionName(cells[1] || ''),
|
|
1356
|
+
filePath: _extractFilePath(cells[1] || ''),
|
|
1357
|
+
dependsOn: _extractDependsOn(notes)
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
return out;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function reuseMapCmd(root) {
|
|
1364
|
+
root = absRoot(root || process.cwd());
|
|
1365
|
+
// 단일 프로젝트 모드
|
|
1366
|
+
if (!has('--all-apps') && !arg('--include', null)) {
|
|
1367
|
+
const entries = _readReuseMap(root);
|
|
1368
|
+
if (has('--json')) { log(JSON.stringify({ project: path.basename(root), entries }, null, 2)); return; }
|
|
1369
|
+
log(`# Reuse Map — ${path.basename(root)} (${entries.length}개)`);
|
|
1370
|
+
if (!entries.length) { log(' (없음) — 새 컴포넌트/유틸 추가 후 등록 권장'); return; }
|
|
1371
|
+
entries.forEach(e => {
|
|
1372
|
+
const dep = e.dependsOn.length ? ` ← depends: ${e.dependsOn.join(', ')}` : '';
|
|
1373
|
+
log(` - ${e.capability} → ${e.element} [${e.method}] ${e.notes}${dep}`);
|
|
1374
|
+
});
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
// 워크스페이스 모드
|
|
1378
|
+
const paths = _collectWorkspacePaths(root);
|
|
1379
|
+
if (!paths.length) return fail('대상 프로젝트 없음. --include 또는 --all-apps 사용.');
|
|
1380
|
+
const strictElements = has('--strict-elements');
|
|
1381
|
+
const byCap = new Map(); // capability(lowercase) → [{ project, entry }]
|
|
1382
|
+
const byFunc = new Map(); // functionName → [{ project, entry }] // 1.9.18
|
|
1383
|
+
const dependsEdges = []; // 1.9.18: { from: {project, cap}, to: cap }
|
|
1384
|
+
const projects = paths.map(p => {
|
|
1385
|
+
const entries = _readReuseMap(p);
|
|
1386
|
+
for (const e of entries) {
|
|
1387
|
+
const k = e.capability.toLowerCase().trim();
|
|
1388
|
+
if (!byCap.has(k)) byCap.set(k, []);
|
|
1389
|
+
byCap.get(k).push({ project: path.basename(p), path: p, entry: e });
|
|
1390
|
+
// 1.9.18: function-name 인덱스
|
|
1391
|
+
if (e.functionName) {
|
|
1392
|
+
const fk = e.functionName.toLowerCase();
|
|
1393
|
+
if (!byFunc.has(fk)) byFunc.set(fk, []);
|
|
1394
|
+
byFunc.get(fk).push({ project: path.basename(p), path: p, entry: e });
|
|
1395
|
+
}
|
|
1396
|
+
// 1.9.18: depends-on 엣지
|
|
1397
|
+
for (const dep of e.dependsOn) {
|
|
1398
|
+
dependsEdges.push({ from: { project: path.basename(p), cap: e.capability }, to: dep });
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
return { project: path.basename(p), path: p, entries };
|
|
1402
|
+
});
|
|
1403
|
+
// exact capability 중복
|
|
1404
|
+
const dupes = [...byCap.entries()].filter(([, occ]) => occ.length >= 2);
|
|
1405
|
+
// 1.9.18: --strict-elements: 같은 함수명이 다른 capability로 등록된 경우 잠재 중복
|
|
1406
|
+
const funcDupes = strictElements ? [...byFunc.entries()].filter(([, occ]) => {
|
|
1407
|
+
if (occ.length < 2) return false;
|
|
1408
|
+
// 정확 capability 중복이 아닌 경우만 (이미 dupes로 잡힌 건 제외)
|
|
1409
|
+
const caps = new Set(occ.map(o => o.entry.capability.toLowerCase()));
|
|
1410
|
+
return caps.size >= 2;
|
|
1411
|
+
}) : [];
|
|
1412
|
+
|
|
1413
|
+
if (has('--json')) {
|
|
1414
|
+
const duplicates = dupes.map(([cap, occ]) => ({ capability: cap, occurrences: occ.length, projects: occ.map(o => o.project) }));
|
|
1415
|
+
const fuzzyDuplicates = funcDupes.map(([fn, occ]) => ({
|
|
1416
|
+
functionName: fn,
|
|
1417
|
+
occurrences: occ.length,
|
|
1418
|
+
entries: occ.map(o => ({ project: o.project, capability: o.entry.capability, element: o.entry.element }))
|
|
1419
|
+
}));
|
|
1420
|
+
log(JSON.stringify({
|
|
1421
|
+
workspace: path.basename(root),
|
|
1422
|
+
projects,
|
|
1423
|
+
duplicates,
|
|
1424
|
+
fuzzyDuplicates,
|
|
1425
|
+
dependsEdges,
|
|
1426
|
+
totalCapabilities: byCap.size,
|
|
1427
|
+
strictElements
|
|
1428
|
+
}, null, 2));
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
log(`# Workspace Reuse Map — ${paths.length}개 프로젝트 / ${byCap.size}개 capability (1.9.18)`);
|
|
1432
|
+
log('');
|
|
1433
|
+
log(`## 프로젝트별 등록 수`);
|
|
1434
|
+
projects.forEach(p => log(` ${p.project}: ${p.entries.length}개`));
|
|
1435
|
+
|
|
1436
|
+
log('');
|
|
1437
|
+
log(`## 🔁 정확 중복 capability (이름 동일)`);
|
|
1438
|
+
if (!dupes.length) log(' (없음) — 모든 capability 이름이 단일 프로젝트에만 존재');
|
|
1439
|
+
else {
|
|
1440
|
+
for (const [cap, occ] of dupes) {
|
|
1441
|
+
log(` - "${occ[0].entry.capability}" — ${occ.length}개 프로젝트`);
|
|
1442
|
+
for (const o of occ) log(` · ${o.project}: ${o.entry.element} [${o.entry.method}]`);
|
|
1443
|
+
}
|
|
1444
|
+
log('');
|
|
1445
|
+
log(` 💡 권장: 가장 안정적인 1개 구현을 추출해 ${dupes.length}건 중복을 공통 모듈로 통합 검토`);
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// 1.9.18: --strict-elements 결과
|
|
1449
|
+
if (strictElements) {
|
|
1450
|
+
log('');
|
|
1451
|
+
log(`## 🔍 잠재 중복 (--strict-elements: 함수명 동일 / capability 이름 다름)`);
|
|
1452
|
+
if (!funcDupes.length) log(' (없음) — 동일 함수명을 다른 capability로 등록한 경우 없음');
|
|
1453
|
+
else {
|
|
1454
|
+
for (const [fn, occ] of funcDupes) {
|
|
1455
|
+
log(` - 함수 "${fn}()" — ${occ.length}건 (이름 다름)`);
|
|
1456
|
+
for (const o of occ) log(` · ${o.project}/${o.entry.capability}: ${o.entry.element}`);
|
|
1457
|
+
}
|
|
1458
|
+
log('');
|
|
1459
|
+
log(` 💡 같은 함수를 다른 capability 이름으로 부르고 있을 가능성. 명명 통일 검토.`);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// 1.9.18: depends-on 그래프
|
|
1464
|
+
if (dependsEdges.length) {
|
|
1465
|
+
log('');
|
|
1466
|
+
log(`## 🔗 의존 관계 (depends-on, ${dependsEdges.length}개 엣지)`);
|
|
1467
|
+
for (const e of dependsEdges) log(` - ${e.from.project}/${e.from.cap} ─→ ${e.to}`);
|
|
1468
|
+
log('');
|
|
1469
|
+
log(` 💡 의존 capability는 제거하지 말 것. depends-on 표기: \`notes\` 컬럼에 "depends-on: A, B"`);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
log('');
|
|
1473
|
+
const fuzzyCount = funcDupes.length;
|
|
1474
|
+
log(`## 📊 워크스페이스 총합: capability ${byCap.size}건 / 정확 중복 ${dupes.length}건${strictElements ? ` / 잠재 중복 ${fuzzyCount}건` : ''} / 의존 ${dependsEdges.length}건`);
|
|
1475
|
+
if (!strictElements) log(` 💡 \`--strict-elements\`로 함수명 기반 잠재 중복도 탐지 가능`);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
// 1.9.18: verify-claim — progress-tracker의 evidence 컬럼 자동 검증
|
|
1479
|
+
// "src/foo.js + 5개 테스트 (54/54 통과)" 같은 주장을 파싱해 실제 파일/카운트 확인
|
|
1480
|
+
function verifyClaimCmd(root, taskId) {
|
|
1481
|
+
root = absRoot(root);
|
|
1482
|
+
if (!taskId) return fail('verify-claim <T-ID> 필요. 예: leerness verify-claim T-0008');
|
|
1483
|
+
const rows = readProgressRows(root);
|
|
1484
|
+
const row = rows.find(r => r.id === taskId);
|
|
1485
|
+
if (!row) return fail(`progress-tracker.md에 ${taskId} 없음.`);
|
|
1486
|
+
|
|
1487
|
+
const evidence = row.evidence || '';
|
|
1488
|
+
// 파일 경로 추출: src/x.js, bin/y.js, tests/z.js 등
|
|
1489
|
+
const filePatterns = evidence.match(/(?:src|bin|tests|public|lib)\/[\w./-]+\.(?:js|ts|html|css|json|md|webmanifest|xml)/g) || [];
|
|
1490
|
+
const files = Array.from(new Set(filePatterns));
|
|
1491
|
+
// 테스트 수 / pass 비율: "X/Y 통과" 또는 "X개 테스트"
|
|
1492
|
+
const passMatch = evidence.match(/(\d+)\s*\/\s*(\d+)\s*(통과|passed|pass)/);
|
|
1493
|
+
const testCountMatch = evidence.match(/(\d+)\s*개\s*테스트/);
|
|
1494
|
+
const declaredPass = passMatch ? { num: parseInt(passMatch[1], 10), denom: parseInt(passMatch[2], 10) } : null;
|
|
1495
|
+
const declaredTestCount = testCountMatch ? parseInt(testCountMatch[1], 10) : null;
|
|
1496
|
+
|
|
1497
|
+
// 실제 파일 존재 검사
|
|
1498
|
+
const fileChecks = files.map(f => ({ file: f, exists: exists(path.join(root, f)) }));
|
|
1499
|
+
// 테스트 카운트: tests/test.js의 check( 또는 it( 또는 test( 개수
|
|
1500
|
+
let actualTestCount = null;
|
|
1501
|
+
const candidateTestFiles = ['tests/test.js', 'test/test.js', 'tests/index.js'];
|
|
1502
|
+
for (const tf of candidateTestFiles) {
|
|
1503
|
+
const tp = path.join(root, tf);
|
|
1504
|
+
if (exists(tp)) {
|
|
1505
|
+
const t = read(tp);
|
|
1506
|
+
actualTestCount = (t.match(/\bcheck\s*\(/g) || t.match(/\b(it|test)\s*\(/g) || []).length;
|
|
1507
|
+
break;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
if (has('--json')) {
|
|
1512
|
+
log(JSON.stringify({
|
|
1513
|
+
project: path.basename(root),
|
|
1514
|
+
taskId, row,
|
|
1515
|
+
declared: { files: files.length, pass: declaredPass, testCount: declaredTestCount },
|
|
1516
|
+
actual: { fileChecks, testCount: actualTestCount },
|
|
1517
|
+
verdict: {
|
|
1518
|
+
filesAllExist: fileChecks.every(c => c.exists),
|
|
1519
|
+
testCountMatch: declaredTestCount == null || actualTestCount == null || actualTestCount >= declaredTestCount
|
|
1520
|
+
}
|
|
1521
|
+
}, null, 2));
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
log(`# verify-claim ${taskId} (${path.basename(root)})`);
|
|
1526
|
+
log(`Request: ${row.request}`);
|
|
1527
|
+
log(`Status: ${row.status} · Updated: ${row.updated}`);
|
|
1528
|
+
log(`Evidence: ${evidence.slice(0, 200)}${evidence.length > 200 ? '…' : ''}`);
|
|
1529
|
+
log('');
|
|
1530
|
+
log(`## 📂 파일 검증 (${files.length}건 주장)`);
|
|
1531
|
+
if (!files.length) log(' (evidence에서 파일 경로를 추출하지 못함)');
|
|
1532
|
+
else {
|
|
1533
|
+
for (const c of fileChecks) log(` ${c.exists ? '✓' : '✗'} ${c.file}${c.exists ? '' : ' ← 누락'}`);
|
|
1534
|
+
}
|
|
1535
|
+
log('');
|
|
1536
|
+
log(`## 🧪 테스트 카운트`);
|
|
1537
|
+
if (declaredPass) log(` 주장 (pass): ${declaredPass.num}/${declaredPass.denom}`);
|
|
1538
|
+
if (declaredTestCount) log(` 주장 (개수): ${declaredTestCount}개`);
|
|
1539
|
+
if (actualTestCount != null) log(` 실측: tests/test.js에 ${actualTestCount}개 check/test 호출`);
|
|
1540
|
+
else log(` 실측: 테스트 파일 못 찾음 (tests/test.js 등)`);
|
|
1541
|
+
log('');
|
|
1542
|
+
const allFilesOk = fileChecks.every(c => c.exists);
|
|
1543
|
+
const testOk = declaredTestCount == null || actualTestCount == null || actualTestCount >= declaredTestCount;
|
|
1544
|
+
log(`## 종합`);
|
|
1545
|
+
log(` - 파일 모두 존재: ${allFilesOk ? '✓ pass' : '✗ FAIL (일부 누락)'}`);
|
|
1546
|
+
log(` - 테스트 카운트: ${testOk ? '✓ pass (실측 ≥ 주장)' : '⚠ 주장보다 적음'}`);
|
|
1547
|
+
if (!allFilesOk || !testOk) {
|
|
1548
|
+
log('');
|
|
1549
|
+
log(` ⚠ evidence 주장과 실제가 일치하지 않음 — task 상태 재검토 권장`);
|
|
1550
|
+
return process.exit(1);
|
|
1551
|
+
}
|
|
1552
|
+
log('');
|
|
1553
|
+
log(` ✓ evidence 주장이 실제 파일·테스트와 일치`);
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1203
1556
|
function sessionClose(root) {
|
|
1204
1557
|
root = absRoot(root);
|
|
1205
1558
|
const rows = readProgressRows(root);
|
|
@@ -1297,6 +1650,22 @@ function sessionClose(root) {
|
|
|
1297
1650
|
const left = 5 - (sc.count % 5);
|
|
1298
1651
|
log(` 💡 ${left}세션 후 자동 깊은 회고 — \`leerness retro\`로 즉시 실행 가능`);
|
|
1299
1652
|
}
|
|
1653
|
+
// 1.9.16: 워크스페이스 안내 (다른 leerness 프로젝트가 있으면)
|
|
1654
|
+
try {
|
|
1655
|
+
const wsCands = [path.resolve(root, '_apps'), path.resolve(root, '..', '_apps')];
|
|
1656
|
+
let wsCount = 0;
|
|
1657
|
+
for (const base of wsCands) {
|
|
1658
|
+
if (!exists(base)) continue;
|
|
1659
|
+
try { if (!fs.statSync(base).isDirectory()) continue; } catch { continue; }
|
|
1660
|
+
for (const e of fs.readdirSync(base)) {
|
|
1661
|
+
try {
|
|
1662
|
+
const p = path.join(base, e);
|
|
1663
|
+
if (fs.statSync(p).isDirectory() && exists(path.join(p, '.harness')) && p !== root) wsCount++;
|
|
1664
|
+
} catch {}
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
if (wsCount > 0) log(` 🌐 워크스페이스에 ${wsCount}개 다른 leerness 프로젝트 — \`leerness retro --all-apps\`로 통합 회고`);
|
|
1668
|
+
} catch {}
|
|
1300
1669
|
} catch (e) {
|
|
1301
1670
|
warn('retro 요약 실패: ' + (e && e.message ? e.message : e));
|
|
1302
1671
|
}
|
|
@@ -1356,6 +1725,15 @@ function readSessionCounter(root) {
|
|
|
1356
1725
|
}
|
|
1357
1726
|
function writeSessionCounter(root, c) { writeUtf8(sessionCounterPath(root), JSON.stringify(c, null, 2) + '\n'); }
|
|
1358
1727
|
|
|
1728
|
+
// 1.9.14 A/D: 결정 블록 추출 — 코드 블록 안의 ### + Template 제외
|
|
1729
|
+
function _extractDecisionBlocks(text) {
|
|
1730
|
+
// 줄 시작의 ```부터 줄 시작의 ```까지를 코드블록으로 인식 (인라인 백틱 무시)
|
|
1731
|
+
const cleaned = String(text || '').replace(/^```[^\n]*\n[\s\S]*?\n```\s*$/gm, '');
|
|
1732
|
+
return cleaned.split(/\n(?=### )/).filter(b =>
|
|
1733
|
+
b.startsWith('### ') && !/^### (Template|템플릿)\b/.test(b.trim())
|
|
1734
|
+
);
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1359
1737
|
function _retroAggregate(root) {
|
|
1360
1738
|
root = absRoot(root);
|
|
1361
1739
|
const rows = readProgressRows(root);
|
|
@@ -1369,8 +1747,8 @@ function _retroAggregate(root) {
|
|
|
1369
1747
|
for (const s of STATUSES) statusCounts[s] = 0;
|
|
1370
1748
|
for (const r of rows) if (statusCounts[r.status] != null) statusCounts[r.status]++;
|
|
1371
1749
|
|
|
1372
|
-
// 2) 결정 블록 수
|
|
1373
|
-
const decisionBlocks = decisions
|
|
1750
|
+
// 2) 결정 블록 수 (1.9.14: 코드블록/Template 제외)
|
|
1751
|
+
const decisionBlocks = _extractDecisionBlocks(decisions);
|
|
1374
1752
|
// recent decisions (날짜로 정렬 시 가장 최근)
|
|
1375
1753
|
const recentDecisions = decisionBlocks.slice(-5).map(b => {
|
|
1376
1754
|
const t = (b.match(/^### (.+)$/m) || [, ''])[1];
|
|
@@ -1412,9 +1790,10 @@ function _retroAggregate(root) {
|
|
|
1412
1790
|
const activeRules = rules.filter(r => r.status === 'active');
|
|
1413
1791
|
const verifiedRules = rules.filter(r => r.lastVerified && r.lastVerified !== '-');
|
|
1414
1792
|
|
|
1415
|
-
// 7)
|
|
1416
|
-
const
|
|
1417
|
-
|
|
1793
|
+
// 7) 다음 우선 작업 — 우선순위: in-progress > blocked/waiting/on-hold/incomplete > planned/requested (1.9.14 C)
|
|
1794
|
+
const _priority = { 'in-progress': 0, 'blocked': 1, 'waiting': 1, 'on-hold': 1, 'incomplete': 1, 'planned': 2, 'requested': 2 };
|
|
1795
|
+
const focusNext = rows.filter(r => _priority[r.status] != null)
|
|
1796
|
+
.sort((a, b) => (_priority[a.status] || 9) - (_priority[b.status] || 9));
|
|
1418
1797
|
|
|
1419
1798
|
return {
|
|
1420
1799
|
statusCounts,
|
|
@@ -1456,11 +1835,49 @@ function _retroOneLine(agg) {
|
|
|
1456
1835
|
return parts.join(' · ');
|
|
1457
1836
|
}
|
|
1458
1837
|
|
|
1838
|
+
// 1.9.15: --all-apps / --include 경로 모음
|
|
1839
|
+
function _collectWorkspacePaths(rootBase) {
|
|
1840
|
+
const set = new Set();
|
|
1841
|
+
if (exists(path.join(rootBase, '.harness'))) set.add(rootBase);
|
|
1842
|
+
if (has('--all-apps')) {
|
|
1843
|
+
const baseCandidates = [path.resolve(rootBase, '_apps'), path.resolve(rootBase, '..', '_apps')];
|
|
1844
|
+
for (const base of baseCandidates) {
|
|
1845
|
+
if (!exists(base)) continue;
|
|
1846
|
+
let st; try { st = fs.statSync(base); } catch { continue; }
|
|
1847
|
+
if (!st.isDirectory()) continue;
|
|
1848
|
+
for (const e of fs.readdirSync(base)) {
|
|
1849
|
+
const p = path.join(base, e);
|
|
1850
|
+
try {
|
|
1851
|
+
if (fs.statSync(p).isDirectory() && exists(path.join(p, '.harness'))) set.add(p);
|
|
1852
|
+
} catch {}
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
const include = arg('--include', null);
|
|
1857
|
+
if (include) {
|
|
1858
|
+
for (const p of String(include).split(',')) {
|
|
1859
|
+
const abs = path.resolve(p.trim());
|
|
1860
|
+
if (exists(path.join(abs, '.harness'))) set.add(abs);
|
|
1861
|
+
else warn(`--include 무시: ${abs} (.harness 없음)`);
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
return Array.from(set);
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1459
1867
|
function retroCmd(root) {
|
|
1460
1868
|
root = absRoot(root);
|
|
1869
|
+
// 1.9.15: --all-apps / --include 통합 모드
|
|
1870
|
+
if (has('--all-apps') || arg('--include', null)) {
|
|
1871
|
+
return _retroWorkspace(root);
|
|
1872
|
+
}
|
|
1461
1873
|
const days = parseInt(arg('--days', '7'), 10);
|
|
1462
1874
|
const cutoff = new Date(Date.now() - days * 86400 * 1000).toISOString().slice(0, 10);
|
|
1463
1875
|
const agg = _retroAggregate(root);
|
|
1876
|
+
// 1.9.16: --json
|
|
1877
|
+
if (has('--json')) {
|
|
1878
|
+
log(JSON.stringify({ project: path.basename(root), days, cutoff, summary: _retroOneLine(agg), data: agg }, null, 2));
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1464
1881
|
log(`# 회고 (retro) — 최근 ${days}일 (since ${cutoff})`);
|
|
1465
1882
|
log(`\n📈 한 줄 요약: ${_retroOneLine(agg)}`);
|
|
1466
1883
|
|
|
@@ -1506,9 +1923,66 @@ function retroCmd(root) {
|
|
|
1506
1923
|
log(` 4. \`leerness brainstorm <주제>\`로 누적 데이터 기반 컨텍스트 적재`);
|
|
1507
1924
|
}
|
|
1508
1925
|
|
|
1926
|
+
// 1.9.15: 워크스페이스 통합 retro (다수 프로젝트 묶음 회고)
|
|
1927
|
+
function _retroWorkspace(rootBase) {
|
|
1928
|
+
const paths = _collectWorkspacePaths(rootBase);
|
|
1929
|
+
if (!paths.length) return fail('대상 프로젝트 없음. --include <path1,path2> 또는 --all-apps 사용 필요.');
|
|
1930
|
+
// 1.9.16: --json
|
|
1931
|
+
if (has('--json')) {
|
|
1932
|
+
const projects = paths.map(p => {
|
|
1933
|
+
const a = _retroAggregate(p);
|
|
1934
|
+
return { project: path.basename(p), path: p, summary: _retroOneLine(a), data: a };
|
|
1935
|
+
});
|
|
1936
|
+
const totals = projects.reduce((t, p) => ({
|
|
1937
|
+
tasks: t.tasks + p.data.totalTasks, done: t.done + p.data.doneCount,
|
|
1938
|
+
decisions: t.decisions + p.data.decisionBlocks, skills: t.skills + p.data.skillUsage.length,
|
|
1939
|
+
usage: t.usage + p.data.totalSkillUsage, opts: t.opts + p.data.totalOptimizations,
|
|
1940
|
+
activeRules: t.activeRules + p.data.activeRules, pass: t.pass + p.data.passSignals, fix: t.fix + p.data.fixSignals
|
|
1941
|
+
}), { tasks: 0, done: 0, decisions: 0, skills: 0, usage: 0, opts: 0, activeRules: 0, pass: 0, fix: 0 });
|
|
1942
|
+
log(JSON.stringify({ projects, totals, projectCount: paths.length }, null, 2));
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
log(`# Cross-project retro — ${paths.length}개 프로젝트`);
|
|
1946
|
+
const totals = { tasks: 0, done: 0, decisions: 0, skills: 0, totalSkillUsage: 0, totalOpts: 0, activeRules: 0, fixSig: 0, passSig: 0 };
|
|
1947
|
+
for (const p of paths) {
|
|
1948
|
+
const agg = _retroAggregate(p);
|
|
1949
|
+
const name = path.basename(p);
|
|
1950
|
+
log(`\n## ${name}`);
|
|
1951
|
+
log(` 📈 ${_retroOneLine(agg)}`);
|
|
1952
|
+
const f = agg.focusNext[0];
|
|
1953
|
+
log(` 🎯 다음 우선: ${f ? `${f.id} [${f.status}] ${f.request.slice(0, 50)}` : '(없음)'}`);
|
|
1954
|
+
log(` 📚 top 스킬: ${agg.skillUsage.length ? agg.skillUsage[0].id + ' (' + agg.skillUsage[0].count + '회)' : '(없음)'}`);
|
|
1955
|
+
totals.tasks += agg.totalTasks;
|
|
1956
|
+
totals.done += agg.doneCount;
|
|
1957
|
+
totals.decisions += agg.decisionBlocks;
|
|
1958
|
+
totals.skills += agg.skillUsage.length;
|
|
1959
|
+
totals.totalSkillUsage += agg.totalSkillUsage;
|
|
1960
|
+
totals.totalOpts += agg.totalOptimizations;
|
|
1961
|
+
totals.activeRules += agg.activeRules;
|
|
1962
|
+
totals.fixSig += agg.fixSignals;
|
|
1963
|
+
totals.passSig += agg.passSignals;
|
|
1964
|
+
}
|
|
1965
|
+
log(`\n## 📊 워크스페이스 총합 (${paths.length} 프로젝트)`);
|
|
1966
|
+
log(` - 누적 task: ${totals.tasks}${totals.tasks ? ` (done ${totals.done} = ${Math.round(totals.done / totals.tasks * 100)}%)` : ''}`);
|
|
1967
|
+
log(` - 누적 결정: ${totals.decisions}건`);
|
|
1968
|
+
log(` - 스킬: ${totals.skills}종 / 사용 ${totals.totalSkillUsage}회 / 최적화 ${totals.totalOpts}건`);
|
|
1969
|
+
log(` - 활성 룰: ${totals.activeRules}건`);
|
|
1970
|
+
log(` - 시그널: pass ${totals.passSig} · fix ${totals.fixSig}${totals.passSig + totals.fixSig > 0 ? ` (비율 ${totals.fixSig ? (totals.passSig / totals.fixSig).toFixed(2) : '∞'})` : ''}`);
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1509
1973
|
function insightsCmd(root) {
|
|
1510
1974
|
root = absRoot(root);
|
|
1975
|
+
// 1.9.15: --all-apps / --include 통합 모드
|
|
1976
|
+
if (has('--all-apps') || arg('--include', null)) {
|
|
1977
|
+
return _insightsWorkspace(root);
|
|
1978
|
+
}
|
|
1511
1979
|
const agg = _retroAggregate(root);
|
|
1980
|
+
// 1.9.16: --json
|
|
1981
|
+
if (has('--json')) {
|
|
1982
|
+
const sc = readSessionCounter(root);
|
|
1983
|
+
log(JSON.stringify({ project: path.basename(root), sessionCount: sc.count, lastCloseAt: sc.lastCloseAt, data: agg }, null, 2));
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1512
1986
|
const sc = readSessionCounter(root);
|
|
1513
1987
|
log(`# Insights — 누적 통계`);
|
|
1514
1988
|
log(`\n## 📊 핵심 지표`);
|
|
@@ -1543,22 +2017,182 @@ function insightsCmd(root) {
|
|
|
1543
2017
|
if (agg.statusCounts.blocked > 0) log(` - blocked 작업 ${agg.statusCounts.blocked}건 — \`leerness lessons --query "blocked"\`로 과거 패턴 회수`);
|
|
1544
2018
|
}
|
|
1545
2019
|
|
|
2020
|
+
function _insightsWorkspace(rootBase) {
|
|
2021
|
+
const paths = _collectWorkspacePaths(rootBase);
|
|
2022
|
+
if (!paths.length) return fail('대상 프로젝트 없음. --include 또는 --all-apps 사용.');
|
|
2023
|
+
// 1.9.16: --json
|
|
2024
|
+
if (has('--json')) {
|
|
2025
|
+
const projects = paths.map(p => ({ project: path.basename(p), path: p, data: _retroAggregate(p) }));
|
|
2026
|
+
log(JSON.stringify({ projects, projectCount: paths.length }, null, 2));
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
2029
|
+
log(`# Workspace Insights — ${paths.length}개 프로젝트`);
|
|
2030
|
+
log(`\n| Project | Task | Done % | Decisions | Skills | Usage | Opts | Pass/Fix |`);
|
|
2031
|
+
log(`|---|---|---|---|---|---|---|---|`);
|
|
2032
|
+
const totals = { tasks: 0, done: 0, decisions: 0, skills: 0, usage: 0, opts: 0, pass: 0, fix: 0 };
|
|
2033
|
+
for (const p of paths) {
|
|
2034
|
+
const a = _retroAggregate(p);
|
|
2035
|
+
const donePct = a.totalTasks ? Math.round(a.doneCount / a.totalTasks * 100) : 0;
|
|
2036
|
+
const pf = a.fixSignals ? (a.passSignals / a.fixSignals).toFixed(1) : '∞';
|
|
2037
|
+
log(`| ${path.basename(p)} | ${a.totalTasks} | ${donePct}% | ${a.decisionBlocks} | ${a.skillUsage.length} | ${a.totalSkillUsage} | ${a.totalOptimizations} | ${a.passSignals}/${a.fixSignals} (${pf}) |`);
|
|
2038
|
+
totals.tasks += a.totalTasks; totals.done += a.doneCount; totals.decisions += a.decisionBlocks;
|
|
2039
|
+
totals.skills += a.skillUsage.length; totals.usage += a.totalSkillUsage; totals.opts += a.totalOptimizations;
|
|
2040
|
+
totals.pass += a.passSignals; totals.fix += a.fixSignals;
|
|
2041
|
+
}
|
|
2042
|
+
const tpf = totals.fix ? (totals.pass / totals.fix).toFixed(1) : '∞';
|
|
2043
|
+
const tDonePct = totals.tasks ? Math.round(totals.done / totals.tasks * 100) : 0;
|
|
2044
|
+
log(`| **TOTAL** | **${totals.tasks}** | **${tDonePct}%** | **${totals.decisions}** | **${totals.skills}** | **${totals.usage}** | **${totals.opts}** | **${totals.pass}/${totals.fix} (${tpf})** |`);
|
|
2045
|
+
log(`\n## 📈 평가`);
|
|
2046
|
+
if (totals.pass > totals.fix * 3) log(` - 안정성: 우수 (pass÷fix = ${tpf})`);
|
|
2047
|
+
else if (totals.pass > totals.fix) log(` - 안정성: 보통 (pass÷fix = ${tpf})`);
|
|
2048
|
+
else if (totals.fix > 0) log(` - 안정성: 주의 (fix가 pass보다 많음) — verify-code 자동화 검토`);
|
|
2049
|
+
if (totals.opts === 0) log(` - 최적화 누적 없음 — \`leerness skill optimize\` 활용 권장`);
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
// 1.9.16: brainstorm 핵심 로직 분리 — 단일 프로젝트 결과 반환
|
|
2053
|
+
function _brainstormFor(root, topic) {
|
|
2054
|
+
function _escUnicode(s) { return String(s).replace(/[\\^$*+?.()|[\]{}]/g, '\\$&'); }
|
|
2055
|
+
const tokens = String(topic).split(/\s+/).filter(t => t.length >= 2);
|
|
2056
|
+
const wordRes = tokens.map(t => new RegExp(`(?<![\\p{L}\\p{N}_])${_escUnicode(t)}(?![\\p{L}\\p{N}_])`, 'iu'));
|
|
2057
|
+
function matches(text) { return wordRes.every(re => re.test(text)); }
|
|
2058
|
+
const hits = { decisions: [], skills: [], tasks: [], rules: [], evidence: [], lessons: [] };
|
|
2059
|
+
const dec = exists(decisionsPath(root)) ? read(decisionsPath(root)) : '';
|
|
2060
|
+
const decLines = dec.split('\n');
|
|
2061
|
+
for (const b of _extractDecisionBlocks(dec)) {
|
|
2062
|
+
if (matches(b)) {
|
|
2063
|
+
const t = (b.match(/^### (.+)$/m) || [, ''])[1];
|
|
2064
|
+
const lineIdx = decLines.findIndex(line => line === `### ${t}`);
|
|
2065
|
+
const lineNo = lineIdx >= 0 ? lineIdx + 1 : 0;
|
|
2066
|
+
hits.decisions.push({ title: t, preview: b.slice(0, 200).replace(/\n+/g, ' '), line: lineNo });
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
const skillsDir = path.join(root, '.harness/skills');
|
|
2070
|
+
if (exists(skillsDir)) {
|
|
2071
|
+
for (const id of fs.readdirSync(skillsDir)) {
|
|
2072
|
+
const f = path.join(skillsDir, id, 'skill.json');
|
|
2073
|
+
if (!exists(f)) continue;
|
|
2074
|
+
try {
|
|
2075
|
+
const s = JSON.parse(read(f));
|
|
2076
|
+
if (matches(JSON.stringify(s))) hits.skills.push({ id, displayNameKo: s.displayNameKo, capabilities: s.capabilities, usage: s.usage });
|
|
2077
|
+
} catch {}
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
const rows = readProgressRows(root);
|
|
2081
|
+
const progressText = exists(progressPath(root)) ? read(progressPath(root)) : '';
|
|
2082
|
+
for (const r of rows) {
|
|
2083
|
+
const fields = [];
|
|
2084
|
+
if (matches(r.request)) fields.push('request');
|
|
2085
|
+
if (matches(r.evidence)) fields.push('evidence');
|
|
2086
|
+
if (matches(r.nextAction)) fields.push('nextAction');
|
|
2087
|
+
if (fields.length) {
|
|
2088
|
+
const idx = progressText.indexOf(`| ${r.id} |`);
|
|
2089
|
+
const lineNo = idx >= 0 ? progressText.slice(0, idx).split('\n').length : 0;
|
|
2090
|
+
hits.tasks.push({ ...r, _fields: fields, line: lineNo });
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
if (exists(rulesPath(root))) {
|
|
2094
|
+
const rulesText = read(rulesPath(root));
|
|
2095
|
+
for (const r of readRules(root)) {
|
|
2096
|
+
if (matches(r.rule)) {
|
|
2097
|
+
const idx = rulesText.indexOf(`| ${r.id} |`);
|
|
2098
|
+
const lineNo = idx >= 0 ? rulesText.slice(0, idx).split('\n').length : 0;
|
|
2099
|
+
hits.rules.push({ ...r, line: lineNo });
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
const ev = exists(evidencePath(root)) ? read(evidencePath(root)) : '';
|
|
2104
|
+
for (const block of ev.split(/\n(?=## )/)) {
|
|
2105
|
+
if (!block.startsWith('## ')) continue;
|
|
2106
|
+
if (matches(block)) {
|
|
2107
|
+
const t = (block.match(/^## (.+)$/m) || [, ''])[1];
|
|
2108
|
+
const idx = ev.indexOf(block);
|
|
2109
|
+
const lineNo = idx >= 0 ? ev.slice(0, idx).split('\n').length : 0;
|
|
2110
|
+
hits.evidence.push({ title: t.trim(), preview: block.slice(0, 200).replace(/\n+/g, ' '), line: lineNo });
|
|
2111
|
+
if (/✗|fail|롤백|incomplete|버그/i.test(block)) hits.lessons.push({ title: t.trim(), line: lineNo });
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
return hits;
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
function _brainstormTotal(h) { return h.decisions.length + h.skills.length + h.tasks.length + h.rules.length + h.evidence.length; }
|
|
2118
|
+
|
|
2119
|
+
// 1.9.16: 워크스페이스 통합 brainstorm
|
|
2120
|
+
function _brainstormWorkspace(rootBase, topic) {
|
|
2121
|
+
const paths = _collectWorkspacePaths(rootBase);
|
|
2122
|
+
if (!paths.length) return fail('대상 프로젝트 없음. --include 또는 --all-apps 사용.');
|
|
2123
|
+
if (has('--json')) {
|
|
2124
|
+
const result = paths.map(p => ({ project: path.basename(p), path: p, hits: _brainstormFor(p, topic) }));
|
|
2125
|
+
log(JSON.stringify({ topic, projects: result, total: result.reduce((a, b) => a + _brainstormTotal(b.hits), 0) }, null, 2));
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
2128
|
+
log(`# Cross-project Brainstorm — "${topic}" — ${paths.length}개 프로젝트`);
|
|
2129
|
+
let grandTotal = 0;
|
|
2130
|
+
for (const p of paths) {
|
|
2131
|
+
const h = _brainstormFor(p, topic);
|
|
2132
|
+
const n = _brainstormTotal(h);
|
|
2133
|
+
grandTotal += n;
|
|
2134
|
+
if (n === 0) continue;
|
|
2135
|
+
log(`\n## ${path.basename(p)} (${n}건)`);
|
|
2136
|
+
if (h.decisions.length) {
|
|
2137
|
+
log(` 🧠 결정 (${h.decisions.length})`);
|
|
2138
|
+
h.decisions.slice(0, 3).forEach(d => log(` - decisions.md:${d.line || '?'} — ${d.title}`));
|
|
2139
|
+
}
|
|
2140
|
+
if (h.skills.length) {
|
|
2141
|
+
log(` 📚 스킬 (${h.skills.length})`);
|
|
2142
|
+
h.skills.slice(0, 3).forEach(s => log(` - ${s.id} (${s.displayNameKo}) · 사용 ${s.usage?.count || 0}회`));
|
|
2143
|
+
}
|
|
2144
|
+
if (h.tasks.length) {
|
|
2145
|
+
log(` 📌 task (${h.tasks.length})`);
|
|
2146
|
+
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('+')})`));
|
|
2147
|
+
}
|
|
2148
|
+
if (h.rules.length) {
|
|
2149
|
+
log(` ⚡ 룰 (${h.rules.length})`);
|
|
2150
|
+
h.rules.slice(0, 3).forEach(r => log(` - rules.md:${r.line || '?'} — ${r.id} [${r.trigger}]`));
|
|
2151
|
+
}
|
|
2152
|
+
if (h.evidence.length) {
|
|
2153
|
+
log(` 🧪 evidence (${h.evidence.length})`);
|
|
2154
|
+
h.evidence.slice(0, 3).forEach(e => log(` - review-evidence.md:${e.line || '?'} — ${e.title}`));
|
|
2155
|
+
}
|
|
2156
|
+
if (h.lessons.length) {
|
|
2157
|
+
log(` ⚠ 과거 실패/롤백 (${h.lessons.length})`);
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
log(`\n## 📊 워크스페이스 총합: ${grandTotal}건 매치 (${paths.length} 프로젝트)`);
|
|
2161
|
+
if (grandTotal === 0) log(` ⓘ 어느 프로젝트에서도 "${topic}" 관련 자원 없음 — 새 영역. 첫 결정/스킬을 기록하면 다음 brainstorm이 풍부해짐.`);
|
|
2162
|
+
}
|
|
2163
|
+
|
|
1546
2164
|
function brainstormCmd(root, topic) {
|
|
1547
2165
|
root = absRoot(root);
|
|
1548
2166
|
if (!topic) return fail('topic required (e.g., brainstorm "API rate limit")');
|
|
2167
|
+
// 1.9.16: --all-apps / --include 통합 모드
|
|
2168
|
+
if (has('--all-apps') || arg('--include', null)) {
|
|
2169
|
+
return _brainstormWorkspace(root, topic);
|
|
2170
|
+
}
|
|
2171
|
+
// 1.9.16: --json 단일 프로젝트
|
|
2172
|
+
if (has('--json')) {
|
|
2173
|
+
const h = _brainstormFor(root, topic);
|
|
2174
|
+
log(JSON.stringify({ topic, project: path.basename(root), hits: h, total: _brainstormTotal(h) }, null, 2));
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
1549
2177
|
log(`# Brainstorm — "${topic}"`);
|
|
1550
2178
|
log(`\n누적된 leerness 데이터에서 주제 관련 자원을 회수합니다.`);
|
|
1551
2179
|
|
|
1552
|
-
|
|
2180
|
+
// 1.9.14 B: 토큰 기반 매칭 — unicode word boundary. unicode 모드에서 하이픈은 escape 불필요.
|
|
2181
|
+
function _escUnicode(s) { return String(s).replace(/[\\^$*+?.()|[\]{}]/g, '\\$&'); }
|
|
2182
|
+
const tokens = String(topic).split(/\s+/).filter(t => t.length >= 2);
|
|
2183
|
+
const wordRes = tokens.map(t => new RegExp(`(?<![\\p{L}\\p{N}_])${_escUnicode(t)}(?![\\p{L}\\p{N}_])`, 'iu'));
|
|
2184
|
+
function matches(text) { return wordRes.every(re => re.test(text)); }
|
|
1553
2185
|
const hits = { decisions: [], skills: [], tasks: [], rules: [], evidence: [], lessons: [] };
|
|
1554
2186
|
|
|
1555
|
-
// decisions
|
|
2187
|
+
// decisions (1.9.14: 코드블록/Template 제외, 1.9.15: 라인 번호)
|
|
1556
2188
|
const dec = exists(decisionsPath(root)) ? read(decisionsPath(root)) : '';
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
if (
|
|
2189
|
+
const decLines = dec.split('\n');
|
|
2190
|
+
for (const b of _extractDecisionBlocks(dec)) {
|
|
2191
|
+
if (matches(b)) {
|
|
1560
2192
|
const t = (b.match(/^### (.+)$/m) || [, ''])[1];
|
|
1561
|
-
|
|
2193
|
+
const lineIdx = decLines.findIndex(line => line === `### ${t}`);
|
|
2194
|
+
const lineNo = lineIdx >= 0 ? lineIdx + 1 : 0;
|
|
2195
|
+
hits.decisions.push({ title: t, preview: b.slice(0, 200).replace(/\n+/g, ' '), line: lineNo });
|
|
1562
2196
|
}
|
|
1563
2197
|
}
|
|
1564
2198
|
// skills
|
|
@@ -1570,54 +2204,75 @@ function brainstormCmd(root, topic) {
|
|
|
1570
2204
|
try {
|
|
1571
2205
|
const s = JSON.parse(read(f));
|
|
1572
2206
|
const text = JSON.stringify(s);
|
|
1573
|
-
if (
|
|
2207
|
+
if (matches(text)) hits.skills.push({ id, displayNameKo: s.displayNameKo, capabilities: s.capabilities, usage: s.usage });
|
|
1574
2208
|
} catch {}
|
|
1575
2209
|
}
|
|
1576
2210
|
}
|
|
1577
|
-
// tasks
|
|
2211
|
+
// tasks (1.9.14: token 매칭, 1.9.15: 매치 필드 + 라인 번호)
|
|
1578
2212
|
const rows = readProgressRows(root);
|
|
1579
|
-
|
|
1580
|
-
|
|
2213
|
+
const progressText = exists(progressPath(root)) ? read(progressPath(root)) : '';
|
|
2214
|
+
for (const r of rows) {
|
|
2215
|
+
const fields = [];
|
|
2216
|
+
if (matches(r.request)) fields.push('request');
|
|
2217
|
+
if (matches(r.evidence)) fields.push('evidence');
|
|
2218
|
+
if (matches(r.nextAction)) fields.push('nextAction');
|
|
2219
|
+
if (fields.length) {
|
|
2220
|
+
const idx = progressText.indexOf(`| ${r.id} |`);
|
|
2221
|
+
const lineNo = idx >= 0 ? progressText.slice(0, idx).split('\n').length : 0;
|
|
2222
|
+
hits.tasks.push({ ...r, _fields: fields, line: lineNo });
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
// rules (1.9.15: 라인 번호)
|
|
1581
2226
|
if (exists(rulesPath(root))) {
|
|
1582
|
-
|
|
2227
|
+
const rulesText = read(rulesPath(root));
|
|
2228
|
+
for (const r of readRules(root)) {
|
|
2229
|
+
if (matches(r.rule)) {
|
|
2230
|
+
const idx = rulesText.indexOf(`| ${r.id} |`);
|
|
2231
|
+
const lineNo = idx >= 0 ? rulesText.slice(0, idx).split('\n').length : 0;
|
|
2232
|
+
hits.rules.push({ ...r, line: lineNo });
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
1583
2235
|
}
|
|
1584
|
-
// evidence — lessons 키워드 (fail/롤백/incomplete) 동반
|
|
2236
|
+
// evidence — lessons 키워드 (fail/롤백/incomplete) 동반 (1.9.15: 라인 번호)
|
|
1585
2237
|
const ev = exists(evidencePath(root)) ? read(evidencePath(root)) : '';
|
|
1586
2238
|
for (const block of ev.split(/\n(?=## )/)) {
|
|
1587
2239
|
if (!block.startsWith('## ')) continue;
|
|
1588
|
-
if (
|
|
2240
|
+
if (matches(block)) {
|
|
1589
2241
|
const t = (block.match(/^## (.+)$/m) || [, ''])[1];
|
|
1590
|
-
|
|
1591
|
-
|
|
2242
|
+
const idx = ev.indexOf(block);
|
|
2243
|
+
const lineNo = idx >= 0 ? ev.slice(0, idx).split('\n').length : 0;
|
|
2244
|
+
hits.evidence.push({ title: t.trim(), preview: block.slice(0, 200).replace(/\n+/g, ' '), line: lineNo });
|
|
2245
|
+
if (/✗|fail|롤백|incomplete|버그/i.test(block)) hits.lessons.push({ title: t.trim(), line: lineNo });
|
|
1592
2246
|
}
|
|
1593
2247
|
}
|
|
1594
2248
|
|
|
1595
2249
|
const total = hits.decisions.length + hits.skills.length + hits.tasks.length + hits.rules.length + hits.evidence.length;
|
|
1596
2250
|
log(`\n📦 총 ${total}건 발견 (decisions ${hits.decisions.length} · skills ${hits.skills.length} · tasks ${hits.tasks.length} · rules ${hits.rules.length} · evidence ${hits.evidence.length})`);
|
|
1597
2251
|
|
|
2252
|
+
// 1.9.15: 모든 출력에 출처 파일:라인 표시
|
|
1598
2253
|
if (hits.decisions.length) {
|
|
1599
2254
|
log(`\n## 🧠 관련 결정 (${hits.decisions.length})`);
|
|
1600
|
-
hits.decisions.slice(0, 5).forEach(d => log(` - ${d.title}`));
|
|
2255
|
+
hits.decisions.slice(0, 5).forEach(d => log(` - .harness/decisions.md:${d.line || '?'} — ${d.title}`));
|
|
1601
2256
|
}
|
|
1602
2257
|
if (hits.skills.length) {
|
|
1603
2258
|
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}`));
|
|
2259
|
+
hits.skills.forEach(s => log(` - .harness/skills/${s.id}/skill.json — ${s.id} (${s.displayNameKo}) · 사용 ${s.usage?.count || 0}회 · cap ${(s.capabilities || []).length}`));
|
|
1605
2260
|
}
|
|
1606
2261
|
if (hits.tasks.length) {
|
|
1607
2262
|
log(`\n## 📌 관련 과거 task (${hits.tasks.length})`);
|
|
1608
|
-
hits.tasks.slice(0, 5).forEach(t => log(` - ${t.id} [${t.status}] ${t.request}`));
|
|
2263
|
+
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
2264
|
}
|
|
1610
2265
|
if (hits.rules.length) {
|
|
1611
2266
|
log(`\n## ⚡ 관련 룰 (${hits.rules.length})`);
|
|
1612
|
-
hits.rules.forEach(r => log(` - ${r.id} [${r.trigger}] ${r.rule}`));
|
|
2267
|
+
hits.rules.forEach(r => log(` - .harness/rules.md:${r.line || '?'} — ${r.id} [${r.trigger}] ${r.rule}`));
|
|
1613
2268
|
}
|
|
1614
2269
|
if (hits.evidence.length) {
|
|
1615
2270
|
log(`\n## 🧪 관련 검증 기록 (${hits.evidence.length})`);
|
|
1616
|
-
hits.evidence.slice(0, 5).forEach(e => log(` - ${e.title}`));
|
|
2271
|
+
hits.evidence.slice(0, 5).forEach(e => log(` - .harness/review-evidence.md:${e.line || '?'} — ${e.title}`));
|
|
1617
2272
|
}
|
|
1618
2273
|
if (hits.lessons.length) {
|
|
1619
2274
|
log(`\n## ⚠ 같은 주제 과거 실패/롤백 (${hits.lessons.length}) — 같은 실수 방지`);
|
|
1620
|
-
hits.lessons.slice(0, 5).forEach(l => log(` - ${l.title}`));
|
|
2275
|
+
hits.lessons.slice(0, 5).forEach(l => log(` - .harness/review-evidence.md:${l.line || '?'} — ${l.title}`));
|
|
1621
2276
|
}
|
|
1622
2277
|
|
|
1623
2278
|
log(`\n## 💡 시작 전 권장 액션`);
|
|
@@ -2434,9 +3089,8 @@ function lessonsCmd(root) {
|
|
|
2434
3089
|
const tlog = exists(taskLogPath(root)) ? read(taskLogPath(root)) : '';
|
|
2435
3090
|
const handoff = exists(handoffPath(root)) ? read(handoffPath(root)) : '';
|
|
2436
3091
|
const lessons = [];
|
|
2437
|
-
// decisions: ### 블록 전체
|
|
2438
|
-
for (const block of decisions
|
|
2439
|
-
if (!block.startsWith('### ')) continue;
|
|
3092
|
+
// decisions: ### 블록 전체 (1.9.14: 코드블록/Template 제외)
|
|
3093
|
+
for (const block of _extractDecisionBlocks(decisions)) {
|
|
2440
3094
|
const m = block.match(/^### (.+)$/m);
|
|
2441
3095
|
if (!m) continue;
|
|
2442
3096
|
lessons.push({ source: 'decisions.md', title: m[1].trim(), block });
|
|
@@ -2894,10 +3548,10 @@ function viewworkInstall(root) {
|
|
|
2894
3548
|
}
|
|
2895
3549
|
|
|
2896
3550
|
function help() {
|
|
2897
|
-
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 "<주제>"
|
|
3551
|
+
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] [--all-apps] [--include p1,p2] [--since 24h|3d] [--json] # 1.9.17/18 워크스페이스\n leerness reuse-map [path] [--all-apps] [--include p1,p2] [--strict-elements] [--json] # 1.9.18 중복/잠재중복/depends-on\n leerness verify-claim <T-ID> [--path .] [--json] # 1.9.18 progress evidence 자동 검증\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
|
|
3552
|
+
leerness retro [path] [--days 7] [--all-apps] [--include p1,p2] [--json] # 회고 (1.9.13~1.9.16)
|
|
3553
|
+
leerness insights [path] [--all-apps] [--include p1,p2] [--json] # 누적 통계 (1.9.13~1.9.16)
|
|
3554
|
+
leerness brainstorm "<주제>" [--all-apps] [--include p1,p2] [--json] # 브레인스토밍 (1.9.13~1.9.16)
|
|
2901
3555
|
leerness roadmap [path] [--out file.html] # 좌→우 수평 트리 + 상하 중앙정렬 + 화이트보드 (1.9.11)
|
|
2902
3556
|
leerness roadmap auto on|off|status [--on-every-change] [--out file.html] # 자동 갱신 (1.9.12, install/session-close 기본 ON)
|
|
2903
3557
|
leerness verify-code [path] [--build] # npm test/lint/typecheck 자동 실행 + evidence 자동 기록 (1.9.7)
|
|
@@ -2927,7 +3581,9 @@ async function main() {
|
|
|
2927
3581
|
if (cmd === 'encoding' && args[1] === 'check') return encodingCheck(args[2] || process.cwd());
|
|
2928
3582
|
if (cmd === 'lazy' && args[1] === 'detect') return lazyDetect(args[2] || process.cwd());
|
|
2929
3583
|
if (cmd === 'memory' && args[1] === 'search') return memorySearch(arg('--path', process.cwd()), args.slice(2).join(' '));
|
|
2930
|
-
if (cmd === 'handoff')
|
|
3584
|
+
if (cmd === 'handoff') return handoffCmd(args[1] || process.cwd());
|
|
3585
|
+
if (cmd === 'reuse-map') return reuseMapCmd(args[1] || process.cwd());
|
|
3586
|
+
if (cmd === 'verify-claim') return verifyClaimCmd(arg('--path', process.cwd()), args[1]);
|
|
2931
3587
|
if (cmd === 'session' && args[1] === 'close') { const r = sessionClose(args[2] || process.cwd()); viewworkEmit(args[2] || process.cwd(), { action: 'task', tool: 'session-close', note: 'session close' }); return r; }
|
|
2932
3588
|
if (cmd === 'viewwork' && args[1] === 'install') return viewworkInstall(args[2] || process.cwd());
|
|
2933
3589
|
if (cmd === 'viewwork' && args[1] === 'emit') return viewworkEmit(args[2] || process.cwd(), { action: arg('--action','task'), note: arg('--note',''), agent: arg('--agent','leerness'), tool: arg('--tool','leerness-cli') });
|