@tangle-network/browser-agent-driver 0.11.0 → 0.12.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.
Files changed (41) hide show
  1. package/dist/brain/index.d.ts +1 -0
  2. package/dist/brain/index.d.ts.map +1 -1
  3. package/dist/brain/index.js +7 -1
  4. package/dist/brain/index.js.map +1 -1
  5. package/dist/cli-auth.d.ts +19 -0
  6. package/dist/cli-auth.d.ts.map +1 -0
  7. package/dist/cli-auth.js +195 -0
  8. package/dist/cli-auth.js.map +1 -0
  9. package/dist/cli-design-audit.d.ts +8 -0
  10. package/dist/cli-design-audit.d.ts.map +1 -1
  11. package/dist/cli-design-audit.js +609 -30
  12. package/dist/cli-design-audit.js.map +1 -1
  13. package/dist/cli-ui.d.ts.map +1 -1
  14. package/dist/cli-ui.js +18 -0
  15. package/dist/cli-ui.js.map +1 -1
  16. package/dist/cli.js +48 -1
  17. package/dist/cli.js.map +1 -1
  18. package/dist/drivers/playwright.d.ts +1 -0
  19. package/dist/drivers/playwright.d.ts.map +1 -1
  20. package/dist/drivers/playwright.js +21 -5
  21. package/dist/drivers/playwright.js.map +1 -1
  22. package/dist/drivers/types.d.ts +8 -0
  23. package/dist/drivers/types.d.ts.map +1 -1
  24. package/dist/runner/effect-verification.d.ts +12 -0
  25. package/dist/runner/effect-verification.d.ts.map +1 -0
  26. package/dist/runner/effect-verification.js +169 -0
  27. package/dist/runner/effect-verification.js.map +1 -0
  28. package/dist/runner/index.d.ts +1 -0
  29. package/dist/runner/index.d.ts.map +1 -1
  30. package/dist/runner/index.js +2 -0
  31. package/dist/runner/index.js.map +1 -1
  32. package/dist/runner/runner.d.ts.map +1 -1
  33. package/dist/runner/runner.js +10 -57
  34. package/dist/runner/runner.js.map +1 -1
  35. package/dist/runner.d.ts +1 -1
  36. package/dist/runner.d.ts.map +1 -1
  37. package/dist/runner.js +2 -0
  38. package/dist/runner.js.map +1 -1
  39. package/dist/types.d.ts +47 -0
  40. package/dist/types.d.ts.map +1 -1
  41. package/package.json +3 -1
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import * as fs from 'node:fs';
8
8
  import * as path from 'node:path';
9
+ import { execSync } from 'node:child_process';
9
10
  import chalk from 'chalk';
10
11
  import { chromium } from 'playwright';
11
12
  import { Brain } from './brain/index.js';
@@ -67,55 +68,169 @@ MARKETING/LANDING PAGE AUDIT — evaluate as a potential customer deciding in 10
67
68
  - Footer: is navigation complete? Legal links present?
68
69
 
