@invisibleloop/pulse 0.12.3 → 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.
Files changed (181) hide show
  1. package/.claude/commands/build-page.md +5 -1
  2. package/.claude/commands/verify.md +31 -17
  3. package/.claude/pulse-checklist.md +21 -1
  4. package/.github/workflows/publish.yml +1 -0
  5. package/.pulse-verified +1 -0
  6. package/CLAUDE.md +56 -15
  7. package/docs/.claude/commands/verify.md +4 -0
  8. package/docs/.claude/pulse-checklist.md +20 -2
  9. package/docs/public/.pulse-ui-version +1 -1
  10. package/docs/public/andypixel.css +237 -0
  11. package/docs/public/bookworms.css +422 -0
  12. package/docs/public/coutumes.css +584 -0
  13. package/docs/public/images/bookworms/hero-books.jpg +0 -0
  14. package/docs/public/images/bookworms/how-we-read-now.jpg +0 -0
  15. package/docs/public/images/bookworms/lantern-keepers.jpg +0 -0
  16. package/docs/public/images/bookworms/provincial-bookseller.jpg +0 -0
  17. package/docs/public/images/journey/178.jpg +0 -0
  18. package/docs/public/images/journey/29.jpg +0 -0
  19. package/docs/public/images/retrowheels/hero.jpg +0 -0
  20. package/docs/public/images/shop/devbook-pro.jpg +0 -0
  21. package/docs/public/images/suzukibento/1015.jpg +0 -0
  22. package/docs/public/images/suzukibento/1025.jpg +0 -0
  23. package/docs/public/images/suzukibento/1043.jpg +0 -0
  24. package/docs/public/images/suzukibento/1059.jpg +0 -0
  25. package/docs/public/images/suzukibento/1062.jpg +0 -0
  26. package/docs/public/images/suzukibento/1084.jpg +0 -0
  27. package/docs/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
  28. package/docs/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
  29. package/docs/public/images/swish/1485518882345-15568b007407.jpg +0 -0
  30. package/docs/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
  31. package/docs/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
  32. package/docs/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
  33. package/docs/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
  34. package/docs/public/images/swish/1551803091-e20673f15770.jpg +0 -0
  35. package/docs/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
  36. package/docs/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
  37. package/docs/public/journey.css +528 -0
  38. package/docs/public/pulse-ui.css +1036 -22
  39. package/docs/public/retrowheels.css +402 -0
  40. package/docs/public/shop.css +235 -0
  41. package/docs/public/sparkys.css +349 -0
  42. package/docs/public/suzukibento.css +517 -0
  43. package/docs/public/swish.css +394 -0
  44. package/docs/server.js +19 -4
  45. package/docs/src/lib/nav.js +14 -3
  46. package/docs/src/pages/components/decorate.js +74 -0
  47. package/docs/src/pages/components/gallery.js +102 -0
  48. package/docs/src/pages/components/hero.js +1 -0
  49. package/docs/src/pages/components/marquee.js +76 -0
  50. package/docs/src/pages/components/photo-card.js +74 -0
  51. package/docs/src/pages/components/section.js +40 -1
  52. package/docs/src/pages/meta.js +26 -0
  53. package/docs/src/pages/templates/andypixel.js +260 -0
  54. package/docs/src/pages/templates/bookworms.js +227 -0
  55. package/docs/src/pages/templates/coutumes.js +226 -0
  56. package/docs/src/pages/templates/journey.js +186 -0
  57. package/docs/src/pages/templates/retrowheels.js +342 -0
  58. package/docs/src/pages/templates/shop.js +257 -0
  59. package/docs/src/pages/templates/sparkys.js +249 -0
  60. package/docs/src/pages/templates/suzukibento.js +200 -0
  61. package/docs/src/pages/templates/swish.js +313 -0
  62. package/docs/src/pages/templates.js +313 -2
  63. package/docs/src/pages/testing.js +2 -1
  64. package/examples/.claude/commands/pulse-build.md +7 -0
  65. package/examples/.claude/commands/pulse-dev.md +7 -0
  66. package/examples/.claude/commands/pulse-start.md +9 -0
  67. package/examples/.claude/commands/pulse-stop.md +5 -0
  68. package/examples/.claude/commands/verify.md +84 -0
  69. package/examples/.claude/coverage-check.js +127 -0
  70. package/examples/.claude/pulse-checklist.md +177 -0
  71. package/examples/andypixel.css +237 -0
  72. package/examples/andypixel.js +258 -0
  73. package/examples/bookworms.css +422 -0
  74. package/examples/bookworms.js +225 -0
  75. package/examples/contact.js +1 -0
  76. package/examples/counter.js +1 -0
  77. package/examples/dev.server.js +17 -0
  78. package/examples/journey.css +528 -0
  79. package/examples/journey.js +186 -0
  80. package/examples/pricing.js +1 -0
  81. package/examples/public/.pulse-ui-version +1 -0
  82. package/examples/public/pulse-ui.css +4031 -0
  83. package/examples/public/pulse-ui.js +257 -0
  84. package/examples/quiz.js +1 -0
  85. package/examples/retrowheels.css +402 -0
  86. package/examples/retrowheels.js +340 -0
  87. package/examples/shared.js +6 -0
  88. package/examples/suzukibento.css +513 -0
  89. package/examples/suzukibento.js +200 -0
  90. package/examples/swish.css +394 -0
  91. package/examples/swish.js +311 -0
  92. package/examples/todos.js +1 -0
  93. package/package.json +2 -3
  94. package/public/.pulse-ui-version +1 -1
  95. package/public/images/bookworms/hero-books.jpg +0 -0
  96. package/public/images/bookworms/how-we-read-now.jpg +0 -0
  97. package/public/images/bookworms/lantern-keepers.jpg +0 -0
  98. package/public/images/bookworms/provincial-bookseller.jpg +0 -0
  99. package/public/images/journey/178.jpg +0 -0
  100. package/public/images/journey/29.jpg +0 -0
  101. package/public/images/retrowheels/hero.jpg +0 -0
  102. package/public/images/shop/devbook-pro.jpg +0 -0
  103. package/public/images/suzukibento/1015.jpg +0 -0
  104. package/public/images/suzukibento/1025.jpg +0 -0
  105. package/public/images/suzukibento/1043.jpg +0 -0
  106. package/public/images/suzukibento/1059.jpg +0 -0
  107. package/public/images/suzukibento/1062.jpg +0 -0
  108. package/public/images/suzukibento/1084.jpg +0 -0
  109. package/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
  110. package/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
  111. package/public/images/swish/1485518882345-15568b007407.jpg +0 -0
  112. package/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
  113. package/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
  114. package/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
  115. package/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
  116. package/public/images/swish/1551803091-e20673f15770.jpg +0 -0
  117. package/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
  118. package/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
  119. package/public/pulse-ui.css +1080 -22
  120. package/scripts/build.js +72 -18
  121. package/src/agent/checklist.md +22 -2
  122. package/src/agent/commands/verify.md +4 -0
  123. package/src/agent/guide-components.md +146 -8
  124. package/src/agent/guide-design-gallery.md +288 -0
  125. package/src/agent/guide-design-references.md +172 -0
  126. package/src/agent/guide-explore.md +313 -0
  127. package/src/agent/guide-styles.md +212 -11
  128. package/src/agent/guide-templates.md +223 -10
  129. package/src/agent/guide.md +9 -4
  130. package/src/agent/identity.md +2 -1
  131. package/src/agent/skills/verify/SKILL.md +64 -28
  132. package/src/agent/workflow.md +247 -61
  133. package/src/cli/dev.js +39 -14
  134. package/src/cli/fmt.js +113 -0
  135. package/src/cli/index.js +60 -33
  136. package/src/cli/logger.js +145 -0
  137. package/src/cli/scaffold.js +57 -3
  138. package/src/html.js +6 -1
  139. package/src/mcp/server.js +2561 -99
  140. package/src/mcp/validate-worker.js +49 -5
  141. package/src/runtime/ssr.js +4 -1
  142. package/src/server/index.js +29 -5
  143. package/src/spec/schema.js +21 -0
  144. package/src/spec/schema.test.js +28 -0
  145. package/src/testing/html.js +130 -4
  146. package/src/testing/testing.test.js +58 -0
  147. package/src/ui/banner.js +24 -9
  148. package/src/ui/brand-statement.js +58 -0
  149. package/src/ui/button.js +13 -2
  150. package/src/ui/card.js +20 -1
  151. package/src/ui/colophon.js +29 -0
  152. package/src/ui/contact-row.js +41 -0
  153. package/src/ui/cta.js +18 -4
  154. package/src/ui/decorate.js +55 -0
  155. package/src/ui/display-heading.js +44 -0
  156. package/src/ui/drop-cap.js +29 -0
  157. package/src/ui/editorial-hero.js +63 -0
  158. package/src/ui/entry-list.js +36 -0
  159. package/src/ui/feature.js +20 -3
  160. package/src/ui/footer.js +78 -9
  161. package/src/ui/gallery.js +56 -0
  162. package/src/ui/hero.js +68 -18
  163. package/src/ui/image-pair.js +72 -0
  164. package/src/ui/index.js +20 -0
  165. package/src/ui/input.js +1 -0
  166. package/src/ui/marquee.js +48 -0
  167. package/src/ui/nav.js +19 -9
  168. package/src/ui/photo-card.js +44 -0
  169. package/src/ui/product-card.js +65 -0
  170. package/src/ui/pullquote.js +14 -3
  171. package/src/ui/search.js +1 -0
  172. package/src/ui/section-label.js +38 -0
  173. package/src/ui/section.js +1 -1
  174. package/src/ui/service-card.js +47 -0
  175. package/src/ui/stat-grid.js +40 -0
  176. package/src/ui/stat.js +16 -2
  177. package/src/ui/testimonial.js +17 -6
  178. package/src/ui/textarea.js +1 -0
  179. package/src/ui/top-strip.js +47 -0
  180. package/src/ui/trust-list.js +46 -0
  181. package/src/ui/ui.test.js +35 -0
