@oaklandzoo/ostup 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.mjs +30 -1
- package/package.json +1 -1
- package/src/export-pro.mjs +87 -0
- package/src/templates.mjs +6 -0
- package/templates/.claude/commands/break-into-stories.md +101 -0
- package/templates/.claude/commands/generate-brand-kit.md +225 -0
- package/templates/.claude/commands/generate-content-pack.md +215 -0
- package/templates/.claude/commands/handoff-doctor.md +93 -0
- package/templates/.claude/commands/resume.md +102 -0
- package/templates/AGENTS.md +3 -2
- package/templates/START_HERE.md +3 -0
- package/templates/scripts/verify.sh +128 -0
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
|
@@ -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/templates.mjs
CHANGED
|
@@ -28,6 +28,11 @@ 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' },
|
|
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' },
|
|
31
36
|
{ src: 'CLAUDE.md', dest: 'CLAUDE.md' },
|
|
32
37
|
{ src: 'AGENTS.md', dest: 'AGENTS.md' },
|
|
33
38
|
{ src: 'START_HERE.md', dest: 'START_HERE.md' },
|
|
@@ -39,6 +44,7 @@ export const REGISTRY = [
|
|
|
39
44
|
{ src: 'tasks/.gitkeep', dest: 'tasks/.gitkeep' },
|
|
40
45
|
{ src: 'inputs/README.md', dest: 'inputs/README.md' },
|
|
41
46
|
{ src: 'scripts/screenshot.sh', dest: 'scripts/screenshot.sh', executable: true },
|
|
47
|
+
{ src: 'scripts/verify.sh', dest: 'scripts/verify.sh', executable: true },
|
|
42
48
|
];
|
|
43
49
|
|
|
44
50
|
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.
|
|
@@ -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,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Audit HANDOFF.md against actual repo state. Flags stale claims, missing follow-throughs, and orphan files not mentioned anywhere. Read-only; surfaces a punch list for /prompt-end to fix.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# /handoff-doctor
|
|
6
|
+
|
|
7
|
+
HANDOFF.md should reflect repo reality. When it does not, agents (including you) make decisions based on lies. This command catches lies before they cause work loss.
|
|
8
|
+
|
|
9
|
+
## Step 1: probe
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
echo "=== HANDOFF claims ==="
|
|
13
|
+
grep -E "^(\*\*Status:\*\*|\*\*Branch:\*\*|\*\*Working tree:\*\*|\*\*Last session ended:\*\*)" HANDOFF.md | head -8
|
|
14
|
+
|
|
15
|
+
echo ""
|
|
16
|
+
echo "=== Repo reality ==="
|
|
17
|
+
echo "Branch: $(git branch --show-current)"
|
|
18
|
+
echo "Last commit: $(git log -1 --oneline)"
|
|
19
|
+
echo "Ahead of origin: $(git rev-list --count origin/$(git branch --show-current)..HEAD 2>/dev/null || echo 'no upstream')"
|
|
20
|
+
echo "Behind origin: $(git rev-list --count HEAD..origin/$(git branch --show-current) 2>/dev/null || echo 'no upstream')"
|
|
21
|
+
echo "Working tree:"
|
|
22
|
+
git status --short
|
|
23
|
+
|
|
24
|
+
echo ""
|
|
25
|
+
echo "=== Files HANDOFF claims were touched ==="
|
|
26
|
+
grep -E "^\s*-\s*\`[^\`]+\`" HANDOFF.md | head -20
|
|
27
|
+
|
|
28
|
+
echo ""
|
|
29
|
+
echo "=== Files actually changed in last 5 commits ==="
|
|
30
|
+
git log --name-only --oneline -5 | grep -v "^[0-9a-f]\{7\}" | sort -u | head -30
|
|
31
|
+
|
|
32
|
+
echo ""
|
|
33
|
+
echo "=== Tasks mentioned in HANDOFF ==="
|
|
34
|
+
grep -E "tasks/|prd-|\.md" HANDOFF.md | head -10
|
|
35
|
+
|
|
36
|
+
echo ""
|
|
37
|
+
echo "=== Tasks actually in tasks/ ==="
|
|
38
|
+
ls tasks/ 2>/dev/null
|
|
39
|
+
|
|
40
|
+
echo ""
|
|
41
|
+
echo "=== Manual blockers claimed ==="
|
|
42
|
+
[ -f docs/MANUAL_TASKS.md ] && grep -c "^\s*-\s*\[ \]" docs/MANUAL_TASKS.md
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Step 2: diagnose
|
|
46
|
+
|
|
47
|
+
Audit each of these axes. For each finding, state CLAIM vs ACTUAL.
|
|
48
|
+
|
|
49
|
+
1. **Branch mismatch.** HANDOFF says branch X, git is on branch Y.
|
|
50
|
+
2. **Push lag.** HANDOFF says "push commit X" but `git log origin/$(git branch --show-current)` already includes X.
|
|
51
|
+
3. **Working tree drift.** HANDOFF says "clean" but `git status` shows N dirty files.
|
|
52
|
+
4. **Last-session timestamp.** HANDOFF "Last session ended" is more than 7 days old. Mark as STALE.
|
|
53
|
+
5. **Orphan tasks.** Files in `tasks/` not referenced by HANDOFF.
|
|
54
|
+
6. **Phantom files.** HANDOFF references files that do not exist (`ls` them to verify).
|
|
55
|
+
7. **Phantom commits.** HANDOFF references SHAs not in `git log`.
|
|
56
|
+
8. **Manual-task drift.** HANDOFF says no blockers but MANUAL_TASKS.md has Active items (or vice versa).
|
|
57
|
+
9. **Brief drift.** docs/brief.md exists but HANDOFF makes no reference to it; or HANDOFF references a brief that does not exist.
|
|
58
|
+
10. **Test-pass lie.** HANDOFF says "N/N tests green" — run `npm test` (or similar) and compare.
|
|
59
|
+
|
|
60
|
+
## Step 3: print findings
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
HANDOFF DOCTOR REPORT
|
|
64
|
+
|
|
65
|
+
Overall: <CLEAN | NEEDS REWRITE | CRITICAL>
|
|
66
|
+
|
|
67
|
+
Findings:
|
|
68
|
+
|
|
69
|
+
[CRITICAL] <description> Fix: <what to do>
|
|
70
|
+
[STALE] <description> Fix: <what to do>
|
|
71
|
+
[ORPHAN] <description> Fix: <what to do>
|
|
72
|
+
[PHANTOM] <description> Fix: <what to do>
|
|
73
|
+
[CLEAN] No issues on this axis: <axis name>
|
|
74
|
+
|
|
75
|
+
Recommended actions:
|
|
76
|
+
1. <action>
|
|
77
|
+
2. <action>
|
|
78
|
+
|
|
79
|
+
Run /prompt-end to rewrite HANDOFF.md with current reality.
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Hard rules
|
|
83
|
+
|
|
84
|
+
- Read-only. Never modify HANDOFF.md from this command. `/prompt-end` is the writer.
|
|
85
|
+
- Be specific in findings. "HANDOFF says push 9a4708f but it is already at origin/main" is useful. "HANDOFF is wrong" is not.
|
|
86
|
+
- If a finding is borderline (e.g. timestamp 6 days old, threshold is 7), mark it CLEAN with a note rather than STALE.
|
|
87
|
+
- If everything passes, say so. Do not invent issues.
|
|
88
|
+
|
|
89
|
+
## When to run
|
|
90
|
+
|
|
91
|
+
- Before `/prompt-start` if you suspect HANDOFF is wrong.
|
|
92
|
+
- After a long AFK session before a new agent picks up.
|
|
93
|
+
- Any time HANDOFF claims something that does not match what you just saw in git.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Resume an in-progress session by reading git diff, HANDOFF, tasks, and the brief if present. Briefs the agent on actual current state vs claimed state. Use instead of /prompt-start when you suspect HANDOFF.md is stale.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# /resume
|
|
6
|
+
|
|
7
|
+
`/prompt-start` reads HANDOFF and trusts it. `/resume` reads HANDOFF and **verifies it against git reality**. Use when:
|
|
8
|
+
|
|
9
|
+
- You stopped mid-feature and HANDOFF was not rewritten.
|
|
10
|
+
- You came back after several days and are not sure HANDOFF is current.
|
|
11
|
+
- A teammate worked in this repo while you were away.
|
|
12
|
+
- Something feels off about the briefing.
|
|
13
|
+
|
|
14
|
+
## Step 1: collect ground truth
|
|
15
|
+
|
|
16
|
+
Run all of these in one bash block:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
echo "=== Git reality ==="
|
|
20
|
+
git status --short
|
|
21
|
+
git log --oneline -10
|
|
22
|
+
git diff --stat HEAD~5..HEAD 2>/dev/null || git diff --stat
|
|
23
|
+
|
|
24
|
+
echo ""
|
|
25
|
+
echo "=== HANDOFF.md (claimed state) ==="
|
|
26
|
+
[ -f HANDOFF.md ] && head -60 HANDOFF.md || echo "no HANDOFF.md"
|
|
27
|
+
|
|
28
|
+
echo ""
|
|
29
|
+
echo "=== Active tasks / PRDs ==="
|
|
30
|
+
ls tasks/ 2>/dev/null
|
|
31
|
+
[ -f tasks/prd-initial-build.md ] && head -30 tasks/prd-initial-build.md
|
|
32
|
+
|
|
33
|
+
echo ""
|
|
34
|
+
echo "=== Brief (if present) ==="
|
|
35
|
+
[ -f docs/brief.md ] && head -40 docs/brief.md || echo "no brief"
|
|
36
|
+
|
|
37
|
+
echo ""
|
|
38
|
+
echo "=== Manual blockers ==="
|
|
39
|
+
[ -f docs/MANUAL_TASKS.md ] && grep -A 2 "## Active" docs/MANUAL_TASKS.md | head -20
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Step 2: compare claimed vs actual
|
|
43
|
+
|
|
44
|
+
Build a diff in your head between:
|
|
45
|
+
|
|
46
|
+
| Source | Status they show |
|
|
47
|
+
|---|---|
|
|
48
|
+
| HANDOFF.md "What to do next" | What the last session said is queued |
|
|
49
|
+
| `git log` | What actually got committed |
|
|
50
|
+
| `git status` | What is in-flight and uncommitted |
|
|
51
|
+
| `tasks/` PRDs and PRD-seeds | What features are scoped |
|
|
52
|
+
| `docs/brief.md` (if any) | What the project is supposed to be |
|
|
53
|
+
|
|
54
|
+
Flag every mismatch. Examples of mismatches:
|
|
55
|
+
|
|
56
|
+
- HANDOFF says "push commit X" but `git log` shows X is already pushed (HANDOFF is stale; rewrite at session close).
|
|
57
|
+
- HANDOFF lists a "next action" but uncommitted work in `git status` shows that action was started and abandoned.
|
|
58
|
+
- `tasks/prd-foo.md` exists but is not mentioned in HANDOFF (forgotten artifact).
|
|
59
|
+
- Brief says profile is `booking` but the codebase has no booking-related files.
|
|
60
|
+
|
|
61
|
+
## Step 3: print the resume brief
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
RESUMED
|
|
65
|
+
|
|
66
|
+
Last commit: <SHA + subject>
|
|
67
|
+
Working tree: <clean | N dirty files: list>
|
|
68
|
+
HANDOFF claim: <one-line status>
|
|
69
|
+
Git reality: <does it match? if not, what's the gap?>
|
|
70
|
+
|
|
71
|
+
In-flight (uncommitted) work:
|
|
72
|
+
<git status summary>
|
|
73
|
+
|
|
74
|
+
What the brief expects (if brief exists):
|
|
75
|
+
<profile + add-ons + must-have sections>
|
|
76
|
+
|
|
77
|
+
Queued from HANDOFF:
|
|
78
|
+
1. <action>
|
|
79
|
+
2. <action>
|
|
80
|
+
|
|
81
|
+
Mismatches surfaced:
|
|
82
|
+
- <mismatch 1, or "none">
|
|
83
|
+
- <mismatch 2>
|
|
84
|
+
|
|
85
|
+
Recommended next move:
|
|
86
|
+
<pick one: continue queued action / resolve mismatch / pivot>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Step 4: ask before doing
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
Continue with the recommended next move? Or pivot?
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Wait for confirmation. Do NOT modify any files until the operator picks.
|
|
96
|
+
|
|
97
|
+
## Hard rules
|
|
98
|
+
|
|
99
|
+
- Never silently rewrite HANDOFF to match git. Surface the mismatch; let the operator decide.
|
|
100
|
+
- Never silently amend or rebase to "fix" git history.
|
|
101
|
+
- If `git status` shows uncommitted changes that the operator might not remember making, ask before doing anything that could lose them.
|
|
102
|
+
- This command is read-only by default. The only writes are after the operator confirms the next move.
|
package/templates/AGENTS.md
CHANGED
|
@@ -27,9 +27,9 @@ Operator materials live in `{{INPUTS_PATH}}`. Read `{{INPUTS_PATH}}README.md` fo
|
|
|
27
27
|
|
|
28
28
|
## Slash commands available
|
|
29
29
|
|
|
30
|
-
Session lifecycle: `/bootstrap`, `/prompt-start`, `/prompt-mid`, `/prompt-end`, `/preflight`
|
|
30
|
+
Session lifecycle: `/bootstrap`, `/prompt-start`, `/prompt-mid`, `/prompt-end`, `/preflight`, `/resume`, `/handoff-doctor`
|
|
31
31
|
|
|
32
|
-
Building: `/create-prd`, `/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`
|
|
33
33
|
|
|
34
34
|
See each file under `.claude/commands/` for the full routine.
|
|
35
35
|
|
|
@@ -42,6 +42,7 @@ See each file under `.claude/commands/` for the full routine.
|
|
|
42
42
|
## Helpers
|
|
43
43
|
|
|
44
44
|
- `scripts/screenshot.sh <url> [out] [WxH]` — headless Chrome screenshot. Required for visual verification per `CLAUDE.md` Part 19.
|
|
45
|
+
- `scripts/verify.sh [profile] [deploy_url]` — profile-aware verification gate. Auto-detects profile from `docs/brief.json` and deploy URL from `.vercel/project.json`. Runs common checks (build, env, live URL) plus profile-specific checks (e.g. `/api/contact` for lead-gen, `/rss.xml` for blog).
|
|
45
46
|
|
|
46
47
|
## If `docs/brief.md` is present in this project
|
|
47
48
|
|
package/templates/START_HERE.md
CHANGED
|
@@ -63,6 +63,9 @@ Type these in your CLI agent. Each runs a structured routine.
|
|
|
63
63
|
| `/add-storage` | Provision a Vercel Blob, KV, Postgres, or Edge Config store + scaffold a typed `src/lib/<type>.ts` helper + pull env vars. | When you need persistent storage. |
|
|
64
64
|
| `/generate-image-prompt` | Compose an image-generation prompt from the project brief + 2-3 questions. No API call. Paste the prompt into your preferred image tool. | When you need a visual asset and want to use Midjourney, DALL-E, Imagen, etc. directly. |
|
|
65
65
|
| `/generate-image` | Compose the prompt AND call Vercel AI Gateway, saving the image to `inputs/images/`. Requires `VERCEL_AI_GATEWAY_KEY`. | When you want the image generated in-line without leaving your agent. |
|
|
66
|
+
| `/resume` | Read git diff + HANDOFF + tasks + brief, surface mismatches between claimed and actual state. | When HANDOFF.md feels stale, you came back after several days, or something feels off about the briefing. |
|
|
67
|
+
| `/handoff-doctor` | Audit HANDOFF.md against actual repo reality (commits, working tree, files, tasks, manual blockers). Read-only punch list. | Before `/prompt-start` if you suspect HANDOFF is wrong; after a long AFK session. |
|
|
68
|
+
| `/break-into-stories` | Break a PRD into 3-7 vertical, shippable stories before generating granular tasks. | Between `/create-prd` and `/generate-tasks` for any non-trivial feature. |
|
|
66
69
|
|
|
67
70
|
## Three workflows
|
|
68
71
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Profile-aware verification gate. Runs the right checks for the project's profile.
|
|
3
|
+
# Called after /update-image, /update-gui, /update-backend, or at any time the operator wants to verify.
|
|
4
|
+
#
|
|
5
|
+
# Usage: scripts/verify.sh [profile] [deploy_url]
|
|
6
|
+
# Reads: docs/brief.json (for profile) if no profile arg
|
|
7
|
+
# Defaults: profile=marketing, deploy_url from .vercel/project.json if available
|
|
8
|
+
|
|
9
|
+
set -e
|
|
10
|
+
|
|
11
|
+
PROFILE="${1:-}"
|
|
12
|
+
DEPLOY_URL="${2:-}"
|
|
13
|
+
|
|
14
|
+
# Auto-detect profile from brief.json if not given
|
|
15
|
+
if [ -z "$PROFILE" ] && [ -f docs/brief.json ]; then
|
|
16
|
+
PROFILE=$(python3 -c "import json; print(json.load(open('docs/brief.json'))['scaffold']['profile'])" 2>/dev/null || echo "marketing")
|
|
17
|
+
fi
|
|
18
|
+
PROFILE="${PROFILE:-marketing}"
|
|
19
|
+
|
|
20
|
+
# Auto-detect deploy URL if not given (best-effort; operator may need to pass it)
|
|
21
|
+
if [ -z "$DEPLOY_URL" ]; then
|
|
22
|
+
if [ -f .vercel/project.json ]; then
|
|
23
|
+
PROJECT_NAME=$(python3 -c "import json; print(json.load(open('.vercel/project.json'))['projectName'])" 2>/dev/null || echo "")
|
|
24
|
+
if [ -n "$PROJECT_NAME" ]; then
|
|
25
|
+
DEPLOY_URL="https://${PROJECT_NAME}.vercel.app"
|
|
26
|
+
fi
|
|
27
|
+
fi
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
echo "=========================================="
|
|
31
|
+
echo " Verifying profile: $PROFILE"
|
|
32
|
+
echo " Deploy URL: ${DEPLOY_URL:-<not detected; pass as arg 2>}"
|
|
33
|
+
echo "=========================================="
|
|
34
|
+
|
|
35
|
+
PASS=0
|
|
36
|
+
FAIL=0
|
|
37
|
+
|
|
38
|
+
check() {
|
|
39
|
+
local desc="$1"
|
|
40
|
+
local cmd="$2"
|
|
41
|
+
echo -n " [$PROFILE] $desc ... "
|
|
42
|
+
if eval "$cmd" > /tmp/verify-out 2>&1; then
|
|
43
|
+
echo "PASS"
|
|
44
|
+
PASS=$((PASS + 1))
|
|
45
|
+
else
|
|
46
|
+
echo "FAIL"
|
|
47
|
+
echo " Output: $(head -3 /tmp/verify-out)"
|
|
48
|
+
FAIL=$((FAIL + 1))
|
|
49
|
+
fi
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# === Common checks (all profiles) ===
|
|
53
|
+
echo ""
|
|
54
|
+
echo "--- common ---"
|
|
55
|
+
check "build succeeds" "npm run build"
|
|
56
|
+
check ".env.example exists" "[ -f .env.example ]"
|
|
57
|
+
check "no {{TOKEN}} placeholders in CLAUDE.md" "! grep -E '\\{\\{[A-Z_]+\\}\\}' CLAUDE.md || true"
|
|
58
|
+
|
|
59
|
+
if [ -n "$DEPLOY_URL" ]; then
|
|
60
|
+
check "live URL returns 200" "curl -sI '$DEPLOY_URL' | head -1 | grep -E '200|301|302'"
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# === Profile-specific checks ===
|
|
64
|
+
case "$PROFILE" in
|
|
65
|
+
lead-gen)
|
|
66
|
+
echo ""
|
|
67
|
+
echo "--- lead-gen ---"
|
|
68
|
+
check "/api/contact route exists" "[ -f src/app/api/contact/route.ts ] || [ -f src/app/api/contact/route.js ] || [ -f app/api/contact/route.ts ]"
|
|
69
|
+
check "JSON-LD LocalBusiness in HTML" "grep -r 'LocalBusiness' src/ 2>/dev/null"
|
|
70
|
+
if [ -n "$DEPLOY_URL" ]; then
|
|
71
|
+
check "POST /api/contact returns 200 or 400" "curl -s -X POST '$DEPLOY_URL/api/contact' -H 'Content-Type: application/json' -d '{\"name\":\"verify\",\"email\":\"verify@example.com\",\"message\":\"verify\"}' -o /dev/null -w '%{http_code}' | grep -E '^(200|400)$'"
|
|
72
|
+
fi
|
|
73
|
+
;;
|
|
74
|
+
booking)
|
|
75
|
+
echo ""
|
|
76
|
+
echo "--- booking ---"
|
|
77
|
+
check "booking API route exists" "find src/app/api/booking -type f 2>/dev/null | head -1 | grep -q ."
|
|
78
|
+
check "DATABASE_URL referenced in code or env" "grep -r 'DATABASE_URL' src/ .env.example 2>/dev/null | head -1"
|
|
79
|
+
;;
|
|
80
|
+
saas-dashboard)
|
|
81
|
+
echo ""
|
|
82
|
+
echo "--- saas-dashboard ---"
|
|
83
|
+
check "auth middleware exists" "[ -f src/middleware.ts ] || [ -f src/middleware.js ] || [ -f middleware.ts ]"
|
|
84
|
+
check "/login or /signup page exists" "find src/app -type d \\( -name 'login' -o -name 'signup' -o -name 'auth' \\) 2>/dev/null | head -1 | grep -q ."
|
|
85
|
+
check "/dashboard page exists" "find src/app -type d -name 'dashboard' 2>/dev/null | head -1 | grep -q ."
|
|
86
|
+
if [ -n "$DEPLOY_URL" ]; then
|
|
87
|
+
check "/dashboard redirects unauthenticated" "curl -s -o /dev/null -w '%{http_code}' '$DEPLOY_URL/dashboard' | grep -E '^(301|302|307|401)$'"
|
|
88
|
+
fi
|
|
89
|
+
;;
|
|
90
|
+
blog)
|
|
91
|
+
echo ""
|
|
92
|
+
echo "--- blog ---"
|
|
93
|
+
check "content/posts/ exists" "[ -d content/posts ] || [ -d src/content/posts ]"
|
|
94
|
+
check "mdx package installed" "grep -q '@next/mdx\\|next-mdx-remote\\|@mdx-js' package.json 2>/dev/null"
|
|
95
|
+
if [ -n "$DEPLOY_URL" ]; then
|
|
96
|
+
check "/rss.xml returns 200" "curl -sI '$DEPLOY_URL/rss.xml' | head -1 | grep -q 200"
|
|
97
|
+
check "/sitemap.xml returns 200" "curl -sI '$DEPLOY_URL/sitemap.xml' | head -1 | grep -q 200"
|
|
98
|
+
fi
|
|
99
|
+
;;
|
|
100
|
+
marketing|*)
|
|
101
|
+
echo ""
|
|
102
|
+
echo "--- marketing baseline ---"
|
|
103
|
+
if [ -n "$DEPLOY_URL" ]; then
|
|
104
|
+
check "homepage has hero text" "curl -s '$DEPLOY_URL' | grep -iE '<h1' | head -1 | grep -q ."
|
|
105
|
+
fi
|
|
106
|
+
;;
|
|
107
|
+
esac
|
|
108
|
+
|
|
109
|
+
# === Visual verification reminder ===
|
|
110
|
+
echo ""
|
|
111
|
+
echo "--- visual (per CLAUDE.md Part 19) ---"
|
|
112
|
+
if [ -n "$DEPLOY_URL" ] && [ -x "scripts/screenshot.sh" ]; then
|
|
113
|
+
scripts/screenshot.sh "$DEPLOY_URL" /tmp/verify-screenshot.png 420,900 > /dev/null && \
|
|
114
|
+
echo " Screenshot: /tmp/verify-screenshot.png (READ it to complete Part 19 verification)"
|
|
115
|
+
else
|
|
116
|
+
echo " Skipped (no deploy URL or no scripts/screenshot.sh)"
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
# === Summary ===
|
|
120
|
+
echo ""
|
|
121
|
+
echo "=========================================="
|
|
122
|
+
echo " Verify summary: PASS=$PASS FAIL=$FAIL"
|
|
123
|
+
echo "=========================================="
|
|
124
|
+
|
|
125
|
+
if [ "$FAIL" -gt 0 ]; then
|
|
126
|
+
exit 1
|
|
127
|
+
fi
|
|
128
|
+
exit 0
|