@invisibleloop/pulse 0.17.0 → 0.17.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/commands/build-page.md +1 -1
- package/.claude/commands/verify.md +2 -2
- package/.claude/pulse-checklist.md +3 -2
- package/CLAUDE.md +5 -1
- package/README.md +3 -3
- package/docs/.claude/pulse-checklist.md +3 -2
- package/docs/src/pages/project-structure.js +2 -0
- package/examples/.claude/pulse-checklist.md +3 -2
- package/package.json +1 -1
- package/public/.pulse-ui-version +1 -1
- package/src/agent/checklist.md +3 -2
- package/src/agent/commands/verify.md +2 -2
- package/src/agent/coverage-check.js +5 -0
- package/src/agent/guide-spec.md +6 -0
- package/src/agent/skills/build-page/SKILL.md +7 -5
- package/src/agent/skills/verify/SKILL.md +2 -2
- package/src/agent/workflow.md +10 -0
- package/src/cli/scaffold.js +18 -2
- package/src/mcp/mcp.test.js +62 -2
- package/src/mcp/server.js +27 -0
|
@@ -50,7 +50,7 @@ Check for:
|
|
|
50
50
|
- Layout looks correct
|
|
51
51
|
- No console errors via `mcp__chrome-devtools__list_console_messages`
|
|
52
52
|
|
|
53
|
-
Then ask the user: "Happy with the design and layout, or would you like any changes before I run tests and Lighthouse?" — if your host provides a question tool (e.g. AskUserQuestion), offer the choices `["Yes, looks good — run tests and Lighthouse", "I'd like some changes first"]`;
|
|
53
|
+
Then ask the user: "Happy with the design and layout, or would you like any changes before I run tests and Lighthouse?" — if your host provides a question tool (e.g. AskUserQuestion), offer the choices `["Yes, looks good — run tests and Lighthouse", "I'd like some changes first"]`; it waits for the answer without ending the turn. If you must ask in plain prose and end your turn, call `pulse_await_approval` first so the Stop hooks let the turn end. If a `VERIFY REQUIRED` stop-hook block fires while your question is unanswered, that is not permission to continue — call `pulse_await_approval`, re-ask, and end your turn.
|
|
54
54
|
|
|
55
55
|
**Do not proceed to step 7 until the user explicitly confirms.** If they want changes, make them and return to step 6.
|
|
56
56
|
|
|
@@ -15,8 +15,8 @@ If $ARGUMENTS is provided, use it as the file path or route. Otherwise, identify
|
|
|
15
15
|
If this is a new page build (came through `pulse_intake` or `build-page`):
|
|
16
16
|
|
|
17
17
|
1. Take a screenshot and describe what you see to the user.
|
|
18
|
-
2. Ask the user: "Happy with the design and layout, or would you like any changes before I run Lighthouse?" — if your host provides a question tool (e.g. AskUserQuestion), offer the choices `["Yes, looks good — proceed", "I'd like some changes first"]`;
|
|
19
|
-
3. **Do not proceed to step 3 until the user explicitly confirms.** If they want changes, stop here, make the changes, then restart from step 2. Lighthouse takes ~90 seconds — do not waste it on a design the user hasn't approved.
|
|
18
|
+
2. Ask the user: "Happy with the design and layout, or would you like any changes before I run Lighthouse?" — if your host provides a question tool (e.g. AskUserQuestion), offer the choices `["Yes, looks good — proceed", "I'd like some changes first"]`; it waits for the answer without ending the turn. If you must ask in plain prose and end your turn, call `pulse_await_approval` first so the Stop hooks let the turn end.
|
|
19
|
+
3. **Do not proceed to step 3 until the user explicitly confirms.** If they want changes, stop here, make the changes, then restart from step 2. Lighthouse takes ~90 seconds — do not waste it on a design the user hasn't approved. If a `VERIFY REQUIRED` stop-hook block fires while your question is unanswered, that is not permission to continue — call `pulse_await_approval`, re-ask, and end your turn.
|
|
20
20
|
|
|
21
21
|
### 3. Validate the spec
|
|
22
22
|
|
|
@@ -66,8 +66,8 @@ Before finishing any spec, verify every point below. Fix anything that fails.
|
|
|
66
66
|
```js
|
|
67
67
|
const card = ({ title, body }) => `<div class="card"><h3>${title}</h3><p>${body}</p></div>`
|
|
68
68
|
```
|
|
69
|
-
- **Create a shared component in `src/
|
|
70
|
-
- **Do not abstract a pattern that appears only once.** Duplication is cheaper than the wrong abstraction.
|
|
69
|
+
- **Create a shared component in `src/components/` when the same pattern is needed across 2 or more different specs.** A named export that returns an HTML string — same shape as the `layout()` pattern in the routing guide. Never paste a section's HTML into a second spec: extract it at the second use, then import it from both. (`src/ui/` is the framework's own library inside the package — project-level shared code always goes in `src/components/`.)
|
|
70
|
+
- **Do not abstract a pattern that appears only once.** Duplication is cheaper than the wrong abstraction. Within a single spec, wait until the third use before extracting a helper; across specs, extract at the second use.
|
|
71
71
|
|
|
72
72
|
### Correctness
|
|
73
73
|
|
|
@@ -106,6 +106,7 @@ Before finishing any spec, verify every point below. Fix anything that fails.
|
|
|
106
106
|
|
|
107
107
|
- **`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.
|
|
108
108
|
- **Load order in `meta.styles`:** `pulse-ui.css` → `theme.css` → `app.css`. Theme tokens must be defined before `app.css` references them.
|
|
109
|
+
- **CSS for a shared section lives in the shared `app.css`, once.** Every spec that renders the section lists the same file in `meta.styles`. Never copy a CSS block into a second per-page stylesheet — two copies drift apart. Per-page stylesheets are for styles only one page uses.
|
|
109
110
|
|
|
110
111
|
### Security
|
|
111
112
|
|
package/CLAUDE.md
CHANGED
|
@@ -6,7 +6,7 @@ Pulse is a spec-first frontend framework. The spec is the source of truth. No co
|
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npm run dev # start dev server at http://localhost:3000 (pulse_build serves production on 3001)
|
|
9
|
-
npm test # run
|
|
9
|
+
npm test # run the full unit test suite
|
|
10
10
|
npm run build # bundle for production → public/dist/
|
|
11
11
|
```
|
|
12
12
|
|
|
@@ -37,6 +37,8 @@ public/
|
|
|
37
37
|
|
|
38
38
|
A spec is a plain JS object. Every property is optional except `route` and `view`. `state` defaults to `{}` — omit it on purely server-rendered pages.
|
|
39
39
|
|
|
40
|
+
**One spec = one page = one file.** A site is multiple specs, one per route. A dynamic route (`route: '/blog/:slug'`) is still a single spec serving every matching URL — never one spec per item, and never several pages folded into one spec with `ctx` conditionals. Shared sections live in `src/components/` and are imported by each spec.
|
|
41
|
+
|
|
40
42
|
```js
|
|
41
43
|
export const mySpec = {
|
|
42
44
|
route: '/path', // URL pattern, supports :params
|
|
@@ -360,6 +362,8 @@ There are two CSS files and they have distinct roles — do not mix them:
|
|
|
360
362
|
|
|
361
363
|
**`app.css` must never contain hex values.** `rgba()` and `hsla()` are allowed for translucency — but if the same translucent colour is used more than once, extract it as a token in `theme.css` (e.g. `--brand-overlay-12: rgba(0,0,0,0.12)`). If you need to define or override a colour token, put it in `public/theme.css` and reference it via `var()` in `app.css`.
|
|
362
364
|
|
|
365
|
+
**CSS for a section used by more than one spec lives in the shared `app.css`, once** — every spec that renders the section lists the same file in `meta.styles`. Never copy a CSS block into a second per-page stylesheet; two copies drift. Per-page stylesheets are only for styles a single page uses.
|
|
366
|
+
|
|
363
367
|
```css
|
|
364
368
|
/* public/theme.css — hex values live here */
|
|
365
369
|
[data-theme="light"] {
|
package/README.md
CHANGED
|
@@ -7,7 +7,6 @@ Write a plain JavaScript object that describes what a page does. Pulse handles r
|
|
|
7
7
|
```js
|
|
8
8
|
export default {
|
|
9
9
|
route: '/counter',
|
|
10
|
-
hydrate: '/src/pages/counter.js',
|
|
11
10
|
meta: {
|
|
12
11
|
title: 'Counter',
|
|
13
12
|
styles: ['/app.css'],
|
|
@@ -54,7 +53,7 @@ Full documentation: **[pulseframework.dev](https://pulseframework.dev)**
|
|
|
54
53
|
|
|
55
54
|
**The spec is the source of truth.** Every page is a single JS object — data fetching, state, mutations, async actions, and view all in one place. No separate files for routes, controllers, or templates.
|
|
56
55
|
|
|
57
|
-
**Performance is built in.** Streaming SSR, immutable asset caching, and zero layout shift are automatic. Every scaffolded project targets Lighthouse 100
|
|
56
|
+
**Performance is built in.** Streaming SSR, immutable asset caching, and zero layout shift are automatic. Every scaffolded project targets Lighthouse 100 for Accessibility, Best Practices, and SEO — and is fast by design (streaming HTML before data resolves, CLS 0.00).
|
|
58
57
|
|
|
59
58
|
**No client-side dependencies.** Pulse ships no runtime framework to the browser — only a small hydration bundle (~2 kB brotli) that binds your spec's mutations and actions to the DOM.
|
|
60
59
|
|
|
@@ -83,12 +82,13 @@ Full reference at [pulseframework.dev](https://pulseframework.dev):
|
|
|
83
82
|
- **Guard** — authentication and authorisation before data fetches
|
|
84
83
|
- **Validation & constraints** — declarative form validation and state bounds
|
|
85
84
|
- **Raw responses** — RSS feeds, sitemaps, JSON APIs
|
|
85
|
+
- **Markdown** — built-in parser for blog posts and docs pages, zero browser JS
|
|
86
86
|
- **Performance** — how the framework hits Lighthouse 100
|
|
87
87
|
- **UI components** — 50+ built-in components
|
|
88
88
|
|
|
89
89
|
## Status
|
|
90
90
|
|
|
91
|
-
This is an early-access release. The core architecture is stable and production-quality, but the API may evolve before v1. Feedback and issues welcome on [GitHub](https://github.com/invisibleloop/pulse).
|
|
91
|
+
This is an early-access release. The core architecture is stable and production-quality, but the API may evolve before v1. Feedback and issues welcome on [GitHub](https://github.com/invisibleloop/pulse-framework).
|
|
92
92
|
|
|
93
93
|
## License
|
|
94
94
|
|
|
@@ -66,8 +66,8 @@ Before finishing any spec, verify every point below. Fix anything that fails.
|
|
|
66
66
|
```js
|
|
67
67
|
const card = ({ title, body }) => `<div class="card"><h3>${title}</h3><p>${body}</p></div>`
|
|
68
68
|
```
|
|
69
|
-
- **Create a shared component in `src/
|
|
70
|
-
- **Do not abstract a pattern that appears only once.** Duplication is cheaper than the wrong abstraction.
|
|
69
|
+
- **Create a shared component in `src/components/` when the same pattern is needed across 2 or more different specs.** A named export that returns an HTML string — same shape as the `layout()` pattern in the routing guide. Never paste a section's HTML into a second spec: extract it at the second use, then import it from both. (`src/ui/` is the framework's own library inside the package — project-level shared code always goes in `src/components/`.)
|
|
70
|
+
- **Do not abstract a pattern that appears only once.** Duplication is cheaper than the wrong abstraction. Within a single spec, wait until the third use before extracting a helper; across specs, extract at the second use.
|
|
71
71
|
|
|
72
72
|
### Correctness
|
|
73
73
|
|
|
@@ -106,6 +106,7 @@ Before finishing any spec, verify every point below. Fix anything that fails.
|
|
|
106
106
|
|
|
107
107
|
- **`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.
|
|
108
108
|
- **Load order in `meta.styles`:** `pulse-ui.css` → `theme.css` → `app.css`. Theme tokens must be defined before `app.css` references them.
|
|
109
|
+
- **CSS for a shared section lives in the shared `app.css`, once.** Every spec that renders the section lists the same file in `meta.styles`. Never copy a CSS block into a second per-page stylesheet — two copies drift apart. Per-page stylesheets are for styles only one page uses.
|
|
109
110
|
|
|
110
111
|
### Security
|
|
111
112
|
|
|
@@ -82,6 +82,8 @@ export default {
|
|
|
82
82
|
${section('one-file-per-page', 'One file per page')}
|
|
83
83
|
<p>Pulse enforces <strong>one spec per file</strong>. Every page is self-contained — state, view, mutations, actions, and validation in one place. This is not a style preference. It is how the framework eliminates the question of where things live.</p>
|
|
84
84
|
|
|
85
|
+
<p>This holds for dynamic routes too: <code>route: '/blog/:slug'</code> is <strong>one spec serving every matching URL</strong> — the <code>:param</code> does the multiplying, so a thousand blog posts are still a single file. A site is therefore a small collection of specs, one per route, sharing layouts and sections from <code>src/components/</code> rather than repeating them.</p>
|
|
86
|
+
|
|
85
87
|
${callout('tip', 'For large pages, view fragments and helpers can be imported from other modules. The spec file remains the coordination point — the structure is always clear regardless of how the implementation is organised.')}
|
|
86
88
|
`,
|
|
87
89
|
}),
|
|
@@ -66,8 +66,8 @@ Before finishing any spec, verify every point below. Fix anything that fails.
|
|
|
66
66
|
```js
|
|
67
67
|
const card = ({ title, body }) => `<div class="card"><h3>${title}</h3><p>${body}</p></div>`
|
|
68
68
|
```
|
|
69
|
-
- **Create a shared component in `src/
|
|
70
|
-
- **Do not abstract a pattern that appears only once.** Duplication is cheaper than the wrong abstraction.
|
|
69
|
+
- **Create a shared component in `src/components/` when the same pattern is needed across 2 or more different specs.** A named export that returns an HTML string — same shape as the `layout()` pattern in the routing guide. Never paste a section's HTML into a second spec: extract it at the second use, then import it from both. (`src/ui/` is the framework's own library inside the package — project-level shared code always goes in `src/components/`.)
|
|
70
|
+
- **Do not abstract a pattern that appears only once.** Duplication is cheaper than the wrong abstraction. Within a single spec, wait until the third use before extracting a helper; across specs, extract at the second use.
|
|
71
71
|
|
|
72
72
|
### Correctness
|
|
73
73
|
|
|
@@ -106,6 +106,7 @@ Before finishing any spec, verify every point below. Fix anything that fails.
|
|
|
106
106
|
|
|
107
107
|
- **`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.
|
|
108
108
|
- **Load order in `meta.styles`:** `pulse-ui.css` → `theme.css` → `app.css`. Theme tokens must be defined before `app.css` references them.
|
|
109
|
+
- **CSS for a shared section lives in the shared `app.css`, once.** Every spec that renders the section lists the same file in `meta.styles`. Never copy a CSS block into a second per-page stylesheet — two copies drift apart. Per-page stylesheets are for styles only one page uses.
|
|
109
110
|
|
|
110
111
|
### Security
|
|
111
112
|
|
package/package.json
CHANGED
package/public/.pulse-ui-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.17.
|
|
1
|
+
0.17.1
|
package/src/agent/checklist.md
CHANGED
|
@@ -66,8 +66,8 @@ Before finishing any spec, verify every point below. Fix anything that fails.
|
|
|
66
66
|
```js
|
|
67
67
|
const card = ({ title, body }) => `<div class="card"><h3>${title}</h3><p>${body}</p></div>`
|
|
68
68
|
```
|
|
69
|
-
- **Create a shared component in `src/
|
|
70
|
-
- **Do not abstract a pattern that appears only once.** Duplication is cheaper than the wrong abstraction.
|
|
69
|
+
- **Create a shared component in `src/components/` when the same pattern is needed across 2 or more different specs.** A named export that returns an HTML string — same shape as the `layout()` pattern in the routing guide. Never paste a section's HTML into a second spec: extract it at the second use, then import it from both. (`src/ui/` is the framework's own library inside the package — project-level shared code always goes in `src/components/`.)
|
|
70
|
+
- **Do not abstract a pattern that appears only once.** Duplication is cheaper than the wrong abstraction. Within a single spec, wait until the third use before extracting a helper; across specs, extract at the second use.
|
|
71
71
|
|
|
72
72
|
### Correctness
|
|
73
73
|
|
|
@@ -106,6 +106,7 @@ Before finishing any spec, verify every point below. Fix anything that fails.
|
|
|
106
106
|
|
|
107
107
|
- **`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.
|
|
108
108
|
- **Load order in `meta.styles`:** `pulse-ui.css` → `theme.css` → `app.css`. Theme tokens must be defined before `app.css` references them.
|
|
109
|
+
- **CSS for a shared section lives in the shared `app.css`, once.** Every spec that renders the section lists the same file in `meta.styles`. Never copy a CSS block into a second per-page stylesheet — two copies drift apart. Per-page stylesheets are for styles only one page uses.
|
|
109
110
|
|
|
110
111
|
### Security
|
|
111
112
|
|
|
@@ -15,8 +15,8 @@ If $ARGUMENTS is provided, use it as the file path or route. Otherwise, identify
|
|
|
15
15
|
If this is a new page build (came through `pulse_intake` or `build-page`):
|
|
16
16
|
|
|
17
17
|
1. Take a screenshot and describe what you see to the user.
|
|
18
|
-
2. Ask the user: "Happy with the design and layout, or would you like any changes before I run Lighthouse?" — if your host provides a question tool (e.g. AskUserQuestion), offer the choices `["Yes, looks good — proceed", "I'd like some changes first"]`;
|
|
19
|
-
3. **Do not proceed to step 3 until the user explicitly confirms.** If they want changes, stop here, make the changes, then restart from step 2. Lighthouse takes ~90 seconds — do not waste it on a design the user hasn't approved.
|
|
18
|
+
2. Ask the user: "Happy with the design and layout, or would you like any changes before I run Lighthouse?" — if your host provides a question tool (e.g. AskUserQuestion), offer the choices `["Yes, looks good — proceed", "I'd like some changes first"]`; it waits for the answer without ending the turn. If you must ask in plain prose and end your turn, call `pulse_await_approval` first so the Stop hooks let the turn end.
|
|
19
|
+
3. **Do not proceed to step 3 until the user explicitly confirms.** If they want changes, stop here, make the changes, then restart from step 2. Lighthouse takes ~90 seconds — do not waste it on a design the user hasn't approved. If a `VERIFY REQUIRED` stop-hook block fires while your question is unanswered, that is not permission to continue — call `pulse_await_approval`, re-ask, and end your turn.
|
|
20
20
|
|
|
21
21
|
### 3. Validate the spec
|
|
22
22
|
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
import { execSync } from 'child_process'
|
|
14
14
|
import fs from 'fs'
|
|
15
15
|
|
|
16
|
+
// Agent is ending its turn to wait for the user's answer (design-approval gate) —
|
|
17
|
+
// let the turn end. The marker is consumed on the user's next prompt, so the
|
|
18
|
+
// gates return in full force afterwards.
|
|
19
|
+
if (fs.existsSync('.pulse-awaiting-approval')) process.exit(0)
|
|
20
|
+
|
|
16
21
|
// No test files → nothing to check
|
|
17
22
|
const hasTests = fs.existsSync('src/pages') && (function scan(d) {
|
|
18
23
|
for (const f of fs.readdirSync(d)) {
|
package/src/agent/guide-spec.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
Pulse is a spec-first frontend framework. Pages are JS files that export a default spec object.
|
|
2
2
|
|
|
3
|
+
## Mental model — one spec per page
|
|
4
|
+
|
|
5
|
+
- **One spec = one route = one file** under `src/pages/`. Every page of a site is its own spec file — a site is a collection of specs, not one big spec.
|
|
6
|
+
- **A dynamic route is still one spec.** `route: '/blog/:slug'` is a single file serving *every* blog post URL — the `:param` does the multiplying. Never create one spec per item (`post-1.js`, `post-2.js`, …), and never merge several distinct pages into one spec with conditionals on `ctx`.
|
|
7
|
+
- **Specs share code, they don't repeat it.** Shared sections, layouts, nav, and footer live in `src/components/` and are imported by each spec that needs them (see `pulse://guide/routing` for the `layout()` pattern).
|
|
8
|
+
|
|
3
9
|
## Spec structure
|
|
4
10
|
|
|
5
11
|
```js
|
|
@@ -40,11 +40,11 @@ Write the spec file. Follow these rules:
|
|
|
40
40
|
|
|
41
41
|
Use `pulse_validate` on the spec. Fix any errors before continuing.
|
|
42
42
|
|
|
43
|
-
### 5.
|
|
43
|
+
### 5. Restart and reload
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
Pages under `src/pages/` are auto-discovered — no server entry file to edit. Call `pulse_restart_server`, then `navigate_page` so the browser is not on a stale tab.
|
|
46
46
|
|
|
47
|
-
### 6. Screenshot
|
|
47
|
+
### 6. Screenshot + design approval
|
|
48
48
|
|
|
49
49
|
Use the chrome-devtools navigate_page tool to load the page, then take_screenshot.
|
|
50
50
|
|
|
@@ -53,13 +53,15 @@ Check for:
|
|
|
53
53
|
- Layout looks correct
|
|
54
54
|
- No console errors via list_console_messages
|
|
55
55
|
|
|
56
|
+
Then ask the user: *"Happy with the design and layout, or would you like any changes before I run tests and Lighthouse?"* — use your host's question tool if it has one (e.g. AskUserQuestion); it waits for the answer without ending the turn. If you must ask in plain prose and end your turn, call `pulse_await_approval` first so the Stop hooks let the turn end. **Do not proceed until the user explicitly approves.** If a `VERIFY REQUIRED` stop-hook block fires while your question is unanswered, that is not permission to continue — call `pulse_await_approval`, re-ask, and end your turn.
|
|
57
|
+
|
|
56
58
|
### 7. Lighthouse
|
|
57
59
|
|
|
58
|
-
Run lighthouse_audit (desktop, then mobile)
|
|
60
|
+
Run lighthouse_audit (desktop, then mobile) against the production server — call `pulse_build` first and navigate to the production URL. **Pass bar: Accessibility, Best Practices, and SEO must all be 100.** Performance is reported but not gated (it varies with machine load).
|
|
59
61
|
|
|
60
62
|
### 8. Run the checklist
|
|
61
63
|
|
|
62
|
-
Go through `.
|
|
64
|
+
Go through `.claude/pulse-checklist.md` and confirm every applicable point passes. Fix anything that fails.
|
|
63
65
|
|
|
64
66
|
### 9. Write tests
|
|
65
67
|
|
|
@@ -34,8 +34,8 @@ Navigate to the page route with `mcp__chrome-devtools__navigate_page`, then take
|
|
|
34
34
|
**If `pulse_intake` ran earlier in this session** (i.e. this is a new page or site, not an edit or bug fix):
|
|
35
35
|
|
|
36
36
|
1. Show the screenshot to the user.
|
|
37
|
-
2. Ask: *"Happy with the layout and direction, or any changes before I run Lighthouse?"*
|
|
38
|
-
3. **Stop and wait for their response.** Do not proceed to step 4b or Lighthouse until the user explicitly approves.
|
|
37
|
+
2. Ask: *"Happy with the layout and direction, or any changes before I run Lighthouse?"* — use your host's question tool if it has one (e.g. AskUserQuestion); it waits for the answer without ending the turn. If you must ask in plain prose and end your turn, call `pulse_await_approval` first so the Stop hooks let the turn end.
|
|
38
|
+
3. **Stop and wait for their response.** Do not proceed to step 4b or Lighthouse until the user explicitly approves. If a `VERIFY REQUIRED` stop-hook block fires while your question is unanswered, that is not permission to continue — call `pulse_await_approval`, re-ask, and end your turn.
|
|
39
39
|
|
|
40
40
|
If the user requests changes: make edits, restart the server, take a new screenshot, describe what changed, ask again. Repeat until approved. No Lighthouse between design rounds.
|
|
41
41
|
|
package/src/agent/workflow.md
CHANGED
|
@@ -59,6 +59,8 @@ If the user's request is already very specific (a one-liner edit, a bug fix, "ad
|
|
|
59
59
|
|
|
60
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).
|
|
61
61
|
|
|
62
|
+
**Check `src/components/` for shared sections before building.** If a layout, nav, footer, CTA strip, or section you're about to write already exists there — or already exists inline in another spec — reuse or extract it now rather than writing a second copy. Catching duplication here is cheap; catching it at review (phase 7) means the expensive gates already ran against the duplicated version.
|
|
63
|
+
|
|
62
64
|
Do not fetch every guide section for every task. Fetch what you need.
|
|
63
65
|
|
|
64
66
|
---
|
|
@@ -157,6 +159,8 @@ Or use the guide-components table. Never write these patterns from scratch in Mo
|
|
|
157
159
|
|
|
158
160
|
If you're about to type `class="hero"` or `class="product-card"` in Mode A, stop — use the component instead.
|
|
159
161
|
|
|
162
|
+
**Reuse across specs (both modes):** one spec = one page; a site is many specs sharing code. Before writing a new section, check `src/components/` — and if a section you're writing already exists in another spec, extract it to `src/components/` *now* and import it from both, rather than pasting a copy. Same for CSS: a shared section's styles go in the shared `app.css` (listed in `meta.styles` by every spec that uses it), never duplicated into per-page stylesheets.
|
|
163
|
+
|
|
160
164
|
**Mode B — Creative override (raw HTML throughout)**
|
|
161
165
|
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
166
|
|
|
@@ -236,6 +240,12 @@ Do this before every screenshot, console check, or visual inspection. Never debu
|
|
|
236
240
|
3. **Ask the user directly:** *"Happy with the layout and direction, or any changes before I run tests and Lighthouse?"*
|
|
237
241
|
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.
|
|
238
242
|
|
|
243
|
+
**How to wait without tripping the Stop hooks.** The project's Stop hooks (missing tests, coverage, verify stamp) block any turn that ends with unverified spec edits — they cannot tell "abandoning the work" from "pausing for the user's answer". So:
|
|
244
|
+
|
|
245
|
+
- **Preferred:** ask the question with your host's question tool (e.g. AskUserQuestion in Claude Code). A question tool waits for the answer *without ending the turn*, so the Stop hooks never fire.
|
|
246
|
+
- **Otherwise:** call `pulse_await_approval` immediately before asking in plain prose and ending your turn. It writes a one-turn marker the Stop hooks respect; the marker is consumed when the user replies.
|
|
247
|
+
- **Never** respond to a `VERIFY REQUIRED` block by running `/verify` while your approval question is unanswered. The block does not override the approval gate — an unanswered question means the design is not approved, and Lighthouse on an unapproved design wastes ~90 seconds and pre-empts the user's changes.
|
|
248
|
+
|
|
239
249
|
**If the user requests changes:**
|
|
240
250
|
- Make the edits
|
|
241
251
|
- Take a new screenshot
|
package/src/cli/scaffold.js
CHANGED
|
@@ -188,12 +188,26 @@ ${port !== 3000 ? ` port: ${port},\n` : ''}${agent && agent !== 'claude' ? ` a
|
|
|
188
188
|
]
|
|
189
189
|
}
|
|
190
190
|
],
|
|
191
|
+
UserPromptSubmit: [
|
|
192
|
+
{
|
|
193
|
+
hooks: [
|
|
194
|
+
{
|
|
195
|
+
type: 'command',
|
|
196
|
+
// Consume the awaiting-approval marker: it grants exactly one turn-end
|
|
197
|
+
// (the one that asks the user a blocking question). As soon as the user
|
|
198
|
+
// replies, the Stop gates are back in force for the next turn.
|
|
199
|
+
command: `node -e "try{require('fs').unlinkSync('.pulse-awaiting-approval')}catch{}"`,
|
|
200
|
+
statusMessage: 'Clearing approval pause...',
|
|
201
|
+
}
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
],
|
|
191
205
|
Stop: [
|
|
192
206
|
{
|
|
193
207
|
hooks: [
|
|
194
208
|
{
|
|
195
209
|
type: 'command',
|
|
196
|
-
command: `node -e "const fs=require('fs');const{execSync}=require('child_process');let changed=new Set();let hasGit=false;try{execSync('git rev-parse --is-inside-work-tree',{stdio:'pipe'});hasGit=true;execSync('git status --porcelain',{encoding:'utf8'}).split('\\n').forEach(l=>{const f=l.slice(3).trim().split(' -> ').pop();if(f)changed.add(f);});}catch{}if(!hasGit&&fs.existsSync('src/pages')){for(const f of fs.readdirSync('src/pages')){if(f.endsWith('.js')&&!f.endsWith('.test.js'))changed.add('src/pages/'+f);}}const specs=[...changed].filter(f=>f.match(/^src\\/pages\\/.+\\.js$/)&&!f.endsWith('.test.js'));const missing=specs.filter(f=>{const t=f.replace(/\\.js$/,'.test.js');return!fs.existsSync(t)&&!changed.has(t);});if(missing.length){process.stdout.write(JSON.stringify({decision:'block',reason:'TESTS MISSING: '+missing.join(', ')+' — you must write tests before finishing. Use renderSync/render from @invisibleloop/pulse/testing to test view output, mutations, and any extracted logic functions. Run the tests with node to confirm they pass.'}));}"`,
|
|
210
|
+
command: `node -e "const fs=require('fs');if(fs.existsSync('.pulse-awaiting-approval'))process.exit(0);const{execSync}=require('child_process');let changed=new Set();let hasGit=false;try{execSync('git rev-parse --is-inside-work-tree',{stdio:'pipe'});hasGit=true;execSync('git status --porcelain',{encoding:'utf8'}).split('\\n').forEach(l=>{const f=l.slice(3).trim().split(' -> ').pop();if(f)changed.add(f);});}catch{}if(!hasGit&&fs.existsSync('src/pages')){for(const f of fs.readdirSync('src/pages')){if(f.endsWith('.js')&&!f.endsWith('.test.js'))changed.add('src/pages/'+f);}}const specs=[...changed].filter(f=>f.match(/^src\\/pages\\/.+\\.js$/)&&!f.endsWith('.test.js'));const missing=specs.filter(f=>{const t=f.replace(/\\.js$/,'.test.js');return!fs.existsSync(t)&&!changed.has(t);});if(missing.length){process.stdout.write(JSON.stringify({decision:'block',reason:'TESTS MISSING: '+missing.join(', ')+' — you must write tests before finishing. Use renderSync/render from @invisibleloop/pulse/testing to test view output, mutations, and any extracted logic functions. Run the tests with node to confirm they pass.\\n\\nEXCEPTION — waiting on the user: if you are ending this turn to wait for the user\\'s answer (e.g. the design-approval gate), call pulse_await_approval first, then ask and end your turn. Do not write tests or run /verify on an unapproved design.'}));}"`,
|
|
197
211
|
statusMessage: 'Checking for missing tests...',
|
|
198
212
|
},
|
|
199
213
|
{
|
|
@@ -203,7 +217,7 @@ ${port !== 3000 ? ` port: ${port},\n` : ''}${agent && agent !== 'claude' ? ` a
|
|
|
203
217
|
},
|
|
204
218
|
{
|
|
205
219
|
type: 'command',
|
|
206
|
-
command: `node -e "const fs=require('fs');const{execSync}=require('child_process');let specs=[];try{const out=execSync('git status --porcelain',{encoding:'utf8'});out.split('\\n').forEach(l=>{const f=l.slice(3).trim().split(' -> ').pop();if(f&&f.match(/^src\\/pages\\/.+\\.js$/)&&!f.endsWith('.test.js'))specs.push(f);});}catch{if(fs.existsSync('src/pages'))for(const f of fs.readdirSync('src/pages')){if(f.endsWith('.js')&&!f.endsWith('.test.js'))specs.push('src/pages/'+f);}}if(!specs.length)process.exit(0);const stamp='.pulse-verified';let stampMtime=0;try{stampMtime=fs.statSync(stamp).mtimeMs;}catch{}const stale=specs.filter(f=>{try{return fs.statSync(f).mtimeMs>stampMtime;}catch{return true;}});if(stale.length){process.stdout.write(JSON.stringify({decision:'block',reason:'VERIFY REQUIRED: These pages have not been verified since last edit:\\n'+stale.map(f=>' '+f).join('\\n')+'\\n\\
|
|
220
|
+
command: `node -e "const fs=require('fs');if(fs.existsSync('.pulse-awaiting-approval'))process.exit(0);const{execSync}=require('child_process');let specs=[];try{const out=execSync('git status --porcelain',{encoding:'utf8'});out.split('\\n').forEach(l=>{const f=l.slice(3).trim().split(' -> ').pop();if(f&&f.match(/^src\\/pages\\/.+\\.js$/)&&!f.endsWith('.test.js'))specs.push(f);});}catch{if(fs.existsSync('src/pages'))for(const f of fs.readdirSync('src/pages')){if(f.endsWith('.js')&&!f.endsWith('.test.js'))specs.push('src/pages/'+f);}}if(!specs.length)process.exit(0);const stamp='.pulse-verified';let stampMtime=0;try{stampMtime=fs.statSync(stamp).mtimeMs;}catch{}const stale=specs.filter(f=>{try{return fs.statSync(f).mtimeMs>stampMtime;}catch{return true;}});if(stale.length){process.stdout.write(JSON.stringify({decision:'block',reason:'VERIFY REQUIRED: These pages have not been verified since last edit:\\n'+stale.map(f=>' '+f).join('\\n')+'\\n\\nIMPORTANT — check first: are you ending this turn to wait for the user\\'s answer (e.g. the design-approval question)? If so, do NOT run /verify — the user has not approved the design yet. Call pulse_await_approval, re-ask your question, and end your turn; this gate will let you stop.\\n\\nOtherwise run /verify for each changed page. /verify runs Lighthouse (desktop + mobile), performance trace, console check, and code review — then writes the stamp that clears this check.'}));}"`,
|
|
207
221
|
statusMessage: 'Checking verification stamp...',
|
|
208
222
|
}
|
|
209
223
|
]
|
|
@@ -217,6 +231,8 @@ ${port !== 3000 ? ` port: ${port},\n` : ''}${agent && agent !== 'claude' ? ` a
|
|
|
217
231
|
'node_modules',
|
|
218
232
|
'public/dist',
|
|
219
233
|
'.pulse-build',
|
|
234
|
+
'.pulse-verified',
|
|
235
|
+
'.pulse-awaiting-approval',
|
|
220
236
|
'.DS_Store',
|
|
221
237
|
'# Intake images — local design references, not committed',
|
|
222
238
|
'public/intake/*.jpg',
|
package/src/mcp/mcp.test.js
CHANGED
|
@@ -152,14 +152,19 @@ const agentDocs = [
|
|
|
152
152
|
'.claude/commands/verify.md',
|
|
153
153
|
'.claude/commands/new-doc-page.md',
|
|
154
154
|
'CLAUDE.md',
|
|
155
|
+
'README.md',
|
|
155
156
|
]
|
|
156
157
|
|
|
157
158
|
test('no agent doc states a four-score Lighthouse bar (100/100/100/100)', () => {
|
|
159
|
+
const fourScore = (src) =>
|
|
160
|
+
src.includes('100/100/100/100') ||
|
|
161
|
+
/all four (scores|categories)/i.test(src) ||
|
|
162
|
+
/Accessibility, Best Practices, SEO,? and Performance[^.\n]*must (all )?be 100/i.test(src)
|
|
158
163
|
const offenders = agentDocs.filter(f =>
|
|
159
164
|
fs.existsSync(path.join(ROOT, f)) &&
|
|
160
|
-
fs.readFileSync(path.join(ROOT, f), 'utf8')
|
|
165
|
+
fourScore(fs.readFileSync(path.join(ROOT, f), 'utf8'))
|
|
161
166
|
)
|
|
162
|
-
if (serverSrc
|
|
167
|
+
if (fourScore(serverSrc)) offenders.push('src/mcp/server.js')
|
|
163
168
|
assert(
|
|
164
169
|
offenders.length === 0,
|
|
165
170
|
`Four-score Lighthouse bar found in: ${offenders.join(', ')}. ` +
|
|
@@ -204,6 +209,61 @@ test('workflow.md Mode B instructs writing the creative-override spec comment',
|
|
|
204
209
|
)
|
|
205
210
|
})
|
|
206
211
|
|
|
212
|
+
test('checklist points cross-spec shared components at src/components/, not src/ui/', () => {
|
|
213
|
+
const checklist = fs.readFileSync(path.join(agentDir, 'checklist.md'), 'utf8')
|
|
214
|
+
assert(
|
|
215
|
+
!/shared component in `src\/ui\/`/.test(checklist),
|
|
216
|
+
'checklist.md tells the agent to create shared components in src/ui/ — that is the ' +
|
|
217
|
+
'framework\'s own library inside the package. Project-level shared code goes in src/components/.'
|
|
218
|
+
)
|
|
219
|
+
assert(
|
|
220
|
+
/shared component in `src\/components\/`/.test(checklist),
|
|
221
|
+
'checklist.md must direct cross-spec shared components to src/components/.'
|
|
222
|
+
)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test('guide-spec.md states the one-spec-per-page mental model', () => {
|
|
226
|
+
const guideSpec = fs.readFileSync(path.join(agentDir, 'guide-spec.md'), 'utf8')
|
|
227
|
+
assert(
|
|
228
|
+
/one spec per page/i.test(guideSpec) && /dynamic route is still one spec/i.test(guideSpec),
|
|
229
|
+
'guide-spec.md must state the mental model explicitly: one spec = one route = one file, ' +
|
|
230
|
+
'and a dynamic route is still one spec serving many URLs.'
|
|
231
|
+
)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
test('approval pause mechanism is wired end to end', () => {
|
|
235
|
+
// The design-approval gate requires the agent to end its turn and wait for the
|
|
236
|
+
// user — but the scaffolded Stop hooks block any turn ending with unverified
|
|
237
|
+
// edits. Without an escape hatch the hooks steamroll the approval gate (the
|
|
238
|
+
// agent gets blocked, reads "Run /verify", and proceeds without an answer).
|
|
239
|
+
const scaffoldSrc = fs.readFileSync(path.join(ROOT, 'src/cli/scaffold.js'), 'utf8')
|
|
240
|
+
const coverageSrc = fs.readFileSync(path.join(agentDir, 'coverage-check.js'), 'utf8')
|
|
241
|
+
const workflow = fs.readFileSync(path.join(agentDir, 'workflow.md'), 'utf8')
|
|
242
|
+
|
|
243
|
+
// 1. The MCP server must expose the tool that writes the marker
|
|
244
|
+
assert(/registerTool\(\s*'pulse_await_approval'/.test(serverSrc),
|
|
245
|
+
'server.js must register pulse_await_approval')
|
|
246
|
+
|
|
247
|
+
// 2. Every inline Stop hook in the scaffold must early-exit on the marker
|
|
248
|
+
const stopHookCount = (scaffoldSrc.match(/decision:'block'/g) || []).length
|
|
249
|
+
const markerCheckCount = (scaffoldSrc.match(/\.pulse-awaiting-approval'\)\)process\.exit\(0\)/g) || []).length
|
|
250
|
+
assert(stopHookCount >= 2 && markerCheckCount >= stopHookCount,
|
|
251
|
+
`scaffold.js has ${stopHookCount} blocking Stop hooks but only ${markerCheckCount} check ` +
|
|
252
|
+
`.pulse-awaiting-approval — every blocking Stop hook must allow the approval pause.`)
|
|
253
|
+
|
|
254
|
+
// 3. coverage-check.js (the third Stop hook) must early-exit on the marker
|
|
255
|
+
assert(coverageSrc.includes('.pulse-awaiting-approval'),
|
|
256
|
+
'coverage-check.js must early-exit when .pulse-awaiting-approval exists')
|
|
257
|
+
|
|
258
|
+
// 4. The marker must be consumed when the user replies — one turn-end only
|
|
259
|
+
assert(/UserPromptSubmit/.test(scaffoldSrc) && /unlinkSync\('\.pulse-awaiting-approval'\)/.test(scaffoldSrc),
|
|
260
|
+
'scaffold.js must register a UserPromptSubmit hook that deletes .pulse-awaiting-approval')
|
|
261
|
+
|
|
262
|
+
// 5. The workflow must teach the mechanism at the design-approval gate
|
|
263
|
+
assert(workflow.includes('pulse_await_approval'),
|
|
264
|
+
'workflow.md Phase 5a must explain how to pause: host question tool preferred, pulse_await_approval otherwise')
|
|
265
|
+
})
|
|
266
|
+
|
|
207
267
|
test('identity.md declares the creative override (Design Freedom) carve-out', () => {
|
|
208
268
|
const identity = fs.readFileSync(path.join(agentDir, 'identity.md'), 'utf8')
|
|
209
269
|
assert(
|
package/src/mcp/server.js
CHANGED
|
@@ -1203,6 +1203,33 @@ The stop hook compares each changed spec's mtime against this stamp. Any spec ne
|
|
|
1203
1203
|
}
|
|
1204
1204
|
)
|
|
1205
1205
|
|
|
1206
|
+
// ---------------------------------------------------------------------------
|
|
1207
|
+
// pulse_await_approval
|
|
1208
|
+
// ---------------------------------------------------------------------------
|
|
1209
|
+
|
|
1210
|
+
server.registerTool(
|
|
1211
|
+
'pulse_await_approval',
|
|
1212
|
+
{
|
|
1213
|
+
description: `Pause the workflow to wait for the user's answer — call this immediately BEFORE ending your turn with a blocking question (e.g. the design-approval gate: "Happy with the layout and direction…?").
|
|
1214
|
+
|
|
1215
|
+
It writes a .pulse-awaiting-approval marker that tells the Stop hooks (missing tests, coverage, verify stamp) to let this one turn end without demanding /verify first. The marker is deleted automatically when the user sends their next message — the gates return in full force for your following turn.
|
|
1216
|
+
|
|
1217
|
+
**Prefer your host's question tool if it has one** (e.g. AskUserQuestion in Claude Code): a question tool waits for the answer *without ending the turn*, so the Stop hooks never fire and this marker is unnecessary. Use pulse_await_approval only when you must ask in plain prose and end the turn.
|
|
1218
|
+
|
|
1219
|
+
Do NOT use this to skip verification — it buys exactly one turn-end, paired with a question the user must answer. Calling it without asking anything just delays the gates by one message.`,
|
|
1220
|
+
inputSchema: {},
|
|
1221
|
+
},
|
|
1222
|
+
() => {
|
|
1223
|
+
const markerPath = path.join(ROOT, '.pulse-awaiting-approval')
|
|
1224
|
+
try {
|
|
1225
|
+
fs.writeFileSync(markerPath, String(Math.floor(Date.now() / 1000)), 'utf8')
|
|
1226
|
+
return text('✓ Awaiting-approval marker written. Ask the user your question and end your turn — the Stop hooks will allow it. The marker is cleared when the user replies.')
|
|
1227
|
+
} catch (err) {
|
|
1228
|
+
return text(`Error writing .pulse-awaiting-approval: ${err.message}\nFallback: run \`touch .pulse-awaiting-approval\` in Bash, then ask your question.`)
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
)
|
|
1232
|
+
|
|
1206
1233
|
// ---------------------------------------------------------------------------
|
|
1207
1234
|
// pulse_run_tests
|
|
1208
1235
|
// ---------------------------------------------------------------------------
|