@invisibleloop/pulse 0.12.0 → 0.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,16 +4,42 @@ Before finishing any spec, verify every point below. Fix anything that fails.
4
4
 
5
5
  ### Critical
6
6
 
7
- - **`hydrate` is set on every interactive page.** Without it, `data-event` / `data-action` bindings do nothing, `persist` never runs, and client-side navigation cannot re-mount the page. Every spec with `mutations`, `actions`, or `persist` must include:
7
+ - **`meta` must be a plain object — never a function.** Individual fields (`title`, `description`, `styles`) can be `async (ctx) => value` functions, but `meta` itself is always `{}`.
8
+
8
9
  ```js
9
- hydrate: '/src/pages/my-page.js', // browser-importable path to this file
10
+ // WRONG — meta is not a function factory
11
+ meta: async (ctx) => ({ title: '...', styles: [...] })
12
+
13
+ // CORRECT — meta is a plain object; individual fields are functions
14
+ meta: {
15
+ title: async (ctx) => (await post(ctx)).frontmatter.title,
16
+ description: async (ctx) => (await post(ctx)).frontmatter.description,
17
+ styles: ['/pulse-ui.css', '/app.css'],
18
+ }
10
19
  ```
11
- Omit `hydrate` only for purely server-rendered pages with zero client interactivity.
20
+
21
+ - **Do not set `hydrate` in specs.** It is auto-derived by the framework from the URL entry passed to `createServer`. Specs with `mutations`, `actions`, or `persist` are hydrated automatically. Purely server-rendered specs get zero JavaScript — no configuration needed.
22
+
23
+ - **Always restart the server and reload the browser after every file edit — before checking the result.** The dev server restarts on file changes but the browser tab stays stale. The required sequence is: `pulse_restart_server` → `navigate_page`. Never attempt to debug a visual problem without doing both steps first.
12
24
 
13
25
  ### Components first
14
26
 
15
27
  - **Before writing any HTML by hand, check `src/ui/index.js`.** There are 50+ components. Use `button`, `card`, `alert`, `input`, `spinner`, `badge`, `modal`, `nav`, `pagination`, `table`, etc. before writing equivalent HTML from scratch.
16
28
 
29
+ - **Never write App Store or Google Play download buttons by hand.** Always use `appBadge({ store: 'apple', href })` and `appBadge({ store: 'google', href })`. Raw `<a>` tags with badge images are incorrect — `appBadge` renders the correct accessible, styled badge for each store.
30
+
31
+ ```js
32
+ import { appBadge } from '@invisibleloop/pulse/ui'
33
+
34
+ // In a hero() actions slot:
35
+ ${hero({
36
+ actions: `
37
+ ${appBadge({ store: 'apple', href: 'https://apps.apple.com/...' })}
38
+ ${appBadge({ store: 'google', href: 'https://play.google.com/...' })}
39
+ `,
40
+ })}
41
+ ```
42
+
17
43
  ### Reuse (DRY)
18
44
 
19
45
  - **Extract a view helper when the same HTML pattern appears 3 or more times in a single spec.** A plain JS function returning an HTML string is sufficient — no framework needed:
@@ -56,6 +82,11 @@ Before finishing any spec, verify every point below. Fix anything that fails.
56
82
  - **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.
57
83
  - **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.
58
84
 
85
+ ### CSS
86
+
87
+ - **`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.
88
+ - **Load order in `meta.styles`:** `pulse-ui.css` → `theme.css` → `app.css`. Theme tokens must be defined before `app.css` references them.
89
+
59
90
  ### Security
60
91
 
61
92
  - Any value from user input (URL params, form fields, external APIs) interpolated into view HTML must be escaped.
@@ -81,6 +112,12 @@ Before finishing any spec, verify every point below. Fix anything that fails.
81
112
  ```
82
113
  Supported selectors: `tag`, `.class`, `#id`, `[attr]`, `[attr="value"]`, and combinations (`button.primary[disabled]`).
83
114
 
115
+ ### Markdown
116
+
117
+ - **Use `md()` from `@invisibleloop/pulse/md` for `.md` file content.** Never read and parse markdown manually. Pass the result's `html` to `prose()`.
118
+ - **Call the same `md()` fetcher in both `meta` and `server`** — it caches on `ctx._mdCache` so the file is only read once per request. Do not create two separate fetchers for the same file.
119
+ - **Always add `onViewError` on dynamic markdown routes** (routes with `:param` segments loading `.md` files). A missing file throws `{ status: 404 }` which must be caught gracefully.
120
+
84
121
  ### View error handling
85
122
 
86
123
  - **Define `onViewError` on any page where the view could throw due to bad or missing data.** Without it, a runtime view error returns a 500 on the server and shows a generic inline message on the client. With it, the server returns 200 with your fallback HTML instead of a 500, and the client renders your fallback:
@@ -109,3 +146,14 @@ Before finishing any spec, verify every point below. Fix anything that fails.
109
146
  - Interactive elements without visible text have an `aria-label`.
110
147
  - Disabled state is reflected with the `disabled` attribute, not just CSS.
111
148
  - The page has a `<main id="main-content">` landmark.
