leerness 1.9.0 → 1.9.1
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 +20 -0
- package/bin/harness.js +59 -10
- package/package.json +1 -1
- package/scripts/e2e.js +66 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.1 — 2026-05-08
|
|
4
|
+
|
|
5
|
+
1.9.0을 실 프로젝트(memo-cli)에서 운영하며 발견한 **5개 메타 감사 사항**을 패치합니다.
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **P1**: `autoUpdateInstall`이 legacy `leerness-plus update --check` SessionStart hook을 자동 정리. fork 시절 잔재로 인해 매 세션 npm 호출이 2회 발생하던 문제 해소.
|
|
10
|
+
- **P2**: `managedMerge`에 `MERGE_OVERWRITE_FILES` 화이트리스트 추가 (`skill-index.md`, `manifest.json`, `skills-lock.json`, `HARNESS_VERSION`, `LANGUAGE`, `context-routing.md`). 다단계 migrate를 거쳐도 표/메타데이터가 누적되지 않음.
|
|
11
|
+
- **P4**: `audit`이 `<!-- leerness:na <reason> -->` 마커를 인식. CLI 패키지 등 디자인 토큰/재사용 맵이 NA인 프로젝트에서 영구 경고가 사라짐.
|
|
12
|
+
- **P6**: `lazy detect`의 evidence 정규식을 `/^plan:M-\d{4}\s*$/`로 좁힘. `plan:M-XXXX` 단독은 부족 판정, `tests:32/32 (plan:M-0002)`처럼 검증 키워드 동반 시 통과.
|
|
13
|
+
- **P7**: `install`이 끝날 때 디폴트 `M-0001`이 plan에 있는데 progress에 row가 없으면 `T-XXXX` 자동 생성. audit "milestones without progress entry: M-0001" 경고가 init 직후 사라짐.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `leerness skill list [path]` 출력에 **설치됨** 컬럼 추가 (root가 인자/현재 디렉토리에 있을 때).
|
|
18
|
+
|
|
19
|
+
### Migration
|
|
20
|
+
|
|
21
|
+
기존 1.9.0 설치본은 `npx leerness@latest update . --yes`로 즉시 자동 마이그레이션됩니다.
|
|
22
|
+
|
|
3
23
|
## 1.9.0 — 2026-05-08
|
|
4
24
|
|
|
5
25
|
이번 minor 릴리스는 1.8.0의 6개 결함을 수정하고, 자동 감지·자동 업데이트·핸드오프 자동 작성·게으름/시크릿/인코딩 자동 가드를 흡수한 큰 강화입니다. 기존 `npx leerness init` 흐름은 그대로 유지됩니다.
|
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.1';
|
|
10
10
|
const MARK = '<!-- leerness:managed -->';
|
|
11
11
|
const README_START = '<!-- leerness:project-readme:start -->';
|
|
12
12
|
const README_END = '<!-- leerness:project-readme:end -->';
|
|
@@ -236,10 +236,20 @@ function createBackup(root, reason, files, dry = false) {
|
|
|
236
236
|
return { archiveDir: ar, candidates };
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
// 1.9.1 P2: 데이터/인덱스 파일은 preserved 블록 없이 overwrite (누적 방지).
|
|
240
|
+
const MERGE_OVERWRITE_FILES = new Set([
|
|
241
|
+
'.harness/skill-index.md',
|
|
242
|
+
'.harness/manifest.json',
|
|
243
|
+
'.harness/skills-lock.json',
|
|
244
|
+
'.harness/HARNESS_VERSION',
|
|
245
|
+
'.harness/LANGUAGE',
|
|
246
|
+
'.harness/context-routing.md'
|
|
247
|
+
]);
|
|
239
248
|
function managedMerge(file, next, previous, archiveDir) {
|
|
240
249
|
if (!previous || previous.trim() === next.trim()) return next;
|
|
241
250
|
const tag = '<!-- leerness:migration-preserved -->';
|
|
242
251
|
if (previous.includes(tag)) return next;
|
|
252
|
+
if (MERGE_OVERWRITE_FILES.has(file.replace(/\\/g, '/'))) return next;
|
|
243
253
|
return next.trimEnd() + `\n\n---\n${tag}\n## Preserved previous content\n\nPrevious content was backed up before migration. Archive reference:\n\n\`${archiveDir ? path.relative(process.cwd(), archiveDir).replace(/\\/g, '/') : '.harness/archive'}\`\n\n<details>\n<summary>Previous ${file}</summary>\n\n\`\`\`md\n${previous.replace(/```/g, '\\`\\`\\`')}\n\`\`\`\n\n</details>\n`;
|
|
244
254
|
}
|
|
245
255
|
|
|
@@ -366,6 +376,18 @@ async function install(root, opts = {}) {
|
|
|
366
376
|
syncReadme(root);
|
|
367
377
|
installSkills(root, skills);
|
|
368
378
|
writeMigrationReport(root, backup, actions);
|
|
379
|
+
// 1.9.1 P7: 디폴트 M-0001이 plan에 있고 progress에 row가 없으면 자동 추가
|
|
380
|
+
try {
|
|
381
|
+
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
382
|
+
if (/### M-0001\./.test(planText)) {
|
|
383
|
+
const rows = readProgressRows(root);
|
|
384
|
+
const linked = rows.some(r => /M-0001/.test(r.evidence));
|
|
385
|
+
if (!linked) {
|
|
386
|
+
const tid = nextId(root, 'T');
|
|
387
|
+
upsertProgress(root, { id: tid, status: 'planned', request: '프로젝트 계획 정리', evidence: 'init default plan:M-0001', nextAction: 'project-brief.md를 실제 목적으로 업데이트' });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
} catch {}
|
|
369
391
|
if (!has('--no-auto-update')) {
|
|
370
392
|
try { autoUpdateInstall(root); } catch (e) { warn('auto-update hook install skipped: ' + (e && e.message)); }
|
|
371
393
|
}
|
|
@@ -382,10 +404,25 @@ function addSkill(root, name, silent = false) {
|
|
|
382
404
|
if (!silent) ok(`skill installed: ${name}`);
|
|
383
405
|
}
|
|
384
406
|
|
|
385
|
-
function skillList() {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
407
|
+
function skillList(root) {
|
|
408
|
+
// 1.9.1: 설치 여부 컬럼 추가 (root가 없으면 생략)
|
|
409
|
+
let installed = new Set();
|
|
410
|
+
if (root) {
|
|
411
|
+
const skillsDir = path.join(absRoot(root), '.harness/skills');
|
|
412
|
+
if (exists(skillsDir)) {
|
|
413
|
+
try { for (const e of fs.readdirSync(skillsDir)) installed.add(e); } catch {}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
const showInstalled = root && installed.size >= 0;
|
|
417
|
+
if (showInstalled) {
|
|
418
|
+
log('| ID | 한글명 | 가능한 작업 | 최종 업데이트 | 검증 | 설치됨 |');
|
|
419
|
+
log('|---|---|---|---|---|---|');
|
|
420
|
+
for (const [k, v] of Object.entries(skillCatalog)) log(`| ${k} | ${v.displayNameKo} | ${v.capabilities.join('<br>')} | ${v.lastUpdated} | ${v.verification} | ${installed.has(k) ? '✓' : '·'} |`);
|
|
421
|
+
} else {
|
|
422
|
+
log('| ID | 한글명 | 가능한 작업 | 최종 업데이트 | 검증 |');
|
|
423
|
+
log('|---|---|---|---|---|');
|
|
424
|
+
for (const [k, v] of Object.entries(skillCatalog)) log(`| ${k} | ${v.displayNameKo} | ${v.capabilities.join('<br>')} | ${v.lastUpdated} | ${v.verification} |`);
|
|
425
|
+
}
|
|
389
426
|
}
|
|
390
427
|
function skillInfo(name) {
|
|
391
428
|
const v = skillCatalog[name];
|
|
@@ -556,12 +593,16 @@ function audit(root) {
|
|
|
556
593
|
const dups = designCands.filter(f => exists(path.join(root,f)));
|
|
557
594
|
if (dups.length) { warnings++; warn(`design guide duplicates outside canonical: ${dups.join(', ')} (run: leerness consistency merge-design-guide)`); }
|
|
558
595
|
else ok('no duplicate design guide candidates');
|
|
596
|
+
// 1.9.1 P4: <!-- leerness:na --> 마커가 있는 파일은 placeholder 경고 스킵.
|
|
597
|
+
const naMarker = '<!-- leerness:na';
|
|
559
598
|
const ds = exists(path.join(root,'.harness/design-system.md')) ? read(path.join(root,'.harness/design-system.md')) : '';
|
|
560
|
-
if (
|
|
599
|
+
if (ds.includes(naMarker)) ok('design-system.md marked NA (skipped)');
|
|
600
|
+
else if (!/\| color\.primary \|/.test(ds) || /\(실제 값으로 업데이트\)/.test(ds)) { warnings++; warn('design-system.md tokens not customized'); }
|
|
561
601
|
else ok('design-system tokens populated');
|
|
562
602
|
const reuse = exists(path.join(root,'.harness/reuse-map.md')) ? read(path.join(root,'.harness/reuse-map.md')) : '';
|
|
563
603
|
const reuseLines = reuse.split('\n').filter(l => l.startsWith('|') && !/Capability|---/.test(l)).length;
|
|
564
|
-
if (
|
|
604
|
+
if (reuse.includes(naMarker)) ok('reuse-map.md marked NA (skipped)');
|
|
605
|
+
else if (reuseLines === 0) { warnings++; warn('reuse-map.md is empty (consider populating known reusable elements)'); }
|
|
565
606
|
else ok(`reuse-map.md has ${reuseLines} entries`);
|
|
566
607
|
const planText = exists(planPath(root)) ? read(planPath(root)) : '';
|
|
567
608
|
const milestoneIds = Array.from(planText.matchAll(/^### (M-\d{4})\./gm)).map(m => m[1]);
|
|
@@ -679,7 +720,9 @@ function lazyDetect(root) {
|
|
|
679
720
|
root = absRoot(root);
|
|
680
721
|
let issues = 0;
|
|
681
722
|
const rows = readProgressRows(root);
|
|
682
|
-
|
|
723
|
+
// 1.9.1 P6: evidence가 단독 plan:M-XXXX 한 줄일 때만 검증 부족 처리.
|
|
724
|
+
// "tests:32/32 (plan:M-0002)" 같이 검증 키워드를 같이 적은 경우는 통과.
|
|
725
|
+
for (const r of rows) if (r.status === 'done' && (!r.evidence || /^(\s*|user-request|-)$/.test(r.evidence) || /^plan:M-\d{4}\s*$/.test(r.evidence))) {
|
|
683
726
|
issues++; warn(`done row without verifiable evidence: ${r.id} (${r.request})`);
|
|
684
727
|
}
|
|
685
728
|
if (rows.length === 0) { issues++; warn('progress-tracker is empty (no tasks tracked)'); }
|
|
@@ -978,7 +1021,12 @@ function autoUpdateInstall(root) {
|
|
|
978
1021
|
let settings = {};
|
|
979
1022
|
if (exists(settingsFile)) { try { settings = JSON.parse(read(settingsFile)); } catch {} }
|
|
980
1023
|
settings.hooks = settings.hooks || {};
|
|
981
|
-
|
|
1024
|
+
// 1.9.1 P1: legacy 'leerness-plus update' hook 자동 제거 (이전 fork 시절 잔재).
|
|
1025
|
+
let removedLegacy = 0;
|
|
1026
|
+
settings.hooks.SessionStart = (settings.hooks.SessionStart || []).filter(h => {
|
|
1027
|
+
if (h && h.command && /\bleerness-plus update\b/.test(h.command)) { removedLegacy++; return false; }
|
|
1028
|
+
return true;
|
|
1029
|
+
});
|
|
982
1030
|
if (!settings.hooks.SessionStart.some(h => h.command && h.command.includes('leerness update'))) {
|
|
983
1031
|
settings.hooks.SessionStart.push({ matcher: '*', command: 'leerness update --check' });
|
|
984
1032
|
}
|
|
@@ -986,6 +1034,7 @@ function autoUpdateInstall(root) {
|
|
|
986
1034
|
writeUtf8(path.join(root, '.claude/commands/update.md'),
|
|
987
1035
|
`# /update\n\nleerness 자동 업데이트 (감지 → 마이그레이션 → 검증).\n\n\`\`\`\n!leerness update --yes\n\`\`\`\n\n체크만:\n\n\`\`\`\n!leerness update --check\n\`\`\`\n`);
|
|
988
1036
|
ok('auto-update SessionStart hook installed (.claude/settings.local.json)');
|
|
1037
|
+
if (removedLegacy) ok(`legacy hook 제거: ${removedLegacy}건 (leerness-plus → leerness 통합)`);
|
|
989
1038
|
ok('/update slash command added');
|
|
990
1039
|
}
|
|
991
1040
|
|
|
@@ -1063,7 +1112,7 @@ async function main() {
|
|
|
1063
1112
|
if (cmd === 'readme' && args[1] === 'sync') return readmeCmd(args[2] || process.cwd());
|
|
1064
1113
|
if (cmd === 'consistency' && args[1] === 'check') return consistencyCheck(args[2] || process.cwd());
|
|
1065
1114
|
if (cmd === 'consistency' && args[1] === 'merge-design-guide') return mergeDesign(args[2] || process.cwd());
|
|
1066
|
-
if (cmd === 'skill' && args[1] === 'list') return skillList();
|
|
1115
|
+
if (cmd === 'skill' && args[1] === 'list') return skillList(args[2] || arg('--path', null));
|
|
1067
1116
|
if (cmd === 'skill' && args[1] === 'info') return skillInfo(args[2]);
|
|
1068
1117
|
if (cmd === 'skill' && args[1] === 'add') return addSkill(absRoot(arg('--path', process.cwd())), args[2]);
|
|
1069
1118
|
if (cmd === 'plan') {
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -77,6 +77,72 @@ total++;
|
|
|
77
77
|
} else { failed++; console.log('✗ .claude/settings.local.json missing'); }
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
// 1.9.1 P6 회귀: evidence가 plan:M-XXXX + 검증 키워드면 lazy detect가 통과해야 한다.
|
|
81
|
+
total++;
|
|
82
|
+
{
|
|
83
|
+
const trackerPath = path.join(tmp, '.harness/progress-tracker.md');
|
|
84
|
+
let cur = fs.readFileSync(trackerPath, 'utf8');
|
|
85
|
+
cur = cur.replace(/^\| T-0001 \|.*$/m, '| T-0001 | done | mile A | tests:32/32 (plan:M-0002) | next | 2026-05-08 |');
|
|
86
|
+
fs.writeFileSync(trackerPath, cur, 'utf8');
|
|
87
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'lazy', 'detect', tmp], { encoding: 'utf8' });
|
|
88
|
+
const ok = r.status === 0;
|
|
89
|
+
console.log(ok ? '✓ B(P6) lazy detect: plan:M-XXXX + 검증 키워드 → pass' : `✗ B(P6) FAIL exit=${r.status}\n${r.stdout}`);
|
|
90
|
+
if (!ok) failed++;
|
|
91
|
+
}
|
|
92
|
+
// 1.9.1 P6 negative: plan:M-XXXX 단독은 여전히 경고
|
|
93
|
+
total++;
|
|
94
|
+
{
|
|
95
|
+
const trackerPath = path.join(tmp, '.harness/progress-tracker.md');
|
|
96
|
+
let cur = fs.readFileSync(trackerPath, 'utf8');
|
|
97
|
+
cur = cur.replace(/^\| T-0001 \|.*$/m, '| T-0001 | done | mile A | plan:M-0002 | next | 2026-05-08 |');
|
|
98
|
+
fs.writeFileSync(trackerPath, cur, 'utf8');
|
|
99
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'lazy', 'detect', tmp], { encoding: 'utf8' });
|
|
100
|
+
const ok = r.status === 1 && /done row without verifiable evidence/.test(r.stdout);
|
|
101
|
+
console.log(ok ? '✓ B(P6 neg) lazy detect: plan:M-XXXX 단독 → warn 유지' : `✗ B(P6 neg) FAIL exit=${r.status}`);
|
|
102
|
+
if (!ok) failed++;
|
|
103
|
+
// restore
|
|
104
|
+
cur = cur.replace(/^\| T-0001 \|.*$/m, '| T-0001 | done | mile A | tests:32/32 (plan:M-0002) | next | 2026-05-08 |');
|
|
105
|
+
fs.writeFileSync(trackerPath, cur, 'utf8');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 1.9.1 P1 회귀: legacy leerness-plus hook이 있으면 auto-update install이 정리해야 한다.
|
|
109
|
+
total++;
|
|
110
|
+
{
|
|
111
|
+
const settingsFile = path.join(tmp, '.claude/settings.local.json');
|
|
112
|
+
const s = JSON.parse(fs.readFileSync(settingsFile, 'utf8'));
|
|
113
|
+
s.hooks.SessionStart = [{ matcher: '*', command: 'leerness-plus update --check' }];
|
|
114
|
+
fs.writeFileSync(settingsFile, JSON.stringify(s, null, 2));
|
|
115
|
+
cp.spawnSync(process.execPath, [CLI, 'auto-update', 'install', tmp], { encoding: 'utf8' });
|
|
116
|
+
const after = JSON.parse(fs.readFileSync(settingsFile, 'utf8'));
|
|
117
|
+
const hasLegacy = (after.hooks?.SessionStart || []).some(h => /leerness-plus update/.test(h.command || ''));
|
|
118
|
+
const hasNew = (after.hooks?.SessionStart || []).some(h => /\bleerness update\b/.test(h.command || ''));
|
|
119
|
+
const ok = !hasLegacy && hasNew;
|
|
120
|
+
console.log(ok ? '✓ B(P1) legacy leerness-plus hook 자동 제거' : '✗ B(P1) legacy hook 남음');
|
|
121
|
+
if (!ok) failed++;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 1.9.1 P4 회귀: NA 마커가 있으면 audit이 placeholder 경고를 스킵.
|
|
125
|
+
total++;
|
|
126
|
+
{
|
|
127
|
+
const ds = path.join(tmp, '.harness/design-system.md');
|
|
128
|
+
fs.appendFileSync(ds, '\n<!-- leerness:na CLI 프로젝트라 디자인 토큰 없음 -->\n');
|
|
129
|
+
const r = cp.spawnSync(process.execPath, [CLI, 'audit', tmp], { encoding: 'utf8' });
|
|
130
|
+
const ok = !/design-system\.md tokens not customized/.test(r.stdout) && /marked NA/.test(r.stdout);
|
|
131
|
+
console.log(ok ? '✓ B(P4) NA 마커 인식: design-system 경고 스킵' : '✗ B(P4) NA 마커 미작동');
|
|
132
|
+
if (!ok) failed++;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 1.9.1 P7 회귀: init 직후 progress에 M-0001 연결 row가 자동으로 있다.
|
|
136
|
+
total++;
|
|
137
|
+
{
|
|
138
|
+
const tmp2 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-e2e-p7-'));
|
|
139
|
+
cp.spawnSync(process.execPath, [CLI, 'init', tmp2, '--yes', '--language', 'ko', '--skills', 'recommended'], { encoding: 'utf8' });
|
|
140
|
+
const tracker = fs.readFileSync(path.join(tmp2, '.harness/progress-tracker.md'), 'utf8');
|
|
141
|
+
const ok = /M-0001/.test(tracker);
|
|
142
|
+
console.log(ok ? '✓ B(P7) init: M-0001 → progress row 자동 생성' : '✗ B(P7) progress에 M-0001 row 없음');
|
|
143
|
+
if (!ok) failed++;
|
|
144
|
+
}
|
|
145
|
+
|
|
80
146
|
run('viewwork install', ['viewwork', 'install', tmp]);
|
|
81
147
|
run('viewwork emit', ['viewwork', 'emit', tmp, '--action', 'note', '--note', 'e2e ping']);
|
|
82
148
|
run('route planning', ['route', 'planning']);
|