@@ -1,57 +1,193 @@
1
1
  # Build Workflow
2
2
 
3
- Every task follows this sequence exactly. Each phase has a pass gate — you do not move to the next phase until the gate is cleared. Do not skip phases. Do not reorder them.
3
+ ---
4
+
5
+ ## Quick decision tree
6
+
7
+ | Situation | Start here |
8
+ |---|---|
9
+ | New site, new branded page, or "build me X from scratch" | `pulse_intake` → `pulse_sketch` → `pulse_intent` → build |
10
+ | Editing an existing page, adding a section, fixing a bug | Go straight to Step 1 — Understand |
11
+ | "Add X to the existing Y" (one-liner) | Read the relevant file, make the change, run `/verify` |
12
+ | Stuck on which component to use | `pulse_intent("describe what I'm building")` |
13
+ | Colours failing contrast / unsure about palette | `pulse_check_contrast` |
14
+ | Something looks wrong in the browser | `pulse_restart_server`, then `/verify` |
15
+ | Lighthouse < 100 | Fix the flagged audit, run `/verify` again — do not skip to commit |
16
+
17
+ ---
18
+
19
+ ## New project or new page? Start with Intake
20
+
21
+ For any **new page, landing page, or branded site**, run the intake sequence first:
22
+
23
+ ```
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]
28
+ 1. pulse_intake(name, pitch, features, ...) → product brief + contrast check
29
+ 2. pulse_sketch(brief, vibe?, antiStyle?) → 3 layout directions to choose from
30
+ 3. pulse_intent(description) → archetype + scaffold + guide list
31
+ ```
32
+
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.
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.
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.
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.
38
+ - **`pulse_intent`** — maps the chosen direction to a spec scaffold and tells you which guides to read.
39
+
40
+ For **small edits, bug fixes, or "add X to existing Y"** — skip intake/sketch and go straight to Step 0 below.
4
41
 
