oh-my-design-cli 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/AGENTS.md +1 -0
  2. package/README.ko.md +1 -1
  3. package/README.md +9 -9
  4. package/agents/omd-master.md +13 -1
  5. package/data/reference-fingerprints.json +1428 -523
  6. package/package.json +5 -4
  7. package/scripts/ctx-prime.cjs +266 -0
  8. package/skills/omd-harness/SKILL.md +135 -7
  9. package/skills/omd-kr-writer/SKILL.md +1 -1
  10. package/web/references/17live/DESIGN.md +424 -0
  11. package/web/references/alipay/DESIGN.md +456 -0
  12. package/web/references/appier/DESIGN.md +420 -0
  13. package/web/references/bilibili/DESIGN.md +426 -0
  14. package/web/references/class101/DESIGN.md +433 -0
  15. package/web/references/cookpad/DESIGN.md +357 -0
  16. package/web/references/dji/DESIGN.md +416 -0
  17. package/web/references/gogoro/DESIGN.md +403 -0
  18. package/web/references/ichef/DESIGN.md +411 -0
  19. package/web/references/kakaopay/DESIGN.md +1 -1
  20. package/web/references/kakaot/DESIGN.md +454 -0
  21. package/web/references/kkday/DESIGN.md +423 -0
  22. package/web/references/meituan/DESIGN.md +424 -0
  23. package/web/references/millie/DESIGN.md +533 -0
  24. package/web/references/money-forward/DESIGN.md +401 -0
  25. package/web/references/myrealtrip/DESIGN.md +445 -0
  26. package/web/references/naverwebtoon/DESIGN.md +429 -0
  27. package/web/references/note/DESIGN.md +318 -0
  28. package/web/references/publy/DESIGN.md +511 -0
  29. package/web/references/smarthr/DESIGN.md +404 -0
  30. package/web/references/smartnews/DESIGN.md +331 -0
  31. package/web/references/spoon/DESIGN.md +446 -0
  32. package/web/references/tada/DESIGN.md +528 -0
  33. package/web/references/tossbank/DESIGN.md +519 -0
  34. package/web/references/triple/DESIGN.md +434 -0
  35. package/web/references/tumblbug/DESIGN.md +530 -0
  36. package/web/references/watcha/DESIGN.md +425 -0
  37. package/web/references/wavve/DESIGN.md +438 -0
  38. package/web/references/wconcept/DESIGN.md +511 -0
  39. package/web/references/xiaohongshu/DESIGN.md +423 -0
  40. package/web/references/yogiyo/DESIGN.md +465 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "oh-my-design-cli",
3
- "version": "1.5.0",
4
- "description": "Bootstrap oh-my-design skills + agents into your project. After install, talk to your AI coding agent in natural language \u2014 no other CLI commands.",
3
+ "version": "1.6.0",
4
+ "description": "Bootstrap oh-my-design skills + agents into your project. After install, talk to your AI coding agent in natural language no other CLI commands.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "oh-my-design": "dist/bin/oh-my-design.js",
@@ -31,7 +31,8 @@
31
31
  ".claude/settings.json",
32
32
  "AGENTS.md",
33
33
  "scripts/postinstall.cjs",
34
- "scripts/context.cjs"
34
+ "scripts/context.cjs",
35
+ "scripts/ctx-prime.cjs"
35
36
  ],
36
37
  "scripts": {
37
38
  "build": "tsup",
@@ -73,4 +74,4 @@
73
74
  "typescript": "^5.8.2",
74
75
  "vitest": "^3.1.1"
75
76
  }
