leerness 1.36.27 → 1.36.29

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 CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.36.29 — 2026-07-15 — `agents multi` 위임 브리프 접두 — 백그라운드 AI 도 leerness 프로토콜을 받도록 (사용자 보고)
4
+
5
+ 사용자 질문("백그라운드에서 호출된 AI 가 leerness 를 참조하는가?")에 대한 조사 결과: `agents dispatch` 는 1.35.6 부터 "[leerness 위임 프로토콜]" 브리프(handoff 적재→task 등록→evidence→verify-claim→session close)를 자동 접두하지만, **`agents multi`(--execute 실 spawn 포함)는 원문 그대로 위임** — 병렬 백그라운드 AI 들이 프로토콜을 받지 못하는 실제 갭이었다.
6
+
7
+ - **multi 3지점 브리프 접두**: `--execute` 실 spawn(`_cliChat`) + 명령 목록 출력 + `--json` commands. dispatch 와 동일 규칙(`--raw` 로 원문 위임). 로그/`_recordRun` 의 task 표시는 원문 유지(기록 정직성).
8
+ - 참조 경로 정리(조사 확정): ① dispatch/multi 프롬프트 접두(이번에 완성) ② AGENTS.md 자동 로드(codex 실측 준수 — cwd 가 루트가 아니거나 미지원 CLI 면 무력해서 접두가 안전망) ③ orchestrate 는 자체 JSON 계약 컨텍스트 포함.
9
+ - **검증**: selftest 299/299(브리프 4단계 + dispatch 와이어 + **multi 3지점 와이어**), 실측(multi 출력에 프로토콜 1회 포함 · `--raw` 0회), 게이트 e2e, 게시본 클린룸.
10
+
11
+ ## 1.36.28 — 2026-07-15 — 미검토 표면 헌트: 데이터 유실 클래스(손상 스토어 클로버 · URL 덮어쓰기) + 정확성(impact 전이 · alias 경계)
12
+
13
+ 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 위험이라 정직하게 이연.
14
+
15
+ - **#3 P1 (데이터 유실 클래스) — 손상 JSON 스토어를 빈 값으로 오인해 덮어쓰기**: state.json/`teams.json`/`platform-constraints.json` 이 손상되면 fail-open 으로 빈 기본값이 되어 기존 데이터(진행 중 run·팀·커스텀 제약)를 소실시켰다. `_assertStoreParsable`(파일 "없음"과 "있으나 손상" 구분) + `_guardStore` 로 **변경(저장)을 거부하고 원본 보존** + `store_corrupt` 구조화 에러. 설계: SAVE 진입점에서만 던짐(handoff 등 비변경 읽기는 resilient 유지) — 단 state 는 카운터 리셋이 run 파일 클로버를 유발해 load 에서 던지고 stateCmd 를 감쌈.
16
+ - **#4 P2 (데이터 유실) — api-skill 이 쿼리 다른 URL 을 같은 id 로 덮어씀**: `?doc=alpha` 와 `?doc=beta` 가 둘 다 `example.md` 로 슬러그돼 앞 URL·방향이 소실. 같은 id 파일의 저장 URL 이 다르면 정규화 URL 해시(`_shortHash`)를 붙여 분리, 같은 URL 재등록은 멱등 갱신 유지.
17
+ - **#8 P2 (정확성) — feature impact 역의존 전이 누락**: `A←B←C` 에서 `impact A` 가 B 만 반환(C 누락). 역방향 depends-on 을 BFS 종료 후 1-홉이 아니라 BFS **안**에서 확장(visited 로 사이클 안전) → C 가 depth 2 로 포착.
18
+ - **#10 P2 (정확성) — intent alias 부분문자열 오탐**: `restore`→'rest'(API), `client`→'cli'(CLI) 오분류. ASCII 단어 alias 는 단어경계를 요구(`_aliasHit`), 구두점/CJK alias 는 substring 유지.
19
+ - **검증**: selftest 298/298(전이+사이클안전+alias 경계+가드 배선+해시), 4건 전부 원 재현본으로 before/after + 정상 무회귀 실측, 게이트 e2e, 게시본 클린룸.
20
+ - 이연: #7(pre-wake-audit 테이블형 in-progress 미인식 — session-resume 매처 재사용), 동시성 #1/#2/#5(state/team 병렬 lost-update — 크로스프로세스 락 필요).
21
+
22
+ **반복 마이그레이션 커스텀 유실 (사용자 보고, P1, 동일 릴리스 합류)**: "AI 에게 매번 최신 마이그레이션을 시키는" 표준 흐름에서 **2번째 마이그레이션부터 CLAUDE.md/AGENTS.md 의 사용자 커스텀 지시가 조용히 사라졌다** — `_managedMerge` 가 preserved 태그를 보면 병합 없이 새 템플릿만 반환했기 때문(구 `<details>` 중첩-방지 설계의 부작용). **실증**: 1.26.0/1.9.206 실제 구버전으로 워크스페이스 구축(23종 마커) → migrate: 1차는 유실 0 이지만, 커스텀 추가 후 2차에서 신·구 커스텀 모두 유실(archive 백업에만 잔존).
23
+ - **재설계**: 라인-집합 diff — "새 템플릿에 없는 라인"만 Preserved 섹션에 평문 이월. 멱등(연속 5회 마이그레이션에서 커스텀 3종 전부 보존, 크기 고정), 중첩 없음, 레거시 `<details>` 형식에서도 커스텀 회수(래퍼 미이월), overwriteSet(통째 교체 파일) 존중. 편향은 명시적으로 false-PRESERVE(과보존 무해) — false-DROP(유실)이 버그.
24
+ - 마이그레이션 유실 전수 검증 결과(이번 실측): `.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 였고 이번에 봉합.
25
+
3
26
  ## 1.36.27 — 2026-07-15 — debug 렌즈 — "근본원인 조사 없이 수정 금지"를 자기질문으로 (obra/superpowers systematic-debugging)
4
27
 
5
28
  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.27 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
124
+ 이 프로젝트는 Leerness v1.36.29 하네스를 사용합니다. 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.27는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
178
+ Leerness v1.36.29는 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.27)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
199
+ 현재 누적: **70 라운드 (1.9.40 → 1.36.29)** · 매 라운드 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.27: 2026-07-15
237
+ Last synced by Leerness v1.36.29: 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.27';
37
+ const VERSION = '1.36.29';
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') 시 호스트 프로세스 오염.
@@ -2952,7 +2952,11 @@ function _selfTestCases() {
2952
2952
  const wired = /has\('--raw'\)[\s\S]{0,120}_harnessBrief\(\) \+ task/.test(agentsSrc) && /_harnessBrief,/.test(read(__filename));
2953
2953
  // (3) bench stdin hang 수정: codex exec 가 열린 stdin 파이프에서 EOF 대기(실측) → spawn stdio ignore.
2954
2954
  const stdinFixed = agentsSrc.includes("{ shell: true, stdio: ['ignore', 'pipe', 'pipe'] }");
2955
- return briefOk && wired && stdinFixed;
2955
+ // (4) 1.36.29 (사용자 보고): multi 도 브리프 접두 — --execute 실 spawn(_cliChat) + 명령 목록/JSON 3지점 전부 briefTask.
2956
+ const multiWired = agentsSrc.includes('_cliChat(root, def.id, briefTask,')
2957
+ && agentsSrc.includes('_dispatchCommand(x.def.id, briefTask, writeMode)')
2958
+ && agentsSrc.includes('_dispatchCommand(def.id, briefTask, writeMode)');
2959
+ return briefOk && wired && stdinFixed && multiWired;
2956
2960
  } },
2957
2961
  { name: '19th GPT5.5평가 (UR-0013): _parseTestStdout 러너 인식(jest/tap/spec/비율) + declared-pass 부풀림 3경로 게이팅 + json ok/reasons (1.35.7)', run: () => {
2958
2962
  // (1) 파서 행위: 기존 4형식 보존 + spec 리포터(패턴 6) 신규 — denom = pass + fail.
@@ -3027,6 +3031,47 @@ function _selfTestCases() {
3027
3031
  const encOk = s.includes("(result.applied || []).some(a => a.action === 'failed')) process.exitCode = 1"); // encoding: 실패 exit1
3028
3032
  return reuseOk && releaseOk && encOk;
3029
3033
  } },
3034
+ { name: '미검토표면 헌트 (1.36.28, codex): #8 impact 역의존 전이 + #10 alias 단어경계 + #3 손상스토어 클로버 차단 + #4 URL 해시 — 순수/소스 행위검사', run: () => {
3035
+ const p = require('../lib/pure-utils');
3036
+ // #8: A←B←C 전이 (F-0003 이 depth 2 로 잡혀야)
3037
+ const nodes = [{ id: 'F1', title: 'A', dependsOn: [] }, { id: 'F2', title: 'B', dependsOn: ['F1'] }, { id: 'F3', title: 'C', dependsOn: ['F2'] }];
3038
+ const imp = p._featureImpactBfs(nodes, 'F1').map(x => x.id);
3039
+ const transitiveOk = imp.includes('F2') && imp.includes('F3');
3040
+ // 사이클 안전: F1↔F2 순환이어도 무한루프/중복 없이 종료
3041
+ const cyc = p._featureImpactBfs([{ id: 'F1', dependsOn: ['F2'] }, { id: 'F2', dependsOn: ['F1'] }], 'F1');
3042
+ const cycleSafe = Array.isArray(cyc) && cyc.length <= 2;
3043
+ // #10: 부분문자열 오탐 차단 + 실매칭 유지
3044
+ const aliasOk = typeof p._aliasHit === 'function'
3045
+ && !p._aliasHit('restore deleted files', 'rest') && !p._aliasHit('the client cache', 'cli')
3046
+ && p._aliasHit('call the rest endpoint', 'rest') && p._aliasHit('run cli now', 'cli')
3047
+ && p._aliasHit('노드제이에스 node.js 설정', 'node.js'); // 구두점 alias 는 substring 유지
3048
+ // #3/#4: 소스 배선 확인 (행위는 e2e/실측, 여기선 가드 존재)
3049
+ const s = read(__filename);
3050
+ const guardOk = s.includes('function _assertStoreParsable') && s.includes("code: 'E_STORE_CORRUPT'")
3051
+ && s.includes('_guardStore(has(') && s.includes("_assertStoreParsable(teamsJsonPath")
3052
+ && s.includes("_assertStoreParsable(_platformConstraintsPath");
3053
+ const hashOk = s.includes('function _shortHash') && s.includes("id = id + '-' + _shortHash(url)") && s.includes('urls:\\s*\\r?\\n');
3054
+ return transitiveOk && cycleSafe && aliasOk && guardOk && hashOk;
3055
+ } },
3056
+ { name: '반복 마이그레이션 커스텀 보존 (1.36.28, 사용자 보고): _managedMerge 라인-diff 이월 — 2번째부터 유실되던 CLAUDE/AGENTS 커스텀 지시 (행위검사)', run: () => {
3057
+ const p = require('../lib/pure-utils');
3058
+ const next = '# T\nline A\nline B\n';
3059
+ const prev = '# T\nline A\nline B\nMY CUSTOM 1\n';
3060
+ const m1 = p._managedMerge('CLAUDE.md', next, prev, '.harness/archive', null);
3061
+ const m2 = p._managedMerge('CLAUDE.md', next, m1, '.harness/archive', null);
3062
+ const m3 = p._managedMerge('CLAUDE.md', next, m2, '.harness/archive', null);
3063
+ // 핵심 판별: 종전 구현은 m2 에서 MY CUSTOM 1 이 사라졌다 (previous 에 preserved 태그 있으면 next 만 반환)
3064
+ const survived = m1.includes('MY CUSTOM 1') && m2.includes('MY CUSTOM 1') && m3.includes('MY CUSTOM 1');
3065
+ const idem = m2 === m3;
3066
+ // 레거시 <details> 형식에서 커스텀 회수 + 래퍼 미이월
3067
+ const F = String.fromCharCode(96, 96, 96);
3068
+ 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';
3069
+ const lm = p._managedMerge('CLAUDE.md', next, legacy, '.harness/archive', null);
3070
+ const legacyOk = lm.includes('OLD CUSTOM X') && !lm.includes('<details>');
3071
+ const cleanOk = p._managedMerge('CLAUDE.md', next, next, '.harness/archive', null) === next;
3072
+ const owOk = p._managedMerge('x.md', next, prev, '.', new Set(['x.md'])) === next;
3073
+ return survived && idem && legacyOk && cleanOk && owOk;
3074
+ } },
3030
3075
  { name: 'debug 렌즈 (1.36.27, obra/superpowers systematic-debugging): 자기질문 6문항 ko/en 락스텝 + affects 유효 + route bugfix 힌트 + 파일매핑 미확장 — 행위검사', run: () => {
3031
3076
  const d = LENS_CATALOG.debug;
3032
3077
  if (!d) return false;
@@ -4702,6 +4747,28 @@ function shellGuardCmd(root, cmd, opts = {}) {
4702
4747
  if (analysis.issues.some(i => i.severity === 'error')) process.exitCode = 1;
4703
4748
  }
4704
4749
 
4750
+ // 1.36.28 (codex 미검토표면 헌트 #3 — 데이터 유실 클래스): 손상된 JSON 스토어를 "빈 값"으로 오인해 덮어쓰는 fail-open 방지.
4751
+ // 파일이 "없음"과 "있으나 손상"을 구분 — 후자는 파싱 실패를 던져 변경(저장)을 거부하고 원본을 보존한다.
4752
+ // 설계: LOAD 가 아니라 SAVE/변경 진입점에서만 던진다(handoff 등 비변경 읽기 경로는 resilient 유지). state 는
4753
+ // 카운터 리셋이 run 파일 클로버를 유발하므로 예외적으로 load 에서 던지고 stateCmd 를 _guardStore 로 감싼다.
4754
+ function _assertStoreParsable(file, label) {
4755
+ if (exists(file)) {
4756
+ try { JSON.parse(read(file)); }
4757
+ catch { throw Object.assign(new Error(`${label} 저장 파일이 손상돼(JSON 파싱 실패) 덮어쓰기를 거부합니다: ${file} — 파일을 복구하거나 삭제 후 재시도하세요`), { code: 'E_STORE_CORRUPT', file }); }
4758
+ }
4759
+ }
4760
+ function _guardStore(jsonMode, fn) {
4761
+ try { return fn(); }
4762
+ catch (e) { if (e && e.code === 'E_STORE_CORRUPT') { failJson(jsonMode, 'store_corrupt', e.message); return undefined; } throw e; }
4763
+ }
4764
+ // 1.36.28 (#4): 0-deps 짧은 결정적 해시 (URL 충돌 구분용, 보안 아님).
4765
+ function _shortHash(s) {
4766
+ let h = 5381;
4767
+ const str = String(s || '');
4768
+ for (let i = 0; i < str.length; i++) h = ((h << 5) + h + str.charCodeAt(i)) >>> 0;
4769
+ return h.toString(36).slice(0, 6);
4770
+ }
4771
+
4705
4772
  // 1.9.245: API skill cache — 공식 문서 + 관련 링크 자동 정리 (사용자 명시 UR-0015)
4706
4773
  // .harness/api-skills/<id>.md 에 frontmatter + 본문 저장. handoff 자동 매칭.
4707
4774
  // Node built-in https (의존성 0), depth=1 same-domain crawl, max 10 links.
@@ -4950,7 +5017,16 @@ async function apiSkillCmd(root, sub) {
4950
5017
  try {
4951
5018
  const doc = await _collectAPIDoc(url, { direction, noCrawl });
4952
5019
  const skillName = name || doc.title || new URL(url).hostname;
4953
- const id = _apiSkillId(url, name);
5020
+ let id = _apiSkillId(url, name);
5021
+ // 1.36.28 (#4): 쿼리/경로가 달라 URL 은 다른데 slug 가 같으면(예: example.com/?doc=alpha vs ?doc=beta) 기존 파일을
5022
+ // 덮어써 이전 URL·방향이 유실됐다. 같은 id 파일이 이미 있고 저장된 URL 이 다르면 정규화 URL 해시를 붙여 분리한다.
5023
+ const _idFile = (i) => path.join(dir, i + '.md');
5024
+ if (fs.existsSync(_idFile(id))) {
5025
+ const existing = fs.readFileSync(_idFile(id), 'utf8');
5026
+ const um = existing.match(/^urls:\s*\r?\n\s*-\s*(\S.*)$/m); // 'urls:' 블록의 첫 항목만 (--- 구분선 오매치 방지)
5027
+ const storedUrl = um ? um[1].trim() : '';
5028
+ if (storedUrl && storedUrl !== url) id = id + '-' + _shortHash(url);
5029
+ }
4954
5030
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
4955
5031
  const md = _serializeAPISkill(id, skillName, [url], direction, doc);
4956
5032
  fs.writeFileSync(path.join(dir, id + '.md'), md, 'utf8');
@@ -5551,6 +5627,7 @@ function _loadPlatformConstraints(root) {
5551
5627
  } catch { return _DEFAULT_PLATFORM_CONSTRAINTS; }
5552
5628
  }
5553
5629
  function _writePlatformConstraints(root, catalog) {
5630
+ _assertStoreParsable(_platformConstraintsPath(root), 'platform-constraints'); // 1.36.28 (#3): 손상 파일 덮어써 커스텀 제약 유실 방지 (try 밖 — 삼켜지지 않게)
5554
5631
  try {
5555
5632
  mkdirp(path.join(root, '.harness'));
5556
5633
  writeUtf8(_platformConstraintsPath(root), JSON.stringify({ ...catalog, updatedAt: new Date().toISOString() }, null, 2));
@@ -7790,6 +7867,7 @@ function _loadTeams(root) {
7790
7867
  }
7791
7868
  function _saveTeams(root, teams) {
7792
7869
  const arr = Array.isArray(teams) ? teams : [];
7870
+ _assertStoreParsable(teamsJsonPath(root), 'team'); // 1.36.28 (#3): 손상 teams.json 을 덮어써 기존 팀 유실 방지
7793
7871
  mkdirp(path.dirname(teamsJsonPath(root)));
7794
7872
  writeUtf8(teamsJsonPath(root), JSON.stringify(arr, null, 2) + '\n');
7795
7873
  writeUtf8(teamsPath(root), _renderTeamsMd(arr));
@@ -18632,7 +18710,8 @@ function _leernessStateDir(root) { return path.join(absRoot(root), '.leerness');
18632
18710
  function _loadLeernessState(root) {
18633
18711
  const f = path.join(_leernessStateDir(root), 'state.json');
18634
18712
  if (!exists(f)) return { schemaVersion: 1, project: detectProjectName(root), currentRunId: null, runCounter: 0, updatedAt: null };
18635
- try { return JSON.parse(read(f)); } catch { return { schemaVersion: 1, project: detectProjectName(root), currentRunId: null, runCounter: 0, updatedAt: null }; }
18713
+ // 1.36.28 (#3): 손상 파일을 상태로 오인하면 카운터가 0으로 리셋돼 run-0001 덮어쓴다(데이터 유실). 파싱 실패는 던진다.
18714
+ try { return JSON.parse(read(f)); } catch { throw Object.assign(new Error(`.leerness/state.json 손상 — 덮어쓰기 거부: ${f} (복구/삭제 후 재시도)`), { code: 'E_STORE_CORRUPT', file: f }); }
18636
18715
  }
18637
18716
  function _saveLeernessState(root, state) {
18638
18717
  const dir = _leernessStateDir(root); mkdirp(dir);
@@ -21504,9 +21583,9 @@ async function main() {
21504
21583
  // 1.9.272: leerness capabilities — 권한/보안 표면 공개 (GPT-5.5 외부 리뷰 반영)
21505
21584
  if (cmd === 'capabilities' || cmd === 'security-surface') return capabilitiesCmd(arg('--path', process.cwd()), { json: has('--json') });
21506
21585
  // 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));
21586
+ if (cmd === 'state') return _guardStore(has('--json'), () => stateCmd(arg('--path', process.cwd()), args[1], ...args.slice(2))); // 1.36.28 (#3): 손상 스토어 클로버 차단
21508
21587
  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 지원(preview/deploy args[2]=id 분리)
21588
+ 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
21589
  if (cmd === 'brief') return briefCmd(arg('--path', process.cwd()), args[1]);
21511
21590
  if (cmd === 'about' || cmd === 'identity') return aboutCmd({ json: has('--json') });
21512
21591
  // 1.9.281 (UR-0034): leerness policy <show|set|check> — 권한 등급 (opt-in enforced)
@@ -21535,7 +21614,7 @@ async function main() {
21535
21614
  // 1.9.245: API skill cache — 공식 문서·관련링크 자동 정리 (사용자 명시 UR-0015)
21536
21615
  if (cmd === 'api-skill') return apiSkillCmd(arg('--path', process.cwd()), args[1] || 'help');
21537
21616
  // 1.9.208: leerness constraints <list|check|add> — 플랫폼/API 제약 사전 체크 (사용자 명시)
21538
- if (cmd === 'constraints') return constraintsCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
21617
+ if (cmd === 'constraints') return _guardStore(has('--json'), () => constraintsCmd(arg('--path', process.cwd()), args[1], ...args.slice(2))); // 1.36.28 (#3): 손상 스토어 클로버 차단
21539
21618
  // 1.9.209: leerness pre-wake-audit — sleep 전 sub-agent audit (사용자 명시)
21540
21619
  if (cmd === 'pre-wake-audit') return preWakeAuditCmd(arg('--path', process.cwd()), args[1]);
21541
21620
  // 1.9.210: leerness wakeup-interval <get|set|auto|history|record> — adaptive interval (사용자 명시)
package/lib/agents.js CHANGED
@@ -74,6 +74,9 @@ function agentsCmd(root, sub, args = [], deps = {}) {
74
74
  if (sub === 'multi') {
75
75
  const task = _taskArg();
76
76
  if (!task) { fail('multi "<task>" 또는 --task 필요'); return process.exit(1); }
77
+ // 1.36.29 (사용자 보고): dispatch 만 위임 브리프를 접두하고 multi(--execute 실 spawn 포함)는 원문 그대로 위임돼
78
+ // 백그라운드 AI 가 leerness 프로토콜(handoff 적재/task 등록/evidence/session close)을 받지 못했다. dispatch 와 동일 규칙: --raw 로 옵트아웃.
79
+ const briefTask = (has('--raw') || typeof _harnessBrief !== 'function') ? task : (_harnessBrief() + task);
77
80
  const onlyArg = arg('--only', null); // 'claude,codex' 처럼 콤마 구분 — 활성 중에서 추가 필터
78
81
  const writeMode = has('--write');
79
82
  const execute = has('--execute'); // 1.9.156: 명령 출력 → 실제 spawn + consensus 합의
@@ -109,7 +112,7 @@ function agentsCmd(root, sub, args = [], deps = {}) {
109
112
  // 병렬 _cliChat 호출 (sandbox 자동: runCommandSafe + env scrub + observability)
110
113
  const results = await Promise.all(ready.map(async ({ def }) => {
111
114
  const start = Date.now();
112
- const r = await _cliChat(root, def.id, task, { timeout });
115
+ const r = await _cliChat(root, def.id, briefTask, { timeout });
113
116
  return {
114
117
  agent: def.id,
115
118
  elapsed: Date.now() - start,
@@ -203,7 +206,7 @@ function agentsCmd(root, sub, args = [], deps = {}) {
203
206
  log(JSON.stringify({
204
207
  task, count: ready.length,
205
208
  agents: ready.map(x => ({ id: x.def.id, version: x.status.version })),
206
- commands: ready.map(x => _dispatchCommand(x.def.id, task, writeMode)),
209
+ commands: ready.map(x => _dispatchCommand(x.def.id, briefTask, writeMode)),
207
210
  // 1.9.266 (UR-0021 2단계): 각 에이전트 슬래시 명령 힌트 — sub-agent 가 알맞은 슬래시 사용
208
211
  slashCommands: ready.reduce((acc, x) => { const h = _agentSlashHint(root, x.def.id); if (h) acc[x.def.id] = { invoke: h.invoke, commands: h.commands.map(c => c.cmd) }; return acc; }, {})
209
212
  }, null, 2));
@@ -219,7 +222,7 @@ function agentsCmd(root, sub, args = [], deps = {}) {
219
222
  for (const { def, status } of ready) {
220
223
  log(`### [${def.id}] (v${status.version || '?'})`);
