@arcadialdev/arcality 2.4.34 → 2.4.36

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcadialdev/arcality",
3
- "version": "2.4.34",
3
+ "version": "2.4.36",
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' +
@@ -956,6 +956,48 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
956
956
  const idx = startIdx + localCount++;
957
957
  htmlEl.setAttribute("agent-idx", idx.toString());
958
958
  let name = text || htmlEl.getAttribute("aria-label") || htmlEl.getAttribute("title") || htmlEl.getAttribute("placeholder");
959
+ if (!name && (tag === "input" || tag === "select" || htmlEl.getAttribute("role") === "combobox")) {
960
+ const labelledBy = htmlEl.getAttribute("aria-labelledby");
961
+ if (labelledBy) {
962
+ const labelEl = document.getElementById(labelledBy);
963
+ if (labelEl)
964
+ name = labelEl.innerText.trim();
965
+ }
966
+ if (!name && htmlEl.id) {
967
+ const forLabel = document.querySelector(`label[for="${htmlEl.id}"]`);
968
+ if (forLabel)
969
+ name = forLabel.innerText.trim();
970
+ }
971
+ if (!name) {
972
+ const parentLabel = htmlEl.closest("label");
973
+ if (parentLabel)
974
+ name = parentLabel.innerText.replace(htmlEl.textContent || "", "").trim();
975
+ }
976
+ if (!name) {
977
+ let prev = htmlEl.previousElementSibling;
978
+ let tries = 0;
979
+ while (prev && tries < 3) {
980
+ const prevText = (prev.innerText || "").trim();
981
+ if (prevText && prevText.length > 1 && prevText.length < 60) {
982
+ name = prevText;
983
+ break;
984
+ }
985
+ prev = prev.previousElementSibling;
986
+ tries++;
987
+ }
988
+ }
989
+ if (!name) {
990
+ const container = htmlEl.closest("div, fieldset, section");
991
+ if (container) {
992
+ const labelInContainer = container.querySelector('label, [class*="label"], [class*="placeholder"]');
993
+ if (labelInContainer) {
994
+ const labelText = (labelInContainer.innerText || "").trim();
995
+ if (labelText && labelText.length < 60)
996
+ name = labelText;
997
+ }
998
+ }
999
+ }
1000
+ }
959
1001
  if (!name) {
960
1002
  const iconMatch = htmlEl.outerHTML.match(/fa-([a-z0-9-]+)|md-([a-z0-9-]+)|bi-([a-z0-9-]+)/i);
961
1003
  if (iconMatch) {
@@ -1130,8 +1172,9 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
1130
1172
  const fs3 = require("fs");
1131
1173
  const path3 = require("path");
1132
1174
  const memoryFile = path3.join(this.contextDir, "memoria-agente.json");
1133
- if (!fs3.existsSync(memoryFile))
1175
+ if (!fs3.existsSync(memoryFile)) {
1134
1176
  return null;
1177
+ }
1135
1178
  const memories = JSON.parse(fs3.readFileSync(memoryFile, "utf8"));
1136
1179
  const state = await this.getPageState();
1137
1180
  const currentUrl = this.page.url();
@@ -1165,11 +1208,13 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
1165
1208
  const matchingRun = memories.filter((m) => m.success && m.steps_data && m.steps_data[historyLength]).reverse().find((m) => {
1166
1209
  const memPrompt = (m.prompt || "").toLowerCase();
1167
1210
  const currentPrompt = prompt.toLowerCase();
1168
- const words = currentPrompt.split(" ").filter((w) => w.length > 3);
1211
+ const stripDynamic = (s) => s.replace(/\d{4,}/g, "").replace(/[A-Z_]{3,}\d+/g, "").replace(/\s+/g, " ").trim();
1212
+ const words = stripDynamic(currentPrompt).split(" ").filter((w) => w.length > 3);
1169
1213
  if (words.length === 0)
1170
1214
  return false;
1171
- const matches = words.filter((w) => memPrompt.includes(w)).length;
1172
- if (matches / words.length < 0.7)
1215
+ const matches = words.filter((w) => stripDynamic(memPrompt).includes(w)).length;
1216
+ const matchRatio = matches / words.length;
1217
+ if (matchRatio < 0.6)
1173
1218
  return false;
1174
1219
  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
1220
  const hasSharedVerb = currentVerbs.some((cv) => memVerbs.includes(cv));
@@ -1188,38 +1233,32 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
1188
1233
  const memStep = m.steps_data[historyLength];
1189
1234
  if (!memStep)
1190
1235
  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;
1236
+ 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}");
1237
+ const memUrl = normalizeUrl((memStep.urlBeforeAction || memStep.url || "").split("?")[0]);
1238
+ const currUrl = normalizeUrl(this.page.url().split("?")[0]);
1239
+ const sameUrlContext = currUrl === memUrl || memUrl.includes("/new") && currUrl.includes("/new") || memUrl.includes("/edit") && currUrl.includes("/edit") || memUrl.includes("/create") && currUrl.includes("/create");
1240
+ return sameUrlContext;
1241
+ });
1242
+ if (matchingRun) {
1243
+ const memStep = matchingRun.steps_data[historyLength];
1196
1244
  const foundComponent = state.components.find((c) => {
1197
1245
  const cleanMemName = (memStep.componentName || "").replace(/\[.*?\]/g, "").replace(/\(.*?\)/g, "").trim().toLowerCase();
1198
1246
  const cleanCurrName = (c.name || "").replace(/\[.*?\]/g, "").replace(/\(.*?\)/g, "").trim().toLowerCase();
1199
- return c.selector === memStep.selector || cleanMemName !== "" && (cleanMemName === cleanCurrName || cleanCurrName.includes(cleanMemName));
1247
+ return c.selector === memStep.action_data?.selector || cleanMemName !== "" && (cleanMemName === cleanCurrName || cleanCurrName.includes(cleanMemName));
1200
1248
  });
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.`);
1249
+ if (!foundComponent && memStep.action_data?.action !== "wait") {
1250
+ console.log(`>>ARCALITY_STATUS>> \u23F3 Componente target no visible ("${memStep.componentName}"). Forzando espera de transici\xF3n DOM...`);
1251
+ console.log(`>>ARCALITY_STATUS>> \u{1F50D} Descartado (Componente visual no encontrado: ${memStep.componentName})`);
1208
1252
  return null;
1209
1253
  }
1210
- if (memCompName.includes("[INFERRED]")) {
1211
- console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Gu\xEDa rechazada: Componente con nombre inferido "${memCompName}". Delegando a IA.`);
1254
+ const memCompName = memStep.componentName || "";
1255
+ if (memCompName.length > 80) {
1256
+ console.log(`>>ARCALITY_STATUS>> \u{1F50D} Descartado (Mensaje enorme o nombre inconsistente: ${memCompName.substring(0, 20)}...)`);
1212
1257
  return null;
1213
1258
  }
1214
1259
  if (!memCompName || memCompName.length < 3) {
1215
- console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Gu\xEDa rechazada: Componente sin nombre claro. Delegando a IA.`);
1216
1260
  return null;
1217
1261
  }
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
1262
  if (foundComponent) {
1224
1263
  return {
1225
1264
  thought: `[GU\xCDA DE \xC9XITO] Reutilizando paso maestro aprendido: ${memStep.action_data.action} en ${foundComponent.name}`,
@@ -1375,12 +1414,20 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
1375
1414
  } catch (e) {
1376
1415
  console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error en hook de contexto: ${e.message}`);
1377
1416
  }
