@invisibleloop/pulse 0.14.1 → 0.14.2

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.
@@ -21,13 +21,17 @@
21
21
  For any **new page, landing page, or branded site**, run the intake sequence first:
22
22
 
23
23
  ```
24
- 0. pulse_extract_inspiration(url/image) → structured design brief [if user shares reference]
24
+ 0. ASK: "Do you have any design inspiration — a site you love, a screenshot, or a mood board
25
+ image? Drop images into public/intake/ or paste a URL and I'll extract the design
26
+ intent before we start." ← MANDATORY. Ask this before doing anything else.
27
+ 0b. pulse_extract_inspiration(url/image) → structured design brief [if user supplies reference]
25
28
  1. pulse_intake(name, pitch, features, ...) → product brief + contrast check
26
29
  2. pulse_sketch(brief, vibe?, antiStyle?) → 3 layout directions to choose from
27
30
  3. pulse_intent(description) → archetype + scaffold + guide list
28
31
  ```
29
32
 
30
- - **`pulse_extract_inspiration`** — call this first if the user shares a URL, site name, or image they like. Gives you a structured extraction template: visit the URL or analyse the image with your vision tools, extract colours, layout, typography, and feel, then feed those findings into pulse_intake. If no inspiration is shared, skip this step.
33
+ - **Step 0 — Ask for inspiration first (mandatory):** Before calling any tool or presenting any plan, ask the user: *"Do you have any design inspiration — a site you love, a screenshot, or a mood board image? Drop images into `public/intake/` or paste a URL and I'll extract the design intent before we start."* Wait for the answer. Do not skip this question, even if the user's brief already seems detailed. Check `public/intake/` for any images already dropped there.
34
+ - **`pulse_extract_inspiration`** — call this after step 0 if the user supplies a URL, site name, or image they like. Also call it for any images already present in `public/intake/`. Gives you a structured extraction template: visit the URL or analyse the image with your vision tools, extract colours, layout, typography, and feel, then feed those findings into pulse_intake. If the user explicitly says they have no reference, skip this step.
31
35
  - **`pulse_intake`** — captures the real content (name, pitch, features, palette, vibe, what it should NOT look like). Returns a brief with copy-ready content and early contrast warnings. Always ask the user one question at a time — never multi-choice for open-ended questions.
32
36
  - **`pulse_sketch`** — generates 3 structurally distinct layout directions (full-bleed, asymmetric split, typography-only, editorial, dense grid, story scroll, content-first). **Call this before writing any code.** Prevents defaulting to centred hero + three-column features every time.
33
37
  - **Skip `pulse_sketch` only** when the vibe is `corporate` or `minimal` AND the user hasn't expressed any structural preference. For `playful`, `bold`, `brutalist`, `retro`, or `neon` vibes, `pulse_sketch` is mandatory — templates have default section orders that actively fight these vibes.
@@ -86,7 +90,8 @@ Not every task needs all 8 phases. Match the tier to the task:
86
90
  | 1 | Intent + Understand | — |
87
91
  | 2 | Build | announce brief → write files |
88
92
  | 3 | Validate | `pulse_validate` clean |
89
- | 4 | Browser | screenshot + Lighthouse desktop + mobile (100/100/100) |
93
+ | 4a | Design approval | screenshot → show user → **wait for approval** *(new-build only)* |
94
+ | 4b | Verify | `pulse_design_review` (if intake ran) → `pulse_layout_review` → Lighthouse desktop + mobile (100/100/100) |
90
95
 
91
96
  No tests required for pure view specs with no logic. Add tests if the view has non-trivial helper functions.
92
97
 
@@ -100,15 +105,16 @@ No tests required for pure view specs with no logic. Add tests if the view has n
100
105
  | 2 | Plan | user confirmation (skip if unambiguous) |
101
106
  | 3 | Build | announce brief → write files |
102
107
  | 4 | Validate | `pulse_validate` clean |
103
- | 5 | Browser | screenshot → `pulse_design_review` (if intake ran) → `pulse_layout_review` → Lighthouse desktop + mobile (100/100/100) + CLS 0.00 |
108
+ | 5a | Design approval | screenshot → show user → **wait for approval** *(new-build only)* |
109
+ | 5b | Verify | `pulse_design_review` (if intake ran) → `pulse_layout_review` → Lighthouse desktop + mobile (100/100/100) + CLS 0.00 |
104
110
  | 6 | Tests | mutations, view landmarks, any utility functions |
