leerness 1.17.0 → 1.19.0

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/bin/leerness.js CHANGED
@@ -32,7 +32,7 @@ const { _evidenceQuality, _parseEvidenceStats, _shellGuardAnalyze, _claimFileInG
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
 
35
- const VERSION = '1.17.0';
35
+ const VERSION = '1.19.0';
36
36
 
37
37
  // 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
38
38
  // 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
@@ -216,7 +216,7 @@ function _resolveRoot(positional) {
216
216
  }
217
217
  function nonFlagArgs() {
218
218
  const out = [];
219
- const withValue = new Set(['--language','--skills','--path','--status','--progress','--goal','--reason','--next','--target','--token-env','--package','--out','--from','--repo','--id','--note','--evidence','--query','--limit','--action','--agent','--tool','--doc','--command','--capability','--before','--after','--display','--threshold','--trigger','--check','--set','--min-score','--include','--days','--gh-pages-src','--roadmap','--since','--agents','--model','--timeout','--retry-on-fail','--label','--score','--tokens','--alternatives','--impact','--tag','--surface','--depends-on','--affects','--co-changes-with','--files','--branch','--remote','--task-add','--next-action','--role','--provider','--env-var','--deploy','--token-lifetime-hours','--port','--secret','--keep','--shell','--ps-version','--done-when']); // 1.14.2 (UR-0032): --done-when 값이 positional 로 누출돼 milestone 제목에 흡수되던 것 차단
219
+ const withValue = new Set(['--language','--skills','--path','--status','--progress','--goal','--reason','--next','--target','--token-env','--package','--out','--from','--repo','--id','--note','--evidence','--query','--limit','--action','--agent','--tool','--doc','--command','--capability','--before','--after','--display','--threshold','--trigger','--check','--set','--min-score','--include','--days','--gh-pages-src','--roadmap','--since','--agents','--model','--timeout','--retry-on-fail','--label','--score','--tokens','--alternatives','--impact','--tag','--surface','--depends-on','--affects','--co-changes-with','--files','--branch','--remote','--task-add','--next-action','--role','--provider','--env-var','--deploy','--token-lifetime-hours','--port','--secret','--keep','--shell','--ps-version','--done-when','--test-cmd']); // 1.14.2 (UR-0032): --done-when 값이 positional 로 누출돼 milestone 제목에 흡수되던 것 차단. 1.17.2 (UR-0045): --test-cmd 동일 원칙(신규 value-flag 는 반드시 여기 등록)
220
220
  const a = process.argv.slice(2);
221
221
  for (let i = 0; i < a.length; i++) {
222
222
  const x = a[i];
@@ -410,7 +410,7 @@ function coreFiles(root, lang = 'ko', selectedSkills = [], opts = {}) {
410
410
  const project = detectProjectName(root);
411
411
  const skillRows = Object.entries(skillCatalog).map(([k, v]) => `| ${k} | ${v.displayNameKo} | ${v.capabilities.join(', ')} | ${v.lastUpdated} | ${v.verification} |`).join('\n');
412
412
  const _files = {
413
- 'AGENTS.md': `${MARK}\n# Leerness Agent Instructions\n\n## ⭐ 매 세션 첫 행동 (1.9.39+)\n**반드시 \`.harness/session-workflow.md\`를 먼저 읽고 6단계 워크플로를 따른다**: 요청분석→계획→분배→sub-agent작업→종합검증→마감. 라운드 길이/복잡도 무관, drift 방지를 위해 모든 작업에 동일 흐름 유지.\n\n## 정적 vs 동적 — leerness 역할 경계 (1.9.282, UR-0035)\n**AGENTS.md = 정적 프로젝트 지침** (코딩 규칙·테스트 명령·금지 사항·배포 절차 — 자주 안 변함).\n**leerness = 동적 작업 상태·기억·검증·인수인계** (현재 목표·수정 파일·실패 시도·검증 결과·다음 에이전트 인계 — 매 작업 변함).\n- 규칙/명령/금지는 여기 AGENTS.md 에 적는다.\n- 동적 상태(결정/교훈/계획/진행/검증/인수인계)는 leerness 가 **기본 워크스페이스 \`.harness/\`** 에 기록한다 (decisions.md / lessons.md / plan.md / progress-tracker.md / session-handoff.md). \`leerness handoff\` · \`decision add\` · \`lesson save\` 등이 여기에 쓴다.\n- (선택) \`leerness state show|start|record|verify|handoff\` (또는 MCP \`leerness_state_*\`) 의 JSON 상태 substrate 는 \`.leerness/\` (에이전트 간 인수인계 표준, 1.9.278 — state 명령 사용 시 생성). 메인 워크스페이스(.harness)와 별개.\n- leerness 는 AGENTS.md 를 **대체하지 않고 보완**한다. 정적 지침은 여기, 동적 상태는 leerness.\n\n## Mandatory read order (session start)\n1. **.harness/session-workflow.md** (1.9.39+ 6단계 워크플로 — 최우선)\n2. .harness/context-routing.md\n3. .harness/session-handoff.md\n4. .harness/current-state.md\n5. .harness/plan.md\n6. .harness/progress-tracker.md\n7. .harness/guideline.md\n8. .harness/protected-files.md\n9. .harness/writeback-policy.md\n10. .harness/anti-lazy-work-policy.md\n11. **.harness/rules.md** (사용자 정의 영구 룰 — 매 세션 반드시 따름)\n\n## Required behavior\n- 작업 시작 시 \`leerness handoff .\`를 실행해 컨텍스트를 적재합니다 (handoff가 active rules를 자동 출력).\n- 작업 분류는 \`leerness route <task-type>\`로 확인합니다 (planning, feature, bugfix, refactor, research, consistency, release, migration, session-start, session-close, harness-maintenance).\n- 보호 파일/관리 섹션을 삭제하지 않습니다. 머지·아카이브·deprecated 표시를 사용합니다.\n- 의미 있는 변경 후 progress-tracker, current-state, task-log, session-handoff를 갱신합니다.\n- 완료 선언 전 \`leerness check .\` 또는 \`leerness lazy detect .\`로 자기검증합니다.\n- 변경 전 secret/encoding 가드: \`leerness scan secrets .\`, \`leerness encoding check .\`.\n- 같은 기능 중복 생성 전 design-system.md, consistency-policy.md, reuse-map.md를 확인합니다.\n- 매 세션 종료 시 \`leerness session close .\`로 9개 카테고리(완료/진행중/미완료/예정/대기/보류/차단/드랍/검증) + **활성 룰 검증 결과**를 보고합니다.\n- 업데이트는 \`leerness update --check\` (감지) → \`leerness update --yes\` (자동 마이그레이션).\n\n## 자연어 회고/통찰/브레인스토밍 (1.9.13)\n사용자가 자연어로 회고/통찰/브레인스토밍을 요청하면 즉시 leerness 명령으로 호출합니다.\n\n| 사용자 발화 (자연어) | 즉시 실행할 명령 |\n|---|---|\n| "회고해줘 / 돌아보자 / 정리해줘" | \`leerness retro\` |\n| "최근 N일 회고" | \`leerness retro --days N\` |\n| "통계 / 누적 지표 / insights" | \`leerness insights\` |\n| "X에 대해 브레인스토밍 / X 관련 자료 / X 시작 전 검토" | \`leerness brainstorm "X"\` |\n\nsession close가 매번 자동으로 한 줄 요약을 출력하고, 5세션마다 자동 깊은 회고를 실행합니다. 사용자가 명시 요청 시 즉시 호출.\n\n## 자연어 룰 처리 (1.9.8)\n사용자가 자연어로 영구 룰을 요청하면 즉시 leerness rule 명령으로 등록합니다.\n\n| 사용자 발화 (자연어) | 즉시 실행할 명령 |\n|---|---|\n| "매 업데이트마다 버전 bump해줘" | \`leerness rule add "버전을 patch로 bump" --trigger every-update\` |\n| "매 커밋마다 패치노트 추가해줘" | \`leerness rule add "패치노트 추가" --trigger every-commit\` |\n| "세션 종료마다 배포해줘" | \`leerness rule add "배포 (release publish)" --trigger session-close\` |\n| "X 룰 중지/그만/끄기" | \`leerness rule pause <ID>\` (해당 룰 ID는 list로 확인) |\n| "X 룰 제거/삭제" | \`leerness rule remove <ID>\` |\n| "모든 룰 중지" | \`leerness rule stop\` |\n| "룰 다시 켜줘" | \`leerness rule resume-all\` 또는 \`leerness rule resume <ID>\` |\n\n룰을 등록한 후 사용자에게 등록 결과(ID + trigger + 설명)를 보고하고, 그 이후 매 세션마다 자동 적용합니다. 사용자가 "중지" 또는 "제거"를 명시적으로 말하기 전까지는 룰을 비활성화하지 않습니다.\n\n## 룰 자동 적용 (1.9.8)\nleerness가 자동 검증 가능한 trigger:\n- **every-update / version bump 키워드 룰**: package.json의 version이 갱신됐는지 검사 (handoff/session close가 baseline 캐시와 비교).\n- **CHANGELOG / 패치노트 키워드 룰**: CHANGELOG.md의 mtime이 갱신됐는지 검사.\n- **test / 테스트 / verify 키워드 룰**: review-evidence.md에 오늘 verify-code 흔적이 있는지 검사.\n- **배포 / publish / push 키워드 룰**: 자동 검증 불가 → 사용자에게 release publish 명령을 안내.\n\n자동 검증 가능한 룰의 실행은 \`leerness release bump\`, \`leerness release note "..."\`, \`leerness release publish\`를 사용해 자동화합니다.\n`,
413
+ 'AGENTS.md': `${MARK}\n# Leerness Agent Instructions\n\n## ⭐ 매 세션 첫 행동 (1.9.39+)\n**반드시 \`.harness/session-workflow.md\`를 먼저 읽고 6단계 워크플로를 따른다**: 요청분석→계획→분배→sub-agent작업→종합검증→마감. 라운드 길이/복잡도 무관, drift 방지를 위해 모든 작업에 동일 흐름 유지.\n\n## 정적 vs 동적 — leerness 역할 경계 (1.9.282, UR-0035)\n**AGENTS.md = 정적 프로젝트 지침** (코딩 규칙·테스트 명령·금지 사항·배포 절차 — 자주 안 변함).\n**leerness = 동적 작업 상태·기억·검증·인수인계** (현재 목표·수정 파일·실패 시도·검증 결과·다음 에이전트 인계 — 매 작업 변함).\n- 규칙/명령/금지는 여기 AGENTS.md 에 적는다.\n- 동적 상태(결정/교훈/계획/진행/검증/인수인계)는 leerness 가 **기본 워크스페이스 \`.harness/\`** 에 기록한다 (decisions.md / lessons.md / plan.md / progress-tracker.md / session-handoff.md). \`leerness handoff\` · \`decision add\` · \`lesson save\` 등이 여기에 쓴다.\n- (선택) \`leerness state show|start|record|verify|handoff\` (또는 MCP \`leerness_state_*\`) 의 JSON 상태 substrate 는 \`.leerness/\` (에이전트 간 인수인계 표준, 1.9.278 — state 명령 사용 시 생성). 메인 워크스페이스(.harness)와 별개.\n- leerness 는 AGENTS.md 를 **대체하지 않고 보완**한다. 정적 지침은 여기, 동적 상태는 leerness.\n\n## Mandatory read order (session start)\n1. **.harness/session-workflow.md** (1.9.39+ 6단계 워크플로 — 최우선)\n2. .harness/context-routing.md\n3. .harness/session-handoff.md\n4. .harness/current-state.md\n5. .harness/plan.md\n6. .harness/progress-tracker.md\n7. .harness/guideline.md\n8. .harness/protected-files.md\n9. .harness/writeback-policy.md\n10. .harness/anti-lazy-work-policy.md\n11. **.harness/rules.md** (사용자 정의 영구 룰 — 매 세션 반드시 따름)\n\n## Required behavior\n- 작업 시작 시 \`leerness handoff .\`를 실행해 컨텍스트를 적재합니다 (handoff가 active rules를 자동 출력).\n- 작업 분류는 \`leerness route <task-type>\`로 확인합니다 (planning, feature, bugfix, refactor, research, consistency, release, migration, session-start, session-close, harness-maintenance).\n- 보호 파일/관리 섹션을 삭제하지 않습니다. 머지·아카이브·deprecated 표시를 사용합니다.\n- 의미 있는 변경 후 progress-tracker, current-state, task-log, session-handoff를 갱신합니다.\n- 완료 선언 전 \`leerness check .\` 또는 \`leerness lazy detect .\`로 자기검증하고, \`leerness lens\`의 분야별 자기질문에 답합니다 (코드: "선임 개발자가 복잡하다고 느끼지 않을까?" / 디자인: "선임 디자이너와 일반 사용자가 이쁘고 직관적이라 느낄까?" — 1.18.3).\n- 변경 전 secret/encoding 가드: \`leerness scan secrets .\`, \`leerness encoding check .\`.\n- 같은 기능 중복 생성 전 design-system.md, consistency-policy.md, reuse-map.md를 확인합니다.\n- 매 세션 종료 시 \`leerness session close .\`로 9개 카테고리(완료/진행중/미완료/예정/대기/보류/차단/드랍/검증) + **활성 룰 검증 결과**를 보고합니다.\n- 업데이트는 \`leerness update --check\` (감지) → \`leerness update --yes\` (자동 마이그레이션).\n\n## 자연어 회고/통찰/브레인스토밍 (1.9.13)\n사용자가 자연어로 회고/통찰/브레인스토밍을 요청하면 즉시 leerness 명령으로 호출합니다.\n\n| 사용자 발화 (자연어) | 즉시 실행할 명령 |\n|---|---|\n| "회고해줘 / 돌아보자 / 정리해줘" | \`leerness retro\` |\n| "최근 N일 회고" | \`leerness retro --days N\` |\n| "통계 / 누적 지표 / insights" | \`leerness insights\` |\n| "X에 대해 브레인스토밍 / X 관련 자료 / X 시작 전 검토" | \`leerness brainstorm "X"\` |\n\nsession close가 매번 자동으로 한 줄 요약을 출력하고, 5세션마다 자동 깊은 회고를 실행합니다. 사용자가 명시 요청 시 즉시 호출.\n\n## 자연어 룰 처리 (1.9.8)\n사용자가 자연어로 영구 룰을 요청하면 즉시 leerness rule 명령으로 등록합니다.\n\n| 사용자 발화 (자연어) | 즉시 실행할 명령 |\n|---|---|\n| "매 업데이트마다 버전 bump해줘" | \`leerness rule add "버전을 patch로 bump" --trigger every-update\` |\n| "매 커밋마다 패치노트 추가해줘" | \`leerness rule add "패치노트 추가" --trigger every-commit\` |\n| "세션 종료마다 배포해줘" | \`leerness rule add "배포 (release publish)" --trigger session-close\` |\n| "X 룰 중지/그만/끄기" | \`leerness rule pause <ID>\` (해당 룰 ID는 list로 확인) |\n| "X 룰 제거/삭제" | \`leerness rule remove <ID>\` |\n| "모든 룰 중지" | \`leerness rule stop\` |\n| "룰 다시 켜줘" | \`leerness rule resume-all\` 또는 \`leerness rule resume <ID>\` |\n\n룰을 등록한 후 사용자에게 등록 결과(ID + trigger + 설명)를 보고하고, 그 이후 매 세션마다 자동 적용합니다. 사용자가 "중지" 또는 "제거"를 명시적으로 말하기 전까지는 룰을 비활성화하지 않습니다.\n\n## 룰 자동 적용 (1.9.8)\nleerness가 자동 검증 가능한 trigger:\n- **every-update / version bump 키워드 룰**: package.json의 version이 갱신됐는지 검사 (handoff/session close가 baseline 캐시와 비교).\n- **CHANGELOG / 패치노트 키워드 룰**: CHANGELOG.md의 mtime이 갱신됐는지 검사.\n- **test / 테스트 / verify 키워드 룰**: review-evidence.md에 오늘 verify-code 흔적이 있는지 검사.\n- **배포 / publish / push 키워드 룰**: 자동 검증 불가 → 사용자에게 release publish 명령을 안내.\n\n자동 검증 가능한 룰의 실행은 \`leerness release bump\`, \`leerness release note "..."\`, \`leerness release publish\`를 사용해 자동화합니다.\n`,
414
414
  'CLAUDE.md': `${MARK}\n# Claude Code Instructions\n\nFollow AGENTS.md. Always run \`leerness handoff .\` at the start and \`leerness session close .\` before ending a session.\n\n**⭐ 매 세션 첫 행동 (1.9.39+)**: \`.harness/session-workflow.md\`의 6단계 워크플로(요청분석→계획→분배→sub-agent→종합검증→마감)를 따라야 함. drift critical 시 \`leerness drift check --auto-fix\`로 자동 회복.\n\nProtected files must not be deleted. Read .harness/anti-lazy-work-policy.md before claiming completion.\n\n## 자연어 영구 룰 (1.9.8)\n사용자가 "매 X마다 Y를 해줘" 같은 자연어 룰을 말하면 즉시 \`leerness rule add "Y" --trigger every-X\`로 등록하세요. 등록된 룰은 매 세션 \`handoff\`가 자동 출력하고, \`session close\`가 자동 검증해 보고합니다. 사용자가 "중지" / "그만" / "끄기"를 명시할 때만 \`rule pause/remove\`를 호출합니다.\n\n자세한 매핑은 AGENTS.md의 "자연어 룰 처리" 표를 참고하세요.\n`,
415
415
  '.cursor/rules/leerness.mdc': `${MARK}\n---\nalwaysApply: true\n---\nFollow AGENTS.md and .harness/context-routing.md.\nRun: \`leerness handoff .\` at session start.\nRun: \`leerness session close .\` at session end.\nPreserve Leerness protected files.\n`,
416
416
  '.github/copilot-instructions.md': `${MARK}\n# Copilot Instructions\n\nUse AGENTS.md and .harness/ as project memory.\nDo not remove protected Leerness files.\nBefore completion, ensure plan.md, progress-tracker.md, current-state.md, session-handoff.md are updated.\n`,
@@ -602,7 +602,7 @@ leerness memory restore <surface> <target> # archive → active 복귀 (DELETE
602
602
  - ⚠ "테스트 돌렸으니 PASS" 자기 보고만 → verify-claim --run-tests 미실행
603
603
  - ⚠ contract verify 생략 → 사양 불일치 BUG가 사용자에게 노출
604
604
  `),
605
- '.harness/anti-lazy-work-policy.md': fm('anti-lazy-work-policy', ['완료 선언 전'], ['게으른 작업 방지 기준 변경'], `# Anti Lazy Work Policy\n\n## Rules\n1. **증거 없는 완료 금지**: \"완료\"를 선언하려면 progress-tracker의 evidence 컬럼에 명령 출력/테스트 결과/스크린샷 경로 등이 있어야 합니다.\n2. **빈 핸드오프 금지**: 세션 종료 시 session-handoff.md의 Completed/In Progress/Next Exact Step이 모두 비어 있으면 close가 \"insufficient\" 상태로 표시됩니다.\n3. **부분 구현 자기보고**: 완전 구현이 아니면 status를 \`incomplete\`로, Next Exact Step에 \"무엇을 추가해야 끝나는지\" 한 줄을 적습니다.\n4. **검증 기록**: typecheck/lint/test 결과를 review-evidence.md에 누적 기록합니다.\n5. **TODO 표지**: 코드에 \`TODO\`/\`FIXME\`/\`XXX\`를 새로 도입하면 progress-tracker에 동일 ID로 추적합니다.\n6. **거짓 완료 자동 감지**: \`leerness lazy detect\`는 다음을 자동 점검합니다.\n - progress-tracker에 done인데 evidence가 비어있는 row\n - session-handoff의 Completed가 비어있고 Next Exact Step도 비어있음\n - 코드에 새 TODO/FIXME 추가 + progress-tracker에 추적 항목 없음\n - test 명령 실행 흔적 없음 (review-evidence.md 또는 task-log.md에 명령 기록)\n`),
605
+ '.harness/anti-lazy-work-policy.md': fm('anti-lazy-work-policy', ['완료 선언 전'], ['게으른 작업 방지 기준 변경'], `# Anti Lazy Work Policy\n\n## Rules\n1. **증거 없는 완료 금지**: \"완료\"를 선언하려면 progress-tracker의 evidence 컬럼에 명령 출력/테스트 결과/스크린샷 경로 등이 있어야 합니다.\n2. **빈 핸드오프 금지**: 세션 종료 시 session-handoff.md의 Completed/In Progress/Next Exact Step이 모두 비어 있으면 close가 \"insufficient\" 상태로 표시됩니다.\n3. **부분 구현 자기보고**: 완전 구현이 아니면 status를 \`incomplete\`로, Next Exact Step에 \"무엇을 추가해야 끝나는지\" 한 줄을 적습니다.\n4. **검증 기록**: typecheck/lint/test 결과를 review-evidence.md에 누적 기록합니다.\n5. **TODO 표지**: 코드에 \`TODO\`/\`FIXME\`/\`XXX\`를 새로 도입하면 progress-tracker에 동일 ID로 추적합니다.\n6. **거짓 완료 자동 감지**: \`leerness lazy detect\`는 다음을 자동 점검합니다.\n - progress-tracker에 done인데 evidence가 비어있는 row\n - session-handoff의 Completed가 비어있고 Next Exact Step도 비어있음\n - 코드에 새 TODO/FIXME 추가 + progress-tracker에 추적 항목 없음\n - test 명령 실행 흔적 없음 (review-evidence.md 또는 task-log.md에 명령 기록)\n7. **품질 렌즈 자가질문 (1.18.3)**: 완료 선언 전 \`leerness lens\`의 분야별 질문에 스스로 답합니다 — 코드: "선임 개발자가 이 코드를 보고 복잡하다고 느끼지 않을까?" / 디자인: "선임 디자이너와 일반 사용자가 봤을 때 이쁘고 편하고 직관적인가?". "그렇다(통과)"라고 답할 수 없으면 완료가 아닙니다. 분야를 바꾸면 인과관계로 연결된 분야(\`lens\` 출력의 ↔ 인과)의 질문도 다시 확인합니다.\n`),
606
606
  '.harness/rules.md': _rulesHeader() + '\n',
607
607
  '.harness/session-handoff.md': fm('session-handoff', ['세션 시작','다음 작업 이어받기'], ['세션 종료'], `# Session Handoff\n\nLast generated: (자동)\n\n## Completed\n-\n\n## In Progress\n-\n\n## Incomplete / Waiting / On Hold / Blocked\n-\n\n## Dropped\n-\n\n## Verification\n-\n\n## Recommended Direction\n-\n\n## Next Exact Step\n-\n`),
608
608
  '.harness/leerness-maintenance.md': fm('leerness-maintenance', ['작업 시작','마이그레이션/릴리즈 전'], ['버전 정책 변경'], `# Leerness Maintenance\n\nAI agents should check:\n\n\`\`\`bash\nleerness --version\nleerness self check .\nleerness update --check # 24h 캐시 자동 감지\nleerness update --yes # 새 버전 발견 시 자동 마이그레이션\ncat .harness/HARNESS_VERSION\nnpm view leerness version\n\`\`\`\n`),
@@ -886,23 +886,9 @@ async function resolveInstallOptions(root, opts = {}) {
886
886
  // 이전 1.9.146 의 3-tier 선택 prompt 는 사용자 경험 복잡도 증가 + 잘못된 선택 (full) 시 위험 →
887
887
  // 안전한 기본 (basic) 자동 시작 + REPL 진입 시점에 필요 시 변경하는 흐름이 더 안전하고 간편.
888
888
  const permissionMode = 'basic';
889
- // 1.9.151: 모든 문항 종료 — REPL 모드 즉시 활성화 여부 (사용자 명시 요청)
890
- // 선택된 에이전트가 있을 때만 표시. 설치 완료 후 install() 처리.
891
- let startRepl = false;
892
- const hasAgents = Array.isArray(agentsOptIn) && agentsOptIn.length > 0;
893
- if (shouldAsk && hasAgents && !opts._skipReplPrompt) {
894
- if (useInteractive) {
895
- const rOpt = await _selectOne('설치 완료 후 REPL agent 모드를 즉시 시작할까요?', [
896
- { label: '아니오 — 설치만 완료 (나중에 `leerness agent` 로 실행)', description: '권장 — 토큰/모델 설정 후 사용', id: 'no' },
897
- { label: '예 — 설치 직후 REPL 모드 진입 (Hermes/OpenClaw 스타일)', description: 'Ollama 우선 — 가능하면 자동 모델 선택', id: 'yes' }
898
- ], { defaultIndex: 0 });
899
- startRepl = rOpt && rOpt.id === 'yes';
900
- } else {
901
- log('\n설치 완료 후 REPL agent 모드를 즉시 시작할까요? (y/N)');
902
- const a = (await ask('선택 [N]: ')).trim().toLowerCase();
903
- startRepl = a === 'y' || a === 'yes';
904
- }
905
- }
889
+ // 1.18.3 (사용자 명시): 설치 직후 REPL agent 모드 진입 문항 제거 — REPL 완성도가 올라가면 그때 구현 예정.
890
+ // 수동 진입(`leerness agent`) 그대로 유지.
891
+ const startRepl = false;
906
892
  return { lang, skills, agentsOptIn, permissionMode, startRepl };
907
893
  }
908
894
 
@@ -947,7 +933,6 @@ async function install(root, opts = {}) {
947
933
  const list = Array.isArray(resolved.agentsOptIn) ? resolved.agentsOptIn.join(', ') : String(resolved.agentsOptIn);
948
934
  log(`Agents 활성화: ${list}`);
949
935
  }
950
- if (resolved.startRepl) log(`REPL 자동 시작: 예 (설치 완료 후 \`leerness agent\` 진입)`);
951
936
  if (resolved.permissionMode) log(`Agent 권한 모드: ${resolved.permissionMode} (1.9.174 — REPL에서 \`:permissions extended|full\` 로 즉시 변경 가능)`);
952
937
  // 1.9.10: 스킬 카탈로그 출처 안내
953
938
  // 1.9.184 (사용자 명시): leerness-skillpack 미사용 정책 — 안내 메시지 제거. builtin catalog 만 사용.
@@ -1168,17 +1153,7 @@ async function install(root, opts = {}) {
1168
1153
  // 1.9.148: 1.9.32 중복 prompt 제거 (사용자 명시 — CLI 에이전트 prompt 중복).
1169
1154
  // resolveInstallOptions (1.9.146) 가 이미 모든 prompt 모은 위치에 통합된 4지선다 prompt 있음.
1170
1155
  // 별도 setupAgents 명령은 사용자가 명시적으로 `leerness setup-agents` 호출 시에만.
1171
- // 1.9.151: 설치 완료 직후 startRepl 선택 REPL agent 모드 즉시 진입 (사용자 명시 요청)
1172
- // 1.9.181: 문구 단순화 + provider 하드코딩 제거 (사용자 명시 — install 선택한 CLI를 REPL이 자동 선택)
1173
- if (resolved.startRepl && !opts.migration && process.stdin.isTTY && process.env.LEERNESS_NO_PROMPT !== '1') {
1174
- log('');
1175
- log('🚀 설치 완료 — REPL agent 모드를 시작합니다...');
1176
- log('');
1177
- _cleanupSigint(); // 1.9.184: REPL 진입 전 SIGINT handler 해제 (readline 이 자체 처리)
1178
- try {
1179
- await _agentRepl(root, { role: 'actor' }); // provider 미지정 → _agentRepl 의 auto-select 동작 (1.9.181 fix)
1180
- } catch (e) { warn('REPL 진입 실패: ' + e.message); }
1181
- }
1156
+ // 1.18.3 (사용자 명시): 설치 직후 REPL 자동 진입 제거 startRepl 항상 false (수동 `leerness agent` 만).
1182
1157
  }
1183
1158
  _cleanupSigint(); // 1.9.184: install 함수 종료 시 SIGINT handler 해제 (모든 종료 경로)
1184
1159
  }
@@ -3557,6 +3532,131 @@ function _selfTestCases() {
3557
3532
  const src = read(__filename);
3558
3533
  return src.includes('const _GROUP_USAGE = {') && src.includes("if (_GROUP_USAGE[cmd] && !args[1])") && src.includes("'subcommand_required'");
3559
3534
  } },
3535
+ { name: '17th 버그헌트 P2: plan add 공백제목 trim(기본값) + milestone 파서 개행 미흡수 (1.17.1)', run: () => {
3536
+ const src = read(__filename);
3537
+ const wired = src.includes("args.slice(2).join(' ').trim() || '새 계획'") && src.includes('(M-\\d{4})\\.[ \\t]*(.+?)$');
3538
+ // 파서 동작: 공백제목 milestone 이 다음 줄 'Status:' 를 제목으로 먹지 않음
3539
+ const block = '### M-0006. \nStatus: planned\nProgress: 0%\n';
3540
+ const m = block.match(/^### (M-\d{4})\.[ \t]*(.+?)$/m);
3541
+ const safe = !m || (m[2] || '').indexOf('Status') === -1;
3542
+ return wired && safe;
3543
+ } },
3544
+ { name: '범용성 P1 (UR-0045): verify-claim --run-tests 테스트 명령 해석 체인(--test-cmd>config>실제 npm test>skip) + placeholder 미실행 (1.17.2)', run: () => {
3545
+ const src = read(__filename);
3546
+ const chain = src.includes("let testCmd = arg('--test-cmd', null)") && src.includes("typeof cfg.testCommand === 'string'") && src.includes("!/no test specified/i.test(ts)") && src.includes('테스트 명령 미지정');
3547
+ const wired = src.includes("'--done-when','--test-cmd'") && src.includes('runCommandSafe(testCmd, []') && src.includes('cmd: testCmd');
3548
+ // pytest 출력 파싱: "3 passed in 0.05s"
3549
+ const py = ('3 passed in 0.05s'.match(/(\d+)\s+passed\b/i) || [])[1] === '3';
3550
+ return chain && wired && py;
3551
+ } },
3552
+ { name: '범용성 P1② (UR-0046): verify-claim 스텁 구현 차단 + 테스트-구현 연결 검사 (1.17.3)', run: () => {
3553
+ const src = read(__filename);
3554
+ const wired = src.includes('const stubFiles = [];') && src.includes('implementationSubstance: stubFiles.length === 0') && src.includes('testImplLink: testLinkOk') && src.includes("(claimsChecked && stubFiles.length > 0) || (has('--strict-claims') && testLinkOk === false)");
3555
+ // 스텁 판정 로직 재현: 주석뿐 파일 → 코드줄 0
3556
+ const stub = '// TODO: call stripe\n// TODO: verify signature\n\n';
3557
+ const real = '// handler\nmodule.exports = function(){ return 1; };\n';
3558
+ const count = (s) => s.replace(/\/\*[\s\S]*?\*\//g, '').split('\n').map(l => l.trim()).filter(t => t && !t.startsWith('//') && !t.startsWith('#')).length;
3559
+ return wired && count(stub) === 0 && count(real) > 0;
3560
+ } },
3561
+ { name: '범용성 P2 (UR-0047): 테스트 카운트 관례 확대(pytest/루트 *.test.*) + 측정불가=검증미수행(역전 해소) (1.17.4)', run: () => {
3562
+ const src = read(__filename);
3563
+ const wired = src.includes('const _countTests = (fp)') && src.includes("def\\s+test_") && src.includes('측정 불가 — 주장') && src.includes('out.verdict.testCountMatch === false') && src.includes('const testMeasured = actualTestCount != null;');
3564
+ // 관례 글롭: pytest/루트 test 파일명 매칭
3565
+ const re = /^test_.+\.py$|_test\.py$|\.test\.[cm]?[jt]sx?$|\.spec\.[cm]?[jt]sx?$/i;
3566
+ const glob = re.test('test_calc.py') && re.test('calc_test.py') && re.test('rateLimiter.test.js') && re.test('app.spec.ts') && !re.test('calc.py') && !re.test('index.js');
3567
+ // 파이썬 카운트: def test_ 2개
3568
+ const py = ('def test_a():\n pass\ndef test_b():\n pass\n'.match(/^\s*def\s+test_/gm) || []).length === 2;
3569
+ return wired && glob && py;
3570
+ } },
3571
+ { name: '범용성 P2 (UR-0048): task update unknown flag 거부 + did-you-mean(인수인계 유실 차단) (1.17.5)', run: () => {
3572
+ const src = read(__filename);
3573
+ const wired = src.includes('function _rejectUnknownFlags(allowed, usageHint)') && src.includes("'unknown_flag'") && src.includes("_rejectUnknownFlags(['--status', '--evidence', '--next', '--note']");
3574
+ // did-you-mean prefix 로직: --next-action 은 --next 를 prefix 로 가짐
3575
+ const dymOk = '--next-action'.startsWith('--next');
3576
+ return wired && dymOk;
3577
+ } },
3578
+ { name: '범용성 P2 완결 (UR-0049): session close 마감 정합 — done 낙관 재확인 + 시크릿 재확인 (1.17.6)', run: () => {
3579
+ const sc = read(path.join(path.dirname(__filename), '..', 'lib', 'session-close.js'));
3580
+ const wired = sc.includes('_detectOptimism, _scanCodeForPatterns, _collectSecretFindings } = deps') && sc.includes('optimismUnresolved') && sc.includes('jsonResult.closeSecurity') && sc.includes('마감 보안: 커밋 대상 시크릿');
3581
+ const injected = read(__filename).includes('_updateUserRequest, _detectOptimism, _scanCodeForPatterns, _collectSecretFindings });');
3582
+ return wired && injected;
3583
+ } },
3584
+ { name: '재실증 P1 (1.18.1): basic 모드에서 비-JS 인터프리터는 차단되지만 userAuthorized(--test-cmd) 는 허용 (행위)', run: () => {
3585
+ const basic = { mode: 'basic', shell: { exec: false, allowList: [] } };
3586
+ const blockedDefault = _isCommandPermitted(basic, 'python test_todo.py', {}) === false; // 기본: 차단(원래 126 원인)
3587
+ const blockedPy = _isCommandPermitted(basic, 'pytest -q', {}) === false; // pytest 도 차단
3588
+ const allowedAuth = _isCommandPermitted(basic, 'python test_todo.py', { userAuthorized: true }) === true; // 명시 권한 → 허용
3589
+ const coreStillOk = _isCommandPermitted(basic, 'npm test', {}) === true; // JS 핵심도구는 그대로 허용(회귀 가드)
3590
+ const extOk = _isCommandPermitted({ mode: 'extended', shell: { exec: true, allowList: [] } }, 'python x.py', {}) === true; // exec:true → 허용
3591
+ return blockedDefault && blockedPy && allowedAuth && coreStillOk && extOk;
3592
+ } },
3593
+ { name: '재실증 P1 (1.18.1): verify-claim 차단 실행은 skip(불일치 판정 아님) + 종합 라벨이 실제 cmd (소스 가드)', run: () => {
3594
+ const src = read(__filename);
3595
+ const authPass = src.includes('userAuthorized: true, timeout: 5 * 60 * 1000, kind: ' + "'verify_claim_test'");
3596
+ const skipOnBlock = src.includes('if (r.blocked) {') && src.includes('테스트 명령 차단') && src.includes('불일치 판정 아님');
3597
+ const label = src.includes('` - ${runResult.cmd ' + "|| 'npm test'} 실행:"); // P3: 하드코딩된 npm test 라벨 제거
3598
+ return authPass && skipOnBlock && label;
3599
+ } },
3600
+ { name: '재실증 P2 (1.18.1): task update id 뒤 non-path positional(status) 거부 + path-like 허용 (소스 가드)', run: () => {
3601
+ const src = read(__filename);
3602
+ return src.includes("_pos.slice(3).find(t => t && !t.startsWith('-') && !_pathLike(t))") && src.includes("알 수 없는 인자 '${stray}'") && src.includes('상태는 ${hint} 로 지정');
3603
+ } },
3604
+ { name: '위장 스텁 차단 (1.18.2): 빈 export 껍데기=스텁(우회형 포함), 이름붙은/재노출/실코드=정상 (행위)', run: () => {
3605
+ const E = _vcImplIsEmpty;
3606
+ // 스텁(true): 코드 0줄 · 빈 객체/배열 · 빈 함수/화살표 · export default {} · Python pass
3607
+ const base = E('// TODO\n') === true && E('// c\nmodule.exports = {};\n') === true && E('module.exports = {\n};\n') === true
3608
+ && E('exports = {}') === true && E('export default {}') === true && E('module.exports = []') === true
3609
+ && E('module.exports = () => {}') === true && E('module.exports = function(){}') === true && E('# todo\npass') === true;
3610
+ // 적대 워크플로 우회형(true 여야 함): Object.freeze · new Object · async function · exports.default · =>({}) · class{} · 인라인주석 · TS 캐스트
3611
+ const bypass = E('module.exports = Object.freeze({});\n') === true
3612
+ && E('module.exports = new Object();\n') === true
3613
+ && E('module.exports = async function(){};\n') === true
3614
+ && E('exports.default = {};\n') === true
3615
+ && E('module.exports = () => ({});\n') === true
3616
+ && E('module.exports = class {};\n') === true
3617
+ && E('module.exports = {}; // real code coming\n') === true
3618
+ && E('module.exports = {} as any;\n') === true;
3619
+ // 정상(false): 이름붙은 export · require 재노출 · export * · 실코드 · 멤버 객체 · 비어있지않은 freeze/class/arrow
3620
+ const real = E('function pay(n){ return n*2; }\nmodule.exports = { pay };\n') === false
3621
+ && E('module.exports = require("./pay");\n') === false && E('export * from "./x";\n') === false
3622
+ && E('module.exports = { a: 1 };\n') === false && E('module.exports = () => { return doStuff(); }') === false
3623
+ && E('export default { port: 3000 }') === false && E('class Foo {}\nmodule.exports = Foo;\n') === false
3624
+ && E('module.exports = Object.freeze({ a: 1 });\n') === false
3625
+ && E('module.exports = class { run(){ return 1; } };\n') === false
3626
+ && E('module.exports = (a,b) => a+b;\n') === false;
3627
+ return base && bypass && real;
3628
+ } },
3629
+ { name: '위장 스텁 차단 (1.18.2): stub 루프 _vcImplIsEmpty 사용 + 메시지 + FILE_EXTS java/php 정합 (소스 가드)', run: () => {
3630
+ const src = read(__filename);
3631
+ return src.includes('if (_vcImplIsEmpty(body)) stubFiles.push(c.file);') && src.includes('비주석 코드 0줄 또는 빈 export 껍데기')
3632
+ && /const FILE_EXTS = '[^']*\bjava\b[^']*\bphp\b[^']*'/.test(src);
3633
+ } },
3634
+ { name: '품질 렌즈 (1.18.3): 카탈로그 무결성 — 사용자 원문 질문 + affects 상호참조 유효 (행위)', run: () => {
3635
+ const keys = Object.keys(LENS_CATALOG);
3636
+ const refsOk = keys.every(k => Array.isArray(LENS_CATALOG[k].affects) && LENS_CATALOG[k].affects.every(a => keys.includes(a)) && LENS_CATALOG[k].questions.length >= 3 && LENS_CATALOG[k].persona);
3637
+ const userVerbatim = LENS_CATALOG.code.questions.some(q => q.includes('선임 개발자') && q.includes('복잡'))
3638
+ && LENS_CATALOG.design.persona.includes('선임 디자이너') && LENS_CATALOG.design.persona.includes('일반 사용자')
3639
+ && LENS_CATALOG.docs.questions.some(q => q.includes('30초'));
3640
+ return refsOk && userVerbatim;
3641
+ } },
3642
+ { name: '품질 렌즈 (1.18.3): lens 명령 표면 등재 + REPL 설치문항 제거 (소스 가드)', run: () => {
3643
+ const src = read(__filename);
3644
+ const surface = src.includes("if (cmd === 'lens')") && src.includes("cmd: 'lens [code|design|docs|test|security]") && src.includes('leerness lens [code|design|docs|test|security]');
3645
+ const replGone = !src.includes('설치 완료 후 REPL agent ' + '모드를 즉시 시작할까요') && src.includes('REPL agent 모드 진입 ' + '문항 제거');
3646
+ return surface && replGone;
3647
+ } },
3648
+ { name: 'GPT-5.5 평가 #8 (1.18.4, UR-0005): SECURITY.md 비공개 제보 채널 — 공개 이슈 안내 제거 (행위)', run: () => {
3649
+ const sp = path.join(path.dirname(__filename), '..', 'SECURITY.md');
3650
+ if (!exists(sp)) return true; // 패키지에 없으면 스킵(설치본 안전)
3651
+ const s = read(sp);
3652
+ const hasPrivate = /security\/advisories\/new/.test(s) && /Private Vulnerability Reporting/i.test(s) && /\[leerness security\]/.test(s);
3653
+ const warnsPublic = /공개 이슈로 올리지 마세요|Do NOT open a public issue/i.test(s);
3654
+ return hasPrivate && warnsPublic;
3655
+ } },
3656
+ { name: 'GPT-5.5 평가 #7 (1.18.4, UR-0006): audit 가 README 관리블록 synced 버전 lag 감지 (소스 가드)', run: () => {
3657
+ const a = read(path.join(path.dirname(__filename), '..', 'lib', 'audit.js'));
3658
+ return a.includes('Last synced by Leerness v') && a.includes('readme_synced_version_stale');
3659
+ } },
3560
3660
  { name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
3561
3661
  ];
3562
3662
  }
@@ -4085,6 +4185,75 @@ function pulseCmd(root) {
4085
4185
  // bridge (web/pc/lsp — opt-in)
4086
4186
  // config (init/migrate/update/auto-update/setup-agents/install/workspace-dir/wakeup-interval)
4087
4187
  // advanced (intent/requests/constraints/pre-wake-audit/idempotency/round-history/milestones/pulse)
4188
+ // 1.18.3 (UR-0003 사용자 명시): 분야별 자기질문 품질 렌즈 — AI 가 완료 선언 전 스스로 답해보는 질문 + 분야간 인과관계.
4189
+ // "선임 개발자가 내 코드를 보고 복잡하다고 느끼지 않을까?" / "선임 디자이너와 일반 사용자가 봤을 때 이쁘고 직관적인가?" (사용자 원문).
4190
+ // 질문에 "그렇다(통과)"라고 답할 수 없으면 아직 완료가 아님. affects = 이 분야를 바꿨을 때 다시 물어야 할 분야(인과관계).
4191
+ const LENS_CATALOG = {
4192
+ code: {
4193
+ title: '코드', persona: '선임 개발자',
4194
+ questions: [
4195
+ '선임 개발자가 이 코드를 보고 "복잡하다"고 느끼지 않을까? — 가볍고 단순해야 함',
4196
+ '더 단순한 방법이 있는데 추상화/패턴/옵션을 추가하고 있지 않은가?',
4197
+ '처음 보는 사람이 5분 안에 이 변경을 이해할 수 있는가?'
4198
+ ],
4199
+ affects: ['test', 'docs', 'design'], affectsNote: 'UI 를 만지는 코드 변경이면 design 질문 재확인 필수'
4200
+ },
4201
+ design: {
4202
+ title: '디자인/UX', persona: '선임 디자이너 + 일반 사용자',
4203
+ questions: [
4204
+ '선임 디자이너가 봤을 때 이쁘고 일관적인가?',
4205
+ '일반 사용자가 처음 봤을 때 편하고 직관적이며 헷갈리지 않는가?',
4206
+ '꾸미기 위해 복잡해지고 있지 않은가? — 단순함이 곧 직관'
4207
+ ],
4208
+ affects: ['code', 'docs'], affectsNote: '디자인 단순화는 보통 코드도 단순하게 만든다 (역도 성립)'
4209
+ },
4210
+ docs: {
4211
+ title: '문서/README', persona: '처음 온 사용자 (비개발자 포함)',
4212
+ questions: [
4213
+ '그래서 30초 안에 뭘 해보면 되지?',
4214
+ '비개발자가 터미널 명령 하나 없이 어떻게 사용하지?',
4215
+ '기존 도구가 이미 있는데 이걸 쓸 이유가 뭐지?'
4216
+ ],
4217
+ affects: ['design'], affectsNote: '문서가 어렵다면 보통 제품 흐름(UX) 자체가 어렵다는 신호'
4218
+ },
4219
+ test: {
4220
+ title: '테스트', persona: '검증자',
4221
+ questions: [
4222
+ '이 테스트는 실패할 수 있는 테스트인가? (assert(true) 아님)',
4223
+ '주장한 테스트 개수/통과가 실측과 일치하는가?',
4224
+ '테스트가 구현을 실제로 import/호출하는가?'
4225
+ ],
4226
+ affects: ['code'], affectsNote: '테스트하기 어렵다면 코드가 복잡하다는 신호 — code 질문으로 돌아갈 것'
4227
+ },
4228
+ security: {
4229
+ title: '보안', persona: '공격자',
4230
+ questions: [
4231
+ '시크릿이 코드/커밋에 들어가지 않았는가?',
4232
+ '이 입력을 악의적으로 주면 어떻게 되는가?',
4233
+ '권한/경계를 한 단어 비틀기로 우회할 수 있는가?'
4234
+ ],
4235
+ affects: ['code', 'test'], affectsNote: '보안 가드를 넣었다면 우회/오탐 테스트가 따라와야 함'
4236
+ }
4237
+ };
4238
+ function lensCmd(domain, opts = {}) {
4239
+ const jsonMode = !!opts.json || has('--json');
4240
+ if (domain && !LENS_CATALOG[domain]) {
4241
+ return fail(`알 수 없는 렌즈: ${domain} — 유효값: ${Object.keys(LENS_CATALOG).join(', ')}`);
4242
+ }
4243
+ const picked = domain ? { [domain]: LENS_CATALOG[domain] } : LENS_CATALOG;
4244
+ if (jsonMode) { log(JSON.stringify({ ok: true, lenses: picked }, null, 2)); return; }
4245
+ log(`# leerness lens — 분야별 자기질문 품질 렌즈 (1.18.3)`);
4246
+ log(`완료 선언 전 해당 분야 질문에 스스로 답해보세요. "그렇다(통과)"라고 답할 수 없으면 아직 완료가 아닙니다.`);
4247
+ for (const [key, l] of Object.entries(picked)) {
4248
+ log('');
4249
+ log(`## ${key} (${l.title}) — 페르소나: ${l.persona}`);
4250
+ l.questions.forEach((q, i) => log(` ${i + 1}. ${q}`));
4251
+ log(` ↔ 인과: ${key} 를 바꾸면 → ${l.affects.join(', ')} 질문도 다시 — ${l.affectsNote}`);
4252
+ }
4253
+ log('');
4254
+ log(`사용: leerness lens <${Object.keys(LENS_CATALOG).join('|')}> · 완료 검증과 함께: leerness verify-claim T-XXXX`);
4255
+ }
4256
+
4088
4257
  function commandsCmd(root) {
4089
4258
  const isTty = process.stdout && process.stdout.isTTY;
4090
4259
  const cy = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
@@ -4121,7 +4290,8 @@ function commandsCmd(root) {
4121
4290
  { cmd: 'scan secrets [path]', desc: '시크릿 탐지' },
4122
4291
  { cmd: 'encoding check [path]', desc: '인코딩 검증' },
4123
4292
  { cmd: 'lazy detect [path] [--json]', desc: '게으른 작업 감지 (1.9.101)' },
4124
- { cmd: 'verify-claim <T-ID> [--run-tests] [--strict-claims] [--require-evidence]', desc: '주장 검증 (1.9.18~26) — --require-evidence: done 주장에 파일+테스트 근거 강제 (1.9.287)' },
4293
+ { cmd: 'verify-claim <T-ID> [--run-tests] [--test-cmd "<명령>"] [--strict-claims] [--require-evidence]', desc: '주장 검증 (1.9.18~26) — --require-evidence: done 주장에 파일+테스트 근거 강제 (1.9.287) · --test-cmd: 비-JS 테스트 명령 (1.17.2)' },
4294
+ { cmd: 'lens [code|design|docs|test|security] [--json]', desc: '분야별 자기질문 품질 렌즈 + 분야간 인과관계 (1.18.3)' },
4125
4295
  { cmd: 'optimism-check <T-ID>', desc: '낙관적 API 감지 (1.9.26)' },
4126
4296
  { cmd: 'requests audit|list|complete|drop|auto-complete', desc: '사용자 요청 추적 (1.9.207/223)' },
4127
4297
  { cmd: 'pre-wake-audit [path] [--last]', desc: 'sleep 전 점검 (1.9.209)' },
@@ -6445,7 +6615,7 @@ function planListCmd(root, opts = {}) {
6445
6615
  // ### M-XXXX. <title> 블록 추출
6446
6616
  const blocks = text.split(/\n(?=### M-\d{4}\.)/);
6447
6617
  for (const b of blocks) {
6448
- const headerMatch = b.match(/^### (M-\d{4})\.\s*(.+?)$/m);
6618
+ const headerMatch = b.match(/^### (M-\d{4})\.[ \t]*(.+?)$/m);
6449
6619
  if (!headerMatch) continue;
6450
6620
  const id = headerMatch[1];
6451
6621
  const title = headerMatch[2].trim();
@@ -6715,9 +6885,36 @@ function taskAdd(root, text) {
6715
6885
  } catch {} // review 실패는 task add 자체에 영향 X
6716
6886
  }
6717
6887
  }
6888
+ // 1.17.5 (UR-0048, 5축 실증 P2): 모르는 옵션 조용히 무시 차단 — `task update --next-action "x"` 처럼 오타/미존재 플래그가
6889
+ // "✓ task updated" 와 함께 값을 버려, 쓴 에이전트는 기록됐다고 믿고 다음 에이전트는 placeholder 를 받는(인수인계 유실) 최악의 실패 양식.
6890
+ // prefix 기반 did-you-mean(--next-action → --next) + exit 1. 전역 플래그(--path/--json/--force/--lenient)는 항상 허용.
6891
+ function _rejectUnknownFlags(allowed, usageHint) {
6892
+ const all = new Set([...allowed, '--path', '--json', '--force', '--lenient']);
6893
+ const seen = process.argv.slice(2).filter(a => a.startsWith('--')).map(a => a.split('=')[0]);
6894
+ const unknown = [...new Set(seen.filter(f => !all.has(f)))];
6895
+ if (!unknown.length) return true;
6896
+ const dym = (u) => { const c = [...all].find(k => u.startsWith(k) || k.startsWith(u)); return c ? ` — 혹시 ${c}?` : ''; };
6897
+ failJson(has('--json'), 'unknown_flag', `알 수 없는 옵션: ${unknown.map(u => u + dym(u)).join(', ')} (지원: ${[...allowed].join(' ')}${usageHint ? ' · ' + usageHint : ''}) — 값이 조용히 버려지는 것을 방지하기 위해 거부`);
6898
+ return false;
6899
+ }
6900
+
6718
6901
  function taskUpdate(root, id) {
6719
6902
  if (!_requireInit(root, 'task update')) return; // 1.9.311 (UR-0047): init 가드
6903
+ if (!_rejectUnknownFlags(['--status', '--evidence', '--next', '--note'], 'task update T-0001 --status done --evidence "..." --next "..."')) { process.exitCode = 1; return; } // 1.17.5 (UR-0048)
6720
6904
  if (!id) return fail('id required (e.g., task update T-0001 --status in-progress)');
6905
+ // 1.18.1 (재실증 신규 P2): id 뒤 떠도는 non-path positional 거부 — `task update T-0003 done` 처럼 상태를 위치인자로 주면
6906
+ // 조용히 무시되고 "✓ updated" 가 출력돼 done 이 안 됨 → verify-claim/close 의 정직성 검사가 통째로 건너뛰던 데이터 정합 구멍.
6907
+ // 단, path-like positional(/abs, ./rel, C:\ — UR-0141 task 계열 positional path)은 워크스페이스 경로이므로 허용.
6908
+ {
6909
+ const _pos = nonFlagArgs(); // ['task','update','<id>', ...rest]
6910
+ const _pathLike = (t) => /^([A-Za-z]:[\\/]|\/|\.\.?[\\/])/.test(t);
6911
+ const stray = _pos.slice(3).find(t => t && !t.startsWith('-') && !_pathLike(t));
6912
+ if (stray) {
6913
+ const known = TASK_STATUSES.has(stray);
6914
+ const hint = known ? `--status ${stray}` : `--status <${[...TASK_STATUSES].join('|')}>`;
6915
+ return fail(`알 수 없는 인자 '${stray}' — 상태는 ${hint} 로 지정하세요 (예: task update ${id} ${known ? '--status ' + stray : '--status done'} --evidence "...")`);
6916
+ }
6917
+ }
6721
6918
  if (!_validateChoice(arg('--status', null), TASK_STATUSES, 'task status')) { process.exitCode = 1; return; } // 1.9.310 (UR-0046)
6722
6919
  const rows = readProgressRows(root);
6723
6920
  if (!rows.find(r => r.id === id)) { fail(`task ${id} not found in progress-tracker.md`); return; }
@@ -7147,7 +7344,7 @@ function _jaccard(a, b) {
7147
7344
  function taskRelink(root) {
7148
7345
  root = absRoot(root);
7149
7346
  const planText = exists(planPath(root)) ? read(planPath(root)) : '';
7150
- const milestones = [...planText.matchAll(/^### (M-\d{4})\.\s*(.+?)$/gm)]
7347
+ const milestones = [...planText.matchAll(/^### (M-\d{4})\.[ \t]*(.+?)$/gm)]
7151
7348
  .map(m => ({ id: m[1], text: m[2].trim() }));
7152
7349
  const rows = readProgressRows(root);
7153
7350
  const linkedM = new Set(rows.map(r => (r.evidence.match(/M-\d{4}/) || [])[0]).filter(Boolean));
@@ -9573,6 +9770,46 @@ function _gitChangedFiles(root) {
9573
9770
  }
9574
9771
  // 주장 파일이 git 변경 집합에 있는지(상대경로 prefix 차이 허용).
9575
9772
  // _claimFileInGit → lib/analyzers.js (1.9.304 UR-0025)
9773
+
9774
+ // 1.18.2 (재실증 후속, 위장 스텁 차단): 주장된 구현 파일이 "실체 없는 껍데기"인지 판정 (verify-claim 구현 실체 검사).
9775
+ // ① 비주석 코드줄 0 (기존 1.17.3) — 주석/공백뿐.
9776
+ // ② 빈 export 껍데기 — 비주석 코드가 빈 객체/배열/빈 함수 export(또는 Python pass)뿐: module.exports={} / export default {} / module.exports=()=>{} / pass.
9777
+ // FP 가드(스텁 아님): 이름붙은 export(module.exports={ a, b }) · 재노출(module.exports=require('./x') / export * from) · 실제 선언/로직.
9778
+ // 1.18.1 재실증에서 `module.exports = {};` 한 줄짜리 위장 스텁이 "구현 실체 ✓"로 통과(+require 만 하는 가짜 테스트와 결합 시 --strict 도 exit 0)하던 우회 차단.
9779
+ // 빈 값(zero-logic) producer: 빈 객체/배열 · 빈 컨테이너 생성자/래퍼 · 빈 함수/화살표 · 빈 클래스.
9780
+ // 1.18.2 적대 워크플로(우회 헌터)가 찾은 P1 우회 전부 포함: Object.freeze({}) · new Object() · async function(){} · ()=>({}).
9781
+ // FP 0(오탐 헌터 ~45 합법패턴 통과): 내부가 비어야만 매치 → Object.freeze({a:1}) · class{ m(){} } · (a,b)=>a+b 등은 불매치.
9782
+ const _VC_EMPTY_VAL = '(?:' + [
9783
+ '\\{\\s*\\}', // {}
9784
+ '\\[\\s*\\]', // []
9785
+ 'Object\\.freeze\\(\\s*(?:\\{\\s*\\}|\\[\\s*\\])\\s*\\)', // Object.freeze({}) / ([])
9786
+ '(?:new\\s+)?(?:Object|Array)\\s*\\(\\s*\\)', // new Object() / Object() / new Array()
9787
+ '(?:async\\s+)?function\\s*\\*?\\s*[A-Za-z0-9_$]*\\s*\\([^)]*\\)\\s*\\{\\s*\\}', // (async) function name?(){}
9788
+ '(?:async\\s+)?\\([^)]*\\)\\s*=>\\s*(?:\\{\\s*\\}|\\(\\s*\\{\\s*\\}\\s*\\))', // (async)(...)=>{} | =>({})
9789
+ 'class\\s+[A-Za-z0-9_$]*\\s*\\{\\s*\\}', // class Name? {} (직접 export 형)
9790
+ ].join('|') + ')';
9791
+ const _VC_EMPTY_SHELL_RE = new RegExp(
9792
+ '^(?:' + [
9793
+ '(?:module\\.)?exports(?:\\.[A-Za-z0-9_$]+)?\\s*=\\s*' + _VC_EMPTY_VAL, // module.exports[.x] = EMPTY (exports.default 포함)
9794
+ 'export\\s+default\\s*' + _VC_EMPTY_VAL, // export default EMPTY
9795
+ 'export\\s*\\{\\s*\\}', // export {}
9796
+ 'pass', // python pass
9797
+ ].join('|') + ')(?:\\s+as\\s+[A-Za-z0-9_$.<>\\[\\] ]+?)?\\s*;?$' // 선택적 TS 캐스트(as any) + ;
9798
+ );
9799
+ function _vcImplIsEmpty(body) {
9800
+ if (typeof body !== 'string' || !body) return false;
9801
+ // 블록주석 제거 → 줄별 trim/주석줄 제거 → 인라인 // 주석 제거(따옴표 없는 줄만, 문자열 보호) → join.
9802
+ const codeLines = body.replace(/\/\*[\s\S]*?\*\//g, '').split('\n').map(l => {
9803
+ let t = l.trim();
9804
+ if (!t || t.startsWith('//') || t.startsWith('#')) return '';
9805
+ if (!/['"`]/.test(t)) t = t.replace(/\s*\/\/.*$/, '').trim(); // 1.18.2: 같은-줄 인라인 주석 우회(`{}; // ...`) 차단
9806
+ return t;
9807
+ }).filter(Boolean);
9808
+ if (codeLines.length === 0) return true; // ① 코드 0줄
9809
+ const joined = codeLines.join(' ').replace(/\s+/g, ' ').trim();
9810
+ return _VC_EMPTY_SHELL_RE.test(joined); // ② 빈 export 껍데기뿐
9811
+ }
9812
+
9576
9813
  function verifyClaimCmd(root, taskId) {
9577
9814
  root = absRoot(root);
9578
9815
  const _j = has('--json'); // 1.9.400 (7번째 버그헌트 P1-B, UR-0105): --json 에러도 구조화
@@ -9587,7 +9824,8 @@ function verifyClaimCmd(root, taskId) {
9587
9824
  // 변경: 확장자 화이트리스트 기반. 디렉토리는 선택적 (project.godot 같은 루트 파일도 잡음).
9588
9825
  // 확장자는 길이 내림차순(긴 것 먼저 매치) + \b 종결로 .ts vs .tscn 구분.
9589
9826
  // 1.9.21: 설정/메타 파일 확장자 추가 — Godot export_presets.cfg 등 false negative 보완
9590
- const FILE_EXTS = 'webmanifest|dockerfile|properties|tscn|tres|godot|json5|jsx|tsx|yaml|html|scss|sass|less|gltf|conf|json|toml|lock|mdx|xml|css|svg|yml|cfg|ini|env|md|js|ts|gd|cs|py|rb|go|rs|kt|sh|h';
9827
+ // 1.18.2: java|php|mjs|cjs 추가 — _VC_CODE_EXT 와 정합(이전엔 .java/.php 임플 주장이 추출조차 안 돼 스텁/존재 검사를 무검사 통과).
9828
+ const FILE_EXTS = 'webmanifest|dockerfile|properties|tscn|tres|godot|json5|java|jsx|tsx|yaml|html|scss|sass|less|gltf|conf|json|toml|lock|mdx|xml|css|svg|yml|cfg|ini|env|php|mjs|cjs|md|js|ts|gd|cs|py|rb|go|rs|kt|sh|h';
9591
9829
  const FILE_RE = new RegExp(`(?:[A-Za-z][A-Za-z0-9_-]*\\/)?[A-Za-z][\\w./-]*\\.(?:${FILE_EXTS})\\b`, 'g');
9592
9830
  const filePatterns = evidence.match(FILE_RE) || [];
9593
9831
  // 중복 제거 + "tests/test.js" 같은 결과를 유지 (이미 `..` 없으니 그대로)
@@ -9612,6 +9850,11 @@ function verifyClaimCmd(root, taskId) {
9612
9850
  // 4) N개 테스트 (단순 카운트)
9613
9851
  const m4 = evidence.match(/(\d+)\s*개\s*테스트/);
9614
9852
  if (m4) declaredTestCount = parseInt(m4[1], 10);
9853
+ // 4b) 테스트 N개 (1.17.4 UR-0047: 한국어 자연어순 '테스트 50개 통과' — 이전 미인식으로 부풀린 주장이 카운트 검증을 아예 안 탔음)
9854
+ if (!declaredTestCount) {
9855
+ const m4b = evidence.match(/테스트\s*(\d+)\s*개/);
9856
+ if (m4b) declaredTestCount = parseInt(m4b[1], 10);
9857
+ }
9615
9858
  // 5) N tests (영문 단순 카운트)
9616
9859
  if (!declaredTestCount) {
9617
9860
  const m5 = evidence.match(/(\d+)\s*tests?\b/i);
@@ -9620,6 +9863,27 @@ function verifyClaimCmd(root, taskId) {
9620
9863
 
9621
9864
  // 실제 파일 존재 검사
9622
9865
  const fileChecks = files.map(f => ({ file: f, exists: exists(path.join(root, f)) }));
9866
+
9867
+ // 1.17.3 (UR-0046 범용성 P1②): 빈껍데기(스텁) 구현 + 테스트-구현 연결 검사 — "주석뿐 구현 + assert(true) 테스트"가 verify-claim 을 exit 0 으로 통과하던 공격(5축 실증 Attack C) 차단.
9868
+ // ① 스텁: 주장된 코드 파일(테스트 제외)의 비주석 코드줄이 0 이면 확정 스텁 — done 게이팅 FAIL(확실 신호만, 과탐 0).
9869
+ // ② 연결: 주장에 구현+테스트가 모두 있는데 어떤 테스트도 구현 파일명(basename)을 참조하지 않으면 — 기본 advisory ⚠, --strict-claims 시 FAIL.
9870
+ const _VC_CODE_EXT = /\.(js|mjs|cjs|jsx|ts|tsx|py|rb|go|rs|java|cs|php)$/i;
9871
+ const _VC_TEST_PAT = /(^|[\\/])(test_[^\\/]+\.[a-z]+|[^\\/]+[._-]test\.[a-z]+|[^\\/]+\.spec\.[a-z]+)$|(^|[\\/])tests?[\\/]/i;
9872
+ const stubFiles = [];
9873
+ for (const c of fileChecks) {
9874
+ if (!c.exists || !_VC_CODE_EXT.test(c.file) || _VC_TEST_PAT.test(c.file)) continue;
9875
+ let body = ''; try { body = read(path.join(root, c.file)); } catch { continue; }
9876
+ if (!body || body.length > 512 * 1024) continue;
9877
+ // 1.18.2: 코드 0줄(기존) + 빈 export 껍데기(위장 스텁) 통합 판정.
9878
+ if (_vcImplIsEmpty(body)) stubFiles.push(c.file);
9879
+ }
9880
+ const _vcImpl = fileChecks.filter(c => c.exists && _VC_CODE_EXT.test(c.file) && !_VC_TEST_PAT.test(c.file)).map(c => c.file);
9881
+ const _vcTests = fileChecks.filter(c => c.exists && _VC_CODE_EXT.test(c.file) && _VC_TEST_PAT.test(c.file)).map(c => c.file);
9882
+ let testLinkOk = null; // null = 판단 불가(구현·테스트가 함께 주장되지 않음)
9883
+ if (_vcImpl.length && _vcTests.length) {
9884
+ const bases = _vcImpl.map(f => path.basename(f).replace(/\.[a-z]+$/i, ''));
9885
+ testLinkOk = _vcTests.some(tf => { let t = ''; try { t = read(path.join(root, tf)); } catch { return false; } return bases.some(b => b && t.includes(b)); });
9886
+ }
9623
9887
  // 1.9.302 (UR-0042, 외부리뷰 Opus G-1): git diff 시맨틱 교차검증 — 주장한 파일이 실제로 변경됐는가.
9624
9888
  // "파일 존재"만으로는 "테스트만 통과하면 done" 허위완료를 못 막음(Opus). git working tree+직전커밋 변경과 대조.
9625
9889
  const gitChanged = _gitChangedFiles(root); // Set | null(git repo 아님 → 검증 불가)
@@ -9629,33 +9893,61 @@ function verifyClaimCmd(root, taskId) {
9629
9893
  // 1.13.2 (Karpathy 가이드라인 3 "외과적 변경", UR-0030): 역방향 교차검증 — git 에 변경됐으나 evidence/주장에 없는 파일(scope-creep / 요청 범위 밖 변경 신호). 하네스 자체 기록(.harness 등)은 제외. advisory(오탐 방지 — 기본 FAIL 아님, 표면화만).
9630
9894
  const _SCOPE_SKIP = /^(\.harness[\\/]|\.git[\\/]|node_modules[\\/]|\.claude[\\/]|dist[\\/]|build[\\/])/;
9631
9895
  const changedNotClaimed = gitApplicable ? [...gitChanged].filter(g => !_SCOPE_SKIP.test(g) && !files.some(f => _claimFileInGit(f, new Set([g])))) : [];
9632
- // 테스트 카운트: tests/test.js의 check( 또는 it( 또는 test( 개수
9896
+ // 테스트 카운트 (1.17.4, UR-0047): 주장된 테스트 파일 우선 + 관례 글롭(pytest test_*.py·*_test.py / 루트·tests/ 의 *.test.*·*.spec.*) 인식.
9897
+ // 이전엔 tests/test.js 등 3개 하드코딩 — pytest/node:test 루트 관례가 안 보여 "파일 못 찾음"인데 ✓ pass 로 표기(측정실패=통과 역전, 5축 실증 P2 공통).
9898
+ const _countTests = (fp) => {
9899
+ let t = ''; try { t = read(fp); } catch { return 0; }
9900
+ if (/\.py$/i.test(fp)) { const d = (t.match(/^\s*def\s+test_/gm) || []).length; return d || (t.match(/^\s*assert\b/gm) || []).length; }
9901
+ return (t.match(/\bcheck\s*\(/g) || t.match(/\b(it|test)\s*\(/g) || []).length;
9902
+ };
9633
9903
  let actualTestCount = null;
9634
- const candidateTestFiles = ['tests/test.js', 'test/test.js', 'tests/index.js'];
9635
- for (const tf of candidateTestFiles) {
9636
- const tp = path.join(root, tf);
9637
- if (exists(tp)) {
9638
- const t = read(tp);
9639
- actualTestCount = (t.match(/\bcheck\s*\(/g) || t.match(/\b(it|test)\s*\(/g) || []).length;
9640
- break;
9904
+ if (_vcTests.length) {
9905
+ actualTestCount = _vcTests.reduce((a, f) => a + _countTests(path.join(root, f)), 0);
9906
+ } else {
9907
+ const found = new Set();
9908
+ for (const tf of ['tests/test.js', 'test/test.js', 'tests/index.js']) if (exists(path.join(root, tf))) found.add(tf);
9909
+ if (!found.size) {
9910
+ for (const dir of ['', 'tests', 'test']) {
9911
+ let ents = []; try { ents = fs.readdirSync(path.join(root, dir)); } catch { continue; }
9912
+ for (const e of ents) if (/^test_.+\.py$|_test\.py$|\.test\.[cm]?[jt]sx?$|\.spec\.[cm]?[jt]sx?$/i.test(e)) found.add(dir ? dir + '/' + e : e);
9913
+ if (found.size) break; // 루트 우선 (루트에 있으면 tests/ 중복 스캔 안 함)
9914
+ }
9641
9915
  }
9916
+ if (found.size) actualTestCount = [...found].reduce((a, f) => a + _countTests(path.join(root, f)), 0);
9642
9917
  }
9918
+ const testMeasured = actualTestCount != null;
9643
9919
 
9644
- // 1.9.19: --run-tests — npm test 자동 실행 + pass/fail 파싱
9920
+ // 1.9.19: --run-tests — 테스트 자동 실행 + pass/fail 파싱
9921
+ // 1.17.2 (UR-0045 범용성 P1): 테스트 명령 해석 체인 — --test-cmd > leerness-config.json testCommand > 실제 npm test 스크립트 > skip.
9922
+ // 이전엔 npm test 하드코딩 → 비-JS(파이썬 등) 프로젝트에서 npm init 잔재 placeholder("no test specified"&&exit 1)가 실행돼
9923
+ // 테스트 전부 통과한 작업을 "주장 불일치 FAIL"로 오판(5축 클린룸 실증 P1). placeholder 는 테스트가 아니므로 skip 처리.
9645
9924
  let runResult = null;
9646
9925
  if (has('--run-tests')) {
9647
- const pkgPath = path.join(root, 'package.json');
9648
- if (!exists(pkgPath)) {
9649
- runResult = { skipped: true, reason: 'package.json 없음' };
9926
+ let testCmd = arg('--test-cmd', null);
9927
+ if (!testCmd) {
9928
+ try { const cfg = JSON.parse(read(path.join(root, '.harness', 'leerness-config.json'))); if (cfg && typeof cfg.testCommand === 'string' && cfg.testCommand.trim()) testCmd = cfg.testCommand.trim(); } catch {}
9929
+ }
9930
+ if (!testCmd) {
9931
+ const pkgPath = path.join(root, 'package.json');
9932
+ if (exists(pkgPath)) {
9933
+ let pkg = null;
9934
+ try { pkg = JSON.parse(read(pkgPath)); } catch {}
9935
+ const ts = pkg && pkg.scripts && pkg.scripts.test;
9936
+ if (ts && !/no test specified/i.test(ts)) testCmd = 'npm test';
9937
+ }
9938
+ }
9939
+ if (!testCmd) {
9940
+ runResult = { skipped: true, reason: '테스트 명령 미지정 — 비-JS 프로젝트는 --test-cmd "<명령>" 또는 .harness/leerness-config.json 의 "testCommand" 로 지정 (불일치 판정 아님)' };
9650
9941
  } else {
9651
- let pkg = null;
9652
- try { pkg = JSON.parse(read(pkgPath)); } catch {}
9653
- const hasTestScript = pkg && pkg.scripts && pkg.scripts.test;
9654
- if (!hasTestScript) {
9655
- runResult = { skipped: true, reason: 'scripts.test 없음' };
9656
- } else {
9657
- // 1.9.299 (UR-0039): 신뢰 못 할 워크스페이스 npm test → runCommandSafe + scrubSecrets (시크릿 노출 차단 + cwd jail).
9658
- const r = runCommandSafe('npm test', [], { cwd: root, root, encoding: 'utf8', allowShell: true, scrubSecrets: true, timeout: 5 * 60 * 1000, kind: 'verify_claim_test' });
9942
+ {
9943
+ // 1.9.299 (UR-0039): 신뢰 못 할 워크스페이스 테스트 실행 → runCommandSafe + scrubSecrets (시크릿 노출 차단 + cwd jail).
9944
+ // 1.18.1 (재실증 P1): testCmd 사용자 명시(--test-cmd/config) → userAuthorized 로 basic 모드 allowList 우회(cwd jail 은 유지).
9945
+ // 이전엔 python/pytest 등 비-JS 인터프리터가 권한 차단(126) verify-claim 이 "주장 불일치" 거짓 FAIL 을 냈음.
9946
+ const r = runCommandSafe(testCmd, [], { cwd: root, root, encoding: 'utf8', allowShell: true, scrubSecrets: true, userAuthorized: true, timeout: 5 * 60 * 1000, kind: 'verify_claim_test' });
9947
+ // 1.18.1: 권한/jail 로 차단된 실행은 "테스트 실패" 가 아니라 "측정 불가" — 절대 불일치 판정으로 둔갑시키지 않음(skip).
9948
+ if (r.blocked) {
9949
+ runResult = { skipped: true, reason: `테스트 명령 차단(${r.error}) '${testCmd}' 실행 불가 (불일치 판정 아님). leerness permissions set extended 또는 allowList 추가 권장` };
9950
+ } else {
9659
9951
  const out = (r.stdout || '') + (r.stderr || '');
9660
9952
  // 1.9.20: 파싱 패턴 확장 — 한국어 + jest/mocha/tap/vitest
9661
9953
  let parsed = null;
@@ -9677,12 +9969,19 @@ function verifyClaimCmd(root, taskId) {
9677
9969
  const m4 = out.match(/#\s*pass\s+(\d+)/i);
9678
9970
  if (m4) parsed = { num: parseInt(m4[1], 10), denom: parseInt(m4[1], 10) };
9679
9971
  }
9972
+ // 5) pytest: "N passed in 0.12s" (UR-0045 — 파이썬 러너 출력 인식)
9973
+ if (!parsed) {
9974
+ const m5 = out.match(/(\d+)\s+passed\b/i);
9975
+ if (m5) parsed = { num: parseInt(m5[1], 10), denom: parseInt(m5[1], 10) };
9976
+ }
9680
9977
  runResult = {
9681
9978
  skipped: false,
9979
+ cmd: testCmd,
9682
9980
  exitCode: r.status,
9683
9981
  parsed,
9684
9982
  allPassed: r.status === 0 && (!parsed || (parsed && parsed.num === parsed.denom))
9685
9983
  };
9984
+ }
9686
9985
  }
9687
9986
  }
9688
9987
  }
@@ -9723,11 +10022,14 @@ function verifyClaimCmd(root, taskId) {
9723
10022
  actual: { fileChecks, testCount: actualTestCount },
9724
10023
  verdict: {
9725
10024
  filesAllExist,
9726
- testCountMatch: declaredTestCount == null || actualTestCount == null || actualTestCount >= declaredTestCount,
10025
+ testCountMatch: declaredTestCount == null ? null : (!testMeasured ? null : actualTestCount >= declaredTestCount), // 1.17.4 (UR-0047): null=측정불가(검증 미수행 — pass 아님), false 만 게이팅
9727
10026
  evidenceComplete: !mustHaveEvidence ? null : evq.ok,
9728
10027
  claimsConsistent: !claimsChecked ? null : strictOk, // 1.11.2 (UR-0175): optimism+정직성 (기본 게이팅)
9729
- gitCrossCheck: !gitApplicable ? null : !gitStrongMismatch // 1.11.2 (UR-0175): git 교차검증 (머신 경로 노출)
10028
+ gitCrossCheck: !gitApplicable ? null : !gitStrongMismatch, // 1.11.2 (UR-0175): git 교차검증 (머신 경로 노출)
10029
+ implementationSubstance: stubFiles.length === 0, // 1.17.3 (UR-0046): 주장된 구현이 주석/빈껍데기뿐이면 false
10030
+ testImplLink: testLinkOk // 1.17.3 (UR-0046): 테스트가 구현을 참조하는가 (null=판단불가)
9730
10031
  },
10032
+ stubFiles: stubFiles.slice(0, 10),
9731
10033
  evidence: { required: mustHaveEvidence, ...evq },
9732
10034
  claims: !claimsChecked ? null : { ok: strictOk, optimism: optimismSuspects.map(s => ({ kind: s.kind, label: s.label })), honesty: honestyFindings.map(f => ({ dim: f.dim, label: f.label })) },
9733
10035
  git: gitChanged === null ? { applicable: false, reason: 'not-a-git-repo' } : (!gitApplicable ? { applicable: false, reason: 'no-working-changes-or-no-claimed-files' } : { applicable: true, claimedInGit: claimedInGit.length, claimedNotInGit, strongMismatch: gitStrongMismatch, changedNotClaimed }),
@@ -9744,7 +10046,7 @@ function verifyClaimCmd(root, taskId) {
9744
10046
  log(JSON.stringify(out, null, 2));
9745
10047
  if (runResult && !runResult.skipped && !runResult.allPassed) return process.exit(1);
9746
10048
  // 1.11.2 (UR-0175): --json 도 optimism+git 게이팅 — 머신 경로가 허위완료를 통과시키지 않도록(human 경로와 동일).
9747
- if (!filesAllExist || !out.verdict.testCountMatch || !evidenceQualityOk || (claimsChecked && !strictOk) || !gitClaimOk) return process.exit(1);
10049
+ if (!filesAllExist || out.verdict.testCountMatch === false || !evidenceQualityOk || (claimsChecked && !strictOk) || !gitClaimOk || (claimsChecked && stubFiles.length > 0) || (has('--strict-claims') && testLinkOk === false)) return process.exit(1); // 1.17.3 (UR-0046): 스텁(done 기본) + 테스트 미연결(strict). 1.17.4 (UR-0047): testCountMatch null(측정불가)은 미기여
9748
10050
  return;
9749
10051
  }
9750
10052
 
@@ -9762,15 +10064,15 @@ function verifyClaimCmd(root, taskId) {
9762
10064
  log(`## 🧪 테스트 카운트`);
9763
10065
  if (declaredPass) log(` 주장 (pass): ${declaredPass.num}/${declaredPass.denom}`);
9764
10066
  if (declaredTestCount) log(` 주장 (개수): ${declaredTestCount}개`);
9765
- if (actualTestCount != null) log(` 실측: tests/test.js에 ${actualTestCount}개 check/test 호출`);
9766
- else log(` 실측: 테스트 파일 못 찾음 (tests/test.js )`);
10067
+ if (actualTestCount != null) log(` 실측: ${actualTestCount}개 테스트 호출 (${_vcTests.length ? '주장된 테스트 파일' : '관례 탐색: 루트/tests·test_*.py·*.test.*'})`);
10068
+ else log(` 실측: 테스트 파일 못 찾음 — 카운트 검증 미수행 (pass 아님)`);
9767
10069
 
9768
10070
  // 1.9.19: --run-tests 결과
9769
10071
  let runTestsOk = true;
9770
10072
  let declaredPassMatchesActual = true;
9771
10073
  if (runResult) {
9772
10074
  log('');
9773
- log(`## 🚦 npm test 실행 (--run-tests)`);
10075
+ log(`## 🚦 ${runResult.cmd || '테스트'} 실행 (--run-tests)`);
9774
10076
  if (runResult.skipped) {
9775
10077
  log(` ⚠ skipped: ${runResult.reason}`);
9776
10078
  } else {
@@ -9792,9 +10094,10 @@ function verifyClaimCmd(root, taskId) {
9792
10094
  const testOk = declaredTestCount == null || actualTestCount == null || actualTestCount >= declaredTestCount;
9793
10095
  log(`## 종합`);
9794
10096
  log(` - 파일 모두 존재: ${allFilesOk ? '✓ pass' : '✗ FAIL (일부 누락)'}`);
9795
- log(` - 테스트 카운트: ${testOk ? ' pass (실측 주장)' : '⚠ 주장보다 적음'}`);
10097
+ // 1.17.4 (UR-0047): 측정 불가는 '통과' 아니라 '검증 미수행' — 이전엔 실측 0 인데 pass(실측≥주장) 모순 표기.
10098
+ log(` - 테스트 카운트: ${declaredTestCount == null ? '⊘ (주장 없음)' : !testMeasured ? `⊘ 측정 불가 — 주장 ${declaredTestCount}개 검증 미수행 (pass 아님)` : testOk ? '✓ pass (실측 ≥ 주장)' : '⚠ 주장보다 적음'}`);
9796
10099
  if (runResult && !runResult.skipped) {
9797
- log(` - npm test 실행: ${runTestsOk ? '✓ all passed' : '✗ FAIL'}`);
10100
+ log(` - ${runResult.cmd || 'npm test'} 실행: ${runTestsOk ? '✓ all passed' : '✗ FAIL'}`);
9798
10101
  if (declaredPass) log(` - 주장과 실행 결과 일치: ${declaredPassMatchesActual ? '✓ pass' : '⚠ 다름'}`);
9799
10102
  }
9800
10103
  // 1.11.2 (UR-0175): optimism+정직성 — done 주장은 기본 게이팅(claimsChecked). 완화: --lenient.
@@ -9822,7 +10125,18 @@ function verifyClaimCmd(root, taskId) {
9822
10125
  log(` - evidence 완전성 (done 기본 강제): ${evidenceQualityOk ? '✓ pass (파일+테스트 근거 있음)' : `✗ FAIL (누락: ${evq.missing.join(', ')})`}`);
9823
10126
  if (!evidenceQualityOk) log(` · done 주장은 수정 파일 경로 + 테스트명/개수 가 evidence 에 있어야 함 (테스트 통과만으로는 불충분). 완화: --lenient`);
9824
10127
  }
9825
- const overallFail = !allFilesOk || !testOk || (runResult && !runResult.skipped && !runTestsOk) || (claimsChecked && !strictOk) || !evidenceQualityOk || !gitClaimOk;
10128
+ // 1.17.3 (UR-0046): 구현 실체(스텁) + 테스트-구현 연결 Attack C(주석뿐 구현+assert(true)) 차단.
10129
+ if (stubFiles.length) {
10130
+ log(` - 구현 실체 (done 기본): ✗ FAIL — 주장된 구현 파일이 주석/빈껍데기뿐: ${stubFiles.slice(0, 5).join(', ')} (비주석 코드 0줄 또는 빈 export 껍데기)`);
10131
+ } else if (claimsChecked && _vcImpl.length) {
10132
+ log(` - 구현 실체 (done 기본): ✓ pass (주장 구현 파일에 실코드 존재)`);
10133
+ }
10134
+ if (testLinkOk === false) {
10135
+ log(` - 테스트-구현 연결: ⚠ 주장된 테스트(${_vcTests.slice(0, 3).join(', ')})가 구현 파일을 참조하지 않음 — 빈 테스트(assert(true)) 의심${has('--strict-claims') ? ' → FAIL' : ' (advisory — --strict-claims 시 FAIL)'}`);
10136
+ } else if (testLinkOk === true && claimsChecked) {
10137
+ log(` - 테스트-구현 연결: ✓ pass (테스트가 구현을 참조)`);
10138
+ }
10139
+ const overallFail = !allFilesOk || !testOk || (runResult && !runResult.skipped && !runTestsOk) || (claimsChecked && !strictOk) || !evidenceQualityOk || !gitClaimOk || (claimsChecked && stubFiles.length > 0) || (has('--strict-claims') && testLinkOk === false);
9826
10140
  // 1.9.287: 정직한 한계 고지 — 테스트 통과 ≠ 의미적 구현 정확성
9827
10141
  if (claimsChecked || mustHaveEvidence) {
9828
10142
  log('');
@@ -11694,7 +12008,7 @@ function llmBenchRecordCmd(root) {
11694
12008
 
11695
12009
  const _sessionClose = require('../lib/session-close');
11696
12010
  // 1.9.425 (UR-0025/UR-0125 큰 핸들러 모듈화 10번째): sessionClose → lib/session-close.js (DI 위임)
11697
- function sessionClose(root, opts = {}) { return _sessionClose.sessionClose(root, opts, { VERSION, STATUSES, MARK, has, arg, harnessPath: __filename, readProgressRows, evidencePath, handoffPath, currentStatePath, taskLogPath, verifyRules, _autoRoadmap, _readUsageStats, readSessionCounter, writeSessionCounter, _retroAggregate, _retroOneLine, retroCmd, _loadDecisions, readRules, planPath, _loadLessons, _readFeatureGraph, _auditUserRequests, _detectDeliveredRequests, _computeRoundHistory, _computeMilestones, _computeRecentChanges, _collectPyFiles, _analyzePyFile, _collectRuntimeEnv, _scanShellScriptsEncoding, _listAPISkills, _matchAPISkills, _loadShellFailures, _shellEnvDrift, _runPreWakeAudit, _saveAndAppendPreWakeReport, _runIdempotencyAudit, _detectAbnormalShutdown, _updateUserRequest }); }
12011
+ function sessionClose(root, opts = {}) { return _sessionClose.sessionClose(root, opts, { VERSION, STATUSES, MARK, has, arg, harnessPath: __filename, readProgressRows, evidencePath, handoffPath, currentStatePath, taskLogPath, verifyRules, _autoRoadmap, _readUsageStats, readSessionCounter, writeSessionCounter, _retroAggregate, _retroOneLine, retroCmd, _loadDecisions, readRules, planPath, _loadLessons, _readFeatureGraph, _auditUserRequests, _detectDeliveredRequests, _computeRoundHistory, _computeMilestones, _computeRecentChanges, _collectPyFiles, _analyzePyFile, _collectRuntimeEnv, _scanShellScriptsEncoding, _listAPISkills, _matchAPISkills, _loadShellFailures, _shellEnvDrift, _runPreWakeAudit, _saveAndAppendPreWakeReport, _runIdempotencyAudit, _detectAbnormalShutdown, _updateUserRequest, _detectOptimism, _scanCodeForPatterns, _collectSecretFindings }); } // 1.17.6 (UR-0049): 마감 정합 — done 낙관 재확인 + 시크릿 재확인
11698
12012
 
11699
12013
  function readmeCmd(root) { syncReadme(absRoot(root)); }
11700
12014
  function consistencyCheck(root) {
@@ -12180,7 +12494,7 @@ function _brainstormFor(root, topic) {
12180
12494
  const planText = read(planFile_brainstorm);
12181
12495
  const milestoneBlocks = planText.split(/\n(?=### M-\d{4}\.)/);
12182
12496
  for (const b of milestoneBlocks) {
12183
- const m = b.match(/^### (M-\d{4})\.\s*(.+?)$/m);
12497
+ const m = b.match(/^### (M-\d{4})\.[ \t]*(.+?)$/m);
12184
12498
  if (m && matches(b)) {
12185
12499
  const idx = planText.indexOf(b);
12186
12500
  const lineNo = idx >= 0 ? planText.slice(0, idx).split('\n').length : 0;
@@ -12475,7 +12789,7 @@ function brainstormCmd(root, topic) {
12475
12789
  const planText = read(planFile_b2);
12476
12790
  const milestoneBlocks = planText.split(/\n(?=### M-\d{4}\.)/);
12477
12791
  for (const b of milestoneBlocks) {
12478
- const m = b.match(/^### (M-\d{4})\.\s*(.+?)$/m);
12792
+ const m = b.match(/^### (M-\d{4})\.[ \t]*(.+?)$/m);
12479
12793
  if (m && matches(b)) {
12480
12794
  const idx = planText.indexOf(b);
12481
12795
  const lineNo = idx >= 0 ? planText.slice(0, idx).split('\n').length : 0;
@@ -16195,8 +16509,25 @@ function _isCwdSafe(root, cwd) {
16195
16509
  return !rel.startsWith('..') && !path.isAbsolute(rel);
16196
16510
  } catch { return false; }
16197
16511
  }
16512
+ // basic 모드에서도 항상 허용하는 핵심 도구 (release/install 흐름 유지)
16513
+ const RUN_CORE_ALLOW = ['git', 'npm', 'npx', 'node', 'pnpm', 'yarn'];
16514
+ // 1.18.1 (재실증 신규 P1): 명령 실행 권한 결정 — 순수 함수(테스트 가능). cwd jail 은 별도(여기서 판단 안 함).
16515
+ // userAuthorized: 사용자가 명시적으로 입력한 명령(예: verify-claim --test-cmd "<명령>" 또는 config testCommand)
16516
+ // → basic 모드 allowList 우회(명시 권한). 이전엔 coreAllow(JS 도구)만 허용해 --test-cmd "python ..." 가
16517
+ // status 126 으로 차단되고, 그게 verify-claim 에서 "테스트 실패 → 주장 불일치" 거짓 FAIL 로 둔갑했음(초록 파이썬 프로젝트 오판).
16518
+ function _isCommandPermitted(perms, cmdStr, opts) {
16519
+ opts = opts || {};
16520
+ perms = perms || {};
16521
+ if (opts.allowOutsideCwd || opts.userAuthorized) return true;
16522
+ const exec = perms.shell && perms.shell.exec !== false;
16523
+ if (exec) return true; // extended/full 모드 (또는 exec:true)
16524
+ const allow = (perms.shell && perms.shell.allowList) || [];
16525
+ const first = String(cmdStr || '').trim().split(/\s+/)[0];
16526
+ return RUN_CORE_ALLOW.includes(first) || allow.includes('*') || allow.includes(first);
16527
+ }
16528
+
16198
16529
  function runCommandSafe(cmd, args, opts) {
16199
- // opts: { cwd, root, timeout, env, stdio, kind, label, allowShell, encoding, input, allowOutsideCwd }
16530
+ // opts: { cwd, root, timeout, env, stdio, kind, label, allowShell, encoding, input, allowOutsideCwd, userAuthorized }
16200
16531
  opts = opts || {};
16201
16532
  const root = opts.root || opts.cwd || process.cwd();
16202
16533
  const cwd = opts.cwd || root;
@@ -16213,17 +16544,11 @@ function runCommandSafe(cmd, args, opts) {
16213
16544
  // 2) permissions allowList (1.9.146)
16214
16545
  try {
16215
16546
  const perms = _readPermissions(root);
16216
- const exec = perms.shell?.exec !== false; // basic 에선 false
16217
- const allow = perms.shell?.allowList || [];
16218
- if (!exec && !opts.allowOutsideCwd) {
16219
- // basic 모드 git/npm/node 같은 핵심 도구는 허용 (release/install 흐름 유지)
16220
- const coreAllow = ['git', 'npm', 'npx', 'node', 'pnpm', 'yarn'];
16221
- const first = cmdStr.split(/\s+/)[0];
16222
- if (!coreAllow.includes(first) && !allow.includes('*') && !allow.includes(first)) {
16223
- const r = { status: 126, stdout: '', stderr: `runCommandSafe: shell.exec=false (mode=${perms.mode}). allowList: ${allow.join(',') || '(없음)'} / core: ${coreAllow.join(',')}`, error: 'permissions', blocked: true };
16224
- try { _recordRun(root, { kind: label, cmd: cmdStr, args: argList, durationMs: Date.now() - t0, ok: false, blocked: 'permissions', mode: perms.mode }); } catch {}
16225
- return r;
16226
- }
16547
+ if (!_isCommandPermitted(perms, cmdStr, opts)) {
16548
+ const allow = perms.shell?.allowList || [];
16549
+ const r = { status: 126, stdout: '', stderr: `runCommandSafe: shell.exec=false (mode=${perms.mode}). allowList: ${allow.join(',') || '(없음)'} / core: ${RUN_CORE_ALLOW.join(',')}`, error: 'permissions', blocked: true };
16550
+ try { _recordRun(root, { kind: label, cmd: cmdStr, args: argList, durationMs: Date.now() - t0, ok: false, blocked: 'permissions', mode: perms.mode }); } catch {}
16551
+ return r;
16227
16552
  }
16228
16553
  } catch {}
16229
16554
  // 3) spawn — shell:false 기본 (shell injection 차단). allowShell=true 시만 shell:true (deploy/build 호환)
@@ -19088,7 +19413,8 @@ function doctorCmd(opts = {}) { return _diag.doctorCmd(opts, { VERSION, _selfTes
19088
19413
  function whichCmd() { return _diag.whichCmd({ VERSION, has, harnessPath: __filename }); }
19089
19414
 
19090
19415
  function help() {
19091
- 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/agy/copilot)\n leerness agents dispatch "<task>" --to <id> # 1.9.30 활성 CLI 대상 실행 명령 생성 (실 호출 X, 사용자 실행)\n leerness agents multi "<task>" [--only c1,c2] [--write] [--execute] [--timeout 60] # 1.9.152/156 활성 N개 일괄 dispatch (--execute: 실 spawn + consensus)\n leerness provider list|add|remove [args] # 1.9.157 Provider Registry — 사용자 정의 CLI provider 동적 추가 (OpenRouter/Bedrock 흡수)\n leerness agents dispatch "<task>" --multi # 1.9.152 multi 모드 alias (또는 --to all)\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 which [--json] # 1.9.164 진단: 현재 실행 경로/버전 + npm 캐시 + PATH 후보 (구버전 충돌 해결)\n leerness selftest [--json] # 1.9.258 코어 함수 무결성 자가 검증 (설치 손상/부분설치 감지, CI 친화 exit 1)\n leerness shell-guard "<command>" [--json] # 1.9.260 터미널 명령 셸 호환성 린터 (PowerShell 5.1 && 미지원 등 실행 전 감지, UR-0020)\n leerness shell-guard --record --cmd "..." --exit N # 1.9.260 실패한 터미널 명령 기록 → 다음 분석 시 회수\n leerness path-setup [--apply] [--json] # 1.9.254 leerness CLI PATH 자동 등록 (npm global bin 미등록 시)\n leerness web check|screenshot|extract <url> [--out file.png] [--selector "css"] # 1.9.165 playwright bridge (opt-in: npm i -g playwright + permissions.browser)\n leerness pc check|click|type|screenshot [--x N --y N] [--text "s"] [--out f.png] # 1.9.166 robotjs/nut-tree bridge (opt-in: npm i -g robotjs + permissions.mouse/keyboard, ⚠ full 모드 권장)\n leerness lsp check|symbols|references <file/name> [--in dir] [--json] # 1.9.167 LSP 어댑터 MVP (typescript opt-in + regex fallback, 코드 인텔리전스)\n leerness review-request "<request>" [--json] # 1.9.176 사용자 요청 사전 검토 (충돌/재사용/효율/권장 단계 — 사용자 명시)\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 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
19416
+ 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/agy/copilot)\n leerness agents dispatch "<task>" --to <id> # 1.9.30 활성 CLI 대상 실행 명령 생성 (실 호출 X, 사용자 실행)\n leerness agents multi "<task>" [--only c1,c2] [--write] [--execute] [--timeout 60] # 1.9.152/156 활성 N개 일괄 dispatch (--execute: 실 spawn + consensus)\n leerness provider list|add|remove [args] # 1.9.157 Provider Registry — 사용자 정의 CLI provider 동적 추가 (OpenRouter/Bedrock 흡수)\n leerness agents dispatch "<task>" --multi # 1.9.152 multi 모드 alias (또는 --to all)\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 which [--json] # 1.9.164 진단: 현재 실행 경로/버전 + npm 캐시 + PATH 후보 (구버전 충돌 해결)\n leerness selftest [--json] # 1.9.258 코어 함수 무결성 자가 검증 (설치 손상/부분설치 감지, CI 친화 exit 1)\n leerness shell-guard "<command>" [--json] # 1.9.260 터미널 명령 셸 호환성 린터 (PowerShell 5.1 && 미지원 등 실행 전 감지, UR-0020)\n leerness shell-guard --record --cmd "..." --exit N # 1.9.260 실패한 터미널 명령 기록 → 다음 분석 시 회수\n leerness path-setup [--apply] [--json] # 1.9.254 leerness CLI PATH 자동 등록 (npm global bin 미등록 시)\n leerness web check|screenshot|extract <url> [--out file.png] [--selector "css"] # 1.9.165 playwright bridge (opt-in: npm i -g playwright + permissions.browser)\n leerness pc check|click|type|screenshot [--x N --y N] [--text "s"] [--out f.png] # 1.9.166 robotjs/nut-tree bridge (opt-in: npm i -g robotjs + permissions.mouse/keyboard, ⚠ full 모드 권장)\n leerness lsp check|symbols|references <file/name> [--in dir] [--json] # 1.9.167 LSP 어댑터 MVP (typescript opt-in + regex fallback, 코드 인텔리전스)\n leerness review-request "<request>" [--json] # 1.9.176 사용자 요청 사전 검토 (충돌/재사용/효율/권장 단계 — 사용자 명시)\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에 낙관적 표시 자동 검사 통합
19417
+ leerness lens [code|design|docs|test|security] [--json] # 1.18.3 분야별 자기질문 품질 렌즈 + 분야간 인과관계 (완료 선언 전 자가 점검)\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 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
19092
19418
  leerness retro [path] [--days 7] [--all-apps] [--include p1,p2] [--json] # 회고 (1.9.13~1.9.16)
19093
19419
  leerness insights [path] [--all-apps] [--include p1,p2] [--json] # 누적 통계 (1.9.13~1.9.16)
19094
19420
  leerness brainstorm "<주제>" [--all-apps] [--include p1,p2] [--json] # 브레인스토밍 (1.9.13~1.9.16)
@@ -19320,6 +19646,7 @@ async function main() {
19320
19646
  if (cmd === 'milestones') return milestonesCmd(_resolveRoot(args[1]));
19321
19647
  // 1.9.231: leerness pulse — 한 줄 종합 요약 (10 핵심 지표)
19322
19648
  if (cmd === 'pulse') return pulseCmd(_resolveRoot(args[1]));
19649
+ if (cmd === 'lens') return lensCmd(args[1]); // 1.18.3 (UR-0003): 분야별 자기질문 품질 렌즈
19323
19650
  // 1.9.233: leerness commands — 카테고리화된 전체 CLI 명령 목록
19324
19651
  if (cmd === 'commands') return commandsCmd(arg('--path', process.cwd()));
19325
19652
  // 1.9.239: leerness py-check — Python 파일 분석 (사용자 명시 UR-0013)
@@ -19433,7 +19760,7 @@ async function main() {
19433
19760
  const root = absRoot(arg('--path', process.cwd())); const sub = args[1] || 'show';
19434
19761
  if (sub==='show') return planShow(root);
19435
19762
  if (sub==='init') return planInit(root);
19436
- if (sub==='add') return planAdd(root, args.slice(2).join(' ') || '새 계획');
19763
+ if (sub==='add') return planAdd(root, args.slice(2).join(' ').trim() || '새 계획'); // 17th 버그헌트 P2: 공백-only 제목이 || 기본값을 우회(truthy)해 plan.md 손상(파서가 다음 줄 'Status:' 흡수) → trim 후 판정
19437
19764
  if (sub==='drop') return planDrop(root, args.slice(2).join(' ') || '드랍 항목');
19438
19765
  if (sub==='remove') return planRemoveCmd(root, args[2]);
19439
19766
  if (sub==='progress') return planProgress(root, { json: has('--json'), updateIntent: args.slice(2).some(a => /^M-\d/i.test(a)) || has('--status') || arg('--progress', null) != null }); // 1.9.447 (UR-0145): --json + 변경의도 인자 경고
@@ -19595,5 +19922,11 @@ module.exports = {
19595
19922
  // 1.9.288: MCP 도구 수 단일 출처 (Codex #5) — 단위 테스트
19596
19923
  _mcpToolCount,
19597
19924
  // 1.9.289: shell-safe 인용 (Codex #3) — 단위 테스트
19598
- _shellQuoteArg
19925
+ _shellQuoteArg,
19926
+ // 1.18.1: 명령 실행 권한 결정 (재실증 신규 P1: --test-cmd 비-JS 인터프리터 거짓차단) — 단위 테스트
19927
+ _isCommandPermitted, RUN_CORE_ALLOW,
19928
+ // 1.18.2: verify-claim 위장 스텁(빈 export 껍데기) 판정 — 단위 테스트
19929
+ _vcImplIsEmpty, _VC_EMPTY_SHELL_RE,
19930
+ // 1.18.3 (UR-0003): 분야별 자기질문 품질 렌즈 — 단위 테스트
19931
+ LENS_CATALOG, lensCmd
19599
19932
  };