1378
- let autoskillsContext = "";
1417
+ let customUserContext = "";
1379
1418
  try {
1419
+ const fs3 = require("fs");
1380
1420
  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());
1421
+ const customContextPath = path3.join(process.cwd(), ".arcality", "qa-context.md");
1422
+ if (fs3.existsSync(customContextPath)) {
1423
+ const content = fs3.readFileSync(customContextPath, "utf8");
1424
+ customUserContext = `
1425
+ <CUSTOMER_BUSINESS_RULES>
1426
+ 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:
1427
+ ${content}
1428
+ </CUSTOMER_BUSINESS_RULES>
1429
+ `;
1430
+ }
1384
1431
  } catch (e) {
1385
1432
  }
1386
1433
  const systemPromptBlocks = [
@@ -1394,7 +1441,7 @@ ${prompt}
1394
1441
 
1395
1442
  ${projectInfo}
1396
1443
  ${skillsContext}
1397
- ${autoskillsContext}
1444
+ ${customUserContext}
1398
1445
  ${memoryContext}
1399
1446
  ${credentialsContext}
1400
1447
  ${memoryBackendContext}`
@@ -1421,7 +1468,11 @@ Every turn, you MUST follow these 4 phases:
1421
1468
  ## Phase 3: ACT
1422
1469
  - Execute your planned actions using \`perform_ui_actions\`.
1423
1470
  - 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.
1471
+ - For dropdowns/selects (MANDATORY PROTOCOL \u2014 complete in 2 turns MAX):
1472
+ 1. **Turn A**: Click the dropdown trigger to open it, then WAIT to see options.
1473
+ 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.
1474
+ - 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.
1475
+ - **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
1476
  - For menus: Click menu icon \u2192 WAIT for next turn \u2192 Click option.
1426
1477
 
1427
1478
  ## Phase 4: VERIFY
@@ -1447,7 +1498,8 @@ These are NOT optional. You MUST use these tools in these situations:
1447
1498
  | Modal/popup blocking page | \`send_keyboard_event\` (Escape) | To close the overlay |
1448
1499
  | Time/Date/Color picker field | \`interact_native_control\` | INSTEAD of fill \u2014 native inputs need special handling |
1449
1500
  | 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 |
1501
+ | Need to close dropdown/menu | \`send_keyboard_event\` (Escape) | IMMEDIATELY after selecting any option \u2014 MANDATORY |
1502
+ | Dropdown selected but still visually open | \`send_keyboard_event\` (Tab) | If Escape did not close it \u2014 force field to lose focus |
1451
1503
  | Navigate between form sections | \`send_keyboard_event\` (Tab) | To move focus between fields |
1452
1504
 
1453
1505
  # CRITICAL RULES
@@ -1456,7 +1508,12 @@ These are NOT optional. You MUST use these tools in these situations:
1456
1508
 
1457
1509
  3. **CURRENT STATE > MEMORY**: What you see in CURRENT COMPONENTS is the truth. SUCCESS MEMORY is a guide, not gospel.
1458
1510
  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.
1511
+ 5. **MENUS & DROPDOWNS**:
1512
+ - After clicking a toggle/menu, WAIT for the next turn to see options. Never click the same menu icon twice in a row.
1513
+ - **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.
1514
+ - If the dropdown selection was the LAST step in the form, you CAN immediately click Siguiente/Guardar in the same turn.
1515
+ - **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.
1516
+ - **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
1517
  6. **ERROR HANDLING**: If you see '\u{1F6D1} [ERROR_CR\xCDTICO]', STOP everything. Analyze the error, fix the data, then retry.
1461
1518
  6b. **DUPLICATE / REPEATED VALUE ERROR \u2014 MANDATORY PROTOCOL**:
1462
1519
  - 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 +2509,7 @@ function captureValidationRule(errorMessage, context) {
2452
2509
  const urlHistory = [];
2453
2510
  const stepsData = [];
2454
2511
  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;
2512
+ 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
2513
  let aiMarkedSuccess = false;
2457
2514
  let hasCriticalError = false;
2458
2515
  let resultsSaved = false;
@@ -2684,14 +2741,25 @@ function captureValidationRule(errorMessage, context) {
2684
2741
  }
2685
2742
  }
2686
2743
  }
2687
- const rawBody = await page.innerText("body").catch(() => "");
2688
- const bodyTxt = rawBody.toLowerCase();
2689
- const hasVisibleError = failureKeywords.test(bodyTxt);
2744
+ let hasVisibleError = false;
2745
+ try {
2746
+ const errorEls = await page.locator('.toast, .alert, [role="alert"], [role="status"], .snackbar, .notification, .error-message').all();
2747
+ for (const el of errorEls) {
2748
+ if (await el.isVisible().catch(() => false)) {
2749
+ const errTxt = await el.innerText().catch(() => "");
2750
+ if (errTxt && failureKeywords.test(errTxt.toLowerCase())) {
2751
+ hasVisibleError = true;
2752
+ break;
2753
+ }
2754
+ }
2755
+ }
2756
+ } catch {
2757
+ }
2690
2758
  let response = null;
2691
2759
  if (!hasVisibleError) {
2692
2760
  response = await agent.getActionFromGuia(prompt, stepsData.length);
2693
2761
  } else {
2694
- console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error detectado en pantalla. Saltando Gu\xEDa para priorizar correcci\xF3n.`);
2762
+ console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error real detectado en pantalla. Saltando Gu\xEDa.`);
2695
2763
  if (stepsData.length > 0) {
2696
2764
  stepsDataBackup = [...stepsData];
2697
2765
  stepsData.length = 0;
@@ -2789,7 +2857,8 @@ function captureValidationRule(errorMessage, context) {
2789
2857
  }
2790
2858
  if (response.actions && response.actions.length > 0) {
2791
2859
  for (const step of response.actions) {
2792
- console.log(`>>ARCALITY_STATUS>> \u{1F3AF} Acci\xF3n: ${step.action} en "${step.description || step.selector}"`);
2860
+ console.log(`
2861
+ \u{1F449} Acci\xF3n: ${step.action} en "${step.description || step.selector}" ${step.value ? `(Valor: "${step.value}")` : ""}`);
2793
2862
  try {
2794
2863
  const frame = step.frameIdx !== void 0 && page.frames()[step.frameIdx] ? page.frames()[step.frameIdx] : page;
2795
2864
  const loc = step.selector ? frame.locator(step.selector).first() : null;
@@ -2836,14 +2905,17 @@ function captureValidationRule(errorMessage, context) {
2836
2905
  }
2837
2906
  }
2838
2907
  const lastAction = history[history.length - 1];
2839
- if (lastAction && lastAction.includes(`click en "${desc}"`)) {
2908
+ const isGenericName = !desc || desc === "[input]" || desc === "[button]" || desc === "[select]" || desc === "[div]" || desc.trim().length < 3;
2909
+ if (!isGenericName && lastAction && lastAction.includes(`click en "${desc}"`)) {
2840
2910
  const isDotsTrigger = desc.includes("dots") || desc.includes("vert") || desc.includes("menu") || desc.includes("opciones");
2841
2911
  const sameActionCount = history.filter((h) => h.includes(`click en "${desc}"`)).length;
2842
- const toleranceLimit = allowRepeatedNames ? 3 : 1;
2843
- if (isDotsTrigger && sameActionCount < 2) {
2912
+ const toleranceLimit = allowRepeatedNames ? 4 : 2;
2913
+ if (isDotsTrigger && sameActionCount < 3) {
2844
2914
  console.log(` \u26A0\uFE0F Re-intentando click en men\xFA "${desc}"...`);
2845
2915
  } else if (allowRepeatedNames && sameActionCount < toleranceLimit) {
2846
2916
  console.log(` \u26A0\uFE0F Click repetido permitido por contexto de misi\xF3n en "${desc}"...`);
2917
+ } else if (sameActionCount < toleranceLimit) {
2918
+ console.log(` \u26A0\uFE0F Re-intento ${sameActionCount} en "${desc}"...`);
2847
2919
  } else {
2848
2920
  const bodyText = await page.innerText("body").catch(() => "");
2849
2921
  if (bodyText.match(/ligado|depende|no se puede/i)) {
@@ -2854,9 +2926,9 @@ function captureValidationRule(errorMessage, context) {
2854
2926
  break;
2855
2927
  }
2856
2928
  console.log(`
