leerness 1.36.25 → 1.36.26

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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.36.26 — 2026-07-15 — `leerness skill lint` — SKILL 메타 품질 lint (2티어 severity · 한국어 트리거절 · CJK 본문예산) (obra/superpowers P1/P2)
4
+
5
+ 1.36.25(P0 — description 데이터)로 선행조건이 충족된 후속. 판별 증거: **P0 없이 lint 를 먼저 붙였으면 내장 9/9 가 실패**했을 것 — 지금은 내장 9종이 lint clean(순서가 맞았다는 실증, selftest 로 고정).
6
+
7
+ - **`_lintSkillMeta(meta, body)` 순수함수** (pure-utils, 의존 0) — 2티어 severity 설계(메모리 교훈: 게이트 휴리스틱은 false-PASS 편향, false-BLOCK 금지):
8
+ - **ERROR**(기계적, FP 0 — 이것만 exit 1): name 부재/문자셋 위반(`^[a-z0-9][a-z0-9-]*$`), description 부재/1024자 초과.
9
+ - **WARNING**(저FP 형태): name 반복 / 40자 미만(표시명 합성 흔적) / capabilities 나열 형태. 기본 exit 0, `--strict` 시 승격.
10
+ - **INFO**(주관적, 절대 실패 아님): 트리거절 부재, 본문 예산 초과.
11
+ - **한국어 적응**: 트리거절 ko(`~할 때 사용`)·en(`Use when`) 모두 인정, 3인칭 검사는 한국어(인칭 무표지)에 부적용이라 제외, **본문 예산 CJK 분기**(비CJK=단어수/CJK=char — 공백분리 단어수를 한국어에 쓰면 어절 카운트라 전면 오발).
12
+ - **`leerness skill lint [id] [--all] [--json] [--strict]`** — 설치 스킬(SKILL.md 우선, skill.json 폴백) + 내장 카탈로그 9종 대상. `--json` 구조화(ok/errors/warnings/results).
13
+ - **검증**: selftest 296/296(ERROR 기계판정 2종 + clean 무오발 + WARNING 분리 + CJK lang + **내장 9종 clean 고정** + CLI 배선), 실측 5종(내장 --all exit 0 / description 없음 ERROR exit 1 / WARNING 기본 0·strict 1 / ko 트리거 무 INFO / --json), 게이트 e2e, 게시본 클린룸.
14
+
3
15
  ## 1.36.25 — 2026-07-15 — 내장 스킬 9종 트리거 description + export/publish 합성 우선순위 — "언제 로드할지"를 알 수 있게 (obra/superpowers P0)
4
16
 
5
17
  superpowers 검토(1.36.22)에서 이연한 P0. **결함(도그푸딩 실측)**: BUILTIN_CATALOG 9개 스킬 전부 `description` 0/9 → (1) 설치 스킬 리더가 capabilities 나열로 폴백("feature-contracts 작성, 재사용 우선 검사…" = 에이전트가 "지금 로드할까"를 판단할 수 없는 워크플로 요약), (2) `skill export`/`skill publish` 가 description 을 displayNameKo("기능 구현 표준 스킬")로 합성 — **좋은 description 이 있어도 우선순위가 displayNameKo 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.25 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
124
+ 이 프로젝트는 Leerness v1.36.26 하네스를 사용합니다. 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.25는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
178
+ Leerness v1.36.26는 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.25는 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.25)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
199
+ 현재 누적: **70 라운드 (1.9.40 → 1.36.26)** · 매 라운드 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.25: 2026-07-15
237
+ Last synced by Leerness v1.36.26: 2026-07-15
238
238
  <!-- leerness:project-readme:end -->
239
239
 