5
42
  ---
6
43
 
7
- ## Phase 1 — Understand
44
+ ## Step 0 — Intent (always first for non-trivial builds)
45
+
46
+ After intake + sketch (or directly for targeted tasks), call `pulse_intent`:
47
+
48
+ ```
49
+ pulse_intent("a settings page with profile editing and a save action")
50
+ ```
51
+
52
+ It returns a matched archetype, recommended components, a starter spec scaffold, and which guide sections to read. This replaces hunting through guide sections manually.
53
+
54
+ If the user's request is already very specific (a one-liner edit, a bug fix, "add X to the existing Y"), skip this and go straight to Understand.
8
55
 
9
- Before writing a single line of code:
56
+ ---
57
+
58
+ ## Step 1 — Understand
10
59
 
11
- 1. Fetch `pulse://guide` for the guide index.
12
- 2. Fetch any topic sections you need (`pulse://guide/spec`, `pulse://guide/components`, etc.).
13
- 3. Call `pulse_list_structure` to see what pages and components already exist.
60
+ Call `pulse_list_structure` to see what already exists. Fetch only the guide sections relevant to your intent (the `pulse_intent` response tells you which ones).
14
61
 
15
- Do not guess about props, patterns, or rules. If you are unsure, fetch the relevant guide section.
62
+ Do not fetch every guide section for every task. Fetch what you need.
16
63
 
17
64
  ---
18
65
 
19
- ## Phase 2 — Plan (confirmation gate)
66
+ ## Step 2 — Plan
20
67
 
21
- Before building, output a concise plan:
68
+ Decide the complexity tier (see below) and confirm with the user if the task is non-trivial. Output a concise plan:
22
69
 
23
- - What page(s) or component(s) you will create or modify
24
- - The route, state shape, mutations/actions, and server fetchers
70
+ - Route, state shape, mutations/actions, server fetchers
25
71
  - Which UI components you will use
