@invisibleloop/pulse 0.7.3 → 0.8.1
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/CLAUDE.md +33 -0
- package/docs/public/docs.css +6 -2
- package/docs/src/pages/home.js +53 -53
- package/package.json +1 -1
- package/public/.pulse-ui-version +1 -1
- package/public/pulse-ui.css +24 -0
- package/src/agent/checklist.md +5 -0
- package/src/agent/commands/verify.md +13 -5
- package/src/agent/guide-styles.md +5 -1
- package/src/agent/workflow.md +51 -15
- package/src/cli/scaffold.js +31 -21
- package/src/mcp/server.js +125 -60
package/CLAUDE.md
CHANGED
|
@@ -336,6 +336,39 @@ Always target `[data-theme="light"]` when overriding colours for the light theme
|
|
|
336
336
|
|
|
337
337
|
If a colour must apply in both themes, set it in both `:root` and `[data-theme="light"]`.
|
|
338
338
|
|
|
339
|
+
## CSS File Responsibilities
|
|
340
|
+
|
|
341
|
+
There are two CSS files and they have distinct roles — do not mix them:
|
|
342
|
+
|
|
343
|
+
| File | Purpose | Allowed content |
|
|
344
|
+
|---|---|---|
|
|
345
|
+
| `public/theme.css` | Token definitions — hex values, raw colours, font URLs | Hex values, `rgb()`, raw values, `@font-face`, `@import` |
|
|
346
|
+
| `app.css` (or per-page) | Layout and component overrides | `var()` tokens **only** — no hex, no raw colour values |
|
|
347
|
+
|
|
348
|
+
**`app.css` must never contain hex values or raw colour values.** A lint hook enforces this. If you need to define or override a colour token, put it in `public/theme.css` and reference it via `var()` in `app.css`.
|
|
349
|
+
|
|
350
|
+
```css
|
|
351
|
+
/* public/theme.css — hex values live here */
|
|
352
|
+
[data-theme="light"] {
|
|
353
|
+
--color-accent: #e25;
|
|
354
|
+
--color-bg: #ffffff;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/* app.css — only var() references */
|
|
358
|
+
.hero {
|
|
359
|
+
background: var(--color-bg);
|
|
360
|
+
color: var(--color-accent);
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Load both files via `meta.styles`, theme first:
|
|
365
|
+
|
|
366
|
+
```js
|
|
367
|
+
meta: {
|
|
368
|
+
styles: ['/pulse-ui.css', '/theme.css', '/app.css'],
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
339
372
|
## HTTP Response Behaviour
|
|
340
373
|
|
|
341
374
|
- **Full page request** — SSR HTML with hydration bootstrap
|
package/docs/public/docs.css
CHANGED
|
@@ -683,6 +683,7 @@ a:hover {
|
|
|
683
683
|
font-weight: 700;
|
|
684
684
|
letter-spacing: -0.02em;
|
|
685
685
|
margin: 0.75rem 0 1.25rem;
|
|
686
|
+
text-align: center;
|
|
686
687
|
}
|
|
687
688
|
|
|
688
689
|
.ai-first-lead {
|
|
@@ -691,6 +692,9 @@ a:hover {
|
|
|
691
692
|
line-height: 1.75;
|
|
692
693
|
max-width: 72ch;
|
|
693
694
|
margin-bottom: 3rem;
|
|
695
|
+
text-align: center;
|
|
696
|
+
margin-left: auto;
|
|
697
|
+
margin-right: auto;
|
|
694
698
|
}
|
|
695
699
|
|
|
696
700
|
.ai-cols {
|
|
@@ -751,10 +755,10 @@ a:hover {
|
|
|
751
755
|
}
|
|
752
756
|
|
|
753
757
|
.ai-col-list li::before {
|
|
754
|
-
content: "
|
|
758
|
+
content: "✓";
|
|
755
759
|
position: absolute;
|
|
756
760
|
left: 0;
|
|
757
|
-
color: var(--
|
|
761
|
+
color: var(--accent);
|
|
758
762
|
}
|
|
759
763
|
|
|
760
764
|
.ai-col--pulse .ai-col-list li {
|
package/docs/src/pages/home.js
CHANGED
|
@@ -37,7 +37,7 @@ export default {
|
|
|
37
37
|
route: '/',
|
|
38
38
|
meta: {
|
|
39
39
|
title: 'Pulse — The spec-first web framework',
|
|
40
|
-
description: 'Pulse is a server-first
|
|
40
|
+
description: 'Pulse is a server-first Node.js framework with zero runtime dependencies. One spec object per page — server data, state, mutations, and view in plain JS. Streaming SSR, security headers, and production caching are enforced by the architecture.',
|
|
41
41
|
styles: ['/pulse-ui.css', '/docs.css'],
|
|
42
42
|
},
|
|
43
43
|
state: {},
|
|
@@ -67,9 +67,9 @@ export default {
|
|
|
67
67
|
</svg>
|
|
68
68
|
</div>
|
|
69
69
|
<div class="hero-badge">v${version} — EARLY ACCESS</div>
|
|
70
|
-
<p class="hero-kicker">A Node.js framework
|
|
71
|
-
<h1 class="hero-title">
|
|
72
|
-
<p class="hero-subtitle">
|
|
70
|
+
<p class="hero-kicker">A server-first Node.js framework. Zero runtime dependencies.</p>
|
|
71
|
+
<h1 class="hero-title">One spec. One way to build. Production quality by design.</h1>
|
|
72
|
+
<p class="hero-subtitle">Each page is a single plain JS object — server data, client state, mutations, and view co-located in one place. Streaming SSR, security headers, and production caching are enforced by the framework, not left to configuration.<br><strong>Designed for AI agents. Simple enough to write yourself.</strong></p>
|
|
73
73
|
<div class="hero-ctas">
|
|
74
74
|
<a href="/getting-started" class="btn-primary">Get Started</a>
|
|
75
75
|
<a href="/spec" class="btn-secondary">Read the Spec</a>
|
|
@@ -94,30 +94,30 @@ export default {
|
|
|
94
94
|
<div class="home-stat-divider"></div>
|
|
95
95
|
<div class="home-stat">
|
|
96
96
|
<span class="home-stat-value">100</span>
|
|
97
|
-
<span class="home-stat-label">Lighthouse score</span>
|
|
97
|
+
<span class="home-stat-label">Lighthouse score, by design</span>
|
|
98
98
|
</div>
|
|
99
99
|
</section>
|
|
100
100
|
|
|
101
101
|
<section class="how">
|
|
102
102
|
<div class="how-inner">
|
|
103
|
-
<h2 class="section-label">
|
|
103
|
+
<h2 class="section-label">The idea</h2>
|
|
104
104
|
<div class="how-steps">
|
|
105
105
|
<div class="how-step">
|
|
106
106
|
<div class="how-step-num">1</div>
|
|
107
|
-
<h3>
|
|
108
|
-
<p>Everything
|
|
107
|
+
<h3>The spec is the page</h3>
|
|
108
|
+
<p>Everything a page needs lives in one plain JS object — server data, client state, mutations, and view. One format, no split files, no hidden conventions.</p>
|
|
109
109
|
</div>
|
|
110
110
|
<div class="how-connector" aria-hidden="true"></div>
|
|
111
111
|
<div class="how-step">
|
|
112
112
|
<div class="how-step-num">2</div>
|
|
113
|
-
<h3>
|
|
114
|
-
<p>
|
|
113
|
+
<h3>The schema is the contract</h3>
|
|
114
|
+
<p>Every spec is validated at startup. Either it's correct or it's rejected — no ambiguity, no misconfiguration that surfaces later in production.</p>
|
|
115
115
|
</div>
|
|
116
116
|
<div class="how-connector" aria-hidden="true"></div>
|
|
117
117
|
<div class="how-step">
|
|
118
118
|
<div class="how-step-num">3</div>
|
|
119
|
-
<h3>
|
|
120
|
-
<p>Streaming SSR, security headers, and
|
|
119
|
+
<h3>The framework is the guarantee</h3>
|
|
120
|
+
<p>Streaming SSR, security headers, and production caching come from the architecture. You write the product logic. The framework ships the quality.</p>
|
|
121
121
|
</div>
|
|
122
122
|
</div>
|
|
123
123
|
</div>
|
|
@@ -126,8 +126,8 @@ export default {
|
|
|
126
126
|
<section class="home-code">
|
|
127
127
|
<div class="home-code-inner">
|
|
128
128
|
<div class="home-code-header">
|
|
129
|
-
<h2>Everything
|
|
130
|
-
<p>Server
|
|
129
|
+
<h2>Everything a page needs. Nothing it doesn't.</h2>
|
|
130
|
+
<p>Server fetchers, client state, mutations, and view are co-located in one object. No split files. No magic exports. The spec <strong>is</strong> the page — readable, predictable, complete.</p>
|
|
131
131
|
</div>
|
|
132
132
|
<div class="home-code-block">
|
|
133
133
|
${codeWindow({ content: exampleSpec, filename: 'src/pages/dashboard.js', lang: 'JavaScript' })}
|
|
@@ -136,26 +136,26 @@ export default {
|
|
|
136
136
|
</section>
|
|
137
137
|
|
|
138
138
|
<section class="ai-first">
|
|
139
|
-
<div class="section-label">
|
|
140
|
-
<h2 class="ai-first-title">
|
|
141
|
-
<p class="ai-first-lead"
|
|
139
|
+
<div class="section-label">AI + Pulse</div>
|
|
140
|
+
<h2 class="ai-first-title">The constraint is the point. For humans and agents alike.</h2>
|
|
141
|
+
<p class="ai-first-lead">Pulse was designed around a simple observation: when there is only one correct way to build a page, every page is built correctly — whether you wrote it or an AI did. The spec format is the advantage. The schema is the enforcement.</p>
|
|
142
142
|
<div class="ai-cols">
|
|
143
143
|
<div class="ai-col">
|
|
144
|
-
<h3 class="ai-col-title
|
|
144
|
+
<h3 class="ai-col-title">For developers</h3>
|
|
145
145
|
<ul class="ai-col-list">
|
|
146
|
-
<li>
|
|
147
|
-
<li>
|
|
148
|
-
<li>
|
|
149
|
-
<li>
|
|
146
|
+
<li>One format to learn — server, state, mutations, view in one place.</li>
|
|
147
|
+
<li>Production quality is the starting point, not a final audit.</li>
|
|
148
|
+
<li>Specs are short, self-contained, and easy to review.</li>
|
|
149
|
+
<li>Nothing to misconfigure — the framework doesn't give you the choice.</li>
|
|
150
150
|
</ul>
|
|
151
151
|
</div>
|
|
152
152
|
<div class="ai-col ai-col--pulse">
|
|
153
|
-
<h3 class="ai-col-title ai-col-title--good">
|
|
153
|
+
<h3 class="ai-col-title ai-col-title--good">For AI agents</h3>
|
|
154
154
|
<ul class="ai-col-list">
|
|
155
|
-
<li>One
|
|
156
|
-
<li>
|
|
157
|
-
<li>
|
|
158
|
-
<li>
|
|
155
|
+
<li>One valid structure per page — agents can't pick the wrong pattern.</li>
|
|
156
|
+
<li>The schema rejects bad output at startup, not in production.</li>
|
|
157
|
+
<li>Security, SSR, and caching can't be accidentally omitted.</li>
|
|
158
|
+
<li>Consistent output across agents, sessions, and team members.</li>
|
|
159
159
|
</ul>
|
|
160
160
|
</div>
|
|
161
161
|
</div>
|
|
@@ -164,7 +164,7 @@ export default {
|
|
|
164
164
|
<section class="versus">
|
|
165
165
|
<div class="section-label">How Pulse compares</div>
|
|
166
166
|
<h2 class="versus-title">Constraints enforced. Not recommended.</h2>
|
|
167
|
-
<p class="versus-sub">Pulse enforces
|
|
167
|
+
<p class="versus-sub">Pulse enforces correctness out of the box. Other frameworks leave production quality to the developer.</p>
|
|
168
168
|
<div class="versus-table-wrap table-sticky-col">
|
|
169
169
|
<table class="versus-table">
|
|
170
170
|
<thead>
|
|
@@ -195,13 +195,13 @@ export default {
|
|
|
195
195
|
<tr>
|
|
196
196
|
<th scope="row">SSR out of the box</th>
|
|
197
197
|
<td class="v-yes">Streaming SSR, zero config</td>
|
|
198
|
-
<td class="v-partial">Yes, but client hydration adds JS
|
|
198
|
+
<td class="v-partial">Yes, but client hydration adds JS on every page</td>
|
|
199
199
|
<td class="v-partial">Yes, but requires an adapter and client runtime on every page</td>
|
|
200
200
|
</tr>
|
|
201
201
|
<tr>
|
|
202
202
|
<th scope="row">Client JS shipped</th>
|
|
203
|
-
<td class="v-yes">~4 kB brotli
|
|
204
|
-
<td class="v-no">50
|
|
203
|
+
<td class="v-yes">~4 kB brotli on first visit; 0 kB on static pages</td>
|
|
204
|
+
<td class="v-no">50–200 kB+ depending on features used</td>
|
|
205
205
|
<td class="v-partial">~15 kB brotli</td>
|
|
206
206
|
</tr>
|
|
207
207
|
<tr>
|
|
@@ -224,7 +224,7 @@ export default {
|
|
|
224
224
|
</tr>
|
|
225
225
|
<tr>
|
|
226
226
|
<th scope="row">Production build step</th>
|
|
227
|
-
<td class="v-yes">
|
|
227
|
+
<td class="v-yes">Server needs none — <code>node server.js</code> is production. Client bundles are optional.</td>
|
|
228
228
|
<td class="v-no">Required — <code>next build</code></td>
|
|
229
229
|
<td class="v-no">Required — <code>vite build</code></td>
|
|
230
230
|
</tr>
|
|
@@ -243,24 +243,24 @@ export default {
|
|
|
243
243
|
</svg>
|
|
244
244
|
</div>
|
|
245
245
|
<h2>Performance by design</h2>
|
|
246
|
-
<p>
|
|
246
|
+
<p>Performance is not a Pulse feature — it is a structural outcome. A high Lighthouse score is the baseline. There is nothing to configure because the architecture makes the right choices automatically.</p>
|
|
247
247
|
</div>
|
|
248
248
|
<ul class="usp-points">
|
|
249
249
|
<li>
|
|
250
250
|
<strong>Fast LCP by design.</strong>
|
|
251
|
-
The shell
|
|
251
|
+
The shell streams to the browser instantly. Deferred segments arrive as data resolves — no blocking, no flash, no placeholder juggling.
|
|
252
252
|
</li>
|
|
253
253
|
<li>
|
|
254
254
|
<strong>~4 kB of JS on first visit.</strong>
|
|
255
|
-
The shared runtime is brotli-compressed and cached across all navigations. Subsequent pages cost 0.4–0.9 kB.
|
|
255
|
+
The shared runtime is brotli-compressed and cached across all navigations. Subsequent pages cost 0.4–0.9 kB. Static pages ship zero JS.
|
|
256
256
|
</li>
|
|
257
257
|
<li>
|
|
258
258
|
<strong>Zero CLS.</strong>
|
|
259
|
-
The shell occupies the correct layout before data arrives. No
|
|
259
|
+
The shell occupies the correct layout before data arrives. No layout shift because there is no placeholder to replace.
|
|
260
260
|
</li>
|
|
261
261
|
<li>
|
|
262
262
|
<strong>Immutable bundle caching.</strong>
|
|
263
|
-
Production bundles are content-hashed and served with <code>immutable, max-age=31536000</code>.
|
|
263
|
+
Production bundles are content-hashed and served with <code>immutable, max-age=31536000</code>. Returning visitors pay no JS cost on deploy.
|
|
264
264
|
</li>
|
|
265
265
|
</ul>
|
|
266
266
|
</div>
|
|
@@ -274,20 +274,20 @@ export default {
|
|
|
274
274
|
</svg>
|
|
275
275
|
</div>
|
|
276
276
|
<h2>Safe by design</h2>
|
|
277
|
-
<p>Security is not a plugin
|
|
277
|
+
<p>Security is not a plugin in Pulse — it is part of the response pipeline. Every response ships the headers most frameworks leave to the developer to remember to add.</p>
|
|
278
278
|
</div>
|
|
279
279
|
<ul class="usp-points">
|
|
280
280
|
<li>
|
|
281
281
|
<strong>Security headers on every response.</strong>
|
|
282
|
-
<code>X-Frame-Options</code>, <code>X-Content-Type-Options</code>, <code>Referrer-Policy</code>, <code>Permissions-Policy</code>, <code>Cross-Origin-Opener-Policy</code> —
|
|
282
|
+
<code>X-Frame-Options</code>, <code>X-Content-Type-Options</code>, <code>Referrer-Policy</code>, <code>Permissions-Policy</code>, <code>Cross-Origin-Opener-Policy</code> — set automatically, including on 404 and 500 pages.
|
|
283
283
|
</li>
|
|
284
284
|
<li>
|
|
285
285
|
<strong>Declarative state constraints.</strong>
|
|
286
|
-
<code>constraints</code> enforce min/max bounds on state after every mutation.
|
|
286
|
+
<code>constraints</code> enforce min/max bounds on state after every mutation. Values can never go out of range regardless of what the client sends.
|
|
287
287
|
</li>
|
|
288
288
|
<li>
|
|
289
289
|
<strong>Co-located validation.</strong>
|
|
290
|
-
Validation rules live next to the state they guard.
|
|
290
|
+
Validation rules live next to the state they guard. Easy to read, easy to review, impossible to misplace.
|
|
291
291
|
</li>
|
|
292
292
|
<li>
|
|
293
293
|
<strong>Guard before data.</strong>
|
|
@@ -314,16 +314,16 @@ export default {
|
|
|
314
314
|
The server is pure Node.js HTTP. No Express, no Fastify, no React. Nothing to add to <code>package.json</code> to run a production server.
|
|
315
315
|
</li>
|
|
316
316
|
<li>
|
|
317
|
-
<strong>No
|
|
318
|
-
<code>node server.js</code> is production. The build step
|
|
317
|
+
<strong>No server build step.</strong>
|
|
318
|
+
<code>node server.js</code> is production. The optional build step generates content-hashed client bundles — the server runs without it.
|
|
319
319
|
</li>
|
|
320
320
|
<li>
|
|
321
321
|
<strong>esbuild only in development.</strong>
|
|
322
|
-
The one dev dependency that compiles client bundles is esbuild. Fast,
|
|
322
|
+
The one dev dependency that compiles client bundles is esbuild. Fast, zero plugins to configure, never part of the production runtime.
|
|
323
323
|
</li>
|
|
324
324
|
<li>
|
|
325
|
-
<strong>No
|
|
326
|
-
|
|
325
|
+
<strong>No breaking upgrades.</strong>
|
|
326
|
+
Page files have no framework imports. The spec is a plain JS object. There is no framework API surface in your code to break across versions.
|
|
327
327
|
</li>
|
|
328
328
|
</ul>
|
|
329
329
|
</div>
|
|
@@ -381,15 +381,15 @@ export default {
|
|
|
381
381
|
</section>` : ''}
|
|
382
382
|
|
|
383
383
|
<section class="home-cta">
|
|
384
|
-
<h2>
|
|
384
|
+
<h2>One format. Every page. Production ready.</h2>
|
|
385
385
|
<ul class="home-cta-checks">
|
|
386
|
-
<li>
|
|
387
|
-
<li>
|
|
388
|
-
<li>
|
|
389
|
-
<li>
|
|
390
|
-
<li>
|
|
386
|
+
<li>One spec object per page — always valid, always readable</li>
|
|
387
|
+
<li>Streaming SSR and security headers, zero configuration</li>
|
|
388
|
+
<li>100 Lighthouse built into the architecture</li>
|
|
389
|
+
<li>Works with AI agents — and without them</li>
|
|
390
|
+
<li>MIT licensed, zero runtime dependencies</li>
|
|
391
391
|
</ul>
|
|
392
|
-
<p>
|
|
392
|
+
<p>Pulse is in early access. The goal is not to compete on features — it is to eliminate the class of problems that come from having too many of them.</p>
|
|
393
393
|
<div class="home-cta-actions">
|
|
394
394
|
<a href="/getting-started" class="btn-primary">Get Started</a>
|
|
395
395
|
<a href="/spec" class="btn-secondary">Read the Spec</a>
|
package/package.json
CHANGED
package/public/.pulse-ui-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.8.1
|
package/public/pulse-ui.css
CHANGED
|
@@ -40,6 +40,30 @@
|
|
|
40
40
|
|
|
41
41
|
/* Touch target minimum per WCAG 2.5.5 */
|
|
42
42
|
--ui-touch-min: 44px;
|
|
43
|
+
|
|
44
|
+
/* ── Spacing scale ── */
|
|
45
|
+
--ui-space-1: 0.25rem; /* 4px */
|
|
46
|
+
--ui-space-2: 0.5rem; /* 8px */
|
|
47
|
+
--ui-space-3: 0.75rem; /* 12px */
|
|
48
|
+
--ui-space-4: 1rem; /* 16px */
|
|
49
|
+
--ui-space-5: 1.25rem; /* 20px */
|
|
50
|
+
--ui-space-6: 1.5rem; /* 24px */
|
|
51
|
+
--ui-space-8: 2rem; /* 32px */
|
|
52
|
+
--ui-space-10: 2.5rem; /* 40px */
|
|
53
|
+
--ui-space-12: 3rem; /* 48px */
|
|
54
|
+
--ui-space-16: 4rem; /* 64px */
|
|
55
|
+
--ui-space-20: 5rem; /* 80px */
|
|
56
|
+
--ui-space-24: 6rem; /* 96px */
|
|
57
|
+
|
|
58
|
+
/* ── Type scale ── */
|
|
59
|
+
--ui-text-xs: 0.75rem; /* 12px */
|
|
60
|
+
--ui-text-sm: 0.875rem; /* 14px */
|
|
61
|
+
--ui-text-base: 1rem; /* 16px */
|
|
62
|
+
--ui-text-lg: 1.125rem; /* 18px */
|
|
63
|
+
--ui-text-xl: 1.25rem; /* 20px */
|
|
64
|
+
--ui-text-2xl: 1.5rem; /* 24px */
|
|
65
|
+
--ui-text-3xl: 1.875rem; /* 30px */
|
|
66
|
+
--ui-text-4xl: 2.25rem; /* 36px */
|
|
43
67
|
}
|
|
44
68
|
|
|
45
69
|
/* ─── Base reset ─────────────────────────────────────────────────────────────
|
package/src/agent/checklist.md
CHANGED
|
@@ -66,6 +66,11 @@ Before finishing any spec, verify every point below. Fix anything that fails.
|
|
|
66
66
|
- **Validate FormData fields before use.** `formData.get('email')` returns `null` if the field is missing. Check for null/empty before passing to an API or database.
|
|
67
67
|
- **Do not trust URL params.** `ctx.params.id` is a raw string from the URL. Validate it before use — check it exists, is the right type, and refers to a real resource. Return a 404 or redirect if it doesn't.
|
|
68
68
|
|
|
69
|
+
### CSS
|
|
70
|
+
|
|
71
|
+
- **`app.css` must contain no hex values or raw colour values.** A lint hook enforces this and will block the build. Hex values belong in `public/theme.css` as token definitions; `app.css` references them via `var()` only.
|
|
72
|
+
- **Load order in `meta.styles`:** `pulse-ui.css` → `theme.css` → `app.css`. Theme tokens must be defined before `app.css` references them.
|
|
73
|
+
|
|
69
74
|
### Security
|
|
70
75
|
|
|
71
76
|
- Any value from user input (URL params, form fields, external APIs) interpolated into view HTML must be escaped.
|
|
@@ -24,13 +24,13 @@ Use `mcp__chrome-devtools__navigate_page` to load the page route, then `mcp__chr
|
|
|
24
24
|
|
|
25
25
|
Run `mcp__chrome-devtools__lighthouse_audit` on the route with `{ "strategy": "desktop" }`.
|
|
26
26
|
|
|
27
|
-
**Pass bar:
|
|
27
|
+
**Pass bar: Accessibility, Best Practices, and SEO must all be 100.** Performance is measured and reported but is not a hard requirement (it varies with machine load). Report the actual scores. If Accessibility, Best Practices, or SEO is below 100, identify the failing audit(s), fix the issue, and restart from step 2.
|
|
28
28
|
|
|
29
29
|
### 6. Lighthouse — mobile
|
|
30
30
|
|
|
31
31
|
Run `mcp__chrome-devtools__lighthouse_audit` on the same route with `{ "strategy": "mobile" }`.
|
|
32
32
|
|
|
33
|
-
**Same pass bar:
|
|
33
|
+
**Same pass bar: Accessibility, Best Practices, and SEO must all be 100.** Fix any failures and restart from step 2.
|
|
34
34
|
|
|
35
35
|
### 7. Performance
|
|
36
36
|
|
|
@@ -42,7 +42,7 @@ Use `mcp__chrome-devtools__list_console_messages` — report any errors or unexp
|
|
|
42
42
|
|
|
43
43
|
### 9. Code review
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
Call `pulse_review` with the absolute path to the spec file. Read the source, rendered HTML, validator output, and checklist it returns. Fix every issue before proceeding. This is the structured review — do not skip it or replace it with a manual checklist read.
|
|
46
46
|
|
|
47
47
|
### 10. Close the browser
|
|
48
48
|
|
|
@@ -56,7 +56,7 @@ Run:
|
|
|
56
56
|
date +%s > .pulse-verified
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
This
|
|
59
|
+
**This must be the last write operation before stopping.** The stop hook compares each edited spec's modification time against this stamp — if any spec is newer than the stamp, the hook blocks. Do not edit any spec file after writing the stamp.
|
|
60
60
|
|
|
61
61
|
### 12. Report
|
|
62
62
|
|
|
@@ -69,4 +69,12 @@ Summarise:
|
|
|
69
69
|
- Console: any errors
|
|
70
70
|
- Review: pass or issues found and fixed
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
**Pass bar — all of these must be met before the page is confirmed good:**
|
|
73
|
+
- Validation: clean (no errors, no warnings)
|
|
74
|
+
- Lighthouse desktop: Accessibility, Best Practices, SEO all 100
|
|
75
|
+
- Lighthouse mobile: same
|
|
76
|
+
- CLS: 0.00 — any layout shift is a blocker
|
|
77
|
+
- LCP: report the value; flag and fix any insight that identifies a fixable cause
|
|
78
|
+
- Console: no errors
|
|
79
|
+
|
|
80
|
+
If any gate fails, fix the issue and run `/verify` again.
|
|
@@ -61,7 +61,11 @@ meta: {
|
|
|
61
61
|
}
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
**Colour tokens:** `--ui-bg`, `--ui-surface`, `--ui-surface-2`, `--ui-border`, `--ui-text`, `--ui-muted`, `--ui-accent`, `--ui-accent-hover`, `--ui-accent-dim`, `--ui-accent-text`, `--ui-green`, `--ui-red`, `--ui-yellow`, `--ui-blue`, `--ui-radius`, `--ui-radius-sm`, `--ui-font`, `--ui-mono`. Never hardcode hex values — override the tokens.
|
|
65
|
+
|
|
66
|
+
**Spacing tokens** (`--ui-space-N`): `--ui-space-1` (4px), `--ui-space-2` (8px), `--ui-space-3` (12px), `--ui-space-4` (16px), `--ui-space-5` (20px), `--ui-space-6` (24px), `--ui-space-8` (32px), `--ui-space-10` (40px), `--ui-space-12` (48px), `--ui-space-16` (64px), `--ui-space-20` (80px), `--ui-space-24` (96px). Use these for `padding`, `margin`, and `gap`.
|
|
67
|
+
|
|
68
|
+
**Type-scale tokens** (`--ui-text-*`): `--ui-text-xs` (12px), `--ui-text-sm` (14px), `--ui-text-base` (16px), `--ui-text-lg` (18px), `--ui-text-xl` (20px), `--ui-text-2xl` (24px), `--ui-text-3xl` (30px), `--ui-text-4xl` (36px). Use these for `font-size`.
|
|
65
69
|
|
|
66
70
|
## Custom fonts
|
|
67
71
|
|
package/src/agent/workflow.md
CHANGED
|
@@ -33,7 +33,39 @@ Wait for the user to confirm or adjust the plan before writing any code.
|
|
|
33
33
|
|
|
34
34
|
## Phase 3 — Build
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
### 3a — Announce before writing
|
|
37
|
+
|
|
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:
|
|
39
|
+
|
|
40
|
+
```
|
|
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>
|
|
48
|
+
```
|
|
49
|
+
|
|
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.
|
|
51
|
+
|
|
52
|
+
### 3b — Write
|
|
53
|
+
|
|
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):
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
Writing src/pages/my-page.js...
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
After the Write tool completes, call `pulse_create_page(name)` to register the page. Only after that call succeeds output:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
✓ src/pages/my-page.js written and registered.
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Rule: never output `✓` before the tool call that confirms the result. Use present-progressive (`Writing...`, `Running...`, `Checking...`) for pre-tool announcements.**
|
|
67
|
+
|
|
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.
|
|
37
69
|
|
|
38
70
|
---
|
|
39
71
|
|
|
@@ -48,15 +80,21 @@ Run `pulse_validate` on the spec file.
|
|
|
48
80
|
|
|
49
81
|
## Phase 5 — Browser check (pass gate)
|
|
50
82
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
83
|
+
**Run `/verify` now — as soon as the first build is working.** Do not defer it to the end. Do not execute these steps manually.
|
|
84
|
+
|
|
85
|
+
`/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.
|
|
86
|
+
|
|
87
|
+
Pass gates (all must be met before continuing to Phase 6):
|
|
88
|
+
- Screenshot: no layout or rendering issues
|
|
89
|
+
- Lighthouse desktop: Accessibility, Best Practices, SEO all 100
|
|
90
|
+
- Lighthouse mobile: same pass bar
|
|
91
|
+
- **CLS: 0.00** — any layout shift is a blocker; fix it before proceeding
|
|
92
|
+
- LCP: measured and reported; flag any fixable insight (render-blocking resources, large image delay)
|
|
93
|
+
- No console errors
|
|
56
94
|
|
|
57
|
-
**If any
|
|
95
|
+
**If any gate fails:** fix the issue and run `/verify` again. Repeat until all gates pass.
|
|
58
96
|
|
|
59
|
-
Output after passing: `✓
|
|
97
|
+
Output after passing: `✓ Verified — writing tests...`
|
|
60
98
|
|
|
61
99
|
---
|
|
62
100
|
|
|
@@ -80,15 +118,13 @@ Output after passing: `✓ Tests passing — ready for review.`
|
|
|
80
118
|
|
|
81
119
|
The Review Agent checks the code for correctness, security, accessibility, DRY violations, and adherence to the checklist. It returns a list of issues.
|
|
82
120
|
|
|
121
|
+
> 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.
|
|
122
|
+
|
|
83
123
|
---
|
|
84
124
|
|
|
85
125
|
## Phase 8 — Fix review issues
|
|
86
126
|
|
|
87
|
-
Fix every issue raised by the Review Agent. If the fixes touch the spec or view
|
|
88
|
-
|
|
89
|
-
- Re-run `pulse_validate`
|
|
90
|
-
- Re-run Lighthouse if visual or structural changes were made
|
|
91
|
-
- Re-run tests
|
|
127
|
+
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.
|
|
92
128
|
|
|
93
129
|
Only declare the task done after phase 8 is complete and all gates still pass.
|
|
94
130
|
|
|
@@ -99,9 +135,9 @@ Only declare the task done after phase 8 is complete and all gates still pass.
|
|
|
99
135
|
```
|
|
100
136
|
Phase 1 Understand (no gate — always run)
|
|
101
137
|
Phase 2 Plan gate: user confirmation
|
|
102
|
-
Phase 3 Build (no gate
|
|
138
|
+
Phase 3 Build announce brief → write files (no gate, but always narrate)
|
|
103
139
|
Phase 4 Validate gate: pulse_validate clean
|
|
104
|
-
Phase 5 Browser gate: Lighthouse 100/100/100 (
|
|
140
|
+
Phase 5 Browser gate: Lighthouse 100/100/100 (A/BP/SEO) + CLS 0.00, desktop + mobile
|
|
105
141
|
Phase 6 Tests gate: all tests pass
|
|
106
142
|
Phase 7 Review Agent (only reached after phases 4–6 all pass)
|
|
107
143
|
Phase 8 Fix + re-verify gate: all gates still pass
|
package/src/cli/scaffold.js
CHANGED
|
@@ -57,8 +57,9 @@ ${port !== 3000 ? ` port: ${port},\n` : ''}}
|
|
|
57
57
|
write(targetDir, 'src/pages/home.js', homePage(name))
|
|
58
58
|
write(targetDir, 'src/pages/home.test.js', homePageTest(name))
|
|
59
59
|
|
|
60
|
-
//
|
|
61
|
-
write(targetDir, 'public/
|
|
60
|
+
// Stylesheets — theme.css holds hex token definitions, app.css uses only var() references
|
|
61
|
+
write(targetDir, 'public/theme.css', themeCSS())
|
|
62
|
+
write(targetDir, 'public/app.css', baseCSS())
|
|
62
63
|
|
|
63
64
|
// Copy pulse-ui assets from the package into public/ so they're served immediately
|
|
64
65
|
const pkgPublic = new URL('../../public', import.meta.url).pathname
|
|
@@ -95,9 +96,6 @@ ${port !== 3000 ? ` port: ${port},\n` : ''}}
|
|
|
95
96
|
|
|
96
97
|
// settings.json — hooks that enforce correct agent behaviour
|
|
97
98
|
write(targetDir, '.claude/settings.json', JSON.stringify({
|
|
98
|
-
enabledPlugins: {
|
|
99
|
-
'frontend-design@claude-plugins-official': true,
|
|
100
|
-
},
|
|
101
99
|
hooks: {
|
|
102
100
|
SessionStart: [
|
|
103
101
|
{
|
|
@@ -148,7 +146,7 @@ ${port !== 3000 ? ` port: ${port},\n` : ''}}
|
|
|
148
146
|
},
|
|
149
147
|
{
|
|
150
148
|
type: 'command',
|
|
151
|
-
command: `node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const f=(i.tool_input||{}).file_path||'';if(!f.endsWith('.css')){return;}const fs=require('fs');let c;try{c=fs.readFileSync(f,'utf8');}catch(e){return;}const m=c.match(/#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3}/g);if(m&&m.length){const u=[...new Set(m)];process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:'PostToolUse',additionalContext:'HARDCODED HEX in '+f+': '+u.join(', ')+'.
|
|
149
|
+
command: `node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const f=(i.tool_input||{}).file_path||'';if(!f.endsWith('.css')){return;}if(f.endsWith('theme.css')){return;}const fs=require('fs');let c;try{c=fs.readFileSync(f,'utf8');}catch(e){return;}const m=c.match(/#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3}/g);if(m&&m.length){const u=[...new Set(m)];process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:'PostToolUse',additionalContext:'HARDCODED HEX in '+f+': '+u.join(', ')+'. Hex values belong in public/theme.css as token definitions — '+f+' should use var(--token-name) references only.'}}));}})"`,
|
|
152
150
|
statusMessage: 'Checking for hardcoded colours...',
|
|
153
151
|
},
|
|
154
152
|
{
|
|
@@ -226,7 +224,7 @@ export default {
|
|
|
226
224
|
meta: {
|
|
227
225
|
title: '${appName}',
|
|
228
226
|
description: '${appName} — built with Pulse.',
|
|
229
|
-
styles: ['/pulse-ui.css', '/app.css'],
|
|
227
|
+
styles: ['/pulse-ui.css', '/theme.css', '/app.css'],
|
|
230
228
|
},
|
|
231
229
|
|
|
232
230
|
state: {
|
|
@@ -313,6 +311,22 @@ test('decrement mutation subtracts 1', () => {
|
|
|
313
311
|
`
|
|
314
312
|
}
|
|
315
313
|
|
|
314
|
+
function themeCSS() {
|
|
315
|
+
return `\
|
|
316
|
+
/*
|
|
317
|
+
* theme.css — token definitions
|
|
318
|
+
* Hex values and raw colours live here. app.css references these via var().
|
|
319
|
+
* Override pulse-ui tokens by targeting [data-theme="light"] (higher specificity than :root).
|
|
320
|
+
*/
|
|
321
|
+
|
|
322
|
+
/* Example — uncomment and edit to set a custom accent colour:
|
|
323
|
+
[data-theme="light"] {
|
|
324
|
+
--color-accent: #0066cc;
|
|
325
|
+
}
|
|
326
|
+
*/
|
|
327
|
+
`
|
|
328
|
+
}
|
|
329
|
+
|
|
316
330
|
function baseCSS() {
|
|
317
331
|
return `\
|
|
318
332
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
@@ -337,9 +351,10 @@ function claudeMd(appName) {
|
|
|
337
351
|
## Project
|
|
338
352
|
|
|
339
353
|
\`\`\`
|
|
340
|
-
src/pages/
|
|
341
|
-
src/components/
|
|
342
|
-
public/
|
|
354
|
+
src/pages/ ← one .js file per page, auto-discovered
|
|
355
|
+
src/components/ ← shared view fragments (JS functions returning HTML strings)
|
|
356
|
+
public/theme.css ← token definitions (hex values, colour overrides) — hex allowed here only
|
|
357
|
+
public/app.css ← layout and component overrides — var() references only, no hex
|
|
343
358
|
\`\`\`
|
|
344
359
|
|
|
345
360
|
## Start of every session
|
|
@@ -349,18 +364,13 @@ public/app.css ← global stylesheet
|
|
|
349
364
|
|
|
350
365
|
The MCP guide is the single source of truth. Follow it for all technical decisions, component usage, and the mandatory verification workflow.
|
|
351
366
|
|
|
352
|
-
##
|
|
353
|
-
|
|
354
|
-
Before planning any new page or component, use the \`frontend-design\` plugin to establish the visual direction:
|
|
355
|
-
|
|
356
|
-
1. Call the plugin's design tools with the page brief (what it does, who it's for, the tone)
|
|
357
|
-
2. The plugin will return decisions — colour palette, typography, spacing scale, component variants, layout approach
|
|
358
|
-
3. **Apply those decisions as CSS custom property overrides in \`public/app.css\`** — not as inline styles or class hacks
|
|
359
|
-
4. Document the chosen tokens at the top of \`public/app.css\` so every page stays consistent
|
|
367
|
+
## CSS and theming
|
|
360
368
|
|
|
361
|
-
|
|
369
|
+
- Hex values and colour token definitions go in \`public/theme.css\` — that is the only file where hex is allowed. The hex lint hook will fire on any other CSS file.
|
|
370
|
+
- \`public/app.css\` must not contain hex colour values. Use \`var(--ui-space-N)\` spacing tokens (\`--ui-space-1\` = 4px through \`--ui-space-24\` = 96px) for padding, margin, and gap. Use \`var(--ui-text-*)\` tokens (\`--ui-text-xs\` through \`--ui-text-4xl\`) for font sizes. Plain values like \`1rem\` are also fine when no token fits.
|
|
371
|
+
- Load order in \`meta.styles\`: \`/pulse-ui.css\` → \`/theme.css\` → \`/app.css\`.
|
|
362
372
|
|
|
363
|
-
|
|
373
|
+
When the user asks for layout or spacing changes, apply them directly — do not ask for confirmation on small CSS tweaks. Only pause for confirmation before large structural changes (new pages, major layout reworks).
|
|
364
374
|
|
|
365
375
|
## Before writing any code
|
|
366
376
|
|
|
@@ -376,7 +386,7 @@ Run these steps in order — do not declare work done without them:
|
|
|
376
386
|
2. \`pulse_review\` — switch into reviewer mode, read the source and rendered HTML critically, fix every issue before continuing
|
|
377
387
|
3. Write tests for every page you created or changed, run \`npm test\` (all pass), then \`npm run test:coverage\` — fix any untested branches
|
|
378
388
|
4. Navigate to the page in the browser and take a screenshot
|
|
379
|
-
5. Run Lighthouse (desktop then mobile) —
|
|
389
|
+
5. Run Lighthouse (desktop then mobile) — Accessibility, Best Practices, and SEO must all be 100
|
|
380
390
|
|
|
381
391
|
## Writing tests
|
|
382
392
|
|
package/src/mcp/server.js
CHANGED
|
@@ -281,19 +281,24 @@ Rules for the spec you write:
|
|
|
281
281
|
server.registerTool(
|
|
282
282
|
'pulse_create_component',
|
|
283
283
|
{
|
|
284
|
-
description: `
|
|
284
|
+
description: `Register a component you have already written to disk with the Write tool.
|
|
285
285
|
|
|
286
|
-
|
|
286
|
+
Workflow — always in this order:
|
|
287
|
+
1. Write the component file to src/components/<name>.js using the Write tool (user sees the diff)
|
|
288
|
+
2. Call pulse_create_component with just the name to confirm it was created correctly
|
|
289
|
+
|
|
290
|
+
Do NOT pass content here — write the file first, then call this tool.
|
|
291
|
+
|
|
292
|
+
Rules for the component you write:
|
|
287
293
|
- Import Pulse UI components from '@invisibleloop/pulse/ui' where applicable
|
|
288
294
|
- Use u- utility classes for spacing/layout — never inline styles
|
|
289
295
|
- Use var(--ui-*) CSS tokens for any colour references — never hardcode hex values
|
|
290
296
|
- Export named functions only (no default export needed)`,
|
|
291
297
|
inputSchema: {
|
|
292
|
-
name:
|
|
293
|
-
content: z.string().describe('JS — export named functions that return HTML strings'),
|
|
298
|
+
name: z.string().describe('Component filename without extension, e.g. "hero" or "nav"'),
|
|
294
299
|
},
|
|
295
300
|
},
|
|
296
|
-
|
|
301
|
+
({ name }) => {
|
|
297
302
|
const safeName = name.replace(/\.js$/, '')
|
|
298
303
|
const fullPath = path.join(COMPONENTS_DIR, `${safeName}.js`)
|
|
299
304
|
|
|
@@ -301,10 +306,15 @@ IMPORTANT rules for component content:
|
|
|
301
306
|
return text('Error: component name must not escape src/components/')
|
|
302
307
|
}
|
|
303
308
|
|
|
304
|
-
fs.
|
|
305
|
-
|
|
309
|
+
if (!fs.existsSync(fullPath)) {
|
|
310
|
+
return text(`Error: ${path.relative(ROOT, fullPath)} does not exist — write the file with the Write tool first, then call pulse_create_component.`)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const content = fs.readFileSync(fullPath, 'utf8')
|
|
314
|
+
const exports = [...content.matchAll(/^export\s+(?:function|const|async function)\s+(\w+)/gm)].map(m => m[1])
|
|
315
|
+
const exportNote = exports.length > 0 ? `Exports: ${exports.join(', ')}` : 'Warning: no named exports found — check the file exports at least one function.'
|
|
306
316
|
|
|
307
|
-
return text(`
|
|
317
|
+
return text(`Registered ${path.relative(ROOT, fullPath)}\n${exportNote}`)
|
|
308
318
|
}
|
|
309
319
|
)
|
|
310
320
|
|
|
@@ -315,42 +325,59 @@ IMPORTANT rules for component content:
|
|
|
315
325
|
server.registerTool(
|
|
316
326
|
'pulse_create_store',
|
|
317
327
|
{
|
|
318
|
-
description: `
|
|
328
|
+
description: `Register a pulse.store.js global store that you have already written to disk with the Write tool.
|
|
329
|
+
|
|
330
|
+
Workflow — always in this order:
|
|
331
|
+
1. Write pulse.store.js to the project root using the Write tool (user sees the diff)
|
|
332
|
+
2. Call pulse_create_store to validate the file you just wrote
|
|
333
|
+
|
|
334
|
+
Do NOT pass content here — write the file first, then call this tool.
|
|
319
335
|
|
|
320
|
-
|
|
336
|
+
Rules for the store you write:
|
|
321
337
|
- server fetchers must be async functions: async (ctx) => value
|
|
322
338
|
- mutations must be pure functions: (storeState, payload?) => partialState — no fetch, no side effects
|
|
323
339
|
- hydrate is required if the store has mutations (enables client-side store mutation dispatch)
|
|
324
340
|
- Register the store in your server file by passing it to createServer({ store })
|
|
325
341
|
- Pages subscribe to store keys via spec.store: ['user', 'settings']`,
|
|
326
|
-
inputSchema: {
|
|
327
|
-
content: z.string().describe('Complete pulse.store.js content — must export default a valid store object'),
|
|
328
|
-
},
|
|
342
|
+
inputSchema: {},
|
|
329
343
|
},
|
|
330
|
-
|
|
344
|
+
() => {
|
|
331
345
|
const storePath = path.join(ROOT, 'pulse.store.js')
|
|
332
346
|
|
|
333
|
-
|
|
347
|
+
if (!fs.existsSync(storePath)) {
|
|
348
|
+
return text('Error: pulse.store.js does not exist — write the file with the Write tool first, then call pulse_create_store.')
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const content = fs.readFileSync(storePath, 'utf8')
|
|
352
|
+
|
|
334
353
|
if (!content.includes('export default')) {
|
|
335
|
-
return text('Invalid: store must contain "export default { ... }"')
|
|
354
|
+
return text('Invalid: pulse.store.js must contain "export default { ... }"')
|
|
336
355
|
}
|
|
337
356
|
|
|
338
|
-
|
|
357
|
+
const hasServer = content.includes('server:')
|
|
358
|
+
const hasMutations = content.includes('mutations:')
|
|
359
|
+
const hasHydrate = content.includes('hydrate:')
|
|
339
360
|
|
|
340
|
-
|
|
361
|
+
const warnings = []
|
|
362
|
+
if (hasMutations && !hasHydrate) {
|
|
363
|
+
warnings.push('Warning: store has mutations but no hydrate — add hydrate: \'/pulse.store.js\' to enable client-side store dispatch.')
|
|
364
|
+
}
|
|
341
365
|
|
|
366
|
+
const lines = ['Validated pulse.store.js']
|
|
367
|
+
if (hasServer) lines.push(' ✓ server fetchers defined')
|
|
368
|
+
if (hasMutations) lines.push(' ✓ mutations defined')
|
|
369
|
+
if (hasHydrate) lines.push(' ✓ hydrate set')
|
|
370
|
+
if (warnings.length) lines.push('', ...warnings)
|
|
371
|
+
lines.push(`
|
|
342
372
|
Next steps:
|
|
343
373
|
1. Import and register it in your server file:
|
|
344
374
|
import store from './pulse.store.js'
|
|
345
375
|
createServer(specs, { store })
|
|
346
376
|
|
|
347
377
|
2. Declare which keys each page uses:
|
|
348
|
-
export default { route: '/dashboard', store: ['user', 'settings'], ... }
|
|
349
|
-
|
|
350
|
-
3. If you added mutations, set hydrate in pulse.store.js:
|
|
351
|
-
hydrate: '/pulse.store.js'
|
|
378
|
+
export default { route: '/dashboard', store: ['user', 'settings'], ... }`)
|
|
352
379
|
|
|
353
|
-
|
|
380
|
+
return text(lines.join('\n'))
|
|
354
381
|
}
|
|
355
382
|
)
|
|
356
383
|
|
|
@@ -447,9 +474,12 @@ server.registerTool(
|
|
|
447
474
|
const proc = spawn(process.execPath, [devScript, '--root', ROOT], { detached: true, stdio: 'ignore' })
|
|
448
475
|
proc.unref()
|
|
449
476
|
|
|
450
|
-
//
|
|
451
|
-
|
|
452
|
-
return text(
|
|
477
|
+
// Wait until the server is actually accepting requests
|
|
478
|
+
const ready = await waitForServer(port)
|
|
479
|
+
return text(ready
|
|
480
|
+
? `Dev server restarted on port ${port}`
|
|
481
|
+
: `Dev server started on port ${port} (did not respond within 10 s — check for errors)`
|
|
482
|
+
)
|
|
453
483
|
}
|
|
454
484
|
)
|
|
455
485
|
|
|
@@ -491,11 +521,12 @@ server.registerTool(
|
|
|
491
521
|
const proc = spawn(process.execPath, [startScript, '--root', ROOT, '--port', String(prodPort)], { detached: true, stdio: 'ignore' })
|
|
492
522
|
proc.unref()
|
|
493
523
|
|
|
494
|
-
// Wait
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
524
|
+
// Wait until the prod server is actually accepting requests
|
|
525
|
+
waitForServer(prodPort, 15_000).then(ready => resolve(text(
|
|
526
|
+
ready
|
|
527
|
+
? `Production build complete. Server running at http://localhost:${prodPort}/\nRun Lighthouse against this URL, then call pulse_restart_server to return to dev.`
|
|
528
|
+
: `Build complete but prod server on port ${prodPort} did not respond within 15 s — check for startup errors.`
|
|
529
|
+
)))
|
|
499
530
|
})
|
|
500
531
|
)
|
|
501
532
|
|
|
@@ -587,7 +618,7 @@ Work through every item. Fix anything that fails.
|
|
|
587
618
|
|
|
588
619
|
### Structure
|
|
589
620
|
- [ ] \`route\` is set explicitly — not left to auto-discovery
|
|
590
|
-
- [ ] \`hydrate\` is set
|
|
621
|
+
- [ ] \`hydrate\` is NOT set manually — the framework injects it automatically. Remove it if present.
|
|
591
622
|
- [ ] \`state\` shape is consistent — no fields that flip between null/string/boolean
|
|
592
623
|
- [ ] \`meta.title\` is meaningful and unique to this page
|
|
593
624
|
- [ ] \`meta.description\` is a real description, not "Built with Pulse"
|
|
@@ -636,6 +667,38 @@ Fix every issue you find. Then confirm what was changed.
|
|
|
636
667
|
}
|
|
637
668
|
)
|
|
638
669
|
|
|
670
|
+
// ---------------------------------------------------------------------------
|
|
671
|
+
// pulse_run_tests
|
|
672
|
+
// ---------------------------------------------------------------------------
|
|
673
|
+
|
|
674
|
+
server.registerTool(
|
|
675
|
+
'pulse_run_tests',
|
|
676
|
+
{
|
|
677
|
+
description: 'Run the project test suite (npm test). Returns the full output. Use after writing or editing specs to verify nothing is broken.',
|
|
678
|
+
inputSchema: {},
|
|
679
|
+
},
|
|
680
|
+
() => new Promise(resolve => {
|
|
681
|
+
const result = spawnSync('npm', ['test'], {
|
|
682
|
+
cwd: ROOT,
|
|
683
|
+
encoding: 'utf8',
|
|
684
|
+
timeout: 120_000,
|
|
685
|
+
})
|
|
686
|
+
|
|
687
|
+
const output = (result.stdout || '') + (result.stderr || '')
|
|
688
|
+
const exitCode = result.status ?? 1
|
|
689
|
+
|
|
690
|
+
if (exitCode === 0) {
|
|
691
|
+
// Surface just the per-suite summary lines — enough to confirm all passed
|
|
692
|
+
const summaryLines = output.split('\n').filter(l => /\d+ tests?:/.test(l) || /passed|failed/.test(l))
|
|
693
|
+
const summary = summaryLines.length ? summaryLines.join('\n') : output.slice(-2000)
|
|
694
|
+
resolve(text(`All tests passed.\n\n${summary}`))
|
|
695
|
+
} else {
|
|
696
|
+
// Return the tail of the output where failures are reported
|
|
697
|
+
resolve(text(`Tests failed (exit ${exitCode}):\n\n${output.slice(-4000)}`))
|
|
698
|
+
}
|
|
699
|
+
})
|
|
700
|
+
)
|
|
701
|
+
|
|
639
702
|
// ---------------------------------------------------------------------------
|
|
640
703
|
// pulse_check_version
|
|
641
704
|
// ---------------------------------------------------------------------------
|
|
@@ -735,6 +798,24 @@ server.registerTool(
|
|
|
735
798
|
// Helpers
|
|
736
799
|
// ---------------------------------------------------------------------------
|
|
737
800
|
|
|
801
|
+
/** Poll until the HTTP server on the given port accepts a connection, or timeout. */
|
|
802
|
+
async function waitForServer(port, maxMs = 10_000) {
|
|
803
|
+
const deadline = Date.now() + maxMs
|
|
804
|
+
while (Date.now() < deadline) {
|
|
805
|
+
const alive = await new Promise(resolve => {
|
|
806
|
+
const req = http.get(`http://localhost:${port}/`, { timeout: 500 }, res => {
|
|
807
|
+
res.resume()
|
|
808
|
+
resolve(true)
|
|
809
|
+
})
|
|
810
|
+
req.on('error', () => resolve(false))
|
|
811
|
+
req.on('timeout', () => { req.destroy(); resolve(false) })
|
|
812
|
+
})
|
|
813
|
+
if (alive) return true
|
|
814
|
+
await new Promise(r => setTimeout(r, 300))
|
|
815
|
+
}
|
|
816
|
+
return false
|
|
817
|
+
}
|
|
818
|
+
|
|
738
819
|
async function validateContent(content) {
|
|
739
820
|
// Write into PAGES_DIR so relative imports (e.g. '../components/nav.js') resolve correctly
|
|
740
821
|
fs.mkdirSync(PAGES_DIR, { recursive: true })
|
|
@@ -843,9 +924,10 @@ const PULSE_GUIDE_INDEX = `# Pulse Framework Guide
|
|
|
843
924
|
- \`pulse_validate\` — validate spec content. Call after every write. Fix all errors AND warnings.
|
|
844
925
|
- \`pulse_review\` — switch into reviewer mode and critically examine a spec you just built. Returns the source, rendered HTML, validator output, and a full review checklist. **Call this only after validate, Lighthouse (desktop + mobile), and tests all pass — it is the final phase before declaring done.**
|
|
845
926
|
- \`pulse_create_page\` — validate a page spec you already wrote to disk. **Always write the file with the Write tool first, then call this.** Never pass content to this tool.
|
|
846
|
-
- \`pulse_create_component\` —
|
|
847
|
-
- \`pulse_create_store\` —
|
|
927
|
+
- \`pulse_create_component\` — register a component you wrote with the Write tool. Write the file first, then call this.
|
|
928
|
+
- \`pulse_create_store\` — register a pulse.store.js you wrote with the Write tool. Write the file first, then call this.
|
|
848
929
|
- \`pulse_create_action\` — generate a correctly-structured action snippet.
|
|
930
|
+
- \`pulse_run_tests\` — run the project test suite (npm test). Use after writing or editing specs.
|
|
849
931
|
- \`pulse_fetch_page(url)\` — HTTP GET the dev server URL. Use to verify SSR output.
|
|
850
932
|
- \`pulse_restart_server\` — stop and restart the dev server.
|
|
851
933
|
- \`pulse_build\` — production build + starts prod server on devPort+1 for Lighthouse. Returns the URL. Call \`pulse_restart_server\` after to return to dev. **Slow — takes 30–60 s. Tell the user before calling.**
|
|
@@ -863,32 +945,15 @@ const PULSE_GUIDE_INDEX = `# Pulse Framework Guide
|
|
|
863
945
|
|
|
864
946
|
## MANDATORY: Verify every build
|
|
865
947
|
|
|
866
|
-
**
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
2. \`mcp__chrome-devtools__navigate_page\` — navigate to the dev server URL. After any CSS or asset change, follow immediately with \`mcp__chrome-devtools__evaluate_script\` running \`location.reload(true)\` to force a hard refresh.
|
|
876
|
-
3. \`mcp__chrome-devtools__take_screenshot\` — check layout, spacing, content visibility, no overflow. Also check headings for orphans (a single short word stranded on the last line). To detect them programmatically, run \`mcp__chrome-devtools__evaluate_script\` with:
|
|
877
|
-
\`\`\`js
|
|
878
|
-
Array.from(document.querySelectorAll('h1,h2,h3,h4,h5,h6')).filter(h=>{const r=document.createRange();r.selectNodeContents(h);const rects=r.getClientRects();if(rects.length<2)return false;const last=rects[rects.length-1];return last.width/h.getBoundingClientRect().width<0.4;}).map(h=>h.tagName+': '+h.textContent.trim())
|
|
879
|
-
\`\`\`
|
|
880
|
-
Any heading returned has a last line shorter than 40% of its width — fix it with \`balance: true\` on the \`heading()\` component.
|
|
881
|
-
4. \`mcp__chrome-devtools__list_console_messages\` (errors) — fix every JS error.
|
|
882
|
-
5. \`mcp__chrome-devtools__list_network_requests\` (failed) — fix every 404 or failed fetch.
|
|
883
|
-
6. \`pulse_fetch_page\` — pass the full URL e.g. \`{ url: "http://localhost:3000/" }\`. Confirm SSR renders expected content, no blank body.
|
|
884
|
-
7. \`pulse_build\` — this builds for production AND starts a prod server on devPort+1. Wait for it to return the prod URL.
|
|
885
|
-
8. \`mcp__chrome-devtools__navigate_page\` — navigate to the production URL returned by \`pulse_build\` (e.g. \`http://localhost:3001/\`).
|
|
886
|
-
9. \`mcp__chrome-devtools__lighthouse_audit\` with \`{ "strategy": "desktop" }\` — run against the production URL. All four scores (Performance, Accessibility, Best Practices, SEO) must be 100. Report the actual scores and fix every failing audit before continuing.
|
|
887
|
-
10. \`mcp__chrome-devtools__lighthouse_audit\` with \`{ "strategy": "mobile" }\` — run the same audit for mobile. All four scores must also be 100. Fix any failures before continuing.
|
|
888
|
-
11. \`pulse_restart_server\` — shut down the prod server and return to dev.
|
|
889
|
-
12. \`mcp__chrome-devtools__list_pages\` then \`mcp__chrome-devtools__close_page\` — close **every** page returned by \`list_pages\` to shut the browser down entirely. \`pageId\` must be a number: \`{ pageId: 2 }\` ✓ — NOT \`{ pageId: "2" }\` ✗ (string will fail). Loop through all page IDs and close each one.
|
|
890
|
-
|
|
891
|
-
Do not declare success until all steps pass (step 12 is cleanup — always run it). If any step reveals a problem, fix it and repeat from step 2.
|
|
948
|
+
**After writing or editing any page spec, you MUST run \`/verify\` before declaring done.**
|
|
949
|
+
|
|
950
|
+
\`/verify\` is the single canonical verification workflow. It runs: validate → screenshot → console check → network check → SSR check → production build → Lighthouse desktop → Lighthouse mobile → performance trace → code review → writes the \`.pulse-verified\` stamp. The stop hook checks this stamp — if it is missing or older than the last spec edit, the hook will block and make you run verification again.
|
|
951
|
+
|
|
952
|
+
**Do not replicate \`/verify\` steps manually.** Running the individual tools yourself does not write the stamp. The agent will be blocked at the end regardless.
|
|
953
|
+
|
|
954
|
+
**RULE: NEVER run \`lighthouse_audit\` against the dev server.** Dev mode serves unminified source files — scores are meaningless. \`/verify\` handles this correctly by calling \`pulse_build\` first.
|
|
955
|
+
|
|
956
|
+
**Before calling any slow tool (\`pulse_build\`, \`lighthouse_audit\`), output a short status message to the user.** Example: "Building for production — this takes ~30 s…". Do not call slow tools silently.
|
|
892
957
|
|
|
893
958
|
${CHECKLIST}`
|
|
894
959
|
|