leerness 1.36.35 → 1.36.37

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,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.36.37 — 2026-07-16 — managed 파일 --force 병합 — 마이그레이션 보존의 마지막 구멍 (1.9.441 드리프트 경로 실측)
4
+
5
+ 도그푸딩에서 발견된 실제 드리프트 세대(1.9.441 — auto-macro 가 쓰는 버전)로 마이그레이션 경로를 전수 재검(11종 마커, 기본→반복→--force): 기본/반복은 유실 0 이었으나 **--force 반복에서 CLAUDE.md 사용자 커스텀 1종 유실** — `writeIfSafe` 에서 force 가 managed 병합(1.36.28 라인-diff 재설계)을 우회해 raw 덮어쓰기했기 때문(1.36.33 `_USER_STATE` 는 .harness 상태 파일만 커버).
6
+
7
+ - **수정**: managed 파일(CLAUDE/AGENTS/cursor/copilot 지침)은 `--force` 여도 **항상 라인-diff 병합** — 병합이 곧 "템플릿 최신화 + 커스텀 보존"이라 raw 교체가 필요한 경우가 없다. 비-managed 파일의 force/preserve 동작은 불변.
8
+ - **실측**: 1.9.441→1.36.37 기본 migrate 유실 0/11 · 반복+2×--force 후 CLAUDE 커스텀+task 모두 생존 · 새 CLI 로 task/rule/requests 정상 판독.
9
+ - 이로써 마이그레이션 보존 3부작 완결: 반복 병합(1.36.28) → --force 상태 파일(1.36.33) → **--force managed 파일(1.36.37)**.
10
+ - **검증**: selftest 308/308(force+managed 병합 / 비-managed force 교체 / 보존 3동작 + rules 복구·보존), exit 전파형 게이트 e2e, 게시본 클린룸.
11
+
12
+ **rules 손상 행 무언 삭제 방지 (codex 4차 헌트 #1 High, 동일 릴리스 합류)**: 수동 편집/중단된 쓰기로 셀이 부족해진 룰 행을 `readRules` 가 버리고 다음 `writeRules`(rule verify 등)가 **그 룰을 조용히 삭제**했다(원 재현: 4셀 절단 → verify 후 소실). → 3~5셀 행은 누락 셀 기본값으로 **복구**(룰 기능 유지), 3셀 미만 파싱 불가 행은 **원문 보존**(경고 표식과 함께 — 수동 복구 대상). 실측: 절단 행 복구·기능 정상 + 파싱 불가 행 잔존. codex 4차 잔여 9건(retro --days 미적용 · v1.9.* 태그 하드코드 · MCP 파라미터 검증 · 이중 소스 등)은 다음 라운드.
13
+
14
+ ## 1.36.36 — 2026-07-15 — `leerness anchors` — 정체성앵커 초안 합성 (도그푸딩 재실측 후속)
15
+
16
+ **배경(실측)**: 1.36.19 에서 앵커 미작성 감지를 출하했지만, 실프로젝트 7곳 재순회 결과 **brief 미작성 4/7 · plan Goal 미작성 6/7** — 감지·노출만으론 전환이 안 된다(반례: AEGIRINE 0/0 은 전환 가능함의 증거). 마찰 지점은 "빈 문서를 처음부터 써야 함" — 실신호 초안으로 제거.
17
+
18
+ - **`leerness anchors [status|draft [--apply]] [path]`**: package.json description · README 첫 산문 · milestones · 활성 task 만으로 Purpose/Goal 초안 합성(순수 `_draftAnchors`). **발명 금지** — 신호가 없으면 `no_signal` 로 거부(빈 프로젝트에 그럴듯한 거짓 목적을 써주지 않음).
19
+ - **안전**: `--apply` 는 해당 섹션이 **미작성일 때만** 교체(`_replaceMdSection` — 그 섹션만, 나머지 불변). 작성된 앵커는 절대 불클로버(재실행 실측). 초안 표식 주석 명시 — AI/사용자 검토 후 주석 제거로 확정.
20
+ - **handoff 힌트 배선**: 앵커 미작성 헤드라인에 `→ leerness anchors draft` 액션 부착 — 다음 세션 AI 가 한 명령으로 전환.
21
+ - **검증**: selftest 306/306(합성/무신호 거부/섹션 교체/배선), E2E 사슬 실측(status→draft→apply→감지 해소→불클로버), exit 전파형 게이트, 클린룸. (부수: audit 마커와의 finding-code 충돌을 selftest 가 잡아 코드명 분리 — 자기참조 트랩 주석 변형 포함 5번째.)
22
+
3
23
  ## 1.36.35 — 2026-07-15 — deps --run-tests 정직화 + #7 의도적 DEFER (codex 3차 잔여 소진)
4
24
 
5
25
  - **#6 `deps <cap> --run-tests`**: (a) `--json` 이 테스트 스윕 **전에** return → 스윕이 무언 무시되고 exit 0(false-green) — json/human 이 같은 `_runImpactSweep` 를 쓰도록 공용화, json 은 `tests[]` 필드 + 실패 시 exit 1. (b) 프로젝트를 basename 으로 역추적해 **동명 프로젝트(a/service, b/service) 중 하나만 테스트**되고 실패쪽이 조용히 생략 — 절대경로 키로 교정. 실측: 동명 2 프로젝트 모두 실행(PASS/FAIL 각각), json/human 동등, 실패 exit 1.
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.35 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
124
+ 이 프로젝트는 Leerness v1.36.37 하네스를 사용합니다. 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.35는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
178
+ Leerness v1.36.37는 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.35는 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.35)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
199
+ 현재 누적: **70 라운드 (1.9.40 → 1.36.37)** · 매 라운드 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.35: 2026-07-15
237
+ Last synced by Leerness v1.36.37: 2026-07-15
238
238
  <!-- leerness:project-readme:end -->