2857
- \u{1F6D1} [AUTO-STOP] Bloqueando click duplicado en "${desc}". Forzando re-evaluaci\xF3n.`);
2858
- history.push(`Turno ${stepCount}: Bloqueado click repetido en "${desc}" (Evitando bucle)`);
2859
- await page.waitForTimeout(1e3);
2929
+ \u{1F6D1} [AUTO-STOP] Bloqueando click repetido en "${desc}" (${sameActionCount} veces). Forzando re-evaluaci\xF3n.`);
2930
+ history.push(`Turno ${stepCount}: \u{1F6D1} [AUTO-STOP] El click en "${desc}" fue bloqueado despu\xE9s de ${sameActionCount} repeticiones. Usa un IDX diferente o intenta hacer scroll para revelar el elemento. Si es un dropdown que no abre, usa interact_native_control.`);
2931
+ await page.waitForTimeout(800);
2860
2932
  break;
2861
2933
  }
2862
2934
  }
@@ -3060,6 +3132,7 @@ function captureValidationRule(errorMessage, context) {
3060
3132
  stepsDataBackup = [...stepsData];
3061
3133
  stepsData.length = 0;
3062
3134
  }
3135
+ history.push(`Turno ${stepCount}: \u{1F6D1} [ERROR_CR\xCDTICO] Feedback de validaci\xF3n/error visible en UI: "${rawTxt.trim()}"`);
3063
3136
  } else if (successKeywords.test(txt)) {
3064
3137
  if (txt !== lastSeenSuccessToast) {
3065
3138
  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
- }