leerness 1.36.18 → 1.36.19

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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.36.19 — 2026-07-14 — 전략 앵커(project-brief/plan Goal) 미작성 감지 — 인계 AI가 프로젝트 맥락을 못 받던 근본원인 표면화 (실사용 7 프로젝트 dogfood)
4
+
5
+ **현장 관찰**: leerness를 오래 적용한 실프로젝트에서, 인계받은 AI(codex 등)가 "leerness를 참조하나 프로젝트 맥락을 이해 못 하고" 작업하는 현상이 보고됨. 7개 실사용 프로젝트를 dogfood 조사한 결과 근본원인이 드러남 — **동적 상태(current-state 7/7, decisions)는 잘 유지되나, 정체성 앵커인 `project-brief.md` Purpose(5/7 미작성)와 `plan.md` Goal/Scope(7/7 미작성)이 템플릿 placeholder 그대로 방치됨**. 27개 결정을 쌓은 프로젝트조차 brief는 빈칸. 그 결과 인계 AI는 "최근 무슨 작업을 했는지"(동적)는 받지만 "이 프로젝트가 무엇이고 범위가 어디까지인지"(정체성)를 못 받아, 최근 스레드만 이어가며 전체 프레임을 놓침. 게다가 leerness 스스로 이 갭을 전혀 경고하지 않았음(감지 부재).
6
+
7
+ - **전략 앵커 미작성 감지 (신규)**: pure-utils에 `_briefUnfilled`/`_planGoalUnfilled` 순수 detector 추가 — `## Purpose`/`## Goal` 섹션이 템플릿 placeholder 문구이거나 실내용 0(빈 bullet)이면 미작성으로 판정. bullet/문단 무관(문단형 실작성도 정상 인식), 섹션 자체가 없으면 미플래그(FP 회피). 7 프로젝트 ground-truth + 컨트롤(placeholder/빈칸/bullet작성/문단작성/섹션없음)로 판별 정밀 검증.
8
+ - **`audit` finding 2종**: project-brief Purpose 미작성 → `project_brief_unfilled` 경고("인계받는 AI가 프로젝트 목적/맥락을 못 받습니다"), plan Goal 미작성 → `plan_goal_unfilled` 경고. `<!-- leerness:na -->` 마커로 의도적 스킵 가능(기존 design-system/reuse-map 관례와 동일).
9
+ - **handoff 헤드라인 표면화**: 세션 시작 헤드라인에 `📋 project-brief 미작성` / `📋 plan Goal 미작성` / `📋 정체성앵커 미작성 (brief+plan)` 노출 — 파묻힌 init task(T-0001) 대신 매 세션 가시화. 채워진 프로젝트엔 무신호(무노이즈).
10
+ - **검증**: selftest 289/289(신규 앵커 감지 행위검사 — placeholder/빈칸/bullet/문단/섹션없음 5판별 + audit/handoff 배선 가드), 실프로젝트 실증(Adzento=brief+plan 경고, cafe-macro/view-work=brief populated·plan만), 게이트 e2e, 게시본 클린룸.
11
+ - **이연(다음 라운드, codex fresh-surface QA)**: #7 `--json`이 mutation 전 early-return(`reuse autodetect`/`release cleanup`/`env encoding` `--apply --json`이 apply를 보고하나 실제 미적용) + 실패 시 exit 0, #8 CRLF `plan.md` tasks:[] & 잘못된 JSON 스키마가 `--json` 경로에서 crash(구조화 에러 아님), #6 `copyRec` lstatSync 심링크 미처리. 재현→수정 예정.
12
+
3
13
  ## 1.36.18 — 2026-07-14 — UR-0052: handoff --compact 단일경로 + ID 5자리+ 대응 (P2-6 주석 마스킹은 조사 후 이연)
4
14
 
5
15
  1.36.17에서 백로그로 이연했던 UR-0052 3건 중 2건(P3-8·P1-2)을 재현·수정. P2-6(주석 마스킹)은 codex 7라운드 적대 재검 끝에 **파서 없이 안전 구현 불가**로 판단해 이연(하단 상세). 각 수정은 파일 스크립트 프로브 + selftest 케이스로 실증(맹신 X).
package/README.md CHANGED
@@ -125,7 +125,7 @@ MIT
125
125
  <!-- leerness:project-readme:start -->
126
126
  ## Leerness Project Harness
127
127
 
128
- 이 프로젝트는 Leerness v1.36.18 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
128
+ 이 프로젝트는 Leerness v1.36.19 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
129
129
 
130
130
  ### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
131
131
 
@@ -179,7 +179,7 @@ leerness memory restore decision <date|title>
179
179
 
180
180
  ### MCP server (외부 AI 통합)
181
181
 
182
- Leerness v1.36.18는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
182
+ Leerness v1.36.19는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
183
183
 
