@oaklandzoo/ostup 0.3.0 → 0.5.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 (33) hide show
  1. package/README.md +23 -0
  2. package/bin/cli.mjs +27 -1
  3. package/package.json +1 -1
  4. package/src/brief/classify.mjs +218 -0
  5. package/src/brief/index.mjs +126 -0
  6. package/src/brief/profile-router.mjs +89 -0
  7. package/src/brief/questions.mjs +333 -0
  8. package/src/brief/render-brief.mjs +232 -0
  9. package/src/brief/sample-briefs.mjs +304 -0
  10. package/src/brief/schema.mjs +176 -0
  11. package/src/mvp-flow.mjs +43 -1
  12. package/src/templates.mjs +4 -0
  13. package/templates/.claude/commands/break-into-stories.md +101 -0
  14. package/templates/.claude/commands/handoff-doctor.md +93 -0
  15. package/templates/.claude/commands/resume.md +102 -0
  16. package/templates/AGENTS.md +13 -2
  17. package/templates/CLAUDE.md +3 -0
  18. package/templates/START_HERE.md +11 -4
  19. package/templates/profiles/blog/.env.example.additions +7 -0
  20. package/templates/profiles/blog/README.md +70 -0
  21. package/templates/profiles/blog/section-prompts.md +63 -0
  22. package/templates/profiles/booking/.env.example.additions +16 -0
  23. package/templates/profiles/booking/README.md +61 -0
  24. package/templates/profiles/booking/section-prompts.md +47 -0
  25. package/templates/profiles/lead-gen/.env.example.additions +8 -0
  26. package/templates/profiles/lead-gen/README.md +58 -0
  27. package/templates/profiles/lead-gen/section-prompts.md +47 -0
  28. package/templates/profiles/marketing/README.md +39 -0
  29. package/templates/profiles/marketing/section-prompts.md +36 -0
  30. package/templates/profiles/saas-dashboard/.env.example.additions +21 -0
  31. package/templates/profiles/saas-dashboard/README.md +60 -0
  32. package/templates/profiles/saas-dashboard/section-prompts.md +52 -0
  33. package/templates/scripts/verify.sh +128 -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
- 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
 
