@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.
@@ -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"]`; otherwise ask in plain prose.
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"]`; otherwise ask in plain prose.
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/ui/` when the same pattern is needed across 2 or more different specs.** Follow the existing pattern: a named export that returns an HTML string.
70
- - **Do not abstract a pattern that appears only once.** Duplication is cheaper than the wrong abstraction. Wait until the third use before extracting.
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 all 92 unit tests
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 across all four categories.
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/ui/` when the same pattern is needed across 2 or more different specs.** Follow the existing pattern: a named export that returns an HTML string.
70
- - **Do not abstract a pattern that appears only once.** Duplication is cheaper than the wrong abstraction. Wait until the third use before extracting.
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/ui/` when the same pattern is needed across 2 or more different specs.** Follow the existing pattern: a named export that returns an HTML string.
70
- - **Do not abstract a pattern that appears only once.** Duplication is cheaper than the wrong abstraction. Wait until the third use before extracting.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invisibleloop/pulse",
3
- "version": "0.17.0",
3
+ "version": "0.17.1",
4
4
  "type": "module",
5
5
  "description": "AI-first frontend framework. The spec is the source of truth.",
6
6
  "license": "MIT",
@@ -1 +1 @@
1
- 0.17.0
1
+ 0.17.1
@@ -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/ui/` when the same pattern is needed across 2 or more different specs.** Follow the existing pattern: a named export that returns an HTML string.
70
- - **Do not abstract a pattern that appears only once.** Duplication is cheaper than the wrong abstraction. Wait until the third use before extracting.
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"]`; otherwise ask in plain prose.
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)) {
@@ -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. Register with the server
43
+ ### 5. Restart and reload
44
44
 
45
- Check the project's server entry file (e.g. `src/server.js` or `pulse.config.js`) and add the new spec if needed.
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). All four scores — Accessibility, Best Practices, SEO, Performance — must be 100 before continuing.
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 `.github/instructions/pulse-checklist.instructions.md` and confirm every applicable point passes. Fix anything that fails.
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
 
@@ -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
@@ -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\\nRun /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.'}));}"`,
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',
@@ -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').includes('100/100/100/100')
165
+ fourScore(fs.readFileSync(path.join(ROOT, f), 'utf8'))
161
166
  )
162
- if (serverSrc.includes('100/100/100/100')) offenders.push('src/mcp/server.js')
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
  // ---------------------------------------------------------------------------