26
- - Any shared components you will create or reuse
72
+ - Files you will create or modify
73
+
74
+ **Skip confirmation for trivially small tasks** (add a button, fix a label, swap a component). When in doubt, confirm.
75
+
76
+ **When asking the user questions:** ask one question at a time. If you present choices, use at most 4 options — the `ask_user` tool enforces this limit and will error if exceeded. For open-ended questions (names, copy, features, hex colours), ask as plain prose with no choices at all.
77
+
78
+ ---
79
+
80
+ ## Complexity tiers
81
+
82
+ Not every task needs all 8 phases. Match the tier to the task:
83
+
84
+ ### Tier 1 — Simple (static or read-only content)
85
+
86
+ *Applies to:* landing pages, blog posts, profile pages, any page with no mutations or actions.
87
+
88
+ | Phase | Action | Gate |
89
+ |---|---|---|
90
+ | 1 | Intent + Understand | — |
91
+ | 2 | Build | announce brief → write files |
92
+ | 3 | Validate | `pulse_validate` clean |
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) |
95
+
96
+ No tests required for pure view specs with no logic. Add tests if the view has non-trivial helper functions.
97
+
98
+ ### Tier 2 — Standard (interactive, single-purpose)
99
+
100
+ *Applies to:* forms, CRUD pages, settings, auth flows, dashboards without complex state machines.
101
+
102
+ | Phase | Action | Gate |
103
+ |---|---|---|
104
+ | 1 | Intent + Understand | — |
105
+ | 2 | Plan | user confirmation (skip if unambiguous) |
106
+ | 3 | Build | announce brief → write files |
107
+ | 4 | Validate | `pulse_validate` clean |
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 |
110
+ | 6 | Tests | mutations, view landmarks, any utility functions |
111
+ | 7 | Code review | `pulse_review` — only after 5–6 pass |
112
+
113
+ ### Tier 3 — Complex (multi-step, shared state, or security-sensitive)
114
+
115
+ *Applies to:* multi-step wizards, pages with guards and auth, store-connected pages, anything touching sensitive data or complex branching state.
116
+
117
+ All phases as originally defined — full flow with every gate.
27
118
 
28
- Wait for the user to confirm or adjust the plan before writing any code.
119
+ | Phase | Action | Gate |
120
+ |---|---|---|
121
+ | 1 | Intent + Understand | — |
122
+ | 2 | Plan | **user confirmation required** |
123
+ | 3 | Build | announce brief → write files |
124
+ | 4 | Validate | `pulse_validate` clean |
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 |
127
+ | 6 | Tests | full coverage: mutations, actions, guards, view |
128
+ | 7 | Code review | `pulse_review` |
129
+ | 8 | Fix + re-verify | re-run all gates |
29
130
 
30
- **Skip this gate only if the task is unambiguous and small** (e.g. "add a delete button to the existing list page"). When in doubt, confirm.
131
+ **If in doubt, default to Tier 2.** Choosing a lower tier than the task warrants means you'll hit problems at gates — the tiers are a shortcut, not a way to skip quality.
31
132
 
32
133
  ---
33
134
 
34
135
  ## Phase 3 — Build
35
136
 
36
- ### 3a — Announce before writing
137
+ ### 3a — Decide: components or creative override
37
138
 
38
- Before writing any file, output a build brief so the user can see what is being constructed and catch misunderstandings before minutes of work are lost. Format:
139
+ **Two valid modes — choose one and state it in the build brief:**
140
+
141
+ **Mode A — Components first (default)**
142
+ Use Pulse UI components for all standard UI patterns. Before writing any view HTML, grep `src/ui/index.js` for the components you need:
143
+
144
+ ```bash
145
+ grep -E "^export" src/ui/index.js | head -20
146
+ ```
147
+
148
+ Or use the guide-components table. Never write these patterns from scratch in Mode A:
149
+
150
+ - Hero section → `hero()`
151
+ - Product/service cards → `card()`
152
+ - Image + text layouts → `media()`
153
+ - Horizontal strips → `banner()`
154
+ - Feature tiles → `feature()`
155
+ - Testimonials → `testimonial()`
156
+ - CTAs → `cta()`
157
+
158
+ If you're about to type `class="hero"` or `class="product-card"` in Mode A, stop — use the component instead.
159
+
160
+ **Mode B — Creative override (raw HTML throughout)**
161
+ When the design intent calls for a more expressive, unconventional, or typographically-driven layout that components would constrain, you may build with raw HTML throughout. This is a deliberate design decision, not a shortcut.
162
+
163
+ Rules for Mode B:
164
+ - Load `/pulse-ui.css` in `meta.styles` — the token system is still required
165
+ - Functional atoms (`button`, `input`, `badge`, `modal`) should still come from components unless there is a specific design reason
166
+ - The quality gate is 100/100/100/100 Lighthouse on both desktop and mobile — this replaces the component checklist as the pass bar
167
+
168
+ Announce Mode B explicitly in the build brief: *"Mode B — component-free, raw HTML for creative control."*
169
+
170
+ ### 3b — Announce before writing
171
+
172
+ Before writing any file, output a build brief:
39
173
 
40
174
  ```
41
- Building: <page name> (<route>)
42
- State: <fields and types, or "none">
43
- Mutations: <list, or "none">
44
- Actions: <list with brief description, or "none">
45
- Server: <fetcher names and what they fetch, or "none">
46
- View: <key sections / landmarks>
47
- Files: <list of files that will be written>
175
+ Building: <page name> (<route>)
176
+ Mode: A — components first | B — creative override (raw HTML, reason: ...)
177
+ State: <fields and types, or "none">
178
+ Mutations: <list, or "none">
179
+ Actions: <list with brief description, or "none">
180
+ Server: <fetcher names and what they fetch, or "none">
181
+ Components: <which UI components you will use, or "none — Mode B">
182
+ View: <key sections / landmarks>
183
+ Files: <list of files that will be written>
48
184
  ```
