@oaklandzoo/ostup 0.5.0 → 0.7.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/bin/cli.mjs CHANGED
@@ -10,7 +10,7 @@ loadDotEnv();
10
10
 
11
11
  const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
12
12
 
13
- const SUBCOMMANDS = new Set(['init', 'update', 'brief']);
13
+ const SUBCOMMANDS = new Set(['init', 'update', 'brief', 'export-pro']);
14
14
 
15
15
  async function readPkg() {
16
16
  const raw = await readFile(resolve(PKG_ROOT, 'package.json'), 'utf8');
@@ -41,6 +41,9 @@ function parseArgs(argv) {
41
41
  else if (a.startsWith('--brief=')) flags.brief = a.slice('--brief='.length);
42
42
  else if (a === '--target') flags.target = argv[++i];
43
43
  else if (a.startsWith('--target=')) flags.target = a.slice('--target='.length);
44
+ else if (a === '--output') flags.output = argv[++i];
45
+ else if (a.startsWith('--output=')) flags.output = a.slice('--output='.length);
46
+ else if (a === '--white-label') flags.whiteLabel = true;
44
47
  else if (a.startsWith('-')) {
45
48
  process.stderr.write(`unknown flag: ${a}\n`);
46
49
  process.exit(1);
@@ -62,6 +65,7 @@ function printHelp() {
62
65
  'Commands:',
63
66
  ' init Scaffold a new project (interactive or with --yes).',
64
67
  ' brief Run the 10-question operator intake; write docs/brief.md + brief.json.',
68
+ ' export-pro Bundle brief + brand + content + initial PRD into a ZIP for client handoff.',
65
69
  ' update Refresh bundled templates from the pinned source.',
66
70
  '',
67
71
  'Flags for `ostup init`:',
@@ -72,6 +76,7 @@ function printHelp() {
72
76
  ' --name <kebab> Skip the projectName prompt.',
73
77
  ' --ingest <path> Copy operator materials from <path> into inputs/.',
74
78
  ' --brief <path> Load a brief.json from <path> and write brief files + apply profile overlay.',
79
+ ' --white-label Strip OSTUP / Goodshin attribution from generated docs (Studio tier).',
75
80
  ' --kit-only Drop the markdown kit into a target dir, no GitHub or Vercel.',
76
81
  ' --config <path> Read .ostup-config.yml from this path (kit-only mode).',
77
82
  '',
@@ -81,6 +86,10 @@ function printHelp() {
81
86
  ' --force Overwrite existing docs/brief.md, docs/brief.json, tasks/prd-initial-build.md.',
82
87
  ' --dry-run Print what would be written without writing.',
83
88
  '',
89
+ 'Flags for `ostup export-pro`:',
90
+ ' --output <file> ZIP filename (default: pro-export-<project>-<timestamp>.zip).',
91
+ ' --dry-run Print what would be bundled without writing the ZIP.',
92
+ '',
84
93
  'Global flags:',
85
94
  ' --version, -v Print version and exit.',
86
95
  ' --help, -h Print this help and exit.',
@@ -142,6 +151,26 @@ if (subcommand === 'brief') {
142
151
  }
143
152
  }
144
153
 
154
+ if (subcommand === 'export-pro') {
155
+ const { exportPro } = await import('../src/export-pro.mjs');
156
+ try {
157
+ const result = await exportPro({ output: flags.output, dryRun: flags.dryRun });
158
+ if (!result.dryRun) {
159
+ const kb = (result.bytes / 1024).toFixed(1);
160
+ process.stdout.write(`Exported: ${result.zipPath} (${kb} KB)\n`);
161
+ process.stdout.write(`Included ${result.present.length} path(s).\n`);
162
+ if (result.missing.length > 0) {
163
+ process.stdout.write(`Skipped ${result.missing.length} missing path(s): ${result.missing.join(', ')}\n`);
164
+ }
165
+ }
166
+ process.exit(0);
167
+ } catch (err) {
168
+ process.stderr.write(`${err.message}\n`);
169
+ const userErrors = new Set(['EXPORT_EMPTY', 'EXPORT_FAILED']);
170
+ process.exit(userErrors.has(err.code) ? 1 : 2);
171
+ }
172
+ }
173
+
145
174
  // subcommand === 'init'
146
175
  if (flags.kitOnly) {
147
176
  const { scaffold } = await import('../src/scaffold.mjs');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oaklandzoo/ostup",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Scaffolds a new repo with the Ostup Agent Kit pre-installed: slash commands, doc templates, and a clean working state.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,87 @@
1
+ // export-pro.mjs: bundle docs/brief + assets/brand + assets/content + tasks/prd-initial-build into a single ZIP for client handoff.
2
+
3
+ import { existsSync } from 'node:fs';
4
+ import { mkdir, readdir, readFile, writeFile, stat } from 'node:fs/promises';
5
+ import { resolve, join, relative, dirname } from 'node:path';
6
+ import { execSync } from 'node:child_process';
7
+
8
+ const INCLUDE_PATHS = [
9
+ 'docs/brief.md',
10
+ 'docs/brief.json',
11
+ 'tasks/prd-initial-build.md',
12
+ 'assets/brand',
13
+ 'assets/content',
14
+ 'README.md',
15
+ 'CLAUDE.md',
16
+ 'AGENTS.md',
17
+ 'START_HERE.md',
18
+ ];
19
+
20
+ async function walk(dir, base = dir, out = []) {
21
+ if (!existsSync(dir)) return out;
22
+ const s = await stat(dir);
23
+ if (!s.isDirectory()) {
24
+ out.push(relative(base, dir) || dir.split('/').pop());
25
+ return out;
26
+ }
27
+ const entries = await readdir(dir, { withFileTypes: true });
28
+ for (const entry of entries) {
29
+ const full = join(dir, entry.name);
30
+ if (entry.isDirectory()) await walk(full, base, out);
31
+ else if (entry.isFile()) out.push(full);
32
+ }
33
+ return out;
34
+ }
35
+
36
+ export async function exportPro({ cwd = process.cwd(), output, dryRun = false } = {}) {
37
+ const root = resolve(cwd);
38
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').slice(0, 17);
39
+ const projectName = (() => {
40
+ try {
41
+ const pkg = require('node:fs').readFileSync(join(root, 'package.json'), 'utf8');
42
+ return JSON.parse(pkg).name?.replace(/[^a-z0-9-]/gi, '-') || 'project';
43
+ } catch {
44
+ return root.split('/').pop() || 'project';
45
+ }
46
+ })();
47
+ const zipName = output || `pro-export-${projectName}-${timestamp}.zip`;
48
+ const zipPath = resolve(root, zipName);
49
+
50
+ // Verify what's available before bundling
51
+ const present = [];
52
+ const missing = [];
53
+ for (const p of INCLUDE_PATHS) {
54
+ if (existsSync(join(root, p))) present.push(p);
55
+ else missing.push(p);
56
+ }
57
+
58
+ if (present.length === 0) {
59
+ const err = new Error('nothing to export: none of the standard pro-export paths exist in this project');
60
+ err.code = 'EXPORT_EMPTY';
61
+ throw err;
62
+ }
63
+
64
+ if (dryRun) {
65
+ process.stdout.write('[export-pro] would bundle:\n');
66
+ for (const p of present) process.stdout.write(` + ${p}\n`);
67
+ if (missing.length > 0) {
68
+ process.stdout.write('[export-pro] missing (skipped):\n');
69
+ for (const p of missing) process.stdout.write(` - ${p}\n`);
70
+ }
71
+ process.stdout.write(`[export-pro] would write: ${zipPath}\n`);
72
+ return { zipPath, present, missing, dryRun: true };
73
+ }
74
+
75
+ // Use zip CLI (universally available on macOS + Linux). Build a list of relative paths to include.
76
+ const args = ['-r', '-q', zipPath, ...present];
77
+ try {
78
+ execSync(`zip ${args.map((a) => `"${a}"`).join(' ')}`, { cwd: root, stdio: 'pipe' });
79
+ } catch (err) {
80
+ const e = new Error(`zip failed: ${err.message || err}`);
81
+ e.code = 'EXPORT_FAILED';
82
+ throw e;
83
+ }
84
+
85
+ const bytes = (await stat(zipPath)).size;
86
+ return { zipPath, present, missing, bytes };
87
+ }
package/src/mvp-flow.mjs CHANGED
@@ -19,6 +19,7 @@ import { REGISTRY, OPTIONAL_REGISTRY } from './templates.mjs';
19
19
  import { run as exec, isDryRun } from './exec.mjs';
20
20
  import { loadBrief, writeBriefFiles } from './brief/index.mjs';
21
21
  import { applyProfileOverlay } from './brief/profile-router.mjs';
22
+ import { applyWhiteLabel } from './white-label.mjs';
22
23
 
23
24
  const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
24
25
  const TEMPLATES_ROOT = resolve(PKG_ROOT, 'templates');
@@ -121,6 +122,13 @@ export async function runMvp({ flags = {}, cwd = process.cwd() } = {}) {
121
122
  await writeProjectEnvFiles({ targetDir, collected: creds.collected });
122
123
  await ensureGitignoreEnv({ targetDir });
123
124
 
125
+ if (flags.whiteLabel) {
126
+ const wl = await applyWhiteLabel({ targetDir });
127
+ if (wl.touched.length > 0) {
128
+ process.stdout.write(`[white-label] scrubbed OSTUP/Goodshin attribution in ${wl.touched.length} file(s)\n`);
129
+ }
130
+ }
131
+
124
132
  await exec('git-init', 'git', ['init'], { cwd: targetDir });
125
133
  await exec('git-branch','git', ['branch', '-M', 'main'], { cwd: targetDir });
126
134
  await exec('git-add', 'git', ['add', '.'], { cwd: targetDir });
package/src/scaffold.mjs CHANGED
@@ -51,6 +51,14 @@ export async function scaffold({ targetDir, flags, stdinIsTTY = process.stdin.is
51
51
  await writeOne({ entry, absTarget, tokens });
52
52
  }
53
53
 
54
+ if (flags.whiteLabel) {
55
+ const { applyWhiteLabel } = await import('./white-label.mjs');
56
+ const wl = await applyWhiteLabel({ targetDir: absTarget });
57
+ if (wl.touched.length > 0) {
58
+ process.stdout.write(`[white-label] scrubbed OSTUP/Goodshin attribution in ${wl.touched.length} file(s)\n`);
59
+ }
60
+ }
61
+
54
62
  initGitIfNeeded(absTarget);
55
63
  printNextSteps(absTarget);
56
64
  }
package/src/templates.mjs CHANGED
@@ -31,6 +31,9 @@ export const REGISTRY = [
31
31
  { src: '.claude/commands/resume.md', dest: '.claude/commands/resume.md' },
32
32
  { src: '.claude/commands/handoff-doctor.md', dest: '.claude/commands/handoff-doctor.md' },
33
33
  { src: '.claude/commands/break-into-stories.md', dest: '.claude/commands/break-into-stories.md' },
34
+ { src: '.claude/commands/generate-brand-kit.md', dest: '.claude/commands/generate-brand-kit.md' },
35
+ { src: '.claude/commands/generate-content-pack.md', dest: '.claude/commands/generate-content-pack.md' },
36
+ { src: '.claude/commands/handoff-package.md', dest: '.claude/commands/handoff-package.md' },
34
37
  { src: 'CLAUDE.md', dest: 'CLAUDE.md' },
35
38
  { src: 'AGENTS.md', dest: 'AGENTS.md' },
36
39
  { src: 'START_HERE.md', dest: 'START_HERE.md' },
@@ -0,0 +1,54 @@
1
+ // white-label.mjs: post-process specific generated files to strip OSTUP / Goodshin attribution.
2
+ // Used when `ostup init --white-label` is passed (Studio tier).
3
+
4
+ import { readFile, writeFile } from 'node:fs/promises';
5
+ import { existsSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+
8
+ const TARGETS = [
9
+ 'AGENTS.md',
10
+ 'README.md',
11
+ 'START_HERE.md',
12
+ '.claude/commands/bootstrap.md',
13
+ '.claude/commands/prompt-start.md',
14
+ '.claude/commands/prompt-end.md',
15
+ '.claude/commands/preflight.md',
16
+ ];
17
+
18
+ const REPLACEMENTS = [
19
+ { from: /Ostup Agent Kit/g, to: 'Project Kit' },
20
+ { from: /the Ostup Agent Kit/g, to: 'the Project Kit' },
21
+ { from: /scaffolded with ostup/gi, to: 'scaffolded' },
22
+ { from: /Built by Goodshin\.?/gi, to: '' },
23
+ { from: /Generated by `ostup brief`/g, to: 'Generated' },
24
+ { from: /Generated by `ostup [a-z-]+`/g, to: 'Generated' },
25
+ { from: /\bostup CLI subcommands.*\n/g, to: '' },
26
+ { from: /^\s*-\s*`ostup [a-z-]+`.*$/gm, to: '' },
27
+ { from: /\bnpx @oaklandzoo\/ostup [a-z-]+/g, to: 'project tooling' },
28
+ { from: /\(Vercel \+ GitHub \+ Goodshin\)/g, to: '(Vercel + GitHub)' },
29
+ ];
30
+
31
+ export async function applyWhiteLabel({ targetDir } = {}) {
32
+ const touched = [];
33
+ for (const rel of TARGETS) {
34
+ const path = join(targetDir, rel);
35
+ if (!existsSync(path)) continue;
36
+ let body = await readFile(path, 'utf8');
37
+ let changed = false;
38
+ for (const rule of REPLACEMENTS) {
39
+ if (rule.to === undefined) continue;
40
+ const next = body.replace(rule.from, rule.to);
41
+ if (next !== body) {
42
+ body = next;
43
+ changed = true;
44
+ }
45
+ }
46
+ if (changed) {
47
+ // Collapse stray double blank lines that replacements may have left.
48
+ body = body.replace(/\n{3,}/g, '\n\n');
49
+ await writeFile(path, body, 'utf8');
50
+ touched.push(rel);
51
+ }
52
+ }
53
+ return { touched };
54
+ }
@@ -0,0 +1,225 @@
1
+ ---
2
+ description: Compose a brand kit from the operator brief and any existing inputs/images. Writes assets/brand/{palette.md, typography.md, voice.md, logo.svg, MANIFEST.md}. Composer-only; no API call. Operator promotes individual assets into the actual project.
3
+ ---
4
+
5
+ # /generate-brand-kit
6
+
7
+ Build a project-specific brand kit from the brief. Composer-only. Designed so the agent can produce a coherent, opinionated brand starter without an external design step.
8
+
9
+ ## Step 1: read context
10
+
11
+ ```bash
12
+ [ -f docs/brief.md ] && cat docs/brief.md
13
+ [ -f docs/brief.json ] && cat docs/brief.json
14
+ ls inputs/images/ 2>/dev/null
15
+ [ -f inputs/images/MANIFEST.md ] && cat inputs/images/MANIFEST.md
16
+ ```
17
+
18
+ Absorb: `brand.vibe`, `brand.color_notes`, `brand.font_notes`, `brand.logo_notes`, `brand.inspiration_urls`, and any image in `inputs/images/`.
19
+
20
+ ## Step 2: clarify if needed (max 2 questions)
21
+
22
+ If the brief is silent on a critical field, ask:
23
+
24
+ - Color direction: warm or cool? Light, dark, or both?
25
+ - Type direction: serif headers + sans body, sans-only, mono-accents?
26
+
27
+ Skip the question if the brief already answers it.
28
+
29
+ ## Step 3: write the 5 brand-kit files
30
+
31
+ ```bash
32
+ mkdir -p assets/brand
33
+ ```
34
+
35
+ ### `assets/brand/palette.md`
36
+
37
+ ```markdown
38
+ # Color palette
39
+
40
+ > Source: `docs/brief.md` (brand.color_notes + brand.vibe).
41
+
42
+ ## Primary
43
+
44
+ | Role | Token | Hex | Use |
45
+ |---|---|---|---|
46
+ | Primary | `--color-primary` | #XXXXXX | Buttons, links, focused state |
47
+ | Primary contrast | `--color-on-primary` | #FFFFFF | Text on primary |
48
+ | Surface | `--color-surface` | #XXXXXX | Default page background |
49
+ | Surface contrast | `--color-on-surface` | #XXXXXX | Body text |
50
+
51
+ ## Secondary
52
+
53
+ | Role | Token | Hex | Use |
54
+ |---|---|---|---|
55
+ | Accent | `--color-accent` | #XXXXXX | Highlights, callouts |
56
+ | Muted | `--color-muted` | #XXXXXX | Secondary text, borders |
57
+ | Surface alt | `--color-surface-alt` | #XXXXXX | Cards, hovered rows |
58
+
59
+ ## Semantic
60
+
61
+ | Role | Token | Hex |
62
+ |---|---|---|
63
+ | Success | `--color-success` | #XXXXXX |
64
+ | Warning | `--color-warning` | #XXXXXX |
65
+ | Error | `--color-error` | #XXXXXX |
66
+
67
+ ## Dark mode
68
+
69
+ Provide a mirrored palette using `prefers-color-scheme`. Same role names; different hex.
70
+
71
+ ## CSS export
72
+
73
+ ```css
74
+ :root {
75
+ --color-primary: #XXXXXX;
76
+ --color-on-primary: #FFFFFF;
77
+ /* etc. */
78
+ }
79
+
80
+ @media (prefers-color-scheme: dark) {
81
+ :root {
82
+ --color-primary: #XXXXXX;
83
+ /* etc. */
84
+ }
85
+ }
86
+ ```
87
+ ```
88
+
89
+ Fill the placeholders with REAL hex codes derived from the brief's color_notes + vibe. Examples:
90
+
91
+ - Warm + premium + lodge → espresso `#3a2a1f`, sand `#e8dcc4`, accent walnut `#5b3a2a`
92
+ - Credible + minimal + SaaS → near-black `#0a0a0a`, off-white `#fafafa`, accent blue `#2563eb`
93
+ - Bold + playful + creator → mid-saturation primary, generous accents, dark+light parity
94
+
95
+ Show the palette in context with usage examples.
96
+
97
+ ### `assets/brand/typography.md`
98
+
99
+ ```markdown
100
+ # Typography
101
+
102
+ > Source: `docs/brief.md` (brand.font_notes + brand.vibe).
103
+
104
+ ## Scale
105
+
106
+ | Role | Family | Size | Weight | Line height | Use |
107
+ |---|---|---|---|---|---|
108
+ | Display | system or named | 48-72px | 700-800 | 1.1 | Hero headlines |
109
+ | H1 | same | 32-40px | 600-700 | 1.2 | Page titles |
110
+ | H2 | same | 24-28px | 600 | 1.3 | Section headers |
111
+ | H3 | same | 20-22px | 600 | 1.3 | Subsections |
112
+ | Body | system sans | 16-18px | 400 | 1.6 | Reading copy |
113
+ | Small | system sans | 14px | 400 | 1.5 | Metadata, caption |
114
+ | Mono | system mono | 14-16px | 400 | 1.5 | Code, terminal |
115
+
116
+ ## Recommended families
117
+
118
+ - **Body:** `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif` (zero webfont weight).
119
+ - **Mono:** `ui-monospace, SFMono-Regular, "Consolas", monospace`.
120
+ - **Display:** decide per the brief. If brief mentions "serif" or "editorial," use `ui-serif, Georgia, serif`. If "minimal" or "tech," stick with system sans at a heavier weight.
121
+
122
+ If the operator wants a named webfont, document the load strategy: `<link rel="preconnect">` + `font-display: swap` + `<link rel="stylesheet">`. Never block first paint.
123
+
124
+ ## Tailwind-safe scale
125
+
126
+ Map the above to Tailwind's default scale where possible. If the brief specifies a custom scale, override in `tailwind.config.ts` under `theme.extend.fontSize`.
127
+ ```
128
+
129
+ ### `assets/brand/voice.md`
130
+
131
+ ```markdown
132
+ # Voice and tone
133
+
134
+ > Source: `docs/brief.md` (brand.vibe + project.summary).
135
+
136
+ ## Three-word vibe
137
+
138
+ <copy from brief>
139
+
140
+ ## Tone rules
141
+
142
+ - **Plain English.** No jargon. No "leverage," no "delight," no "empower."
143
+ - **Short sentences.** Periods over commas. No em dashes.
144
+ - **Anti-fluff.** No apologies, no "we're excited," no "great question."
145
+ - **Honest about limits.** State what the product cannot do.
146
+
147
+ ## Voice in components
148
+
149
+ | Component | Pattern |
150
+ |---|---|
151
+ | Headlines | 5-9 words, declarative, name the change |
152
+ | Subheads | one sentence: who it's for + what changes |
153
+ | Buttons | imperative verb, never "Click here" |
154
+ | Empty states | explain what goes there + how to get something there |
155
+ | Errors | tell the user what to do, not what went wrong technically |
156
+ | Success | one word or one short sentence |
157
+
158
+ ## Adjectives to avoid
159
+
160
+ - Best, leading, world-class, cutting-edge, revolutionary, seamless, frictionless, robust, scalable.
161
+
162
+ ## Adjectives to consider (if the brief vibe matches)
163
+
164
+ - Warm / quiet / opinionated / fast / honest / specific / concrete.
165
+ ```
166
+
167
+ ### `assets/brand/logo.svg`
168
+
169
+ Generate a simple SVG mark based on the brief. Constraints:
170
+
171
+ - 256x256 viewBox.
172
+ - Single color (use the primary from the palette).
173
+ - 2-3 geometric shapes max.
174
+ - Recognizable at 32x32 (favicon).
175
+ - No text in the SVG (wordmark is a separate concern).
176
+
177
+ If the brief mentions a specific mark concept (e.g. "mountain silhouette + lodge name"), translate it geometrically. Otherwise: a simple monogram of the project's initial in a circle or square.
178
+
179
+ ### `assets/brand/MANIFEST.md`
180
+
181
+ ```markdown
182
+ # Brand kit
183
+
184
+ Generated by `/generate-brand-kit` on <YYYY-MM-DD> from `docs/brief.json`.
185
+
186
+ ## Files
187
+
188
+ - `palette.md` — color tokens + CSS export.
189
+ - `typography.md` — type scale + family recommendations.
190
+ - `voice.md` — tone rules + copy patterns.
191
+ - `logo.svg` — single-color geometric mark.
192
+
193
+ ## How to use
194
+
195
+ 1. Copy the CSS export from `palette.md` into your `globals.css` or `tailwind.config.ts`.
196
+ 2. Pick one family pair from `typography.md` and apply in `globals.css` or `<body>`.
197
+ 3. Read `voice.md` once. Apply to every piece of copy you write.
198
+ 4. Use `logo.svg` in `<header>` and as a favicon source.
199
+
200
+ ## Regenerate
201
+
202
+ `/generate-brand-kit` overwrites this folder. Make manual edits OUTSIDE `assets/brand/` (e.g. in `globals.css`) so regeneration does not stomp them.
203
+ ```
204
+
205
+ ## Step 4: report
206
+
207
+ ```
208
+ Brand kit generated: assets/brand/
209
+ - palette.md
210
+ - typography.md
211
+ - voice.md
212
+ - logo.svg
213
+ - MANIFEST.md
214
+
215
+ Apply the CSS export from palette.md to globals.css, then run /update-gui
216
+ to ship the visual change. Verify per CLAUDE.md Part 19.
217
+ ```
218
+
219
+ ## Hard rules
220
+
221
+ - Real hex codes, not placeholders. The agent must commit to specific values derived from the brief.
222
+ - Single color SVG. No multi-color marks v1.
223
+ - No webfonts unless brief explicitly says so.
224
+ - Generated files always land in `assets/brand/`. Operator promotes into the actual app via `/update-gui` or manual copy.
225
+ - Regeneration overwrites; manual edits go elsewhere.
@@ -0,0 +1,215 @@
1
+ ---
2
+ description: Compose page-level copy (homepage hero, CTAs, metadata, OG, JSON-LD) from the operator brief. Writes assets/content/{homepage.md, cta-bank.md, metadata.md, og.md, MANIFEST.md}. Composer-only.
3
+ ---
4
+
5
+ # /generate-content-pack
6
+
7
+ Turn the brief into ready-to-use page copy. Eliminates the "blank page" problem when the agent starts on the homepage.
8
+
9
+ ## Step 1: read context
10
+
11
+ ```bash
12
+ [ -f docs/brief.md ] && cat docs/brief.md
13
+ [ -f docs/brief.json ] && cat docs/brief.json
14
+ [ -f assets/brand/voice.md ] && cat assets/brand/voice.md
15
+ ```
16
+
17
+ ## Step 2: ask at most 1 clarifying question
18
+
19
+ If the brief leaves the headline tone ambiguous (e.g. vibe says both "credible" and "playful"), ask which leads.
20
+
21
+ Otherwise skip.
22
+
23
+ ## Step 3: write the 5 files
24
+
25
+ ```bash
26
+ mkdir -p assets/content
27
+ ```
28
+
29
+ ### `assets/content/homepage.md`
30
+
31
+ ```markdown
32
+ # Homepage copy
33
+
34
+ > Source: `docs/brief.md`. Honor `brand.vibe` + `business_model` + `scope.must_have_sections`.
35
+
36
+ ## Hero
37
+
38
+ **Headline (5-9 words):**
39
+ <derived from project.summary>
40
+
41
+ **Subhead (one sentence):**
42
+ <who it serves + what they get>
43
+
44
+ **Primary CTA:** <imperative verb + object>
45
+
46
+ ## Sections (in order)
47
+
48
+ <for each item in scope.must_have_sections, write a small block:>
49
+
50
+ ### <Section name>
51
+
52
+ **Heading:** <3-7 words>
53
+
54
+ **Body (1-2 sentences):**
55
+ <what this section communicates, in the brand voice>
56
+
57
+ **(if applicable) CTA:** <imperative>
58
+
59
+ ## Footer
60
+
61
+ **Tagline / closing line:** <one-sentence echo of the headline, from a different angle>
62
+ ```
63
+
64
+ ### `assets/content/cta-bank.md`
65
+
66
+ ```markdown
67
+ # CTA bank
68
+
69
+ Reuse across pages. Pick one per context. Never "Click here."
70
+
71
+ ## Primary actions (convert the visitor)
72
+
73
+ | Context | CTA |
74
+ |---|---|
75
+ | Hero | <imperative tied to business model> |
76
+ | Mid-page repeat | <same intent, different verb> |
77
+ | Final call | <last-chance phrasing> |
78
+
79
+ ## Secondary actions
80
+
81
+ | Context | CTA |
82
+ |---|---|
83
+ | Learn more (in feature card) | "See how it works" / "Read the docs" |
84
+ | Email capture | "Get the early access link" / "Join the list" |
85
+ | Contact | "Talk to us" / "Get a quote" |
86
+
87
+ ## What NOT to say
88
+
89
+ - "Click here"
90
+ - "Learn more" with no context
91
+ - "Submit" (use the action, e.g. "Send message")
92
+ - "Sign up now" (drop "now"; the urgency is implicit)
93
+ ```
94
+
95
+ ### `assets/content/metadata.md`
96
+
97
+ ```markdown
98
+ # Page metadata
99
+
100
+ > All pages should set these via Next.js `metadata` export.
101
+
102
+ ## Site-wide defaults
103
+
104
+ ```ts
105
+ export const metadata: Metadata = {
106
+ title: { default: '<project name>', template: '%s — <project name>' },
107
+ description: '<one-sentence project summary from brief.project.summary>',
108
+ metadataBase: new URL(process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'),
109
+ openGraph: {
110
+ title: '<project name>',
111
+ description: '<one-sentence summary>',
112
+ type: 'website',
113
+ images: ['/og.png'],
114
+ },
115
+ twitter: {
116
+ card: 'summary_large_image',
117
+ title: '<project name>',
118
+ description: '<one-sentence summary>',
119
+ },
120
+ robots: { index: true, follow: true },
121
+ };
122
+ ```
123
+
124
+ ## Per-page overrides
125
+
126
+ - Homepage: keep defaults.
127
+ - Post / detail pages (blog/booking/storefront): override title, description, og.type, published_time.
128
+ - Auth pages (/login, /signup): `robots: { index: false }`.
129
+ - Dashboard (saas-dashboard): `robots: { index: false }`, no OG.
130
+
131
+ ## JSON-LD
132
+
133
+ | Page | Schema |
134
+ |---|---|
135
+ | Homepage of lead-gen / booking / marketing | `Organization` or `LocalBusiness` |
136
+ | Booking page | `Reservation` if applicable |
137
+ | Blog post | `Article` with `author`, `datePublished`, `image` |
138
+ | SaaS marketing | `SoftwareApplication` |
139
+ | Directory entry | `LocalBusiness` per entry |
140
+ ```
141
+
142
+ ### `assets/content/og.md`
143
+
144
+ ```markdown
145
+ # Open Graph image
146
+
147
+ Spec for a 1200x630 OG image (Twitter card + LinkedIn + iMessage previews).
148
+
149
+ ## Composition
150
+
151
+ - Solid brand background (use `--color-surface` or `--color-primary`).
152
+ - Wordmark or logo in upper-left at ~80px height.
153
+ - Headline at center: 5-9 words, ~84-96px font size, in the contrasting color.
154
+ - Optional small tag in lower-right: project URL or tagline.
155
+
156
+ ## Variants
157
+
158
+ - Default: the headline from `homepage.md` hero.
159
+ - Per-post (blog): the post title.
160
+ - Per-listing (directory): listing title + city or category.
161
+
162
+ ## How to ship it
163
+
164
+ - Static: hand-design once, save to `public/og.png`, reference in `metadata`.
165
+ - Dynamic: use Next.js `opengraph-image.tsx` route convention. ImageResponse from `next/og`.
166
+ - Generated: run `/generate-image og-image` to compose a prompt then either paste it elsewhere or call Vercel AI Gateway via `/generate-image`.
167
+ ```
168
+
169
+ ### `assets/content/MANIFEST.md`
170
+
171
+ ```markdown
172
+ # Content pack
173
+
174
+ Generated by `/generate-content-pack` on <YYYY-MM-DD> from `docs/brief.json`.
175
+
176
+ ## Files
177
+
178
+ - `homepage.md` — hero + every must-have section, in-voice.
179
+ - `cta-bank.md` — reusable CTA strings, primary + secondary.
180
+ - `metadata.md` — Next.js `metadata` exports + per-page overrides + JSON-LD direction.
181
+ - `og.md` — Open Graph image spec.
182
+
183
+ ## How to use
184
+
185
+ 1. Build the homepage from `homepage.md` (one heading + one body per section).
186
+ 2. Use CTAs from `cta-bank.md` consistently across pages.
187
+ 3. Drop `metadata.md` examples into `app/layout.tsx` and per-page route files.
188
+ 4. Generate or design the OG image per `og.md`.
189
+
190
+ ## Regenerate
191
+
192
+ `/generate-content-pack` overwrites this folder. Manual copy edits to the live site live in your `app/` code, not here.
193
+ ```
194
+
195
+ ## Step 4: report
196
+
197
+ ```
198
+ Content pack generated: assets/content/
199
+ - homepage.md (hero + N sections)
200
+ - cta-bank.md
201
+ - metadata.md
202
+ - og.md
203
+ - MANIFEST.md
204
+
205
+ Next: copy the homepage hero into app/page.tsx, set metadata in
206
+ app/layout.tsx, and run /update-gui to ship. Verify per CLAUDE.md Part 19.
207
+ ```
208
+
209
+ ## Hard rules
210
+
211
+ - Honor the brief's voice rules (or `assets/brand/voice.md` if present).
212
+ - Headlines: 5-9 words. Never longer.
213
+ - CTAs: imperative + concrete object. Never "Click here." Never "Submit." Never "Sign up now."
214
+ - No invented claims. If the brief does not say it, do not write it.
215
+ - Generated files always land in `assets/content/`. Operator copies into `app/` deliberately.
@@ -0,0 +1,123 @@
1
+ ---
2
+ description: Bundle the current project state into a single client-ready handoff document. Useful for agencies handing off a project to a client or a new developer. Writes docs/HANDOFF_PACKAGE.md and optionally calls `ostup export-pro` for the ZIP.
3
+ ---
4
+
5
+ # /handoff-package
6
+
7
+ The Studio-tier (or any operator's) equivalent of a one-page handoff summary. Distills the current state into a single Markdown doc that someone new to the project can read in 10 minutes and understand:
8
+
9
+ - What this project is
10
+ - What is shipped
11
+ - What is in flight
12
+ - How to run it locally
13
+ - How to deploy
14
+ - Who to ask about what
15
+
16
+ ## Step 1: read context
17
+
18
+ ```bash
19
+ [ -f docs/brief.md ] && head -80 docs/brief.md
20
+ [ -f HANDOFF.md ] && cat HANDOFF.md
21
+ [ -f docs/PROJECT_STATE.md ] && cat docs/PROJECT_STATE.md
22
+ [ -f docs/MANUAL_TASKS.md ] && cat docs/MANUAL_TASKS.md
23
+ [ -f docs/ARCHITECTURE.md ] && cat docs/ARCHITECTURE.md
24
+ [ -f README.md ] && head -40 README.md
25
+ ls tasks/ 2>/dev/null
26
+ git log --oneline -10
27
+ ```
28
+
29
+ ## Step 2: write `docs/HANDOFF_PACKAGE.md`
30
+
31
+ ```markdown
32
+ # Handoff: <project name>
33
+
34
+ > One-page client-ready overview. Generated <YYYY-MM-DD>.
35
+ > If something here is wrong, the source of truth is `docs/brief.md` and the git log.
36
+
37
+ ## 1. What this project is
38
+
39
+ <one paragraph from brief.summary + audience>
40
+
41
+ ## 2. Where it lives
42
+
43
+ - **Repo:** <git remote URL>
44
+ - **Live URL:** <from HANDOFF or brief>
45
+ - **Latest commit:** <SHA + subject>
46
+
47
+ ## 3. What is shipped
48
+
49
+ <from PROJECT_STATE "Recently done" + git log of last 10 commits>
50
+
51
+ ## 4. What is in flight
52
+
53
+ <from HANDOFF "Active context" / "What to do next">
54
+
55
+ ## 5. How to run it locally
56
+
57
+ ```bash
58
+ git clone <repo URL>
59
+ cd <project>
60
+ npm install
61
+ cp .env.example .env.local # fill in the values
62
+ npm run dev
63
+ ```
64
+
65
+ ## 6. Environment variables required
66
+
67
+ | Var | Purpose | Where to get it |
68
+ |---|---|---|
69
+ <list each from .env.example with brief purpose>
70
+
71
+ ## 7. How to deploy
72
+
73
+ <from ARCHITECTURE.md or PROJECT_STATE — Vercel default; document any custom CI/CD>
74
+
75
+ ## 8. Profile and add-ons
76
+
77
+ <from brief.scaffold.profile + brief.scaffold.addons; explain each add-on briefly>
78
+
79
+ ## 9. Blockers + things the operator must do manually
80
+
81
+ <from MANUAL_TASKS.md "Active" section>
82
+
83
+ ## 10. Who to ask
84
+
85
+ - **Project owner:** <from brief.project.owner_or_client>
86
+ - **Last agent / dev:** <from HANDOFF if recorded>
87
+
88
+ ## 11. Recommended next steps for whoever picks this up
89
+
90
+ 1. Run `/preflight` to confirm Vercel + GitHub + env state.
91
+ 2. Run `/prompt-start` (or `/resume` if HANDOFF feels stale).
92
+ 3. Read `docs/brief.md` end to end.
93
+ 4. Pick the first item from "What is in flight" and confirm before changing anything.
94
+ ```
95
+
96
+ ## Step 3: optionally bundle into a ZIP
97
+
98
+ Ask the operator: "Bundle into a ZIP via `ostup export-pro`?"
99
+
100
+ If yes:
101
+
102
+ ```bash
103
+ ostup export-pro --output handoff-<project>-$(date +%Y%m%d).zip
104
+ ```
105
+
106
+ The ZIP includes `docs/HANDOFF_PACKAGE.md` plus the standard pro-export contents (brief, brand, content, initial PRD, agent kit).
107
+
108
+ ## Step 4: report
109
+
110
+ ```
111
+ Handoff package: docs/HANDOFF_PACKAGE.md
112
+
113
+ Optional bundle: handoff-<project>-<YYYY-MM-DD>.zip (run ostup export-pro to create)
114
+
115
+ The recipient can read the package in ~10 minutes and pick up the work.
116
+ ```
117
+
118
+ ## Hard rules
119
+
120
+ - Pull every fact from existing files (brief, HANDOFF, PROJECT_STATE, ARCHITECTURE, MANUAL_TASKS, git log). Do not invent.
121
+ - If a section has no source, write `_TBD — operator to fill_` rather than guess.
122
+ - This is a SNAPSHOT. Regenerate when you re-hand-off.
123
+ - Operator can edit the file directly to refine; do not auto-regenerate on every session.
@@ -29,7 +29,7 @@ Operator materials live in `{{INPUTS_PATH}}`. Read `{{INPUTS_PATH}}README.md` fo
29
29
 
30
30
  Session lifecycle: `/bootstrap`, `/prompt-start`, `/prompt-mid`, `/prompt-end`, `/preflight`, `/resume`, `/handoff-doctor`
31
31
 
32
- Building: `/create-prd`, `/break-into-stories`, `/generate-tasks`, `/update-image`, `/update-gui`, `/update-backend`, `/add-storage`, `/generate-image-prompt`, `/generate-image`
32
+ Building: `/create-prd`, `/break-into-stories`, `/generate-tasks`, `/update-image`, `/update-gui`, `/update-backend`, `/add-storage`, `/generate-image-prompt`, `/generate-image`, `/generate-brand-kit`, `/generate-content-pack`, `/handoff-package`
33
33
 
34
34
  See each file under `.claude/commands/` for the full routine.
35
35
 
@@ -38,6 +38,8 @@ See each file under `.claude/commands/` for the full routine.
38
38
  - `ostup brief` — 10-question operator intake; writes `docs/brief.md`, `docs/brief.json`, `tasks/prd-initial-build.md`.
39
39
  - `ostup init --brief <path>` — scaffold using an existing brief.json; applies the matching profile overlay.
40
40
  - `ostup init` — interactive scaffold without brief.
41
+ - `ostup init --white-label` — Studio tier; strips OSTUP/Goodshin attribution from generated docs.
42
+ - `ostup export-pro` — bundle docs/brief + assets/brand + assets/content + initial PRD into a ZIP for client handoff.
41
43
 
42
44
  ## Helpers
43
45