package/bin/leerness.js CHANGED
@@ -22,7 +22,7 @@ const { _isSecretKey, _isPlaceholderSecret, _looksSecretLike, _mergeLines, _merg
22
22
  _detectOptimism: _puDetectOptimism, _computeConfidence: _puComputeConfidence,
23
23
  _personaSummaries, _translate,
24
24
  _decisionsFromMd, _renderDecisionsMd, _renderLessonsMd,
25
- _withBuiltinSource, _esc, _roadmapTokenStyles, _parseSkillMd,
25
+ _withBuiltinSource, _esc, _roadmapTokenStyles, _parseSkillMd, _lintSkillMeta,
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,
@@ -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.25';
37
+ const VERSION = '1.36.26';
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') 시 호스트 프로세스 오염.
@@ -1188,6 +1188,50 @@ function addSkill(root, name, silent = false) {
1188
1188
 
1189
1189
  // ===== Skill registry (catalog + user-defined merged) =====
1190
1190
  function userSkillsDir(root) { return path.join(absRoot(root), '.harness/skills'); }
1191
+
1192
+ // 1.36.26 (obra/superpowers P2): skill 메타 품질 lint — `leerness skill lint [id] [--all] [--json] [--strict]`.
1193
+ // 대상: 설치 스킬(.harness/skills/<id>/SKILL.md 우선, 없으면 skill.json) + 내장 카탈로그 9종(skill.json 형태로 lint).
1194
+ // 기본 exit 0, ERROR 있을 때만 exit 1. --strict 는 WARNING 도 승격. INFO 는 절대 실패 아님(false-BLOCK 방지 설계).
1195
+ function skillLintCmd(root, id) {
1196
+ root = absRoot(root);
1197
+ const jsonMode = has('--json');
1198
+ const strict = has('--strict');
1199
+ const targets = []; // { id, source, meta, body }
1200
+ const seen = new Set();
1201
+ const dir = userSkillsDir(root);
1202
+ const wantAll = !id || has('--all');
1203
+ const pushInstalled = (sid) => {
1204
+ const md = path.join(dir, sid, 'SKILL.md');
1205
+ const sj = path.join(dir, sid, 'skill.json');
1206
+ if (exists(md)) { const p = _parseSkillMd(read(md)); targets.push({ id: sid, source: 'SKILL.md', meta: p.meta, body: p.body }); seen.add(sid); return true; }
1207
+ if (exists(sj)) { try { const j = JSON.parse(read(sj)); targets.push({ id: sid, source: 'skill.json', meta: { name: j.name || sid, description: j.description || '' }, body: '' }); seen.add(sid); return true; } catch {} }
1208
+ return false;
1209
+ };
1210
+ if (wantAll) {
1211
+ if (exists(dir)) for (const e of fs.readdirSync(dir, { withFileTypes: true })) if (e.isDirectory()) pushInstalled(e.name);
1212
+ for (const bid of Object.keys(BUILTIN_CATALOG)) if (!seen.has(bid)) { const d = BUILTIN_CATALOG[bid]; targets.push({ id: bid, source: 'builtin', meta: { name: bid, description: d.description || '' }, body: '' }); }
1213
+ } else {
1214
+ if (!pushInstalled(id)) {
1215
+ if (BUILTIN_CATALOG[id]) { const d = BUILTIN_CATALOG[id]; targets.push({ id, source: 'builtin', meta: { name: id, description: d.description || '' }, body: '' }); }
1216
+ else { failJson(jsonMode, 'skill_not_found', `스킬 없음: ${id} (설치 스킬 또는 내장 카탈로그에 없음)`); return; }
1217
+ }
1218
+ }
1219
+ const results = targets.map(t => ({ id: t.id, source: t.source, ...(_lintSkillMeta(t.meta, t.body)) }));
1220
+ const errN = results.reduce((s, r) => s + r.errors.length, 0);
1221
+ const warnN = results.reduce((s, r) => s + r.warnings.length, 0);
1222
+ const failed = errN > 0 || (strict && warnN > 0);
1223
+ if (failed) process.exitCode = 1;
1224
+ if (jsonMode) { log(JSON.stringify({ version: VERSION, total: results.length, errors: errN, warnings: warnN, strict, ok: !failed, results }, null, 2)); return; }
1225
+ log(`# leerness skill lint — ${results.length}개 (ERROR ${errN} · WARNING ${warnN}${strict ? ' · --strict' : ''})`);
1226
+ for (const r of results) {
1227
+ const flat = [...r.errors.map(x => ['E', x]), ...r.warnings.map(x => ['W', x]), ...r.infos.map(x => ['i', x])];
1228
+ if (!flat.length) { ok(`${r.id} (${r.source}) — clean`); continue; }
1229
+ log(`\n## ${r.id} (${r.source})`);
1230
+ for (const [lv, x] of flat) log(` ${lv === 'E' ? '✗' : lv === 'W' ? '⚠' : 'ⓘ'} [${x.code}] ${x.msg}`);
1231
+ }
1232
+ log('');
1233
+ log(failed ? `✗ lint 실패 (ERROR ${errN}${strict ? ` + strict WARNING ${warnN}` : ''})` : `✓ lint 통과 (ERROR 0${warnN ? ` · WARNING ${warnN}건은 exit 에 미반영 — --strict 로 승격` : ''})`);
1234
+ }
1191
1235
  function userSkillFile(root, id) { return path.join(userSkillsDir(root), id, 'skill.json'); }
1192
1236
 
1193
1237
  function loadUserSkill(root, id) {
@@ -2983,6 +3027,27 @@ function _selfTestCases() {
2983
3027
  const encOk = s.includes("(result.applied || []).some(a => a.action === 'failed')) process.exitCode = 1"); // encoding: 실패 exit1
2984
3028
  return reuseOk && releaseOk && encOk;
2985
3029
  } },
3030
+ { name: 'skill lint (1.36.26, obra/superpowers P1/P2): 2티어 severity(ERROR 만 exit1) + 한국어 트리거절 + CJK 본문예산 분기 — 순수 행위검사', run: () => {
3031
+ const p = require('../lib/pure-utils');
3032
+ if (typeof p._lintSkillMeta !== 'function') return false;
3033
+ // ERROR: name/description 기계 판정
3034
+ const e1 = p._lintSkillMeta({ name: 'bad-one' }, 'b');
3035
+ const errMissing = e1.errors.some(x => x.code === 'description_missing');
3036
+ const errCharset = p._lintSkillMeta({ name: 'Bad_Name!', description: 'x'.repeat(50) }, 'b').errors.some(x => x.code === 'name_charset');
3037
+ // clean 케이스: 트리거 서술(ko) → ERROR/WARNING/no_trigger 전부 0 (false-BLOCK 방지 핵심)
3038
+ const clean = p._lintSkillMeta({ name: 'ok-skill', description: '문서 산출물이 필요할 때 사용 — Word 파일을 만들거나 편집할 때 로드하는 스킬. Use when producing documents.' }, '본문');
3039
+ const cleanOk = clean.errors.length === 0 && clean.warnings.length === 0 && !clean.infos.some(i => i.code === 'no_trigger_clause');
3040
+ // WARNING 은 exit 판정과 분리(짧은 설명) + CJK lang 분기
3041
+ const w1 = p._lintSkillMeta({ name: 'w', description: '짧은 설명' }, 'b');
3042
+ const warnShort = w1.errors.length === 0 && w1.warnings.some(x => x.code === 'description_too_short');
3043
+ const cjkLang = p._lintSkillMeta({ name: 'a', description: 'x'.repeat(50) }, '한글본문 '.repeat(50)).lang === 'cjk';
3044
+ // 내장 카탈로그 9종이 lint clean(1.36.25 데이터 선행 조건 검증 — P0 없이 lint 먼저면 9/9 실패였음)
3045
+ const c = require('../lib/catalogs');
3046
+ const builtinClean = Object.keys(c.BUILTIN_CATALOG).every(k => p._lintSkillMeta({ name: k, description: c.BUILTIN_CATALOG[k].description || '' }, '').errors.length === 0);
3047
+ // CLI 배선
3048
+ const wired = typeof skillLintCmd === 'function' && read(__filename).includes("args[1] === 'lint'");
3049
+ return errMissing && errCharset && cleanOk && warnShort && cjkLang && builtinClean && wired;
3050
+ } },
2986
3051
  { name: 'skill 트리거 description (1.36.25, obra/superpowers P0): BUILTIN_CATALOG 9/9 description(트리거 서술) + export/publish 합성 우선순위 description 우선 — 행위검사', run: () => {
2987
3052
  const c = require('../lib/catalogs');
2988
3053
  const ids = Object.keys(c.BUILTIN_CATALOG);
@@ -21308,6 +21373,7 @@ async function main() {
21308
21373
  if (cmd === 'readme' && args[1] === 'sync') return readmeCmd(arg('--path', args[2] || process.cwd()));
21309
21374
  if (cmd === 'consistency' && args[1] === 'check') return consistencyCheck(arg('--path', args[2] || process.cwd()));
21310
21375
  if (cmd === 'consistency' && args[1] === 'merge-design-guide') return mergeDesign(arg('--path', args[2] || process.cwd()));
21376
+ if (cmd === 'skill' && args[1] === 'lint') return skillLintCmd(arg('--path', args[2] && !args[2].startsWith('-') && /^([A-Za-z]:[\\/]|\/|\.\.?[\\/])/.test(args[2]) ? args[2] : process.cwd()), args[2] && !args[2].startsWith('-') && !/^([A-Za-z]:[\\/]|\/|\.\.?[\\/])/.test(args[2]) ? args[2] : null); // 1.36.26: 메타 품질 lint
21311
21377
  if (cmd === 'skill' && args[1] === 'list') return skillList(args[2] || arg('--path', process.cwd()));
21312
21378
  if (cmd === 'skill' && args[1] === 'info') return skillInfo(args[2], absRoot(arg('--path', process.cwd())));
21313
21379
  if (cmd === 'skill' && args[1] === 'add') return addSkill(absRoot(arg('--path', process.cwd())), args[2]);
package/lib/pure-utils.js CHANGED
@@ -461,6 +461,42 @@ function _parseSkillMd(text) {
461
461
  return { meta, body: m[2] };
462
462
  }
463
463
 
464
+ // 1.36.26 (obra/superpowers P1): SKILL 메타 품질 lint — 순수함수(의존 0), _parseSkillMd 산출(meta, body) 을 받는다.
465
+ // 설계 원칙(메모리 교훈 — 게이트 휴리스틱은 false-PASS 로 편향, false-BLOCK 금지):
466
+ // · ERROR = 기계적 판정만(FP 0 이 확실한 것) — 이것만 exit 1 대상.
467
+ // · WARNING = 형태 검사(저FP — displayNameKo 합성 흔적 등). 기본 exit 0, --strict 시에만 승격.
468
+ // · INFO = 주관적 권고(트리거절 부재 등) — 절대 실패 아님.
469
+ // 한국어 적응: 트리거절은 ko("~할 때 사용"/"언제 사용")·en("Use when/for") 모두 인정. 3인칭 검사는 한국어(인칭 무표지)에
470
+ // 부적용이라 제외. 본문 예산은 CJK 분기 — 공백분리 단어수를 한국어에 쓰면 어절 카운트라 전면 오발(비CJK=단어수, CJK=char).
471
+ function _lintSkillMeta(meta, body, opts = {}) {
472
+ const errors = [], warnings = [], infos = [];
473
+ const m = meta || {};
474
+ const b = String(body || '');
475
+ const name = String(m.name || '').trim();
476
+ const desc = String(m.description || '').trim();
477
+ // ERROR — 기계적
478
+ if (!name) errors.push({ code: 'name_missing', msg: 'frontmatter name 필수' });
479
+ else if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) errors.push({ code: 'name_charset', msg: `name 은 소문자/숫자/하이픈만: "${name}"` });
480
+ if (!desc) errors.push({ code: 'description_missing', msg: 'frontmatter description 필수 — 에이전트가 "지금 로드할까"를 판단하는 유일한 신호' });
481
+ else if (desc.length > 1024) errors.push({ code: 'description_too_long', msg: `description ${desc.length}자 > 1024 (트리거 조건만 담을 것)` });
482
+ // WARNING — 저FP 형태
483
+ if (desc && name && desc.toLowerCase() === name.toLowerCase()) warnings.push({ code: 'description_repeats_name', msg: 'description 이 name 반복 — 트리거 조건이 없음' });
484
+ if (desc && desc.length < 40) warnings.push({ code: 'description_too_short', msg: `description ${desc.length}자 < 40 — 표시명 합성 흔적("~스킬" 류)일 가능성` });
485
+ if (desc && /^[^,]+(, [^,]+){2,}$/.test(desc) && !/(사용|때|Use when|use this)/i.test(desc)) warnings.push({ code: 'description_capability_list', msg: 'description 이 capabilities 나열 형태 — 트리거 조건으로 재작성 권장' });
486
+ // INFO — 주관적(절대 실패 아님)
487
+ const hasTrigger = /Use (when|this when|for)\b/i.test(desc) || /(할|일|하는|이런)\s*(때|경우)에?\s*사용|언제\s*사용|때\s*사용/.test(desc);
488
+ if (desc && !hasTrigger) infos.push({ code: 'no_trigger_clause', msg: '트리거절 없음 — "~할 때 사용" / "Use when …" 형태 권장' });
489
+ // 본문 예산 — CJK 분기 (Hangul/한자/가나 비율 30%+ 면 CJK 로 판단; 문자 기반)
490
+ const cjkChars = (b.match(/[ㄱ-힝一-鿿぀-ヿ]/g) || []).length;
491
+ const totalChars = b.replace(/\s/g, '').length;
492
+ const isCjk = totalChars > 0 && cjkChars / totalChars >= 0.3;
493
+ const wordCount = b.split(/\s+/).filter(Boolean).length;
494
+ const charCount = b.length;
495
+ if (isCjk) { if (charCount > 12000) infos.push({ code: 'body_budget', msg: `본문 ${charCount}자 (CJK 기준 12000자 초과) — 자주 로드되는 스킬이면 분할 권장` }); }
496
+ else if (wordCount > 5000) infos.push({ code: 'body_budget', msg: `본문 ${wordCount}단어 (5000 초과) — 분할 권장` });
497
+ return { errors, warnings, infos, wordCount, charCount, lang: isCjk ? 'cjk' : 'latin' };
498
+ }
499
+
464
500
  // 1.9.333 (UR-0025 심층): 순수 플랫폼 제약 매칭 — catalog + 텍스트 → 매칭 플랫폼/제약/제안 (fs 의존 0, catalog 주입).
465
501
  // 1.31.2 (UR-0010): optional lang ('en') → 영어 suggestion. 기본 'ko' (무회귀, selftest 2-arg 호출 보존).
466
502
  function _matchConstraints(catalog, text, lang) {
@@ -1105,7 +1141,7 @@ module.exports = {
1105
1141
  // 1.9.346 (UR-0025 심층): roadmap CSS 변수 빌더
1106
1142
  _roadmapTokenStyles,
1107
1143
  // 1.9.347 (UR-0025 심층): SKILL.md frontmatter 파서 (BOM-aware)
1108
- _parseSkillMd,
1144
+ _parseSkillMd, _lintSkillMeta,
1109
1145
  // 1.9.333 (UR-0025 심층): 순수 플랫폼 제약 매칭
1110
1146
  _matchConstraints,
1111
1147
  // 1.9.333 패턴 적용: 순수 도메인 매칭
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.36.25",
3
+ "version": "1.36.26",
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",