leerness 1.9.34 → 1.9.35

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,39 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.35 — 2026-05-17
4
+
5
+ **파이프라인 메타-감사에서 도출된 5개 개선 사항 통합**.
6
+
7
+ 이전 라운드(1.9.34)에서 멀티 에이전트 오케스트레이션 전체 파이프라인을 메타-검증한 결과 8개 개선점을 도출. 그 중 high-impact 5건을 1.9.35에 즉시 통합.
8
+
9
+ ### Added
10
+
11
+ - **`leerness contract verify <spec.md> <impl.js>`** (#3) — 사양 ↔ 구현 일치 검사.
12
+ - spec 문서에서 `function fooBar(` / `` `bar(` `` / `tick.<field>` 패턴 추출
13
+ - impl의 `module.exports`와 비교 → 누락된 함수/필드 보고
14
+ - `--json` 출력 지원, exit code 1 if 불일치 (CI 친화)
15
+ - 1.9.34 멀티 에이전트 검증에서 발견한 "tick 페이로드 필드명 불일치" 자동 차단
16
+ - **`leerness reuse autodetect [path]`** (#2) — `src/*.js`의 `module.exports`를 스캔하여 reuse-map.md 후보 자동 등록.
17
+ - `_internal` 헬퍼는 제외 (밑줄로 시작하는 export 자동 필터)
18
+ - `--apply`로 reuse-map.md에 자동 추가, 기본은 dry-run
19
+ - **`leerness audit --fix`** (#5) — 누락된 메타 파일 자동 갱신.
20
+ - `session-handoff.md`의 `Last generated: (자동)` → 실제 타임스탬프
21
+ - `current-state.md`의 stale `Updated` 라인 → today로 갱신
22
+ - `--fix` 미지정 시 기존 경고 동작 유지 (안전한 opt-in)
23
+ - **`handoff <path>` .harness 부재 자동 경고** (#1) — 신규 디렉토리에서 handoff 호출 시 즉시 노랑색 경고 + `leerness init` 명령 안내. `--no-init-check` 또는 `--all-apps` 시 스킵.
24
+ - **`agents dispatch` 안내문에 안전 규칙 추가** (#4) — 멀티 에이전트 분배 시 파일 경로 격리, mtime 검증 요구, contract verify 권장을 안내문에 자동 포함.
25
+
26
+ ### Policy
27
+ - ✅ 모든 신규 명령은 기본 read-only · destructive 동작은 명시적 플래그(`--fix`, `--apply`) 필요
28
+ - ✅ 1.9.34 멀티 에이전트 검증의 모범 사례(파일 경로 격리, mtime 자기 검증, 사양 사전 합의)를 도구로 코드화
29
+ - ❌ 자동 init은 destructive이므로 자동 실행 안 함 — 사용자에게 명령만 안내
30
+
31
+ ### 실측 (이번 라운드)
32
+ - 메타-감사 보고서: `_reports/PIPELINE_META_AUDIT.md` (10 phases, 8 개선점)
33
+ - rpg-replay 통합 패치: 회귀 0건 · 128/128 PASS · BUG-A/B/C 모두 해결 (별도 라운드)
34
+ - contract verify 실 사용 사례: format.js에 spec의 `tick.effect` 필드 없음 발견
35
+ - e2e: 161/161 PASS (1.9.34 156 + 신규 5)
36
+
3
37
  ## 1.9.34 — 2026-05-16
4
38
 
5
39
  **방향키 + 스페이스 인터랙티브 multi-select + 256색 그라데이션 배너 + 멀티레벨 sub-agent 오케스트레이션 검증**.
package/README.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # Leerness
2
2
 
3
- > **한국어 우선 AI 개발 하네스** — AI 에이전트가 거짓 완료 보고 없이, 안전하게 코드를 쓰도록 만드는 CLI 도구.
3
+ > **AI 에이전트 검수·다중 협업 CLI 하네스** — 거짓 완료 차단, 멀티 에이전트 오케스트레이션, 사양 구현 일치 검증, 워크스페이스 통합 가시성. **한국어 우선**.
4
4
 
5
- [![npm](https://img.shields.io/badge/npm-leerness-blue)](https://www.npmjs.com/package/leerness) [![version](https://img.shields.io/badge/version-1.9.34-green)]() [![tests](https://img.shields.io/badge/e2e-155%2F156-success)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
5
+ [![npm](https://img.shields.io/badge/npm-leerness-blue)](https://www.npmjs.com/package/leerness) [![version](https://img.shields.io/badge/version-1.9.35-green)]() [![tests](https://img.shields.io/badge/e2e-161%2F161-success)]() [![license](https://img.shields.io/badge/license-MIT-lightgrey)]()
6
+
7
+ > Claude Code · Cursor · Copilot · Codex · Gemini CLI — 어떤 AI 에이전트로 코드를 짜든, leerness는 **"진짜로 했나? 중복 아닌가? 합의된 사양인가?"** 를 자동으로 검수합니다.
6
8
 
7
9
  ```
8
10
  ╔══════════════════════════════════════════════════════════════╗
@@ -22,19 +24,24 @@
22
24
 
23
25
  ---
24
26
 
25
- ## 🤔 leerness가 무엇인가요?
27
+ ## 🤔 leerness가 해결하는 문제
26
28
 
27
- AI 에이전트(Claude Code, Cursor, Copilot, Codex, Gemini CLI 등) 함께 일할 발생하는 **반복되는 함정**을 자동으로 막아주는 한국어 우선 CLI입니다.
29
+ AI 에이전트(Claude Code, Cursor, Copilot, Codex, Gemini CLI 등) **빠르게 코드를 쓰지만, 다음 함정에 반복해서 빠집니다**:
28
30
 
29
- | AI와 함께 일할 때 흔한 문제 | leerness의 해결책 |
31
+ | AI와 함께 일할 때 흔한 함정 | leerness의 해결책 |
30
32
  |---|---|
31
33
  | 🚨 "완료했습니다"라고 했지만 실제로 코드에 변경이 없음 | `verify-claim --run-tests` — 증거 파일 존재 + 테스트 실제 실행 검증 |
32
34
  | 🚨 "API 호출 완료" 라고 했지만 코드에 URL이 없음 | `optimism-check` — 10 카테고리 패턴 + URL 매핑 + 신뢰도 점수 |
33
35
  | 🚨 같은 함수를 여러 프로젝트에 중복 생성 | `reuse-map --all-apps --strict-elements` — 함수명 fuzzy 중복 감지 |
34
- | 🚨 다음 세션이 컨텍스트를 잃음 | `handoff` — 3채널(plan/progress/decisions) 자동 적재 + `--compact` (500자) |
36
+ | 🚨 다음 세션이 컨텍스트를 잃음 | `handoff` — plan/progress/decisions 3채널 자동 적재 + `--compact` (500자) |
35
37
  | 🚨 표면적 코드 리뷰 (도메인 깊이 부족) | `review --persona security,performance,ux` — 5 도메인 sub-agent 자동 부여 |
36
- | 🚨 외부 AI CLI를 사용하고 싶지만 자동 호출은 위험 | `agents list/dispatch/quota` — 환경변수 활성화 + 명시적 분배 (자동 호출 X) |
37
- | 🚨 npx 캐시로 옛 버전이 실행됨 | `1.9.33`+ `_warnIfStale` — install 시 npm latest 자동 비교 + 경고 |
38
+ | 🚨 외부 AI CLI를 자동 호출하면 위험 | `agents list/dispatch/quota` — 환경변수 활성화 + 명시적 분배 (자동 호출 X) |
39
+ | 🚨 npx 캐시로 옛 버전이 실행됨 | `_warnIfStale` — install 시 npm latest 자동 비교 + 경고 (1.9.33+) |
40
+ | 🚨 멀티 sub-agent가 같은 파일을 동시에 써서 작업 손실 | 프롬프트 템플릿에 **파일 경로 격리 의무**, `agents dispatch` 안내문이 자동 추가 (1.9.34/35) |
41
+ | 🚨 sub-agent마다 사양 해석이 달라 통합 시 BUG 양산 | **`leerness contract verify <spec.md> <impl.js>`** — 명세 ↔ 구현 함수/필드 자동 검사 (1.9.35) |
42
+ | 🚨 신규 모듈의 capability가 reuse-map에 등록 안 됨 | **`leerness reuse autodetect`** — `module.exports` 스캔 + 자동 등록 (1.9.35) |
43
+ | 🚨 신규 디렉토리에서 sub-agent가 컨텍스트 없이 작업 시작 | `handoff` 호출 시 **.harness 부재 자동 경고** (1.9.35) |
44
+ | 🚨 audit warning이 쌓이지만 수동 fix가 번거로움 | **`audit --fix`** — session-handoff/current-state 자동 갱신 (1.9.35) |
38
45
 
39
46
  ---
40
47
 
@@ -263,12 +270,15 @@ leerness brainstorm "키워드" --all-apps --include-code
263
270
  leerness deps <capability> --run-tests # 영향 추적 + 자동 회귀
264
271
  ```
265
272
 
266
- ### 외부 AI CLI (1.9.30~34)
273
+ ### 외부 AI CLI · 멀티 에이전트 (1.9.30~35)
267
274
  ```bash
268
275
  leerness setup-agents . # 인터랙티브 활성화 + 자동 설치
269
276
  leerness agents list # 4 CLI 상태표
270
277
  leerness agents quota # 사용량 추정
271
- leerness agents dispatch "<task>" --to gemini # 명령 생성
278
+ leerness agents dispatch "<task>" --to gemini # 명령 생성 (안전 규칙 포함)
279
+ leerness contract verify <spec.md> <impl.js> # 1.9.35 명세 ↔ 구현 일치 검사
280
+ leerness reuse autodetect [path] [--apply] # 1.9.35 capability 자동 등록
281
+ leerness audit . --fix # 1.9.35 누락 메타 자동 갱신
272
282
  ```
273
283
 
274
284
  ### 페르소나·리뷰 (1.9.29)
@@ -379,6 +389,10 @@ leerness update --from <tgz> # 오프라인/사내 미러
379
389
  | `agents dispatch --to X` | ready CLI에 대상 명령 자동 생성 | 1.9.30 |
380
390
  | `agents quota` | provider별 사용량/한도 추정 | 1.9.31 |
381
391
  | `setup-agents` | 인터랙티브 CLI 활성화 + 자동 설치 시도 | 1.9.32 |
392
+ | `contract verify <spec> <impl>` | 명세 ↔ 구현 함수/필드 일치 자동 검사 | 1.9.35 |
393
+ | `reuse autodetect [--apply]` | `module.exports` 스캔 → reuse-map 후보 등록 | 1.9.35 |
394
+ | `audit --fix` | session-handoff/current-state 자동 갱신 | 1.9.35 |
395
+ | `handoff` (init 부재 경고) | 신규 디렉토리에서 즉시 init 안내 | 1.9.35 |
382
396
  | `_warnIfStale` (자동) | npx 캐시 옛 버전 자동 경고 | 1.9.33 |
383
397
  | `_selectOne/_selectMany` | 방향키/Space 인터랙티브 multi-select | 1.9.34 |
384
398
 
@@ -450,12 +464,13 @@ A. 256색 ANSI를 지원하지 않는 환경입니다. `LEERNESS_NO_INTERACTIVE=
450
464
  npm test # = node ./scripts/e2e.js
451
465
  ```
452
466
 
453
- **155/156 시나리오** 통과 (1.9.7~1.9.34 회귀 + 신규 검증).
467
+ **161/161 시나리오** 통과 (1.9.7~1.9.35 회귀 + 신규 검증).
454
468
 
455
469
  ---
456
470
 
457
471
  ## 📜 변경 이력 (최근)
458
472
 
473
+ - **1.9.35** — 파이프라인 메타-감사에서 도출된 5개 개선 통합: `contract verify` · `reuse autodetect` · `audit --fix` · `handoff` init 부재 경고 · `agents dispatch` 안전 규칙 안내.
459
474
  - **1.9.34** — 방향키/스페이스 인터랙티브 multi-select (`_selectOne`/`_selectMany`) + 256색 그라데이션 배너 + 3단계 sub-agent 오케스트레이션 검증 (2.2× 효율 실측).
460
475
  - **1.9.33** — `_warnIfStale()` — `npx leerness init` 시 옛 버전 자동 경고 + 해결 안내.
461
476
  - **1.9.32** — ASCII 배너 + `setup-agents` 인터랙티브 설정 + 미설치 CLI 자동 설치 시도.
package/bin/harness.js CHANGED
@@ -6,7 +6,7 @@ const path = require('path');
6
6
  const cp = require('child_process');
7
7
  const readline = require('readline');
8
8
 
9
- const VERSION = '1.9.34';
9
+ const VERSION = '1.9.35';
10
10
  const MARK = '<!-- leerness:managed -->';
11
11
  const README_START = '<!-- leerness:project-readme:start -->';
12
12
  const README_END = '<!-- leerness:project-readme:end -->';
@@ -960,6 +960,9 @@ function debug(root) {
960
960
  function audit(root) {
961
961
  root = absRoot(root);
962
962
  let warnings = 0, failures = 0;
963
+ // 1.9.35 개선 #5: --fix 옵션 — 자동 수정 가능한 항목 적용
964
+ const fix = has('--fix');
965
+ let fixed = 0;
963
966
  const designCands = ['designguide.md','design-guide.md','docs/designguide.md','docs/design-guide.md','.harness/designguide.md'];
964
967
  const dups = designCands.filter(f => exists(path.join(root,f)));
965
968
  if (dups.length) { warnings++; warn(`design guide duplicates outside canonical: ${dups.join(', ')} (run: leerness consistency merge-design-guide)`); }
@@ -991,16 +994,34 @@ function audit(root) {
991
994
  }
992
995
  else if (milestoneIds.length) ok('all milestones linked in progress-tracker');
993
996
  const handoff = exists(handoffPath(root)) ? read(handoffPath(root)) : '';
994
- if (handoff.includes('Last generated: (자동)')) { warnings++; warn('session-handoff.md never auto-generated (run: leerness session close .)'); }
997
+ if (handoff.includes('Last generated: (자동)')) {
998
+ warnings++; warn('session-handoff.md never auto-generated (run: leerness session close .)');
999
+ // 1.9.35 #5: --fix → session-handoff.md 자동 생성 마커 갱신
1000
+ if (fix) {
1001
+ const stamped = handoff.replace('Last generated: (자동)', `Last generated: ${today()} (leerness audit --fix)`);
1002
+ writeUtf8(handoffPath(root), stamped);
1003
+ ok(' ↳ fixed: session-handoff.md timestamp 갱신');
1004
+ fixed++;
1005
+ }
1006
+ }
995
1007
  else if (handoff.includes('Last generated:')) ok('session-handoff.md auto-generated previously');
996
1008
  const cur = exists(currentStatePath(root)) ? read(currentStatePath(root)) : '';
997
1009
  const updMatch = cur.match(/Updated: (\d{4}-\d{2}-\d{2})/);
998
1010
  if (updMatch) {
999
1011
  const dDays = (Date.now() - new Date(updMatch[1]).getTime()) / 86400000;
1000
- if (dDays > 7) { warnings++; warn(`current-state.md stale (${Math.round(dDays)} days)`); }
1012
+ if (dDays > 7) {
1013
+ warnings++; warn(`current-state.md stale (${Math.round(dDays)} days)`);
1014
+ // 1.9.35 #5: --fix → current-state.md Updated 라인 갱신
1015
+ if (fix) {
1016
+ const stamped = cur.replace(/Updated: \d{4}-\d{2}-\d{2}/, `Updated: ${today()}`);
1017
+ writeUtf8(currentStatePath(root), stamped);
1018
+ ok(' ↳ fixed: current-state.md Updated 갱신');
1019
+ fixed++;
1020
+ }
1021
+ }
1001
1022
  else ok('current-state.md fresh');
1002
1023
  }
1003
- log(`Audit summary: warnings=${warnings} failures=${failures}`);
1024
+ log(`Audit summary: warnings=${warnings} failures=${failures}${fix ? ` fixed=${fixed}` : ''}`);
1004
1025
  if (failures) process.exitCode = 1;
1005
1026
  }
1006
1027
 
@@ -1411,6 +1432,24 @@ function handoffCmd(root) {
1411
1432
  if (has('--all-apps') || arg('--include', null)) {
1412
1433
  return _handoffWorkspace(absRoot(root));
1413
1434
  }
1435
+ // 1.9.35 개선 #1: .harness 부재 시 즉시 경고 (자동 init 권장)
1436
+ // 사용자가 신규 디렉토리에서 handoff 호출 시 sub-agent 작업이 길을 잃지 않도록.
1437
+ const absR = absRoot(root || process.cwd());
1438
+ if (!exists(path.join(absR, '.harness')) && !has('--no-init-check')) {
1439
+ const isTty = process.stdout && process.stdout.isTTY;
1440
+ const yel = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
1441
+ const dim = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
1442
+ log('');
1443
+ log(yel(' ⚠ leerness init 미실행 디렉토리'));
1444
+ log(dim(' ' + absR));
1445
+ log(dim(' handoff가 표시할 컨텍스트(plan/progress/decisions)가 없습니다.'));
1446
+ log('');
1447
+ log(dim(' 해결:'));
1448
+ log(' ' + yel(`leerness init "${absR}" --yes --language ko`));
1449
+ log('');
1450
+ log(dim(' (--no-init-check 로 끄기)'));
1451
+ log('');
1452
+ }
1414
1453
  return handoff(root);
1415
1454
  }
1416
1455
 
@@ -2793,6 +2832,12 @@ function agentsCmd(root, sub, ...args) {
2793
2832
  log(` - leerness는 외부 CLI를 자동 호출하지 않음 (사용자 명시적 실행)`);
2794
2833
  log(` - 메인 에이전트(Claude)가 위 명령을 보고 sub-agent로 spawn 가능`);
2795
2834
  log(` - quota 체크: \`leerness agents quota\` (1.9.31+)`);
2835
+ log('');
2836
+ log(`## 분배 시 안전 규칙 (1.9.35)`);
2837
+ log(` - sub-agent 프롬프트에 "당신만 수정할 파일 경로"를 명시 (파일 경로 격리)`);
2838
+ log(` - sub-agent에 "보고 시 \`stat <file>\` 또는 mtime 확인 결과 첨부" 요구 (자기 격리 검증)`);
2839
+ log(` - 사양 사전 정의 (예: TICK_SPEC.md) → \`leerness contract verify\`로 사후 검증`);
2840
+ log(` - 같은 파일 동시 쓰기는 last-writer-wins 위험 (1.9.34 검증)`);
2796
2841
  return;
2797
2842
  }
2798
2843
 
@@ -5051,8 +5096,143 @@ function viewworkInstall(root) {
5051
5096
  ok('claude .claude/settings.local.json updated (Stop hook adds a viewwork event)');
5052
5097
  }
5053
5098
 
5099
+ // 1.9.35 개선 #3: contract verify <spec.md> <impl.js>
5100
+ // 사양 문서(spec.md)에 명시된 함수 이름이 실제 module.exports에 모두 있는지 검사.
5101
+ // 사용 예: leerness contract verify TICK_SPEC.md src/format.js
5102
+ function contractVerifyCmd(specPath, implPath) {
5103
+ if (!specPath || !implPath) { fail('사용법: leerness contract verify <spec.md> <impl.js>'); return process.exit(1); }
5104
+ const spec = absRoot('.') + path.sep; // dummy to avoid abs
5105
+ const specFile = path.resolve(specPath);
5106
+ const implFile = path.resolve(implPath);
5107
+ if (!exists(specFile)) { fail(`spec 파일 없음: ${specFile}`); return process.exit(1); }
5108
+ if (!exists(implFile)) { fail(`impl 파일 없음: ${implFile}`); return process.exit(1); }
5109
+ const specText = read(specFile);
5110
+ // spec에서 함수 이름 추출:
5111
+ // `function fooBar(...)` 형태 (markdown 코드블럭 내 JS)
5112
+ // 또는 `**fooBar**` (한국어 문서에서 함수명 강조)
5113
+ // 또는 `tick.amount` (필드명)
5114
+ const fnSpec = new Set();
5115
+ const fieldSpec = new Set();
5116
+ // function 시그니처
5117
+ for (const m of specText.matchAll(/function\s+([A-Za-z_$][\w$]*)\s*\(/g)) fnSpec.add(m[1]);
5118
+ // backtick에 싸인 함수 호출 같은 형태: `xxx(`
5119
+ for (const m of specText.matchAll(/`([A-Za-z_$][\w$]*)\s*\(/g)) fnSpec.add(m[1]);
5120
+ // 필드: tick.<name>
5121
+ for (const m of specText.matchAll(/tick\.([A-Za-z_$][\w$]*)/g)) fieldSpec.add(m[1]);
5122
+ // impl require → exports 추출
5123
+ let exports;
5124
+ try {
5125
+ // 캐시 우회: delete from cache
5126
+ delete require.cache[implFile];
5127
+ exports = require(implFile);
5128
+ } catch (e) { fail(`impl 로드 실패: ${e.message}`); return process.exit(1); }
5129
+ const implExports = new Set();
5130
+ if (exports && typeof exports === 'object') {
5131
+ for (const k of Object.keys(exports)) implExports.add(k);
5132
+ }
5133
+ // 검사: spec에 명시된 함수 중 impl에 없는 것
5134
+ const missing = [];
5135
+ for (const fn of fnSpec) {
5136
+ // common function name (typeof === 'function'인 export만 비교)
5137
+ if (typeof exports[fn] === 'function') continue;
5138
+ // spec에 'function fnName('이 있지만 impl exports에 없으면 미구현
5139
+ // 단, 헬퍼 함수(internal _)는 spec에 없을 수 있으니 단방향만
5140
+ if (specText.includes(`function ${fn}`) && !implExports.has(fn)) missing.push(fn);
5141
+ }
5142
+ // 필드 검증: impl 소스 파일에 spec 필드명이 있는지 grep
5143
+ const implSrc = read(implFile);
5144
+ const fieldMissing = [];
5145
+ for (const f of fieldSpec) {
5146
+ if (!new RegExp(`\\b${f}\\b`).test(implSrc)) fieldMissing.push(f);
5147
+ }
5148
+ // 출력
5149
+ if (has('--json')) {
5150
+ log(JSON.stringify({
5151
+ spec: specFile, impl: implFile,
5152
+ specFunctions: [...fnSpec], specFields: [...fieldSpec],
5153
+ implExports: [...implExports],
5154
+ missingFunctions: missing, missingFields: fieldMissing,
5155
+ ok: missing.length === 0 && fieldMissing.length === 0
5156
+ }, null, 2));
5157
+ return;
5158
+ }
5159
+ log(`# leerness contract verify (1.9.35)`);
5160
+ log(`spec: ${rel(process.cwd(), specFile)}`);
5161
+ log(`impl: ${rel(process.cwd(), implFile)}`);
5162
+ log(``);
5163
+ log(`spec 명시 함수: ${[...fnSpec].join(', ') || '(없음)'}`);
5164
+ log(`spec 명시 필드: ${[...fieldSpec].join(', ') || '(없음)'}`);
5165
+ log(`impl exports: ${[...implExports].join(', ') || '(없음)'}`);
5166
+ log(``);
5167
+ if (missing.length) {
5168
+ log(`✗ 누락된 함수 (${missing.length}건):`);
5169
+ for (const m of missing) log(` - ${m}`);
5170
+ } else log(`✓ 모든 spec 함수가 impl에 존재`);
5171
+ if (fieldMissing.length) {
5172
+ log(`✗ 누락된 필드 (${fieldMissing.length}건):`);
5173
+ for (const m of fieldMissing) log(` - tick.${m}`);
5174
+ } else log(`✓ 모든 spec 필드가 impl 소스에 존재`);
5175
+ const ok = missing.length === 0 && fieldMissing.length === 0;
5176
+ log('');
5177
+ log(ok ? '✅ contract OK' : '❌ contract 불일치');
5178
+ if (!ok) process.exitCode = 1;
5179
+ }
5180
+
5181
+ // 1.9.35 개선 #2: reuse autodetect [path]
5182
+ // src/*.js의 module.exports를 스캔해서 reuse-map.md에 capability 후보 등록.
5183
+ function reuseAutodetectCmd(root) {
5184
+ root = absRoot(root || process.cwd());
5185
+ const srcDir = path.join(root, 'src');
5186
+ if (!exists(srcDir)) { fail(`src/ 디렉토리 없음: ${srcDir}`); return process.exit(1); }
5187
+ const files = fs.readdirSync(srcDir).filter(f => f.endsWith('.js'));
5188
+ const found = [];
5189
+ for (const f of files) {
5190
+ const full = path.join(srcDir, f);
5191
+ let mod;
5192
+ try { delete require.cache[full]; mod = require(full); } catch { continue; }
5193
+ if (!mod || typeof mod !== 'object') continue;
5194
+ const exports = Object.keys(mod).filter(k => typeof mod[k] === 'function');
5195
+ if (!exports.length) continue;
5196
+ for (const e of exports) {
5197
+ if (e.startsWith('_')) continue; // internal helpers 제외
5198
+ found.push({ file: `src/${f}`, name: e });
5199
+ }
5200
+ }
5201
+ if (has('--json')) {
5202
+ log(JSON.stringify({ project: path.basename(root), found }, null, 2));
5203
+ return;
5204
+ }
5205
+ log(`# leerness reuse autodetect (1.9.35)`);
5206
+ log(`project: ${path.basename(root)}`);
5207
+ log(`발견된 capability 후보: ${found.length}건`);
5208
+ log('');
5209
+ log('| Capability | Where | Kind | Note |');
5210
+ log('|---|---|---|---|');
5211
+ for (const c of found) log(`| ${c.name} | ${c.file} | util | (autodetect from module.exports) |`);
5212
+ log('');
5213
+ if (has('--apply')) {
5214
+ // reuse-map.md에 추가 (헤더 보존 + 후보 라인 append)
5215
+ const reusePath = path.join(root, '.harness', 'reuse-map.md');
5216
+ if (!exists(reusePath)) {
5217
+ fail(`.harness/reuse-map.md 없음 — leerness init 먼저 실행`);
5218
+ return process.exit(1);
5219
+ }
5220
+ let body = read(reusePath);
5221
+ let added = 0;
5222
+ for (const c of found) {
5223
+ if (body.includes(`| ${c.name} |`)) continue; // 이미 있음
5224
+ body += `| ${c.name} | ${c.file} | util | autodetect 1.9.35 |\n`;
5225
+ added++;
5226
+ }
5227
+ writeUtf8(reusePath, body);
5228
+ log(`✓ ${added}건 reuse-map.md에 추가됨`);
5229
+ } else {
5230
+ log(`(--apply 로 reuse-map.md에 자동 추가)`);
5231
+ }
5232
+ }
5233
+
5054
5234
  function help() {
5055
- log(`Leerness v${VERSION}\n\nUsage:\n leerness init [path] [--language auto|ko|en] [--skills recommended|all|a,b]\n leerness migrate [path] [--dry-run] [--force]\n leerness update [path] [--check|--yes|--force|--from <tarball>]\n leerness auto-update install [path]\n leerness status [path]\n leerness verify [path]\n leerness debug [path]\n leerness audit [path]\n leerness check [path]\n leerness scan secrets [path]\n leerness encoding check [path]\n leerness lazy detect [path]\n leerness memory search "query" [--limit 5]\n leerness handoff [path] [--all-apps] [--include p1,p2] [--since 24h|3d] [--compact] [--json] # 1.9.17-22 워크스페이스 (--compact: LLM 시스템 프롬프트용 1줄 요약)\n leerness orchestrate "<목표>" [--agents N] [--model qwen2.5:7b-instruct] [--retry-on-fail K] # 1.9.22 Ollama opt-in (LEERNESS_OLLAMA_BASE_URL 필요)\n leerness llm-bench record --score N --model X [--label L] [--tokens T] # 1.9.22 LLM 벤치 히스토리 누적\n leerness deps <capability> [--run-tests] [--json] # 1.9.24 depends-on 역방향 추적 + 자동 회귀 sweep\n leerness memory search "키" [--include-code] # 1.9.25 소스 코드 본문도 검색 (모순 감지 핵심)\n leerness brainstorm "주제" [--include-code] # 1.9.25 코드 본문 hits 포함\n leerness register-pending "<요청>" [--agent X] [--note Y] # 1.9.25 다중 세션 in-progress 즉시 등록\n leerness optimism-check <T-ID> [--json] # 1.9.26/27 낙관적 표시 감지 (1.9.27: 10 카테고리 + URL/메서드 매핑 + 신뢰도 점수)\n leerness persona list|show <id>|add <id> # 1.9.29 페르소나 카탈로그 (보안/성능/UX/testing/docs 5종 내장)\n leerness review <file> --persona <id1,id2,...> # 1.9.29 도메인 페르소나 리뷰 프롬프트 자동 생성\n leerness agents list|check|quota # 1.9.30/31 외부 AI CLI 가용성 + quota 추정 (claude/codex/gemini/copilot)\n leerness agents dispatch "<task>" --to <id> # 1.9.30 활성 CLI 대상 실행 명령 생성 (실 호출 X, 사용자 실행)\n leerness setup-agents [path] [--yes|--no-setup-agents] # 1.9.32 sub-agent CLI 인터랙티브 설정 (.env + 미설치 자동 설치)\n leerness init [path] [--no-stale-check] # 1.9.33 npx 캐시 함정 — 옛 버전 자동 경고 (끄려면 --no-stale-check)\n leerness verify-claim <T-ID> ... [--strict-claims] # 1.9.26 verify-claim에 낙관적 표시 자동 검사 통합\n leerness reuse-map [path] [--all-apps] [--include p1,p2] [--strict-elements] [--json] # 1.9.18 중복/잠재중복/depends-on\n leerness verify-claim <T-ID> [--path .] [--run-tests] [--json] # 1.9.18-20 evidence 자동 검증 (1.9.20: scenes/scripts 등 도메인 폴더 + jest/mocha 파싱)\n leerness verify-code [path] [--build] [--bench] # 1.9.20 --bench: scripts.bench 추가 실행 + evidence 누적\n leerness session close [path]\n leerness viewwork install [path]\n leerness viewwork emit [path] [--action a] [--note n] [--agent x] [--tool t]\n leerness route <task-type>\n leerness self check [path]\n leerness readme sync [path]\n leerness consistency check [path]\n leerness consistency merge-design-guide [path]\n leerness plan show|init|add|drop|progress|sync [args]\n leerness task list|add|update|drop|fix-evidence|relink [args]\n leerness skill list|info <name>\n leerness skill learn <id> --doc <url> --command "..." --capability "..." [--note ...]\n leerness skill use <id> [--note ...]\n leerness skill optimize <id> --before "..." --after "..." [--note ...]\n leerness skill remove <id>\n leerness skill consolidate [--threshold 0.3]\n leerness gate [path] # verify+audit+scan+encoding+lazy
5235
+ log(`Leerness v${VERSION}\n\nUsage:\n leerness init [path] [--language auto|ko|en] [--skills recommended|all|a,b]\n leerness migrate [path] [--dry-run] [--force]\n leerness update [path] [--check|--yes|--force|--from <tarball>]\n leerness auto-update install [path]\n leerness status [path]\n leerness verify [path]\n leerness debug [path]\n leerness audit [path]\n leerness check [path]\n leerness scan secrets [path]\n leerness encoding check [path]\n leerness lazy detect [path]\n leerness memory search "query" [--limit 5]\n leerness handoff [path] [--all-apps] [--include p1,p2] [--since 24h|3d] [--compact] [--json] # 1.9.17-22 워크스페이스 (--compact: LLM 시스템 프롬프트용 1줄 요약)\n leerness orchestrate "<목표>" [--agents N] [--model qwen2.5:7b-instruct] [--retry-on-fail K] # 1.9.22 Ollama opt-in (LEERNESS_OLLAMA_BASE_URL 필요)\n leerness llm-bench record --score N --model X [--label L] [--tokens T] # 1.9.22 LLM 벤치 히스토리 누적\n leerness deps <capability> [--run-tests] [--json] # 1.9.24 depends-on 역방향 추적 + 자동 회귀 sweep\n leerness memory search "키" [--include-code] # 1.9.25 소스 코드 본문도 검색 (모순 감지 핵심)\n leerness brainstorm "주제" [--include-code] # 1.9.25 코드 본문 hits 포함\n leerness register-pending "<요청>" [--agent X] [--note Y] # 1.9.25 다중 세션 in-progress 즉시 등록\n leerness optimism-check <T-ID> [--json] # 1.9.26/27 낙관적 표시 감지 (1.9.27: 10 카테고리 + URL/메서드 매핑 + 신뢰도 점수)\n leerness persona list|show <id>|add <id> # 1.9.29 페르소나 카탈로그 (보안/성능/UX/testing/docs 5종 내장)\n leerness review <file> --persona <id1,id2,...> # 1.9.29 도메인 페르소나 리뷰 프롬프트 자동 생성\n leerness agents list|check|quota # 1.9.30/31 외부 AI CLI 가용성 + quota 추정 (claude/codex/gemini/copilot)\n leerness agents dispatch "<task>" --to <id> # 1.9.30 활성 CLI 대상 실행 명령 생성 (실 호출 X, 사용자 실행)\n leerness setup-agents [path] [--yes|--no-setup-agents] # 1.9.32 sub-agent CLI 인터랙티브 설정 (.env + 미설치 자동 설치)\n leerness init [path] [--no-stale-check] # 1.9.33 npx 캐시 함정 — 옛 버전 자동 경고 (끄려면 --no-stale-check)\n leerness contract verify <spec.md> <impl.js> [--json] # 1.9.35 명세 ↔ 구현 일치 검사 (함수/필드)\n leerness reuse autodetect [path] [--apply] [--json] # 1.9.35 src/*.js의 module.exports → reuse-map 후보 등록\n leerness audit [path] [--fix] # 1.9.35 --fix: session-handoff/current-state 자동 갱신\n leerness verify-claim <T-ID> ... [--strict-claims] # 1.9.26 verify-claim에 낙관적 표시 자동 검사 통합\n leerness reuse-map [path] [--all-apps] [--include p1,p2] [--strict-elements] [--json] # 1.9.18 중복/잠재중복/depends-on\n leerness verify-claim <T-ID> [--path .] [--run-tests] [--json] # 1.9.18-20 evidence 자동 검증 (1.9.20: scenes/scripts 등 도메인 폴더 + jest/mocha 파싱)\n leerness verify-code [path] [--build] [--bench] # 1.9.20 --bench: scripts.bench 추가 실행 + evidence 누적\n leerness session close [path]\n leerness viewwork install [path]\n leerness viewwork emit [path] [--action a] [--note n] [--agent x] [--tool t]\n leerness route <task-type>\n leerness self check [path]\n leerness readme sync [path]\n leerness consistency check [path]\n leerness consistency merge-design-guide [path]\n leerness plan show|init|add|drop|progress|sync [args]\n leerness task list|add|update|drop|fix-evidence|relink [args]\n leerness skill list|info <name>\n leerness skill learn <id> --doc <url> --command "..." --capability "..." [--note ...]\n leerness skill use <id> [--note ...]\n leerness skill optimize <id> --before "..." --after "..." [--note ...]\n leerness skill remove <id>\n leerness skill consolidate [--threshold 0.3]\n leerness gate [path] # verify+audit+scan+encoding+lazy
5056
5236
  leerness retro [path] [--days 7] [--all-apps] [--include p1,p2] [--json] # 회고 (1.9.13~1.9.16)
5057
5237
  leerness insights [path] [--all-apps] [--include p1,p2] [--json] # 누적 통계 (1.9.13~1.9.16)
5058
5238
  leerness brainstorm "<주제>" [--all-apps] [--include p1,p2] [--json] # 브레인스토밍 (1.9.13~1.9.16)
@@ -5100,6 +5280,8 @@ async function main() {
5100
5280
  if (cmd === 'persona') return personaCmd(arg('--path', process.cwd()), args[1], args[2]);
5101
5281
  if (cmd === 'review') return reviewCmd(arg('--path', process.cwd()), args[1]);
5102
5282
  if (cmd === 'agents') return agentsCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
5283
+ if (cmd === 'contract' && args[1] === 'verify') return contractVerifyCmd(args[2], args[3]);
5284
+ if (cmd === 'reuse' && args[1] === 'autodetect') return reuseAutodetectCmd(args[2] || arg('--path', process.cwd()));
5103
5285
  if (cmd === 'setup-agents' || cmd === 'setup' && args[1] === 'agents') return await setupAgentsCmd(args[1] && args[1] !== 'agents' ? args[1] : (args[2] || process.cwd()));
5104
5286
  if (cmd === 'session' && args[1] === 'close') { const r = sessionClose(args[2] || process.cwd()); viewworkEmit(args[2] || process.cwd(), { action: 'task', tool: 'session-close', note: 'session close' }); return r; }
5105
5287
  if (cmd === 'viewwork' && args[1] === 'install') return viewworkInstall(args[2] || process.cwd());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.9.34",
3
+ "version": "1.9.35",
4
4
  "description": "Leerness: 비파괴 마이그레이션, 자동 버전 감지·업데이트, 계획/진행/핸드오프 자동화, 게으름·시크릿·인코딩 자동 가드, Claude Code 슬래시 통합을 갖춘 한국어 우선 AI 개발 하네스.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -950,6 +950,76 @@ total++;
950
950
  if (!ok) { failed++; console.log(r.stdout.slice(0, 800)); }
951
951
  }
952
952
 
953
+ // 1.9.35 회귀: 5개 신규 기능
954
+ total++;
955
+ {
956
+ // 개선#1: handoff 시 .harness 부재 자동 경고
957
+ const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-noinit-'));
958
+ const r = cp.spawnSync(process.execPath, [CLI, 'handoff', tmpC], { encoding: 'utf8', timeout: 10000 });
959
+ const ok = /leerness init 미실행 디렉토리/.test(r.stdout) || /leerness init/.test(r.stdout);
960
+ console.log(ok ? '✓ B(1.9.35) handoff: .harness 부재 자동 경고' : `✗ #1 handoff 경고 실패`);
961
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
962
+ }
963
+
964
+ total++;
965
+ {
966
+ // 개선#5: audit --fix 옵션 (플래그 인식만 검증, 실 fix는 통합 환경 필요)
967
+ const tmpC = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-fix-'));
968
+ cp.spawnSync(process.execPath, [CLI, 'init', tmpC, '--yes', '--no-banner', '--no-stale-check', '--language', 'ko', '--skills', 'recommended'], { stdio: 'ignore', timeout: 30000 });
969
+ const r = cp.spawnSync(process.execPath, [CLI, 'audit', tmpC, '--fix'], { encoding: 'utf8', timeout: 15000 });
970
+ const ok = r.status === 0 && /(Audit summary|fixed=)/.test(r.stdout);
971
+ console.log(ok ? '✓ B(1.9.35) audit --fix: 자동 fix 옵션 인식' : `✗ #5 audit --fix 실패`);
972
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
973
+ }
974
+
975
+ total++;
976
+ {
977
+ // 개선#3: contract verify
978
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-contract-'));
979
+ const specFile = path.join(tmpDir, 'spec.md');
980
+ const implFile = path.join(tmpDir, 'impl.js');
981
+ fs.writeFileSync(specFile, '# Spec\n\nfunction foo(x) {}\nfunction bar(y) {}\n`baz(`\n\ntick.amount\ntick.isCritical\n', 'utf8');
982
+ fs.writeFileSync(implFile, '"use strict";\nfunction foo(x){return x}\nfunction baz(y){return tick.amount}\nmodule.exports={foo, baz};\n', 'utf8');
983
+ const r = cp.spawnSync(process.execPath, [CLI, 'contract', 'verify', specFile, implFile, '--json'], { encoding: 'utf8', timeout: 10000 });
984
+ let parsed = null;
985
+ try { parsed = JSON.parse(r.stdout); } catch {}
986
+ const ok = parsed
987
+ && Array.isArray(parsed.missingFunctions) && parsed.missingFunctions.includes('bar')
988
+ && Array.isArray(parsed.missingFields) && parsed.missingFields.includes('isCritical')
989
+ && parsed.ok === false;
990
+ console.log(ok ? '✓ B(1.9.35) contract verify: bar 함수 + isCritical 필드 누락 정확 감지' : `✗ #3 contract verify 실패`);
991
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
992
+ }
993
+
994
+ total++;
995
+ {
996
+ // 개선#2: reuse autodetect — src/*.js의 module.exports 스캔
997
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-autodetect-'));
998
+ fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true });
999
+ fs.writeFileSync(path.join(tmpDir, 'src', 'util.js'), 'function hello(){}\nfunction _internal(){}\nmodule.exports={hello, _internal};\n', 'utf8');
1000
+ const r = cp.spawnSync(process.execPath, [CLI, 'reuse', 'autodetect', tmpDir, '--json'], { encoding: 'utf8', timeout: 10000 });
1001
+ let parsed = null;
1002
+ try { parsed = JSON.parse(r.stdout); } catch {}
1003
+ // _internal은 _로 시작하므로 제외, hello만 발견
1004
+ const ok = parsed && parsed.found && parsed.found.length === 1 && parsed.found[0].name === 'hello';
1005
+ console.log(ok ? '✓ B(1.9.35) reuse autodetect: module.exports 스캔 + _internal 제외' : `✗ #2 autodetect 실패`);
1006
+ if (!ok) { failed++; console.log(r.stdout.slice(0, 400)); }
1007
+ }
1008
+
1009
+ total++;
1010
+ {
1011
+ // 개선#4: agents dispatch 안내문에 mtime/contract 안전 규칙 추가
1012
+ const env = { ...process.env, LEERNESS_ENABLE_CLAUDE: '1' };
1013
+ const r = cp.spawnSync(process.execPath, [CLI, 'agents', 'dispatch', 'test task', '--to', 'claude'], { encoding: 'utf8', timeout: 15000, env });
1014
+ // claude가 ready면 안내문 출력. 환경 따라 ready 아닐 수도 — 안내문 내용만 확인.
1015
+ const text = r.stdout + r.stderr;
1016
+ const ok = /분배 시 안전 규칙 \(1\.9\.35\)/.test(text) || /파일 경로 격리/.test(text) || /last-writer-wins/.test(text)
1017
+ // claude 미활성 시 거부 메시지도 통과
1018
+ || /비활성|disabled|not-installed/i.test(text);
1019
+ console.log(ok ? '✓ B(1.9.35) agents dispatch: 안전 규칙 안내 (mtime/contract) 또는 비활성 거부' : `✗ #4 dispatch 안내 실패`);
1020
+ if (!ok) { failed++; console.log(text.slice(0, 400)); }
1021
+ }
1022
+
953
1023
  // 1.9.34 회귀: 인터랙티브 multi-select (방향키/스페이스) — 비-TTY 폴백
954
1024
  total++;
955
1025
  {
@@ -974,7 +1044,7 @@ total++;
974
1044
  && /███████╗/.test(r.stdout)
975
1045
  && /verify · reuse-map/.test(r.stdout)
976
1046
  && /한국어 우선 AI 개발 하네스/.test(r.stdout)
977
- && /v1\.9\.34/.test(r.stdout);
1047
+ && /v1\.9\.3\d/.test(r.stdout);
978
1048
  console.log(ok ? '✓ B(1.9.34) 배너 색상 + ASCII + 한국어' : `✗ 배너 색상 실패`);
979
1049
  if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
980
1050
  }