184
184
  ```jsonc
185
185
  // 카테고리별
@@ -200,7 +200,7 @@ Leerness v1.36.18는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
200
200
  `<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
201
201
  1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) stress-v* 신규 작성 + 누적 회귀 → 4) e2e 219/219 → 5) npm pack + git tag + GitHub release → 6) main 자동 push (1.9.140+) → 7) session close → 8) 다음 라운드 예약.
202
202
 
203
- 현재 누적: **70 라운드 (1.9.40 → 1.36.18)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
203
+ 현재 누적: **70 라운드 (1.9.40 → 1.36.19)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
204
204
 
205
205
  ### 성능 가이드 (1.9.140 측정)
206
206
 
@@ -238,6 +238,6 @@ leerness release pack --close --auto-main-push
238
238
  - `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
239
239
  - `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
240
240
 
241
- Last synced by Leerness v1.36.18: 2026-07-14
241
+ Last synced by Leerness v1.36.19: 2026-07-14
242
242
  <!-- leerness:project-readme:end -->
243
243
 
package/bin/leerness.js CHANGED
@@ -26,14 +26,14 @@ 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 } = require('../lib/pure-utils'); // 1.9.318~1.11.4 (UR-0025/.../0007 glossary): 순수 유틸 모듈 분리
29
+ _matchTool, _parsePackageJsonDeps, _parseRequirementsTxt, _buildGlossary, _renderGlossaryMd, _briefUnfilled, _planGoalUnfilled } = require('../lib/pure-utils'); // 1.9.318~1.11.4 (UR-0025/.../0007 glossary): 순수 유틸 모듈 분리
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).
33
33
  const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE_CHECKLIST, _DEFAULT_PLATFORM_CONSTRAINTS, _DEFAULT_DOMAIN_CATALOG, _TOOL_CATALOG, _LSP_LANG_PATTERNS, OPTIMISM_PATTERNS, BUILT_IN_PERSONAS, STRINGS, BUILTIN_CATALOG, ROADMAP_STATUS_LABEL, ROADMAP_STATUS_COLOR, SECRET_PATTERNS, MERGE_OVERWRITE_FILES, MINIMAL_SKIP_KEYS, REQUIRED_WORKSPACE_FILES, KEYWORD_STOPWORDS, SKILL_CATALOG_PRESETS } = require('../lib/catalogs'); // 1.9.344/368/369 (UR-0025): catalog 분리 · 1.11.4 (UR-0007): _TOOL_CATALOG
34
34
  const { findCorruptedStateJson: _findCorruptedStateJson } = require('../lib/state-integrity'); // 1.36.1 (클린룸 리뷰 FN): .harness/*.json 상태 무결성 (audit/health/check 공유)
35
35
 
36
- const VERSION = '1.36.18';
36
+ const VERSION = '1.36.19';
37
37
 
38
38
  // 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
39
39
  // 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
@@ -2984,6 +2984,22 @@ function _selfTestCases() {
2984
2984
  const libNoStale = !read(dir + '/feature.js').includes('F-\\d{4}$') && !read(dir + '/audit.js').includes('(M-\\d{4})');
2985
2985
  return idFive && idFour && ruleFive && mileFive && dateExact4 && readersWidened && dateKept && libWidened && libNoStale;
2986
2986
  } },
2987
+ { name: '전략 앵커 미작성 감지 (1.36.19, 실사용 7 프로젝트 dogfood): project-brief Purpose / plan Goal placeholder·빈칸 감지 + 실작성(bullet/문단) 무오탐 — 행위검사', run: () => {
2988
+ const p = require('../lib/pure-utils');
2989
+ if (typeof p._briefUnfilled !== 'function' || typeof p._planGoalUnfilled !== 'function') return false;
2990
+ const phBrief = p._briefUnfilled('# Project Brief\n## Purpose\n- 이 프로젝트의 목적을 실제 내용으로 업데이트하세요.\n## Users\n-\n'); // 템플릿 placeholder
2991
+ const emptyBrief = p._briefUnfilled('## Purpose\n- \n## Users\n-\n'); // 빈 bullet
2992
+ const filledBullet = !p._briefUnfilled('## Purpose\n- 소셜 발행 자동화 MCP 서버\n'); // 실작성(bullet) 무오탐
2993
+ const filledPara = !p._briefUnfilled('## Purpose\n코드베이스 변화를 실시간 가시화한다\n'); // 실작성(문단) 무오탐
2994
+ const noSection = !p._briefUnfilled('# Project Brief\n## Project\nfoo\n'); // Purpose 섹션 없음 → 미플래그(FP 회피)
2995
+ const phPlan = p._planGoalUnfilled('# Plan\n## Goal\n- 사용자 목적을 기준으로 전체 계획을 유지합니다.\n## Scope\n-\n'); // plan 템플릿
2996
+ const filledPlan = !p._planGoalUnfilled('## Goal\n- 카드뉴스 발행 자동화 완성\n'); // 실작성 plan 무오탐
2997
+ // audit + handoff 배선 가드
2998
+ const s = read(__filename); const au = require('fs').readFileSync(require('path').join(require('path').dirname(__dirname), 'lib', 'audit.js'), 'utf8');
2999
+ const wired = au.includes("_finding('project_brief_unfilled'") && au.includes("_finding('plan_goal_unfilled'")
3000
+ && s.includes('📋 project-brief 미작성');
3001
+ return phBrief && emptyBrief && filledBullet && filledPara && noSection && phPlan && filledPlan && wired;
3002
+ } },
2987
3003
  { name: '시크릿 스캐너 FN 헌트 (1.35.14): 하드코딩 자격증명 복합/JSON키 탐지 + Slack xapp + 사전단어 FP 억제 — 행위검사', run: () => {
2988
3004
  const pats = require('../lib/catalogs').SECRET_PATTERNS;
2989
3005
  const pu = require('../lib/pure-utils');
@@ -9325,6 +9341,19 @@ function handoff(root) {
9325
9341
  }
9326
9342
  }
9327
9343
  } catch {}