package/src/templates.mjs CHANGED
@@ -28,6 +28,9 @@ export const REGISTRY = [
28
28
  { src: '.claude/commands/add-storage.md', dest: '.claude/commands/add-storage.md' },
29
29
  { src: '.claude/commands/generate-image-prompt.md', dest: '.claude/commands/generate-image-prompt.md' },
30
30
  { src: '.claude/commands/generate-image.md', dest: '.claude/commands/generate-image.md' },
31
+ { src: '.claude/commands/resume.md', dest: '.claude/commands/resume.md' },
32
+ { src: '.claude/commands/handoff-doctor.md', dest: '.claude/commands/handoff-doctor.md' },
33
+ { src: '.claude/commands/break-into-stories.md', dest: '.claude/commands/break-into-stories.md' },
31
34
  { src: 'CLAUDE.md', dest: 'CLAUDE.md' },
32
35
  { src: 'AGENTS.md', dest: 'AGENTS.md' },
33
36
  { src: 'START_HERE.md', dest: 'START_HERE.md' },
@@ -39,6 +42,7 @@ export const REGISTRY = [
39
42
  { src: 'tasks/.gitkeep', dest: 'tasks/.gitkeep' },
40
43
  { src: 'inputs/README.md', dest: 'inputs/README.md' },
41
44
  { src: 'scripts/screenshot.sh', dest: 'scripts/screenshot.sh', executable: true },
45
+ { src: 'scripts/verify.sh', dest: 'scripts/verify.sh', executable: true },
42
46
  ];
43
47
 
44
48
  export const OPTIONAL_REGISTRY = [
@@ -0,0 +1,101 @@
1
+ ---
2
+ description: Break a PRD into vertical stories (each story is independently shippable) before generating granular tasks. Sits between /create-prd and /generate-tasks. Borrowed pattern from BMAD; kept light.
3
+ ---
4
+
5
+ # /break-into-stories
6
+
7
+ A PRD is a description. Tasks are atoms. **Stories are the missing middle layer.** A story is a small vertical slice that can be shipped on its own and feels meaningful to the operator on a deployed URL.
8
+
9
+ Use this command after `/create-prd` and before `/generate-tasks`. It produces `tasks/stories-<feature>.md` with 3-7 stories.
10
+
11
+ ## Step 1: locate the PRD
12
+
13
+ ```bash
14
+ ls tasks/prd-*.md
15
+ ```
16
+
17
+ If the operator named a feature (e.g. `/break-into-stories user-auth`), use `tasks/prd-user-auth.md`.
18
+
19
+ If no name: ask the operator which PRD.
20
+
21
+ ## Step 2: read the PRD + the brief
22
+
23
+ ```bash
24
+ cat tasks/prd-<feature>.md
25
+ [ -f docs/brief.md ] && head -80 docs/brief.md
26
+ ```
27
+
28
+ ## Step 3: derive stories
29
+
30
+ For each story, fill this shape:
31
+
32
+ ```markdown
33
+ ## Story <N>: <one-line goal>
34
+
35
+ **Why this is a story:** <one sentence on why this is a meaningful vertical slice>
36
+
37
+ **User journey:** <2-3 sentences describing what the operator or end user does>
38
+
39
+ **Acceptance:**
40
+ - <observable behavior 1>
41
+ - <observable behavior 2>
42
+
43
+ **Deployable check:** <one sentence on what you can show on the live URL after this story ships>
44
+
45
+ **Out of scope for this story:** <bullets of things explicitly NOT in this story; they belong to other stories>
46
+ ```
47
+
48
+ ### Rules for cutting stories
49
+
50
+ 1. **Vertical, not horizontal.** A story crosses UI + API + data if needed. It does NOT say "build the API for X" as a story; that is a task.
51
+ 2. **Shippable.** Each story can be merged + deployed and produces an observable change on the live URL.
52
+ 3. **Small.** 30 minutes to 2 hours of agent work each. If a story feels bigger, split it.
53
+ 4. **Independent where possible.** Story 2 should not require Story 1's completion unless there is a hard dependency (then state it).
54
+ 5. **Acceptance is observable.** "Visitor can click X and see Y" not "The reducer handles X."
55
+ 6. **3 to 7 stories per PRD.** More than 7 means the PRD is too big; less than 3 means you do not need stories, just tasks.
56
+
57
+ ## Step 4: write `tasks/stories-<feature>.md`
58
+
59
+ ```markdown
60
+ # Stories: <feature>
61
+
62
+ > Source PRD: `tasks/prd-<feature>.md`
63
+ > Generated by `/break-into-stories` on <YYYY-MM-DD>.
64
+ > Each story is independently shippable.
65
+
66
+ ## Story order (recommended)
67
+
68
+ 1. Story 1 — <goal>
69
+ 2. Story 2 — <goal>
70
+ 3. ...
71
+
72
+ ## Stories
73
+
74
+ <the N stories from Step 3>
75
+
76
+ ## Dependencies
77
+
78
+ <if any: "Story 3 depends on Story 1 because ..." else "All stories are independent.">
79
+
80
+ ## Next step
81
+
82
+ For the first story, run `/generate-tasks` referencing `Story 1` to break it into atoms. Implement, deploy, verify, then move to the next story.
83
+ ```
84
+
85
+ ## Step 5: report
86
+
87
+ ```
88
+ Stories generated: tasks/stories-<feature>.md (N stories)
89
+
90
+ Recommended first story: Story 1 — <goal>
91
+ Estimated agent work: <30 min | 1 hr | 2 hr>
92
+
93
+ Next: /generate-tasks for Story 1, then implement.
94
+ ```
95
+
96
+ ## Hard rules
97
+
98
+ - Stories never cross PRDs. One PRD → one stories file.
99
+ - Each story's acceptance criteria must be observable from the live URL after deploy, or via a `curl` probe (for backend stories).
100
+ - If a story has no deployable check, it is not a story; it is a task. Roll it into another story.
101
+ - Mark dependencies explicitly. Do not pretend stories are independent when one needs another's API.