49
185
 
50
- Output this before making any tool calls. Do not skip it for "simple" pages — a one-liner is fine for simple pages, but always output something.
186
+ One-liners are fine for simple pages, but always output something. **Declaring the mode upfront** makes the design intent explicit and prevents unnecessary component-reinvention warnings during review.
51
187
 
52
- ### 3b — Write
188
+ ### 3c — Write
53
189
 
54
- Write each file using the **Write tool** — not `pulse_create_page`. This shows the user a readable diff. Before each file write, output a one-line status (present-progressive, no tick — the work hasn't happened yet):
190
+ Write each file using the **Write tool** — not `pulse_create_page`. This shows the user a readable diff. Before each file write, output a one-line status (present-progressive):
55
191
 
56
192
  ```
57
193
  Writing src/pages/my-page.js...
@@ -63,9 +199,11 @@ After the Write tool completes, call `pulse_create_page(name)` to register the p
63
199
  ✓ src/pages/my-page.js written and registered.
64
200
  ```
65
201
 
66
- **Rule: never output `✓` before the tool call that confirms the result. Use present-progressive (`Writing...`, `Running...`, `Checking...`) for pre-tool announcements.**
202
+ ### 3d — Suggest (optional but recommended)
203
+
204
+ After writing the first draft, call `pulse_suggest(content)` before running the hard validator. It catches obvious omissions collaboratively — a second opinion, not a gate.
67
205
 
68
- If multiple files are needed (spec + shared component, spec + test stub, etc.), announce and write them one at a time with a status line before each.
206
+ **Rule: never output `✓` before the tool call that confirms the result.**
69
207
 
70
208
  ---
71
209
 
@@ -74,11 +212,11 @@ If multiple files are needed (spec + shared component, spec + test stub, etc.),
74
212
  Run `pulse_validate` on the spec file.
75
213
 
76
214
  - **If it passes:** output `✓ Validation clean — checking browser...` and continue.
77
- - **If it fails:** fix every error and every warning, then re-run. Repeat until clean. Do not continue until validation is clean.
215
+ - **If it fails:** fix every error and every warning, then re-run. Repeat until clean.
78
216
 
79
217
  ---
80
218
 
81
- ## Phase 5 — Browser check (pass gate)
219
+ ## Phase 5 — Browser check
82
220
 
83
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:
84
222
  1. Call `pulse_restart_server` — waits for the server to be ready
@@ -86,65 +224,113 @@ Run `pulse_validate` on the spec file.
86
224
 
87
225
  Do this before every screenshot, console check, or visual inspection. Never debug a problem that might just be a stale tab.
88
226
 
89
- **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
90
247
 
91
- `/verify` is the canonical implementation of phases 4–7: it validates, screenshots, runs Lighthouse (desktop + mobile), runs the performance trace (LCP + CLS), checks console errors, runs `pulse_review`, and — critically — **writes the `.pulse-verified` stamp** at the end. Running the steps manually without writing the stamp will cause the stop hook to block.
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.
92
249
 
93
- Pass gates (all must be met before continuing to Phase 6):
94
- - Screenshot: no layout or rendering issues
250
+ ---
251
+
252
+ ### Phase 5b — Full verification (pass gate)
253
+
254
+ **Run `/verify` now.** Do not execute these steps manually.
255
+
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.
257
+
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.
259
+
260
+ ### ⛔ After design approval — mandatory steps before continuing
261
+
262
+ 1. `pulse_design_review` (if `pulse_intake` ran) — work through all 7 signals, fix any Fail
263
+ 2. `pulse_layout_review <url>` — 390/768/1280px overflow, broken images, collapsed sections
264
+ 3. `/verify` — Lighthouse desktop + mobile (100/100/100) + CLS 0.00 + `pulse_review`
265
+
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.
267
+
268
+ Pass gates:
269
+ - Design approval: user has explicitly said they're happy with the layout
270
+ - Design review: all 7 signals pass (or intake didn't run)
271
+ - Layout review: no overflow, broken images, or collapsed sections at any viewport
95
272
  - Lighthouse desktop: Accessibility, Best Practices, SEO all 100
96
- - Lighthouse mobile: same pass bar
97
- - **CLS: 0.00** — any layout shift is a blocker; fix it before proceeding
98
- - LCP: measured and reported; flag any fixable insight (render-blocking resources, large image delay)
273
+ - Lighthouse mobile: same
274
+ - **CLS: 0.00** — any layout shift is a blocker
99
275
  - No console errors
100
276
 
101
- **If any gate fails:** fix the issue and run `/verify` again. Repeat until all gates pass.
277
+ **If any gate fails:** fix and run `/verify` again.
102
278
 
103
- Output after passing: `✓ Verified — writing tests...`
279
+ **Troubleshooting browser profile lock:** If you see "browser already running" or chrome profile errors, another session is using the same Chrome profile. This is a `chrome-devtools-mcp` limitation — only one session can use the profile at a time. Ask the user to close other Claude sessions, or wait for the other session to finish.
280
+
281
+ **CSS-only changes after verification:** If you've already run `/verify` successfully and the user asks for minor CSS tweaks (margins, colours, spacing), you must run the full `/verify` workflow again — the stop hook checks the file mtime against the stamp. There is no lighter CSS-only check. This is by design: even CSS changes can affect CLS, colour contrast, or readability.
104
282
 
105
283
  ---
106
284
 
107
- ## Phase 6 — Tests (pass gate)
285
+ ## Phase 6 — Tests (pass gate, Tier 2+)
108
286
 
109
287
  Write tests for every spec you created or modified. At minimum:
110
288
 
111
- - Each mutation as a pure function (assert the returned state shape)
289
+ - Each mutation as a pure function
112
290
  - View renders expected HTML landmarks
113
291
  - Any utility functions defined in the spec
114
292
 
115
- Run the tests. Fix every failure. Repeat until all tests pass.
116
-
117
- Output after passing: `✓ Tests passing — ready for review.`
293
+ Run the tests. Fix every failure.
118
294
 
119
295
  ---
120
296
 
121
- ## Phase 7 — Review
297
+ ## Phase 7 — Review (Tier 2+)
122
298
 
123
- **Only invoke the Review Agent after all of the above gates have passed.** The reviewer must receive: passing validation, passing Lighthouse (desktop + mobile), and passing tests. Never hand off code that has not cleared every gate.
299
+ Only invoke `pulse_review` after phases 4–6 all pass. It checks the code for correctness, security, accessibility, DRY violations, and checklist adherence.
124
300
 
125
- The Review Agent checks the code for correctness, security, accessibility, DRY violations, and adherence to the checklist. It returns a list of issues.
126
-
127
- > Note: `/verify` includes the review step (`pulse_review`) — if you ran `/verify` and it passed cleanly, the Review Agent has already run. You do not need to invoke it again separately.
301
+ > Note: `/verify` includes the review step — if you ran `/verify` and it passed, the Review Agent has already run.
128
302
 
129
303
  ---
130
304
 
131
- ## Phase 8 — Fix review issues
305
+ ## Phase 8 — Fix review issues (Tier 3)
132
306
 
133
- Fix every issue raised by the Review Agent. If the fixes touch the spec or view, run `/verify` again — it re-runs validation, Lighthouse, the performance trace, console check, and review in one pass and writes a fresh stamp.
307
+ Fix every issue raised. If fixes touch the spec or view, run `/verify` again.
134
308
 
135
- Only declare the task done after phase 8 is complete and all gates still pass.
309
+ Only declare the task done after all gates still pass.
136
310
 
137
311
  ---
138
312
 
139
- ## Gate summary
313
+ ## Summary
140
314
 
141
315
  ```
142
- Phase 1 Understand (no gate — always run)
143
- Phase 2 Plan gate: user confirmation
144
- Phase 3 Build announce brief → write files (no gate, but always narrate)
145
- Phase 4 Validate gate: pulse_validate clean
146
- Phase 5 Browser gate: Lighthouse 100/100/100 (A/BP/SEO) + CLS 0.00, desktop + mobile
147
- Phase 6 Tests gate: all tests pass
148
- Phase 7 Review Agent (only reached after phases 4–6 all pass)
149
- Phase 8 Fix + re-verify gate: all gates still pass
316
+ New project / new page:
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/)
320
+ pulse_intake → capture real content, palette, vibe, anti-style
321
+ pulse_sketch → choose 1 of 3 structural layout directions
322
+ pulse_intent → get scaffold + guide list
323
+
324
+ Targeted edit / bug fix:
325
+ pulse_intent → (or skip entirely for trivial changes)
326
+
327
+ Step 1 Understand pulse_list_structure + relevant guides
328
+ Step 2 Plan confirm with user if non-trivial
329
+
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".
150
335
  ```
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
@@ -57,7 +58,15 @@ const PUBLIC_DIR = path.join(ROOT, 'public')
57
58
  const stampPath = path.join(PUBLIC_DIR, '.pulse-ui-version')
58
59
  const stamp = fs.existsSync(stampPath) ? fs.readFileSync(stampPath, 'utf8').trim() : null
59
60
 
60
- if (stamp === pkgVersion) return
61
+ // Compare CSS byte-length so any content change triggers a re-copy, even
62
+ // within the same version (e.g. when iterating locally with npm link).
63
+ const srcCss = path.join(pkgPublic, 'pulse-ui.css')
64
+ const dstCss = path.join(PUBLIC_DIR, 'pulse-ui.css')
65
+ const srcSize = fs.existsSync(srcCss) ? fs.statSync(srcCss).size : -1
66
+ const dstSize = fs.existsSync(dstCss) ? fs.statSync(dstCss).size : -2
67
+ const cssStale = srcSize !== dstSize
68
+
69
+ if (stamp === pkgVersion && !cssStale) return
61
70
 
62
71
  fs.mkdirSync(PUBLIC_DIR, { recursive: true })
63
72
  for (const asset of ['pulse-ui.css', 'pulse-ui.js']) {
@@ -98,19 +107,13 @@ const PUBLIC_DIR = path.join(ROOT, 'public')
98
107
  // Start
99
108
  // ---------------------------------------------------------------------------
100
109
 
101
- console.log(`⚡ Pulse dev server starting...\n`)
102
-
103
110
  const specs = await loadPages(ROOT)
104
111
 
105
112
  if (specs.length === 0) {
106
- 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.')
107
114
  process.exit(1)
108
115
  }
109
116
 
110
- console.log('Pages:\n')
111
- specs.forEach(spec => console.log(` ${spec.route}`))
112
- console.log()
113
-
114
117
  // ---------------------------------------------------------------------------
115
118
  // Hot reload — SSE clients + file watcher
116
119
  // ---------------------------------------------------------------------------
@@ -135,16 +138,37 @@ process.on('SIGTERM', closeReloadClients)
135
138
  process.on('SIGINT', closeReloadClients)
136
139
 
137
140
  let reloadTimer = null
138
- fs.watch(path.join(ROOT, 'src'), { recursive: true }, () => {
141
+ let lastReload = 0
142
+
143
+ async function triggerReload(label = 'File changed') {
139
144
  clearTimeout(reloadTimer)
140
145
  reloadTimer = setTimeout(async () => {
146
+ const now = Date.now()
147
+ if (now - lastReload < 100) return
148
+ lastReload = now
149
+
141
150
  try {
142
- const fresh = await loadPages(ROOT, Date.now())
151
+ const fresh = await loadPages(ROOT, now)
143
152
  updateSpecs(fresh)
144
- } catch { /* spec error — browser will show the old page, not crash */ }
153
+ log.info(`${label} — specs reloaded`)
154
+ } catch (err) {
155
+ log.error(`Spec reload failed: ${err.message}`)
156
+ }
145
157
  notifyReload()
146
- }, 50)
147
- })
158
+ }, 200)
159
+ }
160
+
161
+ // Watch src/ for changes to existing files (spec edits, component edits)
162
+ fs.watch(path.join(ROOT, 'src'), { recursive: true }, () => triggerReload('File changed'))
163
+
164
+ // Also watch the pages directory explicitly so macOS reliably fires on new file creation.
165
+ // On macOS, fs.watch with recursive=true watches subdirectory contents but may not fire
166
+ // for the parent dir entry when a brand-new file is created. Watching the dir non-recursively
167
+ // catches the "new entry added to this directory" event.
168
+ const pagesDir = path.join(ROOT, 'src', 'pages')
169
+ if (fs.existsSync(pagesDir)) {
170
+ fs.watch(pagesDir, { recursive: false }, () => triggerReload('New page detected'))
171
+ }
148
172
 