149
+ - **Every element with `data-event`, `data-store-event`, `data-dialog-open`, or `data-dialog-close` is natively interactive (`button`, `a`, `input`, `select`, `textarea`, `summary`) or has `tabindex="0"`.** Non-interactive tags (`div`, `span`, `li`, `td`, etc.) with these attributes are not keyboard reachable. Prefer `<button>` — it is focusable, fires on Enter/Space, and carries the correct ARIA role for free. Only use `tabindex="0"` on a non-interactive element when a `<button>` genuinely cannot be used.
150
+ - **Every interactive element communicates its purpose and state:**
151
+ - Buttons have visible text or `aria-label`. Links use descriptive text — "click here", "here", "read more" are failures.
152
+ - Form inputs have an associated `<label for="id">` or `aria-label`. `placeholder` alone is not a label.
153
+ - Toggle controls (open/close, show/hide, expand/collapse) carry `aria-expanded="true|false"` or `aria-pressed="true|false"`.
154
+ - While an action is in flight, the trigger button has `aria-busy="true"` or its label changes (e.g. "Saving…"). A spinner alone is not enough.
155
+ - Active nav items have `aria-current="page"`. Selected items in a list, tab set, or option group have `aria-selected="true"`.
156
+ - **The page has a logical tab order:**
157
+ - No `tabindex` value greater than 0. Positive tabindex values override the natural DOM order and create a broken tab sequence. Remove them and reorder the DOM instead.
158
+ - Visually hidden interactive content (off-canvas menus, collapsed panels, `opacity:0` elements) is removed from the tab order with `tabindex="-1"` or the `inert` attribute — CSS alone does not remove an element from tab order.
159
+ - DOM order matches the visual reading order. When flexbox `order` or CSS grid placement visually reposition elements, the tab sequence still follows the DOM — author the DOM in the order a user would read and interact with the page.
@@ -1375,7 +1375,7 @@ a:hover {
1375
1375
  top: 0;
1376
1376
  left: 0;
1377
1377
  width: var(--sidebar-w);
1378
- height: 100vh;
1378
+ height: 100dvh;
1379
1379
  overflow-y: auto;
1380
1380
  background: var(--surface);
1381
1381
  border-right: 1px solid var(--border);
@@ -1941,6 +1941,10 @@ hr.doc-hr {
1941
1941
  padding: 2rem 1.25rem 4rem;
1942
1942
  }
1943
1943
 
1944
+ .sidebar-nav {
1945
+ padding-bottom: 6rem;
1946
+ }
1947
+
1944
1948
  .doc-h1 {
1945
1949
  font-size: 1.75rem;
1946
1950
  }
@@ -68,6 +68,7 @@
68
68
 
69
69
  @media (max-width: 767px) {
70
70
  .obs-article { grid-template-columns: 1fr; }
71
+ .obs-aside-section:first-child { display: none; } /* ToC not useful below article on mobile */
71
72
  }
72
73
 
73
74
  /* Aside sections */
@@ -17,16 +17,19 @@ export default {
17
17
  next,
18
18
  content: `
19
19
  ${h1('Getting Started')}
20
- ${lead('Install Pulse, run one command, and have Claude building your first page — with streaming SSR, security headers, and a 100 Lighthouse score already in place.')}
20
+ ${lead('Install Pulse, run one command, and have an AI agent building your first page — with streaming SSR, security headers, and a 100 Lighthouse score already in place.')}
21
21
 
22
22
  ${section('requirements', 'Requirements')}
23
23
  <ul>
24
24
  <li><strong>Node.js 22 or later</strong> — <a href="https://nodejs.org" target="_blank" rel="noopener" aria-label="nodejs.org (opens in new tab)">nodejs.org</a></li>
25
25
  <li><strong>Google Chrome</strong> — used by the agent for screenshots and Lighthouse audits — <a href="https://www.google.com/chrome" target="_blank" rel="noopener" aria-label="Download Google Chrome (opens in new tab)">google.com/chrome</a></li>
26
- <li><strong>Claude Code</strong> — the CLI for Claude, installed and authenticated — <a href="https://docs.anthropic.com/en/docs/claude-code/getting-started" target="_blank" rel="noopener" aria-label="Claude Code installation guide (opens in new tab)">installation guide</a></li>
26
+ <li><strong>An AI agent</strong> — Claude Code or GitHub Copilot CLI (pick one):</li>
27
27
  </ul>
28
- <p>Claude Code provides the <code>claude</code> command. Pulse launches it automatically with the Pulse MCP server wired in — so the agent has instant access to the framework reference, your project structure, and all Pulse tools without any manual configuration.</p>
29
- ${callout('note', 'GitHub Copilot integration is coming soon. For now, Pulse works exclusively with Claude Code.')}
28
+ <ul>
29
+ <li><strong>Claude Code</strong> — <a href="https://docs.anthropic.com/en/docs/claude-code/getting-started" target="_blank" rel="noopener" aria-label="Claude Code installation guide (opens in new tab)">installation guide</a> — the default; no extra config needed</li>
30
+ <li><strong>GitHub Copilot CLI</strong> — <a href="https://docs.github.com/copilot/concepts/agents/about-copilot-cli" target="_blank" rel="noopener" aria-label="GitHub Copilot CLI documentation (opens in new tab)">documentation</a> — use with the <code>--agent copilot</code> flag</li>
31
+ </ul>
32
+ <p>Pulse launches your chosen agent automatically with the Pulse MCP server wired in — so the agent has instant access to the framework reference, your project structure, and all Pulse tools without any manual configuration.</p>
30
33
 
31
34
  ${section('install', 'Install Pulse')}
32
35
  <p>Install the Pulse CLI globally:</p>
@@ -44,8 +47,17 @@ pulse`, 'bash'))}
44
47
  ${section('session', 'Start a session')}
45
48
  <p>Run <code>pulse</code> again from inside your project directory:</p>
46
49
  ${codeBlock(highlight('pulse', 'bash'))}
47
- <p>This time, Pulse detects the existing project and launches Claude Code with the Pulse MCP server already connected. Claude opens with the complete framework guide loaded, your project structure visible, and all Pulse tools available — ready to build immediately.</p>
48
- ${callout('note', 'Run <code>pulse</code> every time you open a working session. It handles starting Claude and wiring up the MCP server. Once Claude is open, use <code>/pulse-dev</code> to start the dev server.')}
50
+ <p>Pulse detects the existing project and launches Claude Code with the Pulse MCP server already connected. Claude opens with the complete framework guide loaded, your project structure visible, and all Pulse tools available — ready to build immediately.</p>
51
+ ${callout('note', 'Run <code>pulse</code> every time you open a working session. It handles starting the agent and wiring up the MCP server. Once the agent is open, use <code>/pulse-dev</code> (Claude) or the <code>build-page</code> skill (Copilot) to start the dev server.')}
52
+
53
+ ${section('copilot', 'Using GitHub Copilot')}
54
+ <p>To use GitHub Copilot CLI instead of Claude, pass the <code>--agent</code> flag when scaffolding:</p>
55
+ ${codeBlock(highlight('pulse --agent copilot', 'bash'))}
56
+ <p>This scaffolds the project and writes <code>agent: \'copilot\'</code> into <code>pulse.config.js</code> — so subsequent <code>pulse</code> runs pick up the preference automatically. You can also set it manually in <code>pulse.config.js</code> at any time:</p>
57
+ ${codeBlock(highlight(`export default {
58
+ agent: 'copilot',
59
+ }`, 'js'))}
60
+ <p>When using Copilot, project skills (<code>build-page</code>, <code>verify</code>, <code>new-doc-page</code>) are available via the <code>/skills</code> command inside the Copilot CLI session. The Pulse MCP server is automatically injected into your Copilot MCP config for the duration of the session and removed cleanly on exit.</p>
49
61
 
50
62
  ${section('first-build', 'Build your first page')}
51
63
  <p>Once Claude opens, start the dev server and ask for something:</p>
@@ -74,7 +86,7 @@ Validate the email format before submitting."`, 'bash'))}
74
86
  │ ├── app.css ← global stylesheet
75
87
  │ ├── pulse-ui.css ← Pulse component library styles
76
88
  │ └── pulse-ui.js ← Pulse component library behaviour
77
- ├── .claude/
89
+ ├── .claude/ ← Claude Code agent context
78
90
  │ ├── CLAUDE.md ← session instructions Claude reads on startup
79
91
  │ ├── settings.json ← hooks: syntax checks, colour guards, package blocklist
80
92
  │ ├── pulse-checklist.md ← spec review checklist, kept in sync by Pulse
@@ -83,10 +95,19 @@ Validate the email format before submitting."`, 'bash'))}
83
95
  │ ├── pulse-stop.md
84
96
  │ ├── pulse-build.md
85
97
  │ └── pulse-start.md
98
+ ├── .copilot/ ← GitHub Copilot CLI agent context
99
+ │ └── skills/ ← project skills available inside Copilot
100
+ │ ├── build-page/
101
+ │ ├── verify/
102
+ │ └── new-doc-page/
103
+ ├── .github/
104
+ │ ├── copilot-instructions.md ← session instructions for Copilot
105
+ │ └── instructions/
106
+ │ └── pulse-checklist.instructions.md
86
107
  ├── package.json
