@arcadialdev/arcality 2.4.33 → 2.4.35

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.
@@ -45,7 +45,10 @@ Ejecuta las acciones planificadas:
45
45
 
46
46
  1. **Llena múltiples campos por turno** — No pierdas un turno por campo. Agrupa 3-5 fills en una sola llamada a `perform_ui_actions`.
47
47
  2. **Para cada campo fill**: click + fill (en ese orden). El click asegura que el campo tiene focus.
48
- 3. **Para dropdowns/selects**: Click para abrir → espera el siguiente turno → selecciona la opción.
48
+ 3. **Para dropdowns/selects** Protocolo de Re-render:
49
+ - **Turno A**: Click para abrir el dropdown, espera el siguiente turno para ver las opciones.
50
+ - **Turno B**: Click en la opción deseada. ¡PRECAUCIÓN DE SEGURIDAD DOM! Si el formulario requiere llenar más campos después de seleccionar (ej. se habilitan nuevos inputs), **DETENTE AQUÍ**. NO agrupes el select con los siguientes fills en el mismo turno, porque React re-redenrizará el DOM y tus IDs se romperán. Espera al Turno C para seguir llenando.
51
+ - Si la selección del dropdown era el *último* paso, Y YA NO HAY MÁS campos que llenar, SÍ puedes hacer click en `Siguiente`/`Guardar` en ese mismo turno.
49
52
  4. **Si un fill falla**: NO reintentes inmediatamente. Usa `inspect_element_details` para ver si el campo es un select, un datepicker, o tiene un overlay encima.
50
53
 
51
54
  ### Fase 4: VERIFICACIÓN (Verify)
package/README.md CHANGED
@@ -55,6 +55,20 @@ npm run arcality:run
55
55
 
56
56
  ---
57
57
 