105
- | 7 | Code review | `pulse_review` — only after 4–6 pass |
111
+ | 7 | Code review | `pulse_review` — only after 5–6 pass |
106
112
 
107
113
  ### Tier 3 — Complex (multi-step, shared state, or security-sensitive)
108
114
 
109
115
  *Applies to:* multi-step wizards, pages with guards and auth, store-connected pages, anything touching sensitive data or complex branching state.
110
116
 
111
- All phases as originally defined — full 8-phase flow with every gate.
117
+ All phases as originally defined — full flow with every gate.
112
118
 
113
119
  | Phase | Action | Gate |
114
120
  |---|---|---|
@@ -116,7 +122,8 @@ All phases as originally defined — full 8-phase flow with every gate.
116
122
  | 2 | Plan | **user confirmation required** |
117
123
  | 3 | Build | announce brief → write files |
118
124
  | 4 | Validate | `pulse_validate` clean |
119
- | 5 | Browser | screenshot → `pulse_design_review` (if intake ran) → `pulse_layout_review` → Lighthouse 100/100/100 + CLS 0.00, desktop + mobile |
125
+ | 5a | Design approval | screenshot → show user → **wait for approval** *(new-build only)* |
126
+ | 5b | Verify | `pulse_design_review` (if intake ran) → `pulse_layout_review` → Lighthouse 100/100/100 + CLS 0.00, desktop + mobile |
120
127
  | 6 | Tests | full coverage: mutations, actions, guards, view |
121
128
  | 7 | Code review | `pulse_review` |
122
129
  | 8 | Fix + re-verify | re-run all gates |
@@ -209,7 +216,7 @@ Run `pulse_validate` on the spec file.
209
216
 
210
217
  ---
211
218
 
212
- ## Phase 5 — Browser check (pass gate)
219
+ ## Phase 5 — Browser check
213
220
 
214
221
  **After every file edit, restart the server and reload the browser before checking anything.** The dev server auto-restarts on file changes but the browser tab stays stale. Always:
215
222
  1. Call `pulse_restart_server` — waits for the server to be ready
@@ -217,15 +224,40 @@ Run `pulse_validate` on the spec file.
217
224
 
218
225
  Do this before every screenshot, console check, or visual inspection. Never debug a problem that might just be a stale tab.
219
226
 
220
- **Run `/verify` now — as soon as the first build is working.** Do not defer it to the end. Do not execute these steps manually.
227
+ ---
228
+
229
+ ### Phase 5a — Design approval *(new builds from `pulse_intake` only)*
230
+
231
+ **This phase applies when `pulse_intake` ran** — i.e. you are building a new page or site from scratch, not editing an existing one. For edits, bug fixes, or "add X to existing Y", skip to Phase 5b immediately.
232
+
233
+ 1. Take a screenshot of the built page
234
+ 2. Describe what you see in 2–3 plain sentences — layout structure, visual tone, key sections. Do NOT list features or code details.
235
+ 3. **Ask the user directly:** *"Happy with the layout and direction, or any changes before I run tests and Lighthouse?"*
236
+ 4. **Wait for their response before doing anything else.** Do not run design_review, layout_review, or Lighthouse until you have a clear go-ahead.
237
+
238
+ **If the user requests changes:**
239
+ - Make the edits
240
+ - Take a new screenshot
241
+ - Describe what changed
242
+ - Ask again
243
+ - Repeat until approved — each iteration is design-only, no Lighthouse between rounds
244
+
245
+ **If the user approves (or says "looks good", "go ahead", "yes", etc.):**
246
+ - Continue to Phase 5b
247
+
248
+ > The point of this gate is to let the user redirect the design before expensive, slow checks run. A Lighthouse build takes ~90 seconds — do not start it until the user has seen and approved the visual result.
249
+
250
+ ---
251
+
252
+ ### Phase 5b — Full verification (pass gate)
253
+
254
+ **Run `/verify` now.** Do not execute these steps manually.
221
255
 
222
256
  `/verify` validates, screenshots, runs Lighthouse (desktop + mobile), runs the performance trace (LCP + CLS), checks console errors, runs `pulse_review`, and writes the `.pulse-verified` stamp. Running steps manually does not write the stamp.
223
257
 
