leerness 1.18.0 → 1.20.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/CHANGELOG.md +136 -0
- package/README.ko.md +187 -0
- package/README.md +50 -134
- package/SECURITY.md +66 -56
- package/bin/leerness.js +351 -52
- package/docs/clean-room-evaluations.md +67 -0
- package/lib/audit.js +336 -322
- package/package.json +1 -1
- package/scripts/e2e.js +11 -11
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.
|
|
35
|
+
const VERSION = '1.20.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 .\`로
|
|
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.
|
|
890
|
-
//
|
|
891
|
-
|
|
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.
|
|
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,116 @@ 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
|
+
} },
|
|
3660
|
+
{ name: '19th 헌트 (1.19.1): lens 도메인 대소문자/공백 정규화 (소스 가드)', run: () => {
|
|
3661
|
+
const src = read(__filename);
|
|
3662
|
+
return src.includes("if (domain != null) domain = String(domain).trim().toLowerCase();");
|
|
3663
|
+
} },
|
|
3664
|
+
{ name: '렌즈 완전판 v3 (1.19.3, UR-0003): 프로젝트 커스텀 렌즈 병합 (행위)', run: () => {
|
|
3665
|
+
const M = _mergeLensCatalog;
|
|
3666
|
+
const a = M({ code: { title: '코드', persona: 'p', questions: ['Q1'], affects: [] } }, { code: { questions: ['Q1', 'Q2'] } });
|
|
3667
|
+
const appendOk = a.code.questions.length === 2 && a.code.questions[1] === 'Q2' && a.code._customAdded === true;
|
|
3668
|
+
const b = M({ code: { title: '코드', persona: 'p', questions: ['Q'], affects: [] } }, { a11y: { title: '접근성', persona: '스크린리더', questions: ['키보드로 가능?'] } });
|
|
3669
|
+
const newOk = b.a11y && b.a11y.title === '접근성' && b.a11y._custom === true && b.code.questions.length === 1;
|
|
3670
|
+
const c = M({ code: { questions: ['Q'], affects: [] } }, { bad: { title: 'x' }, nope: null });
|
|
3671
|
+
const ignoreOk = !c.bad && !c.nope && c.code.questions.length === 1;
|
|
3672
|
+
const before = LENS_CATALOG.code.questions.length;
|
|
3673
|
+
M(LENS_CATALOG, { code: { questions: ['임시'] } });
|
|
3674
|
+
const immutableOk = LENS_CATALOG.code.questions.length === before;
|
|
3675
|
+
return appendOk && newOk && ignoreOk && immutableOk;
|
|
3676
|
+
} },
|
|
3677
|
+
{ name: '렌즈 완전판 v2 (1.19.2, UR-0003): 파일 확장자 → 렌즈 도메인 매핑 (행위)', run: () => {
|
|
3678
|
+
const F = _lensDomainsForFiles;
|
|
3679
|
+
const eq = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
3680
|
+
return eq(F(['src/api.js']), ['code'])
|
|
3681
|
+
&& eq(F(['ui/Button.tsx', 'styles/app.css']), ['design']) // UI/스타일 → design (코드 ext 아닌 tsx 는 design 우선)
|
|
3682
|
+
&& eq(F(['src/pay.js', 'src/Button.tsx']), ['code', 'design']) // 혼합 → code 먼저, design (최대 2)
|
|
3683
|
+
&& eq(F(['README.md']), ['docs'])
|
|
3684
|
+
&& eq(F(['tests/foo.test.js']), ['code', 'test']) // 테스트파일(.js) → code+test (고정 순서: code 먼저)
|
|
3685
|
+
&& eq(F([]), [])
|
|
3686
|
+
&& eq(F(['a.css', 'b.md', 'c.js', 'd.test.js']).length, 2); // 최대 2개 클러터 방지
|
|
3687
|
+
} },
|
|
3688
|
+
{ name: 'GPT-5.5 평가 #5 (1.19.1, UR-0009): 클린룸 평가 문서 공개 + 한계 명시 (행위)', run: () => {
|
|
3689
|
+
const dp = path.join(path.dirname(__filename), '..', 'docs', 'clean-room-evaluations.md');
|
|
3690
|
+
if (!exists(dp)) return true; // 패키지에 없으면 스킵(설치본 안전)
|
|
3691
|
+
const d = read(dp);
|
|
3692
|
+
return /AI clean-room evaluations/i.test(d) && /heuristic, not semantic/i.test(d) && /npm i leerness@/.test(d);
|
|
3693
|
+
} },
|
|
3609
3694
|
{ name: 'VERSION 형식 (x.y.z)', run: () => /^\d+\.\d+\.\d+$/.test(VERSION) }
|
|
3610
3695
|
];
|
|
3611
3696
|
}
|
|
@@ -4134,6 +4219,126 @@ function pulseCmd(root) {
|
|
|
4134
4219
|
// bridge (web/pc/lsp — opt-in)
|
|
4135
4220
|
// config (init/migrate/update/auto-update/setup-agents/install/workspace-dir/wakeup-interval)
|
|
4136
4221
|
// advanced (intent/requests/constraints/pre-wake-audit/idempotency/round-history/milestones/pulse)
|
|
4222
|
+
// 1.18.3 (UR-0003 사용자 명시): 분야별 자기질문 품질 렌즈 — AI 가 완료 선언 전 스스로 답해보는 질문 + 분야간 인과관계.
|
|
4223
|
+
// "선임 개발자가 내 코드를 보고 복잡하다고 느끼지 않을까?" / "선임 디자이너와 일반 사용자가 봤을 때 이쁘고 직관적인가?" (사용자 원문).
|
|
4224
|
+
// 질문에 "그렇다(통과)"라고 답할 수 없으면 아직 완료가 아님. affects = 이 분야를 바꿨을 때 다시 물어야 할 분야(인과관계).
|
|
4225
|
+
const LENS_CATALOG = {
|
|
4226
|
+
code: {
|
|
4227
|
+
title: '코드', persona: '선임 개발자',
|
|
4228
|
+
questions: [
|
|
4229
|
+
'선임 개발자가 이 코드를 보고 "복잡하다"고 느끼지 않을까? — 가볍고 단순해야 함',
|
|
4230
|
+
'더 단순한 방법이 있는데 추상화/패턴/옵션을 추가하고 있지 않은가?',
|
|
4231
|
+
'처음 보는 사람이 5분 안에 이 변경을 이해할 수 있는가?'
|
|
4232
|
+
],
|
|
4233
|
+
affects: ['test', 'docs', 'design'], affectsNote: 'UI 를 만지는 코드 변경이면 design 질문 재확인 필수'
|
|
4234
|
+
},
|
|
4235
|
+
design: {
|
|
4236
|
+
title: '디자인/UX', persona: '선임 디자이너 + 일반 사용자',
|
|
4237
|
+
questions: [
|
|
4238
|
+
'선임 디자이너가 봤을 때 이쁘고 일관적인가?',
|
|
4239
|
+
'일반 사용자가 처음 봤을 때 편하고 직관적이며 헷갈리지 않는가?',
|
|
4240
|
+
'꾸미기 위해 복잡해지고 있지 않은가? — 단순함이 곧 직관'
|
|
4241
|
+
],
|
|
4242
|
+
affects: ['code', 'docs'], affectsNote: '디자인 단순화는 보통 코드도 단순하게 만든다 (역도 성립)'
|
|
4243
|
+
},
|
|
4244
|
+
docs: {
|
|
4245
|
+
title: '문서/README', persona: '처음 온 사용자 (비개발자 포함)',
|
|
4246
|
+
questions: [
|
|
4247
|
+
'그래서 30초 안에 뭘 해보면 되지?',
|
|
4248
|
+
'비개발자가 터미널 명령 하나 없이 어떻게 사용하지?',
|
|
4249
|
+
'기존 도구가 이미 있는데 이걸 쓸 이유가 뭐지?'
|
|
4250
|
+
],
|
|
4251
|
+
affects: ['design'], affectsNote: '문서가 어렵다면 보통 제품 흐름(UX) 자체가 어렵다는 신호'
|
|
4252
|
+
},
|
|
4253
|
+
test: {
|
|
4254
|
+
title: '테스트', persona: '검증자',
|
|
4255
|
+
questions: [
|
|
4256
|
+
'이 테스트는 실패할 수 있는 테스트인가? (assert(true) 아님)',
|
|
4257
|
+
'주장한 테스트 개수/통과가 실측과 일치하는가?',
|
|
4258
|
+
'테스트가 구현을 실제로 import/호출하는가?'
|
|
4259
|
+
],
|
|
4260
|
+
affects: ['code'], affectsNote: '테스트하기 어렵다면 코드가 복잡하다는 신호 — code 질문으로 돌아갈 것'
|
|
4261
|
+
},
|
|
4262
|
+
security: {
|
|
4263
|
+
title: '보안', persona: '공격자',
|
|
4264
|
+
questions: [
|
|
4265
|
+
'시크릿이 코드/커밋에 들어가지 않았는가?',
|
|
4266
|
+
'이 입력을 악의적으로 주면 어떻게 되는가?',
|
|
4267
|
+
'권한/경계를 한 단어 비틀기로 우회할 수 있는가?'
|
|
4268
|
+
],
|
|
4269
|
+
affects: ['code', 'test'], affectsNote: '보안 가드를 넣었다면 우회/오탐 테스트가 따라와야 함'
|
|
4270
|
+
}
|
|
4271
|
+
};
|
|
4272
|
+
// 1.19.3 (UR-0003 렌즈 완전판 v3): 프로젝트별 커스텀 렌즈 — .harness/quality-lenses.json 읽기-병합(쓰기 명령 없음, AI/사용자가 편집).
|
|
4273
|
+
// 포맷: { "domains": { "code": { "questions": ["추가 질문"] }, "a11y": { "title":"접근성", "persona":"스크린리더 사용자", "questions":[...], "affects":["design"] } } }
|
|
4274
|
+
function _loadProjectLenses(root) {
|
|
4275
|
+
try {
|
|
4276
|
+
const p = path.join(absRoot(root || process.cwd()), '.harness', 'quality-lenses.json');
|
|
4277
|
+
if (!exists(p)) return {};
|
|
4278
|
+
const j = JSON.parse(read(p));
|
|
4279
|
+
return (j && typeof j === 'object' && j.domains && typeof j.domains === 'object') ? j.domains : {};
|
|
4280
|
+
} catch { return {}; }
|
|
4281
|
+
}
|
|
4282
|
+
// 내장 + 프로젝트 커스텀 병합. 기존 도메인 → 질문 추가(dedup, 최대 8); 신규 도메인 → 기본값으로 추가. _custom 플래그로 표시.
|
|
4283
|
+
function _mergeLensCatalog(builtin, custom) {
|
|
4284
|
+
const out = {};
|
|
4285
|
+
for (const [k, v] of Object.entries(builtin || {})) out[k] = Object.assign({}, v, { questions: (v.questions || []).slice(), affects: (v.affects || []).slice() });
|
|
4286
|
+
for (const [k, c] of Object.entries(custom || {})) {
|
|
4287
|
+
if (!c || typeof c !== 'object') continue;
|
|
4288
|
+
const cq = Array.isArray(c.questions) ? c.questions.filter(q => typeof q === 'string' && q.trim()) : [];
|
|
4289
|
+
if (out[k]) {
|
|
4290
|
+
const seen = new Set(out[k].questions);
|
|
4291
|
+
for (const q of cq) if (!seen.has(q)) { out[k].questions.push(q); seen.add(q); }
|
|
4292
|
+
out[k].questions = out[k].questions.slice(0, 8);
|
|
4293
|
+
out[k]._customAdded = cq.length > 0;
|
|
4294
|
+
} else if (cq.length) {
|
|
4295
|
+
out[k] = { title: (c.title || k), persona: (c.persona || '검토자'), questions: cq.slice(0, 8), affects: Array.isArray(c.affects) ? c.affects : [], affectsNote: c.affectsNote || '(프로젝트 정의)', _custom: true };
|
|
4296
|
+
}
|
|
4297
|
+
}
|
|
4298
|
+
return out;
|
|
4299
|
+
}
|
|
4300
|
+
function _effectiveLensCatalog(root) { return _mergeLensCatalog(LENS_CATALOG, _loadProjectLenses(root)); }
|
|
4301
|
+
|
|
4302
|
+
// 1.19.2 (UR-0003 렌즈 완전판 v2): 주장된 파일 확장자 → 관련 품질 렌즈 도메인 (결정적, 키워드 추론 아님).
|
|
4303
|
+
// verify-claim 이 완료-검증 순간(사용자: "완료 선언 전 자기질문")에 해당 분야 질문을 advisory 로 노출하는 데 사용.
|
|
4304
|
+
function _lensDomainsForFiles(files) {
|
|
4305
|
+
const out = [];
|
|
4306
|
+
const arr = Array.isArray(files) ? files : [];
|
|
4307
|
+
const has = (re) => arr.some(f => typeof f === 'string' && re.test(f));
|
|
4308
|
+
if (has(/\.(css|scss|sass|less|styl|tsx|jsx|vue|svelte|html?|astro)$/i)) out.push('design'); // UI/스타일
|
|
4309
|
+
if (has(/\.(md|mdx|rst|adoc|txt)$/i)) out.push('docs'); // 문서
|
|
4310
|
+
if (has(/(^|[\\/])(test_[^\\/]+\.[a-z]+|[^\\/]+[._-]test\.[a-z]+|[^\\/]+\.spec\.[a-z]+)$|(^|[\\/])tests?[\\/]/i)) out.push('test'); // 테스트
|
|
4311
|
+
if (has(/\.(js|mjs|cjs|ts|py|rb|go|rs|java|cs|php)$/i)) out.push('code'); // 실코드
|
|
4312
|
+
// 순서: code 먼저(가장 일반), 그다음 design/docs/test. 중복 제거 + 최대 2개(클러터 방지).
|
|
4313
|
+
const ordered = ['code', 'design', 'docs', 'test'].filter(d => out.includes(d));
|
|
4314
|
+
return ordered.slice(0, 2);
|
|
4315
|
+
}
|
|
4316
|
+
function lensCmd(domain, opts = {}) {
|
|
4317
|
+
const jsonMode = !!opts.json || has('--json');
|
|
4318
|
+
// 1.19.1 (19th 버그헌트): 도메인 인자 정규화 — `lens Code` / `lens CODE ` 도 인식(대소문자·공백 무관).
|
|
4319
|
+
if (domain != null) domain = String(domain).trim().toLowerCase();
|
|
4320
|
+
// 1.19.3: 내장 + 프로젝트 커스텀(.harness/quality-lenses.json) 병합 catalog.
|
|
4321
|
+
const root = opts.root || arg('--path', process.cwd());
|
|
4322
|
+
const catalog = _effectiveLensCatalog(root);
|
|
4323
|
+
if (domain && !catalog[domain]) {
|
|
4324
|
+
return fail(`알 수 없는 렌즈: ${domain} — 유효값: ${Object.keys(catalog).join(', ')}`);
|
|
4325
|
+
}
|
|
4326
|
+
const picked = domain ? { [domain]: catalog[domain] } : catalog;
|
|
4327
|
+
if (jsonMode) { log(JSON.stringify({ ok: true, lenses: picked }, null, 2)); return; }
|
|
4328
|
+
const hasCustom = Object.values(catalog).some(l => l && (l._custom || l._customAdded));
|
|
4329
|
+
log(`# leerness lens — 분야별 자기질문 품질 렌즈 (1.18.3)${hasCustom ? ' + 프로젝트 커스텀(.harness/quality-lenses.json)' : ''}`);
|
|
4330
|
+
log(`완료 선언 전 해당 분야 질문에 스스로 답해보세요. "그렇다(통과)"라고 답할 수 없으면 아직 완료가 아닙니다.`);
|
|
4331
|
+
for (const [key, l] of Object.entries(picked)) {
|
|
4332
|
+
log('');
|
|
4333
|
+
log(`## ${key} (${l.title}) — 페르소나: ${l.persona}${l._custom ? ' [프로젝트]' : (l._customAdded ? ' [+프로젝트 질문]' : '')}`);
|
|
4334
|
+
l.questions.forEach((q, i) => log(` ${i + 1}. ${q}`));
|
|
4335
|
+
log(` ↔ 인과: ${key} 를 바꾸면 → ${(l.affects || []).join(', ') || '(없음)'} 질문도 다시 — ${l.affectsNote}`);
|
|
4336
|
+
}
|
|
4337
|
+
log('');
|
|
4338
|
+
log(`사용: leerness lens <${Object.keys(catalog).join('|')}> · 완료 검증과 함께: leerness verify-claim T-XXXX`);
|
|
4339
|
+
if (!hasCustom) log(`프로젝트 커스텀 렌즈: .harness/quality-lenses.json 에 { "domains": { "code": { "questions": ["..."] } } } 추가`);
|
|
4340
|
+
}
|
|
4341
|
+
|
|
4137
4342
|
function commandsCmd(root) {
|
|
4138
4343
|
const isTty = process.stdout && process.stdout.isTTY;
|
|
4139
4344
|
const cy = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
|
|
@@ -4171,6 +4376,7 @@ function commandsCmd(root) {
|
|
|
4171
4376
|
{ cmd: 'encoding check [path]', desc: '인코딩 검증' },
|
|
4172
4377
|
{ cmd: 'lazy detect [path] [--json]', desc: '게으른 작업 감지 (1.9.101)' },
|
|
4173
4378
|
{ 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)' },
|
|
4379
|
+
{ cmd: 'lens [code|design|docs|test|security] [--json]', desc: '분야별 자기질문 품질 렌즈 + 분야간 인과관계 (1.18.3)' },
|
|
4174
4380
|
{ cmd: 'optimism-check <T-ID>', desc: '낙관적 API 감지 (1.9.26)' },
|
|
4175
4381
|
{ cmd: 'requests audit|list|complete|drop|auto-complete', desc: '사용자 요청 추적 (1.9.207/223)' },
|
|
4176
4382
|
{ cmd: 'pre-wake-audit [path] [--last]', desc: 'sleep 전 점검 (1.9.209)' },
|
|
@@ -6781,6 +6987,19 @@ function taskUpdate(root, id) {
|
|
|
6781
6987
|
if (!_requireInit(root, 'task update')) return; // 1.9.311 (UR-0047): init 가드
|
|
6782
6988
|
if (!_rejectUnknownFlags(['--status', '--evidence', '--next', '--note'], 'task update T-0001 --status done --evidence "..." --next "..."')) { process.exitCode = 1; return; } // 1.17.5 (UR-0048)
|
|
6783
6989
|
if (!id) return fail('id required (e.g., task update T-0001 --status in-progress)');
|
|
6990
|
+
// 1.18.1 (재실증 신규 P2): id 뒤 떠도는 non-path positional 거부 — `task update T-0003 done` 처럼 상태를 위치인자로 주면
|
|
6991
|
+
// 조용히 무시되고 "✓ updated" 가 출력돼 done 이 안 됨 → verify-claim/close 의 정직성 검사가 통째로 건너뛰던 데이터 정합 구멍.
|
|
6992
|
+
// 단, path-like positional(/abs, ./rel, C:\ — UR-0141 task 계열 positional path)은 워크스페이스 경로이므로 허용.
|
|
6993
|
+
{
|
|
6994
|
+
const _pos = nonFlagArgs(); // ['task','update','<id>', ...rest]
|
|
6995
|
+
const _pathLike = (t) => /^([A-Za-z]:[\\/]|\/|\.\.?[\\/])/.test(t);
|
|
6996
|
+
const stray = _pos.slice(3).find(t => t && !t.startsWith('-') && !_pathLike(t));
|
|
6997
|
+
if (stray) {
|
|
6998
|
+
const known = TASK_STATUSES.has(stray);
|
|
6999
|
+
const hint = known ? `--status ${stray}` : `--status <${[...TASK_STATUSES].join('|')}>`;
|
|
7000
|
+
return fail(`알 수 없는 인자 '${stray}' — 상태는 ${hint} 로 지정하세요 (예: task update ${id} ${known ? '--status ' + stray : '--status done'} --evidence "...")`);
|
|
7001
|
+
}
|
|
7002
|
+
}
|
|
6784
7003
|
if (!_validateChoice(arg('--status', null), TASK_STATUSES, 'task status')) { process.exitCode = 1; return; } // 1.9.310 (UR-0046)
|
|
6785
7004
|
const rows = readProgressRows(root);
|
|
6786
7005
|
if (!rows.find(r => r.id === id)) { fail(`task ${id} not found in progress-tracker.md`); return; }
|
|
@@ -9636,6 +9855,46 @@ function _gitChangedFiles(root) {
|
|
|
9636
9855
|
}
|
|
9637
9856
|
// 주장 파일이 git 변경 집합에 있는지(상대경로 prefix 차이 허용).
|
|
9638
9857
|
// _claimFileInGit → lib/analyzers.js (1.9.304 UR-0025)
|
|
9858
|
+
|
|
9859
|
+
// 1.18.2 (재실증 후속, 위장 스텁 차단): 주장된 구현 파일이 "실체 없는 껍데기"인지 판정 (verify-claim 구현 실체 검사).
|
|
9860
|
+
// ① 비주석 코드줄 0 (기존 1.17.3) — 주석/공백뿐.
|
|
9861
|
+
// ② 빈 export 껍데기 — 비주석 코드가 빈 객체/배열/빈 함수 export(또는 Python pass)뿐: module.exports={} / export default {} / module.exports=()=>{} / pass.
|
|
9862
|
+
// FP 가드(스텁 아님): 이름붙은 export(module.exports={ a, b }) · 재노출(module.exports=require('./x') / export * from) · 실제 선언/로직.
|
|
9863
|
+
// 1.18.1 재실증에서 `module.exports = {};` 한 줄짜리 위장 스텁이 "구현 실체 ✓"로 통과(+require 만 하는 가짜 테스트와 결합 시 --strict 도 exit 0)하던 우회 차단.
|
|
9864
|
+
// 빈 값(zero-logic) producer: 빈 객체/배열 · 빈 컨테이너 생성자/래퍼 · 빈 함수/화살표 · 빈 클래스.
|
|
9865
|
+
// 1.18.2 적대 워크플로(우회 헌터)가 찾은 P1 우회 전부 포함: Object.freeze({}) · new Object() · async function(){} · ()=>({}).
|
|
9866
|
+
// FP 0(오탐 헌터 ~45 합법패턴 통과): 내부가 비어야만 매치 → Object.freeze({a:1}) · class{ m(){} } · (a,b)=>a+b 등은 불매치.
|
|
9867
|
+
const _VC_EMPTY_VAL = '(?:' + [
|
|
9868
|
+
'\\{\\s*\\}', // {}
|
|
9869
|
+
'\\[\\s*\\]', // []
|
|
9870
|
+
'Object\\.freeze\\(\\s*(?:\\{\\s*\\}|\\[\\s*\\])\\s*\\)', // Object.freeze({}) / ([])
|
|
9871
|
+
'(?:new\\s+)?(?:Object|Array)\\s*\\(\\s*\\)', // new Object() / Object() / new Array()
|
|
9872
|
+
'(?:async\\s+)?function\\s*\\*?\\s*[A-Za-z0-9_$]*\\s*\\([^)]*\\)\\s*\\{\\s*\\}', // (async) function name?(){}
|
|
9873
|
+
'(?:async\\s+)?\\([^)]*\\)\\s*=>\\s*(?:\\{\\s*\\}|\\(\\s*\\{\\s*\\}\\s*\\))', // (async)(...)=>{} | =>({})
|
|
9874
|
+
'class\\s+[A-Za-z0-9_$]*\\s*\\{\\s*\\}', // class Name? {} (직접 export 형)
|
|
9875
|
+
].join('|') + ')';
|
|
9876
|
+
const _VC_EMPTY_SHELL_RE = new RegExp(
|
|
9877
|
+
'^(?:' + [
|
|
9878
|
+
'(?:module\\.)?exports(?:\\.[A-Za-z0-9_$]+)?\\s*=\\s*' + _VC_EMPTY_VAL, // module.exports[.x] = EMPTY (exports.default 포함)
|
|
9879
|
+
'export\\s+default\\s*' + _VC_EMPTY_VAL, // export default EMPTY
|
|
9880
|
+
'export\\s*\\{\\s*\\}', // export {}
|
|
9881
|
+
'pass', // python pass
|
|
9882
|
+
].join('|') + ')(?:\\s+as\\s+[A-Za-z0-9_$.<>\\[\\] ]+?)?\\s*;?$' // 선택적 TS 캐스트(as any) + ;
|
|
9883
|
+
);
|
|
9884
|
+
function _vcImplIsEmpty(body) {
|
|
9885
|
+
if (typeof body !== 'string' || !body) return false;
|
|
9886
|
+
// 블록주석 제거 → 줄별 trim/주석줄 제거 → 인라인 // 주석 제거(따옴표 없는 줄만, 문자열 보호) → join.
|
|
9887
|
+
const codeLines = body.replace(/\/\*[\s\S]*?\*\//g, '').split('\n').map(l => {
|
|
9888
|
+
let t = l.trim();
|
|
9889
|
+
if (!t || t.startsWith('//') || t.startsWith('#')) return '';
|
|
9890
|
+
if (!/['"`]/.test(t)) t = t.replace(/\s*\/\/.*$/, '').trim(); // 1.18.2: 같은-줄 인라인 주석 우회(`{}; // ...`) 차단
|
|
9891
|
+
return t;
|
|
9892
|
+
}).filter(Boolean);
|
|
9893
|
+
if (codeLines.length === 0) return true; // ① 코드 0줄
|
|
9894
|
+
const joined = codeLines.join(' ').replace(/\s+/g, ' ').trim();
|
|
9895
|
+
return _VC_EMPTY_SHELL_RE.test(joined); // ② 빈 export 껍데기뿐
|
|
9896
|
+
}
|
|
9897
|
+
|
|
9639
9898
|
function verifyClaimCmd(root, taskId) {
|
|
9640
9899
|
root = absRoot(root);
|
|
9641
9900
|
const _j = has('--json'); // 1.9.400 (7번째 버그헌트 P1-B, UR-0105): --json 에러도 구조화
|
|
@@ -9650,7 +9909,8 @@ function verifyClaimCmd(root, taskId) {
|
|
|
9650
9909
|
// 변경: 확장자 화이트리스트 기반. 디렉토리는 선택적 (project.godot 같은 루트 파일도 잡음).
|
|
9651
9910
|
// 확장자는 길이 내림차순(긴 것 먼저 매치) + \b 종결로 .ts vs .tscn 구분.
|
|
9652
9911
|
// 1.9.21: 설정/메타 파일 확장자 추가 — Godot export_presets.cfg 등 false negative 보완
|
|
9653
|
-
|
|
9912
|
+
// 1.18.2: java|php|mjs|cjs 추가 — _VC_CODE_EXT 와 정합(이전엔 .java/.php 임플 주장이 추출조차 안 돼 스텁/존재 검사를 무검사 통과).
|
|
9913
|
+
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
9914
|
const FILE_RE = new RegExp(`(?:[A-Za-z][A-Za-z0-9_-]*\\/)?[A-Za-z][\\w./-]*\\.(?:${FILE_EXTS})\\b`, 'g');
|
|
9655
9915
|
const filePatterns = evidence.match(FILE_RE) || [];
|
|
9656
9916
|
// 중복 제거 + "tests/test.js" 같은 결과를 유지 (이미 `..` 없으니 그대로)
|
|
@@ -9699,8 +9959,8 @@ function verifyClaimCmd(root, taskId) {
|
|
|
9699
9959
|
if (!c.exists || !_VC_CODE_EXT.test(c.file) || _VC_TEST_PAT.test(c.file)) continue;
|
|
9700
9960
|
let body = ''; try { body = read(path.join(root, c.file)); } catch { continue; }
|
|
9701
9961
|
if (!body || body.length > 512 * 1024) continue;
|
|
9702
|
-
|
|
9703
|
-
if (
|
|
9962
|
+
// 1.18.2: 코드 0줄(기존) + 빈 export 껍데기(위장 스텁) 통합 판정.
|
|
9963
|
+
if (_vcImplIsEmpty(body)) stubFiles.push(c.file);
|
|
9704
9964
|
}
|
|
9705
9965
|
const _vcImpl = fileChecks.filter(c => c.exists && _VC_CODE_EXT.test(c.file) && !_VC_TEST_PAT.test(c.file)).map(c => c.file);
|
|
9706
9966
|
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 +10026,13 @@ function verifyClaimCmd(root, taskId) {
|
|
|
9766
10026
|
} else {
|
|
9767
10027
|
{
|
|
9768
10028
|
// 1.9.299 (UR-0039): 신뢰 못 할 워크스페이스 테스트 실행 → runCommandSafe + scrubSecrets (시크릿 노출 차단 + cwd jail).
|
|
9769
|
-
|
|
10029
|
+
// 1.18.1 (재실증 P1): testCmd 는 사용자 명시(--test-cmd/config) → userAuthorized 로 basic 모드 allowList 우회(cwd jail 은 유지).
|
|
10030
|
+
// 이전엔 python/pytest 등 비-JS 인터프리터가 권한 차단(126)돼 verify-claim 이 "주장 불일치" 거짓 FAIL 을 냈음.
|
|
10031
|
+
const r = runCommandSafe(testCmd, [], { cwd: root, root, encoding: 'utf8', allowShell: true, scrubSecrets: true, userAuthorized: true, timeout: 5 * 60 * 1000, kind: 'verify_claim_test' });
|
|
10032
|
+
// 1.18.1: 권한/jail 로 차단된 실행은 "테스트 실패" 가 아니라 "측정 불가" — 절대 불일치 판정으로 둔갑시키지 않음(skip).
|
|
10033
|
+
if (r.blocked) {
|
|
10034
|
+
runResult = { skipped: true, reason: `테스트 명령 차단(${r.error}) — '${testCmd}' 실행 불가 (불일치 판정 아님). leerness permissions set extended 또는 allowList 에 추가 권장` };
|
|
10035
|
+
} else {
|
|
9770
10036
|
const out = (r.stdout || '') + (r.stderr || '');
|
|
9771
10037
|
// 1.9.20: 파싱 패턴 확장 — 한국어 + jest/mocha/tap/vitest
|
|
9772
10038
|
let parsed = null;
|
|
@@ -9800,6 +10066,7 @@ function verifyClaimCmd(root, taskId) {
|
|
|
9800
10066
|
parsed,
|
|
9801
10067
|
allPassed: r.status === 0 && (!parsed || (parsed && parsed.num === parsed.denom))
|
|
9802
10068
|
};
|
|
10069
|
+
}
|
|
9803
10070
|
}
|
|
9804
10071
|
}
|
|
9805
10072
|
}
|
|
@@ -9915,7 +10182,7 @@ function verifyClaimCmd(root, taskId) {
|
|
|
9915
10182
|
// 1.17.4 (UR-0047): 측정 불가는 '통과' 가 아니라 '검증 미수행' — 이전엔 실측 0 인데 ✓ pass(실측≥주장) 모순 표기.
|
|
9916
10183
|
log(` - 테스트 카운트: ${declaredTestCount == null ? '⊘ (주장 없음)' : !testMeasured ? `⊘ 측정 불가 — 주장 ${declaredTestCount}개 검증 미수행 (pass 아님)` : testOk ? '✓ pass (실측 ≥ 주장)' : '⚠ 주장보다 적음'}`);
|
|
9917
10184
|
if (runResult && !runResult.skipped) {
|
|
9918
|
-
log(` - npm test 실행: ${runTestsOk ? '✓ all passed' : '✗ FAIL'}`);
|
|
10185
|
+
log(` - ${runResult.cmd || 'npm test'} 실행: ${runTestsOk ? '✓ all passed' : '✗ FAIL'}`);
|
|
9919
10186
|
if (declaredPass) log(` - 주장과 실행 결과 일치: ${declaredPassMatchesActual ? '✓ pass' : '⚠ 다름'}`);
|
|
9920
10187
|
}
|
|
9921
10188
|
// 1.11.2 (UR-0175): optimism+정직성 — done 주장은 기본 게이팅(claimsChecked). 완화: --lenient.
|
|
@@ -9945,7 +10212,7 @@ function verifyClaimCmd(root, taskId) {
|
|
|
9945
10212
|
}
|
|
9946
10213
|
// 1.17.3 (UR-0046): 구현 실체(스텁) + 테스트-구현 연결 — Attack C(주석뿐 구현+assert(true)) 차단.
|
|
9947
10214
|
if (stubFiles.length) {
|
|
9948
|
-
log(` - 구현 실체 (done 기본): ✗ FAIL — 주장된 구현 파일이 주석/빈껍데기뿐: ${stubFiles.slice(0, 5).join(', ')} (비주석 코드 0줄)`);
|
|
10215
|
+
log(` - 구현 실체 (done 기본): ✗ FAIL — 주장된 구현 파일이 주석/빈껍데기뿐: ${stubFiles.slice(0, 5).join(', ')} (비주석 코드 0줄 또는 빈 export 껍데기)`);
|
|
9949
10216
|
} else if (claimsChecked && _vcImpl.length) {
|
|
9950
10217
|
log(` - 구현 실체 (done 기본): ✓ pass (주장 구현 파일에 실코드 존재)`);
|
|
9951
10218
|
}
|
|
@@ -9959,6 +10226,19 @@ function verifyClaimCmd(root, taskId) {
|
|
|
9959
10226
|
if (claimsChecked || mustHaveEvidence) {
|
|
9960
10227
|
log('');
|
|
9961
10228
|
log(` ℹ 한계: 테스트 통과는 "의미적 구현 정확성"을 보장하지 않음 — evidence 가 해당 주장(수정 파일/테스트)을 직접 링크해야 신뢰도↑.`);
|
|
10229
|
+
// 1.19.2 (UR-0003 렌즈 완전판 v2): 완료-검증 순간에 분야별 자기질문 advisory — 주장 파일 확장자 기반(결정적).
|
|
10230
|
+
// 기계검증(파일/테스트/스텁)을 통과해도 "사람이 보기에 좋은가"는 별개 → AI 가 스스로 답하도록 권장(advisory, 게이트 아님).
|
|
10231
|
+
const _lensDoms = _lensDomainsForFiles(files);
|
|
10232
|
+
if (_lensDoms.length) {
|
|
10233
|
+
const _lensCat = _effectiveLensCatalog(root); // 1.19.3: 프로젝트 커스텀 질문도 포함
|
|
10234
|
+
log('');
|
|
10235
|
+
log(` 🧭 품질 렌즈 (완료 선언 전 자문 — advisory, 게이트 아님):`);
|
|
10236
|
+
for (const d of _lensDoms) {
|
|
10237
|
+
const l = _lensCat[d];
|
|
10238
|
+
if (l) log(` · ${d}(${l.title}): ${l.questions[0]}`);
|
|
10239
|
+
}
|
|
10240
|
+
log(` → 전체 질문: leerness lens ${_lensDoms[0]}`);
|
|
10241
|
+
}
|
|
9962
10242
|
}
|
|
9963
10243
|
if (overallFail) {
|
|
9964
10244
|
log('');
|
|
@@ -16327,8 +16607,25 @@ function _isCwdSafe(root, cwd) {
|
|
|
16327
16607
|
return !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
16328
16608
|
} catch { return false; }
|
|
16329
16609
|
}
|
|
16610
|
+
// basic 모드에서도 항상 허용하는 핵심 도구 (release/install 흐름 유지)
|
|
16611
|
+
const RUN_CORE_ALLOW = ['git', 'npm', 'npx', 'node', 'pnpm', 'yarn'];
|
|
16612
|
+
// 1.18.1 (재실증 신규 P1): 명령 실행 권한 결정 — 순수 함수(테스트 가능). cwd jail 은 별도(여기서 판단 안 함).
|
|
16613
|
+
// userAuthorized: 사용자가 명시적으로 입력한 명령(예: verify-claim --test-cmd "<명령>" 또는 config testCommand)
|
|
16614
|
+
// → basic 모드 allowList 우회(명시 권한). 이전엔 coreAllow(JS 도구)만 허용해 --test-cmd "python ..." 가
|
|
16615
|
+
// status 126 으로 차단되고, 그게 verify-claim 에서 "테스트 실패 → 주장 불일치" 거짓 FAIL 로 둔갑했음(초록 파이썬 프로젝트 오판).
|
|
16616
|
+
function _isCommandPermitted(perms, cmdStr, opts) {
|
|
16617
|
+
opts = opts || {};
|
|
16618
|
+
perms = perms || {};
|
|
16619
|
+
if (opts.allowOutsideCwd || opts.userAuthorized) return true;
|
|
16620
|
+
const exec = perms.shell && perms.shell.exec !== false;
|
|
16621
|
+
if (exec) return true; // extended/full 모드 (또는 exec:true)
|
|
16622
|
+
const allow = (perms.shell && perms.shell.allowList) || [];
|
|
16623
|
+
const first = String(cmdStr || '').trim().split(/\s+/)[0];
|
|
16624
|
+
return RUN_CORE_ALLOW.includes(first) || allow.includes('*') || allow.includes(first);
|
|
16625
|
+
}
|
|
16626
|
+
|
|
16330
16627
|
function runCommandSafe(cmd, args, opts) {
|
|
16331
|
-
// opts: { cwd, root, timeout, env, stdio, kind, label, allowShell, encoding, input, allowOutsideCwd }
|
|
16628
|
+
// opts: { cwd, root, timeout, env, stdio, kind, label, allowShell, encoding, input, allowOutsideCwd, userAuthorized }
|
|
16332
16629
|
opts = opts || {};
|
|
16333
16630
|
const root = opts.root || opts.cwd || process.cwd();
|
|
16334
16631
|
const cwd = opts.cwd || root;
|
|
@@ -16345,17 +16642,11 @@ function runCommandSafe(cmd, args, opts) {
|
|
|
16345
16642
|
// 2) permissions allowList (1.9.146)
|
|
16346
16643
|
try {
|
|
16347
16644
|
const perms = _readPermissions(root);
|
|
16348
|
-
|
|
16349
|
-
|
|
16350
|
-
|
|
16351
|
-
|
|
16352
|
-
|
|
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
|
-
}
|
|
16645
|
+
if (!_isCommandPermitted(perms, cmdStr, opts)) {
|
|
16646
|
+
const allow = perms.shell?.allowList || [];
|
|
16647
|
+
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 };
|
|
16648
|
+
try { _recordRun(root, { kind: label, cmd: cmdStr, args: argList, durationMs: Date.now() - t0, ok: false, blocked: 'permissions', mode: perms.mode }); } catch {}
|
|
16649
|
+
return r;
|
|
16359
16650
|
}
|
|
16360
16651
|
} catch {}
|
|
16361
16652
|
// 3) spawn — shell:false 기본 (shell injection 차단). allowShell=true 시만 shell:true (deploy/build 호환)
|
|
@@ -19220,7 +19511,8 @@ function doctorCmd(opts = {}) { return _diag.doctorCmd(opts, { VERSION, _selfTes
|
|
|
19220
19511
|
function whichCmd() { return _diag.whichCmd({ VERSION, has, harnessPath: __filename }); }
|
|
19221
19512
|
|
|
19222
19513
|
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에 낙관적 표시 자동 검사
|
|
19514
|
+
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에 낙관적 표시 자동 검사 통합
|
|
19515
|
+
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
19516
|
leerness retro [path] [--days 7] [--all-apps] [--include p1,p2] [--json] # 회고 (1.9.13~1.9.16)
|
|
19225
19517
|
leerness insights [path] [--all-apps] [--include p1,p2] [--json] # 누적 통계 (1.9.13~1.9.16)
|
|
19226
19518
|
leerness brainstorm "<주제>" [--all-apps] [--include p1,p2] [--json] # 브레인스토밍 (1.9.13~1.9.16)
|
|
@@ -19452,6 +19744,7 @@ async function main() {
|
|
|
19452
19744
|
if (cmd === 'milestones') return milestonesCmd(_resolveRoot(args[1]));
|
|
19453
19745
|
// 1.9.231: leerness pulse — 한 줄 종합 요약 (10 핵심 지표)
|
|
19454
19746
|
if (cmd === 'pulse') return pulseCmd(_resolveRoot(args[1]));
|
|
19747
|
+
if (cmd === 'lens') return lensCmd(args[1]); // 1.18.3 (UR-0003): 분야별 자기질문 품질 렌즈
|
|
19455
19748
|
// 1.9.233: leerness commands — 카테고리화된 전체 CLI 명령 목록
|
|
19456
19749
|
if (cmd === 'commands') return commandsCmd(arg('--path', process.cwd()));
|
|
19457
19750
|
// 1.9.239: leerness py-check — Python 파일 분석 (사용자 명시 UR-0013)
|
|
@@ -19727,5 +20020,11 @@ module.exports = {
|
|
|
19727
20020
|
// 1.9.288: MCP 도구 수 단일 출처 (Codex #5) — 단위 테스트
|
|
19728
20021
|
_mcpToolCount,
|
|
19729
20022
|
// 1.9.289: shell-safe 인용 (Codex #3) — 단위 테스트
|
|
19730
|
-
_shellQuoteArg
|
|
20023
|
+
_shellQuoteArg,
|
|
20024
|
+
// 1.18.1: 명령 실행 권한 결정 (재실증 신규 P1: --test-cmd 비-JS 인터프리터 거짓차단) — 단위 테스트
|
|
20025
|
+
_isCommandPermitted, RUN_CORE_ALLOW,
|
|
20026
|
+
// 1.18.2: verify-claim 위장 스텁(빈 export 껍데기) 판정 — 단위 테스트
|
|
20027
|
+
_vcImplIsEmpty, _VC_EMPTY_SHELL_RE,
|
|
20028
|
+
// 1.18.3 (UR-0003): 분야별 자기질문 품질 렌즈 — 단위 테스트. 1.19.2: 파일→도메인 매핑(완료-검증 advisory)
|
|
20029
|
+
LENS_CATALOG, lensCmd, _lensDomainsForFiles, _mergeLensCatalog, _loadProjectLenses, _effectiveLensCatalog
|
|
19731
20030
|
};
|