221
224
  log('```sh');
222
- log(_dispatchCommand(def.id, task, writeMode));
225
+ log(_dispatchCommand(def.id, briefTask, writeMode));
223
226
  log('```');
224
227
  // 1.9.266 (UR-0021 2단계): 에이전트별 슬래시 명령 힌트
225
228
  try {
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.includes(a.toLowerCase())) {
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\nPrevious content was backed up before migration. Archive reference:\n\n\`${ar}\`\n\n<details>\n<summary>Previous ${file}</summary>\n\n\`\`\`md\n${previous.replace(/```/g, '\\`\\`\\`')}\n\`\`\`\n\n</details>\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
- // 역방향: feature에 depends-on 하는 노드도 영향받음
1387
- for (const n of nodes) {
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/lib/toggles.js ADDED
@@ -0,0 +1,70 @@
1
+ // lib/toggles.js — 기능 토글 (1.36.30, 사용자 요청): 온톨로지 그래프 뷰의 스위치와 연동되는 기능 on/off 스토어.
2
+ // .harness/toggles.json 에 저장. AI/CLI 는 여기 상태를 읽어 준수한다(gate/lens 스킵, 위임 브리프 생략, 자동 그래프).
3
+ // 설계: 기본 전부 ON(파일 없으면 결손 아님). 손상 파일은 SAVE 시 덮어쓰기 거부(1.36.28 _assertStoreParsable 패턴).
4
+ 'use strict';
5
+ const path = require('path');
6
+ const { absRoot, exists, read, writeUtf8, mkdirp, log, ok, fail } = require('./io');
7
+
8
+ // 토글 레지스트리 — id: { ko 설명, 영향 명령 }. 새 토글은 여기에 추가.
9
+ const TOGGLE_REGISTRY = {
10
+ 'gate': { desc: '완료 게이트 (verify+audit+scan+encoding+lazy 통합 점검)', affects: 'leerness gate' },
11
+ 'lens': { desc: '품질 렌즈 자기질문 (완료 선언 전 분야별 점검)', affects: 'leerness lens' },
12
+ 'auto-graph': { desc: '온톨로지 그래프(leerness.html) 자동 갱신 (install/session-close)', affects: 'install · session close' },
13
+ 'delegation-brief': { desc: '백그라운드 AI 위임 시 leerness 프로토콜 브리프 자동 접두', affects: 'agents dispatch · agents multi' },
14
+ };
15
+
16
+ function _togglesPath(root) { return path.join(absRoot(root), '.harness', 'toggles.json'); }
17
+
18
+ // 전체 토글 상태 로드 — 파일 없으면 전부 기본 ON. 손상 파일은 읽기에선 기본값(비변경 경로 resilient).
19
+ function loadToggles(root) {
20
+ const out = {};
21
+ for (const id of Object.keys(TOGGLE_REGISTRY)) out[id] = true;
22
+ const f = _togglesPath(root);
23
+ if (!exists(f)) return out;
24
+ try {
25
+ const j = JSON.parse(read(f));
26
+ for (const [k, v] of Object.entries(j || {})) if (k in out) out[k] = v !== false;
27
+ } catch {}
28
+ return out;
29
+ }
30
+
31
+ // 단일 토글 조회 헬퍼 — 호출부 한 줄용.
32
+ function toggleOn(root, id) { return loadToggles(root)[id] !== false; }
33
+
34
+ function saveToggles(root, toggles) {
35
+ const f = _togglesPath(root);
36
+ // 1.36.28 패턴: 손상 파일 덮어쓰기 거부 — 단 토글은 전량 기본값 복원 가능하니 손상 시 재생성 허용이 사용자 친화적.
37
+ // (여기의 데이터는 bool 4개뿐 — 유실 비용이 0 에 가깝고, 거부하면 토글 자체가 잠긴다. 의도적 예외.)
38
+ mkdirp(path.dirname(f));
39
+ writeUtf8(f, JSON.stringify(toggles, null, 2) + '\n');
40
+ }
41
+
42
+ // `leerness toggle [list|set <id> on|off] [--json]`
43
+ function toggleCmd(root, sub, id, val, deps = {}) {
44
+ const { has, VERSION } = deps;
45
+ root = absRoot(root);
46
+ const json = has && has('--json');
47
+ const cur = loadToggles(root);
48
+ if (!sub || sub === 'list') {
49
+ if (json) { log(JSON.stringify({ version: VERSION, toggles: cur, registry: TOGGLE_REGISTRY }, null, 2)); return; }
50
+ log(`# leerness toggle — 기능 토글 (온톨로지 그래프 뷰 ⚙ 탭과 연동)`);
51
+ for (const [k, meta] of Object.entries(TOGGLE_REGISTRY)) {
52
+ log(` ${cur[k] ? '🟢 ON ' : '⚪ OFF'} ${k.padEnd(17)} ${meta.desc} [${meta.affects}]`);
53
+ }
54
+ log(`\n 변경: leerness toggle set <id> on|off · 그래프 뷰: leerness graph --html → leerness.html 의 ⚙ 탭`);
55
+ return;
56
+ }
57
+ if (sub === 'set') {
58
+ if (!TOGGLE_REGISTRY[id]) { fail(`알 수 없는 토글: ${id} (가능: ${Object.keys(TOGGLE_REGISTRY).join(', ')})`); process.exitCode = 1; return; }
59
+ const on = String(val).toLowerCase();
60
+ if (on !== 'on' && on !== 'off') { fail(`값은 on|off (받음: ${val})`); process.exitCode = 1; return; }
61
+ cur[id] = on === 'on';
62
+ saveToggles(root, cur);
63
+ if (json) { log(JSON.stringify({ ok: true, id, value: cur[id], toggles: cur }, null, 2)); return; }
64
+ ok(`toggle ${id} = ${on.toUpperCase()}${cur[id] ? '' : ' — 관련 명령이 스킵 동작으로 전환됩니다'}`);
65
+ return;
66
+ }
67
+ fail(`알 수 없는 하위명령: ${sub} (가능: list, set)`); process.exitCode = 1;
68
+ }
69
+
70
+ module.exports = { TOGGLE_REGISTRY, loadToggles, toggleOn, saveToggles, toggleCmd, _togglesPath };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.36.27",
3
+ "version": "1.36.29",
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",