239
239
 
package/bin/leerness.js CHANGED
@@ -26,7 +26,7 @@ const { _isSecretKey, _isPlaceholderSecret, _looksSecretLike, _mergeLines, _merg
26
26
  _migrationGuideText, _parseContractSpec, _gitignoreMatch,
27
27
  _featureGraphTemplate, _parseFeatureGraph, _nextFeatureId, _featureBlock, _featureImpactBfs,
28
28
  _parseChangelogBetween, _cellSafe, _cellUnescape, _lineSafe, _parseLimit, _parseAddTitle, _parseImplExports, _taskPositionalPath, _completionClaimAllowed, _minorKey, _shouldPublishNpm,
29
- _matchTool, _parsePackageJsonDeps, _parseRequirementsTxt, _buildGlossary, _renderGlossaryMd, _briefUnfilled, _planGoalUnfilled } = require('../lib/pure-utils'); // 1.9.318~1.11.4 (UR-0025/.../0007 glossary): 순수 유틸 모듈 분리
29
+ _matchTool, _parsePackageJsonDeps, _parseRequirementsTxt, _buildGlossary, _renderGlossaryMd, _briefUnfilled, _planGoalUnfilled, _draftAnchors, _replaceMdSection } = require('../lib/pure-utils'); // 1.9.318~1.11.4 (UR-0025/.../0007 glossary): 순수 유틸 모듈 분리 · 1.36.36 anchors
30
30
  // 1.9.304 (UR-0025): 순수 분석/검증 함수 모듈 분리.
31
31
  const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInGit, _epistemicHonestyCheck } = require('../lib/analyzers');
32
32
  // 1.9.295 (UR-0025 4단계): 정적 데이터 카탈로그 모듈 분리 (비파괴, require-based).