9344
+ // 2b) 전략 앵커 미작성 (1.36.19, 실사용 7 프로젝트 dogfood): project-brief Purpose(5/7)·plan Goal(7/7)이 템플릿 그대로
9345
+ // 방치돼 인계받는 AI 가 "프로젝트가 무엇인지/범위"를 못 받던 근본원인 → 세션시작 헤드라인에 표면화(파묻힌 init task 대신 가시화).
9346
+ try {
9347
+ const _naM = '<!-- leerness:na';
9348
+ const _bf = path.join(root, '.harness/project-brief.md');
9349
+ const _briefTxt = exists(_bf) ? read(_bf) : '';
9350
+ const _planTxt = exists(planPath(root)) ? read(planPath(root)) : '';
9351
+ const briefGap = !_briefTxt.includes(_naM) && _briefUnfilled(_briefTxt);
9352
+ const planGap = !!_planTxt && !_planTxt.includes(_naM) && _planGoalUnfilled(_planTxt);
9353
+ if (briefGap && planGap) parts.push(t('📋 정체성앵커 미작성 (brief+plan)', '📋 identity anchors unfilled (brief+plan)'));
9354
+ else if (briefGap) parts.push(t('📋 project-brief 미작성', '📋 project-brief unfilled'));
9355
+ else if (planGap) parts.push(t('📋 plan Goal 미작성', '📋 plan Goal unfilled'));
9356
+ } catch {}
9328
9357
  // 3) MCP 활동 누적
