@arcadialdev/arcality 4.1.0 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/.agents/skills/db-validation-evidence/SKILL.md +43 -0
  2. package/.agents/skills/db-validation-evidence/agents/openai.yaml +4 -0
  3. package/.agents/skills/db-validation-evidence/references/mission-patterns.md +130 -0
  4. package/.agents/skills/evidence-synthesis-reporting/SKILL.md +31 -0
  5. package/.agents/skills/form-expert/SKILL.md +98 -0
  6. package/.agents/skills/investigation-protocol/SKILL.md +56 -0
  7. package/.agents/skills/mission-generation-reviewer/SKILL.md +25 -0
  8. package/.agents/skills/modal-master/SKILL.md +46 -0
  9. package/.agents/skills/native-control-expert/SKILL.md +74 -0
  10. package/.agents/skills/qa-context-governance/SKILL.md +23 -0
  11. package/.agents/skills/security-evidence-triage/SKILL.md +23 -0
  12. package/.agents/skills/test-data-lifecycle/SKILL.md +24 -0
  13. package/README.md +103 -163
  14. package/bin/arcality.mjs +25 -25
  15. package/package.json +75 -75
  16. package/scripts/edit-config.mjs +843 -0
  17. package/scripts/gen-and-run.mjs +237 -169
  18. package/scripts/generate.mjs +236 -236
  19. package/scripts/init.mjs +47 -47
  20. package/src/configManager.mjs +13 -13
  21. package/src/envSetup.ts +229 -205
  22. package/src/services/codebaseAnalyzer.mjs +59 -59
  23. package/src/services/databaseValidationService.mjs +598 -0
  24. package/src/services/executionEvidenceService.mjs +124 -0
  25. package/src/services/generatedMissionSchema.mjs +76 -76
  26. package/src/services/generatedMissionStore.mjs +117 -117
  27. package/src/services/generationContext.mjs +242 -242
  28. package/src/services/mcpStdioClient.mjs +204 -0
  29. package/src/services/missionGenerator.mjs +329 -329
  30. package/src/services/routeDiscovery.mjs +762 -762
  31. package/tests/_helpers/ArcalityReporter.js +1342 -1255
  32. package/tests/_helpers/agentic-runner.bundle.spec.js +1354 -243
  33. package/.agent/skills/form-expert.md +0 -102
  34. package/.agent/skills/investigation-protocol.md +0 -61
  35. package/.agent/skills/modal-master.md +0 -41
  36. package/.agent/skills/native-control-expert.md +0 -82
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // scripts/gen-and-run.mjs
3
- // Main Arcality CLI loop — v3 Single Configuration Mode
3
+ // Main Arcality CLI loop v3 Single Configuration Mode
4
4
  // Reads from arcality.config instead of multi-config .env approach.
5
5
 
6
6
  import 'dotenv/config';