87
- └── pulse.config.js ← port and project settings`, 'bash'))}
88
- <p><code>src/pages/home.js</code> is a complete working spec — a counter with increment and decrement buttons. Open <a href="http://localhost:3000">localhost:3000</a> after running <code>/pulse-dev</code> to see it. Every new page you create goes into <code>src/pages/</code> and is discovered automatically.</p>
89
- <p>The <code>.claude/</code> directory contains the agent's operating context. <code>CLAUDE.md</code> tells Claude how the project is structured, <code>settings.json</code> configures hooks that catch common mistakes before they reach you — hardcoded hex colours, emoji in UI output, and installing client-side rendering libraries are all flagged or blocked automatically.</p>
108
+ └── pulse.config.js ← port, agent, and project settings`, 'bash'))}
109
+ <p><code>src/pages/home.js</code> is a complete working spec — a counter with increment and decrement buttons. Open <a href="http://localhost:3000">localhost:3000</a> after starting the dev server to see it. Every new page you create goes into <code>src/pages/</code> and is discovered automatically.</p>
110
+ <p>The <code>.claude/</code> directory contains the Claude agent's operating context. The <code>.copilot/</code> directory contains the equivalent context for GitHub Copilot — project skills and instructions. Both are scaffolded by default so you can switch agents at any time.</p>
90
111
 
91
112
  ${section('commands', 'Agent commands')}
92
113
  <p>These slash commands are available once Claude is open:</p>
@@ -95,7 +116,8 @@ Validate the email format before submitting."`, 'bash'))}
95
116
  /pulse-build # production build → public/dist/
96
117
  /pulse-start # run the production server
97
118
  /pulse-report # Lighthouse audit + performance report`, 'bash'))}
98
- <p>You can also skip the commands entirely — just describe what you want and the agent handles the rest, including starting the dev server when needed.</p>
119
+ <p>When using GitHub Copilot CLI, the equivalent capabilities are available as project skills. Type <code>/skills</code> inside Copilot to browse them — <code>build-page</code>, <code>verify</code>, and <code>new-doc-page</code> are all pre-installed.</p>
120
+ <p>You can also skip commands and skills entirely — just describe what you want and the agent handles the rest.</p>
99
121
 
100
122
  ${section('update', 'Keeping up to date')}
101
123
  <p>When a new version of Pulse is released, update the package and then run <code>pulse update</code> from your project root:</p>
@@ -104,10 +126,11 @@ pulse update`, 'bash'))}
104
126
  <p><code>pulse update</code> copies the latest UI assets and agent files into your project:</p>
105
127
  <ul>
106
128
  <li><code>public/pulse-ui.css</code> and <code>public/pulse-ui.js</code> — component library styles and behaviour</li>
107
- <li><code>.claude/pulse-checklist.md</code> — spec review checklist</li>
108
- <li><code>.claude/commands/</code> — slash commands</li>
129
+ <li><code>.claude/pulse-checklist.md</code> and <code>.claude/commands/</code> — Claude agent files</li>
130
+ <li><code>.copilot/skills/</code> — Copilot project skills</li>
131
+ <li><code>.github/instructions/pulse-checklist.instructions.md</code> — Copilot checklist</li>
109
132
  </ul>
110
- <p>Your own source files, <code>CLAUDE.md</code>, and <code>pulse.config.js</code> are never touched.</p>
133
+ <p>Your own source files, <code>CLAUDE.md</code>, <code>.github/copilot-instructions.md</code>, and <code>pulse.config.js</code> are never touched.</p>
111
134
 
112
135
  ${section('next-steps', 'Next steps')}
113
136
  <ul>
@@ -111,6 +111,53 @@ const mobileAppPreview = `
111
111
  </div>
112
112
  </div>`
113
113
 
114
+ const blogPostPreview = `
115
+ <div style="display:flex;flex-direction:column;gap:6px;font-size:0;background:#0a2239;border-radius:4px;padding:8px">
116
+
117
+ <!-- nav -->
118
+ <div style="display:flex;align-items:center;gap:6px">
119
+ ${bar('20px', 6, '#53a2be')}
120
+ ${bar('36px', 4, '#53a2be', 2)}
121
+ <div style="flex:1"></div>
122
+ ${bar('28px', 4)} ${bar('28px', 4)} ${bar('28px', 4)}
123
+ </div>
124
+
125
+ <!-- hero -->
126
+ <div style="display:flex;flex-direction:column;align-items:center;gap:4px;padding:8px 0 6px;background:linear-gradient(160deg,#0a2239 0%,#132e32 100%);border-radius:3px;margin-top:2px">
127
+ ${bar('40px', 3, '#53a2be')}
128
+ ${bar('90%', 8, 'rgba(255,255,255,0.7)', 2)}
129
+ ${bar('75%', 8, 'rgba(255,255,255,0.7)', 2)}
130
+ ${bar('65%', 4, 'rgba(83,162,190,0.6)', 2)}
131
+ ${bar('50%', 3, 'rgba(83,162,190,0.4)', 2)}
132
+ </div>
133
+
134
+ <!-- article + aside -->
135
+ <div style="display:flex;gap:6px;margin-top:2px">
136
+ <div style="flex:1;display:flex;flex-direction:column;gap:4px">
137
+ ${bar('100%', 48, 'rgba(83,162,190,0.15)', 3)}
138
+ ${bar('100%', 4, 'rgba(255,255,255,0.5)', 1)}
139
+ ${bar('90%', 4, 'rgba(255,255,255,0.5)', 1)}
140
+ ${bar('95%', 4, 'rgba(255,255,255,0.5)', 1)}
141
+ ${bar('60%', 4, 'rgba(255,255,255,0.5)', 1)}
142
+ ${bar('55%', 6, 'rgba(255,255,255,0.7)', 1)}
143
+ ${bar('100%', 3, 'rgba(255,255,255,0.4)', 1)}
144
+ ${bar('85%', 3, 'rgba(255,255,255,0.4)', 1)}
145
+ </div>
146
+ <div style="width:38px;display:flex;flex-direction:column;gap:3px">
147
+ ${bar('100%', 4, 'rgba(255,255,255,0.5)', 1)}
148
+ ${bar('100%', 3, 'rgba(83,162,190,0.6)', 1)}
149
+ ${bar('100%', 3, 'rgba(83,162,190,0.6)', 1)}
150
+ ${bar('100%', 3, 'rgba(83,162,190,0.6)', 1)}
151
+ <div style="margin-top:3px;background:#132e32;border-radius:3px;padding:3px;display:flex;flex-direction:column;gap:2px">
152
+ ${bar('60%', 3, '#53a2be')}
153
+ ${bar('100%', 5, 'rgba(255,255,255,0.6)', 1)}
154
+ ${bar('100%', 5, 'rgba(255,255,255,0.6)', 1)}
155
+ ${bar('100%', 10, 'rgba(83,162,190,0.5)', 2)}
156
+ </div>
157
+ </div>
158
+ </div>
159
+ </div>`
160
+
114
161
  const TEMPLATES = [
115
162
  {
116
163
  name: 'Mobile App Landing Page',
@@ -126,6 +173,20 @@ const TEMPLATES = [
126
173
  'JS': '~4.2 kB',
127
174
  },
128
175
  },
176
+ {
177
+ name: 'Blog Post',
178
+ route: '/templates/blog-post',
179
+ description: 'A full editorial article page — sticky nav, article hero, prose with image, two-column layout with table-of-contents sidebar, newsletter signup, and footer. Dark astronomy theme.',
180
+ tags: ['Blog', 'Editorial'],
181
+ preview: blogPostPreview,
182
+ stats: {
183
+ 'Accessibility': '100',
184
+ 'Best Practices': '100',
185
+ 'SEO': '100',
186
+ 'CLS': '0.00',
187
+ 'JS': '~4.2 kB',
188
+ },
189
+ },
129
190
  ]
130
191
 
131
192
  const content = `
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invisibleloop/pulse",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
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.12.0
1
+ 0.12.2
@@ -20,10 +20,26 @@ Before finishing any spec, verify every point below. Fix anything that fails.
20
20
 