69
70
  CALIBRATION: Stripe, Linear, Vercel = 9. Average startup landing page = 5. Template sites = 3.`,
71
+ vibecoded: `
72
+ VIBECODED / AI-GENERATED APP AUDIT — evaluate as a design-literate user who can smell defaults:
73
+
74
+ TEMPLATE DETECTION (the #1 sin of vibecoded apps):
75
+ - Is this clearly an unmodified shadcn/ui, MUI, Ant Design, or Chakra template? Score ceiling: 4 if yes.
76
+ - Default border-radius (6-8px shadcn, 4px MUI), default color palette (zinc/slate grays, blue-600 primary)?
77
+ - Default component spacing with no customization? Standard card shadows? Stock empty states?
78
+ - "Looks like every other AI-generated app" = automatic 3-4 score.
79
+
80
+ HIERARCHY & INFORMATION ARCHITECTURE:
81
+ - Is everything the same visual weight? (Common AI pattern: all cards same size, no primary/secondary distinction)
82
+ - Is there clear information hierarchy? Primary action vs secondary vs tertiary?
83
+ - Does the layout have purpose or is it "centered column of cards" (the AI default)?
84
+ - Navigation: is it a dumped list of features or thoughtfully organized?
85
+
86
+ DESIGN SYSTEM COHERENCE:
87
+ - Are there more than 3 distinct border-radius values? (Incoherent)
88
+ - Color palette: intentional and limited (4-6 colors) or random accumulation?
89
+ - Spacing: consistent rhythm on an 8px grid, or arbitrary per-component?
90
+ - Typography: deliberate scale with 3-4 sizes, or every component picking its own?
91
+ - Are interactive states (hover, focus, active, disabled) designed or browser-default?
92
+
93
+ CRAFT SIGNALS (what separates 7 from 9):
94
+ - Custom icons or generic Lucide/Heroicons dump?
95
+ - Micro-interactions: button press feedback, page transitions, loading skeletons?
96
+ - Empty states: designed illustrations or "No data found" text?
97
+ - Error states: helpful messages with recovery actions or raw error strings?
98
+ - Dark mode (if present): properly designed or just "invert colors"?
99
+ - Content-first: does real content drive the layout, or is it a container waiting for content?
100
+
101
+ AGENTIC APP SPECIFICS:
102
+ - Agent status indicators: is it clear what the agent is doing? Progress feedback?
103
+ - Streaming/loading: smooth token streaming or janky text replacement?
104
+ - Conversation UI: proper message bubbles with timestamps, or plain text dump?
105
+ - Tool call visualization: can the user see what tools the agent used?
106
+ - Error recovery: when the agent fails, is there a clear retry/edit path?
107
+
108
+ CALIBRATION:
109
+ - 9-10: Custom design system, thoughtful hierarchy, polished interactions (Linear, Cursor, v0.dev)
110
+ - 7-8: Modified template with intentional design decisions, consistent system
111
+ - 5-6: Lightly customized template, functional but generic (most AI-generated apps)
112
+ - 3-4: Unmodified component library, no design investment, "it works" energy
113
+ - 1-2: Broken layout, clashing styles, unusable
114
+
115
+ Most vibecoded apps score 3-5. The ceiling for unmodified templates is 4 regardless of functionality.`,
70
116
  };
71
117
  // ---------------------------------------------------------------------------
72
118
  // Upgraded system prompt — much more opinionated than the original
73
119
  // ---------------------------------------------------------------------------
74
120
  function buildAuditPrompt(profile) {
75
121
  const rubric = PROFILE_RUBRICS[profile] || PROFILE_RUBRICS.general;
76
- return `You are a brutal, honest design critic with 15 years of experience at top design studios.
77
- You have zero tolerance for mediocrity. You call out every flaw you see.
122
+ return `You are a principal design engineer who has shipped design systems at Linear, Stripe, and Vercel. You review with the precision of a typographer and the ruthlessness of a design director. You have built and maintained production design systems used by millions.
123
+
124
+ Your job: perform an exhaustive visual design audit of this page. You must be specific enough that a developer could fix every issue from your report alone — reference exact elements, computed values, pixel measurements, and CSS properties.
78
125
 
79
- Your job: audit this page's visual design, UX, and polish. Be specific — reference exact elements, colors, spacing values, and positions.
126
+ EVALUATION FRAMEWORK (score each area 1-10, then weight into overall):
80
127
 
81
- EVALUATION CRITERIA:
82
- 1. LAYOUT Grid consistency, alignment, responsive behavior, content hierarchy
83
- 2. TYPOGRAPHY Font pairing, size scale, line height, letter spacing, readability
84
- 3. COLOR Palette coherence, contrast ratios (WCAG AA: 4.5:1 text, 3:1 large text), semantic usage
85
- 4. SPACING Consistent rhythm (4/8px grid), padding/margin consistency, breathing room
86
- 5. COMPONENTS Button styles, input fields, cards, modals — are they consistent?
87
- 6. INTERACTIONS Hover states, focus indicators, transitions, loading states
88
- 7. ACCESSIBILITY — Alt text, labels, keyboard navigation, screen reader compatibility
89
- 8. VISUAL POLISH Border radius consistency, shadow depth, icon style, micro-details
128
+ 1. LAYOUT & GRID (weight: 15%)
129
+ - Is there a consistent grid system? What grid unit? (4px, 8px, etc.)
130
+ - Column alignment: do content blocks align to the same left/right edges?
131
+ - Content width: is max-width appropriate? (prose: 65-75ch, app: fluid with sidebar)
132
+ - Responsive: does the layout reflow intentionally or just shrink?
133
+ - Z-index layering: any stacking context issues? Overlapping elements?
134
+ - Check for: orphaned elements floating outside the grid, inconsistent container padding
135
+
136
+ 2. TYPOGRAPHY SYSTEM (weight: 15%)
137
+ - Type scale: is there a clear hierarchy? Count distinct font-size values — more than 5-6 suggests no scale.
138
+ - Line height: body text should be 1.4-1.6, headings 1.1-1.3. Flag violations.
139
+ - Letter spacing: headings often need negative (-0.01 to -0.03em). Is it tuned?
140
+ - Font pairing: max 2 families (heading + body). Flag 3+.
141
+ - Text rendering: are long paragraphs wider than 75ch? That harms readability.
142
+ - Orphans/widows: any single-word last lines in headings?
143
+ - Font loading: is there FOUT/FOIT? Font-display strategy?
144
+
145
+ 3. COLOR & CONTRAST (weight: 15%)
146
+ - WCAG AA compliance: normal text needs 4.5:1, large text (18px+/14px+ bold) needs 3:1. ESTIMATE ratios.
147
+ - Palette size: count distinct hues. More than 5-6 non-neutral hues = incoherent.
148
+ - Semantic color usage: is the primary color used consistently for primary actions?
149
+ - Background layering: do nested surfaces have clear elevation (bg-0, bg-1, bg-2)?
150
+ - Gray scale: are grays consistent? All blue-gray, or mixed warm/cool? Mixed = incoherent.
151
+ - Accent usage: are accent colors used sparingly or splashed everywhere?
152
+
153
+ 4. SPACING & RHYTHM (weight: 15%)
154
+ - Grid adherence: what % of spacing values are multiples of the base unit?
155
+ - Vertical rhythm: are section gaps consistent? Measure gap between each major section.
156
+ - Component internal spacing: is padding consistent within similar components (all cards, all inputs)?
157
+ - Whitespace ratio: is there enough breathing room, or is everything cramped?
158
+ - Margin collapse issues: any unintended spacing from margin collapse?
159
+
160
+ 5. COMPONENT CONSISTENCY (weight: 15%)
161
+ - Button variants: how many distinct button styles? Are they intentional variants or accidents?
162
+ - Input styling: are all form inputs styled consistently? Border, focus ring, label position?
163
+ - Card patterns: same border-radius, shadow, padding across all cards?
164
+ - Icon system: consistent size (16/20/24px), stroke width, and style?
165
+ - Border radius: count distinct values. More than 3 (e.g., 4px, 8px, full) = incoherent.
166
+ - Shadow system: consistent elevation scale or random drop shadows?
167
+
168
+ 6. INTERACTION DESIGN (weight: 10%)
169
+ - Hover states: do interactive elements have visible hover feedback?
170
+ - Focus indicators: are there visible focus rings for keyboard navigation?
171
+ - Active/pressed states: button feedback on click?
172
+ - Transitions: are they present? Consistent duration (150-300ms)? Appropriate easing?
173
+ - Loading states: skeleton screens, spinners, or no loading feedback at all?
174
+ - Cursor changes: does cursor change to pointer on clickable elements?
175
+
176
+ 7. ACCESSIBILITY (weight: 10%)
177
+ - Semantic HTML: are headings in order (h1 → h2 → h3)? Are buttons actually <button>?
178
+ - ARIA labels: do icon-only buttons have labels? Do images have alt text?
179
+ - Keyboard navigation: can you tell what's focused? Is tab order logical?
180
+ - Touch targets: are mobile tap targets at least 44x44px?
181
+ - Screen reader: is content structured so screen reader users get meaningful navigation?
182
+
183
+ 8. VISUAL POLISH (weight: 5%)
184
+ - Pixel precision: any elements off by 1px? Misaligned text baselines?
185
+ - Image quality: are images sharp on retina displays (2x resolution)?
186
+ - Icon consistency: all from the same set, or a mix of styles/weights?
187
+ - Empty states: designed or raw "no data" text?
188
+ - Error states: styled or browser-default?
189
+ - Favicon and meta: present and professional?
90
190
 
91
191
  ${rubric}
92
192
 
93
- IMPORTANT RULES:
94
- - Be SPECIFIC. "Spacing is inconsistent" is useless. "The gap between the header and hero section is 48px but between hero and features is 24px — inconsistent vertical rhythm" is useful.
95
- - Reference element positions: "top-left navigation", "hero CTA button", "footer column 3".
96
- - Call out GOOD design too — note what works well alongside what doesn't.
97
- - If the page looks like it uses a default component library (shadcn, MUI, Ant) with no customization, say so.
98
- - Compare to best-in-class: "The token selector dropdown lacks the polish of Uniswap's — no token icons, no search, no recent tokens."
193
+ SPECIFICITY REQUIREMENTS — your findings must be THIS specific:
194
+ - BAD: "Spacing is inconsistent" (vague, useless)
195
+ - GOOD: "Section gap between hero and features is 48px, but features-to-pricing is 24px and pricing-to-footer is 64px. Use consistent 48px or 64px vertical rhythm throughout."
196
+ - BAD: "Colors don't look right" (vague)
197
+ - GOOD: "Body text (#6b7280) on white background has ~4.6:1 contrast ratio (barely passes AA). The same gray on the light-gray card background (#f9fafb) drops to ~3.8:1 — fails AA for normal text. Darken body text to #4b5563 (7:1+)."
198
+ - BAD: "Typography needs work" (vague)
199
+ - GOOD: "6 distinct font sizes detected (12, 13, 14, 16, 20, 32px) with no clear scale. Consolidate to a 4-step scale: 14px body, 16px large, 24px h2, 36px h1. Current h2 at 20px lacks sufficient contrast with 16px body text."
200
+
201
+ For EACH finding, you MUST include a concrete CSS fix in the suggestion field. Not "improve spacing" but "gap: 48px" or "font-size: 14px; line-height: 1.5".
99
202
 
100
203
  RESPOND WITH ONLY a JSON object:
101
204
  {
102
205
  "score": 6,
103
- "summary": "One-sentence overall assessment",
104
- "strengths": ["Specific thing done well", "Another strength"],
206
+ "summary": "One-sentence overall assessment with the key design system failure mode",
207
+ "strengths": ["Specific thing done well with evidence", "Another measured strength"],
105
208
  "findings": [
106
209
  {
107
210
  "category": "spacing",
108
211
  "severity": "major",
109
- "description": "Hero section has 64px top padding but only 16px bottom padding before the feature grid, creating visual imbalance",
110
- "location": "Hero section → feature grid transition",
111
- "suggestion": "Use consistent 48px vertical sections throughout"
212
+ "description": "Hero section has 64px top padding but only 16px bottom padding before the feature grid, creating visual imbalance. The 4:1 ratio breaks vertical rhythm.",
213
+ "location": "Hero section → feature grid transition (main > section:nth-child(2))",
214
+ "suggestion": "padding-bottom: 48px on hero section. Standardize all section gaps to 48px or 64px.",
215
+ "cssSelector": "main > section:first-child",
216
+ "cssFix": "padding-bottom: 48px"
112
217
  }
113
- ]
218
+ ],
219
+ "designSystemScore": {
220
+ "layout": 7,
221
+ "typography": 5,
222
+ "color": 6,
223
+ "spacing": 4,
224
+ "components": 6,
225
+ "interactions": 3,
226
+ "accessibility": 5,
227
+ "polish": 4
228
+ }
114
229
  }
115
230
 
116
231
  Categories: visual-bug, layout, contrast, alignment, spacing, typography, accessibility, ux
117
- Severities: critical (blocks usage), major (looks unprofessional), minor (polish issue)
118
- Score: 1-10 per calibration above. Most sites are 5-7. Be honest.`;
232
+ Severities: critical (blocks usage or fails WCAG), major (looks unprofessional), minor (polish detail)
233
+ Score: 1-10 per calibration above. Most production apps score 5-7. Template apps score 3-5. Only world-class ships get 8+. Be honest — inflated scores help nobody.`;
119
234
  }
120
235
  // ---------------------------------------------------------------------------
121
236
  // Page discovery — find key pages by crawling links
@@ -207,15 +322,29 @@ function generateReport(results, profile) {
207
322
  lines.push(`- ${s}`);
208
323
  lines.push('');
209
324
  }
325
+ if (result.designSystemScore) {
326
+ lines.push('**Design System Breakdown:**');
327
+ lines.push('');
328
+ const ds = result.designSystemScore;
329
+ const dsKeys = ['layout', 'typography', 'color', 'spacing', 'components', 'interactions', 'accessibility', 'polish'];
330
+ for (const key of dsKeys) {
331
+ if (ds[key] !== undefined) {
332
+ const bar = '█'.repeat(Math.round(ds[key])) + '░'.repeat(10 - Math.round(ds[key]));
333
+ lines.push(`- ${key}: \`${bar}\` ${ds[key]}/10`);
334
+ }
335
+ }
336
+ lines.push('');
337
+ }
210
338
  if (result.findings.length > 0) {
211
339
  lines.push('**Findings:**');
212
340
  lines.push('');
213
- lines.push('| Sev | Category | Description | Location | Fix |');
214
- lines.push('|-----|----------|-------------|----------|-----|');
341
+ lines.push('| Sev | Category | Description | Location | Fix | CSS |');
342
+ lines.push('|-----|----------|-------------|----------|-----|-----|');
215
343
  for (const f of result.findings) {
216
344
  const esc = (s) => s.replace(/\|/g, '\\|').replace(/\n/g, ' ').slice(0, 120);
217
345
  const icon = f.severity === 'critical' ? '🔴' : f.severity === 'major' ? '🟡' : '⚪';
218
- lines.push(`| ${icon} ${f.severity} | ${f.category} | ${esc(f.description)} | ${esc(f.location)} | ${esc(f.suggestion)} |`);
346
+ const cssFix = f.cssFix ? `\`${esc(f.cssFix)}\`` : '';
347
+ lines.push(`| ${icon} ${f.severity} | ${f.category} | ${esc(f.description)} | ${esc(f.location)} | ${esc(f.suggestion)} | ${cssFix} |`);
219
348
  }
220
349
  lines.push('');
221
350
  }