76
- }
77
+ }
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env node
2
+ // scripts/ctx-prime.cjs — richer, run-scoped repo prime for omd-harness 1.6+.
3
+ //
4
+ // Pure node, no deps. Sub-5s on typical repos (<5k files). Emits
5
+ // <RUN_DIR>/ctx-prime.json with:
6
+ // stack / brand_signal / surface_inventory / audience_hypothesis /
7
+ // wow_moment_candidates / scanned_at / scan_duration_ms
8
+ //
9
+ // Usage: node scripts/ctx-prime.cjs <cwd> <run_dir>
10
+ // Falls back to cwd/.omd if run_dir omitted (for ad-hoc use).
11
+
12
+ const fs = require('node:fs');
13
+ const path = require('node:path');
14
+
15
+ const startedAt = Date.now();
16
+ const cwd = path.resolve(process.argv[2] || process.cwd());
17
+ const runDir = process.argv[3]
18
+ ? path.resolve(process.argv[3])
19
+ : path.join(cwd, '.omd');
20
+ const outFile = path.join(runDir, 'ctx-prime.json');
21
+
22
+ const SKIP_DIRS = new Set([
23
+ 'node_modules', 'dist', 'build', '.next', '.nuxt', '.output',
24
+ 'coverage', '.git', '.cache', '.turbo', '.vercel', '.svelte-kit',
25
+ 'out', 'storybook-static', '__pycache__',
26
+ ]);
27
+
28
+ function safeRead(p) {
29
+ try { return fs.readFileSync(p, 'utf8'); } catch { return null; }
30
+ }
31
+
32
+ function walk(dir, opts) {
33
+ const { exts, max = 200, maxDepth = 6 } = opts;
34
+ const results = [];
35
+ (function rec(d, depth) {
36
+ if (depth > maxDepth || results.length >= max) return;
37
+ let entries;
38
+ try { entries = fs.readdirSync(d, { withFileTypes: true }); }
39
+ catch { return; }
40
+ for (const e of entries) {
41
+ if (e.name.startsWith('.') && e.name !== '.well-known') continue;
42
+ if (SKIP_DIRS.has(e.name)) continue;
43
+ const full = path.join(d, e.name);
44
+ if (e.isDirectory()) { rec(full, depth + 1); continue; }
45
+ if (!exts || exts.some((x) => e.name.endsWith(x))) {
46
+ try {
47
+ const stat = fs.statSync(full);
48
+ results.push({ path: path.relative(cwd, full), size: stat.size });
49
+ if (results.length >= max) return;
50
+ } catch {}
51
+ }
52
+ }
53
+ })(dir, 0);
54
+ return results;
55
+ }
56
+
57
+ // --- 1. stack ---
58
+ const pkgRaw = safeRead(path.join(cwd, 'package.json'));
59
+ const pkg = pkgRaw ? safeJson(pkgRaw) : null;
60
+ const deps = pkg ? Object.assign({}, pkg.dependencies, pkg.devDependencies) : {};
61
+
62
+ function safeJson(s) { try { return JSON.parse(s); } catch { return null; } }
63
+
64
+ const stack = {
65
+ framework: 'unknown',
66
+ kind: pkg ? 'node' : (fs.existsSync(path.join(cwd, 'index.html')) ? 'static-html' : 'unknown'),
67
+ ts: !!safeRead(path.join(cwd, 'tsconfig.json')),
68
+ tailwind: !!deps['tailwindcss'] || !!safeRead(path.join(cwd, 'tailwind.config.js')) || !!safeRead(path.join(cwd, 'tailwind.config.ts')),
69
+ has_shadcn: !!deps['@shadcn/ui'] || !!deps['shadcn-ui'],
70
+ has_charts: !!(deps['recharts'] || deps['chart.js'] || deps['d3'] || deps['visx']),
71
+ has_motion: !!(deps['framer-motion'] || deps['motion']),
72
+ };
73
+ if (deps['next']) stack.framework = 'next';
74
+ else if (deps['nuxt']) stack.framework = 'nuxt';
75
+ else if (deps['vite']) stack.framework = 'vite';
76
+ else if (deps['react']) stack.framework = 'react';
77
+ else if (deps['vue']) stack.framework = 'vue';
78
+ else if (deps['svelte'] || deps['@sveltejs/kit']) stack.framework = 'svelte';
79
+ else if (deps['solid-js']) stack.framework = 'solid';
80
+ else if (deps['astro']) stack.framework = 'astro';
81
+
82
+ // --- 2. brand signal (color + font + voice + language) ---
83
+ const cssLike = walk(cwd, { exts: ['.css', '.scss', '.tsx', '.jsx', '.vue', '.svelte', '.html'], max: 80 });
84
+
85
+ const hexCounts = new Map();
86
+ const fontFamilies = new Set();
87
+ let sampledChars = 0;
88
+ const SAMPLE_BUDGET = 600_000; // bytes
89
+
90
+ for (const f of cssLike) {
91
+ if (sampledChars > SAMPLE_BUDGET) break;
92
+ const content = safeRead(path.join(cwd, f.path));
93
+ if (!content) continue;
94
+ sampledChars += content.length;
95
+ // hex colors
96
+ const hexes = content.match(/#[0-9a-fA-F]{6}\b/g) || [];
97
+ for (const h of hexes) {
98
+ const norm = h.toLowerCase();
99
+ if (norm === '#ffffff' || norm === '#000000') {
100
+ hexCounts.set(norm, (hexCounts.get(norm) || 0) + 1);
101
+ continue;
102
+ }
103
+ hexCounts.set(norm, (hexCounts.get(norm) || 0) + 1);
104
+ }
105
+ // font-family declarations
106
+ const fams = content.match(/font-family\s*:\s*[^;]+/g) || [];
107
+ for (const decl of fams) {
108
+ const inside = decl.split(':').slice(1).join(':');
109
+ const parts = inside.split(',').map((s) => s.trim().replace(/['"]/g, '').replace(/;.*/, ''));
110
+ for (const p of parts.slice(0, 2)) {
111
+ if (p && p.length < 40 && !p.startsWith('var(') && !/^(sans-serif|serif|monospace|system-ui|-apple-system|inherit|initial)$/i.test(p)) {
112
+ fontFamilies.add(p);
113
+ }
114
+ }
115
+ }
116
+ }
117
+
118
+ // dominant color: highest count excluding pure white/black
119
+ const sortedHex = [...hexCounts.entries()]
120
+ .filter(([h]) => h !== '#ffffff' && h !== '#000000')
121
+ .sort((a, b) => b[1] - a[1]);
122
+ const dominantColor = sortedHex[0]?.[0] || (hexCounts.get('#ffffff') ? '#ffffff' : null);
123
+ const secondaryColors = sortedHex.slice(1, 4).map(([h]) => h);
124
+
125
+ // language detection (README + commit msgs ratio of hangul vs ascii)
126
+ const readmeContent = safeRead(path.join(cwd, 'README.md')) || safeRead(path.join(cwd, 'README')) || '';
127
+ let language = 'en';
128
+ if (readmeContent) {
129
+ const hangul = (readmeContent.match(/[가-힣]/g) || []).length;
130
+ const ascii = (readmeContent.match(/[a-zA-Z]/g) || []).length;
131
+ if (hangul > ascii * 0.3) language = 'ko';
132
+ else if ((readmeContent.match(/[ぁ-んァ-ヶ一-龯]/g) || []).length > ascii * 0.3) language = 'ja';
133
+ }
134
+
135
+ // voice keywords (heuristic — categorize content keywords)
136
+ const voiceKeywords = [];
137
+ const lowerReadme = readmeContent.toLowerCase();
138
+ if (/data|metrics|dashboard|chart|table|kpi/.test(lowerReadme)) voiceKeywords.push('data-dense');
139
+ if (/fintech|finance|invest|trading|bank|pay/.test(lowerReadme)) voiceKeywords.push('fintech');
140
+ if (/ai|llm|gpt|claude|agent/.test(lowerReadme)) voiceKeywords.push('ai-tools');
141
+ if (/community|social|share|connect/.test(lowerReadme)) voiceKeywords.push('community');
142
+ if (/dev|developer|api|sdk|cli/.test(lowerReadme)) voiceKeywords.push('developer-tools');
143
+ if (/health|wellness|fitness|medical/.test(lowerReadme)) voiceKeywords.push('health');
144
+ if (/commerce|shop|store|product|buy/.test(lowerReadme)) voiceKeywords.push('commerce');
145
+ if (/design|brand|aesthetic|visual/.test(lowerReadme)) voiceKeywords.push('design-tools');
146
+ if (voiceKeywords.length === 0) voiceKeywords.push('general');
147
+
148
+ const brand_signal = {
149
+ dominant_color_hex: dominantColor,
150
+ secondary_colors: secondaryColors,
151
+ font_families: [...fontFamilies].slice(0, 5),
152
+ voice_keywords: voiceKeywords.slice(0, 4),
153
+ language,
154
+ };
155
+
156
+ // --- 3. surface inventory (dedicated wider walk) ---
157
+ function classifySurface(p) {
158
+ const lc = p.toLowerCase();
159
+ const seg = lc.split('/');
160
+ // route segment heuristic — look at parent dir name
161
+ const parent = seg[seg.length - 2] || '';
162
+ if (/builder|editor|create|new|compose/.test(parent)) return 'product';
163
+ if (/dashboard|console|admin|manage/.test(parent)) return 'dashboard';
164
+ if (/doc|guide|help|reference/.test(parent)) return 'docs';
165
+ if (/onboard|signup|signin|login|auth/.test(parent)) return 'onboarding';
166
+ if (/about|contact|company|team|marketing/.test(parent)) return 'marketing';
167
+ if (/setting|profile|account/.test(parent)) return 'settings';
168
+ if (/playground|sandbox|test|qa-/.test(parent)) return 'playground';
169
+ // top-level page = landing
170
+ if (/page\.(tsx|jsx|vue|svelte)$|index\.(tsx|jsx|vue|svelte)$|App\.(tsx|jsx|vue|svelte)$/.test(lc) &&
171
+ seg.length <= 4) return 'landing';
172
+ return 'other';
173
+ }
174
+
175
+ const routeFiles = walk(cwd, { exts: ['.tsx', '.jsx', '.vue', '.svelte'], max: 400, maxDepth: 8 })
176
+ .filter((f) => /page\.(tsx|jsx|vue|svelte)$|index\.(tsx|jsx|vue|svelte)$|App\.(tsx|jsx|vue|svelte)$|\/(pages|routes)\//.test(f.path));
177
+ const pageFiles = routeFiles
178
+ .slice(0, 30)
179
+ .map((f) => ({ path: f.path, kind: classifySurface(f.path), size_kb: +(f.size / 1024).toFixed(1) }));
180
+
181
+ // --- 4. audience hypothesis ---
182
+ const audience_hypothesis = [];
183
+ const hasMarketingCopy = pageFiles.some((p) => p.kind === 'landing' || p.kind === 'marketing');
184
+ const hasInternalSurfaces = pageFiles.some((p) => p.kind === 'product' || p.kind === 'dashboard');
185
+ const readmeMentionsVisitors = /visitor|customer|user|investor|vc|prospect|signup|conversion/i.test(readmeContent);
186
+
187
+ if (readmeMentionsVisitors || hasMarketingCopy) {
188
+ audience_hypothesis.push({
189
+ label: '외부 트래픽 — SEO/conversion 우선, 톤 일탈 허용',
190
+ confidence: 0.5,
191
+ evidence: hasMarketingCopy ? 'landing/marketing surface 존재' : 'README가 외부 사용자 언급',
192
+ });
193
+ }
194
+ if (hasInternalSurfaces) {
195
+ audience_hypothesis.push({
196
+ label: '기존 코드베이스 사용자 — 시각 일관성 우선',
197
+ confidence: 0.35,
198
+ evidence: `product/dashboard surface ${pageFiles.filter((p) => p.kind === 'product' || p.kind === 'dashboard').length}개`,
199
+ });
200
+ }
201
+ if (audience_hypothesis.length === 0) {
202
+ audience_hypothesis.push({
203
+ label: '신규 사용자 — 첫인상 우선 (greenfield 추정)',
204
+ confidence: 0.6,
205
+ evidence: 'surface 인벤토리 비어있음',
206
+ });
207
+ }
208
+ if (audience_hypothesis.length === 2) {
209
+ audience_hypothesis.push({
210
+ label: '둘 다 — 모듈러 컴포넌트로 분기',
211
+ confidence: 0.15,
212
+ evidence: 'fallback',
213
+ });
214
+ }
215
+ // normalize confidence
216
+ const sum = audience_hypothesis.reduce((a, b) => a + b.confidence, 0) || 1;
217
+ for (const h of audience_hypothesis) h.confidence = +(h.confidence / sum).toFixed(2);
218
+ audience_hypothesis.sort((a, b) => b.confidence - a.confidence);
219
+
220
+ // --- 5. wow moment candidates ---
221
+ const wow_moment_candidates = [];
222
+ if (pageFiles.length >= 3) {
223
+ wow_moment_candidates.push({
224
+ label: `기존 ${pageFiles.length}-surface 통합 nav / 시각 일관성`,
225
+ evidence: pageFiles.slice(0, 3).map((p) => p.path).join(', '),
226
+ });
227
+ }
228
+ if (dominantColor && dominantColor !== '#ffffff') {
229
+ wow_moment_candidates.push({
230
+ label: `${dominantColor} 브랜드 컬러 + ${stack.framework} 모던 스택`,
231
+ evidence: `dominant hex ${dominantColor} (${sortedHex[0]?.[1] || 0} occurrences)`,
232
+ });
233
+ }
234
+ if (stack.has_charts) {
235
+ wow_moment_candidates.push({
236
+ label: '데이터 시각화 hero (이미 charts 의존성 보유)',
237
+ evidence: 'recharts/d3/chart.js detected',
238
+ });
239
+ }
240
+ if (brand_signal.voice_keywords.includes('fintech')) {
241
+ wow_moment_candidates.push({ label: '핀테크 advisor 톤 — 숫자 강조 hero', evidence: 'voice_keywords includes fintech' });
242
+ }
243
+ if (brand_signal.voice_keywords.includes('ai-tools')) {
244
+ wow_moment_candidates.push({ label: 'AI-first center-text hero (Anthropic/Vercel 패턴)', evidence: 'voice_keywords includes ai-tools' });
245
+ }
246
+ if (wow_moment_candidates.length === 0) {
247
+ wow_moment_candidates.push({ label: '깔끔한 minimal hero — first impression', evidence: 'greenfield default' });
248
+ }
249
+
250
+ // --- emit ---
251
+ const ctx = {
252
+ version: '1.6.0',
253
+ cwd,
254
+ package_name: pkg?.name ?? null,
255
+ stack,
256
+ brand_signal,
257
+ surface_inventory: pageFiles,
258
+ audience_hypothesis,
259
+ wow_moment_candidates: wow_moment_candidates.slice(0, 5),
260
+ scanned_at: new Date().toISOString(),
261
+ scan_duration_ms: Date.now() - startedAt,
262
+ };
263
+
264
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
265
+ fs.writeFileSync(outFile, JSON.stringify(ctx, null, 2), 'utf8');
266
+ process.stdout.write(outFile + '\n');
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: omd:harness
3
- description: "화면 전체나 신규 surface를 처음부터 디자인할 때의 진입점 — Discovery→Wireframe→Components→Microcopy→Validation 파이프라인을 omd-master 오케스트레이터로 실행. '랜딩 처음부터', 'production-ready', 'wireframe to production', 「一からデザイン」, 「從頭設計」류의 요청에 트리거. 단일 컴포넌트 수정은 omd:apply."
3
+ description: "화면 전체나 신규 surface를 처음부터 디자인할 때의 진입점 — Discovery→Wireframe→Components→Microcopy→Validation 파이프라인을 omd-master 오케스트레이터로 실행. 트리거: '랜딩페이지', '랜딩 페이지', '랜딩 만들어줘', '홈 화면', '첫 화면', '프로토타입', '그럴싸한', '구색 갖춰', 'first screen', 'first impression', 'landing page', 'landing', 'prototype', 'MVP UI', 'home', 'production-ready', 'wireframe to production', '랜딩 처음부터', 'production-ready', '一からデザイン', '從頭設計'. 자연어 발화('그럴싸한 랜딩 만들어줘', 'MVP UI 잡아줘', '프로토타입이라도 구색 갖춰서')에도 자동 트리거. 단일 컴포넌트 수정은 omd:apply."
4
4
  ---
5
5
 
6
6
  # omd:harness — Design Harness Entry
@@ -23,18 +23,49 @@ CLI 의존 없음. 모든 부트스트랩은 Bash + Write 툴로 직접 실행
23
23
  shape: "[도메인] + [톤/스타일] + [핵심 화면]" — 예: "토스 스타일 가족용 식단 앱 메인 화면"
24
24
  ```
25
25
 
26
- ## Step 1 — Subagent registration 사전체크 (CRITICAL)
26
+ ## Step 1 — Subagent registration 자동복구 (v1.6.0+)
27
27
 
28
- `omd-master` subagent가 이 세션에 dispatch 가능한지 verify. Agent tool의 사용 가능 subagent 목록에 `omd-master`가 있으면 진행. 없으면:
28
+ `omd-master` subagent가 이 세션에 dispatch 가능한지 verify. Agent tool의 사용 가능 subagent 목록에 `omd-master`가 있으면 Step 2로. **없으면 게이트로 막지 말고 자동복구**:
29
+
30
+ ### 1.1 — Agent 폴더 detect
31
+
32
+ 다음 폴더 중 가장 먼저 존재하는 것을 target:
33
+ - `.claude/agents/` (claude-code agent)
34
+ - `.codex/agents/` (codex-cli agent)
35
+ - `.agents/` (openhands / generic)
36
+
37
+ 없으면 `.claude/agents/`를 mkdir로 생성 (claude-code default).
38
+
39
+ ### 1.2 — 패키지 source 위치 확인 + 복구 Write
40
+
41
+ ```bash
42
+ OMD_DIR=$(npm root -g)/oh-my-design-cli
43
+ [ -d "$OMD_DIR" ] || OMD_DIR=$(npm root)/oh-my-design-cli # local fallback
44
+ SRC="$OMD_DIR/agents/omd-master.md"
45
+ TARGET=".claude/agents/omd-master.md" # (또는 detect한 폴더)
46
+ [ -f "$SRC" ] && cp "$SRC" "$TARGET" && echo "RECOVERED" || echo "SRC_MISSING"
47
+ ```
48
+
49
+ 성공 시 사용자에게 한 줄:
50
+
51
+ ```
52
+ omd-master subagent를 ${TARGET}에 복구했어요. /agents 한 번 실행하거나 다음 turn에서 자동으로 잡힙니다. 계속 진행할게요.
53
+ ```
54
+
55
+ ### 1.3 — 복구도 실패한 경우 (SRC_MISSING / cp 권한 실패)
56
+
57
+ 기존 안내 fallback:
29
58
 
30
59
  ```
31
- omd-master subagent 세션에 등록되어 있지 않아요. Claude Code는 세션 시작 시점에만 .claude/agents/*.md를 로드합니다 — install-skills를 세션 띄운 후에 돌렸으면 이 케이스.
60
+ omd-master subagent 자동복구가 실패했어요 (패키지 source 누락 또는 권한 문제).
32
61
 
33
62
  해결 (가장 빠른 순서):
34
- 1. 현재 세션에서 /agents 실행 Claude Code가 .claude/agents/*.md 강제 재스캔. omd-* 8개가 목록에 나타나면 /omd-harness 재호출.
35
- 2. 안 되면: Claude Code 앱 완전 종료 (Cmd+Q / 터미널 자체 quit) 터미널 claude 재실행 /omd-harness <task> 재호출.
63
+ 1. `npx oh-my-design-cli@latest install-skills --all` 재실행 /omd-harness 재호출
64
+ 2. /agents 실행 omd-* 목록 확인 보이면 Claude Code 재시작
36
65
  ```
37
66
 
67
+ 복구 성공 시에도 main agent가 같은 turn에 master를 dispatch 못하는 경우가 있으니, Step 4 spawn 시 한 번 더 verify해서 실패하면 사용자에게 "다음 turn에서 자동 재발동" 안내 후 Step 2-3 산출물(ctx-prime.json + 페르소나 답)만 보존하고 종료.
68
+
38
69
  ## Step 2 — Run 디렉토리 부트스트랩 (인라인 Bash)
39
70
 
40
71
  이전엔 `omd harness "<task>" --internal` CLI를 호출했지만 1.0.0부터는 스킬이 직접 한다. 결정론적 hard verify gate:
@@ -118,7 +149,104 @@ test -d "${RUN_DIR}" && test -f "${RUN_DIR}/task.md" && echo "OK" || echo "FAIL"
118
149
  하네스 부트스트랩이 실패했어요 (run dir or task.md 누락). 디스크 권한·경로 문제일 수 있어요. 다시 시도하거나 .omd/ 디렉토리를 정리해주세요.
119
150
  ```
120
151
 
121
- 이 gate를 통과해야만 Step 3로.
152
+ 이 gate를 통과해야만 Step 2.5로.
153
+
154
+ ## Step 2.5 — CTX-PRIME — 코드베이스 자동 분석 (v1.6.0+)
155
+
156
+ reference를 고르라고 사용자에게 묻기 전에 **먼저 레포를 본다**. 사용자가 듣고 싶은 첫 문장은 "이 레포 분석했어요 — Next.js 14 + 토스 블루 + 4개 surface" 같은 *진단*이지 "어느 레퍼런스 골라드릴까요?"가 아니다.
157
+
158
+ ### 2.5.1 — ctx-prime helper 실행
159
+
160
+ ```bash
161
+ OMD_DIR=$(npm root -g)/oh-my-design-cli
162
+ [ -d "$OMD_DIR" ] || OMD_DIR=$(npm root)/oh-my-design-cli
163
+ HELPER="$OMD_DIR/scripts/ctx-prime.cjs"
164
+ [ -f "$HELPER" ] || { echo "CTX_PRIME_MISSING"; exit 0; }
165
+
166
+ node "$HELPER" "$(pwd)" "${RUN_DIR}"
167
+ ```
168
+
169
+ 성공 시 `${RUN_DIR}/ctx-prime.json` 생성. ~12-50ms (typical repo).
170
+
171
+ `CTX_PRIME_MISSING` (구버전 CLI) → Step 3로 직진 (legacy path).
172
+
173
+ ### 2.5.2 — ctx-prime.json Read + 사용자 picker 게이트
174
+
175
+ Read 툴로 `${RUN_DIR}/ctx-prime.json` 로드. 다음 필드만 사용자에게 한 줄로 brief:
176
+
177
+ - `stack.framework`, `stack.kind`, `brand_signal.dominant_color_hex`
178
+ - `surface_inventory.length` (몇 개 surface 발견)
179
+ - `brand_signal.language` (ko / en / ja)
180
+
181
+ **AskUserQuestion 1개**를 다음 shape로:
182
+
183
+ ```
184
+ question: "이 레포 분석했어요 — {framework} + {dominant_color_hex} 베이스 + {N}개 surface ({language} 카피). 이번 작업의 1차 타깃 페르소나는?"
185
+ header: "Audience"
186
+ options: ctx-prime.audience_hypothesis 상위 3개 → label/description 매핑
187
+ - audience_hypothesis[0]: label + "(추천)", description = evidence
188
+ - audience_hypothesis[1]: label, description = evidence
189
+ - audience_hypothesis[2]: label, description = evidence (없으면 생략)
190
+ ```
191
+
192
+ (AskUserQuestion이 자동 "Other" 추가하므로 자유 입력 페르소나도 가능.)
193
+
194
+ 사용자 답을 `ctx-prime.json`에 `confirmed_audience` 필드로 merge (Edit 또는 Write):
195
+
196
+ ```jsonc
197
+ {
198
+ // ... 기존 필드 ...
199
+ "confirmed_audience": "외부 트래픽 — SEO/conversion 우선, 톤 일탈 허용"
200
+ }
201
+ ```
202
+
203
+ ### 2.5.3 — Interview-lite (2-4 picker 묶음)
204
+
205
+ 페르소나 확정 직후 **AskUserQuestion 1번 더, 최대 4개 question 묶음**. ctx-prime 결과를 활용해 picker option을 동적 구성:
206
+
207
+ **Question 1 — exit_scope:**
208
+ - "단일 화면만 (한 surface 깊이)"
209
+ - "풀 랜딩 (hero + features + CTA + footer)" — 추천 (대부분의 경우)
210
+ - "다중 surface (랜딩 + product preview + docs)"
211
+
212
+ **Question 2 — wow moment:**
213
+ - ctx-prime.wow_moment_candidates 상위 3개 + "Other"
214
+
215
+ **Question 3 — primary CTA:**
216
+ - "Sign-up / Get started" — 추천 if audience=외부
217
+ - "Book demo / Contact sales"
218
+ - "GitHub star / View source"
219
+ - "View docs / Read more"
220
+
221
+ **Question 4 — visual grounding:**
222
+ - "Live reference capture (느림, 정확)" — 추천 if exit_scope=풀랜딩
223
+ - "Catalog-only (빠름, generic)"
224
+
225
+ 답을 `${RUN_DIR}/handoff/.handoff.json`에 prefilled_slots로 적재:
226
+
227
+ ```bash
228
+ mkdir -p "${RUN_DIR}/handoff"
229
+ cat > "${RUN_DIR}/handoff/.handoff.json" <<EOF
230
+ {
231
+ "state": "PROPOSE_PLAN",
232
+ "prefilled_slots": {
233
+ "audience": "<confirmed_audience>",
234
+ "exit_scope": "<answer 1>",
235
+ "wow_moment": "<answer 2>",
236
+ "cta_primary": "<answer 3>",
237
+ "visual_grounding": "<answer 4>"
238
+ },
239
+ "ctx_prime_ref": "ctx-prime.json",
240
+ "created_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
241
+ }
242
+ EOF
243
+ ```
244
+
245
+ 이 시점부터 master는 SLOT_GATE를 건너뛰고 PROPOSE_PLAN으로 직행한다 (master INTAKE 분기 참고).
246
+
247
+ ### 2.5.4 — Backward compatibility
248
+
249
+ `ctx-prime.json` 누락 또는 사용자가 picker에서 "Other → 알아서 골라줘" 답하면 Step 3 (reference picker) 그대로 진행. `prefilled_slots` 없으면 master는 legacy SLOT_GATE 흐름.
122
250
 
123
251
  ## Step 3 — DESIGN.md 존재 확인 + reference 의미 매칭
124
252
 
@@ -87,7 +87,7 @@ oh-my-design 블로그의 한국어 본문을 **Toss Tech 디자인 카테고리
87
87
  oh-my-design용 변형:
88
88
  ```
89
89
  안녕하세요. oh-my-design을 만드는 [이름]이에요.
90
- 107개 브랜드의 디자인 시스템을 라이브로 캡쳐해서 카탈로그로 모으고 있어요.
90
+ 100개가 넘는 브랜드의 디자인 시스템을 라이브로 캡쳐해서 카탈로그로 모으고 있어요.
91
91
  오늘은 그 중 [브랜드]를 들여다본 이야기를 나누려고 해요.
92
92
  ```
93
93