@oaklandzoo/ostup 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/bin/cli.mjs +27 -1
- package/package.json +1 -1
- package/src/brief/classify.mjs +218 -0
- package/src/brief/index.mjs +126 -0
- package/src/brief/profile-router.mjs +89 -0
- package/src/brief/questions.mjs +333 -0
- package/src/brief/render-brief.mjs +232 -0
- package/src/brief/sample-briefs.mjs +304 -0
- package/src/brief/schema.mjs +176 -0
- package/src/mvp-flow.mjs +43 -1
- package/src/templates.mjs +2 -0
- package/templates/.claude/commands/generate-image-prompt.md +118 -0
- package/templates/.claude/commands/generate-image.md +168 -0
- package/templates/.claude/commands/preflight.md +8 -0
- package/templates/AGENTS.md +11 -1
- package/templates/CLAUDE.md +3 -0
- package/templates/START_HERE.md +10 -4
- package/templates/profiles/blog/.env.example.additions +7 -0
- package/templates/profiles/blog/README.md +70 -0
- package/templates/profiles/blog/section-prompts.md +63 -0
- package/templates/profiles/booking/.env.example.additions +16 -0
- package/templates/profiles/booking/README.md +61 -0
- package/templates/profiles/booking/section-prompts.md +47 -0
- package/templates/profiles/lead-gen/.env.example.additions +8 -0
- package/templates/profiles/lead-gen/README.md +58 -0
- package/templates/profiles/lead-gen/section-prompts.md +47 -0
- package/templates/profiles/marketing/README.md +39 -0
- package/templates/profiles/marketing/section-prompts.md +36 -0
- package/templates/profiles/saas-dashboard/.env.example.additions +21 -0
- package/templates/profiles/saas-dashboard/README.md +60 -0
- package/templates/profiles/saas-dashboard/section-prompts.md +52 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// brief/sample-briefs.mjs: fixture briefs used by tests + dry-run flows. One per profile.
|
|
2
|
+
|
|
3
|
+
import { SCHEMA_VERSION } from './schema.mjs';
|
|
4
|
+
|
|
5
|
+
export const BOOKING_BRIEF = {
|
|
6
|
+
schema_version: SCHEMA_VERSION,
|
|
7
|
+
project: {
|
|
8
|
+
name: 'lodge-booking',
|
|
9
|
+
display_name: 'Lodge Booking',
|
|
10
|
+
summary: 'A booking site for a boutique ski lodge with deposits, calendar sync, and a guest dashboard.',
|
|
11
|
+
owner_or_client: 'Hakuba Lodge LLC',
|
|
12
|
+
},
|
|
13
|
+
classification: {
|
|
14
|
+
site_profile: 'booking',
|
|
15
|
+
confidence: 0.94,
|
|
16
|
+
why: ['Booking + deposit explicit', 'Calendar/availability is a core flow', 'Guest dashboard implied'],
|
|
17
|
+
secondary_profiles: ['saas-dashboard'],
|
|
18
|
+
},
|
|
19
|
+
audience: { primary: 'small_business', buyer: 'Lodge owner', end_users: ['Travel guests', 'Lodge admins'] },
|
|
20
|
+
business_model: {
|
|
21
|
+
types: ['booking_revenue', 'deposit'],
|
|
22
|
+
pricing_notes: '$200-600/night peak; 30% deposit at booking',
|
|
23
|
+
},
|
|
24
|
+
scope: {
|
|
25
|
+
must_have_sections: ['hero', 'availability calendar', 'rooms', 'amenities', 'about', 'contact'],
|
|
26
|
+
core_flows: ['booking', 'payment', 'contact_form'],
|
|
27
|
+
non_goals: ['Channel manager integration v1', 'Multi-property support v1'],
|
|
28
|
+
},
|
|
29
|
+
data_model: {
|
|
30
|
+
needs_storage: true,
|
|
31
|
+
entities: [
|
|
32
|
+
{ name: 'Booking', fields: ['date_start', 'date_end', 'guest_name', 'status', 'deposit_status'], source: 'intake' },
|
|
33
|
+
{ name: 'Room', fields: ['name', 'capacity', 'nightly_rate'], source: 'intake' },
|
|
34
|
+
],
|
|
35
|
+
recommended_storage: ['postgres', 'blob'],
|
|
36
|
+
},
|
|
37
|
+
brand: {
|
|
38
|
+
vibe: ['warm', 'rustic', 'premium'],
|
|
39
|
+
inspiration_urls: [],
|
|
40
|
+
color_notes: 'Espresso, sand, walnut. Earthy.',
|
|
41
|
+
font_notes: 'Serif header + clean sans body.',
|
|
42
|
+
logo_notes: 'Mountain silhouette + lodge name.',
|
|
43
|
+
},
|
|
44
|
+
inputs: { ingested: false, source_path: null, manifest_path: null },
|
|
45
|
+
constraints: {
|
|
46
|
+
deadline: 'mvp_7_days',
|
|
47
|
+
budget_posture: 'under_50_month',
|
|
48
|
+
must_use: ['Stripe'],
|
|
49
|
+
must_avoid: [],
|
|
50
|
+
},
|
|
51
|
+
scaffold: {
|
|
52
|
+
base: 'next',
|
|
53
|
+
profile: 'booking',
|
|
54
|
+
addons: ['postgres', 'email', 'payments'],
|
|
55
|
+
template_paths: ['templates/base', 'templates/profiles/booking'],
|
|
56
|
+
},
|
|
57
|
+
verification: {
|
|
58
|
+
acceptance_tests: [
|
|
59
|
+
'Visitor can request dates for a room',
|
|
60
|
+
'Admin receives booking-request email',
|
|
61
|
+
'Deposit checkout can be initiated',
|
|
62
|
+
],
|
|
63
|
+
visual_checks: ['Hero readable on mobile', 'Calendar mobile-friendly'],
|
|
64
|
+
deploy_checks: ['Live URL returns 200', 'OG image renders'],
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const SAAS_BRIEF = {
|
|
69
|
+
schema_version: SCHEMA_VERSION,
|
|
70
|
+
project: {
|
|
71
|
+
name: 'tasktrace',
|
|
72
|
+
display_name: 'TaskTrace',
|
|
73
|
+
summary: 'A SaaS dashboard for solo founders to trace AI agent actions across their codebases.',
|
|
74
|
+
owner_or_client: 'GG Solo',
|
|
75
|
+
},
|
|
76
|
+
classification: {
|
|
77
|
+
site_profile: 'saas-dashboard',
|
|
78
|
+
confidence: 0.92,
|
|
79
|
+
why: ['Subscription billing planned', 'Auth + dashboard required', 'Multi-tenant data implied'],
|
|
80
|
+
secondary_profiles: ['marketing'],
|
|
81
|
+
},
|
|
82
|
+
audience: { primary: 'founder', buyer: 'Solo dev', end_users: ['Solo developers', 'Small teams'] },
|
|
83
|
+
business_model: {
|
|
84
|
+
types: ['subscription', 'dashboard'],
|
|
85
|
+
pricing_notes: '$19/mo solo, $49/mo team',
|
|
86
|
+
},
|
|
87
|
+
scope: {
|
|
88
|
+
must_have_sections: ['landing hero', 'pricing', 'login', 'dashboard shell', 'settings', 'billing'],
|
|
89
|
+
core_flows: ['auth', 'payment', 'dashboard', 'contact_form'],
|
|
90
|
+
non_goals: ['Custom roles v1', 'SSO v1'],
|
|
91
|
+
},
|
|
92
|
+
data_model: {
|
|
93
|
+
needs_storage: true,
|
|
94
|
+
entities: [
|
|
95
|
+
{ name: 'User', fields: ['email', 'name', 'created_at', 'plan'], source: 'intake' },
|
|
96
|
+
{ name: 'Trace', fields: ['user_id', 'repo', 'agent', 'action', 'timestamp'], source: 'intake' },
|
|
97
|
+
],
|
|
98
|
+
recommended_storage: ['postgres'],
|
|
99
|
+
},
|
|
100
|
+
brand: {
|
|
101
|
+
vibe: ['credible', 'fast', 'minimal'],
|
|
102
|
+
inspiration_urls: ['https://linear.app', 'https://vercel.com'],
|
|
103
|
+
color_notes: 'Dark mode default. Neutral grays + one accent.',
|
|
104
|
+
font_notes: 'Geist or system sans.',
|
|
105
|
+
logo_notes: 'Geometric mark.',
|
|
106
|
+
},
|
|
107
|
+
inputs: { ingested: false, source_path: null, manifest_path: null },
|
|
108
|
+
constraints: {
|
|
109
|
+
deadline: 'production_2_4_weeks',
|
|
110
|
+
budget_posture: 'under_50_month',
|
|
111
|
+
must_use: ['Stripe', 'Neon'],
|
|
112
|
+
must_avoid: [],
|
|
113
|
+
},
|
|
114
|
+
scaffold: {
|
|
115
|
+
base: 'next',
|
|
116
|
+
profile: 'saas-dashboard',
|
|
117
|
+
addons: ['auth', 'payments', 'postgres'],
|
|
118
|
+
template_paths: ['templates/base', 'templates/profiles/saas-dashboard'],
|
|
119
|
+
},
|
|
120
|
+
verification: {
|
|
121
|
+
acceptance_tests: [
|
|
122
|
+
'User can sign up and log in',
|
|
123
|
+
'Stripe subscription can be initiated',
|
|
124
|
+
'Authenticated user sees the dashboard shell',
|
|
125
|
+
],
|
|
126
|
+
visual_checks: ['Dashboard readable on mobile', 'Login page focused on the form'],
|
|
127
|
+
deploy_checks: ['Live URL returns 200 unauthenticated landing', 'Auth flow round-trips'],
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export const BLOG_BRIEF = {
|
|
132
|
+
schema_version: SCHEMA_VERSION,
|
|
133
|
+
project: {
|
|
134
|
+
name: 'goodshin-notes',
|
|
135
|
+
display_name: 'Goodshin Notes',
|
|
136
|
+
summary: 'A content engine for technical essays and case studies, MDX-based, SEO-first, with RSS and sitemap.',
|
|
137
|
+
owner_or_client: 'GG',
|
|
138
|
+
},
|
|
139
|
+
classification: {
|
|
140
|
+
site_profile: 'blog',
|
|
141
|
+
confidence: 0.93,
|
|
142
|
+
why: ['Content engine explicit', 'MDX requested', 'SEO + RSS are core flows'],
|
|
143
|
+
secondary_profiles: ['portfolio'],
|
|
144
|
+
},
|
|
145
|
+
audience: { primary: 'creator', buyer: 'GG', end_users: ['Engineers', 'Indie hackers', 'Designers'] },
|
|
146
|
+
business_model: {
|
|
147
|
+
types: ['content_traffic', 'brand_awareness'],
|
|
148
|
+
pricing_notes: 'No revenue v1; lead-gen for consulting',
|
|
149
|
+
},
|
|
150
|
+
scope: {
|
|
151
|
+
must_have_sections: ['homepage with latest 6', 'individual post page', 'tag index', 'about', 'rss', 'sitemap'],
|
|
152
|
+
core_flows: ['content_crud', 'newsletter'],
|
|
153
|
+
non_goals: ['Member-gated posts v1', 'Comments v1'],
|
|
154
|
+
},
|
|
155
|
+
data_model: {
|
|
156
|
+
needs_storage: false,
|
|
157
|
+
entities: [{ name: 'Post', fields: ['title', 'slug', 'date', 'tags', 'body_mdx'], source: 'intake' }],
|
|
158
|
+
recommended_storage: [],
|
|
159
|
+
},
|
|
160
|
+
brand: {
|
|
161
|
+
vibe: ['quiet', 'opinionated', 'craft'],
|
|
162
|
+
inspiration_urls: ['https://overreacted.io', 'https://jvns.ca'],
|
|
163
|
+
color_notes: 'Off-white background, dark text, restrained accent.',
|
|
164
|
+
font_notes: 'Serif headers, mono code, sans body.',
|
|
165
|
+
logo_notes: 'Wordmark only.',
|
|
166
|
+
},
|
|
167
|
+
inputs: { ingested: false, source_path: null, manifest_path: null },
|
|
168
|
+
constraints: {
|
|
169
|
+
deadline: 'mvp_7_days',
|
|
170
|
+
budget_posture: 'free_tier_first',
|
|
171
|
+
must_use: [],
|
|
172
|
+
must_avoid: ['JS-heavy content tools', 'analytics that track readers'],
|
|
173
|
+
},
|
|
174
|
+
scaffold: {
|
|
175
|
+
base: 'next',
|
|
176
|
+
profile: 'blog',
|
|
177
|
+
addons: ['mdx'],
|
|
178
|
+
template_paths: ['templates/base', 'templates/profiles/blog'],
|
|
179
|
+
},
|
|
180
|
+
verification: {
|
|
181
|
+
acceptance_tests: [
|
|
182
|
+
'Visitor can read a post',
|
|
183
|
+
'RSS feed validates',
|
|
184
|
+
'Sitemap includes every post',
|
|
185
|
+
],
|
|
186
|
+
visual_checks: ['Post body has comfortable line length', 'Code blocks readable on mobile'],
|
|
187
|
+
deploy_checks: ['Live URL returns 200', '/rss.xml returns valid XML'],
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export const LEAD_GEN_BRIEF = {
|
|
192
|
+
schema_version: SCHEMA_VERSION,
|
|
193
|
+
project: {
|
|
194
|
+
name: 'palm-roofing',
|
|
195
|
+
display_name: 'Palm Roofing Co.',
|
|
196
|
+
summary: 'A lead-gen site for a roofing company that captures contact requests and routes them to the owner by email.',
|
|
197
|
+
owner_or_client: 'Palm Roofing Co.',
|
|
198
|
+
},
|
|
199
|
+
classification: {
|
|
200
|
+
site_profile: 'lead-gen',
|
|
201
|
+
confidence: 0.95,
|
|
202
|
+
why: ['Service company explicit', 'Contact-request flow is the primary action', 'No ecommerce signals'],
|
|
203
|
+
secondary_profiles: ['marketing'],
|
|
204
|
+
},
|
|
205
|
+
audience: { primary: 'small_business', buyer: 'Roofing co. owner', end_users: ['Homeowners with roof issues'] },
|
|
206
|
+
business_model: {
|
|
207
|
+
types: ['lead_capture', 'brand_awareness'],
|
|
208
|
+
pricing_notes: 'Free quotes; revenue is offline at job acceptance',
|
|
209
|
+
},
|
|
210
|
+
scope: {
|
|
211
|
+
must_have_sections: ['hero', 'services', 'service area', 'testimonials', 'faq', 'contact form'],
|
|
212
|
+
core_flows: ['contact_form'],
|
|
213
|
+
non_goals: ['Online payments v1', 'Customer portal v1'],
|
|
214
|
+
},
|
|
215
|
+
data_model: {
|
|
216
|
+
needs_storage: false,
|
|
217
|
+
entities: [{ name: 'Lead', fields: ['name', 'email', 'phone', 'address', 'message', 'created_at'], source: 'intake' }],
|
|
218
|
+
recommended_storage: [],
|
|
219
|
+
},
|
|
220
|
+
brand: {
|
|
221
|
+
vibe: ['trustworthy', 'local', 'no-nonsense'],
|
|
222
|
+
inspiration_urls: [],
|
|
223
|
+
color_notes: 'Navy and white. Accent orange for CTAs.',
|
|
224
|
+
font_notes: 'Bold sans for headlines.',
|
|
225
|
+
logo_notes: 'Wordmark with palm icon.',
|
|
226
|
+
},
|
|
227
|
+
inputs: { ingested: false, source_path: null, manifest_path: null },
|
|
228
|
+
constraints: {
|
|
229
|
+
deadline: 'mvp_7_days',
|
|
230
|
+
budget_posture: 'free_tier_first',
|
|
231
|
+
must_use: ['Resend'],
|
|
232
|
+
must_avoid: [],
|
|
233
|
+
},
|
|
234
|
+
scaffold: {
|
|
235
|
+
base: 'next',
|
|
236
|
+
profile: 'lead-gen',
|
|
237
|
+
addons: ['email'],
|
|
238
|
+
template_paths: ['templates/base', 'templates/profiles/lead-gen'],
|
|
239
|
+
},
|
|
240
|
+
verification: {
|
|
241
|
+
acceptance_tests: [
|
|
242
|
+
'Visitor can submit the contact form',
|
|
243
|
+
'Owner receives the lead by email',
|
|
244
|
+
'JSON-LD LocalBusiness schema renders',
|
|
245
|
+
],
|
|
246
|
+
visual_checks: ['Hero CTA above the fold on mobile', 'Service area map placeholder or text present'],
|
|
247
|
+
deploy_checks: ['Live URL returns 200', '/contact form posts to /api/contact'],
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
export const MARKETING_BRIEF = {
|
|
252
|
+
schema_version: SCHEMA_VERSION,
|
|
253
|
+
project: {
|
|
254
|
+
name: 'launch-soon',
|
|
255
|
+
display_name: 'Launch Soon',
|
|
256
|
+
summary: 'A single-page marketing site that announces a product, captures interest emails, and links to the GitHub repo.',
|
|
257
|
+
owner_or_client: 'GG',
|
|
258
|
+
},
|
|
259
|
+
classification: {
|
|
260
|
+
site_profile: 'marketing',
|
|
261
|
+
confidence: 0.85,
|
|
262
|
+
why: ['No data persistence beyond email list', 'Single primary CTA', 'No transaction flow'],
|
|
263
|
+
secondary_profiles: ['lead-gen'],
|
|
264
|
+
},
|
|
265
|
+
audience: { primary: 'founder', buyer: 'GG', end_users: ['Early-interest visitors'] },
|
|
266
|
+
business_model: { types: ['brand_awareness'], pricing_notes: 'Free; pre-launch' },
|
|
267
|
+
scope: {
|
|
268
|
+
must_have_sections: ['hero', 'features', 'pricing', 'email signup', 'footer'],
|
|
269
|
+
core_flows: ['newsletter'],
|
|
270
|
+
non_goals: ['Multi-page v1', 'Blog v1'],
|
|
271
|
+
},
|
|
272
|
+
data_model: { needs_storage: false, entities: [], recommended_storage: [] },
|
|
273
|
+
brand: {
|
|
274
|
+
vibe: ['bold', 'clear', 'modern'],
|
|
275
|
+
inspiration_urls: [],
|
|
276
|
+
color_notes: 'High contrast.',
|
|
277
|
+
font_notes: 'System sans, large hero.',
|
|
278
|
+
logo_notes: 'Wordmark.',
|
|
279
|
+
},
|
|
280
|
+
inputs: { ingested: false, source_path: null, manifest_path: null },
|
|
281
|
+
constraints: { deadline: 'demo_today', budget_posture: 'free_tier_first', must_use: [], must_avoid: [] },
|
|
282
|
+
scaffold: {
|
|
283
|
+
base: 'next',
|
|
284
|
+
profile: 'marketing',
|
|
285
|
+
addons: [],
|
|
286
|
+
template_paths: ['templates/base', 'templates/profiles/marketing'],
|
|
287
|
+
},
|
|
288
|
+
verification: {
|
|
289
|
+
acceptance_tests: ['Visitor can submit email', 'Hero is readable on mobile'],
|
|
290
|
+
visual_checks: ['Hero readable at 420x900'],
|
|
291
|
+
deploy_checks: ['Live URL returns 200'],
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// Alias export so that profile-name-derived lookups work consistently.
|
|
296
|
+
export const SAAS_DASHBOARD_BRIEF = SAAS_BRIEF;
|
|
297
|
+
|
|
298
|
+
export const SAMPLE_BRIEFS = {
|
|
299
|
+
marketing: MARKETING_BRIEF,
|
|
300
|
+
'lead-gen': LEAD_GEN_BRIEF,
|
|
301
|
+
booking: BOOKING_BRIEF,
|
|
302
|
+
blog: BLOG_BRIEF,
|
|
303
|
+
'saas-dashboard': SAAS_BRIEF,
|
|
304
|
+
};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// brief/schema.mjs: shape of brief.json + validation. Schema version 1.0.0.
|
|
2
|
+
|
|
3
|
+
export const SCHEMA_VERSION = '1.0.0';
|
|
4
|
+
|
|
5
|
+
export const PROFILES = ['marketing', 'lead-gen', 'booking', 'blog', 'storefront', 'saas-dashboard', 'directory', 'portfolio'];
|
|
6
|
+
|
|
7
|
+
export const ADDONS = ['auth', 'payments', 'email', 'postgres', 'mdx', 'blob', 'analytics'];
|
|
8
|
+
|
|
9
|
+
export const AUDIENCES = ['small_business', 'founder', 'agency', 'creator', 'community', 'other'];
|
|
10
|
+
|
|
11
|
+
export const BUSINESS_MODELS = [
|
|
12
|
+
'lead_capture',
|
|
13
|
+
'booking_revenue',
|
|
14
|
+
'deposit',
|
|
15
|
+
'subscription',
|
|
16
|
+
'one_time_sale',
|
|
17
|
+
'content_traffic',
|
|
18
|
+
'dashboard',
|
|
19
|
+
'operations',
|
|
20
|
+
'brand_awareness',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export const CORE_FLOWS = [
|
|
24
|
+
'contact_form',
|
|
25
|
+
'booking',
|
|
26
|
+
'payment',
|
|
27
|
+
'auth',
|
|
28
|
+
'dashboard',
|
|
29
|
+
'content_crud',
|
|
30
|
+
'search_filter',
|
|
31
|
+
'newsletter',
|
|
32
|
+
'file_upload_download',
|
|
33
|
+
'other',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export const DATA_KINDS = [
|
|
37
|
+
'none',
|
|
38
|
+
'leads',
|
|
39
|
+
'bookings',
|
|
40
|
+
'products_orders',
|
|
41
|
+
'users_accounts',
|
|
42
|
+
'content',
|
|
43
|
+
'listings',
|
|
44
|
+
'files',
|
|
45
|
+
'unsure',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
export const DEADLINES = ['demo_today', 'mvp_7_days', 'production_2_4_weeks', 'none'];
|
|
49
|
+
|
|
50
|
+
export const BUDGET_POSTURES = ['free_tier_first', 'under_50_month', 'under_250_month', 'paid_ok'];
|
|
51
|
+
|
|
52
|
+
const STRING = (v) => typeof v === 'string';
|
|
53
|
+
const NONEMPTY = (v) => STRING(v) && v.trim().length > 0;
|
|
54
|
+
const ARRAY_OF = (pred) => (v) => Array.isArray(v) && v.every(pred);
|
|
55
|
+
|
|
56
|
+
export function validateBrief(brief) {
|
|
57
|
+
const errors = [];
|
|
58
|
+
if (!brief || typeof brief !== 'object') {
|
|
59
|
+
return { ok: false, errors: ['brief must be an object'] };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (brief.schema_version !== SCHEMA_VERSION) {
|
|
63
|
+
errors.push(`schema_version must be ${SCHEMA_VERSION}, got ${brief.schema_version}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const p = brief.project;
|
|
67
|
+
if (!p || typeof p !== 'object') {
|
|
68
|
+
errors.push('project missing');
|
|
69
|
+
} else {
|
|
70
|
+
if (!NONEMPTY(p.name)) errors.push('project.name required');
|
|
71
|
+
if (!NONEMPTY(p.summary)) errors.push('project.summary required');
|
|
72
|
+
if (p.summary && (p.summary.length < 20 || p.summary.length > 240)) {
|
|
73
|
+
errors.push('project.summary must be 20-240 chars');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const c = brief.classification;
|
|
78
|
+
if (!c || typeof c !== 'object') {
|
|
79
|
+
errors.push('classification missing');
|
|
80
|
+
} else {
|
|
81
|
+
if (!PROFILES.includes(c.site_profile)) {
|
|
82
|
+
errors.push(`classification.site_profile must be one of: ${PROFILES.join(', ')}`);
|
|
83
|
+
}
|
|
84
|
+
if (typeof c.confidence !== 'number' || c.confidence < 0 || c.confidence > 1) {
|
|
85
|
+
errors.push('classification.confidence must be number 0-1');
|
|
86
|
+
}
|
|
87
|
+
if (!ARRAY_OF(NONEMPTY)(c.why)) {
|
|
88
|
+
errors.push('classification.why must be array of non-empty strings');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const a = brief.audience;
|
|
93
|
+
if (!a || typeof a !== 'object') {
|
|
94
|
+
errors.push('audience missing');
|
|
95
|
+
} else {
|
|
96
|
+
if (!AUDIENCES.includes(a.primary)) {
|
|
97
|
+
errors.push(`audience.primary must be one of: ${AUDIENCES.join(', ')}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const bm = brief.business_model;
|
|
102
|
+
if (!bm || typeof bm !== 'object') {
|
|
103
|
+
errors.push('business_model missing');
|
|
104
|
+
} else if (!Array.isArray(bm.types) || !bm.types.every((t) => BUSINESS_MODELS.includes(t))) {
|
|
105
|
+
errors.push(`business_model.types must be array of values from: ${BUSINESS_MODELS.join(', ')}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const scope = brief.scope;
|
|
109
|
+
if (!scope || typeof scope !== 'object') {
|
|
110
|
+
errors.push('scope missing');
|
|
111
|
+
} else {
|
|
112
|
+
if (!ARRAY_OF(NONEMPTY)(scope.must_have_sections) || scope.must_have_sections.length === 0) {
|
|
113
|
+
errors.push('scope.must_have_sections must be non-empty array');
|
|
114
|
+
}
|
|
115
|
+
if (!ARRAY_OF(NONEMPTY)(scope.core_flows) || scope.core_flows.length === 0) {
|
|
116
|
+
errors.push('scope.core_flows must be non-empty array');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const dm = brief.data_model;
|
|
121
|
+
if (!dm || typeof dm !== 'object') {
|
|
122
|
+
errors.push('data_model missing');
|
|
123
|
+
} else if (typeof dm.needs_storage !== 'boolean') {
|
|
124
|
+
errors.push('data_model.needs_storage must be boolean');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const brand = brief.brand;
|
|
128
|
+
if (!brand || typeof brand !== 'object') {
|
|
129
|
+
errors.push('brand missing');
|
|
130
|
+
} else if (!ARRAY_OF(NONEMPTY)(brand.vibe)) {
|
|
131
|
+
errors.push('brand.vibe must be array of strings');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const constraints = brief.constraints;
|
|
135
|
+
if (!constraints || typeof constraints !== 'object') {
|
|
136
|
+
errors.push('constraints missing');
|
|
137
|
+
} else {
|
|
138
|
+
if (!DEADLINES.includes(constraints.deadline)) {
|
|
139
|
+
errors.push(`constraints.deadline must be one of: ${DEADLINES.join(', ')}`);
|
|
140
|
+
}
|
|
141
|
+
if (!BUDGET_POSTURES.includes(constraints.budget_posture)) {
|
|
142
|
+
errors.push(`constraints.budget_posture must be one of: ${BUDGET_POSTURES.join(', ')}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const sc = brief.scaffold;
|
|
147
|
+
if (!sc || typeof sc !== 'object') {
|
|
148
|
+
errors.push('scaffold missing');
|
|
149
|
+
} else {
|
|
150
|
+
if (!PROFILES.includes(sc.profile)) {
|
|
151
|
+
errors.push(`scaffold.profile must be one of: ${PROFILES.join(', ')}`);
|
|
152
|
+
}
|
|
153
|
+
if (!Array.isArray(sc.addons) || !sc.addons.every((a) => ADDONS.includes(a))) {
|
|
154
|
+
errors.push(`scaffold.addons must be array of values from: ${ADDONS.join(', ')}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { ok: errors.length === 0, errors };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function emptyBrief() {
|
|
162
|
+
return {
|
|
163
|
+
schema_version: SCHEMA_VERSION,
|
|
164
|
+
project: { name: '', display_name: '', summary: '', owner_or_client: '' },
|
|
165
|
+
classification: { site_profile: 'marketing', confidence: 0, why: [], secondary_profiles: [] },
|
|
166
|
+
audience: { primary: 'small_business', buyer: '', end_users: [] },
|
|
167
|
+
business_model: { types: [], pricing_notes: '' },
|
|
168
|
+
scope: { must_have_sections: [], core_flows: [], non_goals: [] },
|
|
169
|
+
data_model: { needs_storage: false, entities: [], recommended_storage: [] },
|
|
170
|
+
brand: { vibe: [], inspiration_urls: [], color_notes: '', font_notes: '', logo_notes: '' },
|
|
171
|
+
inputs: { ingested: false, source_path: null, manifest_path: null },
|
|
172
|
+
constraints: { deadline: 'mvp_7_days', budget_posture: 'free_tier_first', must_use: [], must_avoid: [] },
|
|
173
|
+
scaffold: { base: 'next', profile: 'marketing', addons: [], template_paths: ['templates/base'] },
|
|
174
|
+
verification: { acceptance_tests: [], visual_checks: [], deploy_checks: [] },
|
|
175
|
+
};
|
|
176
|
+
}
|
package/src/mvp-flow.mjs
CHANGED
|
@@ -17,6 +17,8 @@ import { renderSummary } from './summary.mjs';
|
|
|
17
17
|
import { substitute, buildTokenMap } from './substitute.mjs';
|
|
18
18
|
import { REGISTRY, OPTIONAL_REGISTRY } from './templates.mjs';
|
|
19
19
|
import { run as exec, isDryRun } from './exec.mjs';
|
|
20
|
+
import { loadBrief, writeBriefFiles } from './brief/index.mjs';
|
|
21
|
+
import { applyProfileOverlay } from './brief/profile-router.mjs';
|
|
20
22
|
|
|
21
23
|
const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
22
24
|
const TEMPLATES_ROOT = resolve(PKG_ROOT, 'templates');
|
|
@@ -33,7 +35,23 @@ export async function runMvp({ flags = {}, cwd = process.cwd() } = {}) {
|
|
|
33
35
|
|
|
34
36
|
preflightOrExit();
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
let brief = null;
|
|
39
|
+
if (flags.brief) {
|
|
40
|
+
brief = await loadBrief(flags.brief);
|
|
41
|
+
process.stdout.write(`Loaded brief from ${resolve(flags.brief)}\n`);
|
|
42
|
+
process.stdout.write(` Profile: ${brief.classification.site_profile} (confidence ${brief.classification.confidence})\n`);
|
|
43
|
+
process.stdout.write(` Add-ons: ${(brief.scaffold.addons || []).join(', ') || '(none)'}\n`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const promptFlags = brief
|
|
47
|
+
? {
|
|
48
|
+
...flags,
|
|
49
|
+
name: flags.name || brief.project.name,
|
|
50
|
+
profile: flags.profile || 'goodshin',
|
|
51
|
+
}
|
|
52
|
+
: flags;
|
|
53
|
+
|
|
54
|
+
const answers = await runProjectPrompts({ flags: promptFlags });
|
|
37
55
|
if (!answers || answers.cancelled) {
|
|
38
56
|
process.stdout.write('Cancelled.\n');
|
|
39
57
|
const err = new Error('user cancelled prompts');
|
|
@@ -41,6 +59,14 @@ export async function runMvp({ flags = {}, cwd = process.cwd() } = {}) {
|
|
|
41
59
|
throw err;
|
|
42
60
|
}
|
|
43
61
|
|
|
62
|
+
if (brief) {
|
|
63
|
+
answers.displayName = brief.project.display_name || answers.displayName;
|
|
64
|
+
answers.description = brief.project.summary || answers.description;
|
|
65
|
+
if (brief.inputs?.source_path && !answers.ingestPath) {
|
|
66
|
+
answers.ingestPath = brief.inputs.source_path;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
44
70
|
printAnswersSummary(answers);
|
|
45
71
|
if (!flags.yes) {
|
|
46
72
|
const proceed = await confirmProceed();
|
|
@@ -76,6 +102,22 @@ export async function runMvp({ flags = {}, cwd = process.cwd() } = {}) {
|
|
|
76
102
|
});
|
|
77
103
|
|
|
78
104
|
await overlayKit({ targetDir, tokens });
|
|
105
|
+
if (brief) {
|
|
106
|
+
await writeBriefFiles({ targetDir, brief, force: true });
|
|
107
|
+
const overlay = await applyProfileOverlay({
|
|
108
|
+
targetDir,
|
|
109
|
+
profile: brief.scaffold.profile,
|
|
110
|
+
tokens,
|
|
111
|
+
dryRun: isDryRun(),
|
|
112
|
+
});
|
|
113
|
+
if (overlay.applied) {
|
|
114
|
+
process.stdout.write(`[brief] applied profile overlay '${brief.scaffold.profile}': ${overlay.files.length} file(s)\n`);
|
|
115
|
+
} else if (isDryRun()) {
|
|
116
|
+
process.stdout.write(`[brief] would apply profile overlay '${brief.scaffold.profile}': ${overlay.files.length} file(s)\n`);
|
|
117
|
+
} else {
|
|
118
|
+
process.stdout.write(`[brief] profile overlay '${brief.scaffold.profile}': ${overlay.reason}\n`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
79
121
|
await writeProjectEnvFiles({ targetDir, collected: creds.collected });
|
|
80
122
|
await ensureGitignoreEnv({ targetDir });
|
|
81
123
|
|
package/src/templates.mjs
CHANGED
|
@@ -26,6 +26,8 @@ export const REGISTRY = [
|
|
|
26
26
|
{ src: '.claude/commands/update-gui.md', dest: '.claude/commands/update-gui.md' },
|
|
27
27
|
{ src: '.claude/commands/update-backend.md', dest: '.claude/commands/update-backend.md' },
|
|
28
28
|
{ src: '.claude/commands/add-storage.md', dest: '.claude/commands/add-storage.md' },
|
|
29
|
+
{ src: '.claude/commands/generate-image-prompt.md', dest: '.claude/commands/generate-image-prompt.md' },
|
|
30
|
+
{ src: '.claude/commands/generate-image.md', dest: '.claude/commands/generate-image.md' },
|
|
29
31
|
{ src: 'CLAUDE.md', dest: 'CLAUDE.md' },
|
|
30
32
|
{ src: 'AGENTS.md', dest: 'AGENTS.md' },
|
|
31
33
|
{ src: 'START_HERE.md', dest: 'START_HERE.md' },
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Compose an image-generation prompt from the project brief plus 2-3 clarifying questions. Outputs a copy-pasteable prompt the operator pastes into DALL-E, Midjourney, Imagen, or any image tool. No API call, no key required.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Generate image prompt (composer only)
|
|
6
|
+
|
|
7
|
+
Use this when you need an image asset the project does not have yet. Composer only: produces a prompt for the operator to paste elsewhere. When the resulting image is dropped into `inputs/images/`, run `/update-image` to promote it per CLAUDE.md Part 19.
|
|
8
|
+
|
|
9
|
+
## Step 1: identify the asset type
|
|
10
|
+
|
|
11
|
+
If the operator named a type (e.g. `/generate-image-prompt hero`), use it. Otherwise ask.
|
|
12
|
+
|
|
13
|
+
Asset types:
|
|
14
|
+
|
|
15
|
+
| Type | Dimensions | Notes |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `hero` | 1920x1080 | Focal point off-center to leave room for overlaid headline |
|
|
18
|
+
| `background` | 2400x3000 or 1920x2400 | Full-bleed, subtle or busy is operator's call |
|
|
19
|
+
| `og-image` | 1200x630 | Bold, brand-forward, high contrast for social previews |
|
|
20
|
+
| `favicon` | 512x512 source | Mark only, no scene, must read at 16x16 |
|
|
21
|
+
| `infographic` | operator-defined | Clean type, flat or isometric, tight palette |
|
|
22
|
+
| `brand-scene` | 1920x1080 | Lifestyle or product photography style |
|
|
23
|
+
| `card` | 1200x900 or 1080x1080 | Focal image with headline headroom |
|
|
24
|
+
| `logo` | 1024x1024 | Mark only, transparent background preferred |
|
|
25
|
+
|
|
26
|
+
## Step 2: read the project context
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
[ -f docs/branding/ostup-brand-brief.md ] && cat docs/branding/ostup-brand-brief.md 2>/dev/null
|
|
30
|
+
[ -f inputs/INGEST_MANIFEST.md ] && cat inputs/INGEST_MANIFEST.md 2>/dev/null
|
|
31
|
+
[ -f inputs/README.md ] && cat inputs/README.md 2>/dev/null
|
|
32
|
+
ls inputs/images/ 2>/dev/null
|
|
33
|
+
grep -A 5 "Brand\|palette\|Visual identity\|tone" CLAUDE.md AGENTS.md 2>/dev/null | head -40
|
|
34
|
+
[ -f docs/brief.md ] && cat docs/brief.md 2>/dev/null
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Absorb: brand voice, color palette, mood, any existing visual style references, anything the brief specifies.
|
|
38
|
+
|
|
39
|
+
## Step 3: ask at most 3 clarifying questions
|
|
40
|
+
|
|
41
|
+
Tailor per asset type. Defaults below.
|
|
42
|
+
|
|
43
|
+
For `hero`:
|
|
44
|
+
- What is the subject in one phrase?
|
|
45
|
+
- Mood: bold / quiet / playful / serious?
|
|
46
|
+
- Any image already in `inputs/images/` to riff on?
|
|
47
|
+
|
|
48
|
+
For `og-image`:
|
|
49
|
+
- What headline word or short phrase should dominate?
|
|
50
|
+
- Same palette as the rest of the site or a contrast variant?
|
|
51
|
+
|
|
52
|
+
For `favicon`:
|
|
53
|
+
- Use the existing logo mark, or a simplified abstraction of it?
|
|
54
|
+
|
|
55
|
+
For `background`:
|
|
56
|
+
- Specific subject or abstract texture?
|
|
57
|
+
- Light or dark dominant?
|
|
58
|
+
|
|
59
|
+
For others: subject, palette, mood unless the brief makes it obvious.
|
|
60
|
+
|
|
61
|
+
## Step 4: compose the prompt
|
|
62
|
+
|
|
63
|
+
Use this exact output shape. Fill every field. Honor the brand palette from the brief.
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
PROMPT
|
|
67
|
+
------
|
|
68
|
+
<one paragraph, vivid, specific. Cover: subject, composition, lighting, color
|
|
69
|
+
palette (echo the brief), style references like "editorial photography" or
|
|
70
|
+
"isometric flat illustration" or "product hero shot". End with concrete
|
|
71
|
+
detail anchors.>
|
|
72
|
+
|
|
73
|
+
MODEL RECOMMENDATION
|
|
74
|
+
--------------------
|
|
75
|
+
<pick one with a one-line reason:
|
|
76
|
+
- DALL-E 3: photorealism, reliable text-following
|
|
77
|
+
- Midjourney v6: strongest art direction, painterly
|
|
78
|
+
- Imagen: best typography
|
|
79
|
+
- Stable Diffusion via Replicate: fastest iteration, lowest cost>
|
|
80
|
+
|
|
81
|
+
SIZE / ASPECT RATIO
|
|
82
|
+
-------------------
|
|
83
|
+
<exact dims for the asset type from Step 1>
|
|
84
|
+
|
|
85
|
+
NEGATIVE PROMPT (if applicable)
|
|
86
|
+
-------------------------------
|
|
87
|
+
<one line: things to exclude. e.g. "no text, no people, no logos in frame, no watermarks">
|
|
88
|
+
|
|
89
|
+
STYLE NOTES
|
|
90
|
+
-----------
|
|
91
|
+
<one line: e.g. "seed=2024 for reproducibility, subject off-center to leave
|
|
92
|
+
right third clear for overlaid headline">
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Step 5: report
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
Composed prompt for <asset type>.
|
|
99
|
+
|
|
100
|
+
Paste the PROMPT block above into your chosen image tool. When the image is
|
|
101
|
+
back, save it to inputs/images/ with a clear filename (e.g.
|
|
102
|
+
inputs/images/hero-v1.png).
|
|
103
|
+
|
|
104
|
+
Then run /update-image <slot> to promote it into the project and verify
|
|
105
|
+
visually per CLAUDE.md Part 19.
|
|
106
|
+
|
|
107
|
+
To skip the paste-into-another-tool step, use /generate-image instead. That
|
|
108
|
+
calls Vercel AI Gateway directly and saves the result to inputs/images/
|
|
109
|
+
(requires VERCEL_AI_GATEWAY_KEY in env).
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Hard rules
|
|
113
|
+
|
|
114
|
+
- Always honor the brand palette from the brief. Do not invent colors.
|
|
115
|
+
- Always specify exact dimensions.
|
|
116
|
+
- Never claim done. This command only composes a prompt.
|
|
117
|
+
- If the brief and operator answers conflict, surface the conflict and ask. Do not silently choose one.
|
|
118
|
+
- The PROMPT block must be copy-pasteable as-is; do not wrap it in commentary.
|