@saulwade/swl-ses 1.6.3 → 1.6.6

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 (46) hide show
  1. package/CLAUDE.md +3 -3
  2. package/README.md +2 -2
  3. package/agentes/gh-fix-ci-swl.md +275 -0
  4. package/agentes/nemesis-auditor-swl.md +90 -1
  5. package/comandos/swl/exportar-vault.md +106 -14
  6. package/comandos/swl/nemesis.md +70 -3
  7. package/comandos/swl/release.md +62 -2
  8. package/comandos/swl/salud.md +32 -0
  9. package/comandos/swl/verificar.md +116 -2
  10. package/habilidades/agent-browser/SKILL.md +111 -4
  11. package/habilidades/agent-deep-links/SKILL.md +148 -0
  12. package/habilidades/backend-async-postgres-testing/SKILL.md +215 -0
  13. package/habilidades/backend-error-design/SKILL.md +221 -0
  14. package/habilidades/browser-interaction-patterns/SKILL.md +514 -0
  15. package/habilidades/browser-research-domains/SKILL.md +635 -0
  16. package/habilidades/changelog-generator/SKILL.md +172 -0
  17. package/habilidades/changelog-generator/scripts/parse-commits.js +354 -0
  18. package/habilidades/devsecops-pipeline-security/SKILL.md +3 -0
  19. package/habilidades/fastapi-experto/SKILL.md +49 -4
  20. package/habilidades/harness-claude-code/SKILL.md +4 -1
  21. package/habilidades/postgresql-experto/SKILL.md +80 -4
  22. package/habilidades/proceso-discovery-machote/SKILL.md +157 -0
  23. package/habilidades/proceso-modular-split/SKILL.md +256 -0
  24. package/habilidades/tdd-workflow/SKILL.md +12 -5
  25. package/hooks/extraccion-aprendizajes.js +8 -0
  26. package/hooks/lib/deep-links.js +185 -0
  27. package/hooks/lib/evolution-tracker.js +148 -20
  28. package/hooks/lib/gateway-notify.js +70 -7
  29. package/manifiestos/modulos.json +13 -3
  30. package/manifiestos/skills-lock.json +1247 -1191
  31. package/package.json +92 -92
  32. package/plugin.json +371 -362
  33. package/reglas/arquitectura.md +38 -0
  34. package/reglas/arreglar-al-detectar.md +93 -0
  35. package/reglas/auditorias-documentales-estructurales.md +38 -0
  36. package/reglas/registro-componentes-nuevos.md +14 -0
  37. package/reglas/tests-cleanup.md +220 -0
  38. package/scripts/instalador.js +72 -4
  39. package/scripts/lib/mcp_config.py +29 -14
  40. package/scripts/lib/notificaciones-telegram.js +14 -0
  41. package/scripts/lib/transformadores/codex.js +4 -0
  42. package/scripts/lib/transformadores/cursor.js +5 -0
  43. package/scripts/mcp-orchestrator.py +153 -131
  44. package/scripts/mcp-pool-manager.py +132 -107
  45. package/scripts/mcp-telemetry.py +139 -120
  46. package/scripts/verificar-release.js +199 -1
