leerness 1.36.27 → 1.36.28
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 +15 -0
- package/README.md +4 -4
- package/bin/leerness.js +81 -6
- package/lib/pure-utils.js +41 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.36.28 — 2026-07-15 — 미검토 표면 헌트: 데이터 유실 클래스(손상 스토어 클로버 · URL 덮어쓰기) + 정확성(impact 전이 · alias 경계)
|
|
4
|
+
|
|
5
|
+
rotate-review-targets 교훈 적용 — 최근 잘 검토된 표면(lens/memory/plan/requests/skill)을 **제외**하고 미검토 표면(state/team/api-skill/wakeup/intent/feature)을 codex 로 QA 헌트. 10 findings 중 결정적 5건을 맹신 X(전부 재현) 후, 데이터 유실 클래스 + 저위험 순수 교정 4건을 수정. 동시성 3건(크로스프로세스 락)은 0-deps rabbit-hole 위험이라 정직하게 이연.
|
|
6
|
+
|
|
7
|
+
- **#3 P1 (데이터 유실 클래스) — 손상 JSON 스토어를 빈 값으로 오인해 덮어쓰기**: state.json/`teams.json`/`platform-constraints.json` 이 손상되면 fail-open 으로 빈 기본값이 되어 기존 데이터(진행 중 run·팀·커스텀 제약)를 소실시켰다. `_assertStoreParsable`(파일 "없음"과 "있으나 손상" 구분) + `_guardStore` 로 **변경(저장)을 거부하고 원본 보존** + `store_corrupt` 구조화 에러. 설계: SAVE 진입점에서만 던짐(handoff 등 비변경 읽기는 resilient 유지) — 단 state 는 카운터 리셋이 run 파일 클로버를 유발해 load 에서 던지고 stateCmd 를 감쌈.
|
|
8
|
+
- **#4 P2 (데이터 유실) — api-skill 이 쿼리 다른 URL 을 같은 id 로 덮어씀**: `?doc=alpha` 와 `?doc=beta` 가 둘 다 `example.md` 로 슬러그돼 앞 URL·방향이 소실. 같은 id 파일의 저장 URL 이 다르면 정규화 URL 해시(`_shortHash`)를 붙여 분리, 같은 URL 재등록은 멱등 갱신 유지.
|
|
9
|
+
- **#8 P2 (정확성) — feature impact 역의존 전이 누락**: `A←B←C` 에서 `impact A` 가 B 만 반환(C 누락). 역방향 depends-on 을 BFS 종료 후 1-홉이 아니라 BFS **안**에서 확장(visited 로 사이클 안전) → C 가 depth 2 로 포착.
|
|
10
|
+
- **#10 P2 (정확성) — intent alias 부분문자열 오탐**: `restore`→'rest'(API), `client`→'cli'(CLI) 오분류. ASCII 단어 alias 는 단어경계를 요구(`_aliasHit`), 구두점/CJK alias 는 substring 유지.
|
|
11
|
+
- **검증**: selftest 298/298(전이+사이클안전+alias 경계+가드 배선+해시), 4건 전부 원 재현본으로 before/after + 정상 무회귀 실측, 게이트 e2e, 게시본 클린룸.
|
|
12
|
+
- 이연: #7(pre-wake-audit 테이블형 in-progress 미인식 — session-resume 매처 재사용), 동시성 #1/#2/#5(state/team 병렬 lost-update — 크로스프로세스 락 필요).
|
|
13
|
+
|
|
14
|
+
**반복 마이그레이션 커스텀 유실 (사용자 보고, P1, 동일 릴리스 합류)**: "AI 에게 매번 최신 마이그레이션을 시키는" 표준 흐름에서 **2번째 마이그레이션부터 CLAUDE.md/AGENTS.md 의 사용자 커스텀 지시가 조용히 사라졌다** — `_managedMerge` 가 preserved 태그를 보면 병합 없이 새 템플릿만 반환했기 때문(구 `<details>` 중첩-방지 설계의 부작용). **실증**: 1.26.0/1.9.206 실제 구버전으로 워크스페이스 구축(23종 마커) → migrate: 1차는 유실 0 이지만, 커스텀 추가 후 2차에서 신·구 커스텀 모두 유실(archive 백업에만 잔존).
|
|
15
|
+
- **재설계**: 라인-집합 diff — "새 템플릿에 없는 라인"만 Preserved 섹션에 평문 이월. 멱등(연속 5회 마이그레이션에서 커스텀 3종 전부 보존, 크기 고정), 중첩 없음, 레거시 `<details>` 형식에서도 커스텀 회수(래퍼 미이월), overwriteSet(통째 교체 파일) 존중. 편향은 명시적으로 false-PRESERVE(과보존 무해) — false-DROP(유실)이 버그.
|
|
16
|
+
- 마이그레이션 유실 전수 검증 결과(이번 실측): `.harness/` 데이터 파일(task/lesson/decision/rule/request/plan/feature/team/skill + 수기 편집 project-brief/current-state/handoff/guideline/design-system)은 **1.26.0→최신, 1.9.206→최신, 반복 실행 모두 유실 0** — 유일한 유실 경로가 위 managed-merge 였고 이번에 봉합.
|
|
17
|
+
|
|
3
18
|
## 1.36.27 — 2026-07-15 — debug 렌즈 — "근본원인 조사 없이 수정 금지"를 자기질문으로 (obra/superpowers systematic-debugging)
|
|
4
19
|
|
|
5
20
|
superpowers 검토 아크 마지막 채택분. 원문(systematic-debugging SKILL)의 규율을 **lens 카테고리 정합대로 자기질문 6문항으로 재작성** — 대문자 명령형/강제 게이트는 이식하지 않음(lens 는 advisory 표면, 강제로 포장하면 과장).
|
package/README.md
CHANGED
|
@@ -121,7 +121,7 @@ MIT
|
|
|
121
121
|
<!-- leerness:project-readme:start -->
|
|
122
122
|
## Leerness Project Harness
|
|
123
123
|
|
|
124
|
-
이 프로젝트는 Leerness v1.36.
|
|
124
|
+
이 프로젝트는 Leerness v1.36.28 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
|
|
125
125
|
|
|
126
126
|
### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
|
|
127
127
|
|
|
@@ -175,7 +175,7 @@ leerness memory restore decision <date|title>
|
|
|
175
175
|
|
|
176
176
|
### MCP server (외부 AI 통합)
|
|
177
177
|
|
|
178
|
-
Leerness v1.36.
|
|
178
|
+
Leerness v1.36.28는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
|
|
179
179
|
|
|
180
180
|
```jsonc
|
|
181
181
|
// 카테고리별
|
|
@@ -196,7 +196,7 @@ Leerness v1.36.27는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
|
|
|
196
196
|
`<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
|
|
197
197
|
1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) stress-v* 신규 작성 + 누적 회귀 → 4) e2e 219/219 → 5) npm pack + git tag + GitHub release → 6) main 자동 push → 7) session close → 8) 다음 라운드 예약.
|
|
198
198
|
|
|
199
|
-
현재 누적: **70 라운드 (1.9.40 → 1.36.
|
|
199
|
+
현재 누적: **70 라운드 (1.9.40 → 1.36.28)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
|
|
200
200
|
|
|
201
201
|
### 성능 가이드
|
|
202
202
|
|
|
@@ -234,6 +234,6 @@ leerness release pack --close --auto-main-push
|
|
|
234
234
|
- `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
|
|
235
235
|
- `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
|
|
236
236
|
|
|
237
|
-
Last synced by Leerness v1.36.
|
|
237
|
+
Last synced by Leerness v1.36.28: 2026-07-15
|
|
238
238
|
<!-- leerness:project-readme:end -->
|
|
239
239
|
|
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.28';
|
|
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') 시 호스트 프로세스 오염.
|
|
@@ -3027,6 +3027,47 @@ function _selfTestCases() {
|
|
|
3027
3027
|
const encOk = s.includes("(result.applied || []).some(a => a.action === 'failed')) process.exitCode = 1"); // encoding: 실패 exit1
|
|
3028
3028
|
return reuseOk && releaseOk && encOk;
|
|
3029
3029
|
} },
|
|
3030
|
+
{ name: '미검토표면 헌트 (1.36.28, codex): #8 impact 역의존 전이 + #10 alias 단어경계 + #3 손상스토어 클로버 차단 + #4 URL 해시 — 순수/소스 행위검사', run: () => {
|
|
3031
|
+
const p = require('../lib/pure-utils');
|
|
3032
|
+
// #8: A←B←C 전이 (F-0003 이 depth 2 로 잡혀야)
|
|
3033
|
+
const nodes = [{ id: 'F1', title: 'A', dependsOn: [] }, { id: 'F2', title: 'B', dependsOn: ['F1'] }, { id: 'F3', title: 'C', dependsOn: ['F2'] }];
|
|
3034
|
+
const imp = p._featureImpactBfs(nodes, 'F1').map(x => x.id);
|
|
3035
|
+
const transitiveOk = imp.includes('F2') && imp.includes('F3');
|
|
3036
|
+
// 사이클 안전: F1↔F2 순환이어도 무한루프/중복 없이 종료
|
|
3037
|
+
const cyc = p._featureImpactBfs([{ id: 'F1', dependsOn: ['F2'] }, { id: 'F2', dependsOn: ['F1'] }], 'F1');
|
|
3038
|
+
const cycleSafe = Array.isArray(cyc) && cyc.length <= 2;
|
|
3039
|
+
// #10: 부분문자열 오탐 차단 + 실매칭 유지
|
|
3040
|
+
const aliasOk = typeof p._aliasHit === 'function'
|
|
3041
|
+
&& !p._aliasHit('restore deleted files', 'rest') && !p._aliasHit('the client cache', 'cli')
|
|
3042
|
+
&& p._aliasHit('call the rest endpoint', 'rest') && p._aliasHit('run cli now', 'cli')
|
|
3043
|
+
&& p._aliasHit('노드제이에스 node.js 설정', 'node.js'); // 구두점 alias 는 substring 유지
|
|
3044
|
+
// #3/#4: 소스 배선 확인 (행위는 e2e/실측, 여기선 가드 존재)
|
|
3045
|
+
const s = read(__filename);
|
|
3046
|
+
const guardOk = s.includes('function _assertStoreParsable') && s.includes("code: 'E_STORE_CORRUPT'")
|
|
3047
|
+
&& s.includes('_guardStore(has(') && s.includes("_assertStoreParsable(teamsJsonPath")
|
|
3048
|
+
&& s.includes("_assertStoreParsable(_platformConstraintsPath");
|
|
3049
|
+
const hashOk = s.includes('function _shortHash') && s.includes("id = id + '-' + _shortHash(url)") && s.includes('urls:\\s*\\r?\\n');
|
|
3050
|
+
return transitiveOk && cycleSafe && aliasOk && guardOk && hashOk;
|
|
3051
|
+
} },
|
|
3052
|
+
{ name: '반복 마이그레이션 커스텀 보존 (1.36.28, 사용자 보고): _managedMerge 라인-diff 이월 — 2번째부터 유실되던 CLAUDE/AGENTS 커스텀 지시 (행위검사)', run: () => {
|
|
3053
|
+
const p = require('../lib/pure-utils');
|
|
3054
|
+
const next = '# T\nline A\nline B\n';
|
|
3055
|
+
const prev = '# T\nline A\nline B\nMY CUSTOM 1\n';
|
|
3056
|
+
const m1 = p._managedMerge('CLAUDE.md', next, prev, '.harness/archive', null);
|
|
3057
|
+
const m2 = p._managedMerge('CLAUDE.md', next, m1, '.harness/archive', null);
|
|
3058
|
+
const m3 = p._managedMerge('CLAUDE.md', next, m2, '.harness/archive', null);
|
|
3059
|
+
// 핵심 판별: 종전 구현은 m2 에서 MY CUSTOM 1 이 사라졌다 (previous 에 preserved 태그 있으면 next 만 반환)
|
|
3060
|
+
const survived = m1.includes('MY CUSTOM 1') && m2.includes('MY CUSTOM 1') && m3.includes('MY CUSTOM 1');
|
|
3061
|
+
const idem = m2 === m3;
|
|
3062
|
+
// 레거시 <details> 형식에서 커스텀 회수 + 래퍼 미이월
|
|
3063
|
+
const F = String.fromCharCode(96, 96, 96);
|
|
3064
|
+
const legacy = next + '\n---\n<!-- leerness:migration-preserved -->\n## Preserved previous content\n\n<details>\n<summary>Previous CLAUDE.md</summary>\n\n' + F + 'md\n# T\nOLD CUSTOM X\n' + F + '\n\n</details>\n';
|
|
3065
|
+
const lm = p._managedMerge('CLAUDE.md', next, legacy, '.harness/archive', null);
|
|
3066
|
+
const legacyOk = lm.includes('OLD CUSTOM X') && !lm.includes('<details>');
|
|
3067
|
+
const cleanOk = p._managedMerge('CLAUDE.md', next, next, '.harness/archive', null) === next;
|
|
3068
|
+
const owOk = p._managedMerge('x.md', next, prev, '.', new Set(['x.md'])) === next;
|
|
3069
|
+
return survived && idem && legacyOk && cleanOk && owOk;
|
|
3070
|
+
} },
|
|
3030
3071
|
{ name: 'debug 렌즈 (1.36.27, obra/superpowers systematic-debugging): 자기질문 6문항 ko/en 락스텝 + affects 유효 + route bugfix 힌트 + 파일매핑 미확장 — 행위검사', run: () => {
|
|
3031
3072
|
const d = LENS_CATALOG.debug;
|
|
3032
3073
|
if (!d) return false;
|
|
@@ -4702,6 +4743,28 @@ function shellGuardCmd(root, cmd, opts = {}) {
|
|
|
4702
4743
|
if (analysis.issues.some(i => i.severity === 'error')) process.exitCode = 1;
|
|
4703
4744
|
}
|
|
4704
4745
|
|
|
4746
|
+
// 1.36.28 (codex 미검토표면 헌트 #3 — 데이터 유실 클래스): 손상된 JSON 스토어를 "빈 값"으로 오인해 덮어쓰는 fail-open 방지.
|
|
4747
|
+
// 파일이 "없음"과 "있으나 손상"을 구분 — 후자는 파싱 실패를 던져 변경(저장)을 거부하고 원본을 보존한다.
|
|
4748
|
+
// 설계: LOAD 가 아니라 SAVE/변경 진입점에서만 던진다(handoff 등 비변경 읽기 경로는 resilient 유지). state 는
|
|
4749
|
+
// 카운터 리셋이 run 파일 클로버를 유발하므로 예외적으로 load 에서 던지고 stateCmd 를 _guardStore 로 감싼다.
|
|
4750
|
+
function _assertStoreParsable(file, label) {
|
|
4751
|
+
if (exists(file)) {
|
|
4752
|
+
try { JSON.parse(read(file)); }
|
|
4753
|
+
catch { throw Object.assign(new Error(`${label} 저장 파일이 손상돼(JSON 파싱 실패) 덮어쓰기를 거부합니다: ${file} — 파일을 복구하거나 삭제 후 재시도하세요`), { code: 'E_STORE_CORRUPT', file }); }
|
|
4754
|
+
}
|
|
4755
|
+
}
|
|
4756
|
+
function _guardStore(jsonMode, fn) {
|
|
4757
|
+
try { return fn(); }
|
|
4758
|
+
catch (e) { if (e && e.code === 'E_STORE_CORRUPT') { failJson(jsonMode, 'store_corrupt', e.message); return undefined; } throw e; }
|
|
4759
|
+
}
|
|
4760
|
+
// 1.36.28 (#4): 0-deps 짧은 결정적 해시 (URL 충돌 구분용, 보안 아님).
|
|
4761
|
+
function _shortHash(s) {
|
|
4762
|
+
let h = 5381;
|
|
4763
|
+
const str = String(s || '');
|
|
4764
|
+
for (let i = 0; i < str.length; i++) h = ((h << 5) + h + str.charCodeAt(i)) >>> 0;
|
|
4765
|
+
return h.toString(36).slice(0, 6);
|
|
4766
|
+
}
|
|
4767
|
+
|
|
4705
4768
|
// 1.9.245: API skill cache — 공식 문서 + 관련 링크 자동 정리 (사용자 명시 UR-0015)
|
|
4706
4769
|
// .harness/api-skills/<id>.md 에 frontmatter + 본문 저장. handoff 자동 매칭.
|
|
4707
4770
|
// Node built-in https (의존성 0), depth=1 same-domain crawl, max 10 links.
|
|
@@ -4950,7 +5013,16 @@ async function apiSkillCmd(root, sub) {
|
|
|
4950
5013
|
try {
|
|
4951
5014
|
const doc = await _collectAPIDoc(url, { direction, noCrawl });
|
|
4952
5015
|
const skillName = name || doc.title || new URL(url).hostname;
|
|
4953
|
-
|
|
5016
|
+
let id = _apiSkillId(url, name);
|
|
5017
|
+
// 1.36.28 (#4): 쿼리/경로가 달라 URL 은 다른데 slug 가 같으면(예: example.com/?doc=alpha vs ?doc=beta) 기존 파일을
|
|
5018
|
+
// 덮어써 이전 URL·방향이 유실됐다. 같은 id 파일이 이미 있고 저장된 URL 이 다르면 정규화 URL 해시를 붙여 분리한다.
|
|
5019
|
+
const _idFile = (i) => path.join(dir, i + '.md');
|
|
5020
|
+
if (fs.existsSync(_idFile(id))) {
|
|
5021
|
+
const existing = fs.readFileSync(_idFile(id), 'utf8');
|
|
5022
|
+
const um = existing.match(/^urls:\s*\r?\n\s*-\s*(\S.*)$/m); // 'urls:' 블록의 첫 항목만 (--- 구분선 오매치 방지)
|
|
5023
|
+
const storedUrl = um ? um[1].trim() : '';
|
|
5024
|
+
if (storedUrl && storedUrl !== url) id = id + '-' + _shortHash(url);
|
|
5025
|
+
}
|
|
4954
5026
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
4955
5027
|
const md = _serializeAPISkill(id, skillName, [url], direction, doc);
|
|
4956
5028
|
fs.writeFileSync(path.join(dir, id + '.md'), md, 'utf8');
|
|
@@ -5551,6 +5623,7 @@ function _loadPlatformConstraints(root) {
|
|
|
5551
5623
|
} catch { return _DEFAULT_PLATFORM_CONSTRAINTS; }
|
|
5552
5624
|
}
|
|
5553
5625
|
function _writePlatformConstraints(root, catalog) {
|
|
5626
|
+
_assertStoreParsable(_platformConstraintsPath(root), 'platform-constraints'); // 1.36.28 (#3): 손상 파일 덮어써 커스텀 제약 유실 방지 (try 밖 — 삼켜지지 않게)
|
|
5554
5627
|
try {
|
|
5555
5628
|
mkdirp(path.join(root, '.harness'));
|
|
5556
5629
|
writeUtf8(_platformConstraintsPath(root), JSON.stringify({ ...catalog, updatedAt: new Date().toISOString() }, null, 2));
|
|
@@ -7790,6 +7863,7 @@ function _loadTeams(root) {
|
|
|
7790
7863
|
}
|
|
7791
7864
|
function _saveTeams(root, teams) {
|
|
7792
7865
|
const arr = Array.isArray(teams) ? teams : [];
|
|
7866
|
+
_assertStoreParsable(teamsJsonPath(root), 'team'); // 1.36.28 (#3): 손상 teams.json 을 덮어써 기존 팀 유실 방지
|
|
7793
7867
|
mkdirp(path.dirname(teamsJsonPath(root)));
|
|
7794
7868
|
writeUtf8(teamsJsonPath(root), JSON.stringify(arr, null, 2) + '\n');
|
|
7795
7869
|
writeUtf8(teamsPath(root), _renderTeamsMd(arr));
|
|
@@ -18632,7 +18706,8 @@ function _leernessStateDir(root) { return path.join(absRoot(root), '.leerness');
|
|
|
18632
18706
|
function _loadLeernessState(root) {
|
|
18633
18707
|
const f = path.join(_leernessStateDir(root), 'state.json');
|
|
18634
18708
|
if (!exists(f)) return { schemaVersion: 1, project: detectProjectName(root), currentRunId: null, runCounter: 0, updatedAt: null };
|
|
18635
|
-
|
|
18709
|
+
// 1.36.28 (#3): 손상 파일을 빈 상태로 오인하면 카운터가 0으로 리셋돼 run-0001 을 덮어쓴다(데이터 유실). 파싱 실패는 던진다.
|
|
18710
|
+
try { return JSON.parse(read(f)); } catch { throw Object.assign(new Error(`.leerness/state.json 손상 — 덮어쓰기 거부: ${f} (복구/삭제 후 재시도)`), { code: 'E_STORE_CORRUPT', file: f }); }
|
|
18636
18711
|
}
|
|
18637
18712
|
function _saveLeernessState(root, state) {
|
|
18638
18713
|
const dir = _leernessStateDir(root); mkdirp(dir);
|
|
@@ -21504,9 +21579,9 @@ async function main() {
|
|
|
21504
21579
|
// 1.9.272: leerness capabilities — 권한/보안 표면 공개 (GPT-5.5 외부 리뷰 반영)
|
|
21505
21580
|
if (cmd === 'capabilities' || cmd === 'security-surface') return capabilitiesCmd(arg('--path', process.cwd()), { json: has('--json') });
|
|
21506
21581
|
// 1.9.278 (UR-0032): leerness state <show|start|record|verify|handoff> — .leerness/ 구조화 상태 substrate
|
|
21507
|
-
if (cmd === 'state') return stateCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
|
|
21582
|
+
if (cmd === 'state') return _guardStore(has('--json'), () => stateCmd(arg('--path', process.cwd()), args[1], ...args.slice(2))); // 1.36.28 (#3): 손상 스토어 클로버 차단
|
|
21508
21583
|
if (cmd === 'context') return contextCmd(_resolveRoot(args[1]), { json: has('--json') });
|
|
21509
|
-
if (cmd === 'team') return teamCmd(_resolveRoot(args[1] === 'list' ? args[2] : null), args[1], args[2], { json: has('--json') }); // 1.9.371 (UR-0073 Phase A): 팀 레지스트리. 1.9.412 (UR-0100): team list 만 positional path
|
|
21584
|
+
if (cmd === 'team') return _guardStore(has('--json'), () => teamCmd(_resolveRoot(args[1] === 'list' ? args[2] : null), args[1], args[2], { json: has('--json') })); // 1.9.371 (UR-0073 Phase A): 팀 레지스트리. 1.9.412 (UR-0100): team list 만 positional path 지원. 1.36.28 (#3): 손상 스토어 클로버 차단
|
|
21510
21585
|
if (cmd === 'brief') return briefCmd(arg('--path', process.cwd()), args[1]);
|
|
21511
21586
|
if (cmd === 'about' || cmd === 'identity') return aboutCmd({ json: has('--json') });
|
|
21512
21587
|
// 1.9.281 (UR-0034): leerness policy <show|set|check> — 권한 등급 (opt-in enforced)
|
|
@@ -21535,7 +21610,7 @@ async function main() {
|
|
|
21535
21610
|
// 1.9.245: API skill cache — 공식 문서·관련링크 자동 정리 (사용자 명시 UR-0015)
|
|
21536
21611
|
if (cmd === 'api-skill') return apiSkillCmd(arg('--path', process.cwd()), args[1] || 'help');
|
|
21537
21612
|
// 1.9.208: leerness constraints <list|check|add> — 플랫폼/API 제약 사전 체크 (사용자 명시)
|
|
21538
|
-
if (cmd === 'constraints') return constraintsCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
|
|
21613
|
+
if (cmd === 'constraints') return _guardStore(has('--json'), () => constraintsCmd(arg('--path', process.cwd()), args[1], ...args.slice(2))); // 1.36.28 (#3): 손상 스토어 클로버 차단
|
|
21539
21614
|
// 1.9.209: leerness pre-wake-audit — sleep 전 sub-agent audit (사용자 명시)
|
|
21540
21615
|
if (cmd === 'pre-wake-audit') return preWakeAuditCmd(arg('--path', process.cwd()), args[1]);
|
|
21541
21616
|
// 1.9.210: leerness wakeup-interval <get|set|auto|history|record> — adaptive interval (사용자 명시)
|
package/lib/pure-utils.js
CHANGED
|
@@ -518,13 +518,22 @@ function _matchConstraints(catalog, text, lang) {
|
|
|
518
518
|
return { matched, suggestions, totalPlatforms: Object.keys(catalog.platforms).length };
|
|
519
519
|
}
|
|
520
520
|
|
|
521
|
+
// 1.36.28 (codex 미검토표면 헌트 #10): alias 매칭 — 순수 ASCII 단어 alias 는 단어경계를 요구해
|
|
522
|
+
// 'rest'→'restore', 'cli'→'client' 같은 부분문자열 오탐을 막는다. 구두점/CJK alias 는 경계 개념이 없어 substring 유지.
|
|
523
|
+
function _aliasHit(lowerText, alias) {
|
|
524
|
+
const a = String(alias || '').toLowerCase();
|
|
525
|
+
if (!a) return false;
|
|
526
|
+
if (/^[a-z0-9]+$/.test(a)) return new RegExp('(?:^|[^a-z0-9])' + a + '(?![a-z0-9])').test(lowerText);
|
|
527
|
+
return lowerText.includes(a);
|
|
528
|
+
}
|
|
529
|
+
|
|
521
530
|
// 1.9.333 패턴 적용: 순수 도메인 매칭 — catalog + 텍스트 → 첫 매칭 domain/alias/components (fs 의존 0, catalog 주입).
|
|
522
531
|
function _matchDomain(catalog, text) {
|
|
523
532
|
if (!text || typeof text !== 'string' || !catalog || !catalog.domains) return { domain: null, alias: null };
|
|
524
533
|
const lower = text.toLowerCase();
|
|
525
534
|
for (const [domain, info] of Object.entries(catalog.domains)) {
|
|
526
535
|
for (const a of info.aliases || []) {
|
|
527
|
-
if (lower
|
|
536
|
+
if (_aliasHit(lower, a)) {
|
|
528
537
|
return { domain, alias: a, components: info.components };
|
|
529
538
|
}
|
|
530
539
|
}
|
|
@@ -746,10 +755,29 @@ function _mergeReadmeSection(existing, block, START, END) {
|
|
|
746
755
|
function _managedMerge(file, next, previous, archiveRel, overwriteSet) {
|
|
747
756
|
if (!previous || previous.trim() === next.trim()) return next;
|
|
748
757
|
const tag = '<!-- leerness:migration-preserved -->';
|
|
749
|
-
if (previous.includes(tag)) return next;
|
|
750
758
|
if (overwriteSet && overwriteSet.has(String(file).replace(/\\/g, '/'))) return next;
|
|
759
|
+
// 1.36.28 (사용자 보고 — 반복 마이그레이션 데이터 유실): 종전 <details> 중첩 설계는 previous 에 preserved 태그가
|
|
760
|
+
// 있으면 통째로 next 만 반환 → 2번째 마이그레이션부터 사용자가 CLAUDE/AGENTS 에 직접 쓴 커스텀 지시가 조용히
|
|
761
|
+
// 사라졌다(매 버전 AI 마이그레이션 흐름에서 상시 발화). 재설계: 라인-집합 diff 로 "새 템플릿에 없는 라인"만
|
|
762
|
+
// Preserved 섹션에 평문으로 이월 — 멱등(재실행해도 동일), 중첩 없음, 크기는 커스텀 라인 수에 비례.
|
|
763
|
+
// 편향은 false-PRESERVE(과보존은 무해) 쪽 — false-DROP(유실)이 버그다.
|
|
764
|
+
const nextLines = new Set(next.split('\n').map(l => l.trim()).filter(Boolean));
|
|
765
|
+
const seen = new Set();
|
|
766
|
+
const custom = [];
|
|
767
|
+
for (const raw of previous.split('\n')) {
|
|
768
|
+
const l = raw.trim();
|
|
769
|
+
if (!l || nextLines.has(l)) continue;
|
|
770
|
+
// preserved 래퍼 자체의 보일러플레이트는 이월 대상 아님 (구 <details> 형식 + 신 평문 형식 모두)
|
|
771
|
+
if (l === tag || l === '---' || l === '<details>' || l === '</details>') continue;
|
|
772
|
+
if (/^```/.test(l) || /^<summary>/.test(l)) continue;
|
|
773
|
+
if (/^## Preserved previous content|^Previous content was backed up|^`[^`]*archive[^`]*`$|^> 이전 버전에서 이어진/.test(l)) continue;
|
|
774
|
+
if (seen.has(l)) continue;
|
|
775
|
+
seen.add(l);
|
|
776
|
+
custom.push(raw.replace(/\\`\\`\\`/g, '```')); // 구 형식에서 이스케이프된 펜스 복원
|
|
777
|
+
}
|
|
778
|
+
if (!custom.length) return next;
|
|
751
779
|
const ar = archiveRel || '.harness/archive';
|
|
752
|
-
return next.trimEnd() + `\n\n---\n${tag}\n## Preserved previous content\n\
|
|
780
|
+
return next.trimEnd() + `\n\n---\n${tag}\n## Preserved previous content\n\n> 이전 버전에서 이어진 사용자/프로젝트 커스텀 내용 — 마이그레이션마다 자동 이월됩니다. 전체 원본 백업: \`${ar}\`\n\n` + custom.join('\n') + '\n';
|
|
753
781
|
}
|
|
754
782
|
|
|
755
783
|
// 1.9.369 (UR-0025): --skills 값 파싱 순수 코어 — catalog 주입(harness skillCatalog 비결합). all/recommended/csv 처리 + catalog 필터.
|
|
@@ -1145,7 +1173,7 @@ module.exports = {
|
|
|
1145
1173
|
// 1.9.333 (UR-0025 심층): 순수 플랫폼 제약 매칭
|
|
1146
1174
|
_matchConstraints,
|
|
1147
1175
|
// 1.9.333 패턴 적용: 순수 도메인 매칭
|
|
1148
|
-
_matchDomain,
|
|
1176
|
+
_matchDomain, _aliasHit,
|
|
1149
1177
|
// 1.9.335 (UR-0025 심층): LSP 서브시스템 — 순수 언어감지 + 정규식 심볼 매처
|
|
1150
1178
|
_detectLspLang, _matchLspSymbols,
|
|
1151
1179
|
// anti-laziness optimism-check 순수 로직
|
|
@@ -1370,6 +1398,12 @@ function _featureBlock(node) {
|
|
|
1370
1398
|
// 1.9.391 (UR-0025): feature 영향 BFS — affects + co-changes-with transitive + depends-on 역방향. 순수(nodes,startId→result). harness(handoff/audit)+lib/feature 공유.
|
|
1371
1399
|
function _featureImpactBfs(nodes, startId) {
|
|
1372
1400
|
const byId = new Map(nodes.map(n => [n.id, n]));
|
|
1401
|
+
// 역방향 인접: X 에 depends-on 하는 노드들 (BFS 안에서 전이 확장하려고 사전 구축)
|
|
1402
|
+
const dependents = new Map();
|
|
1403
|
+
for (const n of nodes) for (const dep of n.dependsOn || []) {
|
|
1404
|
+
if (!dependents.has(dep)) dependents.set(dep, []);
|
|
1405
|
+
dependents.get(dep).push(n.id);
|
|
1406
|
+
}
|
|
1373
1407
|
const visited = new Set();
|
|
1374
1408
|
const queue = [{ id: startId, depth: 0, via: 'self' }];
|
|
1375
1409
|
const result = [];
|
|
@@ -1382,14 +1416,9 @@ function _featureImpactBfs(nodes, startId) {
|
|
|
1382
1416
|
if (cur.depth > 0) result.push({ id: cur.id, title: node.title, depth: cur.depth, via: cur.via, files: node.files, errorModes: node.errorModes });
|
|
1383
1417
|
for (const next of node.affects || []) queue.push({ id: next, depth: cur.depth + 1, via: 'affects' });
|
|
1384
1418
|
for (const next of node.coChangesWith || []) queue.push({ id: next, depth: cur.depth + 1, via: 'co-changes-with' });
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
if (visited.has(n.id)) continue;
|
|
1389
|
-
if ((n.dependsOn || []).includes(startId)) {
|
|
1390
|
-
result.push({ id: n.id, title: n.title, depth: 1, via: 'depends-on(reverse)', files: n.files, errorModes: n.errorModes });
|
|
1391
|
-
visited.add(n.id);
|
|
1392
|
-
}
|
|
1419
|
+
// 1.36.28 (codex 미검토표면 헌트 #8): 역방향 depends-on 도 BFS 안에서 확장 — 전이 영향(A←B←C 에서 A 변경이 C 까지)을
|
|
1420
|
+
// 잡는다. 종전엔 BFS 종료 후 1-홉만 추가해 간접 의존이 누락됐다. visited 로 사이클 안전.
|
|
1421
|
+
for (const next of dependents.get(cur.id) || []) queue.push({ id: next, depth: cur.depth + 1, via: 'depends-on(reverse)' });
|
|
1393
1422
|
}
|
|
1394
1423
|
return result;
|
|
1395
1424
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leerness",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.28",
|
|
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",
|