58
+ ## 💻 CLI Commands Cheatsheet
59
+
60
+ The Arcality flow is designed to be used globally or project-wide via \`npx\`. Below are all currently supported commands:
61
+
62
+ | Command | Description | Internal Behavior |
63
+ | :--- | :--- | :--- |
64
+ | \`npx arcality\` | **Interactive Menu**<br/>Launches a visual menu in the terminal to guide the user. | Prompts for the Mission to execute. Ideal for new users who want a guided experience. |
65
+ | \`npx arcality init\` | **Initial Configuration**<br/>Connects the current project to Arcality Cloud. | Prompts for your API Key and Project ID, generates \`arcality.config.json\`, and validates the ADO / Portal connection. |
66
+ | \`npx arcality setup\` | **Dependency Setup**<br/>Downloads browser binaries. | Silently executes the installation of internal Playwright browsers (Chromium) ensuring the local machine can run tests. |
67
+ | \`npx arcality run\` | **Mission Runner (Agent Mode)**<br/>Launches the QA Agent directly. | Skips the interactive menu, scans the _package.json_ ("Ghost Mode"), injects ephemeral _autoskills_, and unleashes the AI agent. Ideal for CI/CD. |
68
+ | \`npx arcality --logs\` | **History Viewer**<br/>Displays test performance and tokens spent. | Reads the local \`arcality-history.log\` and renders a colorful table listing each mission, token consumption, and success status. |
69
+
70
+ ---
71
+
58
72
  ## 🛠️ Internal Architecture
59
73
 
60
74
  Arcality operates on a highly decoupled modular system designed for maximum resilience:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcadialdev/arcality",
3
- "version": "2.4.33",
3
+ "version": "2.4.35",
4
4
  "description": "AI-powered QA testing tool — Autonomous web testing agent by Arcadial",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -8,15 +8,32 @@ const __filename = fileURLToPath(import.meta.url);
8
8
  const __dirname = path.dirname(__filename);
9
9
  const PROJECT_ROOT = process.env.ARCALITY_ROOT || path.join(__dirname, "..");
10
10
 
11
- const contextDir = path.join(process.cwd(), "out");
12
- const centralLog = path.join(contextDir, "arcality-history.log");
11
+ const rootOutDir = path.join(process.cwd(), ".arcality", "out");
12
+
13
+ // Buscar el archivo arcality-history.log recursivamente (o tomar el más reciente si hay varios reportes)
14
+ let centralLog = null;
15
+ const findLogRecursively = (dir) => {
16
+ if (!fs.existsSync(dir)) return;
17
+ const files = fs.readdirSync(dir);
18
+ for (const file of files) {
19
+ const fullPath = path.join(dir, file);
20
+ if (fs.statSync(fullPath).isDirectory()) {
21
+ findLogRecursively(fullPath);
22
+ } else if (file === "arcality-history.log") {
23
+ // Guardamos el primero que encontremos, asumiendo que es el contexto activo
24
+ if (!centralLog) centralLog = fullPath;
25
+ }
26
+ }
27
+ };
28
+
29
+ findLogRecursively(rootOutDir);
13
30
 
14
31
  console.log(chalk.cyan(`\n🔍 Arcality Logs Viewer`));
15
32
  console.log(chalk.gray(`=========================\n`));
16
33
 
17
- if (!fs.existsSync(centralLog)) {
34
+ if (!centralLog || !fs.existsSync(centralLog)) {
18
35
  console.log(chalk.yellow(`No se encontró un historial de logs en este proyecto.`));
19
- console.log(chalk.gray(`(Directorio actual: ${contextDir})\n`));
36
+ console.log(chalk.gray(`(Directorio buscado: ${rootOutDir})\n`));
20
37
  console.log(`Ejecuta algunas misiones de prueba primero usando el comando arcality.\n`);
21
38
  process.exit(0);
22
39
  }
@@ -850,7 +850,8 @@ async function main() {
850
850
  const missionsDir = process.env.MISSIONS_DIR;
851
851
  const yamlPathMissions = path.join(missionsDir, `${safeName}.yaml`);
852
852
 
853
- let yamlData = `name: "${name}"\nprompt: "${prompt}"\nproject: "${projectConfig?.project?.name || 'Default'}"\ncreated_at: "${new Date().toISOString()}"\n`;
853
+ const safeProjectName = projectConfig?.project?.name || 'Default';
854
+ let yamlData = `name: ${JSON.stringify(name)}\nprompt: ${JSON.stringify(prompt)}\nproject: ${JSON.stringify(safeProjectName)}\ncreated_at: ${JSON.stringify(new Date().toISOString())}\n`;
854
855
 
855
856
  const smartYamlPath = path.join(process.env.CONTEXT_DIR, 'last-mission-smart.yaml');
856
857
  if (fs.existsSync(smartYamlPath)) {
package/scripts/init.mjs CHANGED
@@ -185,21 +185,31 @@ async function main() {
185
185
  });
186
186
  if (isCancel(baseUrl)) { cancel('Setup cancelled.'); process.exit(0); }
187
187
 
188
- const username = await text({
189
- message: chalk.cyan('👤 Login username / email:'),
190
- validate: (v) => {
191
- if (!v || !v.trim()) return 'Username is required.';
192
- },
188
+ const hasLogin = await confirm({
189
+ message: chalk.cyan('🔐 Does your application require a Login/Authentication?'),
193
190
  });
194
- if (isCancel(username)) { cancel('Setup cancelled.'); process.exit(0); }
191
+ if (isCancel(hasLogin)) { cancel('Setup cancelled.'); process.exit(0); }
195
192
 
196
- const password = await passwordPrompt({
197
- message: chalk.cyan('🔒 Login password:'),
198
- validate: (v) => {
199
- if (!v || !v.length) return 'Password is required.';
200
- },
201
- });
202
- if (isCancel(password)) { cancel('Setup cancelled.'); process.exit(0); }
193
+ let username = '';
194
+ let password = '';
195
+
196
+ if (hasLogin) {
197
+ username = await text({
198
+ message: chalk.cyan('👤 Login username / email:'),
199
+ validate: (v) => {
200
+ if (!v || !v.trim()) return 'Username is required.';
201
+ },
202
+ });
203
+ if (isCancel(username)) { cancel('Setup cancelled.'); process.exit(0); }
204
+
205
+ password = await passwordPrompt({
206
+ message: chalk.cyan('🔒 Login password:'),
207
+ validate: (v) => {
208
+ if (!v || !v.length) return 'Password is required.';
209
+ },
210
+ });
211
+ if (isCancel(password)) { cancel('Setup cancelled.'); process.exit(0); }
212
+ }
203
213
 
204
214
  // ── Step 4: Detect framework ──
205
215
  const frameworkDetected = detectFramework(projectRoot);
@@ -271,6 +281,39 @@ async function main() {
271
281
 
272
282
  saveProjectConfig(config, projectRoot);
273
283
 
284
+ // ── Step 7: Custom QA Context ──
285
+ const wantsContext = await confirm({
286
+ message: chalk.cyan('📄 Do you want to generate a local QA Context file (.arcality/qa-context.md) to teach the AI specific business rules?'),
287
+ });
288
+
289
+ if (!isCancel(wantsContext) && wantsContext) {
290
+ const arcalityDir = path.join(projectRoot, '.arcality');
291
+ if (!fs.existsSync(arcalityDir)) {
292
+ fs.mkdirSync(arcalityDir, { recursive: true });
293
+ }
294
+
295
+ const qaContextPath = path.join(arcalityDir, 'qa-context.md');
296
+ const qaTemplate = `# 🧠 Arcality QA Context
297
+ Este archivo es leído automáticamente por tu Agente Arcality antes de cada misión.
298
+ Siéntete libre de borrar, modificar o agregar información para enseñarle a la IA las reglas exclusivas de tu sitio web.
299
+
300
+ ## 💼 Reglas Generales del Dominio
301
+ > (Ejemplo: Formatos de documentos, restricciones, etc.)
302
+ -
303
+
304
+ ## 🧭 Navegación y UI Especial
305
+ > (Ejemplo: Tiempos de espera, clics dobles necesarios, etc.)
306
+ -
307
+
308
+ ## 🛑 Casos Prohibidos y Anti-patrones
309
+ > (Cosas que el agente NUNCA debe intentar)
310
+ -
311
+ `;
312
+ if (!fs.existsSync(qaContextPath)) {
313
+ fs.writeFileSync(qaContextPath, qaTemplate);
314
+ }
315
+ }
316
+
274
317
  // ── Summary ──
275
318
  note(
276
319
  chalk.green('✅ arcality.config created successfully!') + '\n\n' +
@@ -1130,8 +1130,9 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
1130
1130
  const fs3 = require("fs");
1131
1131
  const path3 = require("path");
1132
1132
  const memoryFile = path3.join(this.contextDir, "memoria-agente.json");
1133
- if (!fs3.existsSync(memoryFile))
1133
+ if (!fs3.existsSync(memoryFile)) {
1134
1134
  return null;
1135
+ }
1135
1136
  const memories = JSON.parse(fs3.readFileSync(memoryFile, "utf8"));
1136
1137
  const state = await this.getPageState();
1137
1138
  const currentUrl = this.page.url();
@@ -1165,11 +1166,13 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
1165
1166
  const matchingRun = memories.filter((m) => m.success && m.steps_data && m.steps_data[historyLength]).reverse().find((m) => {
1166
1167
  const memPrompt = (m.prompt || "").toLowerCase();
1167
1168
  const currentPrompt = prompt.toLowerCase();
1168
- const words = currentPrompt.split(" ").filter((w) => w.length > 3);
1169
+ const stripDynamic = (s) => s.replace(/\d{4,}/g, "").replace(/[A-Z_]{3,}\d+/g, "").replace(/\s+/g, " ").trim();
1170
+ const words = stripDynamic(currentPrompt).split(" ").filter((w) => w.length > 3);
1169
1171
  if (words.length === 0)
1170
1172
  return false;
1171
- const matches = words.filter((w) => memPrompt.includes(w)).length;
1172
- if (matches / words.length < 0.7)
1173
+ const matches = words.filter((w) => stripDynamic(memPrompt).includes(w)).length;
1174
+ const matchRatio = matches / words.length;
1175
+ if (matchRatio < 0.6)
1173
1176
  return false;
1174
1177
  const memVerbs = ["elimina", "borra", "quita", "delete", "remove", "crea", "nueva", "a\xF1ade", "add", "create", "new", "edita", "modifica", "cambia", "edit", "update"].filter((v) => memPrompt.includes(v));
1175
1178
  const hasSharedVerb = currentVerbs.some((cv) => memVerbs.includes(cv));
@@ -1188,38 +1191,32 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
1188
1191
  const memStep = m.steps_data[historyLength];
1189
1192
  if (!memStep)
1190
1193
  return false;
1191
- const normalizeUrl = (u) => u.replace(/\/[a-z]{2}-[A-Z]{2}\//g, "/{locale}/").replace(/\/[a-z]{2}\//g, "/{lang}/");
1192
- const memUrl = normalizeUrl((memStep.url || "").split("?")[0]);
1193
- const currUrl = normalizeUrl(currentUrl.split("?")[0]);
1194
- if (memUrl !== currUrl)
1195
- return false;
1194
+ const normalizeUrl = (u) => u.replace(/\/[a-z]{2}-[A-Z]{2}\//g, "/{locale}/").replace(/\/[a-z]{2}\//g, "/{lang}/").replace(/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g, "/{uuid}").replace(/\/\d+/g, "/{id}");
1195
+ const memUrl = normalizeUrl((memStep.urlBeforeAction || memStep.url || "").split("?")[0]);
1196
+ const currUrl = normalizeUrl(this.page.url().split("?")[0]);
1197
+ const sameUrlContext = currUrl === memUrl || memUrl.includes("/new") && currUrl.includes("/new") || memUrl.includes("/edit") && currUrl.includes("/edit") || memUrl.includes("/create") && currUrl.includes("/create");
1198
+ return sameUrlContext;
1199
+ });
1200
+ if (matchingRun) {
1201
+ const memStep = matchingRun.steps_data[historyLength];
1196
1202
  const foundComponent = state.components.find((c) => {
1197
1203
  const cleanMemName = (memStep.componentName || "").replace(/\[.*?\]/g, "").replace(/\(.*?\)/g, "").trim().toLowerCase();
1198
1204
  const cleanCurrName = (c.name || "").replace(/\[.*?\]/g, "").replace(/\(.*?\)/g, "").trim().toLowerCase();
1199
- return c.selector === memStep.selector || cleanMemName !== "" && (cleanMemName === cleanCurrName || cleanCurrName.includes(cleanMemName));
1205
+ return c.selector === memStep.action_data?.selector || cleanMemName !== "" && (cleanMemName === cleanCurrName || cleanCurrName.includes(cleanMemName));
1200
1206
  });
1201
- return !!foundComponent;
1202
- });
1203
- if (matchingRun) {
1204
- const memStep = matchingRun.steps_data[historyLength];
1205
- const memCompName = memStep.componentName || "";
1206
- if (memCompName.length > 80) {
1207
- console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Gu\xEDa rechazada: Nombre de componente demasiado largo (${memCompName.length} chars). Delegando a IA.`);
1207
+ if (!foundComponent && memStep.action_data?.action !== "wait") {
1208
+ console.log(`>>ARCALITY_STATUS>> \u23F3 Componente target no visible ("${memStep.componentName}"). Forzando espera de transici\xF3n DOM...`);
1209
+ console.log(`>>ARCALITY_STATUS>> \u{1F50D} Descartado (Componente visual no encontrado: ${memStep.componentName})`);
1208
1210
  return null;
1209
1211
  }
