chati-dev 4.2.2 → 4.3.0

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 (220) hide show
  1. package/README.md +80 -53
  2. package/bin/chati.js +150 -5
  3. package/framework/agents/build/dev.md +509 -76
  4. package/framework/agents/deploy/devops.md +40 -25
  5. package/framework/agents/discover/brief.md +156 -22
  6. package/framework/agents/discover/brownfield-wu.md +24 -14
  7. package/framework/agents/discover/greenfield-wu.md +100 -14
  8. package/framework/agents/plan/architect-data-engineer.md +6 -6
  9. package/framework/agents/plan/architect-system.md +46 -12
  10. package/framework/agents/plan/architect.md +40 -20
  11. package/framework/agents/plan/detail.md +36 -24
  12. package/framework/agents/plan/phases.md +36 -26
  13. package/framework/agents/plan/tasks.md +114 -33
  14. package/framework/agents/plan/ux-brand-architect.md +240 -8
  15. package/framework/agents/plan/ux-component-engineer.md +28 -13
  16. package/framework/agents/plan/ux-researcher.md +7 -6
  17. package/framework/agents/plan/ux.md +55 -22
  18. package/framework/agents/quality/qa-implementation.md +143 -74
  19. package/framework/agents/quality/qa-planning.md +115 -42
  20. package/framework/agents/quality/qa-visual.md +439 -0
  21. package/framework/agents/shared/visualizer.md +128 -0
  22. package/framework/config.yaml +7 -6
  23. package/framework/constitution.md +127 -44
  24. package/framework/context/governance.md +12 -7
  25. package/framework/context/quality.md +6 -5
  26. package/framework/context/root.md +6 -6
  27. package/framework/data/entity-registry.yaml +377 -4
  28. package/framework/data/model-limits.json +19 -0
  29. package/framework/domains/agents/qa-visual.yaml +74 -0
  30. package/framework/domains/constitution.yaml +46 -2
  31. package/framework/domains/workflows/greenfield-fullstack.yaml +2 -2
  32. package/framework/hooks/advance-trigger.js +131 -0
  33. package/framework/hooks/brief-validator.js +83 -0
  34. package/framework/hooks/constitution-guard.js +24 -5
  35. package/framework/hooks/license-guard.js +62 -27
  36. package/framework/hooks/mode-governance.js +13 -2
  37. package/framework/hooks/model-governance.js +1 -0
  38. package/framework/hooks/post-dev.js +207 -0
  39. package/framework/hooks/prism-engine.js +274 -105
  40. package/framework/hooks/reasoning-escalator.js +371 -0
  41. package/framework/hooks/reference-trigger.js +117 -0
  42. package/framework/hooks/session-digest.js +50 -1
  43. package/framework/hooks/settings.json +32 -1
  44. package/framework/hooks/style-guard.js +25 -6
  45. package/framework/hooks/team-quality-gate.js +19 -12
  46. package/framework/hooks/undercover-guard.js +4 -2
  47. package/framework/i18n/en.yaml +3 -3
  48. package/framework/i18n/es.yaml +3 -3
  49. package/framework/i18n/fr.yaml +3 -3
  50. package/framework/i18n/pt.yaml +3 -3
  51. package/framework/intelligence/context-engine.md +4 -5
  52. package/framework/intelligence/decision-engine.md +1 -1
  53. package/framework/intelligence/hooks-performance.md +3 -3
  54. package/framework/migrations/v1.0-to-v1.1.yaml +1 -1
  55. package/framework/migrations/v1.4-to-v2.0.yaml +11 -11
  56. package/framework/migrations/v4.0-to-v4.1.yaml +2 -2
  57. package/framework/migrations/v4.2-to-v4.3.yaml +29 -0
  58. package/framework/orchestrator/chati-router.js +387 -0
  59. package/framework/orchestrator/chati-update.md +40 -40
  60. package/framework/orchestrator/chati.md +294 -94
  61. package/framework/scaffold/motion-premium/README.md +89 -0
  62. package/framework/scaffold/motion-premium/app/globals.css.template +400 -0
  63. package/framework/scaffold/motion-premium/app/layout.tsx.template +110 -0
  64. package/framework/scaffold/motion-premium/components/animation/BackgroundCrossfadeProvider.tsx.template +170 -0
  65. package/framework/scaffold/motion-premium/components/animation/LenisProvider.tsx.template +49 -0
  66. package/framework/scaffold/motion-premium/components/animation/PageTransitionWrapper.tsx.template +83 -0
  67. package/framework/scaffold/motion-premium/components/animation/Preloader.tsx.template +171 -0
  68. package/framework/scaffold/motion-premium/components/ui/Container.tsx.template +69 -0
  69. package/framework/scaffold/motion-premium/components/ui/PageSection.tsx.template +74 -0
  70. package/framework/scaffold/motion-premium/lib/animations/gsap.ts.template +112 -0
  71. package/framework/scaffold/motion-premium/lib/animations/refreshCoordinator.ts.template +75 -0
  72. package/framework/scaffold/motion-premium/lib/animations/tokens.ts.template +119 -0
  73. package/framework/scaffold/motion-premium/lib/animations/useGsapContext.ts.template +92 -0
  74. package/framework/scaffold/motion-premium/lib/animations/useScrollSnapStepper.ts.template +265 -0
  75. package/framework/scaffold/motion-premium/lib/animations/useSmoothScroll.ts.template +67 -0
  76. package/framework/scaffold/motion-premium/lib/brand.ts.template +43 -0
  77. package/framework/scaffold/motion-premium/scaffold.yaml +174 -0
  78. package/framework/scaffold/motion-premium-3d/README.md +80 -0
  79. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoCanvas.tsx.template +81 -0
  80. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoClient.tsx.template +75 -0
  81. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/page.tsx.template +26 -0
  82. package/framework/scaffold/motion-premium-3d/components/3d/CameraRig.tsx.template +100 -0
  83. package/framework/scaffold/motion-premium-3d/components/3d/CanvasProvider.tsx.template +85 -0
  84. package/framework/scaffold/motion-premium-3d/components/3d/InvalidateOnScroll.tsx.template +51 -0
  85. package/framework/scaffold/motion-premium-3d/components/3d/MeshCrossfade.tsx.template +79 -0
  86. package/framework/scaffold/motion-premium-3d/components/3d/ScrollCrossfade.tsx.template +88 -0
  87. package/framework/scaffold/motion-premium-3d/components/3d/ScrollScene.tsx.template +121 -0
  88. package/framework/scaffold/motion-premium-3d/components/webgl/SceneFallback.tsx.template +65 -0
  89. package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +68 -0
  90. package/framework/scaffold/motion-premium-3d/lib/webgl/detect.ts.template +69 -0
  91. package/framework/scaffold/motion-premium-3d/scaffold.yaml +133 -0
  92. package/framework/schemas/session.schema.json +109 -21
  93. package/framework/scripts/reference-capture.js +430 -0
  94. package/framework/scripts/visual-qa.js +674 -0
  95. package/framework/tasks/orchestrator-handoff.md +1 -1
  96. package/framework/tasks/orchestrator-resume.md +1 -1
  97. package/framework/tasks/orchestrator-route.md +1 -1
  98. package/framework/tasks/orchestrator-status.md +3 -3
  99. package/framework/tasks/qa-planning-gate-define.md +1 -1
  100. package/framework/templates/brandbook-html-tmpl.md +1 -1
  101. package/framework/templates/brandbook-tmpl.yaml +1 -1
  102. package/framework/templates/component-spec-tmpl.yaml +1 -1
  103. package/framework/templates/design-token-tmpl.yaml +1 -1
  104. package/framework/templates/icon-system-tmpl.yaml +1 -1
  105. package/framework/templates/team-planning-tasks.yaml +6 -5
  106. package/framework/workflows/brownfield-discovery.yaml +2 -2
  107. package/framework/workflows/brownfield-fullstack.yaml +15 -11
  108. package/framework/workflows/brownfield-service.yaml +14 -10
  109. package/framework/workflows/brownfield-ui.yaml +15 -11
  110. package/framework/workflows/greenfield-fullstack.yaml +16 -13
  111. package/framework/workflows/quick-flow.yaml +3 -3
  112. package/framework/workflows/standard-flow.yaml +12 -9
  113. package/package.json +10 -5
  114. package/src/autonomy/autonomous-gate.js +1 -0
  115. package/src/autonomy/build-state.js +1 -2
  116. package/src/autonomy/progress-reporter.js +1 -1
  117. package/src/config/agent-customizer.js +11 -3
  118. package/src/config/claude-settings-generator.js +27 -7
  119. package/src/config/context-file-generator.js +41 -21
  120. package/src/config/framework-adapter.js +1 -0
  121. package/src/config/gemini-hooks-generator.js +19 -7
  122. package/src/config/mcp-configs.js +1 -0
  123. package/src/context/layers/l1-global.js +2 -1
  124. package/src/dashboard/data-reader.js +4 -3
  125. package/src/dashboard/layout.js +2 -1
  126. package/src/decision/analyzer.js +6 -30
  127. package/src/decision/engine.js +4 -28
  128. package/src/decision/registry-healer.js +3 -2
  129. package/src/decision/registry-updater.js +23 -14
  130. package/src/extensions/loader.js +2 -8
  131. package/src/gates/g1-planning-complete.js +2 -1
  132. package/src/gates/g2-qa-planning.js +2 -1
  133. package/src/gates/g3-implementation.js +2 -1
  134. package/src/gates/g4-qa-implementation.js +3 -2
  135. package/src/gates/g5-deploy-ready.js +2 -1
  136. package/src/health/engine.js +4 -3
  137. package/src/installer/core.js +422 -81
  138. package/src/installer/preflight.js +131 -0
  139. package/src/installer/provider-overlay.js +3 -3
  140. package/src/installer/scaffold-applier.js +358 -0
  141. package/src/installer/templates.js +46 -29
  142. package/src/installer/validator.js +17 -12
  143. package/src/intelligence/registry-manager.js +22 -29
  144. package/src/intelligence/timeline.js +11 -6
  145. package/src/license/commands.js +1 -1
  146. package/src/license/wait.js +102 -0
  147. package/src/memory/agent-memory.js +81 -0
  148. package/src/memory/dream.js +32 -1
  149. package/src/merger/replace-merger.js +28 -15
  150. package/src/orchestrator/agent-selector.js +2 -1
  151. package/src/orchestrator/cli.js +1869 -71
  152. package/src/orchestrator/doctor.js +270 -0
  153. package/src/orchestrator/handoff-engine.js +4 -3
  154. package/src/orchestrator/index.js +2 -0
  155. package/src/orchestrator/pipeline-manager.js +306 -15
  156. package/src/orchestrator/session-manager.js +331 -6
  157. package/src/tasks/handoff.js +3 -2
  158. package/src/telemetry/config.js +4 -3
  159. package/src/telemetry/schema.js +1 -0
  160. package/src/terminal/collector.js +3 -2
  161. package/src/terminal/index.js +1 -2
  162. package/src/terminal/isolation.js +52 -18
  163. package/src/terminal/prompt-builder.js +42 -25
  164. package/src/terminal/run-parallel.js +1 -1
  165. package/src/terminal/run-team.js +3 -3
  166. package/src/terminal/team-task-list.js +43 -4
  167. package/src/upgrade/backup.js +3 -2
  168. package/src/upgrade/checker.js +3 -2
  169. package/src/upgrade/migrator.js +65 -7
  170. package/src/upgrade/tracked-files-detector.js +86 -0
  171. package/src/upgrade/user-messages.js +94 -0
  172. package/src/utils/config-parser.js +2 -1
  173. package/src/utils/feature-flags.js +2 -1
  174. package/src/utils/flatten-entities.js +69 -0
  175. package/src/utils/framework-dir.js +16 -0
  176. package/src/utils/model-id.js +85 -0
  177. package/src/utils/provider-limits.js +84 -23
  178. package/src/utils/schema-validator.js +1 -1
  179. package/src/wizard/i18n.js +5 -4
  180. package/src/wizard/index.js +14 -0
  181. package/assets/logo - c/303/263pia.png +0 -0
  182. package/assets/logo.svg +0 -42
  183. package/assets/logo2.png +0 -0
  184. package/assets/social-preview.png +0 -0
  185. package/scripts/bundle-framework.js +0 -69
  186. package/scripts/changelog-generator.js +0 -222
  187. package/scripts/codebase-mapper.js +0 -728
  188. package/scripts/commit-message-generator.js +0 -167
  189. package/scripts/coverage-analyzer.js +0 -260
  190. package/scripts/dependency-analyzer.js +0 -280
  191. package/scripts/doctor/checks/agents.js +0 -77
  192. package/scripts/doctor/checks/constitution.js +0 -41
  193. package/scripts/doctor/checks/domain-alignment.js +0 -58
  194. package/scripts/doctor/checks/prism-layers.js +0 -84
  195. package/scripts/doctor/checks/registry.js +0 -55
  196. package/scripts/doctor/checks/schemas.js +0 -61
  197. package/scripts/doctor/fixes/reference-fix.js +0 -100
  198. package/scripts/doctor/fixes/registry-fix.js +0 -56
  199. package/scripts/doctor/index.js +0 -212
  200. package/scripts/framework-analyzer.js +0 -308
  201. package/scripts/generate-constitution-domain.js +0 -253
  202. package/scripts/generate-signing-key.js +0 -33
  203. package/scripts/health-check.js +0 -481
  204. package/scripts/ide-sync.js +0 -326
  205. package/scripts/performance-analyzer.js +0 -325
  206. package/scripts/plan-tracker.js +0 -278
  207. package/scripts/populate-entity-registry.js +0 -481
  208. package/scripts/pr-review.js +0 -317
  209. package/scripts/rollback-manager.js +0 -310
  210. package/scripts/semantic-lint.js +0 -328
  211. package/scripts/sign-manifest.js +0 -53
  212. package/scripts/stuck-detector.js +0 -343
  213. package/scripts/test-quality-assessment.js +0 -257
  214. package/scripts/validate-agents.js +0 -368
  215. package/scripts/validate-package.js +0 -505
  216. package/scripts/validate-tasks.js +0 -465
  217. package/src/autonomy/worktree-manager.js +0 -250
  218. package/src/intelligence/decision-engine.js +0 -256
  219. package/src/intelligence/document-sharder.js +0 -221
  220. package/src/intelligence/elicitation.js +0 -265