@@ -239,7 +368,7 @@ export async function runDesignAudit(opts) {
239
368
  process.exit(1);
240
369
  }
241
370
  const maxPages = opts.pages ?? 5;
242
- const provider = (opts.provider ?? 'openai');
371
+ const provider = (opts.provider ?? 'claude-code');
243
372
  const modelName = resolveProviderModelName(provider, opts.model);
244
373
  const apiKey = opts.apiKey ?? resolveProviderApiKey(provider);
245
374
  const [vw, vh] = (opts.viewport ?? '1440x900').split('x').map(Number);
@@ -270,6 +399,7 @@ export async function runDesignAudit(opts) {
270
399
  provider: opts.provider,
271
400
  vision: true,
272
401
  debug: opts.debug,
402
+ llmTimeoutMs: 120_000, // design audits generate ~8k tokens of structured JSON — need 2min
273
403
  });
274
404
  const driver = new PlaywrightDriver(page);
275
405
  // Audit each page
@@ -320,8 +450,448 @@ export async function runDesignAudit(opts) {
320
450
  if (screenshotDir)
321
451
  console.log(` ${chalk.dim('Screenshots →')} ${screenshotDir}`);
322
452
  console.log('');
453
+ // ── Reproducibility mode: run 3x and report variance ──
454
+ if (opts.reproducibility) {
455
+ console.log(` ${chalk.bold('Reproducibility test')} — running 2 additional audits…`);
456
+ const scores = [avgScore];
457
+ for (let rep = 0; rep < 2; rep++) {
458
+ const repResults = [];
459
+ for (const url of pages) {
460
+ const r = await auditSinglePage(brain, driver, page, url, profile);
461
+ repResults.push(r);
462
+ }
463
+ const repAvg = repResults.reduce((s, r) => s + r.score, 0) / repResults.length;
464
+ scores.push(repAvg);
465
+ console.log(` ${chalk.dim(` Rep ${rep + 2}:`)} ${repAvg.toFixed(1)}/10`);
466
+ }
467
+ const mean = scores.reduce((a, b) => a + b, 0) / scores.length;
468
+ const variance = Math.sqrt(scores.reduce((sum, s) => sum + (s - mean) ** 2, 0) / scores.length);
469
+ const pass = variance <= 0.5;
470
+ const varColor = pass ? chalk.green : chalk.red;
471
+ console.log(` ${chalk.dim('Scores:')} ${scores.map(s => s.toFixed(1)).join(', ')}`);
472
+ console.log(` ${chalk.dim('Mean:')} ${mean.toFixed(2)} ${chalk.dim('±')} ${varColor(variance.toFixed(2))} ${pass ? chalk.green('PASS (±0.5)') : chalk.red('FAIL (>±0.5)')}`);
473
+ console.log('');
474
+ if (opts.json) {
475
+ const repPath = path.join(outputDir, 'reproducibility.json');
476
+ fs.writeFileSync(repPath, JSON.stringify({ scores, mean, stddev: variance, pass }, null, 2));
477
+ }
478
+ }
479
+ // ── Evolve mode: closed-loop fix → re-audit ──
480
+ if (opts.evolve) {
481
+ // --evolve=css (or --evolve=true) → CSS injection
482
+ // --evolve=claude-code|codex|opencode|<custom> → agent dispatch
483
+ const evolveMode = opts.evolve === true || opts.evolve === 'true' || opts.evolve === 'css' ? 'css' : opts.evolve;
484
+ let evolveResult;
485
+ if (evolveMode !== 'css') {
486
+ // Agent-dispatched evolve — a coding agent edits the actual source code
487
+ const projectDir = opts.projectDir ?? process.cwd();
488
+ evolveResult = await runAgentEvolveLoop(brain, driver, page, pages, profile, results, outputDir, opts.evolveRounds ?? 3, evolveMode, projectDir, opts.debug);
489
+ }
490
+ else {
491
+ // CSS-injection evolve — ephemeral fixes injected into the browser page
492
+ evolveResult = await runEvolveLoop(brain, driver, page, pages, profile, results, outputDir, opts.evolveRounds ?? 3);
493
+ }
494
+ // Write evolve report
495
+ const evolvePath = path.join(outputDir, 'evolve-report.md');
496
+ fs.writeFileSync(evolvePath, generateEvolveReport(evolveResult));
497
+ console.log(` ${chalk.dim('Evolve report →')} ${evolvePath}`);
498
+ // Write CSS override file (CSS-injection mode only)
499
+ if (evolveResult.cssOverride) {
500
+ const cssPath = path.join(outputDir, 'design-fixes.css');
501
+ fs.writeFileSync(cssPath, evolveResult.cssOverride);
502
+ console.log(` ${chalk.dim('CSS fixes →')} ${cssPath}`);
503
+ }
504
+ if (opts.json) {
505
+ const evJsonPath = path.join(outputDir, 'evolve.json');
506
+ fs.writeFileSync(evJsonPath, JSON.stringify(evolveResult, null, 2));
507
+ }
508
+ console.log('');
509
+ }
323
510
  await browser.close();
324
511
  }