21
21
  - **Do not set `hydrate` in specs.** It is auto-derived by the framework from the URL entry passed to `createServer`. Specs with `mutations`, `actions`, or `persist` are hydrated automatically. Purely server-rendered specs get zero JavaScript — no configuration needed.
22
22
 
23
+ - **Always restart the server and reload the browser after every file edit — before checking the result.** The dev server restarts on file changes but the browser tab stays stale. The required sequence is: `pulse_restart_server` → `navigate_page`. Never attempt to debug a visual problem without doing both steps first.
24
+
23
25
  ### Components first
24
26
 
25
27
  - **Before writing any HTML by hand, check `src/ui/index.js`.** There are 50+ components. Use `button`, `card`, `alert`, `input`, `spinner`, `badge`, `modal`, `nav`, `pagination`, `table`, etc. before writing equivalent HTML from scratch.
26
28
 
29
+ - **Never write App Store or Google Play download buttons by hand.** Always use `appBadge({ store: 'apple', href })` and `appBadge({ store: 'google', href })`. Raw `<a>` tags with badge images are incorrect — `appBadge` renders the correct accessible, styled badge for each store.
30
+
31
+ ```js
32
+ import { appBadge } from '@invisibleloop/pulse/ui'
33
+
34
+ // In a hero() actions slot:
35
+ ${hero({
36
+ actions: `
37
+ ${appBadge({ store: 'apple', href: 'https://apps.apple.com/...' })}
38
+ ${appBadge({ store: 'google', href: 'https://play.google.com/...' })}
39
+ `,
40
+ })}
41
+ ```
42
+
27
43
  ### Reuse (DRY)
28
44
 
29
45
  - **Extract a view helper when the same HTML pattern appears 3 or more times in a single spec.** A plain JS function returning an HTML string is sufficient — no framework needed:
