@invisibleloop/pulse 0.2.10 → 0.2.12

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.
@@ -32,21 +32,41 @@ Run `mcp__chrome-devtools__lighthouse_audit` on the same route with `{ "strategy
32
32
 
33
33
  **Same pass bar: all four categories must be 100.** Report the actual scores. If any score is below 100, fix and restart from step 2.
34
34
 
35
- ### 7. Console errors
35
+ ### 7. Performance
36
+
37
+ Navigate to the page route, then run `mcp__chrome-devtools__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.).
38
+
39
+ ### 8. Console errors
36
40
 
37
41
  Use `mcp__chrome-devtools__list_console_messages` — report any errors or unexpected warnings.
38
42
 
39
- ### 8. Close the browser
43
+ ### 9. Code review
44
+
45
+ Check the spec against the checklist in `.claude/pulse-checklist.md`. Work through every item. Fix anything that fails before proceeding to the next step.
46
+
47
+ ### 10. Close the browser
40
48
 
41
49
  Use `mcp__chrome-devtools__list_pages` to get all open pages, then call `mcp__chrome-devtools__close_page` for every page ID returned. This closes the entire browser, not just the tab. `pageId` must be a number, not a string.
42
50
 
43
- ### 9. Report
51
+ ### 11. Write verification stamp
52
+
53
+ Run:
54
+
55
+ ```bash
56
+ date +%s > .pulse-verified
57
+ ```
58
+
59
+ This records that verify completed successfully. The stop hook checks this stamp — do not skip this step.
60
+
61
+ ### 12. Report
44
62
 
45
63
  Summarise:
46
64
  - Validation: pass or fail (with errors if any)
47
65
  - Visual: what was visible in the screenshot
48
66
  - Lighthouse desktop: scores for all four categories
49
67
  - Lighthouse mobile: scores for all four categories
68
+ - Performance: LCP and CLS values, any flagged insights
50
69
  - Console: any errors
70
+ - Review: pass or issues found and fixed
51
71
 
52
- Only confirm the page is good when validation passes, both Lighthouse runs are 100/100/100/100, and there are no console errors. Otherwise, fix and run `/verify` again.
72
+ Only confirm the page is good when validation passes, both Lighthouse runs are 100/100/100/100, CLS is 0.00, and there are no console errors. Otherwise, fix and run `/verify` again.
@@ -78,7 +78,12 @@ Validate the email format before submitting."`, 'bash'))}
78
78
  ├── .claude/
79
79
  │ ├── CLAUDE.md ← session instructions Claude reads on startup
80
80
  │ ├── settings.json ← hooks: syntax checks, colour guards, package blocklist
81
- │ └── pulse-checklist.md ← spec review checklist, kept in sync by Pulse
81
+ │ ├── pulse-checklist.md ← spec review checklist, kept in sync by Pulse
82
+ │ └── commands/ ← slash commands available inside Claude
83
+ │ ├── pulse-dev.md
84
+ │ ├── pulse-stop.md
85
+ │ ├── pulse-build.md
86
+ │ └── pulse-start.md
82
87
  ├── package.json
83
88
  └── pulse.config.js ← port and project settings`, 'bash'))}
84
89
  <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>
@@ -93,6 +98,18 @@ Validate the email format before submitting."`, 'bash'))}
93
98
  /pulse-report # Lighthouse audit + performance report`, 'bash'))}
94
99
  <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>
95
100
 
101
+ ${section('update', 'Keeping up to date')}
102
+ <p>When a new version of Pulse is released, update the package and then run <code>pulse update</code> from your project root:</p>
103
+ ${codeBlock(highlight(`npm update @invisibleloop/pulse
104
+ pulse update`, 'bash'))}
105
+ <p><code>pulse update</code> copies the latest UI assets and agent files into your project:</p>
106
+ <ul>
107
+ <li><code>public/pulse-ui.css</code> and <code>public/pulse-ui.js</code> — component library styles and behaviour</li>
108
+ <li><code>.claude/pulse-checklist.md</code> — spec review checklist</li>
109
+ <li><code>.claude/commands/</code> — slash commands</li>
110
+ </ul>
111
+ <p>Your own source files, <code>CLAUDE.md</code>, and <code>pulse.config.js</code> are never touched.</p>
112
+
96
113
  ${section('next-steps', 'Next steps')}
97
114
  <ul>
