leerness 1.18.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.18.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') 시 호스트 프로세스 오염.
@@ -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
  }
@@ -3606,6 +3581,82 @@ function _selfTestCases() {
3606
3581
  const injected = read(__filename).includes('_updateUserRequest, _detectOptimism, _scanCodeForPatterns, _collectSecretFindings });');
3607
3582
  return wired && injected;
3608
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
+ } },
3609
3660
  { name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
3610
3661
  ];
3611
3662
  }
@@ -4134,6 +4185,75 @@ function pulseCmd(root) {
4134
4185
  // bridge (web/pc/lsp — opt-in)
4135
4186
  // config (init/migrate/update/auto-update/setup-agents/install/workspace-dir/wakeup-interval)
4136
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
+
4137
4257
  function commandsCmd(root) {
4138
4258
  const isTty = process.stdout && process.stdout.isTTY;
4139
4259
  const cy = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
@@ -4171,6 +4291,7 @@ function commandsCmd(root) {
4171
4291
  { cmd: 'encoding check [path]', desc: '인코딩 검증' },
4172
4292
  { cmd: 'lazy detect [path] [--json]', desc: '게으른 작업 감지 (1.9.101)' },
4173
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)' },
4174
4295
  { cmd: 'optimism-check <T-ID>', desc: '낙관적 API 감지 (1.9.26)' },
4175
4296
  { cmd: 'requests audit|list|complete|drop|auto-complete', desc: '사용자 요청 추적 (1.9.207/223)' },
4176
4297
  { cmd: 'pre-wake-audit [path] [--last]', desc: 'sleep 전 점검 (1.9.209)' },
@@ -6781,6 +6902,19 @@ function taskUpdate(root, id) {
6781
6902
  if (!_requireInit(root, 'task update')) return; // 1.9.311 (UR-0047): init 가드
6782
6903
  if (!_rejectUnknownFlags(['--status', '--evidence', '--next', '--note'], 'task update T-0001 --status done --evidence "..." --next "..."')) { process.exitCode = 1; return; } // 1.17.5 (UR-0048)
6783
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
+ }
6784
6918
  if (!_validateChoice(arg('--status', null), TASK_STATUSES, 'task status')) { process.exitCode = 1; return; } // 1.9.310 (UR-0046)
6785
6919
  const rows = readProgressRows(root);
6786
6920
  if (!rows.find(r => r.id === id)) { fail(`task ${id} not found in progress-tracker.md`); return; }
@@ -9636,6 +9770,46 @@ function _gitChangedFiles(root) {
9636
9770
  }
9637
9771
  // 주장 파일이 git 변경 집합에 있는지(상대경로 prefix 차이 허용).
9638
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
+
9639
9813
  function verifyClaimCmd(root, taskId) {
9640
9814
  root = absRoot(root);
9641
9815
  const _j = has('--json'); // 1.9.400 (7번째 버그헌트 P1-B, UR-0105): --json 에러도 구조화
@@ -9650,7 +9824,8 @@ function verifyClaimCmd(root, taskId) {
9650
9824
  // 변경: 확장자 화이트리스트 기반. 디렉토리는 선택적 (project.godot 같은 루트 파일도 잡음).
9651
9825
  // 확장자는 길이 내림차순(긴 것 먼저 매치) + \b 종결로 .ts vs .tscn 구분.
9652
9826
  // 1.9.21: 설정/메타 파일 확장자 추가 — Godot export_presets.cfg 등 false negative 보완
9653
- 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';
9654
9829
  const FILE_RE = new RegExp(`(?:[A-Za-z][A-Za-z0-9_-]*\\/)?[A-Za-z][\\w./-]*\\.(?:${FILE_EXTS})\\b`, 'g');
9655
9830
  const filePatterns = evidence.match(FILE_RE) || [];
9656
9831
  // 중복 제거 + "tests/test.js" 같은 결과를 유지 (이미 `..` 없으니 그대로)
@@ -9699,8 +9874,8 @@ function verifyClaimCmd(root, taskId) {
9699
9874
  if (!c.exists || !_VC_CODE_EXT.test(c.file) || _VC_TEST_PAT.test(c.file)) continue;
9700
9875
  let body = ''; try { body = read(path.join(root, c.file)); } catch { continue; }
9701
9876
  if (!body || body.length > 512 * 1024) continue;
9702
- const codeLines = body.replace(/\/\*[\s\S]*?\*\//g, '').split('\n').map(l => l.trim()).filter(t => t && !t.startsWith('//') && !t.startsWith('#'));
9703
- if (codeLines.length === 0) stubFiles.push(c.file);
9877
+ // 1.18.2: 코드 0줄(기존) + export 껍데기(위장 스텁) 통합 판정.
9878
+ if (_vcImplIsEmpty(body)) stubFiles.push(c.file);
9704
9879
  }
9705
9880
  const _vcImpl = fileChecks.filter(c => c.exists && _VC_CODE_EXT.test(c.file) && !_VC_TEST_PAT.test(c.file)).map(c => c.file);
9706
9881
  const _vcTests = fileChecks.filter(c => c.exists && _VC_CODE_EXT.test(c.file) && _VC_TEST_PAT.test(c.file)).map(c => c.file);
@@ -9766,7 +9941,13 @@ function verifyClaimCmd(root, taskId) {
9766
9941
  } else {
9767
9942
  {
9768
9943
  // 1.9.299 (UR-0039): 신뢰 못 할 워크스페이스 테스트 실행 → runCommandSafe + scrubSecrets (시크릿 노출 차단 + cwd jail).
9769
- const r = runCommandSafe(testCmd, [], { cwd: root, root, encoding: 'utf8', allowShell: true, scrubSecrets: true, timeout: 5 * 60 * 1000, kind: 'verify_claim_test' });
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 {
9770
9951
  const out = (r.stdout || '') + (r.stderr || '');
9771
9952
  // 1.9.20: 파싱 패턴 확장 — 한국어 + jest/mocha/tap/vitest
9772
9953
  let parsed = null;
@@ -9800,6 +9981,7 @@ function verifyClaimCmd(root, taskId) {
9800
9981
  parsed,
9801
9982
  allPassed: r.status === 0 && (!parsed || (parsed && parsed.num === parsed.denom))
9802
9983
  };
9984
+ }
9803
9985
  }
9804
9986
  }
9805
9987
  }
@@ -9915,7 +10097,7 @@ function verifyClaimCmd(root, taskId) {
9915
10097
  // 1.17.4 (UR-0047): 측정 불가는 '통과' 가 아니라 '검증 미수행' — 이전엔 실측 0 인데 ✓ pass(실측≥주장) 모순 표기.
9916
10098
  log(` - 테스트 카운트: ${declaredTestCount == null ? '⊘ (주장 없음)' : !testMeasured ? `⊘ 측정 불가 — 주장 ${declaredTestCount}개 검증 미수행 (pass 아님)` : testOk ? '✓ pass (실측 ≥ 주장)' : '⚠ 주장보다 적음'}`);
9917
10099
  if (runResult && !runResult.skipped) {
9918
- log(` - npm test 실행: ${runTestsOk ? '✓ all passed' : '✗ FAIL'}`);
10100
+ log(` - ${runResult.cmd || 'npm test'} 실행: ${runTestsOk ? '✓ all passed' : '✗ FAIL'}`);
9919
10101
  if (declaredPass) log(` - 주장과 실행 결과 일치: ${declaredPassMatchesActual ? '✓ pass' : '⚠ 다름'}`);
9920
10102
  }
9921
10103
  // 1.11.2 (UR-0175): optimism+정직성 — done 주장은 기본 게이팅(claimsChecked). 완화: --lenient.
@@ -9945,7 +10127,7 @@ function verifyClaimCmd(root, taskId) {
9945
10127
  }
9946
10128
  // 1.17.3 (UR-0046): 구현 실체(스텁) + 테스트-구현 연결 — Attack C(주석뿐 구현+assert(true)) 차단.
9947
10129
  if (stubFiles.length) {
9948
- log(` - 구현 실체 (done 기본): ✗ FAIL — 주장된 구현 파일이 주석/빈껍데기뿐: ${stubFiles.slice(0, 5).join(', ')} (비주석 코드 0줄)`);
10130
+ log(` - 구현 실체 (done 기본): ✗ FAIL — 주장된 구현 파일이 주석/빈껍데기뿐: ${stubFiles.slice(0, 5).join(', ')} (비주석 코드 0줄 또는 빈 export 껍데기)`);
9949
10131
  } else if (claimsChecked && _vcImpl.length) {
9950
10132
  log(` - 구현 실체 (done 기본): ✓ pass (주장 구현 파일에 실코드 존재)`);
9951
10133
  }
@@ -16327,8 +16509,25 @@ function _isCwdSafe(root, cwd) {
16327
16509
  return !rel.startsWith('..') && !path.isAbsolute(rel);
16328
16510
  } catch { return false; }
16329
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
+
16330
16529
  function runCommandSafe(cmd, args, opts) {
16331
- // 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 }
16332
16531
  opts = opts || {};
16333
16532
  const root = opts.root || opts.cwd || process.cwd();
16334
16533
  const cwd = opts.cwd || root;
@@ -16345,17 +16544,11 @@ function runCommandSafe(cmd, args, opts) {
16345
16544
  // 2) permissions allowList (1.9.146)
16346
16545
  try {
16347
16546
  const perms = _readPermissions(root);
16348
- const exec = perms.shell?.exec !== false; // basic 에선 false
16349
- const allow = perms.shell?.allowList || [];
16350
- if (!exec && !opts.allowOutsideCwd) {
16351
- // basic 모드 git/npm/node 같은 핵심 도구는 허용 (release/install 흐름 유지)
16352
- const coreAllow = ['git', 'npm', 'npx', 'node', 'pnpm', 'yarn'];
16353
- const first = cmdStr.split(/\s+/)[0];
16354
- if (!coreAllow.includes(first) && !allow.includes('*') && !allow.includes(first)) {
16355
- const r = { status: 126, stdout: '', stderr: `runCommandSafe: shell.exec=false (mode=${perms.mode}). allowList: ${allow.join(',') || '(없음)'} / core: ${coreAllow.join(',')}`, error: 'permissions', blocked: true };
16356
- try { _recordRun(root, { kind: label, cmd: cmdStr, args: argList, durationMs: Date.now() - t0, ok: false, blocked: 'permissions', mode: perms.mode }); } catch {}
16357
- return r;
16358
- }
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;
16359
16552
  }
16360
16553
  } catch {}
16361
16554
  // 3) spawn — shell:false 기본 (shell injection 차단). allowShell=true 시만 shell:true (deploy/build 호환)
@@ -19220,7 +19413,8 @@ function doctorCmd(opts = {}) { return _diag.doctorCmd(opts, { VERSION, _selfTes
19220
19413
  function whichCmd() { return _diag.whichCmd({ VERSION, has, harnessPath: __filename }); }
19221
19414
 
19222
19415
  function help() {
19223
- 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
19224
19418
  leerness retro [path] [--days 7] [--all-apps] [--include p1,p2] [--json] # 회고 (1.9.13~1.9.16)
19225
19419
  leerness insights [path] [--all-apps] [--include p1,p2] [--json] # 누적 통계 (1.9.13~1.9.16)
19226
19420
  leerness brainstorm "<주제>" [--all-apps] [--include p1,p2] [--json] # 브레인스토밍 (1.9.13~1.9.16)
@@ -19452,6 +19646,7 @@ async function main() {
19452
19646
  if (cmd === 'milestones') return milestonesCmd(_resolveRoot(args[1]));
19453
19647
  // 1.9.231: leerness pulse — 한 줄 종합 요약 (10 핵심 지표)
19454
19648
  if (cmd === 'pulse') return pulseCmd(_resolveRoot(args[1]));
19649
+ if (cmd === 'lens') return lensCmd(args[1]); // 1.18.3 (UR-0003): 분야별 자기질문 품질 렌즈
19455
19650
  // 1.9.233: leerness commands — 카테고리화된 전체 CLI 명령 목록
19456
19651
  if (cmd === 'commands') return commandsCmd(arg('--path', process.cwd()));
19457
19652
  // 1.9.239: leerness py-check — Python 파일 분석 (사용자 명시 UR-0013)
@@ -19727,5 +19922,11 @@ module.exports = {
19727
19922
  // 1.9.288: MCP 도구 수 단일 출처 (Codex #5) — 단위 테스트
19728
19923
  _mcpToolCount,
19729
19924
  // 1.9.289: shell-safe 인용 (Codex #3) — 단위 테스트
19730
- _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
19731
19932
  };