@@ -153,7 +153,7 @@ Use `prose()` for HTML from external sources (CMS, markdown). Use `heading()`/`l
153
153
  | `testimonial` | `quote`, `name`, `role`, `src`, `rating` (1–5) |
154
154
  | `pricing` | `name`, `level` (1–6, default 3), `price`, `period`, `features` ([strings]), `action` (HTML slot), `highlighted` |
155
155
  | `accordion` | `items` ([{question,answer}]) — native `<details>`, no JS |
156
- | `appBadge` | `store` (apple/google), `href` |
156
+ | `appBadge` | `store` (apple/google), `href` — **always use this for App Store / Google Play download buttons. Never write a raw `<a>` with an SVG badge image.** Designed to sit in a `hero()` `actions` slot or anywhere a download CTA is needed. |
157
157
  | `cta` | `eyebrow`, `title`, `level` (1–6, default 2), `subtitle`, `actions` (HTML slot), `align` |
158
158
 
159
159
  ### Layout components
@@ -62,6 +62,13 @@ Every build task follows a fixed sequence of phases with explicit pass gates. Fe
62
62
 
63
63
  You work exclusively inside the Pulse project directory. You do not read or modify files outside that directory.
64
64
 
65
+ **After writing any file, always refresh before checking the result.** The dev server restarts automatically on file changes (node --watch), but the browser does not. The sequence after every edit is:
66
+ 1. Call `pulse_restart_server` — waits for the server to be ready
67
+ 2. Call `navigate_page` with the page URL — reloads the browser tab
68
+ 3. Only then take a screenshot or check for errors
69
+
70
+ Never attempt to diagnose a visual problem without first doing these two steps. A stale browser tab is not a bug — it is an unrefreshed tab.
71
+
65
72
  When a shell command fails, you diagnose the root cause before retrying. You never retry the same failing command more than once. If a command fails due to permissions, auth, or token scope issues — these are unrecoverable without user action. Stop immediately, explain what failed and why, and tell the user exactly what they need to do to unblock it. Do not attempt workarounds that will also fail.
66
73
 
67
74
  Before installing any npm package, check whether the task can be accomplished with Node.js built-ins or code already in the project. Only install a package if there is no reasonable built-in alternative.
@@ -0,0 +1,75 @@
1
+ ---
2
+ name: build-page
3
+ description: "Scaffold a new Pulse page spec, validate it, and visually confirm it renders correctly. $ARGUMENTS Read `src/ui/index.js` to see what UI components are available before writing any HTML. Note which components apply to this page."
4
+ ---
5
+
6
+ # Build Page
7
+
8
+ Scaffold a new Pulse page spec, validate it, and visually confirm it renders correctly.
9
+
10
+ ## What to build
11
+
12
+ $ARGUMENTS
13
+
14
+ ## Steps — follow in order
15
+
16
+ ### 1. Check existing components
17
+
18
+ Read `src/ui/index.js` to see what UI components are available before writing any HTML. Note which components apply to this page.
19
+
20
+ ### 2. Plan
21
+
22
+ State before writing any files:
23
+ - The route (e.g. `/dashboard`)
24
+ - The file path (e.g. `src/pages/dashboard.js`)
25
+ - State shape
26
+ - Mutations and/or actions needed
27
+ - Which UI components will be used
28
+ - Whether `server` data fetchers are needed
29
+
30
+ ### 3. Build the spec
31
+
32
+ Write the spec file. Follow these rules:
33
+ - Use `class` not `className`, template literals not JSX
34
+ - No React imports, hooks, or patterns
35
+ - Use components from `src/ui/` — do not hand-write HTML that a component already covers
36
+ - Use optional chaining (`?.`) on any server data that could be null
37
+ - Escape any user-supplied values interpolated into HTML
38
+
39
+ ### 4. Validate
40
+
41
+ Use `pulse_validate` on the spec. Fix any errors before continuing.
42
+
43
+ ### 5. Register with the server
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.
46
+
47
+ ### 6. Screenshot
48
+
49
+ Use the chrome-devtools navigate_page tool to load the page, then take_screenshot.
50
+
51
+ Check for:
52
+ - Page renders without a blank screen or error
53
+ - Layout looks correct
54
+ - No console errors via list_console_messages
55
+
56
+ ### 7. Lighthouse
57
+
58
+ Run lighthouse_audit (desktop, then mobile). All four scores — Accessibility, Best Practices, SEO, Performance — must be 100 before continuing.
59
+
60
+ ### 8. Run the checklist
61
+
62
+ Go through `.github/instructions/pulse-checklist.instructions.md` and confirm every applicable point passes. Fix anything that fails.
63
+
64
+ ### 9. Write tests
65
+
66
+ Write a `src/pages/<name>.test.js` covering:
67
+ - View renders expected HTML landmarks
68
+ - Each mutation as a pure function
69
+ - Any utility functions the page defines
70
+
71
+ Run them with `node src/pages/<name>.test.js` and fix every failure.
72
+
73
+ ### 10. Report
74
+
75
+ Summarise what was built, what components were used, and confirm validation + visual check passed.
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: new-doc-page
3
+ description: "Create a new documentation page for the Pulse docs site. $ARGUMENTS - Read `docs/src/lib/nav.js` to understand the full nav structure and find where the new page fits"
4
+ ---
5
+
6
+ # New Docs Page
7
+
8
+ Create a new documentation page for the Pulse docs site.
9
+
10
+ ## What to build
11
+
12
+ $ARGUMENTS
13
+
14
+ ## Steps — follow in order
15
+
16
+ ### 1. Research
17
+ - Read `docs/src/lib/nav.js` to understand the full nav structure and find where the new page fits
18
+ - Read 2–3 adjacent pages (the pages that will be `prev`/`next` neighbours) to match tone, section depth, and helper usage (`h1`, `lead`, `section`, `sub`, `codeBlock`, `callout`, `table`)
19
+ - Read `docs/src/lib/layout.js` exported functions to confirm available helpers
20
+ - If the feature being documented has a source file (e.g. in `src/`), read it to ensure accuracy
21
+
22
+ ### 2. Plan
23
+ Before writing any files, state:
24
+ - The route (e.g. `/request-bodies`)
25
+ - Where it sits in the nav (section, between which two pages)
26
+ - Sections the page will cover (IDs + titles)
27
+ - Any doc pages that need updating to link to or mention this page
28
+
29
+ Wait for approval if anything is uncertain.
30
+
31
+ ### 3. Implement
32
+
33
+ **a) Write the page file** at `docs/src/pages/<name>.js`:
34
+ - Import from `../lib/layout.js`, `../lib/nav.js`, `../lib/highlight.js`
35
+ - Use `prevNext('/your-route')` for prev/next links
36
+ - Export a valid Pulse spec as `export default { route, meta, state, view }`
37
+ - `meta.styles` must be `['/docs.css']`
38
+ - Use `renderLayout({ currentHref, prev, next, content })` for the page body
39
+ - All code examples must use `${codeBlock(highlight(\`...\`, 'js'))}` — never raw `<pre>`
40
+ - Tables use `${table(['Col1', 'Col2'], [['val', 'val']])}`
41
+ - Callouts use `${callout('note|tip|warning|info', 'text')}`
42
+ - Section anchors use `${section('anchor-id', 'Section Title')}`
43
+
44
+ **b) Register in nav** — add the entry to `docs/src/lib/nav.js` in the correct position
45
+
46
+ **c) Verify syntax** — run `node --check docs/src/pages/<name>.js`
47
+
48
+ **d) Check neighbours** — read the prev and next pages. If they reference topics this new page covers, add a cross-link.
49
+
50
+ **e) Final check** — confirm the new route appears in `prevNext` output for its neighbours (the nav order drives prev/next automatically, so adding to NAV is sufficient).
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: verify
3
+ description: "Run the full verification loop on the current page or the page specified in $ARGUMENTS. If $ARGUMENTS is provided, use it as the file path or route. Otherwise, identify the most recently edited spec file from context. Use `pulse_validate` with the spec file content. If validation fails, report every error clearly and stop — do not proceed to screenshot until the spec is valid."
4
+ ---
5
+
6
+ # Verify
7
+
8
+ Run the full verification loop on the current page or the page specified in $ARGUMENTS.
9
+
10
+ ## Steps
11
+
12
+ ### 1. Identify the target
13
+
14
+ If $ARGUMENTS is provided, use it as the file path or route. Otherwise, identify the most recently edited spec file from context.
15
+
16
+ ### 2. Validate the spec
17
+
18
+ Use `pulse_validate` with the spec file content. If validation fails, report every error clearly and stop — do not proceed to screenshot until the spec is valid.
19
+
20
+ ### 3. Check the dev server
21
+
22
+ Use `pulse_fetch_page` to confirm the server is responding for the route. If it errors, use `pulse_restart_server` and retry once.
23
+
24
+ ### 4. Screenshot
25
+
26
+ Use the chrome-devtools navigate_page tool to load the page route, then take_screenshot to capture the result. Describe what you see — layout, content, any obvious rendering issues.
27
+
28
+ ### 5. Lighthouse — desktop
29
+
30
+ Run lighthouse_audit on the route in desktop mode.
31
+
32
+ **Pass bar: Accessibility, Best Practices, and SEO must all be 100.** Report the actual scores. If any score is below 100, identify the failing audit(s), fix the issue, and restart from step 2.
33
+
34
+ ### 6. Lighthouse — mobile
35
+
36
+ Run lighthouse_audit on the same route in mobile mode.
37
+
38
+ **Same pass bar: all three categories must be 100.** Report the actual scores. If any score is below 100, fix and restart from step 2.
39
+
40
+ ### 7. Performance
41
+
42
+ Navigate to the page route, then run performance_start_trace with `reload: true` and `autoStop: true`. Report LCP and CLS. Flag any LCP insight that suggests a fixable problem (render-blocking resources, large image load delay, etc.).
43
+
44
+ ### 8. Console errors
45
+
46
+ Use list_console_messages — report any errors or unexpected warnings.
47
+
48
+ ### 9. Code review
49
+
50
+ Use `pulse_review` to switch into reviewer mode. Read the spec against the checklist in `.github/instructions/pulse-checklist.instructions.md`. Work through every item. Fix anything that fails before proceeding.
51
+
52
+ ### 10. Close the browser
53
+
54
+ Use list_pages to get all open pages, then call close_page for every page ID returned.
55
+
56
+ ### 11. Report
57
+
58
+ Summarise:
59
+ - Validation: pass or fail (with errors if any)
60
+ - Visual: what was visible in the screenshot
61
+ - Lighthouse desktop: scores for all three categories
62
+ - Lighthouse mobile: scores for all three categories
63
+ - Performance: LCP and CLS values, any flagged insights
64
+ - Console: any errors
65
+ - Review: pass or issues found and fixed
66
+
67
+ Only confirm the page is good when validation passes, both Lighthouse runs are 100/100/100, CLS is 0.00, and there are no console errors. Otherwise, fix and run `/verify` again.
@@ -80,6 +80,12 @@ Run `pulse_validate` on the spec file.
80
80
 
