agentic-loop 3.1.6 → 3.2.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.
@@ -1,135 +0,0 @@
1
- # Browser Verify Skill
2
-
3
- Verify that a web page loads correctly using real browser automation (Playwright).
4
-
5
- ## Purpose
6
-
7
- This skill launches a real Chromium browser to verify pages work correctly. Unlike asking Claude to "imagine" what a page looks like, this actually:
8
-
9
- - Loads the page in a real browser
10
- - Detects JavaScript console errors
11
- - Detects failed network requests
12
- - Checks for error messages on the page ("Oops!", "Something went wrong")
13
- - Verifies required elements exist
14
- - Takes screenshots for evidence
15
- - Tests mobile viewport
16
-
17
- ## When to Use
18
-
19
- Use this skill for **frontend story verification**:
20
- - After implementing a UI feature
21
- - To verify a page loads without errors
22
- - To check that required elements render
23
- - To test mobile responsiveness
24
-
25
- ## Usage
26
-
27
- ```bash
28
- npx tsx ralph/browser-verify/verify.ts <url> [options]
29
- ```
30
-
31
- ### Options
32
-
33
- | Option | Description | Default |
34
- |--------|-------------|---------|
35
- | `--selectors '["#app", ".btn"]'` | Required elements (JSON array) | `[]` |
36
- | `--screenshot <path>` | Save screenshot to path | none |
37
- | `--timeout <ms>` | Page load timeout | 30000 |
38
- | `--headless` | Run without visible browser | true |
39
- | `--no-headless` | Show browser window | false |
40
- | `--check-console` | Fail on console errors | true |
41
- | `--no-check-console` | Ignore console errors | false |
42
- | `--mobile` | Use mobile viewport (375x667) | false |
43
- | `--viewport <W>x<H>` | Custom viewport size | 1280x720 |
44
-
45
- ### Examples
46
-
47
- **Basic page check:**
48
- ```bash
49
- npx tsx ralph/browser-verify/verify.ts http://localhost:3000/dashboard
50
- ```
51
-
52
- **Check specific elements exist:**
53
- ```bash
54
- npx tsx ralph/browser-verify/verify.ts http://localhost:3000/login \
55
- --selectors '["#email", "#password", "button[type=submit]"]'
56
- ```
57
-
58
- **Take screenshot and check mobile:**
59
- ```bash
60
- npx tsx ralph/browser-verify/verify.ts http://localhost:3000/dashboard \
61
- --screenshot .ralph/screenshots/dashboard.png \
62
- --mobile
63
- ```
64
-
65
- **Debug mode (visible browser):**
66
- ```bash
67
- npx tsx ralph/browser-verify/verify.ts http://localhost:3000/dashboard \
68
- --no-headless
69
- ```
70
-
71
- ## Output
72
-
73
- Returns JSON to stdout:
74
-
75
- ```json
76
- {
77
- "pass": true,
78
- "errors": [],
79
- "warnings": ["Page loaded slowly (3500ms)"],
80
- "screenshot": ".ralph/screenshots/dashboard.png",
81
- "title": "Dashboard - MyApp",
82
- "elementsFound": ["#app", ".header", ".sidebar"],
83
- "elementsMissing": [],
84
- "consoleErrors": [],
85
- "networkErrors": [],
86
- "loadTime": 1234
87
- }
88
- ```
89
-
90
- ### Exit Codes
91
-
92
- - `0` - Verification passed
93
- - `1` - Verification failed (check `errors` array)
94
-
95
- ## What It Catches
96
-
97
- | Issue | How It's Detected |
98
- |-------|-------------------|
99
- | Page won't load | Connection refused, timeout |
100
- | JavaScript errors | Console error messages |
101
- | Failed API calls | Network request failures, 4xx/5xx responses |
102
- | Error pages | Text matching "Oops!", "Something went wrong", etc. |
103
- | Missing elements | Selector not found in DOM |
104
- | Slow pages | Load time > 5 seconds (warning) |
105
-
106
- ## Integration with Ralph
107
-
108
- Ralph automatically uses this skill for frontend story verification:
109
-
110
- 1. Story has `testUrl` defined
111
- 2. Ralph calls `verify.ts` with the URL
112
- 3. Optionally checks `selectors` from story config
113
- 4. Takes screenshot for evidence
114
- 5. Fails verification if errors detected
115
-
116
- ## Requirements
117
-
118
- - Node.js 18+
119
- - Playwright (`npx playwright install chromium`)
120
-
121
- ## Troubleshooting
122
-
123
- **"Cannot find module 'playwright'"**
124
- ```bash
125
- npm install playwright
126
- npx playwright install chromium
127
- ```
128
-
129
- **"Browser closed unexpectedly"**
130
- - Try with `--no-headless` to see what's happening
131
- - Check if the URL is accessible
132
-
133
- **"Timeout waiting for page"**
134
- - Increase timeout: `--timeout 60000`
135
- - Check if dev server is running
@@ -1,450 +0,0 @@
1
- #!/usr/bin/env npx tsx
2
- /**
3
- * browser-verify.ts - Real browser verification for Ralph
4
- *
5
- * Uses Playwright to actually load pages and verify they work.
6
- * Much more reliable than asking Claude to "imagine" what a page looks like.
7
- *
8
- * Usage:
9
- * npx tsx browser-verify.ts <url> [options]
10
- *
11
- * Options:
12
- * --selectors '["#app", ".header"]' - Required elements (CSS selectors)
13
- * --screenshot <path> - Save screenshot to path
14
- * --timeout <ms> - Page load timeout (default: 30000)
15
- * --headless - Run headless (default: true)
16
- * --check-console - Fail on console errors (default: true)
17
- * --check-network - Fail on failed network requests (default: true)
18
- * --viewport <width>x<height> - Viewport size (default: 1280x720)
19
- * --mobile - Use mobile viewport (375x667)
20
- * --auth-cookie <json> - Set auth cookie before loading
21
- *
22
- * Output: JSON to stdout
23
- * { "pass": true/false, "errors": [], "warnings": [], "screenshot": "path" }
24
- */
25
-
26
- import { chromium, Browser, Page, ConsoleMessage } from 'playwright';
27
-
28
- interface AuthLogin {
29
- loginUrl: string;
30
- usernameSelector: string;
31
- passwordSelector: string;
32
- submitSelector: string;
33
- username: string;
34
- password: string;
35
- successIndicator?: string; // Selector or URL pattern to confirm login worked
36
- }
37
-
38
- interface VerifyOptions {
39
- url: string;
40
- selectors: string[];
41
- screenshot?: string;
42
- timeout: number;
43
- headless: boolean;
44
- checkConsole: boolean;
45
- checkNetwork: boolean;
46
- viewport: { width: number; height: number };
47
- authCookie?: { name: string; value: string; domain: string };
48
- authLogin?: AuthLogin;
49
- }
50
-
51
- interface VerifyResult {
52
- pass: boolean;
53
- errors: string[];
54
- warnings: string[];
55
- screenshot?: string;
56
- title?: string;
57
- elementsFound: string[];
58
- elementsMissing: string[];
59
- consoleErrors: string[];
60
- networkErrors: string[];
61
- loadTime: number;
62
- }
63
-
64
- async function parseArgs(): Promise<VerifyOptions> {
65
- const args = process.argv.slice(2);
66
-
67
- if (args.length === 0 || args[0] === '--help') {
68
- console.error(`
69
- Usage: npx tsx browser-verify.ts <url> [options]
70
-
71
- Options:
72
- --selectors '["#app", ".btn"]' Required elements (JSON array)
73
- --screenshot <path> Save screenshot
74
- --timeout <ms> Load timeout (default: 30000)
75
- --headless Run headless (default: true)
76
- --no-headless Show browser window
77
- --check-console Fail on console errors (default: true)
78
- --no-check-console Ignore console errors
79
- --check-network Fail on network errors (default: true)
80
- --no-check-network Ignore network errors
81
- --viewport <W>x<H> Viewport size (default: 1280x720)
82
- --mobile Mobile viewport (375x667)
83
- --auth-cookie '<json>' Set auth cookie
84
- --auth-login '<json>' Login before verifying (see below)
85
-
86
- Auth Login JSON format:
87
- {
88
- "loginUrl": "http://localhost:3000/login",
89
- "usernameSelector": "input[name='email']",
90
- "passwordSelector": "input[name='password']",
91
- "submitSelector": "button[type='submit']",
92
- "username": "test@test.local",
93
- "password": "testpass123",
94
- "successIndicator": "/dashboard" // URL pattern or selector
95
- }
96
- `);
97
- process.exit(1);
98
- }
99
-
100
- const url = args[0];
101
- const options: VerifyOptions = {
102
- url,
103
- selectors: [],
104
- timeout: 30000,
105
- headless: true,
106
- checkConsole: true,
107
- checkNetwork: true,
108
- viewport: { width: 1280, height: 720 },
109
- };
110
-
111
- for (let i = 1; i < args.length; i++) {
112
- const arg = args[i];
113
-
114
- switch (arg) {
115
- case '--selectors':
116
- try {
117
- options.selectors = JSON.parse(args[++i]);
118
- } catch {
119
- console.error('Invalid --selectors JSON');
120
- process.exit(1);
121
- }
122
- break;
123
- case '--screenshot':
124
- options.screenshot = args[++i];
125
- break;
126
- case '--timeout':
127
- options.timeout = parseInt(args[++i], 10);
128
- break;
129
- case '--headless':
130
- options.headless = true;
131
- break;
132
- case '--no-headless':
133
- options.headless = false;
134
- break;
135
- case '--check-console':
136
- options.checkConsole = true;
137
- break;
138
- case '--no-check-console':
139
- options.checkConsole = false;
140
- break;
141
- case '--check-network':
142
- options.checkNetwork = true;
143
- break;
144
- case '--no-check-network':
145
- options.checkNetwork = false;
146
- break;
147
- case '--viewport':
148
- const [w, h] = args[++i].split('x').map(Number);
149
- options.viewport = { width: w, height: h };
150
- break;
151
- case '--mobile':
152
- options.viewport = { width: 375, height: 667 };
153
- break;
154
- case '--auth-cookie':
155
- try {
156
- options.authCookie = JSON.parse(args[++i]);
157
- } catch {
158
- console.error('Invalid --auth-cookie JSON');
159
- process.exit(1);
160
- }
161
- break;
162
- case '--auth-login':
163
- try {
164
- options.authLogin = JSON.parse(args[++i]);
165
- } catch {
166
- console.error('Invalid --auth-login JSON');
167
- process.exit(1);
168
- }
169
- break;
170
- }
171
- }
172
-
173
- return options;
174
- }
175
-
176
- async function performLogin(page: Page, auth: AuthLogin, timeout: number): Promise<{ success: boolean; error?: string }> {
177
- try {
178
- // Navigate to login page
179
- await page.goto(auth.loginUrl, {
180
- timeout,
181
- waitUntil: 'networkidle',
182
- });
183
-
184
- // Wait for and fill username
185
- await page.waitForSelector(auth.usernameSelector, { timeout: 10000 });
186
- await page.fill(auth.usernameSelector, auth.username);
187
-
188
- // Fill password
189
- await page.waitForSelector(auth.passwordSelector, { timeout: 5000 });
190
- await page.fill(auth.passwordSelector, auth.password);
191
-
192
- // Click submit
193
- await page.waitForSelector(auth.submitSelector, { timeout: 5000 });
194
- await page.click(auth.submitSelector);
195
-
196
- // Wait for login to complete
197
- if (auth.successIndicator) {
198
- if (auth.successIndicator.startsWith('/') || auth.successIndicator.startsWith('http')) {
199
- // It's a URL pattern - wait for navigation
200
- await page.waitForURL(`**${auth.successIndicator}*`, { timeout: 15000 });
201
- } else {
202
- // It's a selector - wait for element
203
- await page.waitForSelector(auth.successIndicator, { timeout: 15000 });
204
- }
205
- } else {
206
- // No indicator - just wait for navigation away from login page
207
- await page.waitForFunction(
208
- (loginUrl) => !window.location.href.includes(loginUrl),
209
- auth.loginUrl,
210
- { timeout: 15000 }
211
- );
212
- }
213
-
214
- return { success: true };
215
- } catch (err: any) {
216
- return { success: false, error: `Login failed: ${err.message}` };
217
- }
218
- }
219
-
220
- async function verify(options: VerifyOptions): Promise<VerifyResult> {
221
- const result: VerifyResult = {
222
- pass: true,
223
- errors: [],
224
- warnings: [],
225
- elementsFound: [],
226
- elementsMissing: [],
227
- consoleErrors: [],
228
- networkErrors: [],
229
- loadTime: 0,
230
- };
231
-
232
- let browser: Browser | null = null;
233
- let page: Page | null = null;
234
-
235
- try {
236
- // Launch browser
237
- browser = await chromium.launch({
238
- headless: options.headless,
239
- });
240
-
241
- const context = await browser.newContext({
242
- viewport: options.viewport,
243
- });
244
-
245
- // Set auth cookie if provided
246
- if (options.authCookie) {
247
- await context.addCookies([{
248
- name: options.authCookie.name,
249
- value: options.authCookie.value,
250
- domain: options.authCookie.domain,
251
- path: '/',
252
- }]);
253
- }
254
-
255
- page = await context.newPage();
256
-
257
- // Perform login if auth credentials provided
258
- if (options.authLogin) {
259
- const loginResult = await performLogin(page, options.authLogin, options.timeout);
260
- if (!loginResult.success) {
261
- result.errors.push(loginResult.error || 'Login failed');
262
- result.pass = false;
263
- return result;
264
- }
265
- result.warnings.push('Logged in as ' + options.authLogin.username);
266
- }
267
-
268
- // Collect console errors
269
- const consoleErrors: string[] = [];
270
- page.on('console', (msg: ConsoleMessage) => {
271
- const type = msg.type();
272
- const text = msg.text();
273
-
274
- // Capture actual console.error calls
275
- if (type === 'error') {
276
- // Only ignore truly benign noise (favicons, devtools)
277
- if (!text.includes('favicon') && !text.includes('DevTools')) {
278
- consoleErrors.push(text);
279
- }
280
- }
281
-
282
- // Also capture logs/warnings that indicate errors (e.g., custom loggers)
283
- if ((type === 'log' || type === 'warning') &&
284
- (text.includes('_error') || text.includes('Error:') || text.includes('ERROR'))) {
285
- // Ignore known non-critical patterns
286
- if (!text.includes('ResizeObserver')) {
287
- consoleErrors.push(`[${type}] ${text}`);
288
- }
289
- }
290
- });
291
-
292
- // Collect network errors
293
- const networkErrors: string[] = [];
294
- page.on('requestfailed', (request) => {
295
- const failure = request.failure();
296
- if (failure) {
297
- networkErrors.push(`${request.method()} ${request.url()}: ${failure.errorText}`);
298
- }
299
- });
300
-
301
- // Track failed responses (4xx, 5xx)
302
- page.on('response', (response) => {
303
- const status = response.status();
304
- if (status >= 400) {
305
- const url = response.url();
306
- // Ignore favicon and some common 404s
307
- if (!url.includes('favicon') && !url.includes('hot-update')) {
308
- networkErrors.push(`${response.request().method()} ${url}: ${status}`);
309
- }
310
- }
311
- });
312
-
313
- // Navigate to page
314
- const startTime = Date.now();
315
-
316
- try {
317
- await page.goto(options.url, {
318
- timeout: options.timeout,
319
- waitUntil: 'networkidle',
320
- });
321
- } catch (err: any) {
322
- if (err.message.includes('net::ERR_CONNECTION_REFUSED')) {
323
- result.errors.push(`Cannot connect to ${options.url} - is the server running?`);
324
- result.pass = false;
325
- return result;
326
- }
327
- throw err;
328
- }
329
-
330
- result.loadTime = Date.now() - startTime;
331
-
332
- // Get page title
333
- result.title = await page.title();
334
- if (!result.title) {
335
- result.warnings.push('Page has no title');
336
- }
337
-
338
- // Check for error page indicators
339
- const bodyText = await page.textContent('body') || '';
340
- const errorPatterns = [
341
- /something went wrong/i,
342
- /error occurred/i,
343
- /500 internal server/i,
344
- /503 service unavailable/i,
345
- /404 not found/i,
346
- /oops!/i,
347
- /application error/i,
348
- /unhandled runtime error/i,
349
- ];
350
-
351
- for (const pattern of errorPatterns) {
352
- if (pattern.test(bodyText)) {
353
- result.errors.push(`Page contains error text: "${bodyText.match(pattern)?.[0]}"`);
354
- result.pass = false;
355
- }
356
- }
357
-
358
- // Check required selectors
359
- for (const selector of options.selectors) {
360
- try {
361
- const element = await page.$(selector);
362
- if (element) {
363
- result.elementsFound.push(selector);
364
- } else {
365
- result.elementsMissing.push(selector);
366
- result.errors.push(`Required element not found: ${selector}`);
367
- result.pass = false;
368
- }
369
- } catch (err: any) {
370
- result.elementsMissing.push(selector);
371
- result.errors.push(`Error checking selector "${selector}": ${err.message}`);
372
- result.pass = false;
373
- }
374
- }
375
-
376
- // Check console errors
377
- result.consoleErrors = consoleErrors;
378
- if (options.checkConsole && consoleErrors.length > 0) {
379
- result.errors.push(`${consoleErrors.length} console error(s) detected`);
380
- result.pass = false;
381
- }
382
-
383
- // Check network errors
384
- result.networkErrors = networkErrors;
385
- if (options.checkNetwork && networkErrors.length > 0) {
386
- // Filter out truly benign 404s (static resources)
387
- const criticalErrors = networkErrors.filter(e =>
388
- !e.includes('favicon') &&
389
- !e.includes('.map') &&
390
- !e.includes('hot-update') &&
391
- !e.includes('.woff') &&
392
- !e.includes('.ttf') &&
393
- !e.includes('.png') &&
394
- !e.includes('.jpg') &&
395
- !e.includes('.svg') &&
396
- !e.includes('.ico')
397
- );
398
- if (criticalErrors.length > 0) {
399
- // Show actual URLs so Claude knows what to fix
400
- result.errors.push(`Network errors (fix these):`);
401
- criticalErrors.slice(0, 5).forEach(e => result.errors.push(` ${e}`));
402
- result.pass = false;
403
- }
404
- }
405
-
406
- // Take screenshot
407
- if (options.screenshot) {
408
- await page.screenshot({
409
- path: options.screenshot,
410
- fullPage: true,
411
- });
412
- result.screenshot = options.screenshot;
413
- }
414
-
415
- } catch (err: any) {
416
- result.errors.push(`Browser error: ${err.message}`);
417
- result.pass = false;
418
- } finally {
419
- if (browser) {
420
- await browser.close();
421
- }
422
- }
423
-
424
- return result;
425
- }
426
-
427
- async function main() {
428
- const options = await parseArgs();
429
- const result = await verify(options);
430
-
431
- // Output JSON result
432
- console.log(JSON.stringify(result, null, 2));
433
-
434
- // Exit with appropriate code
435
- process.exit(result.pass ? 0 : 1);
436
- }
437
-
438
- main().catch((err) => {
439
- console.error(JSON.stringify({
440
- pass: false,
441
- errors: [`Fatal error: ${err.message}`],
442
- warnings: [],
443
- elementsFound: [],
444
- elementsMissing: [],
445
- consoleErrors: [],
446
- networkErrors: [],
447
- loadTime: 0,
448
- }, null, 2));
449
- process.exit(1);
450
- });