98
115
  <li><a href="/how-it-works">How It Works</a> — the MCP server, what the agent knows, and the full build cycle</li>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invisibleloop/pulse",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
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.2.10
1
+ 0.2.12
@@ -0,0 +1,72 @@
1
+ # Verify
2
+
3
+ Run the full verification loop on the current page or the page specified in $ARGUMENTS.
4
+
5
+ ## Steps
6
+
7
+ ### 1. Identify the target
8
+
9
+ If $ARGUMENTS is provided, use it as the file path or route. Otherwise, identify the most recently edited spec file from context.
10
+
11
+ ### 2. Validate the spec
12
+
13
+ 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.
14
+
15
+ ### 3. Check the dev server
16
+
17
+ Use `pulse_fetch_page` to confirm the server is responding for the route. If it errors, use `pulse_restart_server` and retry once.
18
+
19
+ ### 4. Screenshot
20
+
21
+ Use `mcp__chrome-devtools__navigate_page` to load the page route, then `mcp__chrome-devtools__take_screenshot` to capture the result. Describe what you see — layout, content, any obvious rendering issues.
22
+
23
+ ### 5. Lighthouse — desktop
24
+
25
+ Run `mcp__chrome-devtools__lighthouse_audit` on the route with `{ "strategy": "desktop" }`.
26
+
27
+ **Pass bar: Performance, 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.
28
+
29
+ ### 6. Lighthouse — mobile
30
+
31
+ Run `mcp__chrome-devtools__lighthouse_audit` on the same route with `{ "strategy": "mobile" }`.
32
+
33
+ **Same pass bar: all four categories must be 100.** Report the actual scores. If any score is below 100, fix and restart from step 2.
34
+
35
+ ### 7. Performance
36
+
37
+ Navigate to the page route, then run `mcp__chrome-devtools__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.).
38
+
39
+ ### 8. Console errors
40
+
41
+ Use `mcp__chrome-devtools__list_console_messages` — report any errors or unexpected warnings.
42
+
43
+ ### 9. Code review
44
+
45
+ Check the spec against the checklist in `.claude/pulse-checklist.md`. Work through every item. Fix anything that fails before proceeding to the next step.
46
+
47
+ ### 10. Close the browser
48
+
49
+ Use `mcp__chrome-devtools__list_pages` to get all open pages, then call `mcp__chrome-devtools__close_page` for every page ID returned. This closes the entire browser, not just the tab. `pageId` must be a number, not a string.
50
+
51
+ ### 11. Write verification stamp
52
+
53
+ Run:
54
+
55
+ ```bash
56
+ date +%s > .pulse-verified
57
+ ```
58
+
59
+ This records that verify completed successfully. The stop hook checks this stamp — do not skip this step.
60
+
61
+ ### 12. Report
62
+
63
+ Summarise:
64
+ - Validation: pass or fail (with errors if any)
65
+ - Visual: what was visible in the screenshot
66
+ - Lighthouse desktop: scores for all four categories
67
+ - Lighthouse mobile: scores for all four categories
68
+ - Performance: LCP and CLS values, any flagged insights
69
+ - Console: any errors
70
+ - Review: pass or issues found and fixed
71
+
72
+ Only confirm the page is good when validation passes, both Lighthouse runs are 100/100/100/100, CLS is 0.00, and there are no console errors. Otherwise, fix and run `/verify` again.
@@ -35,7 +35,7 @@ try {
35
35
  // Parse the coverage report — look for lines with uncovered line numbers
36
36
  const lines = out.split('\n')
37
37
  let inReport = false
38
- const gaps = []
38
+ const rawGaps = []
39
39
 
40
40
  for (const line of lines) {
41
41
  if (line.includes('start of coverage report')) { inReport = true; continue }
@@ -46,11 +46,72 @@ for (const line of lines) {
46
46
  if (m) {
47
47
  const uncov = m[2].trim()
48
48
  if (uncov && uncov !== 'uncovered lines') {
49
- gaps.push(` ${m[1].trim()} — uncovered lines: ${uncov}`)
49
+ rawGaps.push({ file: m[1].trim(), uncovered: uncov })
50
50
  }
51
51
  }
52
52
  }
53
53
 
54
+ /**
55
+ * Returns true if all uncovered line numbers in the given file fall inside
56
+ * a `server:` property block or an action `run:` function — both of which
57
+ * call real external APIs and are explicitly exempt from coverage requirements.
58
+ */
59
+ function allLinesExempt(filePath, uncoveredSpec) {
60
+ let src
61
+ try { src = fs.readFileSync(filePath, 'utf8') } catch { return false }
62
+ const srcLines = src.split('\n')
63
+
64
+ // Expand "30-36 50" → [30, 31, 32, 33, 34, 35, 36, 50]
65
+ const lineNums = []
66
+ for (const part of uncoveredSpec.split(/\s+/)) {
67
+ const range = part.match(/^(\d+)-(\d+)$/)
68
+ if (range) {
69
+ for (let i = parseInt(range[1]); i <= parseInt(range[2]); i++) lineNums.push(i)
70
+ } else if (/^\d+$/.test(part)) {
71
+ lineNums.push(parseInt(part))
72
+ }
73
+ }
74
+
75
+ // Build a set of exempt line numbers: lines inside `server:` blocks or action `run:` functions.
76
+ // Strategy: scan the source tracking brace depth relative to the start of each exempt block.
77
+ const exemptLines = new Set()
78
+ let i = 0
79
+ while (i < srcLines.length) {
80
+ const trimmed = srcLines[i].trimStart()
81
+ // Detect start of `server:` property or action `run:` function
82
+ const isExemptStart = /^server\s*:/.test(trimmed) || /^run\s*:/.test(trimmed) || /^run\s*\(/.test(trimmed)
83
+ if (isExemptStart) {
84
+ // Mark lines from here until the block closes (depth reaches 0 after opening)
85
+ let depth = 0
86
+ let started = false
87
+ const startLine = i
88
+ while (i < srcLines.length) {
89
+ exemptLines.add(i + 1) // 1-based
90
+ for (const ch of srcLines[i]) {
91
+ if (ch === '{' || ch === '(') { depth++; started = true }
92
+ if (ch === '}' || ch === ')') { depth-- }
93
+ }
94
+ if (started && depth <= 0) break
95
+ i++
96
+ }
97
+ }
98
+ i++
99
+ }
100
+
101
+ return lineNums.every(n => exemptLines.has(n))
102
+ }
103
+
104
+ const gaps = []
105
+ for (const { file, uncovered } of rawGaps) {
106
+ // Try to resolve relative to cwd (coverage reports use relative paths)
107
+ const candidates = [file, `src/pages/${file}`, file.replace(/^.*src\/pages\//, 'src/pages/')]
108
+ const resolved = candidates.find(p => fs.existsSync(p))
109
+
110
+ if (resolved && allLinesExempt(resolved, uncovered)) continue // auto-exempt
111
+
112
+ gaps.push(` ${file} — uncovered lines: ${uncovered}`)
113
+ }
114
+
54
115
  if (gaps.length) {
55
116
  process.stdout.write(JSON.stringify({
56
117
  decision: 'block',
package/src/cli/dev.js CHANGED
@@ -63,12 +63,18 @@ const PUBLIC_DIR = path.join(ROOT, 'public')
63
63
  }
64
64
  fs.writeFileSync(stampPath, pkgVersion, 'utf8')
65
65
 
66
- // Sync agent checklist into .claude/ so CLAUDE.md can import it
67
- const checklistSrc = new URL('../agent/checklist.md', import.meta.url).pathname
68
- const checklistDst = path.join(ROOT, '.claude', 'pulse-checklist.md')
69
- if (fs.existsSync(checklistSrc)) {
70
- fs.mkdirSync(path.dirname(checklistDst), { recursive: true })
71
- fs.copyFileSync(checklistSrc, checklistDst)
66
+ // Sync agent files into .claude/
67
+ const agentFiles = [
68
+ ['../agent/checklist.md', 'pulse-checklist.md'],
69
+ ['../agent/coverage-check.js', 'coverage-check.js'],
70
+ ]
71
+ for (const [rel, dst] of agentFiles) {
72
+ const src = new URL(rel, import.meta.url).pathname
73
+ if (fs.existsSync(src)) {
74
+ const dstPath = path.join(ROOT, '.claude', dst)
75
+ fs.mkdirSync(path.dirname(dstPath), { recursive: true })
76
+ fs.copyFileSync(src, dstPath)
77
+ }
72
78
  }
73
79
 
74
80
  // Sync slash commands into .claude/commands/ so /pulse-dev etc. always exist
package/src/cli/index.js CHANGED
@@ -292,13 +292,19 @@ async function runUpdate(root) {
292
292
  updated.push(`public/${asset}`)
293
293
  }
294
294
 
295
- // Sync agent checklist into .claude/
296
- const checklistSrc = new URL('../agent/checklist.md', import.meta.url).pathname
297
- const checklistDst = path.join(root, '.claude', 'pulse-checklist.md')
298
- if (fs.existsSync(checklistSrc)) {
299
- fs.mkdirSync(path.dirname(checklistDst), { recursive: true })
300
- fs.copyFileSync(checklistSrc, checklistDst)
301
- updated.push('.claude/pulse-checklist.md')
295
+ // Sync agent files into .claude/
296
+ const agentFiles = [
297
+ ['../agent/checklist.md', 'pulse-checklist.md'],
298
+ ['../agent/coverage-check.js', 'coverage-check.js'],
299
+ ]
300
+ for (const [rel, dst] of agentFiles) {
301
+ const src = new URL(rel, import.meta.url).pathname
302
+ if (fs.existsSync(src)) {
303
+ const dstPath = path.join(root, '.claude', dst)
304
+ fs.mkdirSync(path.dirname(dstPath), { recursive: true })
305
+ fs.copyFileSync(src, dstPath)
306
+ updated.push(`.claude/${dst}`)
307
+ }
302
308
  }
303
309
 
304
310
  // Sync slash commands into .claude/commands/
@@ -168,6 +168,11 @@ ${port !== 3000 ? ` port: ${port},\n` : ''}}
168
168
  type: 'command',
169
169
  command: `node .claude/coverage-check.js`,
170
170
  statusMessage: 'Checking test coverage...',
171
+ },
172
+ {
173
+ type: 'command',
174
+ 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.'}));}"`,
175
+ statusMessage: 'Checking verification stamp...',
171
176
  }
172
177
  ]
173
178
  }