81
81
  ## Phase 5 — Browser check (pass gate)
82
82
 
83
+ **After every file edit, restart the server and reload the browser before checking anything.** The dev server auto-restarts on file changes but the browser tab stays stale. Always:
84
+ 1. Call `pulse_restart_server` — waits for the server to be ready
85
+ 2. Call `navigate_page` with the page URL — reloads the browser tab
86
+
87
+ Do this before every screenshot, console check, or visual inspection. Never debug a problem that might just be a stale tab.
88
+
83
89
  **Run `/verify` now — as soon as the first build is working.** Do not defer it to the end. Do not execute these steps manually.
84
90
 
85
91
  `/verify` is the canonical implementation of phases 4–7: it validates, screenshots, runs Lighthouse (desktop + mobile), runs the performance trace (LCP + CLS), checks console errors, runs `pulse_review`, and — critically — **writes the `.pulse-verified` stamp** at the end. Running the steps manually without writing the stamp will cause the stop hook to block.
@@ -8,6 +8,9 @@
8
8
  * run: node src/cli/cli.test.js
9
9
  */
10
10
 
11
+ import { existsSync, readFileSync } from 'fs'
12
+ import path from 'path'
13
+
11
14
  let passed = 0
12
15
  let failed = 0
13
16
 
@@ -76,6 +79,32 @@ test('discover: nested pages get unique names (collision prevention)', () => {
76
79
  }
77
80
  })
78
81
 
82
+ // ---------------------------------------------------------------------------
83
+ // Skill source files
84
+
85
+ console.log('\nCopilot skill source files\n')
86
+
87
+ const skillsDir = new URL('../agent/skills', import.meta.url).pathname
88
+ const requiredSkills = ['build-page', 'verify', 'new-doc-page']
89
+
90
+ for (const skill of requiredSkills) {
91
+ const skillMd = path.join(skillsDir, skill, 'SKILL.md')
92
+
93
+ test(`${skill}/SKILL.md exists`, () => {
94
+ if (!existsSync(skillMd)) throw new Error(`Missing: ${skillMd}`)
95
+ })
96
+
97
+ test(`${skill}/SKILL.md has valid frontmatter`, () => {
98
+ const content = readFileSync(skillMd, 'utf8')
99
+ if (!content.startsWith('---\n')) throw new Error('Missing opening YAML frontmatter ---')
100
+ const end = content.indexOf('\n---\n', 4)
101
+ if (end === -1) throw new Error('Missing closing YAML frontmatter ---')
102
+ const frontmatter = content.slice(4, end)
103
+ if (!frontmatter.includes('name:')) throw new Error('Frontmatter missing name:')
104
+ if (!frontmatter.includes('description:')) throw new Error('Frontmatter missing description:')
105
+ })
106
+ }
107
+
79
108
  // ---------------------------------------------------------------------------
80
109
 
81
110
  console.log(`\n${passed + failed} tests: ${passed} passed, ${failed} failed\n`)
package/src/cli/index.js CHANGED
@@ -12,13 +12,24 @@
12
12
  * pulse -v alias for --version
13
13
  * pulse --help show usage and exit
14
14
  * pulse -h alias for --help
15
+ * pulse --agent copilot use GitHub Copilot CLI instead of Claude
15
16
  */
16
17
 
17
18
  import path from 'path'
18
19
  import fs from 'fs'
19
20
  import { scaffold } from './scaffold.js'
20
21
 
21
- const args = process.argv.slice(2)
22
+ // Extract --agent flag before routing, so it works alongside any subcommand
23
+ const rawArgs = process.argv.slice(2)
24
+ let agentFlag = null
25
+ const args = []
26
+ for (let i = 0; i < rawArgs.length; i++) {
27
+ if (rawArgs[i] === '--agent' && rawArgs[i + 1] && !rawArgs[i + 1].startsWith('-')) {
28
+ agentFlag = rawArgs[++i]
29
+ } else {
30
+ args.push(rawArgs[i])
31
+ }
32
+ }
22
33
  const command = args[0]
23
34
  const CWD = process.cwd()
24
35
 
@@ -112,7 +123,7 @@ async function runDefault(root) {
112
123
  }
113
124
 
114
125
  console.log()
115
- await scaffold(targetDir, { name })
126
+ await scaffold(targetDir, { name, agent: agentFlag })
116
127
 
117
128
  if (targetDir !== root) {
118
129
  console.log(`\n✓ Project created at ./${name}/\n`)
@@ -126,21 +137,21 @@ async function runDefault(root) {
126
137
  }
127
138
 
128
139
  // Start dev server + AI session
129
- await launchSession(root)
140
+ await launchSession(root, agentFlag)
130
141
  }
131
142
 
132
143
  // ---------------------------------------------------------------------------
133
144
  // Launch AI session (Claude by default)
134
145
  // ---------------------------------------------------------------------------
135
146
 
136
- async function launchSession(root) {
147
+ async function launchSession(root, agentOverride = null) {
137
148
  const { spawn } = await import('child_process')
138
149
  const os = await import('os')
139
150
 
140
- // Load project config for agent preference
151
+ // Load project config for agent preference — CLI flag takes priority
141
152
  const configPath = path.join(root, 'pulse.config.js')
142
- let agent = 'claude'
143
- if (fs.existsSync(configPath)) {
153
+ let agent = agentOverride || 'claude'
154
+ if (!agentOverride && fs.existsSync(configPath)) {
144
155
  try {
145
156
  const mod = await import(configPath)
146
157
  agent = mod.default?.agent || 'claude'
@@ -157,18 +168,30 @@ async function launchSession(root) {
157
168
  }
158
169
  }
159
170
  }
160
- const mcpConfigPath = path.join(os.default.tmpdir(), `pulse-mcp-${Date.now()}.json`)
161
- fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2))
162
171
 
163
172
  console.log(`\n⚡ Pulse project: ${root}`)
164
173
  console.log(` Use /pulse-dev to start the dev server, /pulse-stop to stop it, /pulse-build to build, /pulse-start to run production.`)
165
174
  console.log(` Tell me what you'd like to build — a new page, a component, a form, or anything else.\n`)
166
175
 
176
+ if (agent === 'copilot') {
177
+ await launchCopilotSession(root, mcpServerPath, spawn, os.default)
178
+ } else {
179
+ await launchClaudeSession(root, mcpConfig, spawn, os.default)
180
+ }
181
+ }
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // Claude session — temp MCP config file passed via --mcp-config flag
185
+ // ---------------------------------------------------------------------------
186
+
187
+ async function launchClaudeSession(root, mcpConfig, spawn, os) {
188
+ const mcpConfigPath = path.join(os.tmpdir(), `pulse-mcp-${Date.now()}.json`)
189
+ fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2))
190
+
167
191
  // Launch the agent with MCP config — don't spawn a dev server here,
168
192
  // Claude Code cannot be launched as a child process from within a Claude session.
169
193
  // The dev server is started via the /dev slash command instead.
