@oaklandzoo/ostup 0.3.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.
@@ -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
- const answers = await runProjectPrompts({ flags });
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
 
@@ -33,6 +33,16 @@ Building: `/create-prd`, `/generate-tasks`, `/update-image`, `/update-gui`, `/up
33
33
 
34
34
  See each file under `.claude/commands/` for the full routine.
35
35
 
36
+ ## ostup CLI subcommands (run outside Claude Code)
37
+
38
+ - `ostup brief` — 10-question operator intake; writes `docs/brief.md`, `docs/brief.json`, `tasks/prd-initial-build.md`.
39
+ - `ostup init --brief <path>` — scaffold using an existing brief.json; applies the matching profile overlay.
40
+ - `ostup init` — interactive scaffold without brief.
41
+
36
42
  ## Helpers
37
43
 
38
44
  - `scripts/screenshot.sh <url> [out] [WxH]` — headless Chrome screenshot. Required for visual verification per `CLAUDE.md` Part 19.
45
+
46
+ ## If `docs/brief.md` is present in this project
47
+
48
+ Treat it as authoritative for scope, profile, brand, business model, and constraints. Downstream commands (`/create-prd`, `/generate-tasks`, etc.) should read `docs/brief.json` first to avoid re-asking the operator the same questions. Profile-specific guidance lives at `templates/profiles/<profile>/` (copied into your project root as `README.md` and `section-prompts.md` when the overlay is applied).
@@ -227,6 +227,9 @@ Detailed file map: `docs/ARCHITECTURE.md`.
227
227
  > Things unique to THIS repo. Filled by `/bootstrap` or as discovered.
228
228
 
229
229
  - Operator materials live in `{{INPUTS_PATH}}`. Read `{{INPUTS_PATH}}README.md` for the layout. If `{{INPUTS_PATH}}INGEST_MANIFEST.md` exists, read it before starting work.
230
+ - If `docs/brief.md` exists, it is the **authoritative source** for scope, profile, brand, business model, and constraints. Read `docs/brief.json` for the machine-readable version. Do not re-ask the operator the questions already answered there.
231
+ - If `tasks/prd-initial-build.md` exists, it is the **first PRD seed** generated from the brief. Start there before writing code.
232
+ - If a `README.md` and `section-prompts.md` exist at the project root from a profile overlay (e.g. `lead-gen`, `booking`, `saas-dashboard`, `blog`), follow that profile's hard rules and section guidance.
230
233
  - {{CONVENTION_1}}
231
234
  - {{CONVENTION_2}}
232
235
  - {{CONVENTION_3}}
@@ -17,9 +17,9 @@ Or use `codex`, `gemini`, or whichever CLI agent you prefer.
17
17
  ### 2. Paste this as your first message
18
18
 
19
19
  ```
20
- Read CLAUDE.md, AGENTS.md, and everything in {{INPUTS_PATH}}. If {{INPUTS_PATH}} has source materials (research, reference repos, brand assets, notes), use them as context. If it is empty or only has a README, that is fine. Either way, ask me up to 3 clarifying questions about what I want to build. Then propose a 30 to 60 minute MVP scope as a numbered list. Do not write code until I approve the scope.
20
+ Read CLAUDE.md, AGENTS.md, docs/brief.md (if it exists), tasks/prd-initial-build.md (if it exists), and everything in {{INPUTS_PATH}}. If {{INPUTS_PATH}} has source materials (research, reference repos, brand assets, notes), use them as context. If it is empty or only has a README, that is fine. Either way, ask me up to 3 clarifying questions about what I want to build. Then propose a 30 to 60 minute MVP scope as a numbered list. Do not write code until I approve the scope.
21
21
 
22
- After I approve, run /bootstrap to fill in this project's metadata, then start building.
22
+ After I approve, run /bootstrap to fill in this project's metadata, then start building. If docs/brief.md exists, treat it as authoritative for scope, brand, and constraints.
23
23
  ```
24
24
 
25
25
  That is the whole jump-in. The agent reads, asks, proposes, you approve, it builds.
@@ -38,6 +38,10 @@ That is the whole jump-in. The agent reads, asks, proposes, you approve, it buil
38
38
  | `docs/ARCHITECTURE.md` | Stack, file map, env vars, deploy details. | Agent |
39
39
  | `{{INPUTS_PATH}}` | Your source materials. May be empty. You can add files anytime. | Both |
40
40
  | `tasks/` | PRDs and task lists for features. | Both |
41
+ | `docs/brief.md` | Operator brief (only if you scaffolded with `ostup brief`). Source of truth for scope, brand, constraints. | Both |
42
+ | `docs/brief.json` | Machine-readable brief (same trigger). Used by downstream commands. | Agent |
43
+ | `tasks/prd-initial-build.md` | Auto-seeded first PRD from the brief (same trigger). | Both |
44
+ | `templates/profiles/<profile>/` | (if applicable) Profile-specific guidance the agent reads on day one. | Agent |
41
45
  | `.claude/commands/` | Slash command definitions. | Agent |
42
46
 
43
47
  ## Slash commands
@@ -66,13 +70,13 @@ Type these in your CLI agent. Each runs a structured routine.
66
70
 
67
71
  1. `claude` in this folder.
68
72
  2. Paste the prompt from step 2 above.
69
- 3. The agent reads everything, asks 2 or 3 questions, proposes an MVP.
73
+ 3. The agent reads everything (including `docs/brief.md` if present), asks 2 or 3 questions, proposes an MVP.
70
74
  4. You approve (or refine).
71
75
  5. The agent runs `/bootstrap` to fill in project metadata.
72
76
  6. The agent builds the first cut and pushes commits.
73
77
  7. `/prompt-end` when you stop.
74
78
 
75
- Works the same whether `{{INPUTS_PATH}}` is empty or full. With materials, the agent uses them. Without, it asks you what to build outright.
79
+ Works the same whether `{{INPUTS_PATH}}` is empty or full. With materials, the agent uses them. Without, it asks you what to build outright. If you scaffolded with `ostup brief` or `ostup init --brief`, the agent has a richer starting point: scope, profile, business model, and add-ons already chosen.
76
80
 
77
81
  ### B. Return session (pick up where you left off)
78
82
 
@@ -0,0 +1,7 @@
1
+ # --- blog profile additions ---
2
+ # Public app URL (used for absolute URLs in RSS + sitemap)
3
+ NEXT_PUBLIC_APP_URL=http://localhost:3000
4
+
5
+ # Optional: site author / RSS metadata
6
+ NEXT_PUBLIC_AUTHOR_NAME=
7
+ NEXT_PUBLIC_AUTHOR_URL=