224
258
  > **Lighthouse pre-flight:** `/verify` calls `pulse_build` automatically before running Lighthouse, and audits against the production server on port 3001. If you ever call `lighthouse_audit` manually outside `/verify`, you must first run `pulse_build` and navigate to `http://localhost:3001/your-route`. Auditing the dev server (port 3000) will produce misleading results.
225
259
 
226
- ### ⛔ After every screenshot — mandatory steps before continuing
227
-
228
- Taking a screenshot is NOT the end of phase 5. After every screenshot, you **must** run:
260
+ ### ⛔ After design approval — mandatory steps before continuing
229
261
 
230
262
  1. `pulse_design_review` (if `pulse_intake` ran) — work through all 7 signals, fix any Fail
231
263
  2. `pulse_layout_review <url>` — 390/768/1280px overflow, broken images, collapsed sections
@@ -234,7 +266,7 @@ Taking a screenshot is NOT the end of phase 5. After every screenshot, you **mus
234
266
  **Do not report the page as done until `/verify` writes the `.pulse-verified` stamp.** Skipping Lighthouse means broken contrast, missing landmarks, and Best Practices failures ship to the user.
235
267
 
236
268
  Pass gates:
237
- - Screenshot: no layout or rendering issues
269
+ - Design approval: user has explicitly said they're happy with the layout
238
270
  - Design review: all 7 signals pass (or intake didn't run)
239
271
  - Layout review: no overflow, broken images, or collapsed sections at any viewport
240
272
  - Lighthouse desktop: Accessibility, Best Practices, SEO all 100
@@ -282,7 +314,9 @@ Only declare the task done after all gates still pass.
282
314
 
283
315
  ```
284
316
  New project / new page:
285
- pulse_extract_inspiration → (if user shares a URL or image) extract palette/layout/feel
317
+ ASK first → "Do you have any design inspiration — a site, screenshot, or
318
+ mood board? Drop into public/intake/ or paste a URL."
319
+ pulse_extract_inspiration → (if user supplies a URL or image, or images found in public/intake/)
286
320
  pulse_intake → capture real content, palette, vibe, anti-style
287
321
  pulse_sketch → choose 1 of 3 structural layout directions
288
322
  pulse_intent → get scaffold + guide list
@@ -293,8 +327,10 @@ Targeted edit / bug fix:
293
327
  Step 1 Understand pulse_list_structure + relevant guides
294
328
  Step 2 Plan confirm with user if non-trivial
295
329
 
296
- Tier 1 Simple → phases 3 → 4 → 5
297
- Tier 2 Standard → phases 3 → 4 → 5 → 6 → 7
298
- Tier 3 Complex → phases 3 → 4 → 5 → 6 → 7 → 8
330
+ Tier 1 Simple → phases 3 → 4 → 5a (design approval) → 5b (verify)
331
+ Tier 2 Standard → phases 3 → 4 → 5a (design approval) → 5b (verify) → 6 → 7
332
+ Tier 3 Complex → phases 3 → 4 → 5a (design approval) → 5b (verify) → 6 → 7 → 8
333
+
334
+ Phase 5a is skipped for edits, bug fixes, and "add X to existing Y".
299
335
  ```
300
336
 
package/src/cli/dev.js CHANGED
@@ -12,6 +12,7 @@ import path from 'path'
12
12
  import fs from 'fs'
13
13
  import { createServer } from '../server/index.js'
14
14
  import { loadPages } from './discover.js'
15
+ import * as log from './logger.js'
15
16
 
16
17
  // ---------------------------------------------------------------------------
17
18
  // Args
@@ -106,19 +107,13 @@ const PUBLIC_DIR = path.join(ROOT, 'public')
106
107
  // Start
107
108
  // ---------------------------------------------------------------------------
108
109
 
109
- console.log(`⚡ Pulse dev server starting...\n`)
110
-
111
110
  const specs = await loadPages(ROOT)
112
111
 
113
112
  if (specs.length === 0) {
114
- console.error('No pages found in src/pages/. Create a page to get started.')
113
+ log.error('No pages found in src/pages/. Create a page to get started.')
115
114
  process.exit(1)
116
115
  }
117
116
 
118
- console.log('Pages:\n')
119
- specs.forEach(spec => console.log(` ${spec.route}`))
120
- console.log()
121
-
122
117
  // ---------------------------------------------------------------------------
123
118
  // Hot reload — SSE clients + file watcher
124
119
  // ---------------------------------------------------------------------------
@@ -148,19 +143,16 @@ let lastReload = 0
148
143
  async function triggerReload(label = 'File changed') {
149
144
  clearTimeout(reloadTimer)
150
145
  reloadTimer = setTimeout(async () => {
151
- // Debounce rapid file changes (e.g. multiple Edit tool calls)
152
146
  const now = Date.now()
153
147
  if (now - lastReload < 100) return
154
148
  lastReload = now
155
-
156
- console.log(` ⟳ ${label}, reloading specs...`)
149
+
157
150
  try {
158
151
  const fresh = await loadPages(ROOT, now)
159
152
  updateSpecs(fresh)
160
- console.log(' ✓ Specs reloaded')
153
+ log.info(`${label} — specs reloaded`)
161
154
  } catch (err) {
162
- console.error(' ✗ Spec reload failed:', err.message)
163
- /* spec error — browser will show the old page, not crash */
155
+ log.error(`Spec reload failed: ${err.message}`)
164
156
  }
165
157
  notifyReload()
166
158
  }, 200)
@@ -199,6 +191,7 @@ const { updateSpecs } = await createServer(specs, {
199
191
  manifest: {}, // never use a build manifest in dev — always serve source files
200
192
  extraBody: reloadScript,
201
193
  dev: true,
194
+ agentMode: !!process.env.PULSE_AGENT_MODE,
202
195
  ...(_config.csp ? { csp: _config.csp } : {}),
203
196
 
204
197
  onRequest(req, res) {
package/src/cli/index.js CHANGED
@@ -33,13 +33,6 @@ for (let i = 0; i < rawArgs.length; i++) {
33
33
  const command = args[0]
34
34
  const CWD = process.cwd()
35
35
 
36
- // Parse --agent flag before routing to commands
37
- let agentFlag = null
38
- const agentIdx = args.indexOf('--agent')
39
- if (agentIdx !== -1 && args[agentIdx + 1]) {
40
- agentFlag = args[agentIdx + 1]
41
- }
42
-
43
36
  // ---------------------------------------------------------------------------
44
37
  // Helpers
45
38
  // ---------------------------------------------------------------------------
@@ -176,13 +169,14 @@ async function launchSession(root, agentOverride = null) {
176
169
  }
177
170
  }
178
171
 
179
- console.log(`\n⚡ Pulse project: ${root}`)
180
- console.log(` Use /pulse-dev to start the dev server, /pulse-stop to stop it, /pulse-build to build, /pulse-start to run production.`)
181
- console.log(` Tell me what you'd like to build — a new page, a component, a form, or anything else.\n`)
172
+ console.log(`\n ⚡ Pulse → ${root}`)
173
+ console.log(` ${agent === 'copilot' ? '● Copilot' : '● Claude'} agent starting — tell me what you'd like to build.\n`)
182
174
 
183
175
  if (agent === 'copilot') {
176
+ process.env.PULSE_AGENT_MODE = '1'
184
177
  await launchCopilotSession(root, mcpServerPath, spawn, os.default)
185
178
  } else {
179
+ process.env.PULSE_AGENT_MODE = '1'
186
180
  await launchClaudeSession(root, mcpConfig, spawn, os.default)
187
181
  }
188
182
  }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Pulse CLI — structured terminal logger
3
+ *
4
+ * Single module that owns all terminal output. Produces clean, scannable
5
+ * output: one-time startup banner, then a timestamped scrolling log.
6
+ *
7
+ * Rules:
8
+ * - Static asset requests (JS, CSS, images, fonts) are dimmed and collapsed
9
+ * - Errors always stand out (red ✗ prefix)
10
+ * - Agent activity gets a distinct indicator
11
+ * - Respects NO_COLOR and CI env vars
12
+ */
13
+
14
+ import { c, icon, elapsed, visLen } from './fmt.js'
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Helpers
18
+ // ---------------------------------------------------------------------------
19
+
20
+ const STATIC_EXT = /\.(js|css|map|ico|png|jpg|jpeg|gif|svg|woff2?|ttf|eot|webp|avif|json)$/i
21
+ const STATIC_PATH = /^\/(dist|fonts|images|public)\//
22
+
23
+ function isStatic(pathname) {
24
+ return STATIC_EXT.test(pathname) || STATIC_PATH.test(pathname)
25
+ }
26
+
27
+ function timestamp() {
28
+ return c.dim(new Date().toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' }))
29
+ }
30
+
31
+ function statusColor(code) {
32
+ if (code >= 500) return c.red(String(code))
33
+ if (code >= 400) return c.yellow(String(code))
34
+ if (code >= 300) return c.cyan(String(code))
35
+ return c.dim(String(code))
36
+ }
37
+
38
+ function methodColor(method) {
39
+ if (method === 'POST' || method === 'PUT' || method === 'PATCH') return c.yellow(method.padEnd(4))
40
+ if (method === 'DELETE') return c.red(method.padEnd(4))
41
+ return c.dim(method.padEnd(4))
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Banner — printed once at startup
46
+ // ---------------------------------------------------------------------------
47
+
48
+ /**
49
+ * Print the startup banner.
50
+ * @param {{ url: string, version: string, specs: Array<{route:string, method?:string, hydrate?:string}>, agentMode?: boolean }} opts
51
+ */
52
+ export function banner({ url, version, specs = [], agentMode = false }) {
53
+ const agentLabel = agentMode ? ` ${c.cyan('●')} ${c.dim('Agent active')}` : ''
54
+
55
+ process.stdout.write('\n')
56
+ process.stdout.write(` ${icon.bolt()} ${c.bold('Pulse')} ${c.dim(`v${version}`)} ${c.dim('→')} ${c.cyan(url)}${agentLabel}\n`)
57
+ process.stdout.write('\n')
58
+
59
+ if (specs.length > 0) {
60
+ // Column widths
61
+ const routeW = Math.max(5, ...specs.map(s => (s.route || '?').length))
62
+ const methodW = 6
63
+
64
+ const header = ` ${c.dim('Route'.padEnd(routeW))} ${c.dim('Method'.padEnd(methodW))} ${c.dim('JS')}`
65
+ const divider = ` ${c.dim('─'.repeat(routeW + methodW + 8))}`
66
+
67
+ process.stdout.write(header + '\n')
68
+ process.stdout.write(divider + '\n')
69
+
70
+ for (const s of specs) {
71
+ const route = (s.route || '?').padEnd(routeW)
72
+ const method = (s.method?.toUpperCase() || 'GET').padEnd(methodW)
73
+ const hydrate = s.hydrate ? c.green('✓ hydrated') : c.dim('— static')
74
+ process.stdout.write(` ${c.cyan(route)} ${c.dim(method)} ${hydrate}\n`)
75
+ }
76
+
77
+ process.stdout.write('\n')
78
+ }
79
+
80
+ process.stdout.write(c.dim(' Ready. Watching for changes…\n'))
81
+ process.stdout.write(c.dim(' ' + '─'.repeat(48) + '\n'))
82
+ process.stdout.write('\n')
83
+ }
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // Request log — one line per request
87
+ // ---------------------------------------------------------------------------
88
+
89
+ /**
90
+ * Log a completed HTTP request.
91
+ * @param {{ method: string, pathname: string, status: number, ms: number }} opts
92
+ */
93
+ export function request({ method, pathname, status, ms }) {
94
+ const isAsset = isStatic(pathname)
95
+
96
+ // Skip health check noise
97
+ if (pathname === '/healthz' || pathname === '/_pulse/health') return
98
+
99
+ const ts = timestamp()
100
+ const m = methodColor(method)
101
+ const p = isAsset ? c.dim(pathname) : pathname
102
+ const s = statusColor(status)
103
+ const t = c.dim(elapsed(ms))
104
+
105
+ if (isAsset) {
106
+ // Static assets — dim the whole line, no leading symbol
107
+ process.stdout.write(`${ts} ${c.dim(method.padEnd(4))} ${p} ${c.dim(String(status))} ${t}\n`)
108
+ } else if (status >= 500) {
109
+ process.stdout.write(`${ts} ${icon.fail()} ${m} ${c.red(pathname)} ${s} ${t}\n`)
110
+ } else if (status >= 400) {
111
+ process.stdout.write(`${ts} ${icon.warn()} ${m} ${c.yellow(pathname)} ${s} ${t}\n`)
112
+ } else {
113
+ process.stdout.write(`${ts} ${icon.info()} ${m} ${p} ${s} ${t}\n`)
114
+ }
115
+ }
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // Named events — server lifecycle
119
+ // ---------------------------------------------------------------------------
120
+
121
+ export function ready(url) {
122
+ process.stdout.write(`${timestamp()} ${icon.ok()} ${c.dim('Server ready')} ${c.dim(url)}\n`)
123
+ }
124
+
125
+ export function shutdown(forced = false) {
126
+ process.stdout.write('\n')
127
+ if (forced) {
128
+ process.stdout.write(` ${icon.fail()} ${c.red('Force-exiting after shutdown timeout')}\n`)
129
+ } else {
130
+ process.stdout.write(` ${icon.info()} ${c.dim('Shutting down gracefully…')}\n`)
131
+ }
132
+ }
133
+
134
+ export function error(msg, err) {
135
+ process.stdout.write(`${timestamp()} ${icon.fail()} ${c.red(msg)}\n`)
136
+ if (err?.stack) process.stdout.write(c.dim(err.stack.split('\n').slice(1, 4).map(l => ' ' + l).join('\n')) + '\n')
137
+ }
138
+
139
+ export function info(msg) {
140
+ process.stdout.write(`${timestamp()} ${icon.info()} ${c.dim(msg)}\n`)
141
+ }
142
+
143
+ export function warn(msg) {
144
+ process.stdout.write(`${timestamp()} ${icon.warn()} ${c.yellow(msg)}\n`)
145
+ }
@@ -27,6 +27,7 @@ export async function scaffold(targetDir, options = {}) {
27
27
  fs.mkdirSync(path.join(targetDir, 'src', 'pages'), { recursive: true })
28
28
  fs.mkdirSync(path.join(targetDir, 'src', 'components'), { recursive: true })
29
29
  fs.mkdirSync(path.join(targetDir, 'public'), { recursive: true })
30
+ fs.mkdirSync(path.join(targetDir, 'public', 'intake'), { recursive: true })
30
31
 
31
32
  // package.json
32
33
  write(targetDir, 'package.json', JSON.stringify({
@@ -62,6 +63,9 @@ ${port !== 3000 ? ` port: ${port},\n` : ''}${agent && agent !== 'claude' ? ` a
62
63
  write(targetDir, 'public/theme.css', themeCSS())
63
64
  write(targetDir, 'public/app.css', baseCSS())
64
65
 
66
+ // Intake directory — drop inspiration images here before running pulse_intake
67
+ write(targetDir, 'public/intake/README.md', intakeReadme())
68
+
65
69
  // Copy pulse-ui assets from the package into public/ so they're served immediately
66
70
  const pkgPublic = new URL('../../public', import.meta.url).pathname
67
71
  for (const asset of ['pulse-ui.css', 'pulse-ui.js']) {
@@ -124,13 +128,18 @@ ${port !== 3000 ? ` port: ${port},\n` : ''}${agent && agent !== 'claude' ? ` a
124
128
  hooks: [
125
129
  {
126
130
  type: 'command',
127
- command: `node -e "process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:'SessionStart',additionalContext:'START OF SESSION: Before doing anything else — (1) run pulse_list_structure to see the current project structure, (2) read pulse://guide from MCP for the complete Pulse reference. Both are mandatory. Do not skip them.'}}))"`,
131
+ command: `node -e "process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:'SessionStart',additionalContext:'START OF SESSION: Before doing anything else — (1) run pulse_list_structure to see the current project structure, (2) read pulse://guide from MCP for the complete Pulse reference. Both are mandatory. Do not skip them.\\n\\nBUILD RULE — ALWAYS ASK BEFORE PLANNING: If the user is requesting a new page or site, your FIRST action must be to ask: \\'Do you have any design inspiration — a site you love, a screenshot, a mood board image? Drop images into public/intake/ or paste a URL and I will extract the design intent before we start.\\' Do not present a plan or write any code until you have asked this question and received an answer. This question is not optional and cannot be skipped.'}}))"`,
128
132
  statusMessage: 'Loading Pulse session...',
129
133
  },
130
134
  {
131
135
  type: 'command',
132
136
  command: `node -e "const{execSync}=require('child_process');let out='';let ok=true;try{out=execSync('npm test 2>&1',{encoding:'utf8',timeout:60000});}catch(e){ok=false;out=e.stdout||e.message;}const lines=out.trim().split('\\n');const summary=lines[lines.length-1]||'';const msg=ok?'TEST BASELINE — all tests passing: '+summary+'. Note this result. If tests fail after your changes, you introduced the regression.':'PRE-EXISTING TEST FAILURES detected before you wrote any code:\\n'+out.slice(-2000)+'\\nDo not treat these as regressions you caused. Fix them first if they are related to your task, otherwise note them and proceed.';process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:'SessionStart',additionalContext:msg}}))"`,
133
137
  statusMessage: 'Running test baseline...',
138
+ },
139
+ {
140
+ type: 'command',
141
+ command: `node -e "const fs=require('fs');const path=require('path');const intakeDir='public/intake';let images=[];try{images=fs.readdirSync(intakeDir).filter(f=>/\\.(jpe?g|png|gif|webp|avif)$/i.test(f));}catch{}if(!images.length){process.exit(0);}const list=images.map(f=>' • '+f).join('\\n');process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:'SessionStart',additionalContext:'DESIGN REFERENCES FOUND in public/intake/:\\n'+list+'\\n\\nBefore running pulse_intake, call pulse_extract_inspiration on each image to extract palette, vibe, layout density, and type signals. This gives the design much more precision than text descriptions alone.\\n\\nWorkflow: pulse_extract_inspiration(image) → fill in the template using your vision → feed results into pulse_intake(inspiration: \\'...\\')'}}));"`,
142
+ statusMessage: 'Checking for design references...',
134
143
  }
135
144
  ]
136
145
  }
@@ -209,6 +218,13 @@ ${port !== 3000 ? ` port: ${port},\n` : ''}${agent && agent !== 'claude' ? ` a
209
218
  'public/dist',
210
219
  '.pulse-build',
211
220
  '.DS_Store',
221
+ '# Intake images — local design references, not committed',
222
+ 'public/intake/*.jpg',
223
+ 'public/intake/*.jpeg',
224
+ 'public/intake/*.png',
225
+ 'public/intake/*.gif',
226
+ 'public/intake/*.webp',
227
+ 'public/intake/*.avif',
212
228
  ].join('\n') + '\n')
213
229
 
214
230
  console.log(' ✓ Project files created')
@@ -349,6 +365,42 @@ function themeCSS() {
349
365
  `
350
366
  }
351
367
 
368
+ function intakeReadme() {
369
+ return `\
370
+ # Design References
371
+
372
+ Drop inspiration images here before starting a new build.
373
+
374
+ The session start hook will detect them and remind the agent to run
375
+ \`pulse_extract_inspiration\` on each one before \`pulse_intake\`.
376
+
377
+ ## What to put here
378
+
379
+ - Screenshots of sites whose design you admire
380
+ - Your brand mood board or style guide
381
+ - Product photography that anchors the visual direction
382
+ - Competitor sites you want to differentiate from
383
+ - Any image that captures the feeling you're going for
384
+
385
+ ## How the agent uses them
386
+
387
+ 1. \`pulse_extract_inspiration(image)\` — reads the image and extracts palette,
388
+ type weight, spacing density, radius signals, and layout intent
389
+ 2. The extracted values feed into \`pulse_intake\` as concrete constraints
390
+ 3. \`pulse_sketch\` uses them to generate layout directions matched to the actual
391
+ visual reference, not just a text description of a vibe
392
+
393
+ ## File formats
394
+
395
+ JPEG, PNG, WebP, AVIF, GIF — any image the model can read.
396
+
397
+ ## Note
398
+
399
+ These files are listed in .gitignore and will not be committed.
400
+ Only this README is tracked.
401
+ `
402
+ }
403
+
352
404
  function baseCSS() {
353
405
  return `\
354
406
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
@@ -377,12 +429,14 @@ src/pages/ ← one .js file per page, auto-discovered
377
429
  src/components/ ← shared view fragments (JS functions returning HTML strings)
378
430
  public/theme.css ← token definitions (hex values, colour overrides) — hex allowed here only
379
431
  public/app.css ← layout and component overrides — var() references only, no hex
432
+ public/intake/ ← drop design reference images here before starting (not committed to git)
380
433
  \`\`\`
381
434
 
382
435
  ## Start of every session
383
436
 
384
437
  1. Run \`pulse_list_structure\` to see what already exists
385
438
  2. Read \`pulse://guide\` from MCP — the complete reference for spec format, components, verification workflow, CSS rules, and patterns
439
+ 3. If images are present in \`public/intake/\`, call \`pulse_extract_inspiration\` on each before \`pulse_intake\` — the session hook will remind you if you forget
386
440
 
387
441
  The MCP guide is the single source of truth. Follow it for all technical decisions, component usage, and the mandatory verification workflow.
388
442