leerness 1.36.74 → 1.36.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.36.76 — 2026-07-25 — 9차 헌트 이월 3건 종결 (P1 1·P2 2) — 헌트 8/8 전건 처리
4
+
5
+ - **P1 (#3) 손상 아카이브 복원 거부**: 잘린 UTF-8 아카이브가 U+FFFD 를 품은 채 "정상 복원"돼 손상 데이터가 active 로 유입되고 아카이브 원본이 소비되던 것(codex 정확 재현으로 확정 — 지난 라운드 제 절단 변형은 미재현이라 이월했었다). 치환문자 감지 시 복원 중단·아카이브 무변경.
6
+ - **P2 (#2) MCP read-only 가 대상을 변형**: ① 텔레메트리가 존재하지 않는 대상까지 디렉토리·usage-stats 를 생성 → 초기화된 하네스에만 기록. ② read-only 선언 `leerness_env_detect` 가 environment.json 을 기록 → MCP 경로는 `--no-write`(CLI 직접 호출은 종전대로 persist).
7
+ - **P2 (#8) 긴/이상 URL 파생 id**: 300자 호스트·IPv6 가 raw ENOENT 로 터지던 것 — 호스트 슬러그화 + 길이 상한 80(초과 시 절단+짧은 해시). 정상 id 는 종전과 동일(문서화 예시 무변).
8
+ - **(R-0001 검수 19회전, read-only 도구 59종 전수 스윕)**: (P1) **read-only 로 오선언된 쓰기 도구 9종** — handoff(last-handoff/tech-profile)·session_close(6파일)·skill_match·lsp·review_request·pre_wake_audit·audit(fix)·slash_commands 를 `safe-write` 로, task_export(--to 임의 경로)는 `project-write` 로 재선언 — enforce 정책이 이제 실제로 막는다. (#2) U+FFFD 판정을 **원시 바이트 fatal decode** 로 교체 — 사용자가 정당하게 붙여넣은 유효 U+FFFD 문자는 복원 가능(오탐 제거), malformed 바이트만 거부. (#3) 절단 해시 시드에 name 포함 — 같은 긴 URL·다른 name 이 같은 id 로 붕괴해 덮어쓰던 것. (#4) 충돌 접미사 후에도 상한 80 유지(단일 캡 함수). (Low) Windows 예약 이름(con 등) 회피.
9
+ - 무회귀 실측: 정상 아카이브 복원·유효 U+FFFD 복원·초기화된 하네스 텔레메트리·CLI env persist·기존 id 형식.
10
+ - 검증: selftest 334(티어 단언 신분류로 갱신), e2e +1(7 단언). 9차 헌트 8건 전건 처리 완료.
11
+
12
+ ## 1.36.75 — 2026-07-25 — UR-0066: 디자인 시안 우선 워크플로 (preview mockup) — R-0001 검수 18회전
13
+
14
+ 사용자 요청: 웹페이지/디자인 작업은 코드 구현 전에 디자인 시안을 먼저 제시하고 수정/승인을 질문으로 받도록.
15
+
16
+ - **`leerness preview mockup <P-ID> [--force]`**: 자립형(오프라인·외부 리소스 0) HTML 시안 스캐폴드를 `.harness/previews/<P-ID>-mockup.html` 에 생성 — AI 가 placeholder 를 실제 레이아웃 초안으로 교체해 사용자에게 브라우저로 제시. 기존 파일은 덮어쓰지 않음(--force 만 재생성, 이력은 mockup-regenerated 로 구분).
17
+ - **`preview add ... --mockup <파일>`**: 이미 만든 시안 첨부(일반 파일 + 프로젝트 루트 안 검증). **디자인/페이지 작업 자동 감지** 시 시안 생성 안내 출력(false-PASS 편향 — 일반 기능 요청엔 강제 안 함).
18
+ - **AGENTS.md(ko/en) 계약 강화**: 신규 페이지·디자인 요청 → 시안 제시·승인 전 실제 코드 작성 금지.
19
+ - **검수 7건 반영**: (High) 조작된 preview id 의 previews 디렉토리 밖 쓰기 차단(P-\d{4,} 강제 + 경로 격리 이중 가드) · (#2) `--mockup` 값이 제목에 흡수 · (#3) 디렉토리/루트 밖 첨부 거부 · (#4) 보호된 기존 시안도 경로는 스토어에 기록 · (#5) **승인 후 시안 생성/재생성 거부**(승인이 낡는 것 방지 — revise 로 되돌린 뒤에만) · (Low×2) 재생성 이력 구분·help 갱신.
20
+ - 검증: XSS 이스케이프·덮어쓰기 보호·감지 힌트·후방 호환(구 항목 mockupPath 없음) 실측, selftest 334, e2e +1(11 단언).
21
+
3
22
  ## 1.36.74 — 2026-07-25 — codex 9차 홀리스틱 헌트: 미점검 표면 6건 수정 (P1 3·P2 3)
4
23
 
5
24
  표면 회전 원칙에 따라 최근 라운드가 비추지 않은 사용자 데이터 표면(MCP 프로토콜·memory archive/restore·.leerness state·api-skill)을 헌트. 8건 보고 중 자체 재현 확정 6건 수정.
package/README.md CHANGED
@@ -122,7 +122,7 @@ MIT
122
122
  <!-- leerness:project-readme:start -->
123
123
  ## Leerness Project Harness
124
124
 
125
- 이 프로젝트는 Leerness v1.36.74 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
125
+ 이 프로젝트는 Leerness v1.36.76 하네스를 사용합니다. AI 에이전트는 작업 전 `leerness handoff`로 컨텍스트를 적재하고, 작업 후 `leerness check`/`leerness audit`/`leerness session close`를 수행해야 합니다.
126
126
 
127
127
  ### 정체성 — AI 에이전트 운영 레이어 (UR-0030)
128
128
 
@@ -176,7 +176,7 @@ leerness memory restore decision <date|title>
176
176
 
177
177
  ### MCP server (외부 AI 통합)
178
178
 
179
- Leerness v1.36.74는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
179
+ Leerness v1.36.76는 stdio JSON-RPC MCP server를 내장합니다 — Claude Code · Cursor · Codex CLI 등 외부 AI에 **86개 도구**를 노출:
180
180
 
181
181
  ```jsonc
182
182
  // 카테고리별
@@ -197,7 +197,7 @@ Leerness v1.36.74는 stdio JSON-RPC MCP server를 내장합니다 — Claude Cod
197
197
  `<<autonomous-loop-dynamic>>` 신호만 보내면 AI가:
198
198
  1) 다음 라운드 후보 선정 → 2) 코드 변경 → 3) 회귀 테스트 갱신 → 4) 전체 e2e 스위트 통과 → 5) npm publish + git tag → 6) main push → 7) session close → 8) 다음 라운드 예약.
199
199
 
200
- 현재 누적: **v1.9.x → 1.36.74 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
200
+ 현재 누적: **v1.9.x → 1.36.76 릴리스 태그 이력** (수백 라운드) · _reports/는 비공개 보존.
201
201
 
202
202
  ### 성능 가이드
203
203
 
@@ -235,5 +235,5 @@ leerness release pack --close --auto-main-push
235
235
  - `.harness/session-handoff.md`: 다음 세션 인수인계 (자동 작성)
236
236
  - `.harness/lessons.md` / `decisions.md` / `rules.md`: 영구 메모리 (5 surface)
237
237
 
238
- Last synced by Leerness v1.36.74: 2026-07-25
238
+ Last synced by Leerness v1.36.76: 2026-07-25
239
239
  <!-- leerness:project-readme:end -->
package/bin/leerness.js CHANGED
@@ -34,7 +34,7 @@ const { CAPABILITY_SURFACE, POWERFUL_COMMANDS, ADAPTERS, REUSE_CATEGORIES, REUSE
34
34
  const { tokenizeForRank: _tokenizeForRank, expandQuery: _expandQuery, scoreHits: _scoreHits, suggestTerms: _suggestTerms } = require('../lib/search-core'); // 1.36.23: memory search 랭킹 코어(순수·0-deps)
35
35
  const { findCorruptedStateJson: _findCorruptedStateJson } = require('../lib/state-integrity'); // 1.36.1 (클린룸 리뷰 FN): .harness/*.json 상태 무결성 (audit/health/check 공유)
36
36
 
37
- const VERSION = '1.36.74';
37
+ const VERSION = '1.36.76';
38
38
 
39
39
  // 1.9.290 (UR-0037, Codex gpt-5.5 #4 수렴): CLI 전용 부작용은 require 시 실행하지 않는다.
40
40
  // 이전: warning listener 제거 / NODE_OPTIONS 변경 / chcp IIFE 가 top-level 즉시 실행 → require('harness') 시 호스트 프로세스 오염.
@@ -548,7 +548,8 @@ function coreFiles(root, lang = 'ko', selectedSkills = [], opts = {}) {
548
548
  const project = detectProjectName(root);
549
549
  const skillRows = Object.entries(skillCatalog).map(([k, v]) => `| ${k} | ${v.displayNameKo} | ${v.capabilities.join(', ')} | ${v.lastUpdated} | ${v.verification} |`).join('\n');
550
550
  const _files = {
551
- 'AGENTS.md': `${MARK}\n# Leerness Agent Instructions\n\n## ⭐ 매 세션 첫 행동\n**반드시 \`.harness/session-workflow.md\`를 먼저 읽고 6단계 워크플로를 따른다**: 요청분석→계획→분배→sub-agent작업→종합검증→마감. 라운드 길이/복잡도 무관, drift 방지를 위해 모든 작업에 동일 흐름 유지.\n\n## 정적 vs 동적 — leerness 역할 경계\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/\` (에이전트 간 인수인계 표준 — state 명령 사용 시 생성). 메인 워크스페이스(.harness)와 별개.\n- leerness 는 AGENTS.md 를 **대체하지 않고 보완**한다. 정적 지침은 여기, 동적 상태는 leerness.\n\n## Mandatory read order (session start)\n1. **.harness/session-workflow.md** (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 clarify "<요청>"\` 이 감지한 질문 목록을 그대로 사용자에게 물어보세요. 신호가 없어도 스스로 판단이 갈리면 질문이 우선입니다.\n- **미리보기 승인 의무 (신규 기능)**: 사용자가 신규 기능 추가/구현을 요청하면 **코드를 먼저 작성하지 않습니다**. ① \`leerness preview add "<기능>" --design "<디자인/UX 설명>" --features "<기능 목록>"\` 으로 미리보기를 등록하고 ② 그 내용을 사용자에게 제시해 승인 또는 수정사항을 질문으로 받습니다. ③ 사용자가 승인하면 \`leerness preview approve <P-ID>\`, 수정 요구면 \`leerness preview revise <P-ID> --note "..."\` 후 미리보기를 고쳐 다시 제시합니다. **approve 전에는 해당 기능의 코드를 작성하지 않습니다.**\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\`의 분야별 자기질문에 답합니다 (코드: "선임 개발자가 복잡하다고 느끼지 않을까?" / 디자인: "선임 디자이너와 일반 사용자가 이쁘고 직관적이라 느낄까?").\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## 자연어 회고/통찰/브레인스토밍\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## 자연어 룰 처리\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## 룰 자동 적용\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`,
551
+ 'AGENTS.md': `${MARK}\n# Leerness Agent Instructions\n\n## ⭐ 매 세션 첫 행동\n**반드시 \`.harness/session-workflow.md\`를 먼저 읽고 6단계 워크플로를 따른다**: 요청분석→계획→분배→sub-agent작업→종합검증→마감. 라운드 길이/복잡도 무관, drift 방지를 위해 모든 작업에 동일 흐름 유지.\n\n## 정적 vs 동적 — leerness 역할 경계\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/\` (에이전트 간 인수인계 표준 — state 명령 사용 시 생성). 메인 워크스페이스(.harness)와 별개.\n- leerness 는 AGENTS.md 를 **대체하지 않고 보완**한다. 정적 지침은 여기, 동적 상태는 leerness.\n\n## Mandatory read order (session start)\n1. **.harness/session-workflow.md** (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 clarify "<요청>"\` 이 감지한 질문 목록을 그대로 사용자에게 물어보세요. 신호가 없어도 스스로 판단이 갈리면 질문이 우선입니다.\n- **미리보기 승인 의무 (신규 기능)**: 사용자가 신규 기능 추가/구현을 요청하면 **코드를 먼저 작성하지 않습니다**. ① \`leerness preview add "<기능>" --design "<디자인/UX 설명>" --features "<기능 목록>"\` 으로 미리보기를 등록하고 ② 그 내용을 사용자에게 제시해 승인 또는 수정사항을 질문으로 받습니다. ③ 사용자가 승인하면 \`leerness preview approve <P-ID>\`, 수정 요구면 \`leerness preview revise <P-ID> --note "..."\` 후 미리보기를 고쳐 다시 제시합니다. **approve 전에는 해당 기능의 코드를 작성하지 않습니다.**
552
+ - **디자인 시안 의무 (웹페이지/디자인 작업)**: 신규 페이지 제작·디자인/리디자인 요청이면 텍스트 설명만으로 끝내지 않습니다. \`leerness preview mockup <P-ID>\` 로 자립형 HTML 시안 스캐폴드(\`.harness/previews/<P-ID>-mockup.html\`)를 만들고, **placeholder 영역을 실제 레이아웃 초안(HTML/CSS, 외부 리소스 없이)으로 교체**한 뒤 사용자에게 브라우저로 열어 보여주고 수정/승인을 질문으로 받습니다. 수정 요구가 오면 시안 파일을 고쳐 다시 제시하고, **approve 전에는 실제 페이지/기능 코드를 작성하지 않습니다.** (이미 만든 시안이 있으면 \`preview add ... --mockup <파일>\` 로 첨부)\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\`의 분야별 자기질문에 답합니다 (코드: "선임 개발자가 복잡하다고 느끼지 않을까?" / 디자인: "선임 디자이너와 일반 사용자가 이쁘고 직관적이라 느낄까?").\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## 자연어 회고/통찰/브레인스토밍\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## 자연어 룰 처리\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## 룰 자동 적용\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`,
552
553
  '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**⭐ 매 세션 첫 행동**: \`.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## 자연어 영구 룰\n사용자가 "매 X마다 Y를 해줘" 같은 자연어 룰을 말하면 즉시 \`leerness rule add "Y" --trigger every-X\`로 등록하세요. 등록된 룰은 매 세션 \`handoff\`가 자동 출력하고, \`session close\`가 자동 검증해 보고합니다. 사용자가 "중지" / "그만" / "끄기"를 명시할 때만 \`rule pause/remove\`를 호출합니다.\n\n자세한 매핑은 AGENTS.md의 "자연어 룰 처리" 표를 참고하세요.\n`,
553
554
  '.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`,
554
555
  '.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`,
@@ -768,7 +769,7 @@ leerness memory restore <surface> <target> # archive → active 복귀 (DELETE
768
769
  // 1.36.59 (외부감사 F-05 시리즈 1회차): en 프로젝트에서 에이전트가 "지시"로 읽는 최상위 5종은 완전 영어 —
769
770
  // 실측: en init 산출물 61개 중 53개 한글 잔존, 그중 지시 레이어가 최고 영향. 나머지 표면은 후속 회차.
770
771
  if (lang === 'en') {
771
- _files['AGENTS.md'] = `${MARK}\n# Leerness Agent Instructions\n\n## ⭐ First action every session\n**Read \`.harness/session-workflow.md\` first and follow its 6-step workflow**: analyze request → plan → distribute → sub-agent work → integrated verification → close. Keep the same flow regardless of round length/complexity — that is what prevents drift.\n\n## Static vs dynamic — the leerness boundary\n**AGENTS.md = static project instructions** (coding rules, test commands, prohibitions, deploy steps — rarely change).\n**leerness = dynamic work state, memory, verification, handoff** (current goal, changed files, failed attempts, verification results, next-agent handoff — change every task).\n- Put rules/commands/prohibitions here in AGENTS.md.\n- Dynamic state (decisions/lessons/plan/progress/verification/handoff) is recorded by leerness in the **default workspace \`.harness/\`** (decisions.md / lessons.md / plan.md / progress-tracker.md / session-handoff.md) via \`leerness handoff\`, \`decision add\`, \`lesson save\`, etc.\n- (Optional) the JSON state substrate of \`leerness state show|start|record|verify|handoff\` (or MCP \`leerness_state_*\`) lives in \`.leerness/\` — the cross-agent handoff standard, separate from the main workspace.\n- leerness **complements** AGENTS.md, it does not replace it. Static instructions here, dynamic state in leerness.\n\n## Mandatory read order (session start)\n1. **.harness/session-workflow.md** (6-step workflow — highest priority)\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** (user-defined standing rules — follow every session)\n\n## Required behavior\n- Run \`leerness handoff .\` at session start to load context (handoff prints active rules automatically).\n- **Ask-on-ambiguity duty**: if the user's request has parts open to interpretation (vague qualifiers, pronouns, multiple alternatives, unclear scope), **ask the user instead of guessing**. Ask the questions produced by \`leerness clarify "<request>"\` verbatim. Even without detected signals, when your own judgment is split, asking wins.\n- **Preview-approval duty (new features)**: when the user asks for a new feature, **do not write code first**. ① Register a preview with \`leerness preview add "<feature>" --design "<design/UX>" --features "<list>"\`, ② present it and ask for approval or revisions, ③ on approval run \`leerness preview approve <P-ID>\`; on revision \`leerness preview revise <P-ID> --note "..."\` and re-present. **Never write the feature's code before approve.**\n- Classify work with \`leerness route <task-type>\` (planning, feature, bugfix, refactor, research, consistency, release, migration, session-start, session-close, harness-maintenance).\n- Never delete protected files/managed sections — merge, archive, or mark deprecated instead.\n- After meaningful changes update progress-tracker, current-state, task-log, session-handoff.\n- Before claiming completion, self-verify with \`leerness check .\` or \`leerness lazy detect .\` and answer the \`leerness lens\` self-questions per domain (code: "would a senior developer find this needlessly complex?" / design: "would a senior designer and an ordinary user find this pretty and intuitive?").\n- Before changes run the guards: \`leerness scan secrets .\`, \`leerness encoding check .\`.\n- Before duplicating a capability check design-system.md, consistency-policy.md, reuse-map.md.\n- Close every session with \`leerness session close .\` — 9 categories (done/in-progress/incomplete/planned/waiting/on-hold/blocked/dropped/verification) + active-rule verification.\n- Updates: \`leerness update --check\` (detect) → \`leerness update --yes\` (auto-migrate).\n\n## Natural-language retro/insights/brainstorm\n| User phrase | Run immediately |\n|---|---|\n| "retrospective / look back / wrap up" | \`leerness retro\` |\n| "retro for last N days" | \`leerness retro --days N\` |\n| "stats / cumulative metrics / insights" | \`leerness insights\` |\n| "brainstorm about X / materials on X / review before starting X" | \`leerness brainstorm "X"\` |\n\nsession close prints a one-line summary automatically every time and runs a deep retrospective every 5 sessions; call the commands immediately when the user asks explicitly.\n\n## Natural-language standing rules\nWhen the user states a standing rule ("do Y every X"), register it immediately:\n\n| User phrase | Run immediately |\n|---|---|\n| "bump the version every update" | \`leerness rule add "bump version (patch)" --trigger every-update\` |\n| "add patch notes every commit" | \`leerness rule add "add patch notes" --trigger every-commit\` |\n| "deploy at session close" | \`leerness rule add "deploy (release publish)" --trigger session-close\` |\n| "pause/stop rule X" | \`leerness rule pause <ID>\` (find the ID with \`rule list\`) |\n| "remove rule X" | \`leerness rule remove <ID>\` |\n| "stop all rules" | \`leerness rule stop\` |\n| "resume rules" | \`leerness rule resume-all\` or \`leerness rule resume <ID>\` |\n\nAfter registering, report the result (ID + trigger + description) and apply it every session until the user explicitly says stop/remove.\n\n## Automatic rule verification\n- **every-update / version rules**: checks package.json version change.\n- **CHANGELOG / patch-note rules**: checks CHANGELOG.md mtime.\n- **test / verify rules**: checks today's verify traces in review-evidence.md.\n- **deploy / publish / push rules**: not auto-verifiable → guides \`leerness release publish\`.\n\nAutomate verifiable rules with \`leerness release bump\`, \`leerness release note "..."\`, and \`leerness release publish\`.\n`;
772
+ _files['AGENTS.md'] = `${MARK}\n# Leerness Agent Instructions\n\n## ⭐ First action every session\n**Read \`.harness/session-workflow.md\` first and follow its 6-step workflow**: analyze request → plan → distribute → sub-agent work → integrated verification → close. Keep the same flow regardless of round length/complexity — that is what prevents drift.\n\n## Static vs dynamic — the leerness boundary\n**AGENTS.md = static project instructions** (coding rules, test commands, prohibitions, deploy steps — rarely change).\n**leerness = dynamic work state, memory, verification, handoff** (current goal, changed files, failed attempts, verification results, next-agent handoff — change every task).\n- Put rules/commands/prohibitions here in AGENTS.md.\n- Dynamic state (decisions/lessons/plan/progress/verification/handoff) is recorded by leerness in the **default workspace \`.harness/\`** (decisions.md / lessons.md / plan.md / progress-tracker.md / session-handoff.md) via \`leerness handoff\`, \`decision add\`, \`lesson save\`, etc.\n- (Optional) the JSON state substrate of \`leerness state show|start|record|verify|handoff\` (or MCP \`leerness_state_*\`) lives in \`.leerness/\` — the cross-agent handoff standard, separate from the main workspace.\n- leerness **complements** AGENTS.md, it does not replace it. Static instructions here, dynamic state in leerness.\n\n## Mandatory read order (session start)\n1. **.harness/session-workflow.md** (6-step workflow — highest priority)\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** (user-defined standing rules — follow every session)\n\n## Required behavior\n- Run \`leerness handoff .\` at session start to load context (handoff prints active rules automatically).\n- **Ask-on-ambiguity duty**: if the user's request has parts open to interpretation (vague qualifiers, pronouns, multiple alternatives, unclear scope), **ask the user instead of guessing**. Ask the questions produced by \`leerness clarify "<request>"\` verbatim. Even without detected signals, when your own judgment is split, asking wins.\n- **Preview-approval duty (new features)**: when the user asks for a new feature, **do not write code first**. ① Register a preview with \`leerness preview add "<feature>" --design "<design/UX>" --features "<list>"\`, ② present it and ask for approval or revisions, ③ on approval run \`leerness preview approve <P-ID>\`; on revision \`leerness preview revise <P-ID> --note "..."\` and re-present. **Never write the feature's code before approve.**\n- **Design-mockup duty (web pages / design work)**: for a new page, redesign, or any visual design request, do not stop at a text description. Run \`leerness preview mockup <P-ID>\` to generate a self-contained HTML mockup scaffold (\`.harness/previews/<P-ID>-mockup.html\`), **replace the placeholder with a real layout draft (HTML/CSS, no external resources)**, show it to the user in a browser, and ask for revisions or approval. On revision requests, edit the mockup and re-present. **Never write the actual page/feature code before approve.** (If you already built a mockup, attach it with \`preview add ... --mockup <file>\`.)\n- Classify work with \`leerness route <task-type>\` (planning, feature, bugfix, refactor, research, consistency, release, migration, session-start, session-close, harness-maintenance).\n- Never delete protected files/managed sections — merge, archive, or mark deprecated instead.\n- After meaningful changes update progress-tracker, current-state, task-log, session-handoff.\n- Before claiming completion, self-verify with \`leerness check .\` or \`leerness lazy detect .\` and answer the \`leerness lens\` self-questions per domain (code: "would a senior developer find this needlessly complex?" / design: "would a senior designer and an ordinary user find this pretty and intuitive?").\n- Before changes run the guards: \`leerness scan secrets .\`, \`leerness encoding check .\`.\n- Before duplicating a capability check design-system.md, consistency-policy.md, reuse-map.md.\n- Close every session with \`leerness session close .\` — 9 categories (done/in-progress/incomplete/planned/waiting/on-hold/blocked/dropped/verification) + active-rule verification.\n- Updates: \`leerness update --check\` (detect) → \`leerness update --yes\` (auto-migrate).\n\n## Natural-language retro/insights/brainstorm\n| User phrase | Run immediately |\n|---|---|\n| "retrospective / look back / wrap up" | \`leerness retro\` |\n| "retro for last N days" | \`leerness retro --days N\` |\n| "stats / cumulative metrics / insights" | \`leerness insights\` |\n| "brainstorm about X / materials on X / review before starting X" | \`leerness brainstorm "X"\` |\n\nsession close prints a one-line summary automatically every time and runs a deep retrospective every 5 sessions; call the commands immediately when the user asks explicitly.\n\n## Natural-language standing rules\nWhen the user states a standing rule ("do Y every X"), register it immediately:\n\n| User phrase | Run immediately |\n|---|---|\n| "bump the version every update" | \`leerness rule add "bump version (patch)" --trigger every-update\` |\n| "add patch notes every commit" | \`leerness rule add "add patch notes" --trigger every-commit\` |\n| "deploy at session close" | \`leerness rule add "deploy (release publish)" --trigger session-close\` |\n| "pause/stop rule X" | \`leerness rule pause <ID>\` (find the ID with \`rule list\`) |\n| "remove rule X" | \`leerness rule remove <ID>\` |\n| "stop all rules" | \`leerness rule stop\` |\n| "resume rules" | \`leerness rule resume-all\` or \`leerness rule resume <ID>\` |\n\nAfter registering, report the result (ID + trigger + description) and apply it every session until the user explicitly says stop/remove.\n\n## Automatic rule verification\n- **every-update / version rules**: checks package.json version change.\n- **CHANGELOG / patch-note rules**: checks CHANGELOG.md mtime.\n- **test / verify rules**: checks today's verify traces in review-evidence.md.\n- **deploy / publish / push rules**: not auto-verifiable → guides \`leerness release publish\`.\n\nAutomate verifiable rules with \`leerness release bump\`, \`leerness release note "..."\`, and \`leerness release publish\`.\n`;
772
773
  _files['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**⭐ First action every session**: follow the 6-step workflow in \`.harness/session-workflow.md\` (analyze → plan → distribute → sub-agent → verify → close). On drift critical, recover with \`leerness drift check --auto-fix\`.\n\nProtected files must not be deleted. Read .harness/anti-lazy-work-policy.md before claiming completion.\n\n## Natural-language standing rules\nWhen the user states "do Y every X", immediately run \`leerness rule add "Y" --trigger every-X\`. Registered rules are printed by every \`handoff\` and verified by \`session close\`. Only \`rule pause/remove\` when the user explicitly says stop/remove.\n\nSee the "Natural-language standing rules" table in AGENTS.md for the full mapping.\n`;
773
774
  // 1.36.62 (F-05 4회차): commands/AX/잔여 참조층 en 완역
774
775
  _files['.harness/test-evidence-policy.md'] = fm('test-evidence-policy', ['recording verification results'], ['verification format changes'], `# Test Evidence Policy\n\nEvery verification is appended to \`.harness/review-evidence.md\`.\n\n## Format\n\`\`\`\n## YYYY-MM-DD HH:MM\nTask: T-XXXX\nCommand: <command>\nExit: <code>\nNote: <key result summary>\nArtifacts: <screenshot/log paths>\n\`\`\`\n`, 'en');
@@ -3203,7 +3204,7 @@ function _selfTestCases() {
3203
3204
  { name: 'writeUtf8: 원자적 쓰기(temp→rename) 손상방지 행위 (UR-0038 외부리뷰 / CV-5 행위화 1.9.366)', run: () => { if (typeof writeUtf8 !== 'function') return false; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '__leerness_wu_')); try { const f = path.join(tmp, 'sub', 'a.txt'); writeUtf8(f, '한글 UTF-8 내용'); const okContent = read(f) === '한글 UTF-8 내용'; const noTmpLeft = fs.readdirSync(path.dirname(f)).every(n => !n.includes('.tmp-')); return okContent && noTmpLeft; } finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } } },
3204
3205
  { name: '_scrubTestEnv: npm test 시크릿 차단(_scrubEnv는 release 토큰 유지) (UR-0039 외부리뷰 1.9.299)', run: () => { const o = { N: process.env.NPM_TOKEN, L: process.env.LEERNESS_NPM_TOKEN }; process.env.NPM_TOKEN = 'sec1'; process.env.LEERNESS_NPM_TOKEN = 'sec2'; const base = _scrubEnv(); const test = _scrubTestEnv(); const r = base.NPM_TOKEN === 'sec1' && base.LEERNESS_NPM_TOKEN === 'sec2' && !test.NPM_TOKEN && !test.LEERNESS_NPM_TOKEN && !!test.PATH; if (o.N === undefined) delete process.env.NPM_TOKEN; else process.env.NPM_TOKEN = o.N; if (o.L === undefined) delete process.env.LEERNESS_NPM_TOKEN; else process.env.LEERNESS_NPM_TOKEN = o.L; return r; } },
3205
3206
  { name: 'shell 주입 표면 제거: fetchNpmLatest execFile+pkg검증 + runCommandSafe argList 인용 (UR-0040 외부리뷰 1.9.300)', run: () => { const src = read(__filename); const npmFix = /'view', pkg, 'version'/.test(src) && !/cp\.exec\(.npm view \$\{pkg\}/.test(src) && /패키지명 charset/.test(src) && !/cp\.execFile\('npm', \[[^\]]*\], \{ timeout: 12000, shell:/.test(src); const argFix = /argList\.map\(_shellQuoteArg\)\.join/.test(src); return npmFix && argFix && typeof _shellQuoteArg === 'function'; } },
3206
- { name: 'MCP requiredTier 메타데이터 + 정책 minTier 게이트 (UR-0041 외부리뷰 1.9.301)', run: () => { const T = require('../lib/mcp-tools'); const allValid = T.length >= 81 && T.every(t => PERMISSION_TIERS.includes(t.requiredTier)); const get = n => (T.find(t => t.name === n) || {}).requiredTier; const classOk = get('leerness_state_record') === 'safe-write' && get('leerness_provider_add') === 'safe-write' && get('leerness_web') === 'network' && get('leerness_handoff') === 'read-only' && get('leerness_audit') === 'read-only'; const src = read(__filename); const gateOk = /_tierRank\(minTier\) > _tierRank\(required\)/.test(src) && /_policyEnforce\(targetPath, cliArgs\.join\(' '\), _toolDef/.test(src); return allValid && classOk && gateOk; } },
3207
+ { name: 'MCP requiredTier 메타데이터 + 정책 minTier 게이트 (UR-0041 외부리뷰 1.9.301)', run: () => { const T = require('../lib/mcp-tools'); const allValid = T.length >= 81 && T.every(t => PERMISSION_TIERS.includes(t.requiredTier)); const get = n => (T.find(t => t.name === n) || {}).requiredTier; const classOk = get('leerness_state_record') === 'safe-write' && get('leerness_provider_add') === 'safe-write' && get('leerness_web') === 'network' && get('leerness_handoff') === 'safe-write' && get('leerness_audit') === 'safe-write' && get('leerness_task_export') === 'project-write'; const src = read(__filename); const gateOk = /_tierRank\(minTier\) > _tierRank\(required\)/.test(src) && /_policyEnforce\(targetPath, cliArgs\.join\(' '\), _toolDef/.test(src); return allValid && classOk && gateOk; } },
3207
3208
  { name: 'verify-claim git diff 시맨틱 교차검증: _gitChangedFiles/_claimFileInGit + strict FAIL 통합 (UR-0042 외부리뷰 1.9.302)', run: () => { const fnOk = typeof _gitChangedFiles === 'function' && typeof _claimFileInGit === 'function'; const matchOk = _claimFileInGit('src/api.js', new Set(['src/api.js'])) === true && _claimFileInGit('./src/api.js', new Set(['src/api.js'])) === true && _claimFileInGit('other.js', new Set(['src/api.js'])) === false && _claimFileInGit('x', null) === null; const src = read(__filename); const wired = /git diff 교차검증/.test(src) && /\|\| !gitClaimOk/.test(src) && /_gitChangedFiles\(root\)/.test(src); return fnOk && matchOk && wired; } },
3208
3209
  { name: '_withLock/_updateRun: lost-update 락(O_EXCL+재진입) + 적용 (UR-0043 외부리뷰 1.9.303)', run: () => { const src = read(__filename); const fnOk = typeof _withLock === 'function' && typeof _sleepSyncMs === 'function' && typeof _updateRun === 'function'; const reentrant = /if \(_heldLocks\.has\(lockPath\)\) return fn\(\)/.test(src); const excl = /fs\.openSync\(lockPath, 'wx'\)/.test(src); const applied = /const id = _withLock\(progressPath\(root\)/.test(src) && /_updateRun\(root, curId/.test(src); return fnOk && reentrant && excl && applied; } },
3209
3210
  { name: 'lib/analyzers: 분석/검증 함수 4종 모듈 단일출처 분리 (UR-0025 1.9.304)', run: () => { const m = require('../lib/analyzers'); return m._evidenceQuality === _evidenceQuality && m._shellGuardAnalyze === _shellGuardAnalyze && m._parseEvidenceStats === _parseEvidenceStats && m._claimFileInGit === _claimFileInGit && !/function _evidenceQuality\(evidence\) \{/.test(read(__filename)) && !/function _shellGuardAnalyze\(cmd, ctx\) \{/.test(read(__filename)); } },
@@ -3315,7 +3316,7 @@ function _selfTestCases() {
3315
3316
  const guardOk = s.includes('function _assertStoreParsable') && s.includes("code: 'E_STORE_CORRUPT'")
3316
3317
  && s.includes('_guardStore(has(') && s.includes("_assertStoreParsable(teamsJsonPath")
3317
3318
  && s.includes("_assertStoreParsable(_platformConstraintsPath");
3318
- const hashOk = s.includes('function _shortHash') && s.includes("id = id + '-' + _shortHash(url)") && s.includes('urls:\\s*\\r?\\n');
3319
+ const hashOk = s.includes('function _shortHash') && s.includes("id = _apiSkillIdCap(id + '-' + _shortHash(url), url, name)") && s.includes('urls:\\s*\\r?\\n');
3319
3320
  return transitiveOk && cycleSafe && aliasOk && guardOk && hashOk;
3320
3321
  } },
3321
3322
  { name: '반복 마이그레이션 커스텀 보존 (1.36.28, 사용자 보고): _managedMerge 라인-diff 이월 — 2번째부터 유실되던 CLAUDE/AGENTS 커스텀 지시 (행위검사)', run: () => {
@@ -5976,14 +5977,26 @@ function _shortHash(s) {
5976
5977
  function _apiSkillsDir(root) {
5977
5978
  return path.join(absRoot(root), '.harness', 'api-skills');
5978
5979
  }
5980
+ // 1.36.76 (검수 #3/#4/Low): id 최종 안전화 단일 지점 — 상한 80(초과 시 절단+해시), 해시 시드는 url+name 전체
5981
+ // 정체성(같은 URL·다른 name 이 같은 id 로 붕괴하던 것), Windows 예약 이름(con/prn/aux/nul/com1..9/lpt1..9) 회피.
5982
+ // 충돌 접미사(-hash) 추가 후에도 이 함수를 다시 거쳐 상한이 유지된다.
5983
+ function _apiSkillIdCap(id, url, name) {
5984
+ const seed = String(url || '') + '' + String(name || '');
5985
+ if (!id || id.length > 80) id = (id || 'api').slice(0, 72).replace(/-+$/, '') + '-' + _shortHash(seed);
5986
+ if (/^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(id)) id = id + '-' + _shortHash(seed);
5987
+ return id;
5988
+ }
5979
5989
  function _apiSkillId(url, name) {
5980
5990
  // 도메인 + path slug 로 id 생성. 예: developers.coupangcorp.com/articles/360033877853 → coupang-articles-360033877853
5981
5991
  try {
5982
5992
  const u = new URL(url);
5983
- const host = u.hostname.replace(/^www\./, '').split('.').slice(0, -1).join('-') || u.hostname;
5993
+ // 1.36.76 (9차 헌트 #8): 파생 id 를 항상 안전한 파일명으로 — 긴 호스트(300자)·IPv6([::1])
5994
+ // raw ENOENT 를 뱉던 것. 호스트도 슬러그화 + 전체 길이 상한(80) 초과 시 절단+짧은 해시로 유일성 유지.
5995
+ const host = (u.hostname.replace(/^www\./, '').split('.').slice(0, -1).join('-') || u.hostname).replace(/[^a-z0-9가-힣\-_]+/gi, '-');
5984
5996
  const pathSlug = u.pathname.replace(/^\/+|\/+$/g, '').replace(/[^a-z0-9가-힣\-_]+/gi, '-').slice(-50);
5985
5997
  const base = `${host}-${pathSlug}`.replace(/-+/g, '-').replace(/^-|-$/g, '');
5986
- return (name ? base + '-' + name.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 30) : base).replace(/-+/g, '-');
5998
+ let id = (name ? base + '-' + name.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 30) : base).replace(/-+/g, '-').replace(/^-|-$/g, '');
5999
+ return _apiSkillIdCap(id, url, name);
5987
6000
  } catch { return 'api-' + Date.now(); }
5988
6001
  }
5989
6002
  function _fetchUrl(url, opts = {}) {
@@ -6238,7 +6251,7 @@ async function apiSkillCmd(root, sub) {
6238
6251
  const existing = fs.readFileSync(_idFile(id), 'utf8');
6239
6252
  const um = existing.match(/^urls:\s*\r?\n\s*-\s*(\S.*)$/m); // 'urls:' 블록의 첫 항목만 (--- 구분선 오매치 방지)
6240
6253
  const storedUrl = um ? um[1].trim() : '';
6241
- if (storedUrl && storedUrl !== url) id = id + '-' + _shortHash(url);
6254
+ if (storedUrl && storedUrl !== url) id = _apiSkillIdCap(id + '-' + _shortHash(url), url, name); // 1.36.76 (검수 #4): 접미 후에도 상한 80 유지
6242
6255
  }
6243
6256
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
6244
6257
  const md = _serializeAPISkill(id, skillName, [url], direction, doc);
@@ -6262,7 +6275,7 @@ async function apiSkillCmd(root, sub) {
6262
6275
  const existing = fs.readFileSync(_idFileS(id), 'utf8');
6263
6276
  const um = existing.match(/^urls:\s*\r?\n\s*-\s*(\S.*)$/m);
6264
6277
  const storedUrl = um ? um[1].trim() : '';
6265
- if (storedUrl && storedUrl !== url) id = id + '-' + _shortHash(url);
6278
+ if (storedUrl && storedUrl !== url) id = _apiSkillIdCap(id + '-' + _shortHash(url), url, name); // 1.36.76 (검수 #4): 접미 후에도 상한 80 유지
6266
6279
  }
6267
6280
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
6268
6281
  const stub = {
@@ -6739,7 +6752,7 @@ function commandsCmd(root) {
6739
6752
  { cmd: 'clarify "<사용자 요청>" [--json]', desc: '요청 모호성 신호 감지 → 사용자에게 물을 질문 생성 (추측 구현 방지) — 1.36.51 UR-0061' },
6740
6753
  { cmd: 'tech [--json]', desc: '기술 프로필 — 개발 언어·연결 서비스 자동 감지 + 마이그레이션/언어전환 이력, 그래프 🛠 탭 표시 — 1.36.53 UR-0062' },
6741
6754
  { cmd: 'integrity check [--repair] [--json]', desc: 'managed 정책-문서 12종 무결성(부재/H1 상실/절단) 점검 — --repair: archive 대피 후 템플릿 재생성 — 1.36.57 감사 F-04' },
6742
- { cmd: 'preview add|list|show|approve|revise', desc: '신규 기능 미리보기 승인 워크플로 — approve 전 코드 작성 금지 계약 — 1.36.51 UR-0061' },
6755
+ { cmd: 'preview add|list|show|approve|revise|mockup', desc: '신규 기능 미리보기 승인 워크플로 — approve 전 코드 작성 금지 계약. mockup <P-ID> [--force]: 자립형 HTML 디자인 시안 스캐폴드 · add --mockup <파일>: 기존 시안 첨부 — 1.36.51 UR-0061 · 1.36.75 UR-0066' },
6743
6756
  { cmd: 'review <file> --persona <ids>', desc: '페르소나 리뷰 (1.9.29)' },
6744
6757
  { cmd: 'brainstorm "<topic>" [--include-code]', desc: '워크스페이스 회수 + 코드 grep' }
6745
6758
  ],
@@ -9737,6 +9750,11 @@ function memoryRestoreCmd(root, surface, target) {
9737
9750
  const hd = path.join(root, '.harness');
9738
9751
  const archivePath = path.join(hd, `${surface}.archive.md`);
9739
9752
  if (!exists(archivePath)) return fail(`${surface}.archive.md 없음 — 복원할 항목 없음`);
9753
+ // 1.36.76 (9차 헌트 #3 P1) → (검수 #2): 잘린/깨진 UTF-8 아카이브가 손상 데이터를 active 로 유입시키던 것.
9754
+ // 판정은 "디코딩 결과에 U+FFFD 포함"이 아니라 **원시 바이트의 UTF-8 유효성**(fatal decode) — 사용자가 정당하게
9755
+ // 붙여넣은 실제 U+FFFD 문자(유효 EF BF BD)는 통과하고, 진짜 malformed 바이트만 거부한다.
9756
+ try { new TextDecoder('utf-8', { fatal: true }).decode(readBuf(archivePath)); }
9757
+ catch { failJson(has('--json'), 'archive_corrupt', `${surface}.archive.md 의 UTF-8 바이트 손상 — 파일이 잘렸거나 인코딩 깨짐. 복원 중단(아카이브 무변경) — 원본 복구 후 재시도`); return; }
9740
9758
  const text = read(archivePath);
9741
9759
  // archive 헤더 (# X archive) 와 본문 분리
9742
9760
  const headerMatch = text.match(/^(# [^\n]*\n+)([\s\S]*)$/);
@@ -18036,6 +18054,9 @@ function _bumpUsage(root, cmdName) {
18036
18054
  // 1.9.70: MCP tools/call 자동 사용 통계 — 도구별 호출 카운트
18037
18055
  function _bumpMcpUsage(root, toolName) {
18038
18056
  try {
18057
+ // 1.36.76 (9차 헌트 #2a): read-only 호출조차 텔레메트리가 대상 프로젝트에 디렉토리·파일을 만들었다
18058
+ // (존재하지 않는 대상까지 통째로 생성). 통계는 이미 초기화된 하네스에만 기록 — 읽기가 대상을 변형하지 않는다.
18059
+ if (!exists(path.join(absRoot(root), '.harness'))) return;
18039
18060
  const stats = _readUsageStats(root);
18040
18061
  if (!stats.mcp) stats.mcp = { tools: {} };
18041
18062
  if (!stats.mcp.tools) stats.mcp.tools = {};
@@ -18809,7 +18830,7 @@ function _mcpToCliArgs(name, args, targetPath) {
18809
18830
  if (args.coChangesWith) cliArgs.push('--co-changes-with', String(args.coChangesWith));
18810
18831
  break;
18811
18832
  // 1.9.145: 실행 환경 자동 감지
18812
- case 'leerness_env_detect': cliArgs = ['env', 'detect', targetPath, '--json']; break;
18833
+ case 'leerness_env_detect': cliArgs = ['env', 'detect', targetPath, '--json', '--no-write']; break; // 1.36.76 (#2b): read-only 선언 도구는 environment.json 을 쓰지 않는다 (persist 는 CLI 직접 호출로)
18813
18834
  // 1.9.158: Provider Registry — 외부 AI 가 등록된 provider 회수
18814
18835
  case 'leerness_provider_list': cliArgs = ['provider', 'list', '--path', targetPath, '--json']; break;
18815
18836
  // 1.9.159: Provider Registry CRUD — 외부 AI 가 자가 확장
@@ -23262,7 +23283,7 @@ async function main() {
23262
23283
  // 미지 플래그(--design/--features/--note)의 값이 positional 로 새는 것 차단 — 원시 argv 에서 플래그+값 스킵 (1.36.49 release note 패턴)
23263
23284
  const _pvRaw = process.argv.slice(2); const _pvI = _pvRaw.indexOf('preview'); const _pvToks = [];
23264
23285
  for (let i = _pvI + 1; i < _pvRaw.length && _pvI >= 0; i++) {
23265
- if (_pvRaw[i].startsWith('--')) { if (['--design', '--features', '--note', '--path'].includes(_pvRaw[i]) && _pvRaw[i + 1]) i++; continue; }
23286
+ if (_pvRaw[i].startsWith('--')) { if (['--design', '--features', '--note', '--path', '--mockup'].includes(_pvRaw[i]) && _pvRaw[i + 1]) i++; continue; } // 1.36.75 (검수 #2): --mockup 값이 제목에 흡수되던 것
23266
23287
  _pvToks.push(_pvRaw[i]);
23267
23288
  }
23268
23289
  return _clar.previewCmd(arg('--path', process.cwd()), _pvToks[0], _pvToks.slice(1), { has, arg, _withLock }); // 1.36.54 (#2): 변경은 락 직렬화
package/lib/clarify.js CHANGED
@@ -82,6 +82,62 @@ function _nextPreviewId(list) {
82
82
 
83
83
  function pendingPreviews(root) { return _loadPreviews(root).filter(p => p.status === 'proposed' || p.status === 'revision-requested'); }
84
84
 
85
+ // 1.36.75 (UR-0066): 디자인/페이지 작업 감지 — "신규 페이지·디자인 요청은 코드 전에 HTML 시안 먼저" 리마인더용.
86
+ // false-PASS 편향: 명시적 신호어가 있을 때만 (일반 기능 요청까지 시안 강제하지 않는다).
87
+ const DESIGN_WORK_RE = /(페이지\s*(제작|추가|만들|생성)|새\s*페이지|신규\s*페이지|디자인\s*(작업|해|변경|개편|시안)|리디자인|랜딩\s*페이지|화면\s*(설계|디자인)|UI\s*(작업|개편|디자인)|redesign|landing page|new page|design (?:a |the )?(?:page|screen|ui))/i;
88
+ function _isDesignWork(text) { return DESIGN_WORK_RE.test(String(text || '')); }
89
+
90
+ // HTML 이스케이프 (시안 스캐폴드에 사용자 텍스트 임베드용)
91
+ function _esc(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); }
92
+
93
+ // 자립형 HTML 시안 스캐폴드 — AI 가 섹션을 채워 사용자에게 브라우저로 제시. 0-deps·오프라인(외부 리소스 없음).
94
+ function _mockupScaffold(p) {
95
+ const feats = (p.features || []).map(f => ` <li>${_esc(f)}</li>`).join('\n');
96
+ return `<!doctype html>
97
+ <html lang="ko">
98
+ <head>
99
+ <meta charset="utf-8">
100
+ <meta name="viewport" content="width=device-width, initial-scale=1">
101
+ <title>${_esc(p.id)} 시안 — ${_esc(p.title)}</title>
102
+ <style>
103
+ :root { --ink:#1a1a1a; --sub:#666; --line:#e5e5e5; --accent:#2563eb; --bg:#fafafa; }
104
+ * { box-sizing:border-box; margin:0; }
105
+ body { font-family:system-ui,-apple-system,'Segoe UI',sans-serif; color:var(--ink); background:var(--bg); }
106
+ .meta { background:#fff; border-bottom:2px solid var(--accent); padding:14px 24px; font-size:13px; color:var(--sub); }
107
+ .meta b { color:var(--ink); }
108
+ .meta .status { float:right; }
109
+ main { max-width:960px; margin:32px auto; padding:0 24px; }
110
+ section.mock { background:#fff; border:1px dashed var(--line); border-radius:10px; padding:28px; margin-bottom:20px; }
111
+ section.mock h2 { font-size:15px; color:var(--accent); margin-bottom:12px; }
112
+ .placeholder { border:2px dashed #cbd5e1; border-radius:8px; padding:40px; text-align:center; color:#94a3b8; font-size:14px; }
113
+ .notes { font-size:13px; color:var(--sub); line-height:1.7; }
114
+ .notes li { margin-left:18px; }
115
+ </style>
116
+ </head>
117
+ <body>
118
+ <div class="meta"><b>${_esc(p.id)}</b> 「${_esc(p.title)}」 — 디자인 시안 <span class="status">status: ${_esc(p.status)} · ${_esc(p.createdAt || '')}</span></div>
119
+ <main>
120
+ <section class="mock">
121
+ <h2>디자인 방향</h2>
122
+ <p class="notes">${_esc(p.design || '(preview add --design 으로 방향을 기록하세요)')}</p>
123
+ </section>
124
+ <section class="mock">
125
+ <h2>포함 기능</h2>
126
+ <ul class="notes">
127
+ ${feats || ' <li>(없음 — --features "a,b" 로 기록)</li>'}
128
+ </ul>
129
+ </section>
130
+ <section class="mock">
131
+ <h2>화면 시안 — ⬇ AI 는 이 영역을 실제 레이아웃 초안으로 교체하세요</h2>
132
+ <div class="placeholder">여기에 헤더 / 히어로 / 본문 / 푸터 등 실제 화면 구조의 HTML/CSS 초안을 그립니다.<br>외부 리소스 없이(오프라인) 이 파일 하나로 열리게 유지하세요.</div>
133
+ </section>
134
+ </main>
135
+ <!-- leerness:mockup ${_esc(p.id)} — 이 파일은 시안입니다. 승인(leerness preview approve ${_esc(p.id)}) 전 실제 코드 작성 금지. -->
136
+ </body>
137
+ </html>
138
+ `;
139
+ }
140
+
85
141
  // `leerness preview add "<제목>" [--design "..."] [--features "a,b"] | list | show <id> | approve <id> | revise <id> --note "..."`
86
142
  function previewCmd(root, sub, rest, deps = {}) {
87
143
  const { has, arg } = deps;
@@ -90,7 +146,7 @@ function previewCmd(root, sub, rest, deps = {}) {
90
146
  if (!exists(path.join(root, '.harness'))) { failJson(json, 'harness_missing', `leerness 미설치: ${root} — 먼저 leerness init`); return; }
91
147
  sub = sub || 'list';
92
148
  // 1.36.54 (#2 High): 변경 하위명령은 락 안에서 재로드→검증→저장 전체를 직렬화 — 동시 add 의 lost-update 차단.
93
- const _mutating = ['add', 'approve', 'revise'].includes(sub);
149
+ const _mutating = ['add', 'approve', 'revise', 'mockup'].includes(sub); // 1.36.75: mockup 도 스토어에 경로 기록
94
150
  if (_mutating && deps._withLock && !deps._locked) {
95
151
  return deps._withLock(_previewsPath(root), () => previewCmd(root, sub, rest, Object.assign({}, deps, { _locked: true })));
96
152
  }
@@ -106,10 +162,25 @@ function previewCmd(root, sub, rest, deps = {}) {
106
162
  const design = arg ? (arg('--design', '') || '') : '';
107
163
  const features = (arg ? (arg('--features', '') || '') : '').split(',').map(s => s.trim()).filter(Boolean);
108
164
  const p = { id: _nextPreviewId(list), title, design, features, status: 'proposed', createdAt: now(), history: [{ at: now(), event: 'proposed' }] };
165
+ // 1.36.75 (UR-0066): --mockup <파일> — AI 가 만든 시안 HTML 을 등록 시점에 첨부 (존재 검증)
166
+ const mk = arg ? (arg('--mockup', '') || '') : '';
167
+ if (mk) {
168
+ const mkAbs = path.resolve(path.isAbsolute(mk) ? mk : path.join(root, mk));
169
+ // (검수 #3 Medium) 존재만 보던 검증 강화: 일반 파일이어야 하고(디렉토리 거부), 프로젝트 루트 안이어야 한다
170
+ // — 시안은 프로젝트 산출물이라 루트 밖 경로는 이식성이 깨진다.
171
+ let _isFile = false;
172
+ try { _isFile = require('fs').statSync(mkAbs).isFile(); } catch {}
173
+ if (!_isFile) { failJson(json, 'mockup_not_found', `--mockup 파일 없음(또는 디렉토리): ${mkAbs} — 먼저 시안 파일을 만들거나, 등록 후 leerness preview mockup <P-ID> 로 스캐폴드 생성`); return; }
174
+ const _rel = path.relative(root, mkAbs);
175
+ if (_rel.startsWith('..') || path.isAbsolute(_rel)) { failJson(json, 'mockup_outside_root', `--mockup 은 프로젝트 루트 안의 파일이어야 합니다: ${mkAbs} (root: ${root})`); return; }
176
+ p.mockupPath = _rel.replace(/\\/g, '/');
177
+ }
109
178
  list.push(p);
110
179
  if (!_savePreviews(root, list, json)) return;
111
180
  if (json) { log(JSON.stringify({ ok: true, ...p }, null, 2)); return; }
112
181
  ok(`preview 등록: ${p.id} 「${title}」 (status: proposed)`);
182
+ if (p.mockupPath) log(` 🎨 시안: ${p.mockupPath} — 사용자가 브라우저로 열어 확인`);
183
+ else if (_isDesignWork(title + ' ' + design)) log(` 🎨 디자인/페이지 작업 감지 — leerness preview mockup ${p.id} 로 HTML 시안 스캐폴드를 만들어 채운 뒤 제시하세요.`);
113
184
  log(` → 사용자에게 이 미리보기(디자인/기능)를 제시하고 승인/수정 답을 받으세요.`);
114
185
  log(` → 승인: leerness preview approve ${p.id} · 수정요구: leerness preview revise ${p.id} --note "..."`);
115
186
  log(` ⓘ 계약: approve 전에는 이 기능의 코드를 작성하지 않는다.`);
@@ -122,7 +193,7 @@ function previewCmd(root, sub, rest, deps = {}) {
122
193
  if (!list.length) { log(' (없음) — 신규 기능 착수 전: leerness preview add "<제목>" --design "..." --features "a,b"'); return; }
123
194
  for (const p of list) {
124
195
  const icon = p.status === 'approved' ? '✅' : (p.status === 'revision-requested' ? '📝' : '⏳');
125
- log(` ${icon} ${p.id} ${p.title} [${p.status}]`);
196
+ log(` ${icon} ${p.id} ${p.title} [${p.status}]${p.mockupPath ? ' 🎨 ' + p.mockupPath : ''}`);
126
197
  }
127
198
  const pend = pendingPreviews(root).length;
128
199
  if (pend) warn(` 미승인 ${pend}건 — 사용자 답을 받기 전 해당 기능 코드 작성 금지`);
@@ -131,14 +202,49 @@ function previewCmd(root, sub, rest, deps = {}) {
131
202
 
132
203
  const id = (rest || [])[0];
133
204
  const p = list.find(x => x.id === id);
134
- if (sub === 'show' || sub === 'approve' || sub === 'revise') {
205
+ if (sub === 'show' || sub === 'approve' || sub === 'revise' || sub === 'mockup') {
135
206
  if (!id) { failJson(json, 'id_required', `preview ${sub} <P-ID> 필요 (leerness preview list 로 확인)`); return; }
136
207
  if (!p) { failJson(json, 'not_found', `preview 없음: ${id}`); return; }
137
208
  }
138
209
 
210
+ // 1.36.75 (UR-0066): `preview mockup <P-ID> [--force]` — 자립형 HTML 시안 스캐폴드 생성 + 스토어에 경로 기록.
211
+ // AI 워크플로: 스캐폴드 생성 → placeholder 를 실제 레이아웃 초안으로 교체 → 사용자에게 브라우저로 제시 → approve/revise.
212
+ if (sub === 'mockup') {
213
+ // (검수 High) 스토어의 id 는 신뢰 입력이 아니다 — 조작된 id(`..\..\owned`)가 경로에 끼어들면 루트 밖 쓰기.
214
+ // 정식 형식(P-\d{4,}) 강제 + 산출 경로의 .harness/previews 내부 확인 이중 가드.
215
+ if (!/^P-\d{4,}$/.test(p.id)) { failJson(json, 'invalid_id', `preview id 형식 무효: ${JSON.stringify(p.id)} — previews.json 수동 복구 필요 (정식: P-0001)`); return; }
216
+ const mkDir = path.join(root, '.harness', 'previews');
217
+ const mkFile = path.resolve(mkDir, `${p.id}-mockup.html`);
218
+ if (path.relative(mkDir, mkFile).startsWith('..')) { failJson(json, 'invalid_id', `시안 경로가 previews 디렉토리를 벗어남: ${mkFile}`); return; }
219
+ // (검수 #5 Medium) 승인 후 시안 생성/재생성은 승인 상태를 낡게 만든다 — revise 로 되돌린 뒤에만 허용
220
+ if (p.status === 'approved') { failJson(json, 'already_approved', `${p.id} 는 이미 승인됨 — 시안을 바꾸려면 먼저 leerness preview revise ${p.id} --note "..." 로 수정 상태로 되돌리세요`); return; }
221
+ const _regen = exists(mkFile);
222
+ if (_regen && !(has && has('--force'))) {
223
+ // 이미 있으면 덮어쓰지 않는다 — AI 가 채워 넣은 시안 보호 (재생성은 --force)
224
+ // (검수 #4 Medium) 보호하더라도 스토어에 경로는 기록 — list/show 에서 시안이 안 보이던 것
225
+ const relPath = path.relative(root, mkFile).replace(/\\/g, '/');
226
+ if (p.mockupPath !== relPath) { p.mockupPath = relPath; if (!_savePreviews(root, list, json)) return; }
227
+ if (json) { log(JSON.stringify({ ok: true, id: p.id, mockupPath: relPath, created: false, note: 'exists — use --force to regenerate' }, null, 2)); return; }
228
+ ok(`시안 이미 존재: ${path.relative(root, mkFile)} — 내용 보호를 위해 덮어쓰지 않음 (재생성: --force) · 경로는 기록됨`);
229
+ return;
230
+ }
231
+ writeUtf8(mkFile, _mockupScaffold(p));
232
+ p.mockupPath = path.relative(root, mkFile).replace(/\\/g, '/');
233
+ // (검수 Low) 재생성은 별도 이벤트로 — 이력 오해 방지
234
+ (p.history = p.history || []).push({ at: now(), event: _regen ? 'mockup-regenerated' : 'mockup-created' });
235
+ if (!_savePreviews(root, list, json)) return;
236
+ if (json) { log(JSON.stringify({ ok: true, id: p.id, mockupPath: p.mockupPath, created: !_regen, regenerated: _regen }, null, 2)); return; }
237
+ ok(`시안 ${_regen ? '재생성' : '스캐폴드 생성'}: ${p.mockupPath}`);
238
+ log(` → AI: placeholder 영역을 실제 레이아웃 초안(HTML/CSS)으로 교체하세요 (외부 리소스 없이).`);
239
+ log(` → 사용자에게 이 파일을 브라우저로 열어 보여주고 승인/수정을 질문으로 받으세요.`);
240
+ log(` ⓘ 계약: approve 전에는 실제 기능 코드를 작성하지 않는다.`);
241
+ return;
242
+ }
243
+
139
244
  if (sub === 'show') {
140
245
  if (json) { log(JSON.stringify({ ok: true, ...p }, null, 2)); return; }
141
246
  log(`# ${p.id} 「${p.title}」 [${p.status}]`);
247
+ if (p.mockupPath) log(` 🎨 시안: ${p.mockupPath}`);
142
248
  if (p.design) log(` 디자인: ${p.design}`);
143
249
  if (p.features && p.features.length) { log(' 기능:'); p.features.forEach(f => log(` - ${f}`)); }
144
250
  (p.history || []).forEach(h => log(` · ${h.at} ${h.event}${h.note ? ` — ${h.note}` : ''}`));
@@ -166,7 +272,7 @@ function previewCmd(root, sub, rest, deps = {}) {
166
272
  ok(`${p.id} 수정요구 기록${note ? ` — ${note}` : ''} — 미리보기를 수정해 다시 제시하세요 (코드 작성은 계속 보류).`);
167
273
  return;
168
274
  }
169
- failJson(json, 'unknown_subcommand', `알 수 없는 preview 하위명령: ${sub} (가능: add, list, show, approve, revise)`);
275
+ failJson(json, 'unknown_subcommand', `알 수 없는 preview 하위명령: ${sub} (가능: add, list, show, approve, revise, mockup)`);
170
276
  }
171
277
 
172
- module.exports = { CLARIFY_SIGNALS, _clarifySignals, clarifyCmd, previewCmd, pendingPreviews, _previewsPath, _loadPreviews, _loadPreviewsChecked };
278
+ module.exports = { CLARIFY_SIGNALS, _clarifySignals, clarifyCmd, previewCmd, pendingPreviews, _previewsPath, _loadPreviews, _loadPreviewsChecked, _isDesignWork, DESIGN_WORK_RE, _mockupScaffold };
package/lib/mcp-tools.js CHANGED
@@ -4,9 +4,9 @@
4
4
  'use strict';
5
5
 
6
6
  module.exports = [
7
- { name: 'leerness_handoff', requiredTier: 'read-only', description: '워크스페이스 컨텍스트(plan/progress/decisions) 적재', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
7
+ { name: 'leerness_handoff', requiredTier: 'safe-write', description: '워크스페이스 컨텍스트(plan/progress/decisions) 적재', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
8
8
  { name: 'leerness_drift_check', requiredTier: 'read-only', description: '1.9.136 — AI 에이전트 leerness 미사용 drift 자동 감지 JSON ({ root, score, level, signals[], healthy }). 5+ 신호 + 4단계 레벨 (🟢 healthy / 🟡 warning / 🟠 caution / 🔴 critical). 보안 신호 통합 (1.9.78)', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
9
- { name: 'leerness_audit', requiredTier: 'read-only', description: '1.9.102 — 워크스페이스 일관성 감사 JSON (warnings/failures/fixed/healthy + findings[]. kind 11종: design_dup/design_system_default/reuse_map_empty/milestone_unlinked/handoff_not_generated/current_state_stale/readme_version_mismatch/npm_cve/gitignore_missing_secrets/env_keys_missing/strict_promoted)', inputSchema: { type: 'object', properties: { path: { type: 'string' }, fix: { type: 'boolean' }, strict: { type: 'boolean' } } } },
9
+ { name: 'leerness_audit', requiredTier: 'safe-write', description: '1.9.102 — 워크스페이스 일관성 감사 JSON (warnings/failures/fixed/healthy + findings[]. kind 11종: design_dup/design_system_default/reuse_map_empty/milestone_unlinked/handoff_not_generated/current_state_stale/readme_version_mismatch/npm_cve/gitignore_missing_secrets/env_keys_missing/strict_promoted)', inputSchema: { type: 'object', properties: { path: { type: 'string' }, fix: { type: 'boolean' }, strict: { type: 'boolean' } } } },
10
10
  { name: 'leerness_verify_claim', requiredTier: 'read-only', description: 'AI 거짓 완료 자동 검증 (evidence 파일 + 실 테스트 실행). 1.9.309(UR-0048): done/완료 주장은 evidence(수정파일+테스트) 기본 강제 — 증거 없는 done 은 FAIL(exit 1). lenient=true 로 완화, runTests/strictClaims 추가 가능. 응답 verdict.evidenceComplete 포함.', inputSchema: { type: 'object', properties: { taskId: { type: 'string' }, path: { type: 'string' }, runTests: { type: 'boolean' }, strictClaims: { type: 'boolean' }, lenient: { type: 'boolean' } }, required: ['taskId'] } },
11
11
  { name: 'leerness_verify_claim_all', requiredTier: 'read-only', description: '1.33.3 — 모든 done/완료 주장을 한 번에 검증(CI·스케일). progress-tracker 의 done 항목 전부를 verify-claim 정밀 검사(파일 존재·스텁·부풀린 카운트·증거 완전성)로 일괄 점검. 응답 { ok, total, failed, results:[{id,ok,reasons}] }. 세션 마감 전 "내 완료 주장 전부 증거와 맞는가?" 자가 점검용. runTests/strictClaims/lenient 추가 가능.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, runTests: { type: 'boolean' }, strictClaims: { type: 'boolean' }, lenient: { type: 'boolean' } } } },
12
12
  { name: 'leerness_contract_verify', requiredTier: 'read-only', description: '명세 ↔ 구현 함수/필드 일치 자동 검사', inputSchema: { type: 'object', properties: { spec: { type: 'string' }, impl: { type: 'string' } }, required: ['spec', 'impl'] } },
@@ -14,13 +14,13 @@ module.exports = [
14
14
  { name: 'leerness_reuse_map', requiredTier: 'read-only', description: '워크스페이스 중복 함수/capability 자동 감지 (--all-apps + fuzzy 매칭)', inputSchema: { type: 'object', properties: { path: { type: 'string' }, allApps: { type: 'boolean' }, strictElements: { type: 'boolean' } } } },
15
15
  { name: 'leerness_whats_new', requiredTier: 'read-only', description: 'CHANGELOG 차분 자동 추출 (from → to 사이 신규 명령/플래그/파일). 1.36.69: 기본 최신 30개 버전만 반환(응답 폭주 방지) — 응답의 totalVersions/shown/truncated 로 절단 여부 확인, 더 필요하면 limit(개수) 또는 all=true(전체, 대용량 주의) 지정.', inputSchema: { type: 'object', properties: { from: { type: 'string' }, to: { type: 'string' }, limit: { type: 'number' }, all: { type: 'boolean' } } } },
16
16
  { name: 'leerness_usage_stats', requiredTier: 'read-only', description: 'leerness 명령별 누적 호출 통계 + drift 통계', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
17
- { name: 'leerness_session_close', requiredTier: 'read-only', description: '1.9.103 — 세션 마감 JSON (handoff/current-state/task-log 갱신 + taskCounts + rules + skillCandidates + drift + topCommands + mcpStats). 외부 AI가 마감 통계 자동 회수', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
17
+ { name: 'leerness_session_close', requiredTier: 'safe-write', description: '1.9.103 — 세션 마감 JSON (handoff/current-state/task-log 갱신 + taskCounts + rules + skillCandidates + drift + topCommands + mcpStats). 외부 AI가 마감 통계 자동 회수', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
18
18
  { name: 'leerness_skill_suggest', requiredTier: 'read-only', description: '1.9.53 — 사용 패턴 자동 분석 → 새 skill 후보 제안 (Hermes-style 자동 학습)', inputSchema: { type: 'object', properties: { path: { type: 'string' }, min: { type: 'number' }, days: { type: 'number' } } } },
19
19
  { name: 'leerness_lessons', requiredTier: 'read-only', description: '1.9.7/54 — 과거 결정·실수 자동 회수 (--auto: 현재 task 키워드 자동 추출)', inputSchema: { type: 'object', properties: { path: { type: 'string' }, query: { type: 'string' }, auto: { type: 'boolean' }, limit: { type: 'number' } } } },
20
- { name: 'leerness_task_export', requiredTier: 'read-only', description: '1.9.60/66 — leerness task → Claude Code TodoWrite 호환 JSON (외부 AI 양방향 sync)', inputSchema: { type: 'object', properties: { path: { type: 'string' }, to: { type: 'string' } } } },
20
+ { name: 'leerness_task_export', requiredTier: 'project-write', description: '1.9.60/66 — leerness task → Claude Code TodoWrite 호환 JSON (외부 AI 양방향 sync)', inputSchema: { type: 'object', properties: { path: { type: 'string' }, to: { type: 'string' } } } },
21
21
  { name: 'leerness_env_check', requiredTier: 'read-only', description: '1.9.71/73 — .env vs .env.example 동기화 검사 (보안: 키만, 값 미노출). exit 1 if 누락 키 있음', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
22
22
  { name: 'leerness_brainstorm', requiredTier: 'read-only', description: '1.9.16/72/77 — 누적 컨텍스트(decisions+skills+tasks+rules+evidence+lessons+skillHistory+taskLogFails) 자원 회수. 외부 AI가 새 작업 시작 전 호출', inputSchema: { type: 'object', properties: { topic: { type: 'string' }, path: { type: 'string' }, allApps: { type: 'boolean' } }, required: ['topic'] } },
23
- { name: 'leerness_skill_match', requiredTier: 'read-only', description: '1.9.45/50/83 — 사용자 task 키워드에 매칭되는 설치된 skill 추천 (jaccard 또는 embedding). 1.9.68 rolling history 자동 누적', inputSchema: { type: 'object', properties: { query: { type: 'string' }, path: { type: 'string' }, useEmbedding: { type: 'boolean' } }, required: ['query'] } },
23
+ { name: 'leerness_skill_match', requiredTier: 'safe-write', description: '1.9.45/50/83 — 사용자 task 키워드에 매칭되는 설치된 skill 추천 (jaccard 또는 embedding). 1.9.68 rolling history 자동 누적', inputSchema: { type: 'object', properties: { query: { type: 'string' }, path: { type: 'string' }, useEmbedding: { type: 'boolean' } }, required: ['query'] } },
24
24
  { name: 'leerness_skill_list', requiredTier: 'read-only', description: '1.9.84 — 워크스페이스에 설치된 skill 목록 + 사용 횟수 + 출처 (catalog/user). 외부 AI가 사용 가능한 skill 조회', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
25
25
  { name: 'leerness_health', requiredTier: 'read-only', description: '1.9.85/86 — 종합 헬스 체크 (drift + 보안 + skills + MCP + tasks + issues 배열). 외부 AI가 워크스페이스 상태 한 번에 확인', inputSchema: { type: 'object', properties: { path: { type: 'string' }, strict: { type: 'boolean' } } } },
26
26
  { name: 'leerness_skill_search', requiredTier: 'read-only', description: '1.9.90/91 — capability 배열에서 부분 일치하는 skill 검색 (substring + case-insensitive). skill match와 다른 정확 매칭', inputSchema: { type: 'object', properties: { capability: { type: 'string' }, path: { type: 'string' } }, required: ['capability'] } },
@@ -57,11 +57,11 @@ module.exports = [
57
57
  { name: 'leerness_provider_remove', requiredTier: 'safe-write', description: '1.9.159 — Provider Registry 에서 사용자 정의 provider 제거. 인자: { id (required), path? }. 빌트인 5종 id 는 제거 불가 (override 만 제거 가능). 🎉 MCP 50 도구 마일스톤 — Provider Registry CRUD MCP 완성 (list/add/remove)', inputSchema: { type: 'object', properties: { id: { type: 'string' }, path: { type: 'string' } }, required: ['id'] } },
58
58
  { name: 'leerness_web', requiredTier: 'network', description: '1.9.168 — Web Bridge (1.9.165 playwright opt-in). sub: check (설치 + permissions.browser 확인) | screenshot (URL → PNG) | extract (URL + CSS selector → DOM 텍스트). 외부 AI가 leerness 의 웹 자동화 능력을 직접 호출. playwright 미설치 시 친절 안내 (graceful). 인자: { sub (required), url?, out?, selector?, path? }', inputSchema: { type: 'object', properties: { sub: { type: 'string', enum: ['check', 'screenshot', 'extract'] }, url: { type: 'string' }, out: { type: 'string' }, selector: { type: 'string' }, path: { type: 'string' } }, required: ['sub'] } },
59
59
  { name: 'leerness_pc', requiredTier: 'shell-write', description: '1.9.168 — PC Bridge (1.9.166 robotjs/nut-tree opt-in). sub: check (설치 + permissions.mouse/keyboard) | click (x,y) | type (text) | screenshot (out). ⚠ full permissions 권장 (mouse/keyboard 접근). 외부 AI가 데스크탑 자동화 능력을 직접 호출. 인자: { sub (required), x?, y?, text?, out?, path? }', inputSchema: { type: 'object', properties: { sub: { type: 'string', enum: ['check', 'click', 'type', 'screenshot'] }, x: { type: 'number' }, y: { type: 'number' }, text: { type: 'string' }, out: { type: 'string' }, path: { type: 'string' } }, required: ['sub'] } },
60
- { name: 'leerness_lsp', requiredTier: 'read-only', description: '1.9.168 — LSP Bridge (1.9.167 typescript opt-in + regex fallback). sub: check (설치 여부) | symbols (file → function/class/interface/type/enum 목록) | references (name + in 디렉토리 → 호출 위치). 외부 AI가 코드 인텔리전스를 직접 호출 (의존성 0 fallback 동작). 🎉 MCP 53 도구 마일스톤. 인자: { sub (required), file?, name?, in?, path? }', inputSchema: { type: 'object', properties: { sub: { type: 'string', enum: ['check', 'symbols', 'references'] }, file: { type: 'string' }, name: { type: 'string' }, in: { type: 'string' }, path: { type: 'string' } }, required: ['sub'] } },
61
- { name: 'leerness_review_request', requiredTier: 'read-only', description: '1.9.176 — 사용자 요청 사전 검토 (사용자 명시 요청). AI 에이전트가 사용자 요구를 **무조건 구현 전**에 호출. 분석: 1) estimatedType (route 추정), 2) conflicts (lesson 실패/진행중 task), 3) reuseCandidates (skill/reuse-map 매칭), 4) lessonsRecall (과거 결정), 5) planConflicts (진행중 milestone), 6) featureConflicts (feature graph 영역 겹침), 7) recommendedSteps (작업 유형별 3-5 단계), 8) efficiencyHints, 9) proceed (true/false). 사용자 결정 도움. 인자: { request (required), path? }', inputSchema: { type: 'object', properties: { request: { type: 'string' }, path: { type: 'string' } }, required: ['request'] } },
60
+ { name: 'leerness_lsp', requiredTier: 'safe-write', description: '1.9.168 — LSP Bridge (1.9.167 typescript opt-in + regex fallback). sub: check (설치 여부) | symbols (file → function/class/interface/type/enum 목록) | references (name + in 디렉토리 → 호출 위치). 외부 AI가 코드 인텔리전스를 직접 호출 (의존성 0 fallback 동작). 🎉 MCP 53 도구 마일스톤. 인자: { sub (required), file?, name?, in?, path? }', inputSchema: { type: 'object', properties: { sub: { type: 'string', enum: ['check', 'symbols', 'references'] }, file: { type: 'string' }, name: { type: 'string' }, in: { type: 'string' }, path: { type: 'string' } }, required: ['sub'] } },
61
+ { name: 'leerness_review_request', requiredTier: 'safe-write', description: '1.9.176 — 사용자 요청 사전 검토 (사용자 명시 요청). AI 에이전트가 사용자 요구를 **무조건 구현 전**에 호출. 분석: 1) estimatedType (route 추정), 2) conflicts (lesson 실패/진행중 task), 3) reuseCandidates (skill/reuse-map 매칭), 4) lessonsRecall (과거 결정), 5) planConflicts (진행중 milestone), 6) featureConflicts (feature graph 영역 겹침), 7) recommendedSteps (작업 유형별 3-5 단계), 8) efficiencyHints, 9) proceed (true/false). 사용자 결정 도움. 인자: { request (required), path? }', inputSchema: { type: 'object', properties: { request: { type: 'string' }, path: { type: 'string' } }, required: ['request'] } },
62
62
  { name: 'leerness_requests_audit', requiredTier: 'read-only', description: '1.9.216 (1.9.207 사용자 명시) — 사용자 명시 요청 누락 확인 절차. .harness/user-requests.json 의 open/in-progress 요청을 task-log/plan/decisions 와 매칭 → missing/tracked/stale 분류. 외부 AI가 "사용자가 했던 요청 중 누락된 게 있나?"를 직접 회수. 응답: { total, open, missing[], tracked[], stale[], completed, dropped }. 인자: { path? }', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
63
63
  { name: 'leerness_constraints_check', requiredTier: 'read-only', description: '1.9.216 (1.9.208 사용자 명시) — 플랫폼/API 제약 사전 체크. 사용자 요청 텍스트에서 플랫폼 alias 매칭 (stripe/openai/anthropic/github/discord/twitter 6종 + 사용자 정의) → 각 플랫폼의 rate-limit / idempotency / auth / cost 제약 노출. 외부 AI가 "이 기능 구현 전 어떤 규정을 봐야 하나?"를 직접 회수. 인자: { request (required), path? }', inputSchema: { type: 'object', properties: { request: { type: 'string' }, path: { type: 'string' } }, required: ['request'] } },
64
- { name: 'leerness_pre_wake_audit', requiredTier: 'read-only', description: '1.9.216 (1.9.209 사용자 명시) — sleep 전 sub-agent audit. 6 영역 점검: missing-user-requests / stale-in-progress / drift-handoff-stale / wakeup-missed / next-action-pending / auto-resume-plan. 외부 AI가 "깨어나기 전 점검할 부분"을 회수. 응답: { auditedAt, findings: {critical, warning, info}, summary }. 인자: { path? }', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
64
+ { name: 'leerness_pre_wake_audit', requiredTier: 'safe-write', description: '1.9.216 (1.9.209 사용자 명시) — sleep 전 sub-agent audit. 6 영역 점검: missing-user-requests / stale-in-progress / drift-handoff-stale / wakeup-missed / next-action-pending / auto-resume-plan. 외부 AI가 "깨어나기 전 점검할 부분"을 회수. 응답: { auditedAt, findings: {critical, warning, info}, summary }. 인자: { path? }', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
65
65
  { name: 'leerness_intent_classify', requiredTier: 'read-only', description: '1.9.216 (1.9.213 사용자 명시) — 사용자 의도 파악 + scope expansion 게이트. 응답: { intent: precise|broad|default, signals, domain, explicitMentions, expansionCandidates, mode: dry-run }. 3원칙 안전: (1) Always-Off Opt-In, (2) Dry-run 기본 (실행 X), (3) 명시 vs 추론 분리 라벨링. 5 도메인 (game/web/api/cli/data). 외부 AI가 "이 요청은 정확히 그것만 / 포괄적 / 기본인가?"를 회수. 인자: { request (required), path? }', inputSchema: { type: 'object', properties: { request: { type: 'string' }, path: { type: 'string' } }, required: ['request'] } },
66
66
  { name: 'leerness_idempotency_audit', requiredTier: 'read-only', description: '1.9.216 (1.9.212 사용자 명시) — 멱등성 위반 탐지. 4영역 점검: rule-duplicate (medium) / task-duplicate-request (medium) / user-request-duplicate (low) / wakeup-duplicate (high). 응답: { violations[], verified[], summary: {totalViolations, high/medium/low, overall} }. 외부 AI가 "워크스페이스에 중복/충돌이 있나?"를 회수. 🎉 MCP 58 도구 마일스톤 (50→58). 인자: { path? }', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
67
67
  { name: 'leerness_session_resume', requiredTier: 'read-only', description: '1.9.221 (1.9.220 사용자 명시) — 비정상 종료 감지 + 자율 재개. 5신호 분석: last-handoff-stale / wakeup-missed / in-progress-stale / auto-resume-plan-unused / release-branch-pending. 응답: { abnormalShutdown, severity (none/low/medium/high), signals[], resumeGuide[] }. 외부 AI가 "절전/시스템종료/세션종료 후 leerness 상태가 정상인가? 어떻게 재개?"를 회수. 🎉 MCP 60 도구 마일스톤 (53→60, +7 in 1.9.168/216/221). 인자: { path? }', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
@@ -79,7 +79,7 @@ module.exports = [
79
79
  { name: 'leerness_api_skill', requiredTier: 'safe-write', description: '1.9.245 (사용자 명시 UR-0015) — API 문서·관련링크 자동 캐시. 공식 API 문서 URL을 fetch 하고 1단계 same-domain 관련 링크까지 정리 → .harness/api-skills/<id>.md 저장. 후속 같은 API 관련 작업 시 자동 참조. 응답: list (skills 배열) / show (전체 본문) / match (task 매칭 결과). 인자: { path?, sub ("list"|"show"|"match"|"add"|"drop"), url? (add), id? (show/drop), query? (match), direction? (add: 구현 방향 텍스트) }. 외부 AI가 "이 프로젝트 어떤 API 문서가 정리되어 있나?" / "내 작업과 매칭되는 API skill 있나?" 회수.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, sub: { type: 'string', enum: ['list', 'show', 'match', 'add', 'drop'] }, url: { type: 'string' }, id: { type: 'string' }, query: { type: 'string' }, direction: { type: 'string' } }, required: ['sub'] } },
80
80
  { name: 'leerness_selftest', requiredTier: 'read-only', description: '1.9.258/259 — 설치된 leerness 바이너리의 코어 함수(보안 _isSecretKey / 버전 compareVer / 인코딩 _classifyCJK 등) 무결성 자가 검증. 응답: { version, total, pass, fail, ok, results[] }. 외부 AI/CI 가 "이 leerness 설치가 정상인가?(npx 캐시 손상·부분 설치 감지)" 를 1초 내 확인. 인자: 없음.', inputSchema: { type: 'object', properties: {} } },
81
81
  { name: 'leerness_shell_guard', requiredTier: 'read-only', description: '1.9.260/261 (사용자 명시 UR-0020) — 터미널 명령 셸 호환성 린터. 외부 AI 가 명령을 실행하기 전에 셸 호환성 문제를 사전 점검. 예: Windows PowerShell 5.1 은 && / || 미지원 → A; if ($?) { B } 제안. 6 규칙(ps5-chain/ps-devnull/ps-inline-env/ps-rm-rf/cmd-semicolon/ps-version-unknown) + 과거 실패 회수(.harness/shell-failures.json). 응답: { shell, psVersion, issues[{rule,severity,detail,suggestion}], pastSame, pastSimilar, ok }. 인자: { command (required), path? }. 현재 셸/PS 버전 자동 감지.', inputSchema: { type: 'object', properties: { command: { type: 'string' }, path: { type: 'string' } }, required: ['command'] } },
82
- { name: 'leerness_slash_commands', requiredTier: 'read-only', description: '1.9.265/266 (사용자 명시 UR-0021) — CLI AI 에이전트별 슬래시 명령어 레지스트리. 외부 AI(메인)가 sub-agent(codex/agy/claude/grok/copilot)를 호출할 때 각 에이전트에 알맞는 슬래시 명령을 참조. 빌트인 + 사용자 .harness/agent-slash-commands.json override 병합. 응답: { agents: { <id>: { label, asOf, invoke(slash|subcommand), note, source, count, commands[{cmd,desc}] } } }. 인자: { path?, agent? (생략 시 전체), refresh? (1.9.267 — 설치된 CLI --help probe 자동 갱신), dryRun? }. agent 지정 시 단일.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, agent: { type: 'string' }, refresh: { type: 'boolean' }, dryRun: { type: 'boolean' } } } },
82
+ { name: 'leerness_slash_commands', requiredTier: 'safe-write', description: '1.9.265/266 (사용자 명시 UR-0021) — CLI AI 에이전트별 슬래시 명령어 레지스트리. 외부 AI(메인)가 sub-agent(codex/agy/claude/grok/copilot)를 호출할 때 각 에이전트에 알맞는 슬래시 명령을 참조. 빌트인 + 사용자 .harness/agent-slash-commands.json override 병합. 응답: { agents: { <id>: { label, asOf, invoke(slash|subcommand), note, source, count, commands[{cmd,desc}] } } }. 인자: { path?, agent? (생략 시 전체), refresh? (1.9.267 — 설치된 CLI --help probe 자동 갱신), dryRun? }. agent 지정 시 단일.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, agent: { type: 'string' }, refresh: { type: 'boolean' }, dryRun: { type: 'boolean' } } } },
83
83
  { name: 'leerness_roles', requiredTier: 'safe-write', description: '1.9.270 (사용자 명시) — 모델별 역할 부여. 여러 AI 에이전트 활성 시 역할(commander/reviewer/coder/architect/designer/debugger/dispatcher)을 provider+model 에 매핑하고, agents dispatch --role 로 라우팅. sub=suggest 면 활성 에이전트 기반 최적 배치 + 근거 반환(방향성 판단). sub=set 시 role/provider/model 필요. 응답: list/suggest/verify 별 JSON. 인자: { path?, sub(list|set|unset|catalog|suggest|verify), role?, provider?, model?, apply? }.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, sub: { type: 'string' }, role: { type: 'string' }, provider: { type: 'string' }, model: { type: 'string' }, apply: { type: 'boolean' } } } },
84
84
  // 1.9.279 (UR-0031, GPT-5.5 범용 하네스): 상태 substrate(.leerness/, 1.9.278)를 MCP 시맨틱 verb 로 노출 — 모든 에이전트 공통 호출 표면.
85
85
  { name: 'leerness_state_show', requiredTier: 'read-only', description: '1.9.279 (UR-0031) — get_project_context / get_current_task. .leerness/ 구조화 상태(현재 run + 누적)를 회수. 외부 에이전트가 작업 시작 전 "지금 무슨 작업이 진행 중인가 / 무엇을 읽고 무엇이 변경됐나"를 JSON 으로 파악. 응답: { state, currentRun }. 인자: { path? }.', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.36.74",
3
+ "version": "1.36.76",
4
4
  "description": "The AI-coding operations layer that makes \"done\" require evidence — persistent memory, evidence-gated completion checks, and clean handoffs for any AI agent (Claude Code, Codex, Cursor). State lives as plain files in your repo. CLI + MCP, 0 runtime dependencies.",
5
5
  "keywords": [
6
6
  "leerness",
package/scripts/e2e.js CHANGED
@@ -3378,9 +3378,12 @@ total++;
3378
3378
  // provider_add: regex 는 read-only(아래 정책 통과 가능) 인데 메타데이터 safe-write → read-only enforce 에서 차단되어야
3379
3379
  const pa = callMcp('leerness_provider_add', { path: pDir, id: 'x', cmd: 'y' });
3380
3380
  const blocked = pa && pa.isError === true && /정책 차단/.test(pa.content[0].text);
3381
- // handoff: read-only 허용
3381
+ // 1.36.76 (검수 P1): handoff 는 last-handoff/tech-profile 을 쓰므로 safe-write 로 재분류 — read-only enforce 에서 차단되어야
3382
3382
  const hd = callMcp('leerness_handoff', { path: pDir });
3383
- const allowed = hd && hd.isError !== true;
3383
+ const handoffBlocked = hd && hd.isError === true && /정책 차단/.test(hd.content[0].text);
3384
+ // 진짜 read-only 도구(drift_check)는 허용
3385
+ const dc = callMcp('leerness_drift_check', { path: pDir });
3386
+ const allowed = handoffBlocked && dc && dc.isError !== true;
3384
3387
  // 모든 도구 유효 tier
3385
3388
  const T = require(path.resolve(__dirname, '..', 'lib', 'mcp-tools.js'));
3386
3389
  const tierOk = T.every(t => typeof t.requiredTier === 'string' && t.requiredTier.length > 0);
@@ -7412,5 +7415,101 @@ total++;
7412
7415
  if (!ok) failed++;
7413
7416
  }
7414
7417
 
7418
+ // 1.36.75 (UR-0066): 디자인 시안 워크플로 — preview mockup 스캐폴드 + 덮어쓰기 보호 + XSS 이스케이프 + 디자인 감지 + --mockup 첨부
7419
+ total++;
7420
+ {
7421
+ let ok = false;
7422
+ const _d = [];
7423
+ try {
7424
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-ur66-')); _d.push(d);
7425
+ cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
7426
+ const P = (a) => cp.spawnSync(process.execPath, [CLI, 'preview', ...a, '--path', d], { encoding: 'utf8', timeout: 20000 });
7427
+ // 디자인 작업 감지 힌트
7428
+ const add = P(['add', '신규 랜딩 페이지 제작', '--design', '히어로+3단', '--features', '히어로,FAQ']);
7429
+ const detectOk = add.status === 0 && /preview mockup P-0001/.test(add.stdout);
7430
+ // 스캐폴드 생성 + 스토어 기록
7431
+ const mk = P(['mockup', 'P-0001']);
7432
+ const mkFile = path.join(d, '.harness', 'previews', 'P-0001-mockup.html');
7433
+ const html = fs.readFileSync(mkFile, 'utf8');
7434
+ const mkOk = mk.status === 0 && html.includes('신규 랜딩 페이지 제작') && html.includes('leerness:mockup P-0001');
7435
+ let showOk = false;
7436
+ try { showOk = JSON.parse(P(['show', 'P-0001', '--json']).stdout).mockupPath === '.harness/previews/P-0001-mockup.html'; } catch {}
7437
+ // 재실행은 AI 가 채운 시안을 보호 (--force 로만 재생성)
7438
+ fs.appendFileSync(mkFile, '<!-- AI-FILLED -->');
7439
+ const protectOk = P(['mockup', 'P-0001']).status === 0 && fs.readFileSync(mkFile, 'utf8').includes('AI-FILLED')
7440
+ && !(P(['mockup', 'P-0001', '--force']).status !== 0) && !fs.readFileSync(mkFile, 'utf8').includes('AI-FILLED');
7441
+ // XSS: 제목의 스크립트가 시안에 이스케이프
7442
+ P(['add', '<script>alert(1)</script> 페이지', '--design', 'x']);
7443
+ P(['mockup', 'P-0002']);
7444
+ const xssOk = !fs.readFileSync(path.join(d, '.harness', 'previews', 'P-0002-mockup.html'), 'utf8').includes('<script>alert');
7445
+ // --mockup 첨부 (존재 검증 포함)
7446
+ fs.writeFileSync(path.join(d, 'my-mockup.html'), '<h1>시안</h1>');
7447
+ let attachOk = false;
7448
+ // (검수 #2) --mockup 값이 제목에 흡수되지 않아야
7449
+ try { const j = JSON.parse(P(['add', '첨부형 페이지', '--mockup', 'my-mockup.html', '--json']).stdout); attachOk = j.mockupPath === 'my-mockup.html' && j.title === '첨부형 페이지'; } catch {}
7450
+ const missOk = P(['add', '없는 첨부', '--mockup', 'no-such.html']).status === 1;
7451
+ // (검수 #3) 디렉토리·루트 밖 첨부 거부
7452
+ fs.mkdirSync(path.join(d, 'adir'), { recursive: true });
7453
+ const dirOk = P(['add', 'd-dir', '--mockup', 'adir']).status === 1;
7454
+ const outFile = path.join(path.dirname(d), `ur66-out-${path.basename(d)}.html`);
7455
+ fs.writeFileSync(outFile, '<h1>o</h1>'); _d.push(outFile.replace(/\.html$/, '')); // rm 은 파일에도 동작(force)
7456
+ const outOk = P(['add', 'd-out', '--mockup', outFile]).status === 1;
7457
+ try { fs.rmSync(outFile, { force: true }); } catch {}
7458
+ // (검수 High) 조작된 id 는 previews 디렉토리 밖 쓰기 불가
7459
+ const evil = path.join(d, '.harness', 'previews.json');
7460
+ const saved = fs.readFileSync(evil, 'utf8');
7461
+ fs.writeFileSync(evil, JSON.stringify([{ id: '..\\..\\owned', title: 'x', status: 'proposed' }]));
7462
+ const evilOk = cp.spawnSync(process.execPath, [CLI, 'preview', 'mockup', '..\\..\\owned', '--path', d], { encoding: 'utf8', timeout: 20000 }).status === 1
7463
+ && !fs.existsSync(path.join(d, '..', 'owned-mockup.html'));
7464
+ fs.writeFileSync(evil, saved);
7465
+ // (검수 #5) 승인 후 mockup 거부
7466
+ P(['approve', 'P-0001']);
7467
+ const apprOk = P(['mockup', 'P-0001', '--force']).status === 1;
7468
+ ok = detectOk && mkOk && showOk && protectOk && xssOk && attachOk && missOk && dirOk && outOk && evilOk && apprOk;
7469
+ if (!ok) console.log(` [ur66 디버그] detect=${detectOk} mk=${mkOk} show=${showOk} protect=${protectOk} xss=${xssOk} attach=${attachOk} miss=${missOk} dir=${dirOk} out=${outOk} evil=${evilOk} appr=${apprOk}`);
7470
+ } catch (e) {} finally { _d.forEach(x => { try { fs.rmSync(x, { recursive: true, force: true }); } catch {} }); }
7471
+ console.log(ok ? '✓ B(1.36.75) UR-0066: preview mockup 시안(스캐폴드·보호·XSS·감지힌트·--mockup 첨부)' : '✗ UR-0066 시안 워크플로 실패');
7472
+ if (!ok) failed++;
7473
+ }
7474
+
7475
+ // 1.36.76 (9차 헌트 이월 3건 종결): 손상 아카이브 복원 거부 · MCP read-only 무변형 · 긴 파생 id 안전화
7476
+ total++;
7477
+ {
7478
+ let ok = false;
7479
+ const _d = [];
7480
+ try {
7481
+ // #3 U+FFFD 아카이브 → 복원 거부 + 아카이브 무변경
7482
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-h9x3-')); _d.push(d);
7483
+ cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--minimal', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
7484
+ const ap = path.join(d, '.harness', 'decisions.archive.md');
7485
+ const bytes = Buffer.from('# Decisions archive\n\n## 제거 2026-07-25 (target: "Trunc")\n\n### 2026-07-25 — Trunc\n- Decision: 데이터', 'utf8');
7486
+ fs.writeFileSync(ap, bytes.slice(0, bytes.length - 1));
7487
+ const before = fs.readFileSync(ap);
7488
+ const rr = cp.spawnSync(process.execPath, [CLI, 'memory', 'restore', 'decisions', 'Trunc', '--path', d], { encoding: 'utf8', timeout: 20000 });
7489
+ const truncOk = rr.status === 1 && before.equals(fs.readFileSync(ap));
7490
+ // #2a 없는 대상에 read-only MCP 호출 → 디렉토리·텔레메트리 미생성 / 초기화된 하네스엔 기록 유지
7491
+ const ghost = path.join(d, 'no-such-target');
7492
+ cp.spawnSync(process.execPath, [CLI, 'mcp', 'serve'], { encoding: 'utf8', timeout: 25000, input: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'leerness_state_show', arguments: { path: ghost } } }) + '\n' });
7493
+ const ghostOk = !fs.existsSync(ghost);
7494
+ cp.spawnSync(process.execPath, [CLI, 'mcp', 'serve'], { encoding: 'utf8', timeout: 25000, input: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'leerness_pulse', arguments: { path: d } } }) + '\n' });
7495
+ const statsOk = fs.existsSync(path.join(d, '.harness', 'cache', 'usage-stats.json'));
7496
+ // #2b MCP env_detect 는 environment.json 을 쓰지 않음 / CLI 직접 호출은 persist
7497
+ fs.rmSync(path.join(d, '.harness', 'environment.json'), { force: true });
7498
+ const envResp = cp.spawnSync(process.execPath, [CLI, 'mcp', 'serve'], { encoding: 'utf8', timeout: 25000, input: JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: 'leerness_env_detect', arguments: { path: d } } }) + '\n' });
7499
+ const noWriteOk = /snapshot/.test(envResp.stdout) && !fs.existsSync(path.join(d, '.harness', 'environment.json'));
7500
+ cp.spawnSync(process.execPath, [CLI, 'env', 'detect', d, '--json'], { encoding: 'utf8', timeout: 20000 });
7501
+ const cliPersistOk = fs.existsSync(path.join(d, '.harness', 'environment.json'));
7502
+ // #8 긴/이상 URL 파생 id — raw ENOENT 없이 저장, 정상 id 는 종전과 동일
7503
+ const A = (u) => cp.spawnSync(process.execPath, [CLI, 'api-skill', 'add', u, '--skeleton', '--no-crawl', '--path', d, '--json'], { encoding: 'utf8', timeout: 25000 });
7504
+ let longOk = false, normOk = false;
7505
+ try { const j = JSON.parse(A('ftp://' + 'a'.repeat(300) + '.test/docs').stdout); longOk = j.ok === true && j.id.length <= 80; } catch {}
7506
+ try { normOk = JSON.parse(A('https://developers.coupangcorp.com/articles/360033877853').stdout).id === 'developers-coupangcorp-articles-360033877853'; } catch {}
7507
+ ok = truncOk && ghostOk && statsOk && noWriteOk && cliPersistOk && longOk && normOk;
7508
+ if (!ok) console.log(` [h9x 디버그] trunc=${truncOk} ghost=${ghostOk} stats=${statsOk} noWrite=${noWriteOk} cli=${cliPersistOk} long=${longOk} norm=${normOk}`);
7509
+ } catch (e) {} finally { _d.forEach(x => { try { fs.rmSync(x, { recursive: true, force: true }); } catch {} }); }
7510
+ console.log(ok ? '✓ B(1.36.76) 9차 이월 종결: U+FFFD 아카이브 복원거부 · MCP read-only 무변형(대상/텔레메트리/env) · 긴 id 안전화' : '✗ 9차 이월 수정 실패');
7511
+ if (!ok) failed++;
7512
+ }
7513
+
7415
7514
  console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
7416
7515
  if (failed > 0) process.exit(1);