@arcadialdev/arcality 4.1.0 → 4.1.2

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 +260 -170
  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 +1377 -244
  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:
@@ -640,6 +657,23 @@ async function waitForEnter(message = 'Presiona Enter para continuar...', option
640
657
  });
641
658
  }
642
659
 
660
+ function getSingleEmptyQueryParamName(urlString) {
661
+ if (!urlString) return null;
662
+
663
+ try {
664
+ const parsed = new URL(String(urlString));
665
+ const emptyParams = [];
666
+
667
+ for (const [key, value] of parsed.searchParams.entries()) {
668
+ if (!String(value || '').trim()) emptyParams.push(key);
669
+ }
670
+
671
+ return emptyParams.length === 1 ? emptyParams[0] : null;
672
+ } catch {
673
+ return null;
674
+ }
675
+ }
676
+
643
677
  function getMissionStartPath(missionSpec, collectionMeta = null, fallbackPath = '') {
644
678
  return missionSpec.startAt || missionSpec.page || missionSpec.pagePath || collectionMeta?.defaultPagePath || fallbackPath;
645
679
  }
@@ -1114,7 +1148,8 @@ async function runCollectionBatch({
1114
1148
  ARCALITY_RUN_REPORTS_DIR: mission.reportsDir,
1115
1149
  ARCALITY_RUN_PLAYWRIGHT_OUTPUT_DIR: mission.playwrightOutputDir,
1116
1150
  ARCALITY_COLLECTION_RUN_ID: runId,
1117
- ARCALITY_COLLECTION_MISSION_SLUG: mission.slug
1151
+ ARCALITY_COLLECTION_MISSION_SLUG: mission.slug,
1152
+ ARCALITY_DB_VALIDATIONS_JSON: mission.missionSpec.dbValidations.length > 0 ? JSON.stringify(mission.missionSpec.dbValidations) : ''
1118
1153
  };
1119
1154
 
1120
1155
  if (runnerPrebuilt) {
@@ -1214,20 +1249,20 @@ function showBanner() {
1214
1249
  } catch { }
1215
1250
 
1216
1251
  const sentinelAscii = `
1217
- █████ ██████ ██████ █████ ██ ██ ████████ ██ ██
1218
- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
1219
- ███████ ██████ ██ ███████ ██ ██ ██ ████
1220
- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
1221
- ██ ██ ██ ██ ██████ ██ ██ ███████ ██ ██ ██ `;
1252
+ █████ ██████ ██████ █████ ██ ██ ████████ ██ ██
1253
+ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
1254
+ ███████ ██████ ██ ███████ ██ ██ ██ ████
1255
+ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
1256
+ ██ ██ ██ ██ ██████ ██ ██ ███████ ██ ██ ██ `;
1222
1257
 
1223
1258
  intro(chalk.gray(sentinelAscii));
1224
1259
 
1225
1260
  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}`),
1261
+ chalk.gray(' Motor de Observación: ') + chalk.bold.white('EN LÍNEA') + '\n' +
1262
+ chalk.gray(' Módulo de Recuperación de UI: ') + chalk.bold.white('ACTIVO') + '\n' +
1263
+ chalk.gray(' Lógica Forense: ') + chalk.bold.white('ACTIVA') + '\n' +
1264
+ chalk.gray(''.repeat(50)) + '\n' +
1265
+ chalk.gray('Versión del Sistema: ') + chalk.white(`v${version}`),
1231
1266
  'Estado Central de Arcality'
1232
1267
  );
1233
1268
 
@@ -1236,8 +1271,8 @@ function showBanner() {
1236
1271
  // Show arcality.config status instead of multi-config switching
1237
1272
  const hasConfig = !!projectConfig;
1238
1273
  const configStatus = hasConfig
1239
- ? chalk.green('◉ Sincronización Establecida')
1240
- : chalk.red('⚠ Desconectado — Ejecuta `arcality init`');
1274
+ ? chalk.green(' Sincronización Establecida')
1275
+ : chalk.red('Desconectado Ejecuta `arcality init`');
1241
1276
 
1242
1277
  const { key: apiKeyVal, source: apiKeySource } = getApiKey();
1243
1278
  const apiKeyDisplay = apiKeyVal
@@ -1246,7 +1281,7 @@ function showBanner() {
1246
1281
 
1247
1282
  const infoLines = [
1248
1283
  chalk.gray('Nodo Objetivo: ') + chalk.white(process.env.NODE_ENV ?? 'development'),
1249
- chalk.gray('Token de Autenticación: ') + apiKeyDisplay,
1284
+ chalk.gray('Token de Autenticación: ') + apiKeyDisplay,
1250
1285
  chalk.gray('Stack: ') + chalk.white(techStack),
1251
1286
  ];
1252
1287
 
@@ -1256,10 +1291,10 @@ function showBanner() {
1256
1291
  chalk.gray('ID de Identidad: ') + chalk.white(projectConfig.projectId || 'N/A'),
1257
1292
  );
1258
1293
  } else {
1259
- infoLines.push(chalk.gray('Configuración: ') + configStatus);
1294
+ infoLines.push(chalk.gray('Configuración: ') + configStatus);
1260
1295
  }
1261
1296
 
1262
- note(infoLines.join('\n'), 'Telemetría del Objetivo');
1297
+ note(infoLines.join('\n'), 'Telemetría del Objetivo');
1263
1298
  }
1264
1299
 
1265
1300
  const ARCALITY_BRAIN_URL = "https://api.anthropic.com/v1/messages";
