leerness 1.36.19 → 1.36.20

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.20 — 2026-07-14 — `--apply --json`이 실제로 적용하도록 수정 — mutation-before-output (codex fresh-QA #7, 3커맨드 재현확정)
4
+
5
+ codex fresh-surface QA(1.36.19 이연분)에서 확정한 **silent no-op** 버그 수정: `--json` 분기가 mutation "전"에 return 해, `--apply --json`이 적용을 보고하고도 실제로는 아무 변경도 안 하던 3개 커맨드.
6
+
7
+ - **`reuse autodetect --apply --json`** — 후보만 보고하고 reuse-map.md를 안 씀(bin:20307의 --json return이 20319의 --apply 앞). → apply를 출력 전에 수행하고 JSON에 `applied`/`added` 반영. 실측: `--apply --json` → `applied:true, added:2` + reuse-map 실제 기록(0→1건), `--apply`(no-json) 무회귀.
8
+ - **`release cleanup --apply --json`** — `apply:true, deleteCount:N`을 보고하고도 브랜치를 안 지움(15018의 --json return이 15051의 삭제 앞). → 삭제를 출력 전에 수행하고 JSON에 `deleted`/`deleteFailed` 반영 + 삭제 실패 시 `exitCode=1`. 실측: `--apply --keep 1 --json` → `deleted:3` + 브랜치 실제 삭제(4→1).
9
+ - **`env encoding --apply --json`** — read-only 파일 apply 실패를 `action:'failed'`로 기록만 하고 exit 0. → 실패 시 `process.exitCode=1`. 실측: read-only→exit 1, writable→exit 0.
10
+ - **검증**: selftest 290/290(신규 소스가드 — 3커맨드의 apply-before-output 구조 + 실패 exit 반영), 각 커맨드 재현확정(임시 프로젝트/git-repo/read-only 파일), 게이트 e2e, 게시본 클린룸.
11
+ - **이연(다음 라운드)**: codex #8(CRLF `plan.md` 태스크 파서 tasks:[] + 잘못된 JSON 스키마가 `--json` 경로 crash — 파서 서브시스템 별도 조사) + #6(`copyRec` lstatSync 심링크).
12
+
3
13
  ## 1.36.19 — 2026-07-14 — 전략 앵커(project-brief/plan Goal) 미작성 감지 — 인계 AI가 프로젝트 맥락을 못 받던 근본원인 표면화 (실사용 7 프로젝트 dogfood)
4
14
 
5
15
  **현장 관찰**: 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 스스로 이 갭을 전혀 경고하지 않았음(감지 부재).
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.19 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
128
+ 이 프로젝트는 Leerness v1.36.20 하네스를 사용합니다. 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.19는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
182
+ Leerness v1.36.20는 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.19는 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.19)** · 매 라운드 GitHub release/태그 생성 · _reports/는 비공개 보존.
203
+ 현재 누적: **70 라운드 (1.9.40 → 1.36.20)** · 매 라운드 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.19: 2026-07-14
241
+ Last synced by Leerness v1.36.20: 2026-07-14
242
242
  <!-- leerness:project-readme:end -->
243
243
 
package/bin/leerness.js CHANGED
@@ -33,7 +33,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
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.19';
36
+ const VERSION = '1.36.20';
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') 시 호스트 프로세스 오염.
@@ -2611,6 +2611,8 @@ function envCmd(root, sub) {
2611
2611
  }
2612
2612
  }
2613
2613
  }
2614
+ // 1.36.20 (codex fresh-QA #7): apply 실패(read-only EPERM 등)를 exit code 에 반영 — 종전엔 action:'failed' 를 기록만 하고 exit 0.
2615
+ if (apply && (result.applied || []).some(a => a.action === 'failed')) process.exitCode = 1;
2614
2616
  if (has('--json')) { log(JSON.stringify(result, null, 2)); return; }
2615
2617
  log(cy(`# leerness env encoding-check (1.9.241/242, UR-0014) — 셸 스크립트 인코딩 위험 감지`));
2616
2618
  log('');