9329
9358
  try {
9330
9359
  const stats = _readUsageStats(root);
package/lib/audit.js CHANGED
@@ -7,6 +7,7 @@ const path = require('path');
7
7
  const { log, ok, warn, fail, failJson, today, now, absRoot, exists, read, readBuf, mkdirp, writeUtf8, append, rel } = require('./io');
8
8
  const { SECRET_PATTERNS } = require('./catalogs');
9
9
  const { findCorruptedStateJson } = require('./state-integrity'); // 1.36.1 (클린룸 리뷰 FN): 상태 JSON 무결성
10
+ const { _briefUnfilled, _planGoalUnfilled } = require('./pure-utils'); // 1.36.19: 전략 앵커(project-brief/plan Goal) 미작성 감지
10
11
 
11
12
  function audit(root, opts = {}, deps = {}) {
12
13
  const { VERSION, arg, has, planPath, readProgressRows, currentStatePath, handoffPath, envDiff, _readFeatureGraph, _matchAPISkills, _listAPISkills, _collectSecretFindings } = deps;
@@ -61,7 +62,15 @@ function audit(root, opts = {}, deps = {}) {
61
62
  if (reuse.includes(naMarker)) ok('reuse-map.md marked NA (skipped)');
62
63
  else if (reuseLines === 0) { warnings++; warn('reuse-map.md is empty (consider populating known reusable elements)'); _finding('reuse_map_empty', 'warn', 'reuse-map.md is empty'); }
63
64
  else ok(`reuse-map.md has ${reuseLines} entries`);
65
+ // 1.36.19 (실사용 7 프로젝트 dogfood): 전략 앵커 미작성 — 동적 상태는 유지되나 project-brief Purpose(5/7)·plan Goal(7/7)이
66
+ // 템플릿 그대로라 인계받는 AI 가 "프로젝트가 무엇인지/범위"를 못 받음(관찰된 근본원인). naMarker 로 의도적 스킵 가능.
67
+ const briefFile = path.join(root, '.harness/project-brief.md');
68
+ const briefTxt = exists(briefFile) ? read(briefFile) : '';
69
+ if (briefTxt.includes(naMarker)) ok('project-brief.md marked NA (skipped)');
70
+ else if (_briefUnfilled(briefTxt)) { warnings++; warn('project-brief.md Purpose 미작성(템플릿 그대로) — 인계받는 AI가 프로젝트 목적/맥락을 못 받습니다 (project-brief.md 채우기)'); _finding('project_brief_unfilled', 'warn', 'project-brief.md Purpose still placeholder/empty — AI handoffs lack project context'); }
71
+ else ok('project-brief.md Purpose populated');
64
72
  const planText = exists(planPath(root)) ? read(planPath(root)) : '';
73
+ if (planText && !planText.includes(naMarker) && _planGoalUnfilled(planText)) { warnings++; warn('plan.md Goal 미작성(템플릿 그대로) — 프로젝트 목표/범위가 비어 인계 AI가 방향을 못 잡습니다 (plan.md Goal/Scope 채우기)'); _finding('plan_goal_unfilled', 'warn', 'plan.md Goal still placeholder/empty'); }
65
74
  const milestoneIds = Array.from(planText.matchAll(/^### (M-\d{4,})\./gm)).map(m => m[1]);
66
75
  const rows = readProgressRows(root);
67
76
  // 1.9.6 수정: 한 row에 여러 plan:M-XXXX 링크가 있어도 모두 인식 (matchAll로 전부 추출)
package/lib/pure-utils.js CHANGED
@@ -1043,8 +1043,33 @@ function _renderGlossaryMd(entries, opts = {}) {
1043
1043
  return s + GLOSSARY_END + '\n';
1044
1044
  }
1045
1045
 
1046
+ // 1.36.19 (실사용 7 프로젝트 dogfood): 전략 앵커(project-brief Purpose / plan Goal) 미작성 감지.
1047
+ // 현장 관찰 — 동적 상태(current-state/decisions)는 7/7 유지되나 정체성 앵커는 brief 5/7·plan 7/7 이 템플릿 placeholder
1048
+ // 그대로 방치됨(27개 결정 쌓은 프로젝트조차 brief 빈칸). 인계받은 AI 가 "프로젝트가 무엇인지"를 못 받는 근본원인 →
1049
+ // audit/handoff 가 표면화하도록 순수 detector. bullet/문단 무관, 빈 섹션·placeholder 문구를 모두 미작성으로 판정.
1050
+ // 섹션 자체가 없으면(구조 미상) 미플래그(FP 회피). `<!-- leerness:na -->` 스킵은 캘러(audit)가 처리.
1051
+ function _mdSectionBody(content, heading) {
1052
+ const lines = String(content || '').split('\n');
1053
+ const h = new RegExp('^##\\s*' + heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s*$', 'i');
1054
+ const i = lines.findIndex(l => h.test(l));
1055
+ if (i < 0) return null;
1056
+ const body = [];
1057
+ for (let j = i + 1; j < lines.length; j++) { if (/^##\s/.test(lines[j])) break; body.push(lines[j]); }
1058
+ return body.join('\n');
1059
+ }
1060
+ const _BRIEF_PLACEHOLDER_RE = /실제\s*(?:내용|목적)\s*으로\s*업데이트|describe .*purpose here|update .*with (?:the )?real/i;
1061
+ const _PLAN_GOAL_PLACEHOLDER_RE = /전체\s*계획을\s*유지합니다|사용자\s*목적을\s*기준으로|state your goal here|maintain the overall plan/i;
1062
+ function _sectionUnfilled(content, heading, placeholderRe) {
1063
+ const body = _mdSectionBody(content, heading);
1064
+ if (body == null) return false; // 섹션 없음 → 미플래그
1065
+ if (placeholderRe.test(body)) return true; // 템플릿 문구 그대로
1066
+ return !body.split('\n').some(l => l.replace(/^\s*[-*]\s*/, '').trim().length > 0); // 실내용(bullet/문단) 0 → 빈칸
1067
+ }
1068
+ function _briefUnfilled(content) { return _sectionUnfilled(content, 'Purpose', _BRIEF_PLACEHOLDER_RE); }
1069
+ function _planGoalUnfilled(content) { return _sectionUnfilled(content, 'Goal', _PLAN_GOAL_PLACEHOLDER_RE); }
1070
+
1046
1071
  module.exports = {
1047
- _parseImplExports,
1072
+ _parseImplExports, _briefUnfilled, _planGoalUnfilled, _mdSectionBody,
1048
1073
  _matchTool, _parsePackageJsonDeps, _parseRequirementsTxt, _buildGlossary, _renderGlossaryMd, GLOSSARY_START, GLOSSARY_END,
1049
1074
  _isSecretKey, compareVer, parseHarnessVersion,
1050
1075
  _isPlaceholderSecret, _looksSecretLike,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.36.18",
3
+ "version": "1.36.19",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",