@@ -1389,7 +1424,7 @@ function run(cmd, args, options = {}) {
1389
1424
  // properly wraps arguments with spaces in quotes natively for CreateProcessW on Windows.
1390
1425
  //
1391
1426
  // 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.
1427
+ // showing the "¿Desea terminar el trabajo por lotes (S/N)?" prompt on Ctrl+C.
1393
1428
  // When stdin is 'inherit', cmd.exe intercepts Ctrl+C at the console level and kills
1394
1429
  // the entire process tree before our SIGINT handler can run, bypassing the finally block.
1395
1430
  const p = spawn(cmd, args, {
@@ -1438,9 +1473,9 @@ function run(cmd, args, options = {}) {
1438
1473
  fs.writeFileSync(lastRunLog, outputBuffer);
1439
1474
  } catch (e) { }
1440
1475
 
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
1476
+ // code===0 clean success
1477
+ // code===null + signal killed by signal (Ctrl+C) treat as non-fatal so finally runs
1478
+ // code===1 test failed (Playwright returns 1 on test failure) also non-fatal
1444
1479
  if (code === 0 || signal !== null || code === 1) resolve();
1445
1480
  else {
1446
1481
  if (isDebug) console.error(chalk.red(`\n[DEBUG] Process exited with code ${code}`));
@@ -1464,7 +1499,7 @@ function runNpmScript(args, options = {}) {
1464
1499
 
1465
1500
  /**
1466
1501
  * Ping the project to register activity.
1467
- * Uses internal API URL — not user-configurable.
1502
+ * Uses internal API URL not user-configurable.
1468
1503
  */
1469
1504
  async function pingProject(projectId, apiKey) {
1470
1505
  if (!projectId || !apiKey) return;
@@ -1479,7 +1514,7 @@ async function pingProject(projectId, apiKey) {
1479
1514
  async function main() {
1480
1515
  process.chdir(PROJECT_ROOT);
1481
1516
 
1482
- // Backward compat: initialize .env if ACTIVE_CONFIG missing (legacy)
1517
+ // Read local .env only for user-managed variables that may still exist.
1483
1518
  let initialEnv = parseDotEnvFile(path.join(PROJECT_ROOT, '.env'));
1484
1519
  const envProjectId = initialEnv.ARCALITY_PROJECT_ID || process.env.ARCALITY_PROJECT_ID;
1485
1520
  if (envProjectId && !isRealProjectId(envProjectId)) {
@@ -1489,16 +1524,6 @@ async function main() {
1489
1524
  process.exit(1);
1490
1525
  }
1491
1526
 
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
1527
 
1503
1528
  const argv = process.argv.slice(2);
1504
1529
  let initialDiscoverPath = null;
@@ -1570,6 +1595,7 @@ async function main() {
1570
1595
  let collectionConcurrency = firstRun ? initialCollectionConcurrency : null;
1571
1596
  let skipSavePrompt = firstRun ? initialSkipSavePrompt : false;
1572
1597
  let batchMode = firstRun ? initialBatchMode : false;
1598
+ let missionDbValidations = [];
1573
1599
 
1574
1600
  let skipMenu = (firstRun && (prompt || agentMode || smartMode || runAllMode || templateMode)) || agentMode || runAllMode || templateMode;
1575
1601
  if (agentMode) skipMenu = true; // Hard-lock: No menu in agent mode
@@ -1579,7 +1605,7 @@ async function main() {
1579
1605
  if (!skipMenu) {
1580
1606
  showBanner();
1581
1607
  try {
1582
- // ── Build menu options (single config mode) ──
1608
+ // ── Build menu options (single config mode) ──
1583
1609
  const missionsDir = process.env.MISSIONS_DIR;
1584
1610
  const savedMissionCollections = listMissionCollections(missionsDir);
1585
1611
 
@@ -1601,38 +1627,40 @@ async function main() {
1601
1627
  const rootYamlFiles = rootMissionFiles;
1602
1628
 
1603
1629
  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' }
1630
+ { label: 'Ejecutar mision', value: 'agent' },
1631
+ ...(savedMissionCollections.length ? [{ label: 'Ejecutar coleccion de misiones', value: 'run_collection' }] : []),
1632
+ ...(savedMissionCollections.length ? [{ label: 'Ejecutar YAML guardado (.yaml)', value: 'launch_saved' }] : []),
1633
+ ...(yamlOutputCollections.length ? [{ label: 'Reusar YAML generado', value: 'reuse_yaml' }] : []),
1634
+ ...(rootYamlFiles.length ? [{ label: 'Abrir YAML desde la raiz', value: 'yaml_list' }] : []),
1635
+ { label: 'Descubrir rutas y generar misiones', value: 'generate_missions' },
1636
+ { label: 'Crear plantillas YAML base', value: 'generate_templates' },
1637
+ { label: 'Editar configuracion actual', value: 'reconfigure' },
1638
+ { label: 'Configurar o reconectar proyecto', value: 'full_reconfigure' },
1639
+ { label: 'Salir de Arcality', value: 'exit' }
1612
1640
  ];
1613
1641
 
1614
1642
  const menuOptions = options
1615
1643
  .filter(option => !['launch_saved', 'reuse_yaml', 'yaml_list'].includes(option.value))
1616
1644
  .map(option => option.value === 'generate_templates'
1617
- ? { ...option, label: '◫ Generar plantillas de misión' }
1645
+ ? { ...option, label: 'Crear plantillas YAML base' }
1618
1646
  : option
1619
1647
  );
1620
1648
 
1621
1649
  if (hasSavedMissionSources) {
1622
1650
  const insertIndex = Math.min(menuOptions.length, 2);
1623
1651
  menuOptions.splice(insertIndex, 0, {
1624
- label: '◌ Abrir misión guardada',
1652
+ label: 'Ejecutar mision guardada',
1625
1653
  value: 'open_saved_mission'
1626
1654
  });
1627
1655
  }
1628
1656
 
1629
1657
  const action = await select({
1630
- message: '¿Qué te gustaría hacer?',
1658
+ message: '¿Qué te gustaría hacer?',
1631
1659
  options: menuOptions
1632
1660
  });
1633
1661
 
1634
1662
  if (isCancel(action) || action === 'exit') {
1635
- outro(chalk.cyan('¡Hasta luego!'));
1663
+ outro(chalk.cyan('¡Hasta luego!'));
1636
1664
  break;
1637
1665
  }
1638
1666
 
@@ -1652,11 +1680,43 @@ async function main() {
1652
1680
  const yamlContent = fs.readFileSync(selectedMission.fullPath, 'utf8');
1653
1681
  const missionSpec = normalizeMissionSpec(loadYaml(yamlContent), selectedMission.fullPath);
1654
1682
  prompt = missionSpec.prompt || yamlContent;
1683
+ missionDbValidations = missionSpec.dbValidations;
1655
1684
  discoverPath = getMissionStartPath(
1656
1685
  missionSpec,
1657
1686
  readCollectionMeta(path.dirname(selectedMission.fullPath), selectedMission.collection)
1658
1687
  );
1659
1688
  agentMode = true;
1689
+ } else if (action === 'generate_missions') {
1690
+ const generateScript = path.join(PROJECT_ROOT, 'scripts', 'generate.mjs');
1691
+ const generationMode = await select({
1692
+ message: 'Como quieres usar la autogeneracion?',
1693
+ options: [
1694
+ { label: 'Solo vista previa (sin archivos)', value: 'dry_run' },
1695
+ { label: 'Generar archivos YAML ahora', value: 'write_yaml' },
1696
+ { label: 'Cancelar', value: 'cancel' }
1697
+ ]
1698
+ });
1699
+ if (isCancel(generationMode) || generationMode === 'cancel') continue;
1700
+
1701
+ try {
1702
+ await new Promise((resolve, reject) => {
1703
+ const childArgs = generationMode === 'dry_run'
1704
+ ? [generateScript, '--dry-run']
1705
+ : [generateScript];
1706
+ const child = spawn('node', childArgs, {
1707
+ stdio: 'inherit',
1708
+ cwd: ORIGINAL_CWD,
1709
+ env: { ...process.env, ARCALITY_ROOT: PROJECT_ROOT }
1710
+ });
1711
+ child.on('exit', code => code === 0 ? resolve() : reject(new Error(`Generate exited with ${code}`)));
1712
+ child.on('error', reject);
1713
+ });
1714
+ } catch (e) {
1715
+ console.log(chalk.red(`\nAutogeneracion fallida: ${e.message}`));
1716
+ }
1717
+
1718
+ await waitForEnter('Presiona Enter para volver al menu...');
1719
+ continue;
1660
1720
  } else if (action === 'generate_templates') {
1661
1721
  const createdMain = ensureMissionTemplates(missionsDir);
1662
1722
  const yamlDir = projectConfig ? getYamlOutputDir(projectConfig, ORIGINAL_CWD) : null;
@@ -1671,7 +1731,30 @@ async function main() {
1671
1731
  await waitForEnter('Presiona Enter para volver al menu...');
1672
1732
  continue;
1673
1733
  } else if (action === 'reconfigure') {
1674
- // Launch arcality init in the user's project directory
1734
+ const editorScript = path.join(PROJECT_ROOT, 'scripts', 'edit-config.mjs');
1735
+ try {
1736
+ await new Promise((resolve, reject) => {
1737
+ const child = spawn('node', [editorScript], {
1738
+ stdio: 'inherit',
1739
+ cwd: ORIGINAL_CWD,
1740
+ env: { ...process.env, ARCALITY_ROOT: PROJECT_ROOT }
1741
+ });
1742
+ child.on('exit', code => code === 0 ? resolve() : reject(new Error(`Config editor exited with ${code}`)));
1743
+ child.on('error', reject);
1744
+ });
1745
+
1746
+ projectConfig = loadProjectConfig(ORIGINAL_CWD) || loadProjectConfig(PROJECT_ROOT);
1747
+ if (projectConfig) {
1748
+ injectConfigToEnv(projectConfig);
1749
+ }
1750
+ setupEnvironment();
1751
+
1752
+ } catch (e) {
1753
+ console.log(chalk.red(`\nConfig update failed: ${e.message}`));
1754
+ await new Promise(r => setTimeout(r, 3000));
1755
+ }
1756
+ continue;
1757
+ } else if (action === 'full_reconfigure') {
1675
1758
  const initScript = path.join(PROJECT_ROOT, 'scripts', 'init.mjs');
1676
1759
  try {
1677
1760
  await new Promise((resolve, reject) => {
@@ -1683,18 +1766,15 @@ async function main() {
1683
1766
  child.on('exit', code => code === 0 ? resolve() : reject(new Error(`Init exited with ${code}`)));
1684
1767
  child.on('error', reject);
1685
1768
  });
1686
-
1687
- // ── REFRESH CONFIG ──
1688
- // Recargar el archivo que init.mjs acaba de escribir
1769
+
1689
1770
  projectConfig = loadProjectConfig(ORIGINAL_CWD) || loadProjectConfig(PROJECT_ROOT);
1690
1771
  if (projectConfig) {
1691
1772
  injectConfigToEnv(projectConfig);
1692
1773
  }
1693
1774
  setupEnvironment();
1694
-
1775
+
1695
1776
  } catch (e) {
1696
- // Mostrar el error claramente antes de que console.clear() lo borre
1697
- console.log(chalk.red(`\n❌ Reconfigure failed: ${e.message}`));
1777
+ console.log(chalk.red(`\nReconnect failed: ${e.message}`));
1698
1778
  await new Promise(r => setTimeout(r, 3000));
1699
1779
  }
1700
1780
  continue;
@@ -1709,6 +1789,7 @@ async function main() {
1709
1789
  const yamlContent = fs.readFileSync(selectedMission.fullPath, 'utf8');
1710
1790
  const missionSpec = normalizeMissionSpec(loadYaml(yamlContent), selectedMission.fullPath);
1711
1791
  prompt = missionSpec.prompt || yamlContent;
1792
+ missionDbValidations = missionSpec.dbValidations;
1712
1793
  discoverPath = getMissionStartPath(missionSpec, readCollectionMeta(path.dirname(selectedMission.fullPath), selectedMission.collection));
1713
1794
  agentMode = true;
1714
1795
  } else if (action === 'reuse_yaml') {
@@ -1723,6 +1804,7 @@ async function main() {
1723
1804
  const yamlContent = fs.readFileSync(selectedMission.fullPath, 'utf8');
1724
1805
  const missionSpec = normalizeMissionSpec(loadYaml(yamlContent), selectedMission.fullPath);
1725
1806
  prompt = missionSpec.prompt || yamlContent;
1807
+ missionDbValidations = missionSpec.dbValidations;
1726
1808
  discoverPath = getMissionStartPath(missionSpec, readCollectionMeta(path.dirname(selectedMission.fullPath), selectedMission.collection));
1727
1809
  agentMode = true;
1728
1810
  } else if (action === 'yaml_list') {
@@ -1735,6 +1817,7 @@ async function main() {
1735
1817
  const yamlContent = fs.readFileSync(path.join(PROJECT_ROOT, sel), 'utf8');
1736
1818
  const missionSpec = normalizeMissionSpec(loadYaml(yamlContent), path.join(PROJECT_ROOT, sel));
1737
1819
  prompt = missionSpec.prompt || yamlContent;
1820
+ missionDbValidations = missionSpec.dbValidations;
1738
1821
  discoverPath = getMissionStartPath(missionSpec);
1739
1822
  agentMode = true;
1740
1823
  }
@@ -1765,7 +1848,7 @@ async function main() {
1765
1848
  if (!batchCollection && process.stdin.isTTY) {
1766
1849
  const availableCollections = listMissionCollections(process.env.MISSIONS_DIR);
1767
1850
  const selectedBatchCollection = await select({
1768
- message: 'Elige la colección a ejecutar:',
1851
+ message: 'Elige la colección a ejecutar:',
1769
1852
  options: [
1770
1853
  { label: `Todas las colecciones (${availableCollections.length})`, value: '__ALL__' },
1771
1854
  ...availableCollections.map(collection => ({
@@ -1823,16 +1906,16 @@ async function main() {
1823
1906
  if (agentMode) {
1824
1907
  if (!prompt || prompt.trim().length < 4) {
1825
1908
  const p = await text({
1826
- message: chalk.cyan('🤖 Misión para Arcality (Requerida):'),
1909
+ message: chalk.cyan('🤖 Misión para Arcality (Requerida):'),
1827
1910
  placeholder: 'Ej., Llena el formulario de registro con datos aleatorios',
1828
1911
  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).';
1912
+ if (!v || !v.trim()) return 'La misión es requerida para iniciar.';
1913
+ if (v.trim().length < 4) return 'La misión debe ser más descriptiva (mínimo 4 caracteres).';
1831
1914
  }
1832
1915
  });
1833
1916
 
1834
1917
  if (isCancel(p)) {
1835
- cancel('Misión cancelada. Regresando al menú principal...');
1918
+ cancel('Misión cancelada. Regresando al menú principal...');
1836
1919
  agentMode = false;
1837
1920
  prompt = "";
1838
1921
  skipMenu = false;
@@ -1843,9 +1926,10 @@ async function main() {
1843
1926
 
1844
1927
  if (!discoverPath) {
1845
1928
  const knownBaseUrl = projectConfig?.project?.baseUrl || process.env.BASE_URL;
1929
+ const emptyQueryParamName = getSingleEmptyQueryParamName(knownBaseUrl);
1846
1930
  const promptMsg = knownBaseUrl
1847
- ? `🌐 URL Completa de Navegación (relative to ${knownBaseUrl}):`
1848
- : '🌐 URL Completa de Navegación (ej., http://localhost:3000/):';
1931
+ ? `🌐 URL Completa de Navegación (relative to ${knownBaseUrl}). Deja vacío para usarla tal cual${emptyQueryParamName ? ` o escribe solo el valor de "${emptyQueryParamName}"` : ''}:`
1932
+ : '🌐 URL Completa de Navegación (ej., http://localhost:3000/):';
1849
1933
 
1850
1934
  const d = await text({
1851
1935
  message: chalk.cyan(promptMsg),
@@ -1857,13 +1941,13 @@ async function main() {
1857
1941
  });
1858
1942
 
1859
1943
  if (isCancel(d)) {
1860
- cancel('Misión abortada. Regresando al menú...');
1944
+ cancel('Misión abortada. Regresando al menú...');
1861
1945
  agentMode = false;
1862
1946
  prompt = "";
1863
1947
  skipMenu = false;
1864
1948
  continue;
1865
1949
  }
1866
- discoverPath = d;
1950
+ discoverPath = typeof d === 'string' ? d.trim() : d;
1867
1951
  }
1868
1952
  }
1869
1953
 
@@ -1873,7 +1957,7 @@ async function main() {
1873
1957
  const libDotEnv = parseDotEnvFile(path.join(PROJECT_ROOT, '.env'));
1874
1958
  const dotEnv = { ...userDotEnv, ...libDotEnv };
1875
1959
 
1876
- // ── API Key and Quota Validation ──
1960
+ // ── API Key and Quota Validation ──
1877
1961
  const { validateApiKey, startMission: requestMission } = await import('../src/arcalityClient.mjs');
1878
1962
 
1879
1963
  const validation = await validateApiKey();
@@ -1887,13 +1971,13 @@ async function main() {
1887
1971
  }
1888
1972
  if (!validation.valid) {
1889
1973
  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',
1974
+ 'no_api_key': '🔑 API Key no encontrada.\n Configúrala vía `arcality init` o en .env (ARCALITY_API_KEY)',
1975
+ 'invalid_format': '🔑 API Key inválida. Debe comenzar con "arc_"',
1976
+ 'invalid_api_key': '🔑 API Key no reconocida por el servidor',
1977
+ 'plan_expired': '💳 Tu plan ha expirado',
1894
1978
  'backend_unavailable': 'No se pudo conectar con Arcality. La herramienta requiere conexion al backend para validar la cuenta.'
1895
1979
  };
1896
- note(chalk.red(errorMessages[validation.error] || `Error: ${validation.error}`), '❌ Autenticación');
1980
+ note(chalk.red(errorMessages[validation.error] || `Error: ${validation.error}`), ' Autenticación');
1897
1981
  agentMode = false;
1898
1982
  prompt = "";
1899
1983
  skipMenu = false;
@@ -1902,11 +1986,11 @@ async function main() {
1902
1986
 
1903
1987
  // Show plan info
1904
1988
  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;
1989
+ const dailyLimitDisplay = validation.daily_limit === -1 ? '' : (validation.daily_limit || 35);
1990
+ const remainingDisplay = validation.remaining >= 999999 ? '' : validation.remaining;
1907
1991
  note(
1908
- chalk.gray('Conexión: ') + chalk.cyanBright('SECURE ') + modeLabel + '\n' +
1909
- chalk.gray('Autorización: ') + chalk.white(validation.plan || 'internal') + '\n' +
1992
+ chalk.gray('Conexión: ') + chalk.cyanBright('SECURE ') + modeLabel + '\n' +
1993
+ chalk.gray('Autorización: ') + chalk.white(validation.plan || 'internal') + '\n' +
1910
1994
  chalk.gray('Misiones: ') + chalk.white(`${validation.daily_used || 0}/${dailyLimitDisplay}`) + '\n' +
1911
1995
  chalk.gray('Reservas: ') + chalk.white(remainingDisplay),
1912
1996
  'Enlace Arcality'
@@ -1944,15 +2028,15 @@ async function main() {
1944
2028
  const projects = data.projects || [];
1945
2029
 
1946
2030
  const projOptions = projects.map(p => ({ label: p.name || p.Name, value: p.id || p.Id }));
1947
- projOptions.push({ label: 'âž• Crear nuevo proyecto', value: 'new' });
2031
+ projOptions.push({ label: ' Crear nuevo proyecto', value: 'new' });
1948
2032
 
1949
2033
  const projSelection = await select({
1950
- message: chalk.cyan('Elige un proyecto para esta misión:'),
2034
+ message: chalk.cyan('Elige un proyecto para esta misión:'),
1951
2035
  options: projOptions
1952
2036
  });
1953
2037
 
1954
2038
  if (isCancel(projSelection)) {
1955
- cancel('Misión abortada.');
2039
+ cancel('Misión abortada.');
1956
2040
  agentMode = false; prompt = ""; skipMenu = false; continue;
1957
2041
  }
1958
2042
 
@@ -1978,9 +2062,9 @@ async function main() {
1978
2062
  note(chalk.red('El backend no devolvio un Project ID valido.'), 'Proyecto no conectado');
1979
2063
  selectedProjectId = null;
1980
2064
  }
1981
- if (selectedProjectId) note(chalk.green(`✅ Proyecto creado: ${newName}`));
2065
+ if (selectedProjectId) note(chalk.green(`✅ Proyecto creado: ${newName}`));
1982
2066
  } else {
1983
- note(chalk.red(`❌ Falló al crear proyecto.`));
2067
+ note(chalk.red(`❌ Falló al crear proyecto.`));
1984
2068
  }
1985
2069
  } else {
1986
2070
  selectedProjectId = projSelection;
@@ -2013,8 +2097,12 @@ async function main() {
2013
2097
  continue;
2014
2098
  }
2015
2099
 
2016
- // ── Start Mission ──
2017
- const mission = await requestMission(prompt, discoverPath || '/');
2100
+ // ── Start Mission ──
2101
+ const effectiveBaseUrl = projectConfig?.project?.baseUrl || dotEnv.BASE_URL || process.env.BASE_URL || '';
2102
+ const missionTargetUrl = discoverPath === '' && effectiveBaseUrl
2103
+ ? effectiveBaseUrl
2104
+ : (discoverPath || '/');
2105
+ const mission = await requestMission(prompt, missionTargetUrl);
2018
2106
  if (!mission.allowed) {
2019
2107
  if (mission.error === 'mission_limit_exceeded') {
2020
2108
  note(
@@ -2044,19 +2132,19 @@ async function main() {
2044
2132
 
2045
2133
  const s = spinner();
2046
2134
  if (!process.argv.includes('--debug')) {
2047
- s.start(`â—‰ [ARCALITY INICIADO] Ejecutando directiva: "${prompt}" ${chalk.gray(`(${mission.daily_used}/${mission.daily_limit})`)}`);
2135
+ s.start(`◉ [ARCALITY INICIADO] Ejecutando directiva: "${prompt}" ${chalk.gray(`(${mission.daily_used}/${mission.daily_limit})`)}`);
2048
2136
  } else {
2049
- console.log(chalk.cyan(`\nâ—‰ [ARCALITY INICIADO] Ejecutando directiva: "${prompt}" ${chalk.gray(`(${mission.daily_used}/${mission.daily_limit})`)}`));
2137
+ console.log(chalk.cyan(`\n [ARCALITY INICIADO] Ejecutando directiva: "${prompt}" ${chalk.gray(`(${mission.daily_used}/${mission.daily_limit})`)}`));
2050
2138
  }
2051
2139
 
2052
2140
  const finalProjectId = process.env.ARCALITY_PROJECT_ID || selectedProjectId;
2053
2141
 
2054
- // ── Cancelación Manual (Ctrl+C) — Estrategia bulletproof ──
2142
+ // ── Cancelación Manual (Ctrl+C) Estrategia bulletproof ──
2055
2143
  // El problema: @clack/prompts registra su propio SIGINT handler cada tick del spinner,
2056
2144
  // que llama a process.exit(0) directamente. removeAllListeners() no es suficiente porque
2057
- // Clack lo re-registra en cada frame de animación.
2145
+ // Clack lo re-registra en cada frame de animación.
2058
2146
  //
2059
- // La solución: interceptar process.exit() antes de que Clack lo llame,
2147
+ // La solución: interceptar process.exit() antes de que Clack lo llame,
2060
2148
  // y usar prependListener para que NUESTRO handler siempre se ejecute primero.
2061
2149
 
2062
2150
  let activeChildProcess = null;
@@ -2068,18 +2156,18 @@ async function main() {
2068
2156
  const _origProcessExit = process.exit.bind(process);
2069
2157
  process.exit = (code) => {
2070
2158
  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.
2159
+ // Clack (u otra lib) está intentando matar el proceso lo bloqueamos.
2160
+ // Nuestro cancelHandler ya habrá sido invocado vía prependListener.
2161
+ // El proceso se cerrará naturalmente cuando el child process termine.
2074
2162
  return;
2075
2163
  }
2076
2164
  _origProcessExit(code);
2077
2165
  };
2078
2166
 
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.
2167
+ // ── RESTORE RAW MODE FOR WINDOWS FIX ──
2168
+ // @clack/prompts pausea stdin y desactiva raw mode al terminar un menú.
2169
+ // Debemos reactivarlo aquí para que nuestro interceptor global de Ctrl+C
2170
+ // siga funcionando durante toda la misión.
2083
2171
  if (process.stdin.isTTY && process.stdin.setRawMode) {
2084
2172
  try {
2085
2173
  process.stdin.setRawMode(true);
@@ -2090,21 +2178,21 @@ async function main() {
2090
2178
  const cancelHandler = () => {
2091
2179
  sigintCount++;
2092
2180
  if (sigintCount > 3) {
2093
- console.log(chalk.red('\n🛑 Forzando salida inmediata...'));
2181
+ console.log(chalk.red('\n🛑 Forzando salida inmediata...'));
2094
2182
  _missionActive = false;
2095
2183
  _origProcessExit(1);
2096
2184
  return;
2097
2185
  }
2098
2186
 
2099
2187
  if (userCanceledMission) {
2100
- console.log(chalk.yellow('\n⏳ Cancelación en progreso, esperando reporte... (Ctrl+C x4 para forzar cierre)'));
2188
+ console.log(chalk.yellow('\n Cancelación en progreso, esperando reporte... (Ctrl+C x4 para forzar cierre)'));
2101
2189
  return;
2102
2190
  }
2103
2191
  userCanceledMission = true;
2104
2192
 
2105
- console.log(chalk.yellow('\n\n⚠️ [ARCALITY] Directiva abortada por el operador.'));
2193
+ console.log(chalk.yellow('\n\n⚠️ [ARCALITY] Directiva abortada por el operador.'));
2106
2194
  if (!process.argv.includes('--debug')) {
2107
- try { s.stop(chalk.yellow('⚠️ Directiva abortada.')); } catch { /* ignore */ }
2195
+ try { s.stop(chalk.yellow('⚠️ Directiva abortada.')); } catch { /* ignore */ }
2108
2196
  }
2109
2197
 
2110
2198
  // Terminar el proceso hijo de Playwright directamente (Windows-safe)
@@ -2136,8 +2224,8 @@ async function main() {
2136
2224
  } catch { /* silencioso */ }
2137
2225
 
2138
2226
  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
2227
+ // Playwright también recibe el SIGINT nativamente desde la terminal Windows
2228
+ // y cerrará su proceso limpiamente, lo que resolverá la Promise del run() finally
2141
2229
  };
2142
2230
 
2143
2231
  // prependListener asegura que nuestro handler se ejecuta ANTES que el de Clack
@@ -2145,19 +2233,20 @@ async function main() {
2145
2233
  process.prependListener('SIGTERM', cancelHandler);
2146
2234
 
2147
2235
 
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,
2236
+ // Build env for the runner merge arcality.config values.
2237
+ // ARCALITY_API_URL must be explicit it comes from the library's internal config,
2150
2238
  // not from the user's project .env, so we inject it directly via getApiUrl().
2151
2239
  const fallbackBaseUrl = (!projectConfig && discoverPath?.startsWith('http')) ? discoverPath : undefined;
2152
2240
 
2153
2241
  const mergedEnv = {
2154
2242
  ...dotEnv,
2155
2243
  ...process.env,
2156
- ARCALITY_API_URL: getApiUrl(), // Internal URL — always injected explicitly
2244
+ ARCALITY_ROOT: PROJECT_ROOT,
2245
+ ARCALITY_API_URL: getApiUrl(), // Internal URL — always injected explicitly
2157
2246
  ARCALITY_PROJECT_ID: finalProjectId,
2158
2247
  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
2248
+ ARCALITY_MISSION_ID: mission.mission_id || mission.missionId || mission.MissionId || '', // Propagate mission_id to every proxy call
2249
+ ARCALITY_ORG_ID: validation.organization_id || validation.organizationId || validation.OrganizationId || process.env.ARCALITY_ORG_ID || '', // Propagate org_id for pattern persistence
2161
2250
  BASE_URL: projectConfig?.project?.baseUrl || dotEnv.BASE_URL || process.env.BASE_URL || fallbackBaseUrl || 'http://localhost:3000',
2162
2251
  LOGIN_USER: projectConfig?.auth?.username || dotEnv.LOGIN_USER || process.env.LOGIN_USER,
2163
2252
  LOGIN_PASSWORD: projectConfig?.auth?.password || dotEnv.LOGIN_PASSWORD || process.env.LOGIN_PASSWORD,
@@ -2168,7 +2257,8 @@ async function main() {
2168
2257
  CONTEXT_DIR: process.env.CONTEXT_DIR,
2169
2258
  REPORTS_DIR: process.env.REPORTS_DIR,
2170
2259
  SMART_PROMPT: prompt,
2171
- TARGET_PATH: discoverPath || '/'
2260
+ TARGET_PATH: discoverPath ?? '/',
2261
+ ARCALITY_DB_VALIDATIONS_JSON: missionDbValidations.length > 0 ? JSON.stringify(missionDbValidations) : (process.env.ARCALITY_DB_VALIDATIONS_JSON || process.env.ARCALITY_DB_VALIDATIONS || '')
2172
2262
  };
2173
2263
 
2174
2264
  if (finalProjectId) {
@@ -2176,17 +2266,17 @@ async function main() {
2176
2266
  console.log(chalk.gray(`>> ARCALITY_ORG_ID: ${mergedEnv.ARCALITY_ORG_ID || 'NONE'}`));
2177
2267
  }
2178
2268
 
2179
- // ── Resolve execution paths ──
2269
+ // ── Resolve execution paths ──
2180
2270
  // tsx and playwright are HOISTED to ORIGINAL_CWD/node_modules when installed via npm.
2181
2271
  // We run from ORIGINAL_CWD so relative paths for playwright work.
2182
2272
  // For the test file and config (inside the arcality package), we use absolute paths.
2183
2273
  //
2184
- // KEY: tsx loader uses file:// URL (pathToFileURL) so spaces encode as %20 — no quoting needed.
2274
+ // KEY: tsx loader uses file:// URL (pathToFileURL) so spaces encode as %20 no quoting needed.
2185
2275
 
2186
2276
  const testFile = path.join(PROJECT_ROOT, 'tests', '_helpers', 'agentic-runner.spec.ts');
2187
2277
  const configFile = path.join(PROJECT_ROOT, 'playwright.config.js');
2188
2278
 
2189
- // Resolve playwright CLI — CRITICAL singleton alignment:
2279
+ // Resolve playwright CLI CRITICAL singleton alignment:
2190
2280
  //
2191
2281
  // @playwright/test uses `playwright` internally. When npm detects a version mismatch
2192
2282
  // between the top-level `playwright` and what `@playwright/test` needs, it installs
@@ -2194,15 +2284,15 @@ async function main() {
2194
2284
  //
2195
2285
  // Playwright's test runner tracks the active suite via a module-level singleton
2196
2286
  // (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()
2287
+ // @playwright/test loaded, their singletons are out of sync "did not expect test()
2198
2288
  // to be called here".
2199
2289
  //
2200
2290
  // Fix: ALWAYS prefer the playwright nested INSIDE @playwright/test, because that is
2201
2291
  // guaranteed to be the same instance that @playwright/test uses for its internals.
2202
2292
  //
2203
2293
  // Resolution priority:
2204
- // 1. <project>/@playwright/test/node_modules/playwright/cli.js ← nested (safe)
2205
- // 2. <project>/playwright/cli.js ← sibling top-level
2294
+ // 1. <project>/@playwright/test/node_modules/playwright/cli.js nested (safe)
2295
+ // 2. <project>/playwright/cli.js sibling top-level
2206
2296
  // (repeated for ORIGINAL_CWD and PROJECT_ROOT)
2207
2297
  let playwrightCli;
2208
2298
  try {
@@ -2221,9 +2311,9 @@ async function main() {
2221
2311
 
2222
2312
  if (!testRunnerPath) throw resolveError;
2223
2313
 
2224
- const testRunnerDir = path.dirname(testRunnerPath); // → .../node_modules/@playwright/test/
2314
+ const testRunnerDir = path.dirname(testRunnerPath); // .../node_modules/@playwright/test/
2225
2315
 
2226
- // Priority 1: playwright nested INSIDE @playwright/test — same singleton as the package uses
2316
+ // Priority 1: playwright nested INSIDE @playwright/test same singleton as the package uses
2227
2317
  const nestedCli = path.join(testRunnerDir, 'node_modules', 'playwright', 'cli.js');
2228
2318
  // Priority 2: playwright as a top-level sibling (clean installs, versions aligned)
2229
2319
  const siblingCli = path.join(testRunnerDir, '..', '..', 'playwright', 'cli.js');
@@ -2242,7 +2332,7 @@ async function main() {
2242
2332
  console.log(chalk.gray(`[DEBUG] Using playwright CLI: ${playwrightCli}`));
2243
2333
  }
2244
2334
  } catch (e) {
2245
- console.error(chalk.red('\n[FATAL] Núcleo de Playwright no encontrado. Asegúrate de que @playwright/test esté instalado.'));
2335
+ console.error(chalk.red('\n[FATAL] Núcleo de Playwright no encontrado. Asegúrate de que @playwright/test esté instalado.'));
2246
2336
  console.error(chalk.red(e.message));
2247
2337
  console.error(chalk.yellow('\nConsejo: Ejecuta `npm install @playwright/test` en tu directorio de proyecto.'));
2248
2338
  _missionActive = false;
@@ -2254,9 +2344,9 @@ async function main() {
2254
2344
  const postProcessSigint = () => {
2255
2345
  _postProcessSigintCount++;
2256
2346
  if (_postProcessSigintCount === 1) {
2257
- console.log(chalk.yellow('\n⏳ Finalizando reporte de telemetría, por favor espera... (Ctrl+C de nuevo para salir ya)'));
2347
+ console.log(chalk.yellow('\n Finalizando reporte de telemetría, por favor espera... (Ctrl+C de nuevo para salir ya)'));
2258
2348
  } else {
2259
- console.log(chalk.red('\n🛑 Saliendo forzado por el usuario.'));
2349
+ console.log(chalk.red('\n🛑 Saliendo forzado por el usuario.'));
2260
2350
  process.removeAllListeners('SIGINT');
2261
2351
  process.removeAllListeners('SIGTERM');
2262
2352
  process.exit(0);
@@ -2274,20 +2364,20 @@ async function main() {
2274
2364
  }
2275
2365
  } else
2276
2366
 
2277
- // ── Auto-compilar el runner antes de ejecutar Playwright para asegurar que los cambios de rama/merge estén aplicados ──
2367
+ // ── Auto-compilar el runner antes de ejecutar Playwright para asegurar que los cambios de rama/merge estén aplicados ──
2278
2368
  try {
2279
2369
  if (!process.argv.includes('--debug')) {
2280
- s.message('⚙️ Compilando agente de pruebas (Arcality Runner)...');
2370
+ s.message('⚙️ Compilando agente de pruebas (Arcality Runner)...');
2281
2371
  } else {
2282
- console.log(chalk.cyan('⚙️ Compilando agente de pruebas (Arcality Runner)...'));
2372
+ console.log(chalk.cyan('⚙️ Compilando agente de pruebas (Arcality Runner)...'));
2283
2373
  }
2284
2374
  await ensureRunnerBundle({ debug: process.argv.includes('--debug') });
2285
2375
  } catch (err) {
2286
- console.warn(chalk.yellow(`\n⚠️ Advertencia: No se pudo auto-compilar el runner: ${err.message}`));
2376
+ console.warn(chalk.yellow(`\n⚠️ Advertencia: No se pudo auto-compilar el runner: ${err.message}`));
2287
2377
  }
2288
2378
 
2289
2379
  try {
2290
- // No test file path is passed — playwright.config.js uses testMatch:'agentic-runner.spec.ts'
2380
+ // No test file path is passed playwright.config.js uses testMatch:'agentic-runner.spec.ts'
2291
2381
  // This eliminates the Windows regex path issue permanently on any user's machine.
2292
2382
  await run('node', [
2293
2383
  '--no-warnings',
@@ -2299,7 +2389,7 @@ async function main() {
2299
2389
  cwd: PROJECT_ROOT,
2300
2390
  env: mergedEnv,
2301
2391
  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
2392
+ onSpawn: (child) => { activeChildProcess = child; } // Capturar referencia para cancelHandler
2303
2393
  });
2304
2394
 
2305
2395
  process.removeAllListeners('SIGINT');
@@ -2310,11 +2400,11 @@ async function main() {
2310
2400
  if (userCanceledMission) {
2311
2401
  missionResult = 'canceled';
2312
2402
  finalFailReason = 'user_canceled';
2313
- // Detener el spinner explícitamente antes de salir
2403
+ // Detener el spinner explícitamente antes de salir
2314
2404
  if (!process.argv.includes('--debug')) {
2315
- try { s.stop(chalk.yellow('⚠️ Misión cancelada por el usuario.')); } catch { }
2405
+ try { s.stop(chalk.yellow('⚠️ Misión cancelada por el usuario.')); } catch { }
2316
2406
  }
2317
- return; // El handler ya tomó el control, finally se ejecutará
2407
+ return; // El handler ya tomó el control, finally se ejecutará
2318
2408
  }
2319
2409
 
2320
2410
  const persistedMissionResult = readMissionResult(
@@ -2346,24 +2436,24 @@ async function main() {
2346
2436
  if (!process.argv.includes('--debug')) s.stop(chalk.green('\u2705 Mision completada exitosamente.'));
2347
2437
  else console.log(chalk.green('\u2705 Mision completada exitosamente.'));
2348
2438
 
2349
- // ── Ping Project ──
2439
+ // ── Ping Project ──
2350
2440
  await pingProject(finalProjectId, process.env.ARCALITY_API_KEY);
2351
2441
 
2352
- // ── Save Mission YAML ──
2442
+ // ── Save Mission YAML ──
2353
2443
  let saveDecision = 'no';
2354
2444
  if (!skipSavePrompt && !batchMode) {
2355
2445
  saveDecision = await select({
2356
- message: '¿Quieres guardar esta misión para ejecutarla de nuevo más tarde?',
2446
+ message: '¿Quieres guardar esta misión para ejecutarla de nuevo más tarde?',
2357
2447
  options: [
2358
- { label: '✅ Sí, guardar como YAML', value: 'yes' },
2359
- { label: '❌ No, gracias', value: 'no' }
2448
+ { label: ' Sí, guardar como YAML', value: 'yes' },
2449
+ { label: ' No, gracias', value: 'no' }
2360
2450
  ]
2361
2451
  });
2362
2452
  }
2363
2453
 
2364
2454
  if (saveDecision === 'yes') {
2365
2455
  const name = await text({
2366
- message: 'Nombre de la misión (ej., login_valido):',
2456
+ message: 'Nombre de la misión (ej., login_valido):',
2367
2457
  placeholder: 'my_mission',
2368
2458
  validate: v => v.length < 3 ? 'Nombre muy corto' : undefined
2369
2459
  });
@@ -2420,9 +2510,9 @@ async function main() {
2420
2510
  fs.mkdirSync(yamlOutputTargetDir, { recursive: true });
2421
2511
  const yamlPathOutput = path.join(yamlOutputTargetDir, `${safeName}.yaml`);
2422
2512
  fs.writeFileSync(yamlPathOutput, yamlData);
2423
- note(chalk.green(`✅ Misión guardada en: ${yamlPathOutput}`), 'Persistencia Arcality');
2513
+ note(chalk.green(`✅ Misión guardada en: ${yamlPathOutput}`), 'Persistencia Arcality');
2424
2514
  } else {
2425
- note(chalk.green(`✅ Misión guardada en: ${yamlPathMissions}`), 'Persistencia Arcality');
2515
+ note(chalk.green(`✅ Misión guardada en: ${yamlPathMissions}`), 'Persistencia Arcality');
2426
2516
  }
2427
2517
  }
2428
2518
  }
@@ -2454,7 +2544,7 @@ async function main() {
2454
2544
  if (userCanceledMission) {
2455
2545
  missionResult = 'canceled';
2456
2546
  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.';
2547
+ finalFailReasoning = 'La misión fue detenida manualmente por el operador (Ctrl+C). Esto no es una falla técnica del sistema.';
2458
2548
  await new Promise(r => setTimeout(r, 800));
2459
2549
  }
2460
2550
 
@@ -2463,9 +2553,9 @@ async function main() {
2463
2553
 
2464
2554
  const sRep = spinner();
2465
2555
  if (!process.argv.includes('--debug')) {
2466
- sRep.start('📊 Generando reporte...');
2556
+ sRep.start('📊 Generando reporte...');
2467
2557
  } else {
2468
- console.log('📊 Generando reporte...');
2558
+ console.log('📊 Generando reporte...');
2469
2559
  }
2470
2560
 
2471
2561
  await run("node", [path.join(PROJECT_ROOT, "scripts/rebrand-report.mjs")]).catch(() => { });
@@ -2496,7 +2586,7 @@ async function main() {
2496
2586
  }
2497
2587
  } catch(e) {}
2498
2588
 
2499
- // ── Upload Evidence (solo si NO fue cancelado por usuario) ──
2589
+ // ── Upload Evidence (solo si NO fue cancelado por usuario) ──
2500
2590
  if (missionResult !== 'canceled') {
2501
2591
  try {
2502
2592
  const { getEvidenceSasToken } = await import('../src/arcalityClient.mjs');
@@ -2510,7 +2600,7 @@ async function main() {
2510
2600
 
2511
2601
  if (sasData && actualSasUrl && actualBasePath) {
2512
2602
  if (!process.argv.includes('--debug')) {
2513
- sRep.start('☁️ Subiendo evidencia a Azure...');
2603
+ sRep.start('☁️ Subiendo evidencia a Azure...');
2514
2604
  }
2515
2605
  process.removeAllListeners('SIGINT');
2516
2606
  process.removeAllListeners('SIGTERM');
@@ -2524,26 +2614,26 @@ async function main() {
2524
2614
  finalReportUrl = `${parsedSas.origin}${parsedSas.pathname}/${actualBasePath}/index.html`;
2525
2615
  } catch(e) {}
2526
2616
 
2527
- if (!process.argv.includes('--debug')) sRep.stop(chalk.green('✅ Evidencia subida exitosamente.'));
2617
+ if (!process.argv.includes('--debug')) sRep.stop(chalk.green(' Evidencia subida exitosamente.'));
2528
2618
  }
2529
2619
  } 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.'));
2620
+ if (process.argv.includes('--debug')) console.log(chalk.yellow(`⚠️ Could not upload evidence: ${e.message}`));
2621
+ if (!process.argv.includes('--debug')) sRep.stop(chalk.yellow('⚠️ Fallo al subir evidencia.'));
2532
2622
  }
2533
2623
  } else {
2534
- console.log(chalk.gray(' >> Saltando subida de evidencia (la misión fue cancelada).'));
2624
+ console.log(chalk.gray(' >> Saltando subida de evidencia (la misión fue cancelada).'));
2535
2625
  }
2536
2626
 
2537
- // ── End Mission — SIEMPRE se llama, independientemente de si la evidencia subió ──
2627
+ // ── End Mission SIEMPRE se llama, independientemente de si la evidencia subió ──
2538
2628
  try {
2539
2629
  const { endMission } = await import('../src/arcalityClient.mjs');
2540
2630
  await Promise.race([
2541
2631
  endMission(mission.mission_id, missionResult || 'failed', finalUsageData, finalFailReason, finalReportUrl, finalFailReasoning),
2542
2632
  new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 8000))
2543
2633
  ]);
2544
- if (process.argv.includes('--debug')) console.log(chalk.gray(`[DEBUG] endMission called → result: ${missionResult}, failReason: ${finalFailReason}`));
2634
+ if (process.argv.includes('--debug')) console.log(chalk.gray(`[DEBUG] endMission called result: ${missionResult}, failReason: ${finalFailReason}`));
2545
2635
  } catch (e) {
2546
- if (process.argv.includes('--debug')) console.log(chalk.yellow(`⚠️ Could not end mission in backend: ${e.message}`));
2636
+ if (process.argv.includes('--debug')) console.log(chalk.yellow(`⚠️ Could not end mission in backend: ${e.message}`));
2547
2637
  }
2548
2638
 
2549
2639
  if (globalReportProcess) {
@@ -2558,10 +2648,10 @@ async function main() {
2558
2648
  }
2559
2649
 
2560
2650
  if (!batchMode && fs.existsSync(arcalityPath)) {
2561
- console.log(chalk.blue(`🌐 Abriendo Reporte de Arcality: ${arcalityPath}`));
2651
+ console.log(chalk.blue(`🌐 Abriendo Reporte de Arcality: ${arcalityPath}`));
2562
2652
  openLocalFile(arcalityPath);
2563
2653
  } else if (!fs.existsSync(arcalityPath)) {
2564
- console.log(chalk.yellow(`⚠️ Reporte no generado (la prueba pudo haber fallado antes de completarse).`));
2654
+ console.log(chalk.yellow(`⚠️ Reporte no generado (la prueba pudo haber fallado antes de completarse).`));
2565
2655
  }
2566
2656
 
2567
2657
  await new Promise(resolve => setTimeout(resolve, 2000));
@@ -2572,7 +2662,7 @@ async function main() {
2572
2662
  new Promise(resolve => setTimeout(resolve, 5000))
2573
2663
  ]);
2574
2664
 
2575
- // Limpiar handlers del post-proceso ANTES de esperar Enter, ya que toda la telemetría y reportes ya finalizaron
2665
+ // Limpiar handlers del post-proceso ANTES de esperar Enter, ya que toda la telemetría y reportes ya finalizaron
2576
2666
  process.removeListener('SIGINT', postProcessSigint);
2577
2667
  process.removeListener('SIGTERM', postProcessSigint);
2578
2668
 
@@ -2584,10 +2674,10 @@ async function main() {
2584
2674
  }
2585
2675
 
2586
2676
  if (false && !batchMode) {
2587
- // Esperar Enter para regresar al menú
2677
+ // Esperar Enter para regresar al menú
2588
2678
  // 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ú...'));
2679
+ // no procesa Enter correctamente cuando setRawMode(true) está activo.
2680
+ console.log(chalk.gray('\n Presiona Enter para regresar al menú...'));
2591
2681
  await new Promise(resolve => {
2592
2682
  const onKey = (key) => {
2593
2683
  const keyStr = String(key);
@@ -2599,7 +2689,7 @@ async function main() {
2599
2689
  process.stdin.setRawMode(false);
2600
2690
  }
2601
2691
  // 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).
2692
+ // Si volvemos al menú, no lo pausamos (para evitar el bug de exit en clack).
2603
2693
  if (skipMenu) {
2604
2694
  process.stdin.pause();
2605
2695
  }
@@ -2612,22 +2702,22 @@ async function main() {
2612
2702
  }
2613
2703
  };
2614
2704
 
2615
- // ── BUGFIX: @clack/prompts llama explícitamente a process.stdin.pause()
2705
+ // ── BUGFIX: @clack/prompts llama explícitamente a process.stdin.pause()
2616
2706
  // 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
2707
+ // a un stream EXPLÍCITAMENTE pausado NO lo reactiva automáticamente.
2708
+ // Resultado: el event loop queda vacío y el proceso termina antes de que
2619
2709
  // el usuario pueda presionar Enter.
2620
- // Solución: llamar resume() explícitamente antes de registrar el listener.
2710
+ // Solución: llamar resume() explícitamente antes de registrar el listener.
2621
2711
  if (process.stdin.isTTY && process.stdin.setRawMode) {
2622
2712
  process.stdin.setRawMode(true); // raw mode para capturar Enter inmediatamente
2623
2713
  }
2624
- process.stdin.resume(); // ← CRÍTICO: reactiva stdin para mantener el event loop vivo
2714
+ process.stdin.resume(); // CRÍTICO: reactiva stdin para mantener el event loop vivo
2625
2715
  process.stdin.on('data', onKey);
2626
2716
  });
2627
2717
  }
2628
2718
 
2629
2719
  if (skipMenu) {
2630
- outro(chalk.cyan('¡Misión terminada! Hasta luego.'));
2720
+ outro(chalk.cyan('¡Misión terminada! Hasta luego.'));
2631
2721
  return;
2632
2722
  }
2633
2723