512
+ async function runEvolveLoop(brain, driver, page, pages, profile, initialResults, outputDir, maxRounds) {
513
+ const initialAvg = initialResults.reduce((s, r) => s + r.score, 0) / initialResults.length;
514
+ const scoreHistory = [initialAvg];
515
+ const appliedFixes = [];
516
+ const skippedFixes = [];
517
+ let cumulativeCSS = '';
518
+ let currentResults = initialResults;
519
+ let currentAvg = initialAvg;
520
+ console.log('');
521
+ console.log(` ${chalk.bold('Design Evolve')} — ${maxRounds} rounds max`);
522
+ console.log(` ${chalk.dim('Initial score:')} ${currentAvg.toFixed(1)}/10`);
523
+ console.log('');
524
+ for (let round = 1; round <= maxRounds; round++) {
525
+ console.log(` ${chalk.dim(`Round ${round}/${maxRounds}`)}`);
526
+ // Collect all findings with CSS fixes across all pages
527
+ const fixableFixes = currentResults
528
+ .flatMap(r => r.findings)
529
+ .filter(f => f.cssSelector && f.cssFix);
530
+ if (fixableFixes.length === 0) {
531
+ console.log(` ${chalk.dim(' No CSS-fixable findings — generating fixes via LLM…')}`);
532
+ // Ask the LLM to generate CSS fixes for the top findings
533
+ const topFindings = currentResults
534
+ .flatMap(r => r.findings)
535
+ .filter(f => f.severity === 'critical' || f.severity === 'major')
536
+ .slice(0, 10);
537
+ if (topFindings.length === 0) {
538
+ console.log(` ${chalk.green(' No major/critical findings remaining')}`);
539
+ break;
540
+ }
541
+ const fixPrompt = buildFixGenerationPrompt(topFindings);
542
+ const fixResult = await brain.auditDesign(await driver.observe(), 'Generate CSS fixes for the design issues listed below', [], fixPrompt);
543
+ // Parse generated fixes
544
+ try {
545
+ let text = fixResult.raw.trim();
546
+ if (text.startsWith('```'))
547
+ text = text.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '');
548
+ const start = text.indexOf('{');
549
+ const end = text.lastIndexOf('}');
550
+ if (start >= 0 && end > start)
551
+ text = text.slice(start, end + 1);
552
+ const parsed = JSON.parse(text);
553
+ if (Array.isArray(parsed.fixes)) {
554
+ for (const fix of parsed.fixes) {
555
+ if (fix.cssSelector && fix.cssFix) {
556
+ fixableFixes.push({
557
+ category: 'ux',
558
+ severity: 'major',
559
+ description: fix.description || '',
560
+ location: fix.location || '',
561
+ suggestion: fix.cssFix,
562
+ cssSelector: fix.cssSelector,
563
+ cssFix: fix.cssFix,
564
+ });
565
+ }
566
+ }
567
+ }
568
+ }
569
+ catch { /* failed to parse fixes */ }
570
+ }
571
+ if (fixableFixes.length === 0) {
572
+ console.log(` ${chalk.dim(' Could not generate fixable CSS — stopping')}`);
573
+ break;
574
+ }
575
+ // Build CSS override from all fixable findings
576
+ const roundCSS = fixableFixes
577
+ .map(f => `/* ${f.severity}: ${f.description?.slice(0, 80)} */\n${f.cssSelector} { ${f.cssFix} }`)
578
+ .join('\n\n');
579
+ cumulativeCSS += '\n' + roundCSS;
580
+ // Track applied fixes
581
+ for (const f of fixableFixes) {
582
+ appliedFixes.push({
583
+ cssSelector: f.cssSelector,
584
+ cssFix: f.cssFix,
585
+ finding: f.description,
586
+ });
587
+ }
588
+ console.log(` ${chalk.dim(` Applying ${fixableFixes.length} CSS fixes…`)}`);
589
+ // Re-audit each page with CSS injected
590
+ const roundResults = [];
591
+ for (const url of pages) {
592
+ try {
593
+ await page.goto(url, { waitUntil: 'networkidle', timeout: 20_000 }).catch(() => page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15_000 }));
594
+ await page.waitForTimeout(1500);
595
+ // Inject cumulative CSS fixes
596
+ await page.addStyleTag({ content: cumulativeCSS });
597
+ await page.waitForTimeout(500);
598
+ // Take screenshot of fixed state
599
+ const screenshotDir = path.join(outputDir, `screenshots-round-${round}`);
600
+ fs.mkdirSync(screenshotDir, { recursive: true });
601
+ const result = await auditSinglePage(brain, driver, page, url, profile, screenshotDir);
602
+ roundResults.push(result);
603
+ }
604
+ catch {
605
+ roundResults.push({
606
+ url,
607
+ score: currentAvg,
608
+ summary: 'Re-audit failed',
609
+ strengths: [],
610
+ findings: [],
611
+ error: 'Re-audit with CSS injection failed',
612
+ });
613
+ }
614
+ }
615
+ const roundAvg = roundResults.reduce((s, r) => s + r.score, 0) / roundResults.length;
616
+ scoreHistory.push(roundAvg);
617
+ const delta = roundAvg - currentAvg;
618
+ const deltaStr = delta >= 0 ? chalk.green(`+${delta.toFixed(1)}`) : chalk.red(delta.toFixed(1));
619
+ console.log(` ${chalk.dim(' Score:')} ${roundAvg.toFixed(1)}/10 (${deltaStr})`);
620
+ currentResults = roundResults;
621
+ currentAvg = roundAvg;
622
+ // Check convergence — if no improvement, stop
623
+ if (delta <= 0.1 && round > 1) {
624
+ console.log(` ${chalk.dim(' Converged — no further improvement')}`);
625
+ break;
626
+ }
627
+ }
628
+ const totalDelta = currentAvg - initialAvg;
629
+ const deltaColor = totalDelta >= 2 ? chalk.green : totalDelta > 0 ? chalk.yellow : chalk.red;
630
+ console.log('');
631
+ console.log(` ${chalk.bold('Evolve complete')}`);
632
+ console.log(` ${chalk.dim('Score:')} ${initialAvg.toFixed(1)} → ${currentAvg.toFixed(1)} (${deltaColor(`+${totalDelta.toFixed(1)}`)})`);
633
+ console.log(` ${chalk.dim('Rounds:')} ${scoreHistory.length - 1}`);
634
+ console.log(` ${chalk.dim('Fixes applied:')} ${appliedFixes.length}`);
635
+ console.log('');
636
+ return {
637
+ beforeScore: initialAvg,
638
+ afterScore: currentAvg,
639
+ delta: totalDelta,
640
+ rounds: scoreHistory.length - 1,
641
+ appliedFixes,
642
+ skippedFixes,
643
+ scoreHistory,
644
+ cssOverride: cumulativeCSS.trim(),
645
+ };
646
+ }
647
+ function buildFixGenerationPrompt(findings) {
648
+ const findingList = findings.map((f, i) => `${i + 1}. [${f.severity}/${f.category}] ${f.description}\n Location: ${f.location}\n Suggestion: ${f.suggestion}`).join('\n');
649
+ return `You are a CSS engineer fixing design issues. For each finding, generate a precise CSS fix.
650
+
651
+ FINDINGS TO FIX:
652
+ ${findingList}
653
+
654
+ RULES:
655
+ - Use specific, targeted CSS selectors. Prefer class-based or semantic selectors.
656
+ - Each fix should be a single CSS rule (selector + property:value pairs).
657
+ - Fixes must not break other elements — be surgical.
658
+ - For spacing: use consistent values (multiples of 4 or 8px).
659
+ - For colors: ensure WCAG AA contrast (4.5:1 for text, 3:1 for large text).
660
+ - For typography: use a limited scale (14px, 16px, 20px, 24px, 32px, 48px).
661
+
662
+ RESPOND WITH ONLY a JSON object:
663
+ {
664
+ "fixes": [
665
+ {
666
+ "cssSelector": "main > section:first-child",
667
+ "cssFix": "padding-bottom: 48px; margin-bottom: 0",
668
+ "description": "Standardize hero section bottom spacing",
669
+ "location": "Hero → features transition"
670
+ }
671
+ ]
672
+ }`;
673
+ }
674
+ function generateEvolveReport(result) {
675
+ const lines = [];
676
+ lines.push('# Design Evolve Report');
677
+ lines.push('');
678
+ lines.push(`**Score:** ${result.beforeScore.toFixed(1)} → ${result.afterScore.toFixed(1)} (+${result.delta.toFixed(1)})`);
679
+ lines.push(`**Rounds:** ${result.rounds}`);
680
+ lines.push(`**Score progression:** ${result.scoreHistory.map(s => s.toFixed(1)).join(' → ')}`);
681
+ lines.push('');
682
+ if (result.appliedFixes.length > 0) {
683
+ lines.push('## Applied Fixes');
684
+ lines.push('');
685
+ for (const fix of result.appliedFixes) {
686
+ lines.push(`- \`${fix.cssSelector}\`: \`${fix.cssFix}\``);
687
+ if (fix.finding)
688
+ lines.push(` - ${fix.finding}`);
689
+ }
690
+ lines.push('');
691
+ }
692
+ if (result.cssOverride) {
693
+ lines.push('## Generated CSS Override');
694
+ lines.push('');
695
+ lines.push('```css');
696
+ lines.push(result.cssOverride);
697
+ lines.push('```');
698
+ lines.push('');
699
+ lines.push('Apply this CSS to your app to fix the identified design issues:');
700
+ lines.push('```html');
701
+ lines.push('<link rel="stylesheet" href="design-fixes.css">');
702
+ lines.push('```');
703
+ }
704
+ return lines.join('\n');
705
+ }
706
+ // ---------------------------------------------------------------------------
707
+ // Agent-dispatched evolve — sends findings to a coding agent that edits source
708
+ // ---------------------------------------------------------------------------
709
+ const AGENT_COMMANDS = {
710
+ 'claude-code': (prompt, dir) => ['claude', '-p', prompt, '--dangerously-skip-permissions', '--add-dir', dir],
711
+ 'codex': (prompt, dir) => ['codex', 'exec', prompt, '-c', `cwd="${dir}"`],
712
+ 'opencode': (prompt, dir) => ['opencode', 'run', prompt],
713
+ };
714
+ function resolveAgentCommand(agent, prompt, projectDir) {
715
+ const builder = AGENT_COMMANDS[agent];
716
+ if (builder) {
717
+ const [cmd, ...args] = builder(prompt, projectDir);
718
+ return { cmd, args, cwd: projectDir };
719
+ }
720
+ // Custom command — treat the agent string as a command template
721
+ // e.g. "aider --message" becomes: aider --message "<prompt>"
722
+ const parts = agent.split(/\s+/);
723
+ return { cmd: parts[0], args: [...parts.slice(1), prompt], cwd: projectDir };
724
+ }
725
+ function buildAgentFixPrompt(results, profile, round) {
726
+ const allFindings = results.flatMap(r => r.findings);
727
+ const critical = allFindings.filter(f => f.severity === 'critical');
728
+ const major = allFindings.filter(f => f.severity === 'major');
729
+ const minor = allFindings.filter(f => f.severity === 'minor');
730
+ const findingsList = [...critical, ...major, ...minor.slice(0, 5)]
731
+ .map((f, i) => {
732
+ let entry = `${i + 1}. [${f.severity}/${f.category}] ${f.description}`;
733
+ entry += `\n Location: ${f.location}`;
734
+ entry += `\n Suggestion: ${f.suggestion}`;
735
+ if (f.cssSelector)
736
+ entry += `\n CSS Selector: ${f.cssSelector}`;
737
+ if (f.cssFix)
738
+ entry += `\n CSS Fix: ${f.cssFix}`;
739
+ return entry;
740
+ })
741
+ .join('\n\n');
742
+ const scoreBreakdowns = results
743
+ .filter(r => r.designSystemScore)
744
+ .map(r => {
745
+ const ds = r.designSystemScore;
746
+ return ` ${r.url}: ${Object.entries(ds).map(([k, v]) => `${k}=${v}`).join(', ')}`;
747
+ })
748
+ .join('\n');
749
+ return `You are fixing design issues found by an automated design audit.
750
+
751
+ AUDIT PROFILE: ${profile}
752
+ ROUND: ${round} (${round === 1 ? 'initial fixes' : 'fixing remaining issues from previous round'})
753
+ CURRENT SCORES:
754
+ Overall: ${(results.reduce((s, r) => s + r.score, 0) / results.length).toFixed(1)}/10
755
+ ${scoreBreakdowns}
756
+
757
+ FINDINGS TO FIX (${critical.length} critical, ${major.length} major, ${minor.length} minor):
758
+
759
+ ${findingsList}
760
+
761
+ INSTRUCTIONS:
762
+ 1. Read the project's source files to understand the styling approach (Tailwind, CSS modules, plain CSS, styled-components, etc.)
763
+ 2. Fix the findings by editing the ACTUAL SOURCE FILES — not by creating new CSS override files
764
+ 3. Match the project's existing styling conventions
765
+ 4. Fix the design SYSTEM (shared components, tokens, globals) not individual instances
766
+ 5. Prioritize critical and major findings
767
+ 6. Only change visual/styling properties — never change business logic, state, or event handlers
768
+ 7. After making changes, verify the dev server is still running (no build errors)
769
+
770
+ Do NOT:
771
+ - Create new standalone CSS override files — edit the existing styles
772
+ - Add comments explaining what you changed — just change it
773
+ - Refactor unrelated code
774
+ - Change component structure or HTML semantics unless a finding specifically requires it`;
775
+ }
776
+ async function runAgentEvolveLoop(brain, driver, page, pages, profile, initialResults, outputDir, maxRounds, agentName, projectDir, debug) {
777
+ const initialAvg = initialResults.reduce((s, r) => s + r.score, 0) / initialResults.length;
778
+ const scoreHistory = [initialAvg];
779
+ const appliedFixes = [];
780
+ let currentResults = initialResults;
781
+ let currentAvg = initialAvg;
782
+ const resolvedProjectDir = path.resolve(projectDir);
783
+ if (!fs.existsSync(resolvedProjectDir)) {
784
+ cliError(`project directory not found: ${resolvedProjectDir}`);
785
+ process.exit(1);
786
+ }
787
+ console.log('');
788
+ console.log(` ${chalk.bold('Design Evolve')} ${chalk.dim('via')} ${chalk.cyan(agentName)}`);
789
+ console.log(` ${chalk.dim('Project:')} ${resolvedProjectDir}`);
790
+ console.log(` ${chalk.dim('Initial score:')} ${currentAvg.toFixed(1)}/10`);
791
+ console.log(` ${chalk.dim('Max rounds:')} ${maxRounds}`);
792
+ console.log('');
793
+ for (let round = 1; round <= maxRounds; round++) {
794
+ console.log(` ${chalk.dim(`Round ${round}/${maxRounds}`)}`);
795
+ // Build the prompt for the agent
796
+ const prompt = buildAgentFixPrompt(currentResults, profile, round);
797
+ // Write the prompt to a file for debugging
798
+ const promptPath = path.join(outputDir, `agent-prompt-round-${round}.txt`);
799
+ fs.writeFileSync(promptPath, prompt);
800
+ // Also write the full report JSON so the agent could read it
801
+ const findingsPath = path.join(outputDir, `findings-round-${round}.json`);
802
+ fs.writeFileSync(findingsPath, JSON.stringify({
803
+ round,
804
+ score: currentAvg,
805
+ results: currentResults.map(r => ({
806
+ url: r.url,
807
+ score: r.score,
808
+ designSystemScore: r.designSystemScore,
809
+ findings: r.findings,
810
+ })),
811
+ }, null, 2));
812
+ // Dispatch to the coding agent
813
+ const { cmd, args, cwd } = resolveAgentCommand(agentName, prompt, resolvedProjectDir);
814
+ console.log(` ${chalk.dim(` Dispatching to ${agentName}…`)}`);
815
+ if (debug) {
816
+ console.log(` ${chalk.dim(` cmd: ${cmd} ${args.map(a => a.length > 80 ? a.slice(0, 80) + '…' : a).join(' ')}`)}`);
817
+ }
818
+ try {
819
+ const result = execSync(`${cmd} ${args.map(a => JSON.stringify(a)).join(' ')}`, {
820
+ cwd,
821
+ stdio: debug ? 'inherit' : 'pipe',
822
+ timeout: 300_000, // 5min max per agent round
823
+ env: { ...process.env },
824
+ });
825
+ if (!debug && result) {
826
+ const agentOutputPath = path.join(outputDir, `agent-output-round-${round}.txt`);
827
+ fs.writeFileSync(agentOutputPath, result.toString());
828
+ }
829
+ console.log(` ${chalk.dim(' Agent completed')}`);
830
+ }
831
+ catch (err) {
832
+ const exitCode = err.status ?? 'unknown';
833
+ console.log(` ${chalk.yellow(` Agent exited with code ${exitCode} — continuing with re-audit`)}`);
834
+ // Write stderr if available
835
+ const stderr = err.stderr;
836
+ if (stderr) {
837
+ const errPath = path.join(outputDir, `agent-error-round-${round}.txt`);
838
+ fs.writeFileSync(errPath, stderr.toString());
839
+ }
840
+ }
841
+ // Wait for hot reload to settle
842
+ console.log(` ${chalk.dim(' Waiting for hot reload…')}`);
843
+ await new Promise(resolve => setTimeout(resolve, 5000));
844
+ // Re-audit
845
+ console.log(` ${chalk.dim(' Re-auditing…')}`);
846
+ const roundResults = [];
847
+ const roundScreenshotDir = path.join(outputDir, `screenshots-round-${round}`);
848
+ fs.mkdirSync(roundScreenshotDir, { recursive: true });
849
+ for (const url of pages) {
850
+ const result = await auditSinglePage(brain, driver, page, url, profile, roundScreenshotDir);
851
+ roundResults.push(result);
852
+ }
853
+ const roundAvg = roundResults.reduce((s, r) => s + r.score, 0) / roundResults.length;
854
+ scoreHistory.push(roundAvg);
855
+ const delta = roundAvg - currentAvg;
856
+ const deltaStr = delta >= 0 ? chalk.green(`+${delta.toFixed(1)}`) : chalk.red(delta.toFixed(1));
857
+ console.log(` ${chalk.dim(' Score:')} ${roundAvg.toFixed(1)}/10 (${deltaStr})`);
858
+ // Track what changed
859
+ const prevFindingCount = currentResults.flatMap(r => r.findings).length;
860
+ const newFindingCount = roundResults.flatMap(r => r.findings).length;
861
+ const resolvedCount = Math.max(0, prevFindingCount - newFindingCount);
862
+ if (resolvedCount > 0) {
863
+ appliedFixes.push({
864
+ cssSelector: `round-${round}`,
865
+ cssFix: `${agentName} resolved ${resolvedCount} findings`,
866
+ finding: `Score: ${currentAvg.toFixed(1)} → ${roundAvg.toFixed(1)}`,
867
+ });
868
+ }
869
+ currentResults = roundResults;
870
+ currentAvg = roundAvg;
871
+ // Check convergence
872
+ if (delta <= 0.1 && round > 1) {
873
+ console.log(` ${chalk.dim(' Converged — no further improvement')}`);
874
+ break;
875
+ }
876
+ }
877
+ const totalDelta = currentAvg - initialAvg;
878
+ const deltaColor = totalDelta >= 2 ? chalk.green : totalDelta > 0 ? chalk.yellow : chalk.red;
879
+ console.log('');
880
+ console.log(` ${chalk.bold('Evolve complete')} ${chalk.dim('via')} ${chalk.cyan(agentName)}`);
881
+ console.log(` ${chalk.dim('Score:')} ${initialAvg.toFixed(1)} → ${currentAvg.toFixed(1)} (${deltaColor(totalDelta >= 0 ? `+${totalDelta.toFixed(1)}` : totalDelta.toFixed(1))})`);
882
+ console.log(` ${chalk.dim('Rounds:')} ${scoreHistory.length - 1}`);
883
+ console.log('');
884
+ return {
885
+ beforeScore: initialAvg,
886
+ afterScore: currentAvg,
887
+ delta: totalDelta,
888
+ rounds: scoreHistory.length - 1,
889
+ appliedFixes,
890
+ skippedFixes: [],
891
+ scoreHistory,
892
+ cssOverride: '', // no CSS override in agent mode — agent edited source directly
893
+ };
894
+ }
325
895
  // Direct page audit using brain.generate with custom prompt