170
- const agentCmd = agentCommand(agent, mcpConfigPath)
171
- const agentProc = spawn(agentCmd.cmd, agentCmd.args, {
194
+ const agentProc = spawn('claude', ['--mcp-config', mcpConfigPath], {
172
195
  stdio: 'inherit',
173
196
  cwd: root,
174
197
  })
@@ -179,13 +202,55 @@ async function launchSession(root) {
179
202
  })
180
203
  }
181
204
 
182
- function agentCommand(agent, mcpConfigPath) {
183
- if (agent === 'claude') {
184
- return { cmd: 'claude', args: ['--mcp-config', mcpConfigPath] }
205
+ // ---------------------------------------------------------------------------
206
+ // Copilot session — pulse entry injected into ~/.copilot/mcp-config.json
207
+ // ---------------------------------------------------------------------------
208
+
209
+ async function launchCopilotSession(root, mcpServerPath, spawn, os) {
210
+ const mcpConfigPath = path.join(os.homedir(), '.copilot', 'mcp-config.json')
211
+
212
+ // Read existing Copilot MCP config (or start fresh)
213
+ let config = { mcpServers: {} }
214
+ if (fs.existsSync(mcpConfigPath)) {
215
+ try { config = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf8')) } catch { /* use default */ }
185
216
  }
186
- // Future: copilot, etc.
187
- console.warn(`Unknown agent "${agent}", falling back to claude`)
188
- return { cmd: 'claude', args: ['--mcp-config', mcpConfigPath] }
217
+ config.mcpServers ??= {}
218
+
219
+ const hadPulse = 'pulse' in config.mcpServers
220
+ const originalPulse = hadPulse ? config.mcpServers.pulse : undefined
221
+
222
+ // Inject pulse MCP server
223
+ config.mcpServers.pulse = {
224
+ type: 'local',
225
+ command: process.execPath,
226
+ args: [mcpServerPath, '--root', root],
227
+ tools: ['*'],
228
+ }
229
+ fs.mkdirSync(path.dirname(mcpConfigPath), { recursive: true })
230
+ fs.writeFileSync(mcpConfigPath, JSON.stringify(config, null, 2))
231
+
232
+ // Restore config on exit — removes pulse entry (or restores original if it existed)
233
+ let cleaned = false
234
+ const cleanup = () => {
235
+ if (cleaned) return
236
+ cleaned = true
237
+ try {
238
+ const current = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf8'))
239
+ if (hadPulse) {
240
+ current.mcpServers.pulse = originalPulse
241
+ } else {
242
+ delete current.mcpServers.pulse
243
+ }
244
+ fs.writeFileSync(mcpConfigPath, JSON.stringify(current, null, 2))
245
+ } catch { /* ignore cleanup errors */ }
246
+ process.exit(0)
247
+ }
248
+
249
+ process.on('SIGINT', cleanup)
250
+ process.on('SIGTERM', cleanup)
251
+
252
+ const agentProc = spawn('copilot', [], { stdio: 'inherit', cwd: root })
253
+ agentProc.on('exit', cleanup)
189
254
  }
190
255
 
191
256
  // ---------------------------------------------------------------------------
@@ -318,6 +383,29 @@ async function runUpdate(root) {
318
383
  }
319
384
  }
320
385
 
386
+ // Sync Copilot skills into .copilot/skills/
387
+ const skillsSrc = new URL('../agent/skills', import.meta.url).pathname
388
+ if (fs.existsSync(skillsSrc)) {
389
+ for (const skillDir of fs.readdirSync(skillsSrc)) {
390
+ const skillMd = path.join(skillsSrc, skillDir, 'SKILL.md')
391
+ if (fs.existsSync(skillMd)) {
392
+ const dstDir = path.join(root, '.copilot', 'skills', skillDir)
393
+ fs.mkdirSync(dstDir, { recursive: true })
394
+ fs.copyFileSync(skillMd, path.join(dstDir, 'SKILL.md'))
395
+ updated.push(`.copilot/skills/${skillDir}/SKILL.md`)
396
+ }
397
+ }
398
+ }
399
+
400
+ // Sync checklist to .github/instructions/ for Copilot
401
+ const checklistSrc = new URL('../agent/checklist.md', import.meta.url).pathname
402
+ if (fs.existsSync(checklistSrc)) {
403
+ const checklistDst = path.join(root, '.github', 'instructions', 'pulse-checklist.instructions.md')
404
+ fs.mkdirSync(path.dirname(checklistDst), { recursive: true })
405
+ fs.copyFileSync(checklistSrc, checklistDst)
406
+ updated.push('.github/instructions/pulse-checklist.instructions.md')
407
+ }
408
+
321
409
  // Read the new version for the success message
322
410
  const versionFile = path.join(publicDir, '.pulse-ui-version')
323
411
  const version = fs.existsSync(versionFile) ? fs.readFileSync(versionFile, 'utf8').trim() : '?'
@@ -362,7 +450,7 @@ switch (command) {
362
450
  console.log(`
363
451
  ⚡ Pulse — spec-first frontend framework
364
452
 
365
- Usage: pulse [command]
453
+ Usage: pulse [command] [options]
366
454
 
367
455
  Commands:
368
456
  (none) detect project or start scaffold wizard
@@ -372,8 +460,13 @@ switch (command) {
372
460
  update re-copy pulse-ui assets from installed package → public/
373
461
 
374
462
  Options:
463
+ --agent <name> AI agent to use: claude (default) or copilot
375
464
  -v, --version print version and exit
376
465
  -h, --help show this help
466
+
467
+ Examples:
468
+ pulse start with Claude (default)
469
+ pulse --agent copilot start with GitHub Copilot CLI
377
470
  `)
378
471
  process.exit(0)
379
472
  case 'dev':
@@ -20,8 +20,9 @@ const PULSE_PKG = '@invisibleloop/pulse'
20
20
  * @param {number} [options.port] - Dev server port (defaults to 3000)
21
21
  */
22
22
  export async function scaffold(targetDir, options = {}) {
23
- const name = options.name || path.basename(targetDir)
24
- const port = options.port || 3000
23
+ const name = options.name || path.basename(targetDir)
24
+ const port = options.port || 3000
25
+ const agent = options.agent || null
25
26
 
26
27
  fs.mkdirSync(path.join(targetDir, 'src', 'pages'), { recursive: true })
27
28
  fs.mkdirSync(path.join(targetDir, 'src', 'components'), { recursive: true })
@@ -47,10 +48,10 @@ export async function scaffold(targetDir, options = {}) {
47
48
  }
48
49
  }, null, 2))
49
50
 
50
- // pulse.config.js
51
+ // pulse.config.js — write agent if specified, otherwise leave as commented hint
51
52
  write(targetDir, 'pulse.config.js',
52
53
  `export default {
53
- ${port !== 3000 ? ` port: ${port},\n` : ''}}
54
+ ${port !== 3000 ? ` port: ${port},\n` : ''}${agent && agent !== 'claude' ? ` agent: '${agent}',\n` : ` // agent: 'copilot', // uncomment to use GitHub Copilot CLI instead of Claude\n`}}
54
55
  `)
55
56
 
56
57
  // Home page + tests — working counter proves the app runs
@@ -94,6 +95,27 @@ ${port !== 3000 ? ` port: ${port},\n` : ''}}
94
95
  }
95
96
  }
96
97
 
98
+ // Copilot skills — .copilot/skills/ so GitHub Copilot CLI picks them up as project skills
99
+ const skillsSrc = new URL('../agent/skills', import.meta.url).pathname
100
+ if (fs.existsSync(skillsSrc)) {
101
+ for (const skillDir of fs.readdirSync(skillsSrc)) {
102
+ const skillMd = path.join(skillsSrc, skillDir, 'SKILL.md')
103
+ if (fs.existsSync(skillMd)) {
104
+ write(targetDir, `.copilot/skills/${skillDir}/SKILL.md`, fs.readFileSync(skillMd, 'utf8'))
105
+ }
106
+ }
107
+ }
108
+
109
+ // Copilot project instructions — .github/copilot-instructions.md is the canonical Copilot location
110
+ write(targetDir, '.github/copilot-instructions.md', copilotInstructionsMd(name))
111
+
112
+ // Copilot checklist as a separate instructions file — auto-loaded from .github/instructions/
113
+ const checklistSrc = new URL('../agent/checklist.md', import.meta.url).pathname
114
+ if (fs.existsSync(checklistSrc)) {
115
+ write(targetDir, '.github/instructions/pulse-checklist.instructions.md',
116
+ fs.readFileSync(checklistSrc, 'utf8'))
117
+ }
118
+
97
119
  // settings.json — hooks that enforce correct agent behaviour
98
120
  write(targetDir, '.claude/settings.json', JSON.stringify({
99
121
  hooks: {
@@ -436,6 +458,98 @@ Descendant selectors (\`tbody tr\`, \`ul li\`) are NOT supported — use \`r.cou
436
458
  `
437
459
  }
438
460
 
461
+ function copilotInstructionsMd(appName) {
462
+ return `\
463
+ # ${appName} — Pulse App
464
+
465
+ ## Project
466
+
467
+ \`\`\`
468
+ src/pages/ ← one .js file per page, auto-discovered
469
+ src/components/ ← shared view fragments (JS functions returning HTML strings)
470
+ public/theme.css ← token definitions (hex values, colour overrides) — hex allowed here only
471
+ public/app.css ← layout and component overrides — var() references only, no hex
472
+ \`\`\`
473
+
474
+ ## Start of every session
475
+
476
+ 1. Run \`pulse_list_structure\` to see what already exists
477
+ 2. Read \`pulse://guide\` from MCP — the complete reference for spec format, components, verification workflow, CSS rules, and patterns
478
+
479
+ The MCP guide is the single source of truth. Follow it for all technical decisions, component usage, and the mandatory verification workflow.
480
+
481
+ ## CSS and theming
482
+
483
+ - Hex values and colour token definitions go in \`public/theme.css\` — that is the only file where hex is allowed.
484
+ - \`public/app.css\` must not contain hex colour values. Use \`var(--ui-space-N)\` spacing tokens for padding, margin, and gap. Use \`var(--ui-text-*)\` tokens for font sizes.
485
+ - Load order in \`meta.styles\`: \`/pulse-ui.css\` → \`/theme.css\` → \`/app.css\`.
486
+
487
+ ## Before writing any code
488
+
489
+ **Always present a plan and wait for the user to confirm before writing a single line of code.**
490
+
491
+ The plan must include: route, page sections, components used, state shape, and whether hydration is needed. End the plan with an explicit question — "Shall I go ahead?" — and stop. Do not proceed until the user says yes (or equivalent). This applies to every new page or significant change, no matter how clear the task seems.
492
+
493
+ ## After completing any feature
494
+
495
+ Run these steps in order — do not declare work done without them:
496
+
497
+ 1. \`pulse_validate\` — fix all errors and warnings
498
+ 2. \`pulse_review\` — switch into reviewer mode, read the source and rendered HTML critically, fix every issue before continuing
499
+ 3. Write tests for every page you created or changed, run \`npm test\` (all pass), then \`npm run test:coverage\` — fix any untested branches
500
+ 4. Navigate to the page in the browser and take a screenshot
501
+ 5. Run Lighthouse (desktop then mobile) — Accessibility, Best Practices, and SEO must all be 100
502
+
503
+ ## Writing tests
504
+
505
+ Test files live next to the page they test: \`src/pages/foo.test.js\` for \`src/pages/foo.js\`.
506
+
507
+ \`\`\`
508
+ npm test # run all tests
509
+ npm run test:coverage # run tests + show branch/line coverage for src/pages/
510
+ \`\`\`
511
+
512
+ Coverage target: **100% branch coverage on every view function**. The coverage report shows uncovered lines — add tests until every branch is exercised. Server fetcher functions (which hit real APIs) are exempt.
513
+
514
+ What to test for every page:
515
+ - View with real/populated server data (success path)
516
+ - View with \`null\` server data per fetcher (each fetcher can fail independently)
517
+ - View with empty arrays/collections (empty state vs populated state)
518
+ - \`onViewError\` fallback — call it directly: \`spec.onViewError(new Error('x'), {}, {})\`
519
+ - Every mutation — pure functions, test directly
520
+ - Every exported pure function (formatters, validators, etc.)
521
+ - XSS: pass \`'<script>alert(1)</script>'\` as user-controlled strings, assert \`!r.has('script')\`
522
+
523
+ \`\`\`js
524
+ import assert from 'node:assert/strict'
525
+ import { renderSync } from '@invisibleloop/pulse/testing'
526
+ import spec from './my-page.js'
527
+
528
+ // View — pass mock state and server data
529
+ const r = renderSync(spec, { state: { count: 5 }, server: { items: [] } })
530
+ assert(r.has('main#main-content')) // element exists
531
+ assert.equal(r.get('h1').text, 'Title') // text content (throws if not found)
532
+ assert(r.has('button[disabled]')) // attribute present
533
+ assert(!r.has('.ui-badge')) // element absent
534
+ assert.equal(r.count('li'), 3) // count elements
535
+
536
+ // Mutations — pure functions, test directly
537
+ assert.deepEqual(spec.mutations.increment({ count: 0 }), { count: 1 })
538
+
539
+ // onViewError
540
+ const fallback = spec.onViewError(new Error('boom'), {}, {})
541
+ assert(fallback.includes('main-content'))
542
+ \`\`\`
543
+
544
+ **Selector support:** \`tag\`, \`.class\`, \`#id\`, \`[attr]\`, \`[attr="value"]\`, and combinations (\`button.primary[disabled]\`).
545
+ Descendant selectors (\`tbody tr\`, \`ul li\`) are NOT supported — use \`r.count('tr')\` not \`r.count('tbody tr')\`.
546
+
547
+ ## Spec review checklist
548
+
549
+ See \`.github/instructions/pulse-checklist.instructions.md\` — loaded automatically by Copilot.
550
+ `
551
+ }
552
+
439
553
  // ---------------------------------------------------------------------------
440
554
  // Helpers
441
555
  // ---------------------------------------------------------------------------