@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.
- package/.agent/skills/form-expert.md +4 -1
- package/README.md +14 -0
- package/package.json +1 -1
- package/scripts/arcality-logs.mjs +21 -4
- package/scripts/gen-and-run.mjs +2 -1
- package/scripts/init.mjs +56 -13
- package/tests/_helpers/agentic-runner.bundle.spec.js +67 -39
- package/src/services/autoskillsService.ts +0 -61
|
@@ -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/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
|
@@ -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' +
|
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
1194
|
-
|
|
1195
|
-
|
|
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
|
-
|
|
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.`);
|
|
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
|
-
|
|
1211
|
-
|
|
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
|
|
1375
|
+
let customUserContext = "";
|
|
1379
1376
|
try {
|
|
1377
|
+
const fs3 = require("fs");
|
|
1380
1378
|
const path3 = require("path");
|
|
1381
|
-
const
|
|
1382
|
-
|
|
1383
|
-
|
|
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
|
-
${
|
|
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
|
|
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) |
|
|
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**:
|
|
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
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
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
|
|
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(
|
|
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
|
-
}
|