leerness 1.36.74 → 1.36.75

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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.36.75 — 2026-07-25 — UR-0066: 디자인 시안 우선 워크플로 (preview mockup) — R-0001 검수 18회전
4
+
5
+ 사용자 요청: 웹페이지/디자인 작업은 코드 구현 전에 디자인 시안을 먼저 제시하고 수정/승인을 질문으로 받도록.
6
+
7
+ - **`leerness preview mockup <P-ID> [--force]`**: 자립형(오프라인·외부 리소스 0) HTML 시안 스캐폴드를 `.harness/previews/<P-ID>-mockup.html` 에 생성 — AI 가 placeholder 를 실제 레이아웃 초안으로 교체해 사용자에게 브라우저로 제시. 기존 파일은 덮어쓰지 않음(--force 만 재생성, 이력은 mockup-regenerated 로 구분).
8
+ - **`preview add ... --mockup <파일>`**: 이미 만든 시안 첨부(일반 파일 + 프로젝트 루트 안 검증). **디자인/페이지 작업 자동 감지** 시 시안 생성 안내 출력(false-PASS 편향 — 일반 기능 요청엔 강제 안 함).
9
+ - **AGENTS.md(ko/en) 계약 강화**: 신규 페이지·디자인 요청 → 시안 제시·승인 전 실제 코드 작성 금지.
10
+ - **검수 7건 반영**: (High) 조작된 preview id 의 previews 디렉토리 밖 쓰기 차단(P-\d{4,} 강제 + 경로 격리 이중 가드) · (#2) `--mockup` 값이 제목에 흡수 · (#3) 디렉토리/루트 밖 첨부 거부 · (#4) 보호된 기존 시안도 경로는 스토어에 기록 · (#5) **승인 후 시안 생성/재생성 거부**(승인이 낡는 것 방지 — revise 로 되돌린 뒤에만) · (Low×2) 재생성 이력 구분·help 갱신.
11
+ - 검증: XSS 이스케이프·덮어쓰기 보호·감지 힌트·후방 호환(구 항목 mockupPath 없음) 실측, selftest 334, e2e +1(11 단언).
12
+
3
13
  ## 1.36.74 — 2026-07-25 — codex 9차 홀리스틱 헌트: 미점검 표면 6건 수정 (P1 3·P2 3)
4
14
 
5
15
  표면 회전 원칙에 따라 최근 라운드가 비추지 않은 사용자 데이터 표면(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.75 하네스를 사용합니다. 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.75는 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.75 릴리스 태그 이력** (수백 라운드) · _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.75: 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.75';
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');
@@ -6739,7 +6740,7 @@ function commandsCmd(root) {
6739
6740
  { cmd: 'clarify "<사용자 요청>" [--json]', desc: '요청 모호성 신호 감지 → 사용자에게 물을 질문 생성 (추측 구현 방지) — 1.36.51 UR-0061' },
6740
6741
  { cmd: 'tech [--json]', desc: '기술 프로필 — 개발 언어·연결 서비스 자동 감지 + 마이그레이션/언어전환 이력, 그래프 🛠 탭 표시 — 1.36.53 UR-0062' },
6741
6742
  { 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' },
6743
+ { 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
6744
  { cmd: 'review <file> --persona <ids>', desc: '페르소나 리뷰 (1.9.29)' },
6744
6745
  { cmd: 'brainstorm "<topic>" [--include-code]', desc: '워크스페이스 회수 + 코드 grep' }
6745
6746
  ],
@@ -23262,7 +23263,7 @@ async function main() {
23262
23263
  // 미지 플래그(--design/--features/--note)의 값이 positional 로 새는 것 차단 — 원시 argv 에서 플래그+값 스킵 (1.36.49 release note 패턴)
23263
23264
  const _pvRaw = process.argv.slice(2); const _pvI = _pvRaw.indexOf('preview'); const _pvToks = [];
23264
23265
  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; }
23266
+ if (_pvRaw[i].startsWith('--')) { if (['--design', '--features', '--note', '--path', '--mockup'].includes(_pvRaw[i]) && _pvRaw[i + 1]) i++; continue; } // 1.36.75 (검수 #2): --mockup 값이 제목에 흡수되던 것
23266
23267
  _pvToks.push(_pvRaw[i]);
23267
23268
  }
23268
23269
  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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leerness",
3
- "version": "1.36.74",
3
+ "version": "1.36.75",
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
@@ -7412,5 +7412,62 @@ total++;
7412
7412
  if (!ok) failed++;
7413
7413
  }
7414
7414
 
7415
+ // 1.36.75 (UR-0066): 디자인 시안 워크플로 — preview mockup 스캐폴드 + 덮어쓰기 보호 + XSS 이스케이프 + 디자인 감지 + --mockup 첨부
7416
+ total++;
7417
+ {
7418
+ let ok = false;
7419
+ const _d = [];
7420
+ try {
7421
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'leerness-ur66-')); _d.push(d);
7422
+ cp.spawnSync(process.execPath, [CLI, 'init', d, '--yes', '--no-env', '--no-stale-check'], { encoding: 'utf8', timeout: 40000 });
7423
+ const P = (a) => cp.spawnSync(process.execPath, [CLI, 'preview', ...a, '--path', d], { encoding: 'utf8', timeout: 20000 });
7424
+ // 디자인 작업 감지 힌트
7425
+ const add = P(['add', '신규 랜딩 페이지 제작', '--design', '히어로+3단', '--features', '히어로,FAQ']);
7426
+ const detectOk = add.status === 0 && /preview mockup P-0001/.test(add.stdout);
7427
+ // 스캐폴드 생성 + 스토어 기록
7428
+ const mk = P(['mockup', 'P-0001']);
7429
+ const mkFile = path.join(d, '.harness', 'previews', 'P-0001-mockup.html');
7430
+ const html = fs.readFileSync(mkFile, 'utf8');
7431
+ const mkOk = mk.status === 0 && html.includes('신규 랜딩 페이지 제작') && html.includes('leerness:mockup P-0001');
7432
+ let showOk = false;
7433
+ try { showOk = JSON.parse(P(['show', 'P-0001', '--json']).stdout).mockupPath === '.harness/previews/P-0001-mockup.html'; } catch {}
7434
+ // 재실행은 AI 가 채운 시안을 보호 (--force 로만 재생성)
7435
+ fs.appendFileSync(mkFile, '<!-- AI-FILLED -->');
7436
+ const protectOk = P(['mockup', 'P-0001']).status === 0 && fs.readFileSync(mkFile, 'utf8').includes('AI-FILLED')
7437
+ && !(P(['mockup', 'P-0001', '--force']).status !== 0) && !fs.readFileSync(mkFile, 'utf8').includes('AI-FILLED');
7438
+ // XSS: 제목의 스크립트가 시안에 이스케이프
7439
+ P(['add', '<script>alert(1)</script> 페이지', '--design', 'x']);
7440
+ P(['mockup', 'P-0002']);
7441
+ const xssOk = !fs.readFileSync(path.join(d, '.harness', 'previews', 'P-0002-mockup.html'), 'utf8').includes('<script>alert');
7442
+ // --mockup 첨부 (존재 검증 포함)
7443
+ fs.writeFileSync(path.join(d, 'my-mockup.html'), '<h1>시안</h1>');
7444
+ let attachOk = false;
7445
+ // (검수 #2) --mockup 값이 제목에 흡수되지 않아야
7446
+ try { const j = JSON.parse(P(['add', '첨부형 페이지', '--mockup', 'my-mockup.html', '--json']).stdout); attachOk = j.mockupPath === 'my-mockup.html' && j.title === '첨부형 페이지'; } catch {}
7447
+ const missOk = P(['add', '없는 첨부', '--mockup', 'no-such.html']).status === 1;
7448
+ // (검수 #3) 디렉토리·루트 밖 첨부 거부
7449
+ fs.mkdirSync(path.join(d, 'adir'), { recursive: true });
7450
+ const dirOk = P(['add', 'd-dir', '--mockup', 'adir']).status === 1;
7451
+ const outFile = path.join(path.dirname(d), `ur66-out-${path.basename(d)}.html`);
7452
+ fs.writeFileSync(outFile, '<h1>o</h1>'); _d.push(outFile.replace(/\.html$/, '')); // rm 은 파일에도 동작(force)
7453
+ const outOk = P(['add', 'd-out', '--mockup', outFile]).status === 1;
7454
+ try { fs.rmSync(outFile, { force: true }); } catch {}
7455
+ // (검수 High) 조작된 id 는 previews 디렉토리 밖 쓰기 불가
7456
+ const evil = path.join(d, '.harness', 'previews.json');
7457
+ const saved = fs.readFileSync(evil, 'utf8');
7458
+ fs.writeFileSync(evil, JSON.stringify([{ id: '..\\..\\owned', title: 'x', status: 'proposed' }]));
7459
+ const evilOk = cp.spawnSync(process.execPath, [CLI, 'preview', 'mockup', '..\\..\\owned', '--path', d], { encoding: 'utf8', timeout: 20000 }).status === 1
7460
+ && !fs.existsSync(path.join(d, '..', 'owned-mockup.html'));
7461
+ fs.writeFileSync(evil, saved);
7462
+ // (검수 #5) 승인 후 mockup 거부
7463
+ P(['approve', 'P-0001']);
7464
+ const apprOk = P(['mockup', 'P-0001', '--force']).status === 1;
7465
+ ok = detectOk && mkOk && showOk && protectOk && xssOk && attachOk && missOk && dirOk && outOk && evilOk && apprOk;
7466
+ 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}`);
7467
+ } catch (e) {} finally { _d.forEach(x => { try { fs.rmSync(x, { recursive: true, force: true }); } catch {} }); }
7468
+ console.log(ok ? '✓ B(1.36.75) UR-0066: preview mockup 시안(스캐폴드·보호·XSS·감지힌트·--mockup 첨부)' : '✗ UR-0066 시안 워크플로 실패');
7469
+ if (!ok) failed++;
7470
+ }
7471
+
7415
7472
  console.log(`\nE2E result: ${total - failed}/${total} passed · ${((Date.now() - _e2eStart) / 1000).toFixed(0)}s`);
7416
7473
  if (failed > 0) process.exit(1);