@@ -30,14 +30,14 @@ import {
30
30
  // Load global config at startup (injects keys into process.env)
31
31
  setupProcessEnv();
32
32
 
33
- // ── WINDOWS FIX: Interceptar Ctrl+C antes que cmd.exe ──
34
- // En Windows, `arcality` se ejecuta vía arcality.cmd (creado por npm para el campo bin).
35
- // cmd.exe intercepta Ctrl+C y muestra "¿Desea terminar el trabajo por lotes (S/N)?".
36
- // Al presionar S, mata el árbol de procesos antes de que nuestro finally pueda correr.
33
+ // ── WINDOWS FIX: Interceptar Ctrl+C antes que cmd.exe ──
34
+ // En Windows, `arcality` se ejecuta vía arcality.cmd (creado por npm para el campo bin).
35
+ // cmd.exe intercepta Ctrl+C y muestra "¿Desea terminar el trabajo por lotes (S/N)?".
36
+ // Al presionar S, mata el árbol de procesos antes de que nuestro finally pueda correr.
37
37
  //
38
- // La solución: setRawMode(true) hace que Node capture los bytes del teclado directamente,
38
+ // La solución: setRawMode(true) hace que Node capture los bytes del teclado directamente,
39
39
  // ANTES de que cmd.exe los procese. Al detectar Ctrl+C (byte 0x03) lo convertimos en
40
- // un SIGINT de Node — cmd.exe nunca lo ve y nunca muestra el prompt.
40
+ // un SIGINT de Node cmd.exe nunca lo ve y nunca muestra el prompt.
41
41
  if (process.stdin.isTTY && process.stdin.setRawMode) {
42
42
  try {
43
43
  process.stdin.setRawMode(true);
@@ -63,7 +63,7 @@ const ORIGINAL_CWD = process.cwd();
63
63
  const COLLECTION_DEFAULT_CONCURRENCY = 3;
64
64
  const COLLECTION_MAX_CONCURRENCY = 5;
65
65
 
66
- // ── Load arcality.config if present ──
66
+ // ── Load arcality.config if present ──
67
67
  let projectConfig = loadProjectConfig(ORIGINAL_CWD) || loadProjectConfig(PROJECT_ROOT);
68
68
  if (projectConfig) {
69
69
  if (!isRealProjectId(projectConfig.projectId)) {
@@ -164,9 +164,9 @@ function setupEnvironment() {
164
164
  try {
165
165
  const pkg = JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, 'package.json'), 'utf8'));
166
166
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
167
- if (deps['next']) techStack = 'Next.js 🚀';
168
- else if (deps['vite']) techStack = 'Vite âš¡';
169
- else if (deps['react-scripts']) techStack = 'Create React App ⚛️';
167
+ if (deps['next']) techStack = 'Next.js 🚀';
168
+ else if (deps['vite']) techStack = 'Vite ';
169
+ else if (deps['react-scripts']) techStack = 'Create React App ⚛️';
170
170
  } catch { }
171
171
 
172
172
  // Override with arcality.config detection if available
@@ -284,7 +284,8 @@ function normalizeMissionSpec(rawMission, filePath = '') {
284
284
  priority: String(mission.priority || '').trim(),
285
285
  tags: normalizeStringList(mission.tags),
286
286
  timeout: Number.isFinite(Number(mission.timeout)) ? Number(mission.timeout) : null,
287
- credentialId: String(mission.credential_id || mission.credentialId || '').trim()
287
+ credentialId: String(mission.credential_id || mission.credentialId || '').trim(),
288
+ dbValidations: Array.isArray(mission.db_validations) ? mission.db_validations : (Array.isArray(mission.dbValidations) ? mission.dbValidations : [])
288
289
  };
289
290
  }
290
291
 
@@ -427,7 +428,7 @@ async function selectSavedMissionSource({ libraryDir, projectDir, rootDir }) {
427
428
  let source = sources[0];
428
429
  if (sources.length > 1) {
429
430
  const selectedSource = await select({
430
- message: '¿De dónde quieres abrir la misión?',
431
+ message: '¿De dónde quieres abrir la misión?',
431
432
  options: sources.map(item => ({
432
433
  label: `${item.label}${item.description ? ` - ${item.description}` : ''}`,
433
434
  value: item.key
@@ -439,7 +440,7 @@ async function selectSavedMissionSource({ libraryDir, projectDir, rootDir }) {
439
440
 
440
441
  if (source.key === 'root') {
441
442
  const selectedFile = await select({
442
- message: 'Elige la misión guardada:',
443
+ message: 'Elige la misión guardada:',
443
444
  options: source.files.map(file => ({ label: file, value: file }))
444
445
  });
445
446
  if (isCancel(selectedFile)) return null;
@@ -456,7 +457,7 @@ async function selectSavedMissionSource({ libraryDir, projectDir, rootDir }) {
456
457
  const selectedMission = await selectMissionYaml(
457
458
  source.dir,
458
459
  `Elige la carpeta de ${source.label.toLowerCase()}:`,
459
- 'Elige la misión guardada:'
460
+ 'Elige la misión guardada:'
460
461
  );
461
462
 
462
463
  if (!selectedMission) return null;
@@ -558,6 +559,22 @@ expected_result: "El registro nuevo aparece en la tabla Timesheet - Hoy."
558
559
  # Optional placeholder for future centralized credentials.
559
560
  credential_id: ""
560
561
  #
562
+ # db_validations:
563
+ # Optional post-mission database corroboration. Each query_id must exist in .arcality/db-validations.yaml.
564
+ # Supports exists and exact field checks against the first returned row.
565
+ db_validations: []
566
+ # Example:
567
+ # db_validations:
568
+ # - name: timesheet_creado_en_bd
569
+ # query_id: timesheet_by_description
570
+ # params:
571
+ # description: "Timesheet QA 001"
572
+ # expect:
573
+ # exists: true
574
+ # fields:
575
+ # status: "ACTIVE"
576
+ #
577
+ #
561
578
  # hooks:
562
579
  # Reserved for future before/after automation hooks.
563
580
  # hooks:
@@ -1114,7 +1131,8 @@ async function runCollectionBatch({
1114
1131
  ARCALITY_RUN_REPORTS_DIR: mission.reportsDir,
1115
1132
  ARCALITY_RUN_PLAYWRIGHT_OUTPUT_DIR: mission.playwrightOutputDir,
1116
1133
  ARCALITY_COLLECTION_RUN_ID: runId,
1117
- ARCALITY_COLLECTION_MISSION_SLUG: mission.slug
1134
+ ARCALITY_COLLECTION_MISSION_SLUG: mission.slug,
1135
+ ARCALITY_DB_VALIDATIONS_JSON: mission.missionSpec.dbValidations.length > 0 ? JSON.stringify(mission.missionSpec.dbValidations) : ''
1118
1136
  };
1119
1137
 
1120
1138
  if (runnerPrebuilt) {
@@ -1214,20 +1232,20 @@ function showBanner() {
1214
1232
  } catch { }
1215
1233
 
1216
1234
  const sentinelAscii = `
1217
- █████ ██████ ██████ █████ ██ ██ ████████ ██ ██
1218
- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
1219
- ███████ ██████ ██ ███████ ██ ██ ██ ████
1220
- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
1221
- ██ ██ ██ ██ ██████ ██ ██ ███████ ██ ██ ██ `;
1235
+ █████ ██████ ██████ █████ ██ ██ ████████ ██ ██
1236
+ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
1237
+ ███████ ██████ ██ ███████ ██ ██ ██ ████
1238
+ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
1239
+ ██ ██ ██ ██ ██████ ██ ██ ███████ ██ ██ ██ `;
1222
1240
 
1223
1241
  intro(chalk.gray(sentinelAscii));
1224
1242
 
1225
1243
  note(
1226
- chalk.gray('◉ Motor de Observación: ') + chalk.bold.white('EN LÍNEA') + '\n' +
1227
- chalk.gray('◉ Módulo de Recuperación de UI: ') + chalk.bold.white('ACTIVO') + '\n' +
1228
- chalk.gray('◉ Lógica Forense: ') + chalk.bold.white('ACTIVA') + '\n' +
1229
- chalk.gray('─'.repeat(50)) + '\n' +
1230
- chalk.gray('Versión del Sistema: ') + chalk.white(`v${version}`),
1244
+ chalk.gray(' Motor de Observación: ') + chalk.bold.white('EN LÍNEA') + '\n' +
1245
+ chalk.gray(' Módulo de Recuperación de UI: ') + chalk.bold.white('ACTIVO') + '\n' +
1246
+ chalk.gray(' Lógica Forense: ') + chalk.bold.white('ACTIVA') + '\n' +
1247
+ chalk.gray(''.repeat(50)) + '\n' +
1248
+ chalk.gray('Versión del Sistema: ') + chalk.white(`v${version}`),
1231
1249
  'Estado Central de Arcality'
1232
1250
  );
1233
1251
 
@@ -1236,8 +1254,8 @@ function showBanner() {
1236
1254
  // Show arcality.config status instead of multi-config switching
1237
1255
  const hasConfig = !!projectConfig;
1238
1256
  const configStatus = hasConfig
1239
- ? chalk.green('◉ Sincronización Establecida')
1240
- : chalk.red('⚠ Desconectado — Ejecuta `arcality init`');
1257
+ ? chalk.green(' Sincronización Establecida')
1258
+ : chalk.red('Desconectado Ejecuta `arcality init`');
1241
1259
 
1242
1260
  const { key: apiKeyVal, source: apiKeySource } = getApiKey();
1243
1261
  const apiKeyDisplay = apiKeyVal
@@ -1246,7 +1264,7 @@ function showBanner() {
1246
1264
 
1247
1265
  const infoLines = [
1248
1266
  chalk.gray('Nodo Objetivo: ') + chalk.white(process.env.NODE_ENV ?? 'development'),
1249
- chalk.gray('Token de Autenticación: ') + apiKeyDisplay,
1267
+ chalk.gray('Token de Autenticación: ') + apiKeyDisplay,
1250
1268
  chalk.gray('Stack: ') + chalk.white(techStack),
1251
1269
  ];
1252
1270
 
@@ -1256,10 +1274,10 @@ function showBanner() {
1256
1274
  chalk.gray('ID de Identidad: ') + chalk.white(projectConfig.projectId || 'N/A'),
1257
1275
  );
1258
1276
  } else {
1259
- infoLines.push(chalk.gray('Configuración: ') + configStatus);
1277
+ infoLines.push(chalk.gray('Configuración: ') + configStatus);
1260
1278
  }
1261
1279
 
1262
- note(infoLines.join('\n'), 'Telemetría del Objetivo');
1280
+ note(infoLines.join('\n'), 'Telemetría del Objetivo');
1263
1281
  }
1264
1282
 
1265
1283
  const ARCALITY_BRAIN_URL = "https://api.anthropic.com/v1/messages";
@@ -1389,7 +1407,7 @@ function run(cmd, args, options = {}) {
1389
1407
  // properly wraps arguments with spaces in quotes natively for CreateProcessW on Windows.
1390
1408
  //
1391
1409
  // CRITICAL (Windows): stdin MUST be 'ignore' (not 'inherit') to prevent cmd.exe from
1392
- // showing the "¿Desea terminar el trabajo por lotes (S/N)?" prompt on Ctrl+C.
1410
+ // showing the "¿Desea terminar el trabajo por lotes (S/N)?" prompt on Ctrl+C.
1393
1411
  // When stdin is 'inherit', cmd.exe intercepts Ctrl+C at the console level and kills
1394
1412
  // the entire process tree before our SIGINT handler can run, bypassing the finally block.
1395
1413
  const p = spawn(cmd, args, {
@@ -1438,9 +1456,9 @@ function run(cmd, args, options = {}) {
1438
1456
  fs.writeFileSync(lastRunLog, outputBuffer);
1439
1457
  } catch (e) { }
1440
1458
 
1441
- // code===0 → clean success
1442
- // code===null + signal → killed by signal (Ctrl+C) — treat as non-fatal so finally runs
1443
- // code===1 → test failed (Playwright returns 1 on test failure) — also non-fatal
1459
+ // code===0 clean success
1460
+ // code===null + signal killed by signal (Ctrl+C) treat as non-fatal so finally runs
1461
+ // code===1 test failed (Playwright returns 1 on test failure) also non-fatal
1444
1462
  if (code === 0 || signal !== null || code === 1) resolve();
1445
1463
  else {
1446
1464
  if (isDebug) console.error(chalk.red(`\n[DEBUG] Process exited with code ${code}`));
@@ -1464,7 +1482,7 @@ function runNpmScript(args, options = {}) {
1464
1482
 
1465
1483
  /**
1466
1484
  * Ping the project to register activity.
1467
- * Uses internal API URL — not user-configurable.
1485
+ * Uses internal API URL not user-configurable.
1468
1486
  */
1469
1487
  async function pingProject(projectId, apiKey) {
1470
1488
  if (!projectId || !apiKey) return;
@@ -1479,7 +1497,7 @@ async function pingProject(projectId, apiKey) {
1479
1497
  async function main() {
1480
1498
  process.chdir(PROJECT_ROOT);
1481
1499
 
1482
- // Backward compat: initialize .env if ACTIVE_CONFIG missing (legacy)
1500
+ // Read local .env only for user-managed variables that may still exist.
1483
1501
  let initialEnv = parseDotEnvFile(path.join(PROJECT_ROOT, '.env'));
1484
1502
  const envProjectId = initialEnv.ARCALITY_PROJECT_ID || process.env.ARCALITY_PROJECT_ID;
1485
1503
  if (envProjectId && !isRealProjectId(envProjectId)) {
@@ -1489,16 +1507,6 @@ async function main() {
1489
1507
  process.exit(1);
1490
1508
  }
1491
1509
 
1492
- if (!projectConfig && !initialEnv.ACTIVE_CONFIG) {
1493
- updateDotEnvFile({
1494
- ACTIVE_CONFIG: 'Default',
1495
- SAVED_CONFIGS: 'Default',
1496
- Default_URL: initialEnv.BASE_URL || '',
1497
- Default_USER: initialEnv.LOGIN_USER || '',
1498
- Default_PASS: initialEnv.LOGIN_PASSWORD || ''
1499
- });
1500
- initialEnv = parseDotEnvFile(path.join(PROJECT_ROOT, '.env'));
1501
- }
1502
1510
 
1503
1511
  const argv = process.argv.slice(2);
1504
1512
  let initialDiscoverPath = null;
@@ -1570,6 +1578,7 @@ async function main() {
1570
1578
  let collectionConcurrency = firstRun ? initialCollectionConcurrency : null;
1571
1579
  let skipSavePrompt = firstRun ? initialSkipSavePrompt : false;
1572
1580
  let batchMode = firstRun ? initialBatchMode : false;
1581
+ let missionDbValidations = [];
1573
1582
 
1574
1583
  let skipMenu = (firstRun && (prompt || agentMode || smartMode || runAllMode || templateMode)) || agentMode || runAllMode || templateMode;
1575
1584
  if (agentMode) skipMenu = true; // Hard-lock: No menu in agent mode
@@ -1579,7 +1588,7 @@ async function main() {
1579
1588
  if (!skipMenu) {
1580
1589
  showBanner();
1581
1590
  try {
1582
- // ── Build menu options (single config mode) ──
1591
+ // ── Build menu options (single config mode) ──
1583
1592
  const missionsDir = process.env.MISSIONS_DIR;
1584
1593
  const savedMissionCollections = listMissionCollections(missionsDir);
1585
1594
 
@@ -1601,38 +1610,40 @@ async function main() {
1601
1610
  const rootYamlFiles = rootMissionFiles;
1602
1611
 
1603
1612
  const options = [
1604
- { label: 'â—‰ Desplegar Arcality (Prompt)', value: 'agent' },
1605
- ...(savedMissionCollections.length ? [{ label: '▶ Ejecutar colección completa', value: 'run_collection' }] : []),
1606
- ...(savedMissionCollections.length ? [{ label: '⟳ Lanzar Misión Archivada (.yaml)', value: 'launch_saved' }] : []),
1607
- ...(yamlOutputCollections.length ? [{ label: '⟶ Activar Modo GUÍA (Reusar YAML)', value: 'reuse_yaml' }] : []),
1608
- ...(rootYamlFiles.length ? [{ label: '📂 Cargar Matriz de Patrones (YAML Raíz)', value: 'yaml_list' }] : []),
1609
- { label: '📄 Generar templates YAML', value: 'generate_templates' },
1610
- { label: 'â—Ž Recalibrar Sistema (init)', value: 'reconfigure' },
1611
- { label: '✕ Terminar Proceso', value: 'exit' }
1613
+ { label: 'Ejecutar mision', value: 'agent' },
1614
+ ...(savedMissionCollections.length ? [{ label: 'Ejecutar coleccion de misiones', value: 'run_collection' }] : []),
1615
+ ...(savedMissionCollections.length ? [{ label: 'Ejecutar YAML guardado (.yaml)', value: 'launch_saved' }] : []),
1616
+ ...(yamlOutputCollections.length ? [{ label: 'Reusar YAML generado', value: 'reuse_yaml' }] : []),
1617
+ ...(rootYamlFiles.length ? [{ label: 'Abrir YAML desde la raiz', value: 'yaml_list' }] : []),
1618
+ { label: 'Descubrir rutas y generar misiones', value: 'generate_missions' },
1619
+ { label: 'Crear plantillas YAML base', value: 'generate_templates' },
1620
+ { label: 'Editar configuracion actual', value: 'reconfigure' },
1621
+ { label: 'Configurar o reconectar proyecto', value: 'full_reconfigure' },
1622
+ { label: 'Salir de Arcality', value: 'exit' }
1612
1623
  ];
1613
1624
 
1614
1625
  const menuOptions = options
1615
1626
  .filter(option => !['launch_saved', 'reuse_yaml', 'yaml_list'].includes(option.value))
1616
1627
  .map(option => option.value === 'generate_templates'
1617
- ? { ...option, label: '◫ Generar plantillas de misión' }
1628
+ ? { ...option, label: 'Crear plantillas YAML base' }
1618
1629
  : option
1619
1630
  );
1620
1631
 
1621
1632
  if (hasSavedMissionSources) {
1622
1633
  const insertIndex = Math.min(menuOptions.length, 2);
1623
1634
  menuOptions.splice(insertIndex, 0, {
1624
- label: '◌ Abrir misión guardada',
1635
+ label: 'Ejecutar mision guardada',
1625
1636
  value: 'open_saved_mission'
1626
1637
  });
1627
1638
  }
1628
1639
 
1629
1640
  const action = await select({
1630
- message: '¿Qué te gustaría hacer?',
1641
+ message: '¿Qué te gustaría hacer?',
1631
1642
  options: menuOptions
1632
1643
  });
1633
1644
 
1634
1645
  if (isCancel(action) || action === 'exit') {
1635
- outro(chalk.cyan('¡Hasta luego!'));
1646
+ outro(chalk.cyan('¡Hasta luego!'));
1636
1647
  break;
1637
1648
  }
1638
1649
 
@@ -1652,11 +1663,43 @@ async function main() {
1652
1663
  const yamlContent = fs.readFileSync(selectedMission.fullPath, 'utf8');
1653
1664
  const missionSpec = normalizeMissionSpec(loadYaml(yamlContent), selectedMission.fullPath);
1654
1665
  prompt = missionSpec.prompt || yamlContent;
1666
+ missionDbValidations = missionSpec.dbValidations;
1655
1667
  discoverPath = getMissionStartPath(
1656
1668
  missionSpec,
1657
1669
  readCollectionMeta(path.dirname(selectedMission.fullPath), selectedMission.collection)
1658
1670
  );
1659
1671
  agentMode = true;
1672
+ } else if (action === 'generate_missions') {
1673
+ const generateScript = path.join(PROJECT_ROOT, 'scripts', 'generate.mjs');
1674
+ const generationMode = await select({
1675
+ message: 'Como quieres usar la autogeneracion?',
1676
+ options: [
1677
+ { label: 'Solo vista previa (sin archivos)', value: 'dry_run' },
1678
+ { label: 'Generar archivos YAML ahora', value: 'write_yaml' },
1679
+ { label: 'Cancelar', value: 'cancel' }
1680
+ ]
1681
+ });
1682
+ if (isCancel(generationMode) || generationMode === 'cancel') continue;
1683
+
1684
+ try {
1685
+ await new Promise((resolve, reject) => {
1686
+ const childArgs = generationMode === 'dry_run'
1687
+ ? [generateScript, '--dry-run']
1688
+ : [generateScript];
1689
+ const child = spawn('node', childArgs, {
1690
+ stdio: 'inherit',
1691
+ cwd: ORIGINAL_CWD,
1692
+ env: { ...process.env, ARCALITY_ROOT: PROJECT_ROOT }
1693
+ });
1694
+ child.on('exit', code => code === 0 ? resolve() : reject(new Error(`Generate exited with ${code}`)));
1695
+ child.on('error', reject);
1696
+ });
1697
+ } catch (e) {
1698
+ console.log(chalk.red(`\nAutogeneracion fallida: ${e.message}`));
1699
+ }
1700
+
1701
+ await waitForEnter('Presiona Enter para volver al menu...');
1702
+ continue;
1660
1703
  } else if (action === 'generate_templates') {
1661
1704
  const createdMain = ensureMissionTemplates(missionsDir);
1662
1705
  const yamlDir = projectConfig ? getYamlOutputDir(projectConfig, ORIGINAL_CWD) : null;
@@ -1671,7 +1714,30 @@ async function main() {
1671
1714
  await waitForEnter('Presiona Enter para volver al menu...');
1672
1715
  continue;
1673
1716
  } else if (action === 'reconfigure') {
1674
- // Launch arcality init in the user's project directory
1717
+ const editorScript = path.join(PROJECT_ROOT, 'scripts', 'edit-config.mjs');
1718
+ try {
1719
+ await new Promise((resolve, reject) => {
1720
+ const child = spawn('node', [editorScript], {
1721
+ stdio: 'inherit',
1722
+ cwd: ORIGINAL_CWD,
1723
+ env: { ...process.env, ARCALITY_ROOT: PROJECT_ROOT }
1724
+ });
1725
+ child.on('exit', code => code === 0 ? resolve() : reject(new Error(`Config editor exited with ${code}`)));
1726
+ child.on('error', reject);
1727
+ });
1728
+
1729
+ projectConfig = loadProjectConfig(ORIGINAL_CWD) || loadProjectConfig(PROJECT_ROOT);
1730
+ if (projectConfig) {
1731
+ injectConfigToEnv(projectConfig);
1732
+ }
1733
+ setupEnvironment();
1734
+
1735
+ } catch (e) {
1736
+ console.log(chalk.red(`\nConfig update failed: ${e.message}`));
1737
+ await new Promise(r => setTimeout(r, 3000));
1738
+ }
1739
+ continue;
1740
+ } else if (action === 'full_reconfigure') {
1675
1741
  const initScript = path.join(PROJECT_ROOT, 'scripts', 'init.mjs');
1676
1742
  try {
1677
1743
  await new Promise((resolve, reject) => {
@@ -1683,18 +1749,15 @@ async function main() {
1683
1749
  child.on('exit', code => code === 0 ? resolve() : reject(new Error(`Init exited with ${code}`)));
1684
1750
  child.on('error', reject);
1685
1751
  });
1686
-
1687
- // ── REFRESH CONFIG ──
1688
- // Recargar el archivo que init.mjs acaba de escribir
1752
+
1689
1753
  projectConfig = loadProjectConfig(ORIGINAL_CWD) || loadProjectConfig(PROJECT_ROOT);
1690
1754
  if (projectConfig) {
1691
1755
  injectConfigToEnv(projectConfig);
1692
1756
  }
1693
1757
  setupEnvironment();
1694
-
1758
+
1695
1759
  } catch (e) {
1696
- // Mostrar el error claramente antes de que console.clear() lo borre
1697
- console.log(chalk.red(`\n❌ Reconfigure failed: ${e.message}`));
1760
+ console.log(chalk.red(`\nReconnect failed: ${e.message}`));
1698
1761
  await new Promise(r => setTimeout(r, 3000));
1699
1762
  }
1700
1763
  continue;
@@ -1709,6 +1772,7 @@ async function main() {
1709
1772
  const yamlContent = fs.readFileSync(selectedMission.fullPath, 'utf8');
1710
1773
  const missionSpec = normalizeMissionSpec(loadYaml(yamlContent), selectedMission.fullPath);
1711
1774
  prompt = missionSpec.prompt || yamlContent;
1775
+ missionDbValidations = missionSpec.dbValidations;
1712
1776
  discoverPath = getMissionStartPath(missionSpec, readCollectionMeta(path.dirname(selectedMission.fullPath), selectedMission.collection));
1713
1777
  agentMode = true;
1714
1778
  } else if (action === 'reuse_yaml') {
@@ -1723,6 +1787,7 @@ async function main() {
1723
1787
  const yamlContent = fs.readFileSync(selectedMission.fullPath, 'utf8');
1724
1788
  const missionSpec = normalizeMissionSpec(loadYaml(yamlContent), selectedMission.fullPath);
1725
1789
  prompt = missionSpec.prompt || yamlContent;
1790
+ missionDbValidations = missionSpec.dbValidations;
1726
1791
  discoverPath = getMissionStartPath(missionSpec, readCollectionMeta(path.dirname(selectedMission.fullPath), selectedMission.collection));
1727
1792
  agentMode = true;
1728
1793
  } else if (action === 'yaml_list') {
@@ -1735,6 +1800,7 @@ async function main() {
1735
1800
  const yamlContent = fs.readFileSync(path.join(PROJECT_ROOT, sel), 'utf8');
1736
1801
  const missionSpec = normalizeMissionSpec(loadYaml(yamlContent), path.join(PROJECT_ROOT, sel));
1737
1802
  prompt = missionSpec.prompt || yamlContent;
1803
+ missionDbValidations = missionSpec.dbValidations;
1738
1804
  discoverPath = getMissionStartPath(missionSpec);
1739
1805
  agentMode = true;
1740
1806
  }
@@ -1765,7 +1831,7 @@ async function main() {
1765
1831
  if (!batchCollection && process.stdin.isTTY) {
1766
1832
  const availableCollections = listMissionCollections(process.env.MISSIONS_DIR);
1767
1833
  const selectedBatchCollection = await select({
1768
- message: 'Elige la colección a ejecutar:',
1834
+ message: 'Elige la colección a ejecutar:',
1769
1835
  options: [
1770
1836
  { label: `Todas las colecciones (${availableCollections.length})`, value: '__ALL__' },
1771
1837
  ...availableCollections.map(collection => ({
@@ -1823,16 +1889,16 @@ async function main() {
1823
1889
  if (agentMode) {
1824
1890
  if (!prompt || prompt.trim().length < 4) {
1825
1891
  const p = await text({
1826
- message: chalk.cyan('🤖 Misión para Arcality (Requerida):'),
1892
+ message: chalk.cyan('🤖 Misión para Arcality (Requerida):'),
1827
1893
  placeholder: 'Ej., Llena el formulario de registro con datos aleatorios',
1828
1894
  validate: v => {
1829
- if (!v || !v.trim()) return 'La misión es requerida para iniciar.';
1830
- if (v.trim().length < 4) return 'La misión debe ser más descriptiva (mínimo 4 caracteres).';
1895
+ if (!v || !v.trim()) return 'La misión es requerida para iniciar.';
1896
+ if (v.trim().length < 4) return 'La misión debe ser más descriptiva (mínimo 4 caracteres).';
1831
1897
  }
1832
1898
  });
1833
1899
 
1834
1900
  if (isCancel(p)) {
1835
- cancel('Misión cancelada. Regresando al menú principal...');
1901
+ cancel('Misión cancelada. Regresando al menú principal...');
1836
1902
  agentMode = false;
1837
1903
  prompt = "";
1838
1904
  skipMenu = false;
@@ -1844,8 +1910,8 @@ async function main() {
1844
1910
  if (!discoverPath) {
1845
1911
  const knownBaseUrl = projectConfig?.project?.baseUrl || process.env.BASE_URL;
1846
1912
  const promptMsg = knownBaseUrl
1847
- ? `🌐 URL Completa de Navegación (relative to ${knownBaseUrl}):`
1848
- : '🌐 URL Completa de Navegación (ej., http://localhost:3000/):';
1913
+ ? `🌐 URL Completa de Navegación (relative to ${knownBaseUrl}):`
1914
+ : '🌐 URL Completa de Navegación (ej., http://localhost:3000/):';
1849
1915
 
1850
1916
  const d = await text({
1851
1917
  message: chalk.cyan(promptMsg),
@@ -1857,7 +1923,7 @@ async function main() {
1857
1923
  });
1858
1924
 
1859
1925
  if (isCancel(d)) {
1860
- cancel('Misión abortada. Regresando al menú...');
1926
+ cancel('Misión abortada. Regresando al menú...');
1861
1927
  agentMode = false;
1862
1928
  prompt = "";
1863
1929
  skipMenu = false;
@@ -1873,7 +1939,7 @@ async function main() {
1873
1939
  const libDotEnv = parseDotEnvFile(path.join(PROJECT_ROOT, '.env'));
1874
1940
  const dotEnv = { ...userDotEnv, ...libDotEnv };
1875
1941
 
1876
- // ── API Key and Quota Validation ──
1942
+ // ── API Key and Quota Validation ──
1877
1943
  const { validateApiKey, startMission: requestMission } = await import('../src/arcalityClient.mjs');
1878
1944
 
1879
1945
  const validation = await validateApiKey();
@@ -1887,13 +1953,13 @@ async function main() {
1887
1953
  }
1888
1954
  if (!validation.valid) {
1889
1955
  const errorMessages = {
1890
- 'no_api_key': '🔑 API Key no encontrada.\n Configúrala vía `arcality init` o en .env (ARCALITY_API_KEY)',
1891
- 'invalid_format': '🔑 API Key inválida. Debe comenzar con "arc_"',
1892
- 'invalid_api_key': '🔑 API Key no reconocida por el servidor',
1893
- 'plan_expired': '💳 Tu plan ha expirado',
1956
+ 'no_api_key': '🔑 API Key no encontrada.\n Configúrala vía `arcality init` o en .env (ARCALITY_API_KEY)',
1957
+ 'invalid_format': '🔑 API Key inválida. Debe comenzar con "arc_"',
1958
+ 'invalid_api_key': '🔑 API Key no reconocida por el servidor',
1959
+ 'plan_expired': '💳 Tu plan ha expirado',
1894
1960
  'backend_unavailable': 'No se pudo conectar con Arcality. La herramienta requiere conexion al backend para validar la cuenta.'
1895
1961
  };
1896
- note(chalk.red(errorMessages[validation.error] || `Error: ${validation.error}`), '❌ Autenticación');
1962
+ note(chalk.red(errorMessages[validation.error] || `Error: ${validation.error}`), ' Autenticación');
1897
1963
  agentMode = false;
1898
1964
  prompt = "";
1899
1965
  skipMenu = false;
@@ -1902,11 +1968,11 @@ async function main() {
1902
1968
 
1903
1969
  // Show plan info
1904
1970
  const modeLabel = validation.mode === 'mock' ? chalk.gray('[sandbox]') : chalk.cyanBright('[live-net]');
1905
- const dailyLimitDisplay = validation.daily_limit === -1 ? '∞' : (validation.daily_limit || 35);
1906
- const remainingDisplay = validation.remaining >= 999999 ? '∞' : validation.remaining;
1971
+ const dailyLimitDisplay = validation.daily_limit === -1 ? '' : (validation.daily_limit || 35);
1972
+ const remainingDisplay = validation.remaining >= 999999 ? '' : validation.remaining;
1907
1973
  note(
1908
- chalk.gray('Conexión: ') + chalk.cyanBright('SECURE ') + modeLabel + '\n' +
1909
- chalk.gray('Autorización: ') + chalk.white(validation.plan || 'internal') + '\n' +
1974
+ chalk.gray('Conexión: ') + chalk.cyanBright('SECURE ') + modeLabel + '\n' +
1975
+ chalk.gray('Autorización: ') + chalk.white(validation.plan || 'internal') + '\n' +
1910
1976
  chalk.gray('Misiones: ') + chalk.white(`${validation.daily_used || 0}/${dailyLimitDisplay}`) + '\n' +
1911
1977
  chalk.gray('Reservas: ') + chalk.white(remainingDisplay),
1912
1978
  'Enlace Arcality'
@@ -1944,15 +2010,15 @@ async function main() {
1944
2010
  const projects = data.projects || [];
1945
2011
 
1946
2012
  const projOptions = projects.map(p => ({ label: p.name || p.Name, value: p.id || p.Id }));
1947
- projOptions.push({ label: 'âž• Crear nuevo proyecto', value: 'new' });
2013
+ projOptions.push({ label: ' Crear nuevo proyecto', value: 'new' });
1948
2014
 
1949
2015
  const projSelection = await select({
1950
- message: chalk.cyan('Elige un proyecto para esta misión:'),
2016
+ message: chalk.cyan('Elige un proyecto para esta misión:'),
1951
2017
  options: projOptions
1952
2018
  });
1953
2019
 
1954
2020
  if (isCancel(projSelection)) {
1955
- cancel('Misión abortada.');
2021
+ cancel('Misión abortada.');
1956
2022
  agentMode = false; prompt = ""; skipMenu = false; continue;
1957
2023
  }
1958
2024
 
@@ -1978,9 +2044,9 @@ async function main() {
1978
2044
  note(chalk.red('El backend no devolvio un Project ID valido.'), 'Proyecto no conectado');
1979
2045
  selectedProjectId = null;
1980
2046
  }
1981
- if (selectedProjectId) note(chalk.green(`✅ Proyecto creado: ${newName}`));
2047
+ if (selectedProjectId) note(chalk.green(`✅ Proyecto creado: ${newName}`));
1982
2048
  } else {
1983
- note(chalk.red(`❌ Falló al crear proyecto.`));
2049
+ note(chalk.red(`❌ Falló al crear proyecto.`));
1984
2050
  }
1985
2051
  } else {
1986
2052
  selectedProjectId = projSelection;
@@ -2013,7 +2079,7 @@ async function main() {
2013
2079
  continue;
2014
2080
  }
2015
2081
 
2016
- // ── Start Mission ──
2082
+ // ── Start Mission ──
2017
2083
  const mission = await requestMission(prompt, discoverPath || '/');
2018
2084
  if (!mission.allowed) {
2019
2085
  if (mission.error === 'mission_limit_exceeded') {
@@ -2044,19 +2110,19 @@ async function main() {
2044
2110
 
2045
2111
  const s = spinner();
2046
2112
  if (!process.argv.includes('--debug')) {
2047
- s.start(`â—‰ [ARCALITY INICIADO] Ejecutando directiva: "${prompt}" ${chalk.gray(`(${mission.daily_used}/${mission.daily_limit})`)}`);
2113
+ s.start(`◉ [ARCALITY INICIADO] Ejecutando directiva: "${prompt}" ${chalk.gray(`(${mission.daily_used}/${mission.daily_limit})`)}`);
2048
2114
  } else {
2049
- console.log(chalk.cyan(`\nâ—‰ [ARCALITY INICIADO] Ejecutando directiva: "${prompt}" ${chalk.gray(`(${mission.daily_used}/${mission.daily_limit})`)}`));
2115
+ console.log(chalk.cyan(`\n [ARCALITY INICIADO] Ejecutando directiva: "${prompt}" ${chalk.gray(`(${mission.daily_used}/${mission.daily_limit})`)}`));
2050
2116
  }
2051
2117
 
2052
2118
  const finalProjectId = process.env.ARCALITY_PROJECT_ID || selectedProjectId;
2053
2119
 
2054
- // ── Cancelación Manual (Ctrl+C) — Estrategia bulletproof ──
2120
+ // ── Cancelación Manual (Ctrl+C) Estrategia bulletproof ──
2055
2121
  // El problema: @clack/prompts registra su propio SIGINT handler cada tick del spinner,
2056
2122
  // que llama a process.exit(0) directamente. removeAllListeners() no es suficiente porque
2057
- // Clack lo re-registra en cada frame de animación.
2123
+ // Clack lo re-registra en cada frame de animación.
2058
2124
  //
2059
- // La solución: interceptar process.exit() antes de que Clack lo llame,
2125
+ // La solución: interceptar process.exit() antes de que Clack lo llame,
2060
2126
  // y usar prependListener para que NUESTRO handler siempre se ejecute primero.
2061
2127
 
2062
2128
  let activeChildProcess = null;
@@ -2068,18 +2134,18 @@ async function main() {
2068
2134
  const _origProcessExit = process.exit.bind(process);
2069
2135
  process.exit = (code) => {
2070
2136
  if (_missionActive) {
2071
- // Clack (u otra lib) está intentando matar el proceso — lo bloqueamos.
2072
- // Nuestro cancelHandler ya habrá sido invocado vía prependListener.
2073
- // El proceso se cerrará naturalmente cuando el child process termine.
2137
+ // Clack (u otra lib) está intentando matar el proceso lo bloqueamos.
2138
+ // Nuestro cancelHandler ya habrá sido invocado vía prependListener.
2139
+ // El proceso se cerrará naturalmente cuando el child process termine.
2074
2140
  return;
2075
2141
  }
2076
2142
  _origProcessExit(code);
2077
2143
  };
2078
2144
 
2079
- // ── RESTORE RAW MODE FOR WINDOWS FIX ──
2080
- // @clack/prompts pausea stdin y desactiva raw mode al terminar un menú.
2081
- // Debemos reactivarlo aquí para que nuestro interceptor global de Ctrl+C
2082
- // siga funcionando durante toda la misión.
2145
+ // ── RESTORE RAW MODE FOR WINDOWS FIX ──
2146
+ // @clack/prompts pausea stdin y desactiva raw mode al terminar un menú.
2147
+ // Debemos reactivarlo aquí para que nuestro interceptor global de Ctrl+C
2148
+ // siga funcionando durante toda la misión.
2083
2149
  if (process.stdin.isTTY && process.stdin.setRawMode) {
2084
2150
  try {
2085
2151
  process.stdin.setRawMode(true);
@@ -2090,21 +2156,21 @@ async function main() {
2090
2156
  const cancelHandler = () => {
2091
2157
  sigintCount++;
2092
2158
  if (sigintCount > 3) {
2093
- console.log(chalk.red('\n🛑 Forzando salida inmediata...'));
2159
+ console.log(chalk.red('\n🛑 Forzando salida inmediata...'));
2094
2160
  _missionActive = false;
2095
2161
  _origProcessExit(1);
2096
2162
  return;
2097
2163
  }
2098
2164
 
2099
2165
  if (userCanceledMission) {
2100
- console.log(chalk.yellow('\n⏳ Cancelación en progreso, esperando reporte... (Ctrl+C x4 para forzar cierre)'));
2166
+ console.log(chalk.yellow('\n Cancelación en progreso, esperando reporte... (Ctrl+C x4 para forzar cierre)'));
2101
2167
  return;
2102
2168
  }
2103
2169
  userCanceledMission = true;
2104
2170
 
2105
- console.log(chalk.yellow('\n\n⚠️ [ARCALITY] Directiva abortada por el operador.'));
2171
+ console.log(chalk.yellow('\n\n⚠️ [ARCALITY] Directiva abortada por el operador.'));
2106
2172
  if (!process.argv.includes('--debug')) {
2107
- try { s.stop(chalk.yellow('⚠️ Directiva abortada.')); } catch { /* ignore */ }
2173
+ try { s.stop(chalk.yellow('⚠️ Directiva abortada.')); } catch { /* ignore */ }
2108
2174
  }
2109
2175
 
2110
2176
  // Terminar el proceso hijo de Playwright directamente (Windows-safe)
@@ -2136,8 +2202,8 @@ async function main() {
2136
2202
  } catch { /* silencioso */ }
2137
2203
 
2138
2204
  console.log(chalk.gray(' >> Esperando que Playwright cierre y genere el reporte...'));
2139
- // Playwright también recibe el SIGINT nativamente desde la terminal Windows
2140
- // y cerrará su proceso limpiamente, lo que resolverá la Promise del run() → finally
2205
+ // Playwright también recibe el SIGINT nativamente desde la terminal Windows
2206
+ // y cerrará su proceso limpiamente, lo que resolverá la Promise del run() finally
2141
2207
  };
2142
2208
 
2143
2209
  // prependListener asegura que nuestro handler se ejecuta ANTES que el de Clack
@@ -2145,19 +2211,20 @@ async function main() {
2145
2211
  process.prependListener('SIGTERM', cancelHandler);
2146
2212
 
2147
2213
 
2148
- // Build env for the runner — merge arcality.config values.
2149
- // ARCALITY_API_URL must be explicit — it comes from the library's internal config,
2214
+ // Build env for the runner merge arcality.config values.
2215
+ // ARCALITY_API_URL must be explicit it comes from the library's internal config,
2150
2216
  // not from the user's project .env, so we inject it directly via getApiUrl().
2151
2217
  const fallbackBaseUrl = (!projectConfig && discoverPath?.startsWith('http')) ? discoverPath : undefined;
2152
2218
 
2153
2219
  const mergedEnv = {
2154
2220
  ...dotEnv,
2155
2221
  ...process.env,
2156
- ARCALITY_API_URL: getApiUrl(), // Internal URL — always injected explicitly
2222
+ ARCALITY_ROOT: PROJECT_ROOT,
2223
+ ARCALITY_API_URL: getApiUrl(), // Internal URL — always injected explicitly
2157
2224
  ARCALITY_PROJECT_ID: finalProjectId,
2158
2225
  ARCALITY_PROJECT_ROOT: ORIGINAL_CWD,
2159
- ARCALITY_MISSION_ID: mission.mission_id || mission.missionId || mission.MissionId || '', // ← Propagate mission_id to every proxy call
2160
- ARCALITY_ORG_ID: validation.organization_id || validation.organizationId || validation.OrganizationId || process.env.ARCALITY_ORG_ID || '', // ← Propagate org_id for pattern persistence
2226
+ ARCALITY_MISSION_ID: mission.mission_id || mission.missionId || mission.MissionId || '', // Propagate mission_id to every proxy call
2227
+ ARCALITY_ORG_ID: validation.organization_id || validation.organizationId || validation.OrganizationId || process.env.ARCALITY_ORG_ID || '', // Propagate org_id for pattern persistence
2161
2228
  BASE_URL: projectConfig?.project?.baseUrl || dotEnv.BASE_URL || process.env.BASE_URL || fallbackBaseUrl || 'http://localhost:3000',
2162
2229
  LOGIN_USER: projectConfig?.auth?.username || dotEnv.LOGIN_USER || process.env.LOGIN_USER,
2163
2230
  LOGIN_PASSWORD: projectConfig?.auth?.password || dotEnv.LOGIN_PASSWORD || process.env.LOGIN_PASSWORD,
@@ -2168,7 +2235,8 @@ async function main() {
2168
2235
  CONTEXT_DIR: process.env.CONTEXT_DIR,
2169
2236
  REPORTS_DIR: process.env.REPORTS_DIR,
2170
2237
  SMART_PROMPT: prompt,
2171
- TARGET_PATH: discoverPath || '/'
2238
+ TARGET_PATH: discoverPath || '/',
2239
+ ARCALITY_DB_VALIDATIONS_JSON: missionDbValidations.length > 0 ? JSON.stringify(missionDbValidations) : (process.env.ARCALITY_DB_VALIDATIONS_JSON || process.env.ARCALITY_DB_VALIDATIONS || '')
2172
2240
  };
2173
2241
 
2174
2242
  if (finalProjectId) {
@@ -2176,17 +2244,17 @@ async function main() {
2176
2244
  console.log(chalk.gray(`>> ARCALITY_ORG_ID: ${mergedEnv.ARCALITY_ORG_ID || 'NONE'}`));
2177
2245
  }
2178
2246
 
2179
- // ── Resolve execution paths ──
2247
+ // ── Resolve execution paths ──
2180
2248
  // tsx and playwright are HOISTED to ORIGINAL_CWD/node_modules when installed via npm.
2181
2249
  // We run from ORIGINAL_CWD so relative paths for playwright work.
2182
2250
  // For the test file and config (inside the arcality package), we use absolute paths.
2183
2251
  //
2184
- // KEY: tsx loader uses file:// URL (pathToFileURL) so spaces encode as %20 — no quoting needed.
2252
+ // KEY: tsx loader uses file:// URL (pathToFileURL) so spaces encode as %20 no quoting needed.
2185
2253
 
2186
2254
  const testFile = path.join(PROJECT_ROOT, 'tests', '_helpers', 'agentic-runner.spec.ts');
2187
2255
  const configFile = path.join(PROJECT_ROOT, 'playwright.config.js');
2188
2256
 
2189
- // Resolve playwright CLI — CRITICAL singleton alignment:
2257
+ // Resolve playwright CLI CRITICAL singleton alignment:
2190
2258
  //
2191
2259
  // @playwright/test uses `playwright` internally. When npm detects a version mismatch
2192
2260
  // between the top-level `playwright` and what `@playwright/test` needs, it installs
@@ -2194,15 +2262,15 @@ async function main() {
2194
2262
  //
2195
2263
  // Playwright's test runner tracks the active suite via a module-level singleton
2196
2264
  // (currentSuite). If the CLI uses a DIFFERENT playwright instance than the one
2197
- // @playwright/test loaded, their singletons are out of sync → "did not expect test()
2265
+ // @playwright/test loaded, their singletons are out of sync "did not expect test()
2198
2266
  // to be called here".
2199
2267
  //
2200
2268
  // Fix: ALWAYS prefer the playwright nested INSIDE @playwright/test, because that is
2201
2269
  // guaranteed to be the same instance that @playwright/test uses for its internals.
2202
2270
  //
2203
2271
  // Resolution priority:
2204
- // 1. <project>/@playwright/test/node_modules/playwright/cli.js ← nested (safe)
2205
- // 2. <project>/playwright/cli.js ← sibling top-level
2272
+ // 1. <project>/@playwright/test/node_modules/playwright/cli.js nested (safe)
2273
+ // 2. <project>/playwright/cli.js sibling top-level
2206
2274
  // (repeated for ORIGINAL_CWD and PROJECT_ROOT)
2207
2275
  let playwrightCli;
2208
2276
  try {
@@ -2221,9 +2289,9 @@ async function main() {
2221
2289
 
2222
2290
  if (!testRunnerPath) throw resolveError;
2223
2291
 
2224
- const testRunnerDir = path.dirname(testRunnerPath); // → .../node_modules/@playwright/test/
2292
+ const testRunnerDir = path.dirname(testRunnerPath); // .../node_modules/@playwright/test/
2225
2293
 
2226
- // Priority 1: playwright nested INSIDE @playwright/test — same singleton as the package uses
2294
+ // Priority 1: playwright nested INSIDE @playwright/test same singleton as the package uses
2227
2295
  const nestedCli = path.join(testRunnerDir, 'node_modules', 'playwright', 'cli.js');
2228
2296
  // Priority 2: playwright as a top-level sibling (clean installs, versions aligned)
2229
2297
  const siblingCli = path.join(testRunnerDir, '..', '..', 'playwright', 'cli.js');
@@ -2242,7 +2310,7 @@ async function main() {
2242
2310
  console.log(chalk.gray(`[DEBUG] Using playwright CLI: ${playwrightCli}`));
2243
2311
  }
2244
2312
  } catch (e) {
2245
- console.error(chalk.red('\n[FATAL] Núcleo de Playwright no encontrado. Asegúrate de que @playwright/test esté instalado.'));
2313
+ console.error(chalk.red('\n[FATAL] Núcleo de Playwright no encontrado. Asegúrate de que @playwright/test esté instalado.'));
2246
2314
  console.error(chalk.red(e.message));
2247
2315
  console.error(chalk.yellow('\nConsejo: Ejecuta `npm install @playwright/test` en tu directorio de proyecto.'));
2248
2316
  _missionActive = false;
@@ -2254,9 +2322,9 @@ async function main() {
2254
2322
  const postProcessSigint = () => {
2255
2323
  _postProcessSigintCount++;
2256
2324
  if (_postProcessSigintCount === 1) {
2257
- console.log(chalk.yellow('\n⏳ Finalizando reporte de telemetría, por favor espera... (Ctrl+C de nuevo para salir ya)'));
2325
+ console.log(chalk.yellow('\n Finalizando reporte de telemetría, por favor espera... (Ctrl+C de nuevo para salir ya)'));
2258
2326
  } else {
2259
- console.log(chalk.red('\n🛑 Saliendo forzado por el usuario.'));
2327
+ console.log(chalk.red('\n🛑 Saliendo forzado por el usuario.'));
2260
2328
  process.removeAllListeners('SIGINT');
2261
2329
  process.removeAllListeners('SIGTERM');
2262
2330
  process.exit(0);
@@ -2274,20 +2342,20 @@ async function main() {
2274
2342
  }
2275
2343
  } else
2276
2344
 
2277
- // ── Auto-compilar el runner antes de ejecutar Playwright para asegurar que los cambios de rama/merge estén aplicados ──
2345
+ // ── Auto-compilar el runner antes de ejecutar Playwright para asegurar que los cambios de rama/merge estén aplicados ──
2278
2346
  try {
2279
2347
  if (!process.argv.includes('--debug')) {
2280
- s.message('⚙️ Compilando agente de pruebas (Arcality Runner)...');
2348
+ s.message('⚙️ Compilando agente de pruebas (Arcality Runner)...');
2281
2349
  } else {
2282
- console.log(chalk.cyan('⚙️ Compilando agente de pruebas (Arcality Runner)...'));
2350
+ console.log(chalk.cyan('⚙️ Compilando agente de pruebas (Arcality Runner)...'));
2283
2351
  }
2284
2352
  await ensureRunnerBundle({ debug: process.argv.includes('--debug') });
2285
2353
  } catch (err) {
2286
- console.warn(chalk.yellow(`\n⚠️ Advertencia: No se pudo auto-compilar el runner: ${err.message}`));
2354
+ console.warn(chalk.yellow(`\n⚠️ Advertencia: No se pudo auto-compilar el runner: ${err.message}`));
2287
2355
  }
2288
2356
 
2289
2357
  try {
2290
- // No test file path is passed — playwright.config.js uses testMatch:'agentic-runner.spec.ts'
2358
+ // No test file path is passed playwright.config.js uses testMatch:'agentic-runner.spec.ts'
2291
2359
  // This eliminates the Windows regex path issue permanently on any user's machine.
2292
2360
  await run('node', [
2293
2361
  '--no-warnings',
@@ -2299,7 +2367,7 @@ async function main() {
2299
2367
  cwd: PROJECT_ROOT,
2300
2368
  env: mergedEnv,
2301
2369
  onStatus: (msg) => { if (!process.argv.includes('--debug')) s.message(msg); else console.log(chalk.gray(`>> Status: ${msg}`)); },
2302
- onSpawn: (child) => { activeChildProcess = child; } // ← Capturar referencia para cancelHandler
2370
+ onSpawn: (child) => { activeChildProcess = child; } // Capturar referencia para cancelHandler
2303
2371
  });
2304
2372
 
2305
2373
  process.removeAllListeners('SIGINT');
@@ -2310,11 +2378,11 @@ async function main() {
2310
2378
  if (userCanceledMission) {
2311
2379
  missionResult = 'canceled';
2312
2380
  finalFailReason = 'user_canceled';
2313
- // Detener el spinner explícitamente antes de salir
2381
+ // Detener el spinner explícitamente antes de salir
2314
2382
  if (!process.argv.includes('--debug')) {
2315
- try { s.stop(chalk.yellow('⚠️ Misión cancelada por el usuario.')); } catch { }
2383
+ try { s.stop(chalk.yellow('⚠️ Misión cancelada por el usuario.')); } catch { }
2316
2384
  }
2317
- return; // El handler ya tomó el control, finally se ejecutará
2385
+ return; // El handler ya tomó el control, finally se ejecutará
2318
2386
  }
2319
2387
 
2320
2388
  const persistedMissionResult = readMissionResult(
@@ -2346,24 +2414,24 @@ async function main() {
2346
2414
  if (!process.argv.includes('--debug')) s.stop(chalk.green('\u2705 Mision completada exitosamente.'));
2347
2415
  else console.log(chalk.green('\u2705 Mision completada exitosamente.'));
2348
2416
 
2349
- // ── Ping Project ──
2417
+ // ── Ping Project ──
2350
2418
  await pingProject(finalProjectId, process.env.ARCALITY_API_KEY);
2351
2419
 
2352
- // ── Save Mission YAML ──
2420
+ // ── Save Mission YAML ──
2353
2421
  let saveDecision = 'no';
2354
2422
  if (!skipSavePrompt && !batchMode) {
2355
2423
  saveDecision = await select({
2356
- message: '¿Quieres guardar esta misión para ejecutarla de nuevo más tarde?',
2424
+ message: '¿Quieres guardar esta misión para ejecutarla de nuevo más tarde?',
2357
2425
  options: [
2358
- { label: '✅ Sí, guardar como YAML', value: 'yes' },
2359
- { label: '❌ No, gracias', value: 'no' }
2426
+ { label: ' Sí, guardar como YAML', value: 'yes' },
2427
+ { label: ' No, gracias', value: 'no' }
2360
2428
  ]
2361
2429
  });
2362
2430
  }
2363
2431
 
2364
2432
  if (saveDecision === 'yes') {
2365
2433
  const name = await text({
2366
- message: 'Nombre de la misión (ej., login_valido):',
2434
+ message: 'Nombre de la misión (ej., login_valido):',
2367
2435
  placeholder: 'my_mission',
2368
2436
  validate: v => v.length < 3 ? 'Nombre muy corto' : undefined
2369
2437
  });
@@ -2420,9 +2488,9 @@ async function main() {
2420
2488
  fs.mkdirSync(yamlOutputTargetDir, { recursive: true });
2421
2489
  const yamlPathOutput = path.join(yamlOutputTargetDir, `${safeName}.yaml`);
2422
2490
  fs.writeFileSync(yamlPathOutput, yamlData);
2423
- note(chalk.green(`✅ Misión guardada en: ${yamlPathOutput}`), 'Persistencia Arcality');
2491
+ note(chalk.green(`✅ Misión guardada en: ${yamlPathOutput}`), 'Persistencia Arcality');
2424
2492
  } else {
2425
- note(chalk.green(`✅ Misión guardada en: ${yamlPathMissions}`), 'Persistencia Arcality');
2493
+ note(chalk.green(`✅ Misión guardada en: ${yamlPathMissions}`), 'Persistencia Arcality');
2426
2494
  }
2427
2495
  }
2428
2496
  }
@@ -2454,7 +2522,7 @@ async function main() {
2454
2522
  if (userCanceledMission) {
2455
2523
  missionResult = 'canceled';
2456
2524
  finalFailReason = 'user_canceled';
2457
- finalFailReasoning = 'La misión fue detenida manualmente por el operador (Ctrl+C). Esto no es una falla técnica del sistema.';
2525
+ finalFailReasoning = 'La misión fue detenida manualmente por el operador (Ctrl+C). Esto no es una falla técnica del sistema.';
2458
2526
  await new Promise(r => setTimeout(r, 800));
2459
2527
  }
2460
2528
 
@@ -2463,9 +2531,9 @@ async function main() {
2463
2531
 
2464
2532
  const sRep = spinner();
2465
2533
  if (!process.argv.includes('--debug')) {
2466
- sRep.start('📊 Generando reporte...');
2534
+ sRep.start('📊 Generando reporte...');
2467
2535
  } else {
2468
- console.log('📊 Generando reporte...');
2536
+ console.log('📊 Generando reporte...');
2469
2537
  }
2470
2538
 
2471
2539
  await run("node", [path.join(PROJECT_ROOT, "scripts/rebrand-report.mjs")]).catch(() => { });
@@ -2496,7 +2564,7 @@ async function main() {
2496
2564
  }
2497
2565
  } catch(e) {}
2498
2566
 
2499
- // ── Upload Evidence (solo si NO fue cancelado por usuario) ──
2567
+ // ── Upload Evidence (solo si NO fue cancelado por usuario) ──
2500
2568
  if (missionResult !== 'canceled') {
2501
2569
  try {
2502
2570
  const { getEvidenceSasToken } = await import('../src/arcalityClient.mjs');
@@ -2510,7 +2578,7 @@ async function main() {
2510
2578
 
2511
2579
  if (sasData && actualSasUrl && actualBasePath) {
2512
2580
  if (!process.argv.includes('--debug')) {
2513
- sRep.start('☁️ Subiendo evidencia a Azure...');
2581
+ sRep.start('☁️ Subiendo evidencia a Azure...');
2514
2582
  }
2515
2583
  process.removeAllListeners('SIGINT');
2516
2584
  process.removeAllListeners('SIGTERM');
@@ -2524,26 +2592,26 @@ async function main() {
2524
2592
  finalReportUrl = `${parsedSas.origin}${parsedSas.pathname}/${actualBasePath}/index.html`;
2525
2593
  } catch(e) {}
2526
2594
 
2527
- if (!process.argv.includes('--debug')) sRep.stop(chalk.green('✅ Evidencia subida exitosamente.'));
2595
+ if (!process.argv.includes('--debug')) sRep.stop(chalk.green(' Evidencia subida exitosamente.'));
2528
2596
  }
2529
2597
  } catch (e) {
2530
- if (process.argv.includes('--debug')) console.log(chalk.yellow(`⚠️ Could not upload evidence: ${e.message}`));
2531
- if (!process.argv.includes('--debug')) sRep.stop(chalk.yellow('⚠️ Fallo al subir evidencia.'));
2598
+ if (process.argv.includes('--debug')) console.log(chalk.yellow(`⚠️ Could not upload evidence: ${e.message}`));
2599
+ if (!process.argv.includes('--debug')) sRep.stop(chalk.yellow('⚠️ Fallo al subir evidencia.'));
2532
2600
  }
2533
2601
  } else {
2534
- console.log(chalk.gray(' >> Saltando subida de evidencia (la misión fue cancelada).'));
2602
+ console.log(chalk.gray(' >> Saltando subida de evidencia (la misión fue cancelada).'));
2535
2603
  }
2536
2604
 
2537
- // ── End Mission — SIEMPRE se llama, independientemente de si la evidencia subió ──
2605
+ // ── End Mission SIEMPRE se llama, independientemente de si la evidencia subió ──
2538
2606
  try {
2539
2607
  const { endMission } = await import('../src/arcalityClient.mjs');
2540
2608
  await Promise.race([
2541
2609
  endMission(mission.mission_id, missionResult || 'failed', finalUsageData, finalFailReason, finalReportUrl, finalFailReasoning),
2542
2610
  new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 8000))
2543
2611
  ]);
2544
- if (process.argv.includes('--debug')) console.log(chalk.gray(`[DEBUG] endMission called → result: ${missionResult}, failReason: ${finalFailReason}`));
2612
+ if (process.argv.includes('--debug')) console.log(chalk.gray(`[DEBUG] endMission called result: ${missionResult}, failReason: ${finalFailReason}`));
2545
2613
  } catch (e) {
2546
- if (process.argv.includes('--debug')) console.log(chalk.yellow(`⚠️ Could not end mission in backend: ${e.message}`));
2614
+ if (process.argv.includes('--debug')) console.log(chalk.yellow(`⚠️ Could not end mission in backend: ${e.message}`));
2547
2615
  }
2548
2616
 
2549
2617
  if (globalReportProcess) {
@@ -2558,10 +2626,10 @@ async function main() {
2558
2626
  }
2559
2627
 
2560
2628
  if (!batchMode && fs.existsSync(arcalityPath)) {
2561
- console.log(chalk.blue(`🌐 Abriendo Reporte de Arcality: ${arcalityPath}`));
2629
+ console.log(chalk.blue(`🌐 Abriendo Reporte de Arcality: ${arcalityPath}`));
2562
2630
  openLocalFile(arcalityPath);
2563
2631
  } else if (!fs.existsSync(arcalityPath)) {
2564
- console.log(chalk.yellow(`⚠️ Reporte no generado (la prueba pudo haber fallado antes de completarse).`));
2632
+ console.log(chalk.yellow(`⚠️ Reporte no generado (la prueba pudo haber fallado antes de completarse).`));
2565
2633
  }
2566
2634
 
2567
2635
  await new Promise(resolve => setTimeout(resolve, 2000));
@@ -2572,7 +2640,7 @@ async function main() {
2572
2640
  new Promise(resolve => setTimeout(resolve, 5000))
2573
2641
  ]);
2574
2642
 
2575
- // Limpiar handlers del post-proceso ANTES de esperar Enter, ya que toda la telemetría y reportes ya finalizaron
2643
+ // Limpiar handlers del post-proceso ANTES de esperar Enter, ya que toda la telemetría y reportes ya finalizaron
2576
2644
  process.removeListener('SIGINT', postProcessSigint);
2577
2645
  process.removeListener('SIGTERM', postProcessSigint);
2578
2646
 
@@ -2584,10 +2652,10 @@ async function main() {
2584
2652
  }
2585
2653
 
2586
2654
  if (false && !batchMode) {
2587
- // Esperar Enter para regresar al menú
2655
+ // Esperar Enter para regresar al menú
2588
2656
  // Usamos escucha directa de stdin (raw mode) porque @clack/text
2589
- // no procesa Enter correctamente cuando setRawMode(true) está activo.
2590
- console.log(chalk.gray('\n Presiona Enter para regresar al menú...'));
2657
+ // no procesa Enter correctamente cuando setRawMode(true) está activo.
2658
+ console.log(chalk.gray('\n Presiona Enter para regresar al menú...'));
2591
2659
  await new Promise(resolve => {
2592
2660
  const onKey = (key) => {
2593
2661
  const keyStr = String(key);
@@ -2599,7 +2667,7 @@ async function main() {
2599
2667
  process.stdin.setRawMode(false);
2600
2668
  }
2601
2669
  // FIX: pausar stdin si vamos a salir para que el event loop no se quede colgado.
2602
- // Si volvemos al menú, no lo pausamos (para evitar el bug de exit en clack).
2670
+ // Si volvemos al menú, no lo pausamos (para evitar el bug de exit en clack).
2603
2671
  if (skipMenu) {
2604
2672
  process.stdin.pause();
2605
2673
  }
@@ -2612,22 +2680,22 @@ async function main() {
2612
2680
  }
2613
2681
  };
2614
2682
 
2615
- // ── BUGFIX: @clack/prompts llama explícitamente a process.stdin.pause()
2683
+ // ── BUGFIX: @clack/prompts llama explícitamente a process.stdin.pause()
2616
2684
  // al terminar cada select()/text(). En Node.js, agregar un listener 'data'
2617
- // a un stream EXPLÍCITAMENTE pausado NO lo reactiva automáticamente.
2618
- // Resultado: el event loop queda vacío y el proceso termina antes de que
2685
+ // a un stream EXPLÍCITAMENTE pausado NO lo reactiva automáticamente.
2686
+ // Resultado: el event loop queda vacío y el proceso termina antes de que
2619
2687
  // el usuario pueda presionar Enter.
2620
- // Solución: llamar resume() explícitamente antes de registrar el listener.
2688
+ // Solución: llamar resume() explícitamente antes de registrar el listener.
2621
2689
  if (process.stdin.isTTY && process.stdin.setRawMode) {
2622
2690
  process.stdin.setRawMode(true); // raw mode para capturar Enter inmediatamente
2623
2691
  }
2624
- process.stdin.resume(); // ← CRÍTICO: reactiva stdin para mantener el event loop vivo
2692
+ process.stdin.resume(); // CRÍTICO: reactiva stdin para mantener el event loop vivo
2625
2693
  process.stdin.on('data', onKey);
2626
2694
  });
2627
2695
  }
2628
2696
 
2629
2697
  if (skipMenu) {
2630
- outro(chalk.cyan('¡Misión terminada! Hasta luego.'));
2698
+ outro(chalk.cyan('¡Misión terminada! Hasta luego.'));
2631
2699
  return;
2632
2700
  }
2633
2701
 
@@ -2643,4 +2711,4 @@ async function main() {
2643
2711
  main().catch(err => {
2644
2712
  console.error(err);
2645
2713
  process.exit(1);
2646
- });
2714
+ });