@@ -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.35';
37
+ const VERSION = '1.36.37';
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') 시 호스트 프로세스 오염.
@@ -719,12 +719,15 @@ function managedMerge(file, next, previous, archiveDir) {
719
719
  function writeIfSafe(root, file, content, opts = {}) {
720
720
  const p = path.join(root, file);
721
721
  const already = exists(p);
722
- if (already && !opts.force && !opts.mergeManaged) return { action: 'preserved', file };
723
- if (already && opts.mergeManaged && !opts.force) {
722
+ // 1.36.37 (1.9.441 드리프트 경로 실측): managed 파일(CLAUDE/AGENTS 등) --force 여도 항상 라인-diff 병합
723
+ // 병합이 "템플릿 최신화 + 사용자 커스텀 보존"이라 raw 덮어쓰기가 필요한 경우가 없고, 종전엔 force
724
+ // 병합을 우회해 CLAUDE.md 커스텀 지시가 archive 에만 남았다(1.36.28 보존 재설계의 force 구멍).
725
+ if (already && opts.mergeManaged) {
724
726
  const prev = read(p);
725
727
  writeUtf8(p, managedMerge(file, content, prev, opts.archiveDir));
726
728
  return { action: 'merged', file };
727
729
  }
730
+ if (already && !opts.force) return { action: 'preserved', file };
728
731
  writeUtf8(p, content);
729
732
  return { action: already ? 'updated' : 'created', file };
730
733
  }
@@ -3178,6 +3181,60 @@ function _selfTestCases() {
3178
3181
  const oldGone = !s.includes('const projPath = allPaths.find(' + 'p => path.basename(p) === projName);');
3179
3182
  return sweepShared && absKey && jsonExit && humanShared && oldGone;
3180
3183
  } },
3184
+ { name: 'anchors draft (1.36.36, 도그푸딩 후속): 실신호 초안 합성 + 미작성만 교체 + 불클로버 — 순수 행위검사', run: () => {
3185
+ const p = require('../lib/pure-utils');
3186
+ if (typeof p._draftAnchors !== 'function' || typeof p._replaceMdSection !== 'function') return false;
3187
+ const d = p._draftAnchors({ pkgDescription: '결제 정산 서비스', readmeText: '# T\n\n웹훅 백엔드.\n', milestones: [{ id: 'M-0001', title: '베타', status: 'planned' }], tasks: [] });
3188
+ const draftOk = d.hasSignal && d.purpose.some(l => l.includes('결제 정산 서비스')) && d.goal.some(l => l.includes('M-0001')) && d.purpose[0].includes('draft:');
3189
+ // 무신호 시 발명 금지
3190
+ const empty = p._draftAnchors({});
3191
+ const noInvent = !empty.hasSignal && empty.purpose.length === 0 && empty.goal.length === 0;
3192
+ // 섹션 교체는 대상 섹션만, 나머지 불변 + 섹션 없으면 원문 그대로
3193
+ const md = '# B\n\n## Purpose\n- placeholder\n\n## Users\n- real users\n';
3194
+ const rep = p._replaceMdSection(md, 'Purpose', ['- new']);
3195
+ const secOk = rep.includes('- new') && !rep.includes('- placeholder') && rep.includes('- real users');
3196
+ const noSec = p._replaceMdSection(md, 'Nope', ['- x']) === md;
3197
+ // CLI 배선 + handoff 힌트
3198
+ const s = read(__filename);
3199
+ const wired = typeof anchorsCmd === 'function' && s.includes("cmd === 'anchors'") && s.includes('→ leerness anchors draft');
3200
+ return draftOk && noInvent && secOk && noSec && wired;
3201
+ } },
3202
+ { name: 'managed 파일 force 병합 (1.36.37, 1.9.441 드리프트 실측): writeIfSafe 가 --force 여도 managed 는 라인-diff 병합 — CLAUDE 커스텀 보존 (행위검사)', run: () => {
3203
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_wif_'));
3204
+ let ok1 = false, ok2 = false, ok3 = false;
3205
+ try {
3206
+ const f = 'CLAUDE.md';
3207
+ fs.writeFileSync(path.join(tmp, f), '# T\nline A\nMY CUSTOM X\n');
3208
+ // force + mergeManaged → 병합 (커스텀 보존)
3209
+ const r1 = writeIfSafe(tmp, f, '# T\nline A\nline B\n', { force: true, mergeManaged: true });
3210
+ const t1 = fs.readFileSync(path.join(tmp, f), 'utf8');
3211
+ ok1 = r1.action === 'merged' && t1.includes('MY CUSTOM X') && t1.includes('line B');
3212
+ // 비-managed + force → 통째 교체(기존 동작 유지)
3213
+ fs.writeFileSync(path.join(tmp, 'g.md'), 'OLD');
3214
+ const r2 = writeIfSafe(tmp, 'g.md', 'NEW', { force: true });
3215
+ ok2 = r2.action === 'updated' && fs.readFileSync(path.join(tmp, 'g.md'), 'utf8') === 'NEW';
3216
+ // 비-managed + no-force → 보존
3217
+ const r3 = writeIfSafe(tmp, 'g.md', 'NEWER', {});
3218
+ ok3 = r3.action === 'preserved' && fs.readFileSync(path.join(tmp, 'g.md'), 'utf8') === 'NEW';
3219
+ } catch {} finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
3220
+ return ok1 && ok2 && ok3;
3221
+ } },
3222
+ { name: 'rules 손상 행 무언삭제 방지 (1.36.37, codex 4차 #1 High): 3~5셀 복구 + 3셀 미만 원문 보존 (행위검사)', run: () => {
3223
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_rl_'));
3224
+ let salvage = false, preserve = false;
3225
+ try {
3226
+ fs.mkdirSync(path.join(tmp, '.harness'), { recursive: true });
3227
+ const f = path.join(tmp, '.harness', 'rules.md');
3228
+ // 4셀 행(status/lastVerified 누락) + 2셀 행(파싱 불가)
3229
+ fs.writeFileSync(f, _rulesHeader() + '\n| R-0001 | every-session | 정상 룰 | 2026-07-16 | active | - |\n| R-0002 | every-commit | 절단 룰 | 2026-07-16 |\n| R-0003 | broken |\n');
3230
+ const rules = readRules(tmp);
3231
+ salvage = rules.length === 2 && rules.some(r => r.id === 'R-0002' && r.status === 'active');
3232
+ writeRules(tmp, rules);
3233
+ const t = fs.readFileSync(f, 'utf8');
3234
+ preserve = t.includes('R-0002') && t.includes('R-0003') && t.includes('파싱 불가');
3235
+ } catch {} finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} }
3236
+ return salvage && preserve;
3237
+ } },
3181
3238
  { name: 'debug 렌즈 (1.36.27, obra/superpowers systematic-debugging): 자기질문 6문항 ko/en 락스텝 + affects 유효 + route bugfix 힌트 + 파일매핑 미확장 — 행위검사', run: () => {
3182
3239
  const d = LENS_CATALOG.debug;
3183
3240
  if (!d) return false;
@@ -4853,6 +4910,58 @@ function shellGuardCmd(root, cmd, opts = {}) {
4853
4910
  if (analysis.issues.some(i => i.severity === 'error')) process.exitCode = 1;
4854
4911
  }
4855
4912
 
4913
+ // 1.36.36 (도그푸딩 실측 후속): `leerness anchors [status|draft [--apply]]` — 정체성앵커(brief Purpose / plan Goal) 미작성 전환 지원.
4914
+ // 실측: 1.36.19 감지 출하 후에도 실프로젝트 brief 4/7 · Goal 6/7 미작성 — 노출만으론 전환 안 됨. 초안 합성으로 마찰 제거.
4915
+ // 안전: --apply 는 해당 섹션이 "미작성일 때만" 교체(작성된 내용 절대 불클로버), 초안 표식 주석 명시.
4916
+ function anchorsCmd(root, sub) {
4917
+ root = absRoot(root);
4918
+ const json = has('--json');
4919
+ if (!exists(path.join(root, '.harness'))) { failJson(json, 'harness_missing', `leerness 미설치: ${root}`); return; } // 코드명 주의: audit 고유 finding kind(초기화 미완 마커)는 selftest 가드가 bin 부재를 단언하므로 여기서 사용 금지
4920
+ const bf = path.join(root, '.harness', 'project-brief.md');
4921
+ const pf = planPath(root);
4922
+ const briefTxt = exists(bf) ? read(bf) : '';
4923
+ const planTxt = exists(pf) ? read(pf) : '';
4924
+ const naM = '<!-- leerness:na';
4925
+ const briefGap = !!briefTxt && !briefTxt.includes(naM) && _briefUnfilled(briefTxt);
4926
+ const planGap = !!planTxt && !planTxt.includes(naM) && _planGoalUnfilled(planTxt);
4927
+ if (!sub || sub === 'status') {
4928
+ if (json) { log(JSON.stringify({ briefUnfilled: briefGap, planGoalUnfilled: planGap }, null, 2)); return; }
4929
+ log(`# leerness anchors — 정체성앵커 상태`);
4930
+ log(` project-brief Purpose: ${briefGap ? '📋 미작성' : '✓ 작성됨'}`);
4931
+ log(` plan Goal: ${planGap ? '📋 미작성' : '✓ 작성됨'}`);
4932
+ if (briefGap || planGap) log(`\n 💡 초안 생성: leerness anchors draft · 적용: leerness anchors draft --apply (미작성 섹션만 채움)`);
4933
+ return;
4934
+ }
4935
+ if (sub === 'draft') {
4936
+ // 실신호 수집 (전부 read-only)
4937
+ let pkgDescription = '';
4938
+ try { const pj = JSON.parse(read(path.join(root, 'package.json'))); pkgDescription = pj.description || ''; } catch {}
4939
+ let readmeText = '';
4940
+ try { if (exists(path.join(root, 'README.md'))) readmeText = read(path.join(root, 'README.md')).slice(0, 4000); } catch {}
4941
+ let milestones = [], tasks = [];
4942
+ try { const rd = _roadmapData(root); milestones = rd.milestones || []; tasks = rd.tasks || []; } catch {}
4943
+ const draft = _draftAnchors({ pkgDescription, readmeText, milestones, tasks });
4944
+ if (!draft.hasSignal) { failJson(json, 'no_signal', '초안을 만들 실신호가 없음 (package.json description / README 산문 / milestone / 활성 task 모두 부재) — 직접 작성 필요'); return; }
4945
+ const apply = has('--apply');
4946
+ const applied = [];
4947
+ if (briefGap && draft.purpose.length) {
4948
+ if (apply) { writeUtf8(bf, _replaceMdSection(briefTxt, 'Purpose', draft.purpose)); applied.push('project-brief.md#Purpose'); }
4949
+ }
4950
+ if (planGap && draft.goal.length) {
4951
+ if (apply) { writeUtf8(pf, _replaceMdSection(planTxt, 'Goal', draft.goal)); applied.push('plan.md#Goal'); }
4952
+ }
4953
+ if (json) { log(JSON.stringify({ briefUnfilled: briefGap, planGoalUnfilled: planGap, draft: { purpose: draft.purpose, goal: draft.goal }, applied, dryRun: !apply }, null, 2)); return; }
4954
+ log(`# leerness anchors draft ${apply ? '(적용)' : '(dry-run — 적용: --apply)'}`);
4955
+ if (briefGap) { log(`\n## project-brief Purpose ${apply ? '← 적용됨' : '초안'}`); draft.purpose.forEach(l => log(' ' + l)); }
4956
+ else log(`\n ✓ project-brief Purpose 는 이미 작성됨 — 건드리지 않음`);
4957
+ if (planGap) { log(`\n## plan Goal ${apply ? '← 적용됨' : '초안'}`); draft.goal.forEach(l => log(' ' + l)); }
4958
+ else log(` ✓ plan Goal 은 이미 작성됨 — 건드리지 않음`);
4959
+ if (apply && applied.length) log(`\n✓ 적용: ${applied.join(', ')} (초안 표식 주석 포함 — 검토 후 주석 제거로 확정)`);
4960
+ return;
4961
+ }
4962
+ fail(`알 수 없는 하위명령: ${sub} (가능: status, draft [--apply])`);
4963
+ }
4964
+
4856
4965
  // 1.36.28 (codex 미검토표면 헌트 #3 — 데이터 유실 클래스): 손상된 JSON 스토어를 "빈 값"으로 오인해 덮어쓰는 fail-open 방지.
4857
4966
  // 파일이 "없음"과 "있으나 손상"을 구분 — 후자는 파싱 실패를 던져 변경(저장)을 거부하고 원본을 보존한다.
4858
4967
  // 설계: LOAD 가 아니라 SAVE/변경 진입점에서만 던진다(handoff 등 비변경 읽기 경로는 resilient 유지). state 는
@@ -9806,7 +9915,7 @@ function handoff(root) {
9806
9915
  const _planTxt = exists(planPath(root)) ? read(planPath(root)) : '';
9807
9916
  const briefGap = !_briefTxt.includes(_naM) && _briefUnfilled(_briefTxt);
9808
9917
  const planGap = !!_planTxt && !_planTxt.includes(_naM) && _planGoalUnfilled(_planTxt);
9809
- if (briefGap && planGap) parts.push(t('📋 정체성앵커 미작성 (brief+plan)', '📋 identity anchors unfilled (brief+plan)'));
9918
+ if (briefGap && planGap) parts.push(t('📋 정체성앵커 미작성 (brief+plan) → leerness anchors draft', '📋 identity anchors unfilled (brief+plan) → leerness anchors draft'));
9810
9919
  else if (briefGap) parts.push(t('📋 project-brief 미작성', '📋 project-brief unfilled'));
9811
9920
  else if (planGap) parts.push(t('📋 plan Goal 미작성', '📋 plan Goal unfilled'));
9812
9921
  } catch {}
@@ -15094,16 +15203,34 @@ function readRules(root) {
15094
15203
  if (!/^\| R-\d{4,} \|/.test(line)) continue;
15095
15204
  // 1.9.399 (7번째 버그헌트 P1-A, UR-0104): 비이스케이프 파이프 분리 + 복원 — rule 텍스트의 '|'가 컬럼 밀림/멱등성 무력화를 못 일으킴.
15096
15205
  const cells = line.split(/(?<!\\)\|/).slice(1, -1).map(s => _cellUnescape(s).trim());
15097
- if (cells.length < 6) continue;
15098
- rules.push({ id: cells[0], trigger: cells[1], rule: cells[2], added: cells[3], status: cells[4], lastVerified: cells[5] });
15206
+ // 1.36.38 (codex 4차 #1, High): 셀 부족 행을 버리면 다음 writeRules 가 그 룰을 무언 삭제했다(수동 편집/중단된 쓰기에서 발생).
15207
+ // 3~5셀은 복구(누락 기본값 기능 유지), 3 미만만 파싱 불가로 남긴다(writeRules 가 원문 보존).
15208
+ if (cells.length < 3) continue;
15209
+ rules.push({ id: cells[0], trigger: cells[1], rule: cells[2], added: cells[3] || '-', status: cells[4] || 'active', lastVerified: cells[5] || '-' });
15099
15210
  }
15100
15211
  return rules;
15101
15212
  }
15102
15213
 
15214
+ // 1.36.38 (#1): 파싱 불가(3셀 미만) 룰-형태 행 수집 — writeRules 가 삭제하지 않고 원문 보존하기 위함.
15215
+ function _unparsableRuleLines(root) {
15216
+ const f = rulesPath(root);
15217
+ if (!exists(f)) return [];
15218
+ const out = [];
15219
+ for (const line of read(f).split('\n')) {
15220
+ if (!/^\| R-\d{4,} \|/.test(line)) continue;
15221
+ const cells = line.split(/(?<!\\)\|/).slice(1, -1);
15222
+ if (cells.length < 3) out.push(line);
15223
+ }
15224
+ return out;
15225
+ }
15226
+
15103
15227
  function writeRules(root, rules) {
15104
15228
  // 1.9.399 (7번째 버그헌트 P1-A, UR-0104): 셀 안전화 — rule 텍스트의 파이프/개행 차단(컬럼 밀림·중복 룰 무한생성 방지).
15105
15229
  const body = rules.map(r => `| ${_cellSafe(r.id)} | ${_cellSafe(r.trigger)} | ${_cellSafe(r.rule)} | ${_cellSafe(r.added)} | ${_cellSafe(r.status)} | ${_cellSafe(r.lastVerified || '-')} |`).join('\n');
15106
- writeUtf8(rulesPath(root), _rulesHeader() + '\n' + body + (body ? '\n' : ''));
15230
+ // 1.36.38 (#1): 파싱 불가 행은 무언 삭제 대신 원문 보존 + 경고 표식 (수동 복구 대상)
15231
+ const damaged = _unparsableRuleLines(root).filter(l => !rules.some(r => l.startsWith(`| ${r.id} `)));
15232
+ const tail = damaged.length ? '\n<!-- ⚠ 아래 행은 손상돼 파싱 불가 — 삭제하지 않고 보존함. 수동 복구 후 위 표로 병합하세요 -->\n' + damaged.join('\n') + '\n' : '';
15233
+ writeUtf8(rulesPath(root), _rulesHeader() + '\n' + body + (body ? '\n' : '') + tail);
15107
15234
  }
15108
15235
 
15109
15236
  function nextRuleId(root) {
@@ -21718,6 +21845,7 @@ async function main() {
21718
21845
  if (cmd === 'milestones') return milestonesCmd(_resolveRoot(args[1]));
21719
21846
  // 1.9.231: leerness pulse — 한 줄 종합 요약 (10 핵심 지표)
21720
21847
  if (cmd === 'pulse') return pulseCmd(_resolveRoot(args[1]));
21848
+ if (cmd === 'anchors') return anchorsCmd(arg('--path', null) || _taskPositionalPath(args, 1) || process.cwd(), args[1] && !args[1].startsWith('-') ? args[1] : null); // 1.36.36: 정체성앵커 초안
21721
21849
  if (cmd === 'toggle') return _tgl.toggleCmd(arg('--path', process.cwd()), args[1], args[2], args[3], { has, VERSION }); // 1.36.30: 기능 토글 (그래프 ⚙ 탭 연동)
21722
21850
  if (cmd === 'lens') return lensCmd(args[1]); // 1.18.3 (UR-0003): 분야별 자기질문 품질 렌즈
21723
21851
  // 1.9.233: leerness commands — 카테고리화된 전체 CLI 명령 목록
package/lib/pure-utils.js CHANGED
@@ -1145,8 +1145,47 @@ function _sectionUnfilled(content, heading, placeholderRe) {
1145
1145
  function _briefUnfilled(content) { return _sectionUnfilled(content, 'Purpose', _BRIEF_PLACEHOLDER_RE); }
1146
1146
  function _planGoalUnfilled(content) { return _sectionUnfilled(content, 'Goal', _PLAN_GOAL_PLACEHOLDER_RE); }
1147
1147
 
1148
+ // 1.36.36 (도그푸딩 실측 후속): 정체성앵커 초안 합성 — 실신호(package.json/README/milestones/tasks)만으로 순수 조합.
1149
+ // 배경: 1.36.19 감지 출하 후에도 실프로젝트 7곳 재실측에서 brief 미작성 4/7 · plan Goal 미작성 6/7 — 노출만으론 전환 안 됨.
1150
+ // 원칙: 발명 금지(신호에 없는 내용은 쓰지 않음) + 초안 표식 명시(확정은 AI/사용자 몫) — 과장 방지.
1151
+ function _draftAnchors(sig = {}) {
1152
+ const purpose = [];
1153
+ const goal = [];
1154
+ const desc = String(sig.pkgDescription || '').trim();
1155
+ // README 첫 문단: 제목/배지/빈줄 건너뛰고 첫 산문 줄
1156
+ let readmeLead = '';
1157
+ for (const line of String(sig.readmeText || '').split(/\r?\n/)) {
1158
+ const t = line.trim();
1159
+ if (!t || t.startsWith('#') || t.startsWith('![') || t.startsWith('[!') || t.startsWith('<')) continue;
1160
+ readmeLead = t; break;
1161
+ }
1162
+ if (desc) purpose.push(`- ${desc}`);
1163
+ if (readmeLead && readmeLead !== desc) purpose.push(`- ${readmeLead.slice(0, 200)}`);
1164
+ const ms = (sig.milestones || []).filter(m => m && m.title).slice(0, 3);
1165
+ const activeTasks = (sig.tasks || []).filter(t => t && /in-progress|planned|requested/.test(String(t.status || ''))).slice(0, 3);
1166
+ if (desc || readmeLead) goal.push(`- ${(desc || readmeLead).slice(0, 160)} 를(을) 동작하는 제품으로 유지·발전시킨다`);
1167
+ for (const m of ms) goal.push(`- [${m.id || 'M'}] ${String(m.title).slice(0, 120)}${m.status ? ` (${m.status})` : ''}`);
1168
+ if (!goal.length && activeTasks.length) for (const t of activeTasks) goal.push(`- ${t.id}: ${String(t.request || '').slice(0, 100)}`);
1169
+ const marker = '<!-- draft: leerness anchors — 실신호 기반 초안. AI/사용자가 검토·수정 후 이 주석을 지우면 확정 -->';
1170
+ return {
1171
+ hasSignal: purpose.length > 0 || goal.length > 0,
1172
+ purpose: purpose.length ? [marker, ...purpose] : [],
1173
+ goal: goal.length ? [marker, ...goal] : [],
1174
+ };
1175
+ }
1176
+
1177
+ // 섹션 본문 교체(## <heading> 다음부터 다음 ## 전까지) — 파일 나머지는 불변. 대상 섹션 없으면 원문 그대로.
1178
+ function _replaceMdSection(content, heading, newBodyLines) {
1179
+ const lines = String(content || '').replace(/\r\n/g, '\n').split('\n');
1180
+ const start = lines.findIndex(l => l.trim().replace(/^#+\s*/, '').toLowerCase() === String(heading).toLowerCase() && /^#+\s/.test(l.trim()));
1181
+ if (start < 0) return content;
1182
+ let end = lines.length;
1183
+ for (let i = start + 1; i < lines.length; i++) if (/^##\s/.test(lines[i])) { end = i; break; }
1184
+ return [...lines.slice(0, start + 1), '', ...newBodyLines, '', ...lines.slice(end)].join('\n');
1185
+ }
1186
+
1148
1187
  module.exports = {
1149
- _parseImplExports, _briefUnfilled, _planGoalUnfilled, _mdSectionBody,
1188
+ _parseImplExports, _briefUnfilled, _planGoalUnfilled, _mdSectionBody, _draftAnchors, _replaceMdSection,
1150
1189
  _matchTool, _parsePackageJsonDeps, _parseRequirementsTxt, _buildGlossary, _renderGlossaryMd, GLOSSARY_START, GLOSSARY_END,
1151
1190
  _isSecretKey, compareVer, parseHarnessVersion,
1152
1191
  _isPlaceholderSecret, _looksSecretLike,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.36.35",
3
+ "version": "1.36.37",
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",