1210
- if (memCompName.includes("[INFERRED]")) {
1211
- console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Gu\xEDa rechazada: Componente con nombre inferido "${memCompName}". Delegando a IA.`);
1212
+ const memCompName = memStep.componentName || "";
1213
+ if (memCompName.length > 80) {
1214
+ console.log(`>>ARCALITY_STATUS>> \u{1F50D} Descartado (Mensaje enorme o nombre inconsistente: ${memCompName.substring(0, 20)}...)`);
1212
1215
  return null;
1213
1216
  }
1214
1217
  if (!memCompName || memCompName.length < 3) {
1215
- console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Gu\xEDa rechazada: Componente sin nombre claro. Delegando a IA.`);
1216
1218
  return null;
1217
1219
  }
1218
- const foundComponent = state.components.find((c) => {
1219
- const cleanMemName = (memStep.componentName || "").replace(/\[.*?\]/g, "").replace(/\(.*?\)/g, "").trim().toLowerCase();
1220
- const cleanCurrName = (c.name || "").replace(/\[.*?\]/g, "").replace(/\(.*?\)/g, "").trim().toLowerCase();
1221
- return c.selector === memStep.selector || cleanMemName !== "" && (cleanMemName === cleanCurrName || cleanCurrName.includes(cleanMemName));
1222
- });
1223
1220
  if (foundComponent) {
1224
1221
  return {
1225
1222
  thought: `[GU\xCDA DE \xC9XITO] Reutilizando paso maestro aprendido: ${memStep.action_data.action} en ${foundComponent.name}`,
@@ -1375,12 +1372,20 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
1375
1372
  } catch (e) {
1376
1373
  console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error en hook de contexto: ${e.message}`);
1377
1374
  }
1378
- let autoskillsContext = "";
1375
+ let customUserContext = "";
1379
1376
  try {
1377
+ const fs3 = require("fs");
1380
1378
  const path3 = require("path");
1381
- const toolsRoot = process.env.ARCALITY_ROOT || path3.join(__dirname, "..", "..");
1382
- const { detectAndFetchEphemeralSkills } = require(path3.join(toolsRoot, "src", "services", "autoskillsService"));
1383
- autoskillsContext = await detectAndFetchEphemeralSkills(process.cwd());
1379
+ const customContextPath = path3.join(process.cwd(), ".arcality", "qa-context.md");
1380
+ if (fs3.existsSync(customContextPath)) {
1381
+ const content = fs3.readFileSync(customContextPath, "utf8");
1382
+ customUserContext = `
1383
+ <CUSTOMER_BUSINESS_RULES>
1384
+ El Ing. de QA o Cliente final ha provisto las siguientes reglas estrictas para este negocio/dominio. DEBES priorizar esta informaci\xF3n sobre todas tus skills base:
1385
+ ${content}
1386
+ </CUSTOMER_BUSINESS_RULES>
1387
+ `;
1388
+ }
1384
1389
  } catch (e) {
1385
1390
  }
1386
1391
  const systemPromptBlocks = [
@@ -1394,7 +1399,7 @@ ${prompt}
1394
1399
 
1395
1400
  ${projectInfo}
1396
1401
  ${skillsContext}
1397
- ${autoskillsContext}
1402
+ ${customUserContext}
1398
1403
  ${memoryContext}
1399
1404
  ${credentialsContext}
1400
1405
  ${memoryBackendContext}`
@@ -1421,7 +1426,11 @@ Every turn, you MUST follow these 4 phases:
1421
1426
  ## Phase 3: ACT
1422
1427
  - Execute your planned actions using \`perform_ui_actions\`.
1423
1428
  - Group multiple related actions in one call (e.g., click+fill for 3 fields = 6 actions in one call).
1424
- - For dropdowns/selects: Click to open \u2192 WAIT for next turn to see options \u2192 Select.
1429
+ - For dropdowns/selects (MANDATORY PROTOCOL \u2014 complete in 2 turns MAX):
1430
+ 1. **Turn A**: Click the dropdown trigger to open it, then WAIT to see options.
1431
+ 2. **Turn B**: Click the desired option AND immediately click the next logical button (e.g., \`Siguiente\`, \`Guardar\`) in the SAME action call. DO NOT wait another turn just to verify.
1432
+ - If the field still shows a validation error after you clicked Siguiente: use \`inspect_element_details\` on the field ONCE. If selector works, try clicking the field and pressing Tab. Then click Siguiente again.
1433
+ - **STRICTLY FORBIDDEN**: Sending Escape repeatedly without clicking Siguiente. If you sent Escape once and the dropdown is still visible, click OUTSIDE the dropdown or click Siguiente directly \u2014 the form validation will confirm the selection.
1425
1434
  - For menus: Click menu icon \u2192 WAIT for next turn \u2192 Click option.
1426
1435
 
1427
1436
  ## Phase 4: VERIFY
@@ -1447,7 +1456,8 @@ These are NOT optional. You MUST use these tools in these situations:
1447
1456
  | Modal/popup blocking page | \`send_keyboard_event\` (Escape) | To close the overlay |
1448
1457
  | Time/Date/Color picker field | \`interact_native_control\` | INSTEAD of fill \u2014 native inputs need special handling |
1449
1458
  | Element not found on screen | \`scroll_page\` | To reveal off-screen content |
1450
- | Need to close dropdown/menu | \`send_keyboard_event\` (Escape) | After inspecting options |
1459
+ | Need to close dropdown/menu | \`send_keyboard_event\` (Escape) | IMMEDIATELY after selecting any option \u2014 MANDATORY |
1460
+ | Dropdown selected but still visually open | \`send_keyboard_event\` (Tab) | If Escape did not close it \u2014 force field to lose focus |
1451
1461
  | Navigate between form sections | \`send_keyboard_event\` (Tab) | To move focus between fields |
1452
1462
 
1453
1463
  # CRITICAL RULES
@@ -1456,7 +1466,12 @@ These are NOT optional. You MUST use these tools in these situations:
1456
1466
 
1457
1467
  3. **CURRENT STATE > MEMORY**: What you see in CURRENT COMPONENTS is the truth. SUCCESS MEMORY is a guide, not gospel.
1458
1468
  4. **PAGE TRANSITIONS**: If URL contains '/edit' or you see a 'Save' button, you ARE in the edit view. Don't go back.
1459
- 5. **MENUS & DROPDOWNS**: After clicking a toggle/menu, WAIT for the next turn. Never click the same menu icon twice in a row.
1469
+ 5. **MENUS & DROPDOWNS**:
1470
+ - After clicking a toggle/menu, WAIT for the next turn to see options. Never click the same menu icon twice in a row.
1471
+ - **DROPDOWN COMPLETION RULE (CRITICAL)**: After clicking an option in a dropdown ('[li]', '[option]'), IF the form requires you to fill MORE subsequent fields in the same section, STOP YOUR TURN AFTER SELECTING the option! Do NOT group the select action with filling the next fields. React/Next.js will re-render the DOM, detaching your cached 'idx' references causing 10000ms timeouts. Wait for the next turn to fill the next fields.
1472
+ - If the dropdown selection was the LAST step in the form, you CAN immediately click Siguiente/Guardar in the same turn.
1473
+ - **ESCAPE RULE**: You may send Escape ONCE after selecting to help close the dropdown. If you already sent Escape and the form still shows the same state, DO NOT send Escape again. Click \`Siguiente\` or \`Guardar\` directly to proceed.
1474
+ - **VALIDATION ERRORS**: If Siguiente reports a validation error on a dropdown field, THEN use \`inspect_element_details\` to understand the field's selector and interact with it precisely. Not before.
1460
1475
  6. **ERROR HANDLING**: If you see '\u{1F6D1} [ERROR_CR\xCDTICO]', STOP everything. Analyze the error, fix the data, then retry.
1461
1476
  6b. **DUPLICATE / REPEATED VALUE ERROR \u2014 MANDATORY PROTOCOL**:
1462
1477
  - If HISTORY contains a line with "ERROR VISIBLE EN PANTALLA" or "ERROR POST-GUARDADO" with words like "repetido", "duplicado", "ya existe", or "already exists":
@@ -2452,7 +2467,7 @@ function captureValidationRule(errorMessage, context) {
2452
2467
  const urlHistory = [];
2453
2468
  const stepsData = [];
2454
2469
  const successKeywords = /exitosamente|creado correctamente|guardado correctamente|misión cumplida|registered successfully|created successfully|saved successfully|operación exitosa|registro guardado|successfully|correctly/i;
2455
- const failureKeywords = /\berror\b|falló|failed|no se puede|cannot|unable|ya existe|already exists|inválido|invalid|incorrecto|incorrect|ligado|linked|depende|depends|duplicado|duplicate|repetido|repeated|reintentar|retry|missing|required|obligatorio|bad request|400|500/i;
2470
+ const failureKeywords = /\berror\b|falló|failed|no se puede|cannot|unable|ya existe|already exists|existente|exist|inválido|invalid|incorrecto|incorrect|ligado|linked|depende|depends|duplicado|duplicate|repetido|repeated|conflict|reintentar|retry|missing|required|obligatorio|bad request|400|500/i;
2456
2471
  let aiMarkedSuccess = false;
2457
2472
  let hasCriticalError = false;
2458
2473
  let resultsSaved = false;
@@ -2684,14 +2699,25 @@ function captureValidationRule(errorMessage, context) {
2684
2699
  }
2685
2700
  }
2686
2701
  }
2687
- const rawBody = await page.innerText("body").catch(() => "");
2688
- const bodyTxt = rawBody.toLowerCase();
2689
- const hasVisibleError = failureKeywords.test(bodyTxt);
2702
+ let hasVisibleError = false;
2703
+ try {
2704
+ const errorEls = await page.locator('.toast, .alert, [role="alert"], [role="status"], .snackbar, .notification, .error-message').all();
2705
+ for (const el of errorEls) {
2706
+ if (await el.isVisible().catch(() => false)) {
2707
+ const errTxt = await el.innerText().catch(() => "");
2708
+ if (errTxt && failureKeywords.test(errTxt.toLowerCase())) {
2709
+ hasVisibleError = true;
2710
+ break;
2711
+ }
2712
+ }
2713
+ }
2714
+ } catch {
2715
+ }
2690
2716
  let response = null;
2691
2717
  if (!hasVisibleError) {
2692
2718
  response = await agent.getActionFromGuia(prompt, stepsData.length);
2693
2719
  } else {
2694
- console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error detectado en pantalla. Saltando Gu\xEDa para priorizar correcci\xF3n.`);
2720
+ console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error real detectado en pantalla. Saltando Gu\xEDa.`);
2695
2721
  if (stepsData.length > 0) {
2696
2722
  stepsDataBackup = [...stepsData];
2697
2723
  stepsData.length = 0;
@@ -2789,7 +2815,8 @@ function captureValidationRule(errorMessage, context) {
2789
2815
  }
2790
2816
  if (response.actions && response.actions.length > 0) {
2791
2817
  for (const step of response.actions) {
2792
- console.log(`>>ARCALITY_STATUS>> \u{1F3AF} Acci\xF3n: ${step.action} en "${step.description || step.selector}"`);
2818
+ console.log(`
2819
+ \u{1F449} Acci\xF3n: ${step.action} en "${step.description || step.selector}" ${step.value ? `(Valor: "${step.value}")` : ""}`);
2793
2820
  try {
2794
2821
  const frame = step.frameIdx !== void 0 && page.frames()[step.frameIdx] ? page.frames()[step.frameIdx] : page;
2795
2822
  const loc = step.selector ? frame.locator(step.selector).first() : null;
@@ -3060,6 +3087,7 @@ function captureValidationRule(errorMessage, context) {
3060
3087
  stepsDataBackup = [...stepsData];
3061
3088
  stepsData.length = 0;
3062
3089
  }
3090
+ history.push(`Turno ${stepCount}: \u{1F6D1} [ERROR_CR\xCDTICO] Feedback de validaci\xF3n/error visible en UI: "${rawTxt.trim()}"`);
3063
3091
  } else if (successKeywords.test(txt)) {
3064
3092
  if (txt !== lastSeenSuccessToast) {
3065
3093
  console.log(`>>ARCALITY_STATUS>> \u2728 \xC9xito visual detectado: "${txt.substring(0, 50)}..."`);
@@ -1,61 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
-
4
- // Mapeo curado de tecnologías clave a sus skills de pruebas/qa/patrones de autoskills.sh
5
- // Son recursos muy ligeros en formato Markdown
6
- const SKILLS_MAP: Record<string, string> = {
7
- 'next': 'https://skills.sh/vercel-labs/next-skills/next-best-practices/raw',
8
- 'vue': 'https://skills.sh/antfu/skills/vue-best-practices/raw',
9
- 'tailwindcss': 'https://skills.sh/giuseppe-trisciuoglio/developer-kit/tailwind-css-patterns/raw',
10
- 'react': 'https://skills.sh/vercel-labs/agent-skills/vercel-react-best-practices/raw',
11
- 'angular': 'https://skills.sh/angular/skills/angular-developer/raw',
12
- 'nuxt': 'https://skills.sh/antfu/skills/nuxt/raw',
13
- 'svelte': 'https://skills.sh/ejirocodes/agent-skills/svelte5-best-practices/raw',
14
- 'astro': 'https://skills.sh/astrolicious/agent-skills/astro/raw',
15
- 'prisma': 'https://skills.sh/prisma/skills/prisma-postgres/raw'
16
- };
17
-
18
- export async function detectAndFetchEphemeralSkills(projectRoot: string): Promise<string> {
19
- const pkgPath = path.join(projectRoot, 'package.json');
20
- if (!fs.existsSync(pkgPath)) return '';
21
-
22
- try {
23
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
24
- const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
25
-
26
- const matchedUrls: string[] = [];
27
- const matchedTechs: string[] = [];
28
-
29
- for (const [lib, url] of Object.entries(SKILLS_MAP)) {
30
- // Buscamos coincidencia parcial (ej. si usan @angular/core, next, react-dom)
31
- const hasLib = Object.keys(deps).some(dep => dep === lib || dep.includes(`/${lib}`));
32
- if (hasLib) {
33
- matchedUrls.push(url);
34
- matchedTechs.push(lib);
35
- }
36
- }
37
-
38
- if (matchedUrls.length === 0) return '';
39
-
40
- console.log(`>>ARCALITY_STATUS>> ⚡ Stack detectado (${matchedTechs.join(', ')}). QA Skills inyectadas en memoria.`);
41
-
42
- // Fetch strings in parallel with simple timeout
43
- const skillsPromises = matchedUrls.map(url => {
44
- const controller = new AbortController();
45
- const id = setTimeout(() => controller.abort(), 2000); // Max 2s per request
46
- return fetch(url, { signal: controller.signal })
47
- .then(r => r.ok ? r.text() : '')
48
- .catch(() => '')
49
- .finally(() => clearTimeout(id));
50
- });
51
-
52
- const skillsTexts = await Promise.all(skillsPromises);
53
-
54
- const validTexts = skillsTexts.filter(Boolean);
55
- if (validTexts.length === 0) return '';
56
-
57
- return `\n<TECH_STACK_CONTEXT>\nEl proyecto objetivo utiliza las siguientes tecnologías. Usa estas directrices técnicas para entender el DOM, rutas y posibles bugs visuales:\n\n${validTexts.join('\n\n---\n\n')}\n</TECH_STACK_CONTEXT>\n`;
58
- } catch {
59
- return ''; // Fails silently
60
- }
61
- }