@@ -0,0 +1,430 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * reference-capture.js — Visual Reference Capture for UX Planning
4
+ *
5
+ * Uses Playwright to capture screenshots + extract real CSS tokens from
6
+ * reference websites. Outputs data the UX Brand Architect uses to base
7
+ * visual direction proposals on REAL data, not AI guesses.
8
+ *
9
+ * Usage:
10
+ * node reference-capture.js --urls "https://oryzo.ai" "https://landonorris.com" --output artifacts/4-UX/references/
11
+ *
12
+ * Per-site output:
13
+ * <output>/<slug>/
14
+ * ├── full-page.png
15
+ * ├── scroll-{0,25,50,75,100}.png
16
+ * ├── hover-{0..4}.png
17
+ * ├── mobile.png, tablet.png
18
+ * └── extracted-tokens.json
19
+ */
20
+
21
+ import { mkdirSync, writeFileSync, readdirSync, renameSync, existsSync } from 'fs';
22
+ import { join } from 'path';
23
+ import { execSync } from 'child_process';
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Args
27
+ // ---------------------------------------------------------------------------
28
+ const args = process.argv.slice(2);
29
+ let urls = [];
30
+ let output = 'artifacts/4-UX/references';
31
+ const TIMEOUT_PER_SITE = 45000;
32
+
33
+ for (let i = 0; i < args.length; i++) {
34
+ if (args[i] === '--urls') {
35
+ while (i + 1 < args.length && !args[i + 1].startsWith('--')) {
36
+ urls.push(args[++i]);
37
+ }
38
+ } else if (args[i] === '--output') output = args[++i];
39
+ else if (args[i] === '--help') {
40
+ console.log(`Usage: node reference-capture.js [options]
41
+ --urls "https://site1.com" "https://site2.com" Reference URLs to analyze
42
+ --output artifacts/4-UX/references/ Output directory (default)
43
+ --help Show this help`);
44
+ process.exit(0);
45
+ }
46
+ }
47
+
48
+ if (urls.length === 0) {
49
+ console.error('Error: No URLs provided. Use --urls "https://example.com"');
50
+ process.exit(1);
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // URL to slug
55
+ // ---------------------------------------------------------------------------
56
+ function urlToSlug(url) {
57
+ try {
58
+ const u = new URL(url);
59
+ return u.hostname.replace(/\./g, '-').replace(/^www-/, '');
60
+ } catch {
61
+ return url.replace(/[^a-z0-9]/gi, '-').slice(0, 40);
62
+ }
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // CSS Token Extraction (runs inside the browser via page.evaluate)
67
+ // ---------------------------------------------------------------------------
68
+ const extractTokensScript = () => {
69
+ const tokens = {
70
+ colors: { backgrounds: {}, text: {}, all: {} },
71
+ typography: { families: {}, sizes: {}, weights: {} },
72
+ spacing: { sectionGaps: [], contentMaxWidth: null },
73
+ animations: {
74
+ gsap_loaded: typeof gsap !== 'undefined',
75
+ lenis_active: !!window.lenis,
76
+ scroll_trigger_count: 0,
77
+ transition_durations: {},
78
+ },
79
+ layout: { breakpoints: [], gridColumns: null },
80
+ };
81
+
82
+ // Helper: normalize color to hex
83
+ function rgbToHex(rgb) {
84
+ if (!rgb || rgb === 'transparent' || rgb === 'rgba(0, 0, 0, 0)') return null;
85
+ const match = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
86
+ if (!match) return rgb;
87
+ return '#' + [match[1], match[2], match[3]].map(n => parseInt(n).toString(16).padStart(2, '0')).join('');
88
+ }
89
+
90
+ // Sample up to 500 visible elements for style data
91
+ const elements = document.querySelectorAll('body *');
92
+ const sample = Array.from(elements).slice(0, 500);
93
+
94
+ for (const el of sample) {
95
+ const style = getComputedStyle(el);
96
+ const rect = el.getBoundingClientRect();
97
+ if (rect.width === 0 && rect.height === 0) continue; // skip invisible
98
+
99
+ // Colors
100
+ const bg = rgbToHex(style.backgroundColor);
101
+ if (bg) tokens.colors.backgrounds[bg] = (tokens.colors.backgrounds[bg] || 0) + 1;
102
+ const color = rgbToHex(style.color);
103
+ if (color) tokens.colors.text[color] = (tokens.colors.text[color] || 0) + 1;
104
+ if (bg) tokens.colors.all[bg] = (tokens.colors.all[bg] || 0) + 1;
105
+ if (color) tokens.colors.all[color] = (tokens.colors.all[color] || 0) + 1;
106
+
107
+ // Typography (text elements only)
108
+ if (el.textContent.trim().length > 0 && ['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'SPAN', 'A', 'LI', 'TD', 'TH', 'LABEL', 'BUTTON'].includes(el.tagName)) {
109
+ const family = style.fontFamily.split(',')[0].trim().replace(/['"]/g, '');
110
+ tokens.typography.families[family] = (tokens.typography.families[family] || 0) + 1;
111
+ tokens.typography.sizes[style.fontSize] = (tokens.typography.sizes[style.fontSize] || 0) + 1;
112
+ tokens.typography.weights[style.fontWeight] = (tokens.typography.weights[style.fontWeight] || 0) + 1;
113
+ }
114
+
115
+ // Transition durations
116
+ if (style.transitionDuration && style.transitionDuration !== '0s') {
117
+ tokens.animations.transition_durations[style.transitionDuration] =
118
+ (tokens.animations.transition_durations[style.transitionDuration] || 0) + 1;
119
+ }
120
+ }
121
+
122
+ // Spacing: section-level gaps
123
+ const sections = document.querySelectorAll('section, [class*="section"], main > div');
124
+ for (const sec of Array.from(sections).slice(0, 20)) {
125
+ const style = getComputedStyle(sec);
126
+ const gap = parseInt(style.paddingTop) + parseInt(style.paddingBottom);
127
+ if (gap > 20) tokens.spacing.sectionGaps.push(`${gap}px`);
128
+ }
129
+ tokens.spacing.sectionGaps = [...new Set(tokens.spacing.sectionGaps)].sort((a, b) => parseInt(a) - parseInt(b));
130
+
131
+ // Content max-width
132
+ const main = document.querySelector('main') || document.querySelector('[class*="container"]');
133
+ if (main) tokens.spacing.contentMaxWidth = getComputedStyle(main).maxWidth;
134
+
135
+ // ScrollTrigger count
136
+ try {
137
+ if (typeof ScrollTrigger !== 'undefined') {
138
+ tokens.animations.scroll_trigger_count = ScrollTrigger.getAll().length;
139
+ }
140
+ } catch { /* not available */ }
141
+
142
+ // CSS custom properties count
143
+ const rootStyles = getComputedStyle(document.documentElement);
144
+ let customPropCount = 0;
145
+ for (let i = 0; i < rootStyles.length; i++) {
146
+ if (rootStyles[i].startsWith('--')) customPropCount++;
147
+ }
148
+ tokens.layout.customProperties = customPropCount;
149
+
150
+ // Sort colors/typography by frequency, keep top entries
151
+ function topN(obj, n = 10) {
152
+ return Object.entries(obj)
153
+ .sort((a, b) => b[1] - a[1])
154
+ .slice(0, n)
155
+ .map(([value, count]) => ({ value, count }));
156
+ }
157
+
158
+ return {
159
+ colors: {
160
+ backgrounds: topN(tokens.colors.backgrounds),
161
+ text: topN(tokens.colors.text),
162
+ all: topN(tokens.colors.all, 15),
163
+ },
164
+ typography: {
165
+ families: topN(tokens.typography.families, 5),
166
+ sizes: topN(tokens.typography.sizes, 10),
167
+ weights: topN(tokens.typography.weights, 5),
168
+ },
169
+ spacing: tokens.spacing,
170
+ animations: {
171
+ gsap_loaded: tokens.animations.gsap_loaded,
172
+ lenis_active: tokens.animations.lenis_active,
173
+ scroll_trigger_count: tokens.animations.scroll_trigger_count,
174
+ transition_durations: topN(tokens.animations.transition_durations, 5),
175
+ },
176
+ layout: {
177
+ customProperties: tokens.layout.customProperties,
178
+ },
179
+ };
180
+ };
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // Capture single site
184
+ // ---------------------------------------------------------------------------
185
+ async function captureSite(browser, url, siteOutput) {
186
+ mkdirSync(siteOutput, { recursive: true });
187
+ const exec = execSync;
188
+
189
+ const consoleErrors = [];
190
+ const VIEWPORTS = [
191
+ { name: 'desktop', width: 1280, height: 800 },
192
+ { name: 'tablet', width: 768, height: 1024 },
193
+ { name: 'mobile', width: 375, height: 812 },
194
+ ];
195
+
196
+ // -------------------------------------------------------------------------
197
+ // Phase 1: Scroll videos — 3 viewports IN PARALLEL
198
+ // Each viewport: 5s load + up to 120s scroll = max 125s video
199
+ // -------------------------------------------------------------------------
200
+ console.log(` Recording 3 viewports in parallel...`);
201
+
202
+ const scrollResults = await Promise.all(VIEWPORTS.map(async (vp) => {
203
+ const vpDir = join(siteOutput, vp.name);
204
+ mkdirSync(vpDir, { recursive: true });
205
+ const framesDir = join(vpDir, 'frames');
206
+ mkdirSync(framesDir, { recursive: true });
207
+
208
+ let ctx, page, scrollHeight = 0, maxScroll = 0, durationS = 0, frameCount = 0;
209
+
210
+ // Step 1: Open browser + navigate + scroll (video records automatically)
211
+ try {
212
+ ctx = await browser.newContext({
213
+ viewport: { width: vp.width, height: vp.height },
214
+ recordVideo: { dir: vpDir, size: { width: vp.width, height: vp.height } },
215
+ });
216
+ page = await ctx.newPage();
217
+ if (vp.name === 'desktop') {
218
+ page.on('console', msg => { if (msg.type() === 'error') consoleErrors.push(msg.text()); });
219
+ page.on('pageerror', err => consoleErrors.push(err.message));
220
+ }
221
+
222
+ await page.goto(url, { waitUntil: 'networkidle', timeout: TIMEOUT_PER_SITE });
223
+ await page.waitForTimeout(5000); // 5s load — user looks at hero
224
+
225
+ scrollHeight = await page.evaluate(() => document.documentElement.scrollHeight);
226
+ maxScroll = Math.max(0, scrollHeight - vp.height);
227
+
228
+ const MAX_SCROLL_MS = 120_000;
229
+ const PREFERRED_SPEED = 30;
230
+ const durationMs = Math.min(MAX_SCROLL_MS, Math.round((maxScroll / PREFERRED_SPEED) * 1000));
231
+ durationS = Math.round(durationMs / 1000);
232
+
233
+ // Time-based smooth scroll
234
+ const scrollStart = Date.now();
235
+ while (Date.now() - scrollStart < durationMs) {
236
+ const elapsed = Date.now() - scrollStart;
237
+ const targetY = Math.floor(maxScroll * elapsed / durationMs);
238
+ await page.evaluate(y => window.scrollTo(0, y), targetY);
239
+ await page.waitForTimeout(16);
240
+ }
241
+ await page.evaluate(y => window.scrollTo(0, y), maxScroll);
242
+ await page.waitForTimeout(1500);
243
+ } catch (scrollErr) {
244
+ // Scroll crashed — video is still partially recorded
245
+ console.log(` ${vp.name}: scroll error (${scrollErr.message?.slice(0, 60)}) — extracting from partial video`);
246
+ }
247
+
248
+ // Step 2: Close page/context to FINALIZE video file (even after crash)
249
+ try { if (page) await page.close(); } catch { /* already closed */ }
250
+ try { if (ctx) await ctx.close(); } catch { /* already closed */ }
251
+
252
+ // Step 3: Rename ANY .webm found (works even if scroll crashed)
253
+ const videoPath = join(vpDir, `scroll-${vp.name}.webm`);
254
+ try {
255
+ const vids = readdirSync(vpDir).filter(f => f.endsWith('.webm') && !f.startsWith('scroll-'));
256
+ if (vids.length > 0) renameSync(join(vpDir, vids[0]), videoPath);
257
+ } catch { /* ignore */ }
258
+
259
+ // Step 4: Extract frames from video (works on partial videos too)
260
+ try {
261
+ const videoToExtract = existsSync(videoPath) ? videoPath : readdirSync(vpDir).find(f => f.endsWith('.webm'));
262
+ if (videoToExtract) {
263
+ const fullVideoPath = existsSync(videoPath) ? videoPath : join(vpDir, videoToExtract);
264
+ exec(`ffmpeg -i "${fullVideoPath}" -vf "fps=0.5" "${join(framesDir, 'frame-%03d.png')}" -y`, { stdio: 'ignore', timeout: 60000 });
265
+ frameCount = readdirSync(framesDir).filter(f => f.endsWith('.png')).length;
266
+ }
267
+ } catch { /* ffmpeg not available or video corrupt */ }
268
+
269
+ return { viewport: vp.name, scrollHeight, maxScroll, durationS, frameCount };
270
+ }));
271
+
272
+ for (const r of scrollResults) {
273
+ console.log(` ${r.viewport}: ${r.maxScroll}px in ${r.durationS}s, ${r.frameCount} frames`);
274
+ }
275
+
276
+ // -------------------------------------------------------------------------
277
+ // Phase 2: Static captures (desktop only, no video recording)
278
+ // Full-page screenshot + hover states
279
+ // -------------------------------------------------------------------------
280
+ const ctx2 = await browser.newContext({ viewport: { width: 1280, height: 800 } });
281
+ const page2 = await ctx2.newPage();
282
+ await page2.goto(url, { waitUntil: 'networkidle', timeout: TIMEOUT_PER_SITE });
283
+ await page2.waitForTimeout(2000);
284
+
285
+ // Full-page screenshot
286
+ await page2.screenshot({ path: join(siteOutput, 'desktop', 'full-page.png'), fullPage: true });
287
+
288
+ // Scroll through once to trigger animations for token extraction
289
+ const scrollHeight = await page2.evaluate(() => document.documentElement.scrollHeight);
290
+ const maxScroll = Math.max(0, scrollHeight - 800);
291
+ const quickScrollStart = Date.now();
292
+ while (Date.now() - quickScrollStart < 10000) {
293
+ const elapsed = Date.now() - quickScrollStart;
294
+ await page2.evaluate(y => window.scrollTo(0, y), Math.floor(maxScroll * elapsed / 10000));
295
+ await page2.waitForTimeout(16);
296
+ }
297
+ await page2.evaluate(y => window.scrollTo(0, y), maxScroll);
298
+ await page2.waitForTimeout(1000);
299
+
300
+ // Hover states
301
+ await page2.evaluate(() => window.scrollTo(0, 0));
302
+ await page2.waitForTimeout(500);
303
+ const interactive = await page2.locator('button:visible, a[href]:visible, [role="button"]:visible').all();
304
+ for (let i = 0; i < Math.min(interactive.length, 5); i++) {
305
+ try {
306
+ await interactive[i].hover({ timeout: 3000 });
307
+ await page2.waitForTimeout(500);
308
+ await page2.screenshot({ path: join(siteOutput, 'desktop', `hover-${i}.png`) });
309
+ } catch { /* element may be obscured */ }
310
+ }
311
+
312
+ // Extract CSS tokens after full scroll
313
+ const tokens = await page2.evaluate(extractTokensScript);
314
+ tokens.url = url;
315
+ tokens.capturedAt = new Date().toISOString();
316
+ tokens.consoleErrors = consoleErrors;
317
+ tokens.scroll = {
318
+ height: scrollHeight,
319
+ viewports: scrollResults.map(r => ({ name: r.viewport, maxScroll: r.maxScroll, duration: r.durationS, frames: r.frameCount })),
320
+ };
321
+
322
+ writeFileSync(join(siteOutput, 'extracted-tokens.json'), JSON.stringify(tokens, null, 2));
323
+ await page2.close();
324
+ await ctx2.close();
325
+
326
+ return tokens;
327
+ }
328
+
329
+ // ---------------------------------------------------------------------------
330
+ // Main
331
+ // ---------------------------------------------------------------------------
332
+ async function main() {
333
+ mkdirSync(output, { recursive: true });
334
+
335
+ let chromium;
336
+ // Try multiple resolution paths for playwright:
337
+ // 1. Project node_modules (normal npm install)
338
+ // 2. Bundled _cli/node_modules (chati.dev self-contained install)
339
+ // 3. NODE_PATH (set by handleAdvance)
340
+ const pwCandidates = [
341
+ () => import('playwright'),
342
+ () => import(join('..', '_cli', 'node_modules', 'playwright', 'index.mjs')),
343
+ ];
344
+ for (const tryImport of pwCandidates) {
345
+ try {
346
+ const pw = await tryImport();
347
+ chromium = pw.chromium || pw.default?.chromium;
348
+ if (chromium) break;
349
+ } catch { /* try next */ }
350
+ }
351
+ if (!chromium) {
352
+ console.error('Error: Playwright not installed. Run: npx playwright install chromium');
353
+ process.exit(1);
354
+ }
355
+
356
+ const browser = await chromium.launch({ headless: true });
357
+ const results = [];
358
+
359
+ for (const url of urls) {
360
+ const slug = urlToSlug(url);
361
+ const siteOutput = join(output, slug);
362
+ console.log(`\nCapturing: ${url} → ${slug}/`);
363
+
364
+ try {
365
+ const tokens = await captureSite(browser, url, siteOutput);
366
+ console.log(` Colors: ${tokens.colors.all.length} unique`);
367
+ console.log(` Fonts: ${tokens.typography.families.map(f => f.value).join(', ')}`);
368
+ console.log(` GSAP: ${tokens.animations.gsap_loaded}, Lenis: ${tokens.animations.lenis_active}`);
369
+ console.log(` Screenshots: full-page + 5 scroll + 5 hover + tablet + mobile`);
370
+ results.push({ url, slug, status: 'ok', tokens });
371
+ } catch (err) {
372
+ console.error(` FAILED: ${err.message}`);
373
+ results.push({ url, slug, status: 'error', error: err.message });
374
+ }
375
+ }
376
+
377
+ await browser.close();
378
+
379
+ // Summary + completion marker
380
+ // capture-complete.json is the single file that Brand Architect polls for.
381
+ // Written ONLY when ALL sites finished (success or failure).
382
+ const siteResults = {};
383
+ for (const r of results) {
384
+ const framesDesktop = join(output, r.slug, 'desktop', 'frames');
385
+ const framesMobile = join(output, r.slug, 'mobile', 'frames');
386
+ const desktopCount = existsSync(framesDesktop) ? readdirSync(framesDesktop).filter(f => f.endsWith('.png')).length : 0;
387
+ const mobileCount = existsSync(framesMobile) ? readdirSync(framesMobile).filter(f => f.endsWith('.png')).length : 0;
388
+ siteResults[r.slug] = {
389
+ url: r.url,
390
+ status: r.status,
391
+ error: r.error || null,
392
+ frames: { desktop: desktopCount, mobile: mobileCount },
393
+ hasTokens: existsSync(join(output, r.slug, 'extracted-tokens.json')),
394
+ };
395
+ }
396
+
397
+ const captureComplete = {
398
+ completed: true,
399
+ completedAt: new Date().toISOString(),
400
+ totalSites: results.length,
401
+ totalCaptured: results.filter(r => r.status === 'ok').length,
402
+ totalFailed: results.filter(r => r.status === 'error').length,
403
+ sites: siteResults,
404
+ };
405
+
406
+ writeFileSync(join(output, 'capture-complete.json'), JSON.stringify(captureComplete, null, 2));
407
+ writeFileSync(join(output, 'capture-summary.json'), JSON.stringify(captureComplete, null, 2));
408
+ console.log(`\nSummary: ${captureComplete.totalCaptured} captured, ${captureComplete.totalFailed} failed`);
409
+ console.log(`Output: ${output}/`);
410
+
411
+ process.exit(captureComplete.totalFailed > 0 && captureComplete.totalCaptured === 0 ? 1 : 0);
412
+ }
413
+
414
+ main().catch(err => {
415
+ // Always write capture-complete.json even on crash — so wait-for-capture
416
+ // doesn't hang forever waiting for a file that will never appear.
417
+ try {
418
+ mkdirSync(output, { recursive: true });
419
+ writeFileSync(join(output, 'capture-complete.json'), JSON.stringify({
420
+ completed: true,
421
+ completedAt: new Date().toISOString(),
422
+ totalSites: 0,
423
+ totalCaptured: 0,
424
+ totalFailed: 1,
425
+ sites: {},
426
+ error: err.message?.slice(0, 200) || 'Unknown crash',
427
+ }, null, 2));
428
+ } catch { /* last resort — nothing we can do */ }
429
+ process.exit(1);
430
+ });