@@ -2963,6 +2965,13 @@ function _selfTestCases() {
2963
2965
  const realDecl = p._parseContractSpec('function realFn(){}\n').declared.includes('realFn'); // 회귀: 펜스 밖 선언 유지
2964
2966
  return bracket && dotStill && fenceExcluded && realDecl;
2965
2967
  } },
2968
+ { name: 'apply-before-output mutation 순서 (1.36.20, codex fresh-QA #7): reuse/release --apply 가 --json return 전 수행 + 실패 exit code — 소스가드', run: () => {
2969
+ const s = read(__filename);
2970
+ const reuseOk = s.includes('project: path.basename(root), found, applied, added') && s.includes("failJson(has('--json'), 'no_reuse_map'"); // reuse: apply 먼저 → json 에 applied/added
2971
+ const releaseOk = s.includes('deleteCount: toDelete.length, deleted, deleteFailed') && s.includes('if (deleteFailed > 0) process.exitCode = 1'); // release: 삭제 먼저 → json 에 deleted
2972
+ const encOk = s.includes("(result.applied || []).some(a => a.action === 'failed')) process.exitCode = 1"); // encoding: 실패 exit1
2973
+ return reuseOk && releaseOk && encOk;
2974
+ } },
2966
2975
  { name: 'ID 리더 5자리+ 대응 (1.36.18, UR-0052 P1-2): \\d{4}→\\d{4,} — 10k+ ID(T-10000) truncation 방지, 날짜 연도는 exactly-4 보존 — 행위검사', run: () => {
2967
2976
  // 행위: 5자리 ID 전체 캡처(구 \d{4}는 앞 4자리만 → truncation/충돌), 4자리 ID 무회귀, 날짜 연도 exactly-4 유지.
2968
2977
  const taskRe = /\bT-(\d{4,})\b/;
@@ -15014,11 +15023,21 @@ function releaseCleanupCmd(root) {
15014
15023
  });
15015
15024
  const recent = mergedBranches.slice(0, keep);
15016
15025
  const toDelete = mergedBranches.slice(keep).filter(b => b !== currentBranch);
15026
+ // 1.36.20 (codex fresh-QA #7): 삭제(mutation)를 출력(json/human) "전"에 수행. 종전엔 --json 분기가 삭제 전 return 해
15027
+ // `release cleanup --apply --json` 이 apply:true·deleteCount N 을 보고하고도 실제로 아무것도 안 지우던 silent no-op.
15028
+ let deleted = 0, deleteFailed = 0;
15029
+ if (apply) {
15030
+ for (const b of toDelete) {
15031
+ const r = cp.spawnSync('git', ['branch', '-d', b], { cwd: root, encoding: 'utf8' });
15032
+ if (r.status === 0) deleted++; else deleteFailed++;
15033
+ }
15034
+ if (deleteFailed > 0) process.exitCode = 1; // 실패 시 nonzero exit
15035
+ }
15017
15036
  // 5) JSON
15018
15037
  if (has('--json')) {
15019
15038
  log(JSON.stringify({
15020
15039
  apply, keep, total: allBranches.length, merged: mergedBranches.length,
15021
- unmerged: unmergedBranches.length, deleteCount: toDelete.length,
15040
+ unmerged: unmergedBranches.length, deleteCount: toDelete.length, deleted, deleteFailed,
15022
15041
  toDelete, recent: recent.slice(0, keep), unmergedSample: unmergedBranches.slice(0, 5)
15023
15042
  }, null, 2));
15024
15043
  return;
@@ -15049,13 +15068,7 @@ function releaseCleanupCmd(root) {
15049
15068
  if (recent.length > 5) log(dm(` ... +${recent.length - 5}개`));
15050
15069
  log('');
15051
15070
  if (apply) {
15052
- let ok = 0, fail = 0;
15053
- for (const b of toDelete) {
15054
- const r = cp.spawnSync('git', ['branch', '-d', b], { cwd: root, encoding: 'utf8' });
15055
- if (r.status === 0) ok++;
15056
- else fail++;
15057
- }
15058
- log(gr(` ✓ 삭제 완료: ${ok}/${toDelete.length}건`) + (fail > 0 ? rd(` · 실패 ${fail}`) : ''));
15071
+ log(gr(` ✓ 삭제 완료: ${deleted}/${toDelete.length}건`) + (deleteFailed > 0 ? rd(` · 실패 ${deleteFailed}`) : '')); // 1.36.20: 삭제는 출력 전 수행됨(#7)
15059
15072
  } else {
15060
15073
  log(dm(` → 적용: leerness release cleanup --apply (안전: merged만 삭제, 현재 branch 보호)`));
15061
15074
  log(dm(` → --keep N: 최근 N개 유지 (default 5)`));
@@ -20304,37 +20317,39 @@ function reuseAutodetectCmd(root) {
20304
20317
  }
20305
20318
  }
20306
20319
  }
20307
- if (has('--json')) {
20308
- log(JSON.stringify({ project: path.basename(root), found }, null, 2));
20309
- return;
20310
- }
20311
- log(`# leerness reuse autodetect (1.9.35)`);
20312
- log(`project: ${path.basename(root)}`);
20313
- log(`발견된 capability 후보: ${found.length}건`);
20314
- log('');
20315
- log('| Capability | Where | Kind | Note |');
20316
- log('|---|---|---|---|');
20317
- for (const c of found) log(`| ${c.name} | ${c.file} | util | (autodetect from module.exports) |`);
20318
- log('');
20320
+ // 1.36.20 (codex fresh-QA #7): --apply mutation 을 출력(human/json) "전"에 수행. 종전엔 --json 분기가 --apply 앞에서
20321
+ // return `reuse autodetect --apply --json` 이 후보만 보고하고 실제 reuse-map 을 안 쓰던 silent no-op. 이제 먼저 mutate
20322
+ // applied/added 를 human/json 양쪽에 반영. 에러도 --json 시 구조화(failJson).
20323
+ let applied = false, added = 0;
20319
20324
  if (has('--apply')) {
20320
- // reuse-map.md에 추가 (헤더 보존 + 후보 라인 append)
20321
20325
  const reusePath = path.join(root, '.harness', 'reuse-map.md');
20322
20326
  if (!exists(reusePath)) {
20323
- fail(`.harness/reuse-map.md 없음 — leerness init 먼저 실행`);
20324
- return process.exit(1);
20327
+ failJson(has('--json'), 'no_reuse_map', `.harness/reuse-map.md 없음 — leerness init 먼저 실행`);
20328
+ return process.exit(process.exitCode || 1);
20325
20329
  }
20326
20330
  let body = read(reusePath);
20327
- let added = 0;
20328
20331
  for (const c of found) {
20329
20332
  if (body.includes(`| ${c.name} |`)) continue; // 이미 있음
20330
20333
  body += `| ${c.name} | ${c.file} | util | autodetect 1.9.35 |\n`;
20331
20334
  added++;
20332
20335
  }
20333
20336
  writeUtf8(reusePath, body);
20334
- log(`✓ ${added}건 reuse-map.md에 추가됨`);
20335
- } else {
20336
- log(`(--apply 로 reuse-map.md에 자동 추가)`);
20337
+ applied = true;
20338
+ }
20339
+ if (has('--json')) {
20340
+ log(JSON.stringify({ project: path.basename(root), found, applied, added }, null, 2));
20341
+ return;
20337
20342
  }
20343
+ log(`# leerness reuse autodetect (1.9.35)`);
20344
+ log(`project: ${path.basename(root)}`);
20345
+ log(`발견된 capability 후보: ${found.length}건`);
20346
+ log('');
20347
+ log('| Capability | Where | Kind | Note |');
20348
+ log('|---|---|---|---|');
20349
+ for (const c of found) log(`| ${c.name} | ${c.file} | util | (autodetect from module.exports) |`);
20350
+ log('');
20351
+ if (applied) log(`✓ ${added}건 reuse-map.md에 추가됨`);
20352
+ else log(`(--apply 로 reuse-map.md에 자동 추가)`);
20338
20353
  }
20339
20354
 
20340
20355
  // 1.9.165: leerness web — playwright bridge MVP (opt-in 의존성, 5능력 #1 보강)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.36.19",
3
+ "version": "1.36.20",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",