@@ -0,0 +1,514 @@
1
+ ---
2
+ name: browser-interaction-patterns
3
+ description: >
4
+ Patrones canónicos para automatización de browser via CDP (Chrome DevTools Protocol):
5
+ dialogs nativos, iframes y shadow DOM, dropdowns, downloads y uploads, screenshots,
6
+ scrolling, cookies, network requests, tabs y viewport. Cargar cuando una sesión
7
+ de browser automation tropieza con mecánica específica de UI (frame stuck, dropdown
8
+ invisible, dialog colgando JS, upload sin progresar) y agent-browser no resuelve
9
+ por defecto. Complementa a agent-browser con patrones de bajo nivel CDP.
10
+ version: "1.0.0"
11
+ herramientasPermitidas: [Read, Bash, WebFetch]
12
+ evolved: false
13
+ fuente: "browser-use/browser-harness — interaction-skills (MIT License, 2026)"
14
+ evolvable: true
15
+ exclusiones:
16
+ - "No cargar para scraping de páginas estáticas sin interacción — usar WebFetch o web-fetcher-routing."
17
+ - "No cargar para research técnico de research-domains (github, arxiv, etc.) — esos viven en browser-research-domains."
18
+ - "No cargar para automatización Playwright/Selenium tradicional — este skill asume CDP directo o agent-browser CLI."
19
+ - "No cargar como reemplazo de agent-browser — es complemento; siempre cargar primero agent-browser y solo escalar aquí ante tropiezos específicos."
20
+ ---
21
+
22
+ # Skill: browser-interaction-patterns
23
+
24
+ Patrones de browser automation a nivel CDP para resolver tropiezos comunes que
25
+ `agent-browser` por sí solo no documenta. Cada sección entrega: anti-patrón
26
+ observable, regla canónica, ejemplo concreto y gotcha clave.
27
+
28
+ Adaptado de `browser-use/browser-harness` (MIT License) — los patrones son
29
+ agnósticos al runtime CLI específico (`browser-harness` o `agent-browser`).
30
+
31
+ ## Cuándo cargar
32
+
33
+ - El agente tropieza con un dialog `alert/confirm/prompt` que congela la página.
34
+ - Una página tiene iframe cross-origin o shadow DOM y el selector falla.
35
+ - Un dropdown nativo `<select>` no responde a click sintético.
36
+ - Una descarga arranca pero no hay forma de verificar que terminó.
37
+ - Un upload con `<input type="file">` no acepta el archivo.
38
+ - El agente abre una pestaña y todas las acciones siguientes ocurren "invisibles" al usuario.
39
+ - Una SPA cambia de ruta y el agente actúa antes de que termine la hidratación.
40
+
41
+ ## Cuándo NO cargar
42
+
43
+ - Páginas estáticas HTML sin interacción → `WebFetch` o `web-fetcher-routing`.
44
+ - Tareas de research técnico contra APIs documentadas → `browser-research-domains`.
45
+ - Tests E2E con framework dedicado (Playwright, Cypress, Detox) → skill correspondiente.
46
+
47
+ ## Regla universal — verificar con screenshot
48
+
49
+ Tras cualquier acción que modifique estado (click, navigation, form submit,
50
+ upload, dialog dismiss), **re-capturar screenshot antes de asumir éxito**.
51
+ La verificación visual es la única señal confiable de que el compositor de Chrome
52
+ aplicó el cambio. `page_info()` o assertions del DOM pueden mentir si la página
53
+ está en transición.
54
+
55
+ ---
56
+
57
+ ## 1. Dialogs nativos (alert / confirm / prompt / beforeunload)
58
+
59
+ Los dialogs nativos congelan el thread de JS hasta resolverse. Si el agente
60
+ intenta cualquier acción mientras hay dialog abierto, todo cuelga.
61
+
62
+ ### Detección — `page_info()` auto-surface
63
+
64
+ Cuando hay dialog pendiente, `page_info()` retorna `{"dialog": {type, message, ...}}`
65
+ en lugar del viewport dict habitual. Verificar antes de actuar.
66
+
67
+ ### Dismiss reactivo via CDP (preferido)
68
+
69
+ Funciona aún con JS frozen. Maneja todos los tipos incluyendo `beforeunload`.
70
+ NO detectable por antibot — no inyecta JS al page.
71
+
72
+ ```python
73
+ cdp("Page.handleJavaScriptDialog", accept=True) # OK
74
+ cdp("Page.handleJavaScriptDialog", accept=False) # Cancel
75
+
76
+ events = drain_events()
77
+ for e in events:
78
+ if e["method"] == "Page.javascriptDialogOpening":
79
+ print(e["params"]["type"]) # alert/confirm/prompt/beforeunload
80
+ print(e["params"]["message"])
81
+ ```
82
+
83
+ ### Stub proactivo via JS (cuando esperas múltiples alerts)
84
+
85
+ Tradeoffs: detectable por antibot, NO maneja `beforeunload`, se pierde al navegar.
86
+
87
+ ```python
88
+ js("""
89
+ window.__dialogs__=[];
90
+ window.alert=m=>window.__dialogs__.push(String(m));
91
+ window.confirm=m=>{window.__dialogs__.push(String(m));return true;};
92
+ window.prompt=(m,d)=>{window.__dialogs__.push(String(m));return d||'';};
93
+ """)
94
+ ```
95
+
96
+ **Gotcha**: `beforeunload` aparece al navegar con cambios sin guardar. Manejar
97
+ SIEMPRE con CDP, nunca con stub JS.
98
+
99
+ ---
100
+
101
+ ## 2. Iframes y cross-origin
102
+
103
+ ### Same-origin
104
+
105
+ Acceder via `contentDocument` / `contentWindow` desde el contexto padre.
106
+ Las coordenadas de click son **frame-local**, no de la página completa.
107
+
108
+ ### Cross-origin — usar `iframe_target`
109
+
110
+ El iframe tiene su propio target CDP. Attach con `iframe_target(url_substr)` y
111
+ pasar el `target_id` a `js(..., target_id=tid)`.
112
+
113
+ ```python
114
+ tid = iframe_target("stripe.com/checkout")
115
+ saldo = js("document.querySelector('.balance').textContent", target_id=tid)
116
+ ```
117
+
118
+ ### Mejor opción para clicks en iframe — coordinate clicks
119
+
120
+ `Input.dispatchMouseEvent` opera a nivel **compositor** de Chrome y pasa por
121
+ iframes cross-origin sin trabajo extra. Es preferible a navegar el árbol de
122
+ targets cuando el objetivo es visible.
123
+
124
+ ```python
125
+ # Click en botón visible dentro de iframe cross-origin
126
+ click_at_xy(x, y) # las coordenadas del screenshot funcionan tal cual
127
+ capture_screenshot() # verificar
128
+ ```
129
+
130
+ **Gotcha**: solo bajar a DOM iframe cuando el target NO tiene geometría visible
131
+ (hidden input, 0×0 node).
132
+
133
+ ---
134
+
135
+ ## 3. Shadow DOM
136
+
137
+ DOM oculto en componentes Web. `querySelector` desde el documento root NO
138
+ penetra `shadowRoot`.
139
+
140
+ ### Traversal recursivo cuando hay que penetrar
141
+
142
+ ```javascript
143
+ function deepQuerySelector(root, selector) {
144
+ const found = root.querySelector(selector);
145
+ if (found) return found;
146
+ for (const el of root.querySelectorAll('*')) {
147
+ if (el.shadowRoot) {
148
+ const inShadow = deepQuerySelector(el.shadowRoot, selector);
149
+ if (inShadow) return inShadow;
150
+ }
151
+ }
152
+ return null;
153
+ }
154
+ ```
155
+
156
+ ### Mejor opción — coordinate clicks
157
+
158
+ Si el elemento es visible, `click_at_xy(x, y)` cruza shadow DOM sin necesidad
159
+ de penetrar. Mismo principio que iframes cross-origin: operar a nivel
160
+ compositor evita la complejidad de los árboles anidados.
161
+
162
+ **Gotcha**: componentes Polymer/Lit/Stencil que componen 5+ shadow roots
163
+ anidados son frágiles para selectors. Default a screenshots + coordinate clicks.
164
+
165
+ ---
166
+
167
+ ## 4. Dropdowns
168
+
169
+ Cuatro variantes con tratamiento distinto:
170
+
171
+ ### Native `<select>`
172
+
173
+ NO responde a `click` sintético. Usar:
174
+
175
+ ```python
176
+ js("""
177
+ const sel = document.querySelector('select#country');
178
+ sel.value = 'MX';
179
+ sel.dispatchEvent(new Event('change', {bubbles:true}));
180
+ """)
181
+ ```
182
+
183
+ ### Custom overlay (Material, Ant Design, Bootstrap)
184
+
185
+ Render con `position: fixed` o `position: absolute`, geometría aparece **después**
186
+ de click en el trigger. Patrón obligatorio:
187
+
188
+ ```python
189
+ click_at_xy(trigger_x, trigger_y)
190
+ wait(0.3) # esperar render del overlay
191
+ capture_screenshot() # re-medir geometría
192
+ # leer la posición de la opción del screenshot nuevo
193
+ click_at_xy(option_x, option_y)
194
+ ```
195
+
196
+ ### Searchable combobox (React Select, Algolia)
197
+
198
+ Escribir texto en el input filtra opciones. `type_text` puede no disparar el
199
+ filtro si el componente espera eventos `input`/`change` reales. Usar
200
+ `fill_input` que dispara los eventos sintéticos.
201
+
202
+ ### Virtualized menu (react-window, AG-Grid)
203
+
204
+ La opción que buscas puede no estar en el DOM hasta hacer scroll dentro del
205
+ overlay. Hacer `scroll(x, y, dy=N)` sobre las coordenadas del overlay.
206
+
207
+ ---
208
+
209
+ ## 5. Downloads
210
+
211
+ Dos modos: browser-triggered (con CDP) vs HTTP directo.
212
+
213
+ ### HTTP directo cuando se conoce la URL del archivo
214
+
215
+ ```python
216
+ # Para PDFs, CSVs, ZIP con URL conocida — más rápido y observable
217
+ import urllib.request
218
+ urllib.request.urlretrieve(url, "/tmp/file.pdf")
219
+ ```
220
+
221
+ ### Browser-triggered (click en "Download")
222
+
223
+ ```python
224
+ cdp("Page.setDownloadBehavior", behavior="allow", downloadPath="/tmp/dl")
225
+ click_at_xy(download_btn_x, download_btn_y)
226
+ # Verificar archivo aparece en /tmp/dl/ con tamaño > 0
227
+ ```
228
+
229
+ **Gotcha**: muchos sites generan el archivo on-the-fly (POST + redirect a S3
230
+ con presigned URL). El `setDownloadBehavior` captura, pero la verificación es
231
+ por filesystem polling, no por DOM signal.
232
+
233
+ ---
234
+
235
+ ## 6. Uploads
236
+
237
+ `<input type="file">` rechaza interacción sintética por sandbox del browser.
238
+ Usar CDP `DOM.setFileInputFiles`:
239
+
240
+ ```python
241
+ upload_file("input[type=file]", "/abs/path/file.pdf")
242
+ ```
243
+
244
+ ### Drag-and-drop como alternativa
245
+
246
+ Si el site solo acepta drop (no input file visible), construir eventos
247
+ `DragEvent` con `DataTransfer` poblada. Si el site usa un componente complejo
248
+ (Filepond, Dropzone), preferir descubrir el `<input>` oculto (siempre existe
249
+ para fallback A11y) y usar `setFileInputFiles` sobre ese.
250
+
251
+ **Gotcha**: path DEBE ser absoluto. Path relativo silently falla sin error
252
+ visible.
253
+
254
+ ---
255
+
256
+ ## 7. Screenshots
257
+
258
+ `capture_screenshot()` escribe PNG del viewport actual. El archivo está en
259
+ **device pixels**: en una pantalla 2× un viewport 2296×1143 CSS produce PNG
260
+ 4592×2286.
261
+
262
+ ### Coordenadas
263
+
264
+ Click coordinates son **CSS pixels**. NO leas posición del PNG y pases directo
265
+ a `click_at_xy()` sin dividir por `devicePixelRatio`:
266
+
267
+ ```python
268
+ dpr = js("window.devicePixelRatio") or 1
269
+ # Si lees (px, py) del PNG, convertir:
270
+ click_at_xy(px / dpr, py / dpr)
271
+ ```
272
+
273
+ ### Límite 2000 px para LLMs vision
274
+
275
+ Algunos modelos rechazan imágenes > 2000 px/lado. Pasar `max_dim=1800` para
276
+ downscale automático:
277
+
278
+ ```python
279
+ capture_screenshot("/tmp/shot.png", max_dim=1800)
280
+ ```
281
+
282
+ Solo downscalea si excede; seguro dejar siempre activo.
283
+
284
+ ### Full-page vs viewport
285
+
286
+ `full=True` captura todo el documento (puede ser MB). Usar solo cuando
287
+ necesites contenido bajo el fold; viewport es mucho más rápido.
288
+
289
+ ---
290
+
291
+ ## 8. Scrolling
292
+
293
+ Identificar qué elemento consume wheel events ANTES de hacer scroll.
294
+
295
+ | Caso | Comando |
296
+ |------|---------|
297
+ | Scroll de page | `scroll(x, y, dy=-300)` con coordenadas dentro del viewport principal |
298
+ | Scroll de contenedor anidado (div con overflow) | `scroll(x, y, ...)` con coordenadas dentro del contenedor |
299
+ | Scroll de virtualized list (react-window) | Igual que contenedor; pero verificar con screenshot que la lista realmente avanzó (puede tener su propio scroll handler) |
300
+ | Scroll de dropdown menu | Coordenadas dentro del overlay tras abrirlo |
301
+
302
+ **Gotcha**: `js("window.scrollBy(0, 300)")` funciona en page pero NO en
303
+ contenedores anidados. Para contenedores anidados, usar `Input.dispatchMouseEvent`
304
+ con `type=mouseWheel` (que es lo que hace `scroll(...)`).
305
+
306
+ ---
307
+
308
+ ## 9. Cookies
309
+
310
+ Cookies son **browser state**, no page state. Acceder via CDP, no via
311
+ `document.cookie` que solo ve cookies del path actual sin `HttpOnly`.
312
+
313
+ ```python
314
+ cookies = cdp("Network.getCookies")["cookies"]
315
+ cdp("Network.setCookie", name="session", value="abc", domain=".example.com", path="/")
316
+ cdp("Network.clearBrowserCookies")
317
+ ```
318
+
319
+ **Gotcha**: cookies `HttpOnly` y `Secure` NO aparecen en `document.cookie`.
320
+ Para auth tokens, SIEMPRE usar `Network.getCookies` CDP.
321
+
322
+ ---
323
+
324
+ ## 10. Network requests
325
+
326
+ Cuando una acción no produce cambio DOM visible (form submit, SPA route change,
327
+ SSE keepalive), inferir desde tráfico de red.
328
+
329
+ ### Patrón — esperar idle de red post-acción
330
+
331
+ ```python
332
+ # El helper canónico — espera N ms sin requests in-flight
333
+ wait_for_network_idle(timeout=10.0, idle_ms=500)
334
+ ```
335
+
336
+ Equivalente conceptual usando `drain_events`:
337
+
338
+ ```python
339
+ deadline = time.time() + 10
340
+ in_flight = set()
341
+ last_activity = time.time()
342
+ while time.time() < deadline:
343
+ for e in drain_events():
344
+ m = e.get("method", "")
345
+ if m == "Network.requestWillBeSent":
346
+ in_flight.add(e["params"]["requestId"])
347
+ last_activity = time.time()
348
+ elif m in ("Network.loadingFinished", "Network.loadingFailed"):
349
+ in_flight.discard(e["params"]["requestId"])
350
+ last_activity = time.time()
351
+ if not in_flight and (time.time() - last_activity) * 1000 >= 500:
352
+ break
353
+ time.sleep(0.1)
354
+ ```
355
+
356
+ **Gotcha**: filtrar eventos por `session_id` activo. Tabs en background
357
+ (polling/SSE) inyectan eventos al buffer global y envenenan el idle check.
358
+
359
+ ---
360
+
361
+ ## 11. Tabs
362
+
363
+ **Regla**: CDP para control, UI automation para orden visible.
364
+
365
+ ### CDP — qué hace bien
366
+
367
+ - Crear tab (`new_tab(url)`)
368
+ - Attach a target conocido
369
+ - Activar tab conocida (`Target.activateTarget`)
370
+ - Capturar screenshot del tab attached incluso si otro está al frente visible
371
+ - Inspeccionar URL/title/viewport
372
+
373
+ ### CDP — qué hace MAL
374
+
375
+ - Matchear el orden visible left-to-right del tab strip del usuario
376
+ - Distinguir target real vs omnibox popup sin filtrar URLs
377
+
378
+ ### Orden visible
379
+
380
+ - **macOS**: AppleScript (`tell application "Google Chrome" / every tab of front window`)
381
+ - **Linux**: `xdotool`, `wmctrl`, scripting del WM
382
+ - **Windows**: UI Automation framework
383
+
384
+ ### Reglas confirmadas
385
+
386
+ - `switch_tab()` NO es suficiente si el usuario espera ver el cambio en Chrome → llamar también `Target.activateTarget`.
387
+ - `list_tabs()` incluye `chrome://newtab/` por default → `include_chrome=False` para solo páginas reales.
388
+ - `chrome://omnibox-popup.top-chrome/` aparece como page target falso → ignorar.
389
+ - Si una page reporta `w=0 h=0`, probablemente attached al target equivocado.
390
+
391
+ ---
392
+
393
+ ## 12. Viewport
394
+
395
+ Cambios de viewport afectan layout, coordenadas y workflows que dependen de
396
+ geometría estable.
397
+
398
+ ```python
399
+ cdp("Emulation.setDeviceMetricsOverride",
400
+ width=1280, height=800, deviceScaleFactor=1, mobile=False)
401
+ ```
402
+
403
+ **Gotcha**: tras cambiar viewport, **re-capturar screenshot y re-medir todas
404
+ las coordenadas**. Posiciones de elementos cambian incluso con cambios mínimos
405
+ de ancho. Para tests responsive, capturar 320/375/768/1024/1440 y verificar
406
+ cada uno.
407
+
408
+ ---
409
+
410
+ ## 13. Print as PDF
411
+
412
+ CDP genera PDF sin necesidad de click en botón de impresión:
413
+
414
+ ```python
415
+ result = cdp("Page.printToPDF", printBackground=True, preferCSSPageSize=True)
416
+ with open("/tmp/page.pdf", "wb") as f:
417
+ f.write(base64.b64decode(result["data"]))
418
+ ```
419
+
420
+ **Gotcha**: si el site fuerza un botón "Print" custom que abre dialog del
421
+ sistema, el botón debe clickearse PRIMERO y luego CDP captura la página
422
+ renderizada. La regla de oro: `Page.printToPDF` siempre funciona en la página
423
+ visible; el botón solo lo necesitas cuando el site ajusta layout para print
424
+ (`@media print`).
425
+
426
+ ---
427
+
428
+ ## 14. Connection y tab visibility
429
+
430
+ Al arrancar Chrome fresh, los únicos targets `type: "page"` pueden ser
431
+ `chrome://inspect` y `chrome://omnibox-popup.top-chrome/` (1px invisible). Si
432
+ el daemon attached al omnibox popup, **TODAS** las acciones siguientes —
433
+ incluyendo `new_tab()` y `goto_url()` — ocurren en tabs CDP pero NO visibles
434
+ al usuario.
435
+
436
+ ### Sequence de bootstrap
437
+
438
+ ```python
439
+ if not daemon_alive():
440
+ cleanup_sockets() # limpiar PIDs/sockets stale
441
+ ensure_daemon()
442
+
443
+ tabs = list_tabs()
444
+ for t in tabs:
445
+ print(t["url"][:60])
446
+
447
+ tab = ensure_real_tab() # attach a tab real, no chrome://
448
+ ```
449
+
450
+ ### Llevar Chrome al frente
451
+
452
+ ```python
453
+ # macOS
454
+ subprocess.run(["osascript", "-e",
455
+ 'tell application "Google Chrome" to activate'])
456
+ # Linux con wmctrl
457
+ subprocess.run(["wmctrl", "-a", "Google Chrome"])
458
+ ```
459
+
460
+ ---
461
+
462
+ ## Patrones transversales
463
+
464
+ ### Coordinate clicks > selector clicks
465
+
466
+ `Input.dispatchMouseEvent` opera a nivel compositor de Chrome y pasa por
467
+ iframes, shadow DOM y cross-origin sin trabajo extra. Default a coordenadas
468
+ si el elemento es visible; bajar a DOM solo cuando NO hay geometría.
469
+
470
+ ### Screenshots primero para verificación
471
+
472
+ Tras cualquier acción que modifica estado, capturar screenshot ANTES de
473
+ asumir éxito. Es la única señal confiable de que el compositor aplicó el
474
+ cambio.
475
+
476
+ ### `wait_for_network_idle` > `wait(N)` fijo
477
+
478
+ Esperar event-driven (CDP Network events) en lugar de timeout ciego. Más
479
+ rápido en páginas rápidas, más confiable en lentas.
480
+
481
+ ### Re-medir geometría post-acción
482
+
483
+ Tras abrir dropdown / modal / overlay, screenshot + leer coordenadas nuevas.
484
+ NO reutilizar posiciones medidas antes de la acción — la geometría cambió.
485
+
486
+ ---
487
+
488
+ ## Gotchas que cuestan horas
489
+
490
+ - **`click_at_xy(x, y)` falla sin error** cuando coordenadas caen en zona
491
+ cubierta por dialog/modal/overlay invisible. Verificar con screenshot ANTES.
492
+ - **`document.cookie` no ve `HttpOnly` cookies**. Auth tokens viven ahí —
493
+ usar `Network.getCookies` CDP.
494
+ - **`wait_for_load()` retorna True antes de hidratación React/Vue**. Para SPAs,
495
+ añadir `wait_for_element(selector_clave)` después.
496
+ - **`new_tab()` puede abrir tab que no es la activa**. Llamar
497
+ `Target.activateTarget` para que sea visible al usuario.
498
+ - **Path absoluto OBLIGATORIO** en `setFileInputFiles`. Path relativo falla
499
+ silencioso.
500
+ - **Tabs en background siguen emitiendo Network events** al buffer global.
501
+ Filtrar por `session_id` activo en `wait_for_network_idle`.
502
+
503
+ ---
504
+
505
+ ## Relación con otras skills
506
+
507
+ - **`agent-browser`**: skill base. Cargar primero. Solo escalar aquí ante
508
+ tropiezos específicos.
509
+ - **`web-fetcher-routing`**: orquesta WebFetch vs agent-browser vs markitdown.
510
+ Si la página es estática, no necesitas este skill.
511
+ - **`browser-research-domains`**: patrones específicos por dominio (github,
512
+ arxiv, etc.) que evitan browser cuando hay API.
513
+
514
+ <!-- Adaptado de browser-use/browser-harness bajo MIT License (browser-use, 2026). -->