leerness 1.36.71 → 1.36.73
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 +16 -0
- package/README.md +4 -4
- package/bin/leerness.js +26 -7
- package/lib/mcp-tools.js +1 -1
- package/lib/pure-utils.js +11 -2
- package/package.json +1 -1
- package/scripts/e2e.js +67 -0
- package/scripts/lint.js +10 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.73 — 2026-07-25 — 8차 헌트 완전 종결: F12 skills-lock 정합 + F9 설계 결정
|
|
4
|
+
|
|
5
|
+
8차 홀리스틱 헌트(16건)의 마지막 설계 판단 2건을 결론 — 이로써 16/16 전건 처리 완료.
|
|
6
|
+
|
|
7
|
+
- **F12**: skills-lock 은 "요청 집합"이 아니라 **디스크 실재**를 기록 — `--skills all` 후 `recommended` 재init 시 9개 디렉토리가 남는데 락은 5개만 주장하던 불일치를 합집합 재생성으로 해소(실측 dirs 9 = lock 9). 스킬 제거는 하지 않음(비파괴 원칙).
|
|
8
|
+
- **F9 (설계 결정)**: 초고령 KO canonical 이 en 프로젝트에 커스텀으로 오인 이월되는 것 — **자동 차감은 하지 않는다**(구판 템플릿 라인과 사용자 노트를 확신으로 구분 불가, false-DROP 이 버그·false-PRESERVE 는 무해). 대신 한글 비중 높은 이월(≥10줄·>50%) 시 Preserved 섹션에 **검토·정리 안내**를 명시해 조용한 동결을 없앤다. 안내는 멱등(재이월 안 됨), 소수 한글 커스텀·ko 프로젝트엔 무경고.
|
|
9
|
+
- 검증: F12 재현 시나리오 실측, F9 순수함수 5프로브(안내/멱등/무손실/소수 무경고/ko 무경고), selftest 334, e2e +1.
|
|
10
|
+
|
|
11
|
+
## 1.36.72 — 2026-07-24 — 성장 한계 클래스 4(task list) + F16 lint 거짓 PASS 차단 — R-0001 검수 15회전
|
|
12
|
+
|
|
13
|
+
- **task list --json 상한**: 기본 최신 100행 + `shown/truncated` 정직 표기(`total` 은 필터 후 전량), `--all/--limit N` 옵트인. MCP `leerness_task_list` 계약 동반 갱신(limit/all 노출+전달). 텍스트 모드 무변경(codex: HEAD 와 바이트 동일 확인).
|
|
14
|
+
- **F16 (8차 헌트 이연분)**: lint 가 존재하지 않는 루트/빈 루트에서 `✓ 0 js clean` **거짓 PASS** — "아무것도 검사하지 않았다"는 명시 실패(exit 1)로 전환. 검증 도구가 무검사 통과 도장을 찍는 것은 핵심 철학 위반.
|
|
15
|
+
- (검수 Low) README/내장 문서의 "전체 JSON 조회" 낡은 계약 서술 정합.
|
|
16
|
+
- codex 실측: status 필터×캡 상호작용, 무효 limit 폴백, --all 우선, 최신 유지, 소비자 부재(tasks.length===total 가정 없음), lint 4개 부정 경로(missing/non-dir/empty/src-only) 전부 exit 1.
|
|
17
|
+
- 검증: selftest 334, e2e +1. 8차 헌트 잔여는 이제 설계 판단 2건(F9/F12)만.
|
|
18
|
+
|
|
3
19
|
## 1.36.71 — 2026-07-24 — 성장 한계 클래스 3: retro/insights --json rows 상한 + 워크스페이스 --days 기존 버그 — R-0001 검수 14회전
|
|
4
20
|
|
|
5
21
|
- **retro/insights --json 의 data.rows 전량 임베드**(무한 성장, 72 task 에 22KB)를 기본 최신 30행으로 상한 — `rowsTotal/rowsShown/rowsTruncated` 정직 표기, `--all/--limit N` 옵트인, **집계 수치(totalTasks 등)는 전량 기준 그대로**(수치 왜곡 없음). 4개 JSON 배출 지점(단일/워크스페이스 × retro/insights) 일괄. 실측 33KB → 14KB.
|
package/README.md
CHANGED
|
@@ -122,7 +122,7 @@ MIT
|
|
|
122
122
|
<!-- leerness:project-readme:start -->
|
|
123
123
|
## Leerness Project Harness
|
|
124
124
|
|
|
125
|
-
이 프로젝트는 Leerness v1.36.
|
|
125
|
+
이 프로젝트는 Leerness v1.36.73 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
126
126
|
|
|
127
127
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
128
128
|
|
|
@@ -176,7 +176,7 @@ leerness memory restore decision <date|title>
|
|
|
176
176
|
|
|
177
177
|
### MCP server (외부 AI 통합)
|
|
178
178
|
|
|
179
|
-
Leerness v1.36.
|
|
179
|
+
Leerness v1.36.73는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
|
|
180
180
|
|
|
181
181
|
```jsonc
|
|
182
182
|
// 카테고리별
|
|
@@ -197,7 +197,7 @@ Leerness v1.36.71는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
197
197
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
198
198
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) 회귀 테스트 갱신 → 4) 전체 e2e 스위트 통과 → 5) npm publish + git tag → 6) main push → 7) session close → 8) 다음 라운드 예약.
|
|
199
199
|
|
|
200
|
-
현재 누적: **v1.9.x → 1.36.
|
|
200
|
+
현재 누적: **v1.9.x → 1.36.73 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
|
|
201
201
|
|
|
202
202
|
### 성능 가이드
|
|
203
203
|
|
|
@@ -235,5 +235,5 @@ leerness release pack --close --auto-main-push
|
|
|
235
235
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
236
236
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
237
237
|
|
|
238
|
-
Last synced by Leerness v1.36.
|
|
238
|
+
Last synced by Leerness v1.36.73: 2026-07-25
|
|
239
239
|
<!-- leerness:project-readme:end -->
|
package/bin/leerness.js
CHANGED
|
@@ -34,7 +34,7 @@ const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE
|
|
|
34
34
|
const { tokenizeForRank: _tokenizeForRank, expandQuery: _expandQuery, scoreHits: _scoreHits, suggestTerms: _suggestTerms } = require('../lib/search-core'); // 1.36.23: memory search 랭킹 코어(순수·0-deps)
|
|
35
35
|
const { findCorruptedStateJson: _findCorruptedStateJson } = require('../lib/state-integrity'); // 1.36.1 (클린룸 리뷰 FN): .harness/*.json 상태 무결성 (audit/health/check 공유)
|
|
36
36
|
|
|
37
|
-
const VERSION = '1.36.
|
|
37
|
+
const VERSION = '1.36.73';
|
|
38
38
|
|
|
39
39
|
// 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
|
|
40
40
|
// 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
|
|
@@ -525,9 +525,21 @@ function mergeReadmeSection(existing, block) {
|
|
|
525
525
|
return _mergeReadmeSection(existing, block, README_START, README_END);
|
|
526
526
|
}
|
|
527
527
|
|
|
528
|
-
function skillLock(skills) {
|
|
528
|
+
function skillLock(skills, root) {
|
|
529
|
+
// 1.36.73 (8차 헌트 F12): 락은 "요청 집합"이 아니라 디스크 실재를 기록 — `--skills all` 후 `recommended` 재init 시
|
|
530
|
+
// 9개 디렉토리가 남는데 락은 5개만 주장하던 불일치. 기존 설치 디렉토리(카탈로그 내장분)와 합집합.
|
|
531
|
+
// 스킬 제거는 하지 않음(비파괴 원칙 — 사용자가 쓰고 있을 수 있다).
|
|
532
|
+
const set = new Set(skills);
|
|
533
|
+
try {
|
|
534
|
+
if (root) {
|
|
535
|
+
const dir = path.join(root, '.harness', 'skills');
|
|
536
|
+
if (exists(dir)) for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
537
|
+
if (e.isDirectory() && skillCatalog[e.name]) set.add(e.name);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
} catch {}
|
|
529
541
|
const data = { leernessVersion: VERSION, updatedAt: now(), installedSkills: {} };
|
|
530
|
-
for (const s of
|
|
542
|
+
for (const s of [...set].sort()) data.installedSkills[s] = skillCatalog[s] || { version: 'unknown' }; // (검수 #4) 정렬 직렬화 — 동일 멤버십이면 바이트 동일(no-op 보존)
|
|
531
543
|
return JSON.stringify(data, null, 2) + '\n';
|
|
532
544
|
}
|
|
533
545
|
|
|
@@ -543,7 +555,7 @@ function coreFiles(root, lang = 'ko', selectedSkills = [], opts = {}) {
|
|
|
543
555
|
'.harness/HARNESS_VERSION': VERSION + '\n',
|
|
544
556
|
'.harness/LANGUAGE': lang + '\n',
|
|
545
557
|
'.harness/manifest.json': JSON.stringify({ project, leernessVersion: VERSION, language: lang, installedAt: now(), minimal: !!opts.minimal }, null, 2) + '\n',
|
|
546
|
-
'.harness/skills-lock.json': skillLock(selectedSkills),
|
|
558
|
+
'.harness/skills-lock.json': skillLock(selectedSkills, root),
|
|
547
559
|
'.harness/project-brief.md': fm('project-brief', ['프로젝트 목적 확인','신규 기능 판단','계획 수립'], ['프로젝트 목적 변경','사용자/범위 변경'], `# Project Brief\n\n## Project\n${project}\n\n## Purpose\n- 이 프로젝트의 목적을 실제 내용으로 업데이트하세요.\n\n## Users\n-\n\n## Success Criteria\n-\n`),
|
|
548
560
|
'.harness/plan.md': fm('plan', ['작업 시작 전','새 요청 접수','범위 변경','신규 프로젝트 감지'], ['계획 추가/수정/드랍','milestone 변경','목표 변경'], `# Plan\n\n## Goal\n- 사용자 목적을 기준으로 전체 계획을 유지합니다.\n\n## Scope\n- 포함 범위를 기록합니다.\n\n## Out of Scope / Dropped\n| ID | Item | Reason | Date |\n|---|---|---|---|\n\n## Milestones\n\n### M-0001. 프로젝트 계획 정리\nStatus: planned\nProgress: 0%\n\nTasks:\n- [ ] project-brief.md를 실제 프로젝트 목적에 맞게 작성\n- [ ] context-map.md를 실제 파일 구조에 맞게 작성\n`),
|
|
549
561
|
'.harness/progress-tracker.md': _canonicalProgressHeader(lang) + '\n', // 1.9.293: _canonicalProgressHeader 단일 출처 · 1.36.63: lang 전달
|
|
@@ -699,7 +711,7 @@ leerness memory restore <surface> <target> # archive → active 복귀 (DELETE
|
|
|
699
711
|
- \`brainstorm\` 회수 범위에 3 archive 파일 (decisions/lessons/plan archive) 통합 — 과거 제거된 ideas 가 새 brainstorm 시 다시 후보로 노출. \`hits.archive\` 필드 + 복원 안내 라인.
|
|
700
712
|
- \`session close\` 텍스트 모드에 archive 누적 라인 추가 — 마감 시점 DELETE 활동 가시화 (handoff 7번째 회수와 symmetric). archive 가시성 6 surface 완성.
|
|
701
713
|
- \`brainstorm\` 텍스트 모드 lessonsExplicit / planMilestones display 추가 — 데이터 수집은 됐지만 display 가 누락돼 있던 gap 보완 fix.
|
|
702
|
-
- \`leerness task list --json\` + MCP **41 도구** (\`leerness_task_list\`) — progress-tracker.md task
|
|
714
|
+
- \`leerness task list --json\` + MCP **41 도구** (\`leerness_task_list\`) — progress-tracker.md task JSON 조회 + \`--status\` 필터 (1.36.72 부터 기본 최신 100행, 전체는 \`--all\`). Task surface CRUD MCP 완전 완성 (add/list/update/drop).
|
|
703
715
|
- MCP **42 도구** (\`leerness_rule_remove\`) — rules.md 에서 특정 rule 제거 + archive 보존. **5 surface CRUD MCP 완전 완성** (task/decision/lesson/plan/rule 모두 add/list/delete MCP 노출).
|
|
704
716
|
- MCP \`leerness_drift_check\` JSON 응답 fix — \`--json\` 플래그 자동 추가하여 외부 AI가 구조화된 drift 신호 회수 (score, level, signals[], healthy).
|
|
705
717
|
- \`.harness/session-workflow.md\` 템플릿에 **🧠 Memory CRUD Quick Reference** 섹션 추가 — 5 surface × CRUD 매트릭스 + archive cycle 워크플로 가이드. 신규 \`init\` 워크스페이스 즉시 적용.
|
|
@@ -9349,11 +9361,18 @@ function taskList(root) {
|
|
|
9349
9361
|
const statusFilter = arg('--status', '');
|
|
9350
9362
|
const filtered = statusFilter ? rows.filter(r => r.status === statusFilter) : rows;
|
|
9351
9363
|
if (has('--json')) {
|
|
9364
|
+
// 1.36.72 (성장 한계 클래스 4, 1.36.69~71 규율): tasks 전량 임베드는 무제한 성장 — 기본 최신 100행 +
|
|
9365
|
+
// shown/truncated 정직 표기, --all/--limit 옵트인. total 은 필터 후 전량 그대로(수치 무왜곡).
|
|
9366
|
+
const _rawLim = parseInt(arg('--limit', '100'), 10);
|
|
9367
|
+
const _lim = has('--all') ? filtered.length : (Number.isFinite(_rawLim) && _rawLim > 0 ? _rawLim : 100);
|
|
9368
|
+
const _shownRows = filtered.slice(-_lim); // rows 는 오래된 것부터 — 최신 유지
|
|
9352
9369
|
const payload = {
|
|
9353
9370
|
version: VERSION,
|
|
9354
9371
|
root: absRoot(root),
|
|
9355
9372
|
total: filtered.length,
|
|
9356
|
-
|
|
9373
|
+
shown: _shownRows.length,
|
|
9374
|
+
truncated: _shownRows.length < filtered.length,
|
|
9375
|
+
tasks: _shownRows.map(r => ({
|
|
9357
9376
|
id: r.id,
|
|
9358
9377
|
status: r.status,
|
|
9359
9378
|
request: r.request,
|
|
@@ -18714,7 +18733,7 @@ function _mcpToCliArgs(name, args, targetPath) {
|
|
|
18714
18733
|
case 'leerness_plan_remove': cliArgs = ['plan', 'remove', String(args.target || ''), '--path', targetPath]; break;
|
|
18715
18734
|
case 'leerness_memory_archive_list': cliArgs = ['memory', 'archive', 'list', '--path', targetPath, '--json', ...(args.surface ? ['--surface', args.surface] : []), ...(args.query ? ['--query', args.query] : [])]; break;
|
|
18716
18735
|
case 'leerness_memory_restore': cliArgs = ['memory', 'restore', String(args.surface || ''), String(args.target || ''), '--path', targetPath]; break;
|
|
18717
|
-
case 'leerness_task_list': cliArgs = ['task', 'list', '--path', targetPath, '--json', ...(args.status ? ['--status', args.status] : [])]; break;
|
|
18736
|
+
case 'leerness_task_list': cliArgs = ['task', 'list', '--path', targetPath, '--json', ...(args.status ? ['--status', args.status] : []), ...(args.all ? ['--all'] : []), ...(args.limit ? ['--limit', String(args.limit)] : [])]; break; // 1.36.72: 상한 조절 인자 전달
|
|
18718
18737
|
case 'leerness_rule_remove': cliArgs = ['rule', 'remove', String(args.id || ''), '--path', targetPath]; break;
|
|
18719
18738
|
// 1.9.141: Feature Causality Graph
|
|
18720
18739
|
case 'leerness_feature_impact': cliArgs = ['feature', 'impact', String(args.id || ''), '--path', targetPath, '--json']; break;
|
package/lib/mcp-tools.js
CHANGED
|
@@ -45,7 +45,7 @@ module.exports = [
|
|
|
45
45
|
{ name: 'leerness_plan_remove', requiredTier: 'safe-write', description: '1.9.126 — plan.md 에서 특정 milestone 블록 (### M-XXXX) 제거 (target: M-XXXX 또는 title substring). 제거된 블록은 .harness/plan.archive.md 에 자동 보존. Memory Surface DELETE 5종 완전 완성', inputSchema: { type: 'object', properties: { target: { type: 'string' }, path: { type: 'string' } }, required: ['target'] } },
|
|
46
46
|
{ name: 'leerness_memory_archive_list', requiredTier: 'read-only', description: '1.9.127 — DELETE 5종 archive 파일 통합 조회 JSON ({ decisions: [], lessons: [], plan: [], totals: { decisions, lessons, plan, all } }). 외부 AI가 과거에 제거된 항목을 회수/복원 후보로 참조. --surface 필터: decisions|lessons|plan. 1.9.138+ --query 키워드 필터 (target/originalHeader case-insensitive 매칭)', inputSchema: { type: 'object', properties: { surface: { type: 'string' }, query: { type: 'string' }, path: { type: 'string' } } } },
|
|
47
47
|
{ name: 'leerness_memory_restore', requiredTier: 'safe-write', description: '1.9.128 — archive 의 항목을 active 파일로 복귀 (DELETE→RESTORE cycle). surface: decisions|lessons|plan. target: date YYYY-MM-DD 또는 target substring 매칭. 복원된 블록은 archive 에서 제거됨. 🎉 MCP 40 도구 마일스톤', inputSchema: { type: 'object', properties: { surface: { type: 'string', enum: ['decisions', 'lessons', 'plan'] }, target: { type: 'string' }, path: { type: 'string' } }, required: ['surface', 'target'] } },
|
|
48
|
-
{ name: 'leerness_task_list', requiredTier: 'read-only', description: '1.9.134 — progress-tracker.md
|
|
48
|
+
{ name: 'leerness_task_list', requiredTier: 'read-only', description: '1.9.134 — progress-tracker.md task 조회 JSON ({ total, shown, truncated, tasks: [...] }). --status 필터 지원 (planned|in-progress|done 등). 1.36.72: 기본 최신 100행(total 은 전량) — 더 필요하면 limit(개수)/all=true(전체) 지정', inputSchema: { type: 'object', properties: { path: { type: 'string' }, status: { type: 'string' }, limit: { type: 'number' }, all: { type: 'boolean' } } } },
|
|
49
49
|
{ name: 'leerness_rule_remove', requiredTier: 'safe-write', description: '1.9.135 — rules.md 에서 특정 rule 제거 (id: R-XXXX). 제거된 rule 은 .harness/rules.archive.md 에 자동 보존 (복구 가능). Rule surface CRUD MCP 완성 (add/list/remove)', inputSchema: { type: 'object', properties: { id: { type: 'string' }, path: { type: 'string' } }, required: ['id'] } },
|
|
50
50
|
{ name: 'leerness_feature_impact', requiredTier: 'read-only', description: '1.9.141 — Feature Causality Graph 인과관계 영향 추적 JSON ({ feature, total, impacted: [{ id, title, depth, via, files, errorModes }] }). 신규 기능 추가/형식 변경 전 호출: id 변경으로 영향받는 다른 feature를 transitive (affects + co-changes + reverse depends-on) 으로 회수. 1+1=20 cascade 방지', inputSchema: { type: 'object', properties: { id: { type: 'string' }, path: { type: 'string' } }, required: ['id'] } },
|
|
51
51
|
{ name: 'leerness_feature_list', requiredTier: 'read-only', description: '1.9.141 — 전체 Feature Graph 노드 + 엣지 JSON. 외부 AI가 시스템 내 기능 의존성을 한 번에 회수', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
|
package/lib/pure-utils.js
CHANGED
|
@@ -804,7 +804,7 @@ function _managedMerge(file, next, previous, archiveRel, overwriteSet, opts = {}
|
|
|
804
804
|
// preserved 래퍼 자체의 보일러플레이트는 이월 대상 아님 (구 <details> 형식 + 신 평문 형식 + en 래퍼 모두)
|
|
805
805
|
if (l === tag || l === '---' || l === '<details>' || l === '</details>') continue;
|
|
806
806
|
if (/^```/.test(l) || /^<summary>/.test(l)) continue;
|
|
807
|
-
if (/^## Preserved previous content|^Previous content was backed up|^`[^`]*archive[^`]*`$|^> 이전 버전에서 이어진|^> Custom user\/project content carried over
|
|
807
|
+
if (/^## Preserved previous content|^Previous content was backed up|^`[^`]*archive[^`]*`$|^> 이전 버전에서 이어진|^> Custom user\/project content carried over|^> ⚠ \d+ Korean lines were preserved below\. If this project never had Korean customizations.*with certainty\.$/.test(l)) continue; // (검수 #2) 생성 문장 전체 앵커 — 같은 접두의 사용자 커스텀 라인 오삭제 방지
|
|
808
808
|
if (seen.has(l)) continue;
|
|
809
809
|
seen.add(l);
|
|
810
810
|
custom.push(raw.replace(/\\`\\`\\`/g, '```')); // 구 형식에서 이스케이프된 펜스 복원
|
|
@@ -812,9 +812,18 @@ function _managedMerge(file, next, previous, archiveRel, overwriteSet, opts = {}
|
|
|
812
812
|
if (!custom.length) return next;
|
|
813
813
|
const ar = archiveRel || '.harness/archive';
|
|
814
814
|
// 1.36.60 (검수): 보존 래퍼 문구도 프로젝트 언어를 따른다 — en 프로젝트에 한글 래퍼가 새 한글원을 만들던 것 해소
|
|
815
|
-
|
|
815
|
+
let note = opts.lang === 'en'
|
|
816
816
|
? `> Custom user/project content carried over from the previous version — re-carried automatically on every migration. Full original backup: \`${ar}\``
|
|
817
817
|
: `> 이전 버전에서 이어진 사용자/프로젝트 커스텀 내용 — 마이그레이션마다 자동 이월됩니다. 전체 원본 백업: \`${ar}\``;
|
|
818
|
+
// 1.36.73 (8차 헌트 F9 설계 결정): 초고령 KO canonical(현행 altTemplate 에 없는 구판 템플릿)이 en 프로젝트에
|
|
819
|
+
// 커스텀으로 오인 이월될 수 있다. 자동 차감은 데이터 손실 위험(어느 라인이 템플릿인지 확신 불가 — false-DROP 이 버그)
|
|
820
|
+
// 이라 하지 않고, 한글 비중이 높으면 "검토·정리 안내"를 명시해 조용한 동결만 없앤다.
|
|
821
|
+
if (opts.lang === 'en') {
|
|
822
|
+
const hangulLines = custom.filter(l => /[가-힣]/.test(l)).length;
|
|
823
|
+
if (hangulLines >= 10 && hangulLines > custom.length * 0.5) {
|
|
824
|
+
note += `\n> ⚠ ${hangulLines} Korean lines were preserved below. If this project never had Korean customizations, most of these are likely template text from an old Korean install — review and prune manually (data is also in the backup above). leerness will not auto-delete them: it cannot distinguish old template lines from your own notes with certainty.`;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
818
827
|
return next.trimEnd() + `\n\n---\n${tag}\n## Preserved previous content\n\n${note}\n\n` + custom.join('\n') + '\n';
|
|
819
828
|
}
|
|
820
829
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.73",
|
|
4
4
|
"description": "The AI-coding operations layer that makes \"done\" require evidence — persistent memory, evidence-gated completion checks, and clean handoffs for any AI agent (Claude Code, Codex, Cursor). State lives as plain files in your repo. CLI + MCP, 0 runtime dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"leerness",
|
package/scripts/e2e.js
CHANGED
|
@@ -7270,5 +7270,72 @@ total++;
|
|
|
7270
7270
|
if (!ok) failed++;
|
|
7271
7271
|
}
|
|
7272
7272
|
|
|
7273
|
+
// 1.36.72 (성장 한계 클래스 4): task list --json 상한 100 + lint 거짓 PASS(F16) 차단
|
|
7274
|
+
total++;
|
|
7275
|
+
{
|
|
7276
|
+
let ok = false;
|
|
7277
|
+
const _d = [];
|
|
7278
|
+
try {
|
|
7279
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-tlcap-')); _d.push(d);
|
|
7280
|
+
cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
|
|
7281
|
+
const R = (extra) => JSON.parse(cp.spawnSync(process.execPath, [CLI, 'task', 'list', '--path', d, '--json', ...extra], { encoding: 'utf8', timeout: 20000, maxBuffer: 32 * 1024 * 1024 }).stdout);
|
|
7282
|
+
const baseline = R(['--all']).total;
|
|
7283
|
+
for (let i = 1; i <= 105; i++) { const a = cp.spawnSync(process.execPath, [CLI, 'task', 'add', `x${i}`, '--path', d], { encoding: 'utf8', timeout: 15000 }); if (a.status !== 0) throw new Error('add fail'); }
|
|
7284
|
+
const expTotal = baseline + 105;
|
|
7285
|
+
const j = R([]);
|
|
7286
|
+
const capOk = j.shown === 100 && j.total === expTotal && j.truncated === true && j.tasks.length === 100 && j.tasks[99].request === 'x105';
|
|
7287
|
+
const allOk = R(['--all']).shown === expTotal;
|
|
7288
|
+
const limOk = R(['--limit', '7']).tasks.length === 7;
|
|
7289
|
+
// F16: lint 가 없는 루트/빈 루트에서 실패해야 함 (거짓 PASS 차단)
|
|
7290
|
+
const LINT = path.join(path.dirname(CLI), '..', 'scripts', 'lint.js');
|
|
7291
|
+
const missOk = cp.spawnSync(process.execPath, [LINT, path.join(d, 'no-such-dir')], { encoding: 'utf8', timeout: 20000 }).status === 1;
|
|
7292
|
+
const emptyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-lintempty-')); _d.push(emptyDir);
|
|
7293
|
+
const emptyOk = cp.spawnSync(process.execPath, [LINT, emptyDir], { encoding: 'utf8', timeout: 20000 }).status === 1;
|
|
7294
|
+
ok = capOk && allOk && limOk && missOk && emptyOk;
|
|
7295
|
+
if (!ok) console.log(` [tlcap 디버그] cap=${capOk}(${j.shown}/${j.total}) all=${allOk} lim=${limOk} lintMiss=${missOk} lintEmpty=${emptyOk}`);
|
|
7296
|
+
} catch (e) {} finally { _d.forEach(x => { try { fs.rmSync(x, { recursive: true, force: true }); } catch {} }); }
|
|
7297
|
+
console.log(ok ? '✓ B(1.36.72) task list --json 상한 100 + --all/--limit + lint 거짓 PASS 차단(F16)' : '✗ task list 상한/lint F16 실패');
|
|
7298
|
+
if (!ok) failed++;
|
|
7299
|
+
}
|
|
7300
|
+
|
|
7301
|
+
// 1.36.73 (8차 헌트 F12/F9 종결): skills-lock = 디스크 실재 + KO-잔존 이월 안내
|
|
7302
|
+
total++;
|
|
7303
|
+
{
|
|
7304
|
+
let ok = false;
|
|
7305
|
+
const _d = [];
|
|
7306
|
+
try {
|
|
7307
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-f12-')); _d.push(d);
|
|
7308
|
+
// (검수 #3) exit 단언 + 정렬 집합 정확 비교 — 카운트만 비교하면 2차 init 실패를 1차 결과가 가렸다
|
|
7309
|
+
const i1 = cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--language', 'en', '--skills', 'all', '--no-stale-check', '--no-env'], { encoding: 'utf8', timeout: 40000 });
|
|
7310
|
+
const i2 = cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--language', 'en', '--skills', 'recommended', '--no-stale-check', '--no-env'], { encoding: 'utf8', timeout: 40000 });
|
|
7311
|
+
const dirSet = fs.readdirSync(path.join(d, '.harness', 'skills'), { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
7312
|
+
const lockSet = Object.keys(JSON.parse(fs.readFileSync(path.join(d, '.harness', 'skills-lock.json'), 'utf8')).installedSkills).sort();
|
|
7313
|
+
const f12Ok = i1.status === 0 && i2.status === 0 && dirSet.length > 5 && JSON.stringify(dirSet) === JSON.stringify(lockSet);
|
|
7314
|
+
// F9: 실 CLI migrate 경유 — 구판 KO canonical 을 en 프로젝트 CLAUDE.md 에 심고 migrate
|
|
7315
|
+
const d9 = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-f9-')); _d.push(d9);
|
|
7316
|
+
const i3 = cp.spawnSync(process.execPath, [CLI, 'init', d9, '--yes', '--language', 'en', '--no-stale-check', '--no-env'], { encoding: 'utf8', timeout: 40000 });
|
|
7317
|
+
const cl = path.join(d9, 'CLAUDE.md');
|
|
7318
|
+
const ko = Array.from({ length: 15 }, (_, i) => '한국어 구판 템플릿 라인 ' + i).join('\n');
|
|
7319
|
+
fs.appendFileSync(cl, '\n' + ko + '\nMY_CUSTOM_KEEP_42\n');
|
|
7320
|
+
fs.writeFileSync(path.join(d9, '.harness', 'HARNESS_VERSION'), '1.36.0');
|
|
7321
|
+
const mg = cp.spawnSync(process.execPath, [CLI, 'migrate', d9], { encoding: 'utf8', timeout: 60000, maxBuffer: 32 * 1024 * 1024 });
|
|
7322
|
+
const after = fs.readFileSync(cl, 'utf8');
|
|
7323
|
+
const mg2 = cp.spawnSync(process.execPath, [CLI, 'migrate', d9], { encoding: 'utf8', timeout: 60000, maxBuffer: 32 * 1024 * 1024 });
|
|
7324
|
+
const after2 = fs.readFileSync(cl, 'utf8');
|
|
7325
|
+
const f9Ok = i3.status === 0 && mg.status === 0 && mg2.status === 0
|
|
7326
|
+
&& after.includes('Korean lines were preserved') // 실 파일에 안내
|
|
7327
|
+
&& after.includes('MY_CUSTOM_KEEP_42') // 커스텀 무손실
|
|
7328
|
+
&& (after2.match(/Korean lines were preserved/g) || []).length === 1; // 재migrate 멱등
|
|
7329
|
+
// (검수 #2 회귀) 같은 접두의 사용자 커스텀 라인은 생존해야
|
|
7330
|
+
const m = require(path.join(path.dirname(CLI), '..', 'lib', 'pure-utils.js'));
|
|
7331
|
+
const userLine = '> ⚠ 10 Korean lines were preserved for localization QA.';
|
|
7332
|
+
const surv = m._managedMerge('CLAUDE.md', '# N\n', '# O\n' + userLine, 'a', null, { lang: 'en' }).includes(userLine);
|
|
7333
|
+
ok = f12Ok && f9Ok && surv;
|
|
7334
|
+
if (!ok) console.log(` [f12/f9 디버그] f12=${f12Ok}(${dirSet.length}/${lockSet.length}) f9=${f9Ok}(mg=${mg.status}) surv=${surv}`);
|
|
7335
|
+
} catch (e) {} finally { _d.forEach(x => { try { fs.rmSync(x, { recursive: true, force: true }); } catch {} }); }
|
|
7336
|
+
console.log(ok ? '✓ B(1.36.73) F12 skills-lock=디스크 실재 + F9 KO-잔존 이월 안내(멱등·무손실·소수 무경고)' : '✗ F12/F9 실패');
|
|
7337
|
+
if (!ok) failed++;
|
|
7338
|
+
}
|
|
7339
|
+
|
|
7273
7340
|
console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
|
|
7274
7341
|
if (failed > 0) process.exit(1);
|
package/scripts/lint.js
CHANGED
|
@@ -8,6 +8,11 @@ const path = require('path');
|
|
|
8
8
|
const cp = require('child_process');
|
|
9
9
|
|
|
10
10
|
const ROOT = path.resolve(process.argv[2] || path.join(__dirname, '..')); // 테스트용 루트 오버라이드 허용
|
|
11
|
+
// 1.36.72 (8차 헌트 F16): 존재하지 않는/빈 루트가 "0 js clean" 거짓 PASS 로 통과하던 것 — 명시 오류
|
|
12
|
+
if (!fs.existsSync(ROOT) || !fs.statSync(ROOT).isDirectory()) {
|
|
13
|
+
console.log(`✗ lint: root not found — ${ROOT}`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
11
16
|
const TARGET_DIRS = ['bin', 'lib', 'scripts'];
|
|
12
17
|
const failures = [];
|
|
13
18
|
|
|
@@ -26,6 +31,11 @@ function listFiles(dir, ext) {
|
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
const jsFiles = TARGET_DIRS.flatMap(d => listFiles(path.join(ROOT, d), '.js'));
|
|
34
|
+
// 1.36.72 (F16): 대상 0건도 거짓 PASS — lint 가 아무것도 검사하지 않았다는 뜻이므로 실패
|
|
35
|
+
if (!jsFiles.length) {
|
|
36
|
+
console.log(`✗ lint: no js files found under ${TARGET_DIRS.join('/')} in ${ROOT} — nothing was checked`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
29
39
|
|
|
30
40
|
// ① 구문 (node --check)
|
|
31
41
|
for (const f of jsFiles) {
|