326
896
  async function auditSinglePage(brain, driver, page, url, profile, screenshotDir) {
327
897
  try {
@@ -347,6 +917,7 @@ async function auditSinglePage(brain, driver, page, url, profile, screenshotDir)
347
917
  const result = await brain.auditDesign(state, `Audit the design quality of this page: ${url}`, [], buildAuditPrompt(profile));
348
918
  let summary = '';
349
919
  let strengths = [];
920
+ let designSystemScore;
350
921
  try {
351
922
  let text = result.raw.trim();
352
923
  if (text.startsWith('```'))
@@ -354,6 +925,13 @@ async function auditSinglePage(brain, driver, page, url, profile, screenshotDir)
354
925
  const parsed = JSON.parse(text);
355
926
  summary = parsed.summary || '';
356
927
  strengths = Array.isArray(parsed.strengths) ? parsed.strengths : [];
928
+ if (parsed.designSystemScore && typeof parsed.designSystemScore === 'object') {
929
+ designSystemScore = {};
930
+ for (const [k, v] of Object.entries(parsed.designSystemScore)) {
931
+ if (typeof v === 'number')
932
+ designSystemScore[k] = v;
933
+ }
934
+ }
357
935
  }
358
936
  catch { /* use defaults */ }
359
937
  return {
@@ -364,6 +942,7 @@ async function auditSinglePage(brain, driver, page, url, profile, screenshotDir)
364
942
  findings: result.findings,
365
943
  screenshotPath,
366
944
  tokensUsed: result.tokensUsed,
945
+ designSystemScore,
367
946
  };
368
947
  }
369
948
  catch (err) {