@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
|
|
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
|
@@ -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
|
|
12
|
-
|
|
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
|
|
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
|
}
|
package/scripts/gen-and-run.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
189
|
-
message: chalk.cyan('
|
|
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(
|
|
191
|
+
if (isCancel(hasLogin)) { cancel('Setup cancelled.'); process.exit(0); }
|
|
195
192
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
1194
|
-
|
|
1195
|
-
|
|
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
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
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
|
-
|
|
1211
|
-
|
|
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
|
|
1417
|
+
let customUserContext = "";
|
|
1379
1418
|
try {
|
|
1419
|
+
const fs3 = require("fs");
|
|
1380
1420
|
const path3 = require("path");
|
|
1381
|
-
const
|
|
1382
|
-
|
|
1383
|
-
|
|
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
|
-
${
|
|
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
|
|
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) |
|
|
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**:
|
|
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
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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 ?
|
|
2843
|
-
if (isDotsTrigger && sameActionCount <
|
|
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
|
|
2858
|
-
history.push(`Turno ${stepCount}:
|
|
2859
|
-
await page.waitForTimeout(
|
|
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
|
-
}
|