149
173
  // Tiny script injected into every page — connects to SSE and reloads on change
150
174
  // Passed as a function so the server can inject the per-request CSP nonce
@@ -160,13 +184,14 @@ const reloadScript = (nonce) => `<script nonce="${nonce}">
160
184
  })();
161
185
  </script>`
162
186
 
163
- const { updateSpecs } = createServer(specs, {
187
+ const { updateSpecs } = await createServer(specs, {
164
188
  port: PORT,
165
189
  stream: true,
166
190
  staticDir: fs.existsSync(PUBLIC_DIR) ? PUBLIC_DIR : null,
167
191
  manifest: {}, // never use a build manifest in dev — always serve source files
168
192
  extraBody: reloadScript,
169
193
  dev: true,
194
+ agentMode: !!process.env.PULSE_AGENT_MODE,
170
195
  ...(_config.csp ? { csp: _config.csp } : {}),
171
196
 
172
197
  onRequest(req, res) {
package/src/cli/fmt.js ADDED
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Pulse CLI — terminal formatting helpers
3
+ *
4
+ * Respects NO_COLOR and CI env vars — falls back to plain text automatically.
5
+ * Zero dependencies, Node built-ins only.
6
+ */
7
+
8
+ const USE_COLOR = !process.env.NO_COLOR && !process.env.CI && process.stdout.isTTY
9
+
10
+ // ANSI escape helpers — no-ops when colour is disabled
11
+ const c = {
12
+ reset: s => USE_COLOR ? `\x1b[0m${s}\x1b[0m` : s,
13
+ bold: s => USE_COLOR ? `\x1b[1m${s}\x1b[0m` : s,
14
+ dim: s => USE_COLOR ? `\x1b[2m${s}\x1b[0m` : s,
15
+ green: s => USE_COLOR ? `\x1b[32m${s}\x1b[0m` : s,
16
+ yellow: s => USE_COLOR ? `\x1b[33m${s}\x1b[0m` : s,
17
+ blue: s => USE_COLOR ? `\x1b[34m${s}\x1b[0m` : s,
18
+ cyan: s => USE_COLOR ? `\x1b[36m${s}\x1b[0m` : s,
19
+ white: s => USE_COLOR ? `\x1b[97m${s}\x1b[0m` : s,
20
+ red: s => USE_COLOR ? `\x1b[31m${s}\x1b[0m` : s,
21
+ purple: s => USE_COLOR ? `\x1b[35m${s}\x1b[0m` : s,
22
+ }
23
+
24
+ // Strip ANSI codes to measure true visual width
25
+ function visLen(s) {
26
+ // eslint-disable-next-line no-control-regex
27
+ return s.replace(/\x1b\[[0-9;]*m/g, '').length
28
+ }
29
+
30
+ // Pad a string to a visual width (accounting for ANSI codes)
31
+ function pad(s, width, align = 'left') {
32
+ const len = visLen(s)
33
+ const fill = Math.max(0, width - len)
34
+ return align === 'right' ? ' '.repeat(fill) + s : s + ' '.repeat(fill)
35
+ }
36
+
37
+ // ─── Box ─────────────────────────────────────────────────────────────────────
38
+
39
+ export function box(lines, opts = {}) {
40
+ const { title = '', style = 'single' } = opts
41
+ const chars = style === 'double'
42
+ ? { tl: '╔', tr: '╗', bl: '╚', br: '╝', h: '═', v: '║' }
43
+ : { tl: '┌', tr: '┐', bl: '└', br: '┘', h: '─', v: '│' }
44
+
45
+ const inner = Array.isArray(lines) ? lines : [lines]
46
+ const maxW = Math.max(...inner.map(visLen), title ? visLen(title) + 2 : 0)
47
+ const bar = chars.h.repeat(maxW + 2)
48
+
49
+ const top = title
50
+ ? `${chars.tl}${chars.h} ${c.bold(title)} ${chars.h.repeat(maxW - visLen(title) - 1)}${chars.tr}`
51
+ : `${chars.tl}${bar}${chars.tr}`
52
+
53
+ const rows = inner.map(l => `${chars.v} ${pad(l, maxW)} ${chars.v}`)
54
+ return [top, ...rows, `${chars.bl}${bar}${chars.br}`].join('\n')
55
+ }
56
+
57
+ // ─── Header (branded section label) ──────────────────────────────────────────
58
+
59
+ export function header(text) {
60
+ const line = `${c.purple('⚡')} ${c.bold(c.white(text))}`
61
+ return `\n${line}\n${c.dim('─'.repeat(visLen('⚡ ' + text) - (USE_COLOR ? 0 : 0)))}`
62
+ }
63
+
64
+ // ─── Table ───────────────────────────────────────────────────────────────────
65
+
66
+ /**
67
+ * Render an aligned table.
68
+ * @param {string[]} cols - Column headers
69
+ * @param {string[][]} rows - Data rows (array of arrays)
70
+ * @param {{align?: ('left'|'right')[]}} opts
71
+ */
72
+ export function table(cols, rows, opts = {}) {
73
+ const aligns = opts.align || cols.map(() => 'left')
74
+ const allRows = [cols, ...rows]
75
+ const widths = cols.map((_, i) => Math.max(...allRows.map(r => visLen(r[i] ?? ''))))
76
+
77
+ const sep = ' '
78
+
79
+ const headerRow = cols.map((h, i) => pad(c.dim(h), widths[i], aligns[i])).join(sep)
80
+ const divider = c.dim('─'.repeat(widths.reduce((s, w, i) => s + w + (i < widths.length - 1 ? sep.length : 0), 0)))
81
+ const body = rows.map(row =>
82
+ row.map((cell, i) => pad(cell ?? '', widths[i], aligns[i])).join(sep)
83
+ )
84
+
85
+ return [headerRow, divider, ...body].join('\n')
86
+ }
87
+
88
+ // ─── Status line helpers ──────────────────────────────────────────────────────
89
+
90
+ export const icon = {
91
+ ok: () => c.green('✓'),
92
+ fail: () => c.red('✗'),
93
+ warn: () => c.yellow('⚠'),
94
+ info: () => c.cyan('·'),
95
+ run: () => c.yellow('›'),
96
+ bolt: () => c.purple('⚡'),
97
+ }
98
+
99
+ export function ok(msg) { return ` ${icon.ok()} ${msg}` }
100
+ export function fail(msg) { return ` ${icon.fail()} ${c.red(msg)}` }
101
+ export function warn(msg) { return ` ${icon.warn()} ${c.yellow(msg)}` }
102
+ export function info(msg) { return ` ${icon.info()} ${c.dim(msg)}` }
103
+
104
+ // ─── Timing ───────────────────────────────────────────────────────────────────
105
+
106
+ export function elapsed(ms) {
107
+ if (ms < 1000) return `${Math.round(ms)}ms`
108
+ return `${(ms / 1000).toFixed(1)}s`
109
+ }
110
+
111
+ // ─── Exports ──────────────────────────────────────────────────────────────────
112
+
113
+ export { c, pad, visLen }