@arcadialdev/arcality 2.6.1 → 2.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcadialdev/arcality",
3
- "version": "2.6.1",
3
+ "version": "2.6.3",
4
4
  "description": "El primer ingeniero QA Autónomo integrado al CI/CD. Creado por Arcadial.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -27,6 +27,29 @@ import {
27
27
  // Load global config at startup (injects keys into process.env)
28
28
  setupProcessEnv();
29
29
 
30
+ // ── WINDOWS FIX: Interceptar Ctrl+C antes que cmd.exe ──
31
+ // En Windows, `arcality` se ejecuta vía arcality.cmd (creado por npm para el campo bin).
32
+ // cmd.exe intercepta Ctrl+C y muestra "¿Desea terminar el trabajo por lotes (S/N)?".
33
+ // Al presionar S, mata el árbol de procesos antes de que nuestro finally pueda correr.
34
+ //
35
+ // La solución: setRawMode(true) hace que Node capture los bytes del teclado directamente,
36
+ // ANTES de que cmd.exe los procese. Al detectar Ctrl+C (byte 0x03) lo convertimos en
37
+ // un SIGINT de Node — cmd.exe nunca lo ve y nunca muestra el prompt.
38
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
39
+ try {
40
+ process.stdin.setRawMode(true);
41
+ process.stdin.resume();
42
+ process.stdin.setEncoding('utf8');
43
+ process.stdin.on('data', (key) => {
44
+ if (key === '\u0003') { // Ctrl+C
45
+ process.emit('SIGINT');
46
+ }
47
+ });
48
+ } catch (e) {
49
+ // Si falla (ej. en CI sin TTY), continuar sin raw mode
50
+ }
51
+ }
52
+
30
53
  const __filename = fileURLToPath(import.meta.url);
31
54
  const __dirname = path.dirname(__filename);
32
55
  const PROJECT_ROOT = process.env.ARCALITY_ROOT || path.join(__dirname, "..");
@@ -317,8 +340,13 @@ function run(cmd, args, options = {}) {
317
340
 
318
341
  // Use standard cross-platform spawn. When shell is false, Node's child_process
319
342
  // properly wraps arguments with spaces in quotes natively for CreateProcessW on Windows.
343
+ //
344
+ // CRITICAL (Windows): stdin MUST be 'ignore' (not 'inherit') to prevent cmd.exe from
345
+ // showing the "¿Desea terminar el trabajo por lotes (S/N)?" prompt on Ctrl+C.
346
+ // When stdin is 'inherit', cmd.exe intercepts Ctrl+C at the console level and kills
347
+ // the entire process tree before our SIGINT handler can run, bypassing the finally block.
320
348
  const p = spawn(cmd, args, {
321
- stdio: isDebug ? ['inherit', 'inherit', 'inherit'] : ['ignore', 'pipe', 'pipe'],
349
+ stdio: isDebug ? ['ignore', 'inherit', 'inherit'] : ['ignore', 'pipe', 'pipe'],
322
350
  shell: false,
323
351
  cwd,
324
352
  env,
@@ -347,24 +375,26 @@ function run(cmd, args, options = {}) {
347
375
  }
348
376
  };
349
377
 
350
- if (!isDebug && p.stdout) {
351
- p.stdout.on('data', handleData);
352
- }
353
- if (!isDebug && p.stderr) {
378
+ if (p.stdout) p.stdout.on('data', handleData);
379
+ if (p.stderr) {
354
380
  p.stderr.on('data', (data) => {
355
- process.stderr.write(data);
381
+ if (isDebug) process.stderr.write(data);
382
+ else process.stderr.write(data);
356
383
  outputBuffer += data.toString();
357
384
  });
358
385
  }
359
386
 
360
- p.on('exit', (code) => {
387
+ p.on('exit', (code, signal) => {
361
388
  const lastRunLog = path.join(LOGS_DIR, 'last-run.log');
362
389
  try {
363
390
  if (!fs.existsSync(LOGS_DIR)) fs.mkdirSync(LOGS_DIR, { recursive: true });
364
391
  fs.writeFileSync(lastRunLog, outputBuffer);
365
392
  } catch (e) { }
366
393
 
367
- if (code === 0) resolve();
394
+ // code===0 → clean success
395
+ // code===null + signal → killed by signal (Ctrl+C) — treat as non-fatal so finally runs
396
+ // code===1 → test failed (Playwright returns 1 on test failure) — also non-fatal
397
+ if (code === 0 || signal !== null || code === 1) resolve();
368
398
  else {
369
399
  if (isDebug) console.error(chalk.red(`\n[DEBUG] Process exited with code ${code}`));
370
400
  reject(new Error(`Process exited with code ${code}`));
@@ -740,19 +770,62 @@ async function main() {
740
770
 
741
771
  const finalProjectId = process.env.ARCALITY_PROJECT_ID || selectedProjectId;
742
772
 
743
- // ── SIGINT Handler: Cancelación manual (Ctrl+C / ESC) ──
744
- // Se registra AQUÍ, justo antes del spawn, para que solo aplique durante la ejecución.
745
- // Al cancelar: mata el proceso hijo, guarda estado "canceled", llama endMission y genera reporte.
746
- let activeChildProcess = null; // Referencia al proceso hijo de Playwright
773
+ // ── Cancelación Manual (Ctrl+C) Estrategia bulletproof ──
774
+ // El problema: @clack/prompts registra su propio SIGINT handler cada tick del spinner,
775
+ // que llama a process.exit(0) directamente. removeAllListeners() no es suficiente porque
776
+ // Clack lo re-registra en cada frame de animación.
777
+ //
778
+ // La solución: interceptar process.exit() antes de que Clack lo llame,
779
+ // y usar prependListener para que NUESTRO handler siempre se ejecute primero.
780
+
781
+ let activeChildProcess = null;
747
782
  let userCanceledMission = false;
748
- const cancelHandler = async () => {
749
- if (userCanceledMission) return; // Evitar doble disparo
783
+ let sigintCount = 0;
784
+ let _missionActive = true; // Mientras true, bloqueamos el process.exit de Clack
785
+
786
+ // Guardar el process.exit original y reemplazarlo temporalmente
787
+ const _origProcessExit = process.exit.bind(process);
788
+ process.exit = (code) => {
789
+ if (_missionActive) {
790
+ // Clack (u otra lib) está intentando matar el proceso — lo bloqueamos.
791
+ // Nuestro cancelHandler ya habrá sido invocado vía prependListener.
792
+ // El proceso se cerrará naturalmente cuando el child process termine.
793
+ return;
794
+ }
795
+ _origProcessExit(code);
796
+ };
797
+
798
+ const cancelHandler = () => {
799
+ sigintCount++;
800
+ if (sigintCount > 3) {
801
+ console.log(chalk.red('\n🛑 Forzando salida inmediata...'));
802
+ _missionActive = false;
803
+ _origProcessExit(1);
804
+ return;
805
+ }
806
+
807
+ if (userCanceledMission) {
808
+ console.log(chalk.yellow('\n⏳ Cancelación en progreso, esperando reporte... (Ctrl+C x4 para forzar cierre)'));
809
+ return;
810
+ }
750
811
  userCanceledMission = true;
751
812
 
752
813
  console.log(chalk.yellow('\n\n⚠️ [ARCALITY] Misión cancelada por el usuario.'));
753
- if (!process.argv.includes('--debug')) s.stop(chalk.yellow('⚠️ Misión cancelada por el usuario.'));
814
+ if (!process.argv.includes('--debug')) {
815
+ try { s.stop(chalk.yellow('⚠️ Misión cancelada.')); } catch { /* ignore */ }
816
+ }
817
+
818
+ // Terminar el proceso hijo de Playwright directamente (Windows-safe)
819
+ if (activeChildProcess && !activeChildProcess.killed) {
820
+ try {
821
+ if (process.platform === 'win32') {
822
+ spawn('taskkill', ['/pid', String(activeChildProcess.pid), '/f', '/t'], { stdio: 'ignore', windowsHide: true });
823
+ } else {
824
+ activeChildProcess.kill('SIGTERM');
825
+ }
826
+ } catch { /* silencioso */ }
827
+ }
754
828
 
755
- // 1. Escribir un agent-log de cancelación para que el reporte lo refleje
756
829
  try {
757
830
  const ctxDir = process.env.CONTEXT_DIR;
758
831
  if (ctxDir && fs.existsSync(ctxDir)) {
@@ -770,28 +843,15 @@ async function main() {
770
843
  }
771
844
  } catch { /* silencioso */ }
772
845
 
773
- // 2. Registraremos la misión como 'canceled' en el bloque finally
774
- console.log(chalk.gray(' >> Registrando misión como "cancelada"...'));
775
-
776
- // 3. Matar el proceso hijo de Playwright — hacerlo AL FINAL para dar tiempo
777
- // a que el agente-log y endMission terminen antes del kill.
778
- // NO llamar process.exit() aquí: el finally del try/catch principal
779
- // se encarga de generar el reporte visual y abrirlo en el browser.
780
- if (activeChildProcess) {
781
- try {
782
- if (process.platform === 'win32') {
783
- spawn('taskkill', ['/pid', activeChildProcess.pid, '/f', '/t'], { stdio: 'ignore', windowsHide: true });
784
- } else {
785
- activeChildProcess.kill('SIGTERM');
786
- }
787
- } catch { /* silencioso */ }
788
- }
789
- // El bloque finally del try/catch de run() tomará el control
790
- // a partir de aquí (el proceso hijo ya terminó → run() rechaza → catch → finally).
846
+ console.log(chalk.gray(' >> Esperando que Playwright cierre y genere el reporte...'));
847
+ // Playwright también recibe el SIGINT nativamente desde la terminal Windows
848
+ // y cerrará su proceso limpiamente, lo que resolverá la Promise del run() → finally
791
849
  };
792
850
 
793
- process.once('SIGINT', cancelHandler);
794
- process.once('SIGTERM', cancelHandler);
851
+ // prependListener asegura que nuestro handler se ejecuta ANTES que el de Clack
852
+ process.prependListener('SIGINT', cancelHandler);
853
+ process.prependListener('SIGTERM', cancelHandler);
854
+
795
855
 
796
856
  // Build env for the runner — merge arcality.config values.
797
857
  // ARCALITY_API_URL must be explicit — it comes from the library's internal config,
@@ -831,27 +891,76 @@ async function main() {
831
891
  const testFile = path.join(PROJECT_ROOT, 'tests', '_helpers', 'agentic-runner.spec.ts');
832
892
  const configFile = path.join(PROJECT_ROOT, 'playwright.config.js');
833
893
 
834
- // Resolve playwright CLI relative to PROJECT_ROOT exactly as Node handles module resolution.
835
- // This prevents the "did not expect test() to be called here" singleton duplication error.
894
+ // Resolve playwright CLI CRITICAL singleton alignment:
895
+ //
896
+ // @playwright/test uses `playwright` internally. When npm detects a version mismatch
897
+ // between the top-level `playwright` and what `@playwright/test` needs, it installs
898
+ // a NESTED copy at: @playwright/test/node_modules/playwright/
899
+ //
900
+ // Playwright's test runner tracks the active suite via a module-level singleton
901
+ // (currentSuite). If the CLI uses a DIFFERENT playwright instance than the one
902
+ // @playwright/test loaded, their singletons are out of sync → "did not expect test()
903
+ // to be called here".
904
+ //
905
+ // Fix: ALWAYS prefer the playwright nested INSIDE @playwright/test, because that is
906
+ // guaranteed to be the same instance that @playwright/test uses for its internals.
907
+ //
908
+ // Resolution priority:
909
+ // 1. <project>/@playwright/test/node_modules/playwright/cli.js ← nested (safe)
910
+ // 2. <project>/playwright/cli.js ← sibling top-level
911
+ // (repeated for ORIGINAL_CWD and PROJECT_ROOT)
836
912
  let playwrightCli;
837
913
  try {
838
- // Find the core @playwright/test that the local project uses
839
- const testRunnerPath = require.resolve('@playwright/test', { paths: [PROJECT_ROOT] });
840
- // Target the cli.js file strictly inside its parallel playwright dependency
841
- playwrightCli = path.join(testRunnerPath, '..', '..', '..', 'playwright', 'cli.js');
842
- if (!fs.existsSync(playwrightCli)) {
843
- throw new Error(`CLI not found exactly at: ${playwrightCli}`);
914
+ const searchPaths = [ORIGINAL_CWD, PROJECT_ROOT];
915
+ let testRunnerPath = null;
916
+ let resolveError = null;
917
+
918
+ for (const searchPath of searchPaths) {
919
+ try {
920
+ testRunnerPath = require.resolve('@playwright/test', { paths: [searchPath] });
921
+ break;
922
+ } catch (e) {
923
+ resolveError = e;
924
+ }
925
+ }
926
+
927
+ if (!testRunnerPath) throw resolveError;
928
+
929
+ const testRunnerDir = path.dirname(testRunnerPath); // → .../node_modules/@playwright/test/
930
+
931
+ // Priority 1: playwright nested INSIDE @playwright/test — same singleton as the package uses
932
+ const nestedCli = path.join(testRunnerDir, 'node_modules', 'playwright', 'cli.js');
933
+ // Priority 2: playwright as a top-level sibling (clean installs, versions aligned)
934
+ const siblingCli = path.join(testRunnerDir, '..', '..', 'playwright', 'cli.js');
935
+
936
+ if (fs.existsSync(nestedCli)) {
937
+ playwrightCli = nestedCli;
938
+ } else if (fs.existsSync(siblingCli)) {
939
+ playwrightCli = siblingCli;
940
+ } else {
941
+ throw new Error(
942
+ `Playwright CLI not found. Tried:\n (nested) ${nestedCli}\n (sibling) ${siblingCli}`
943
+ );
944
+ }
945
+
946
+ if (process.argv.includes('--debug')) {
947
+ console.log(chalk.gray(`[DEBUG] Using playwright CLI: ${playwrightCli}`));
844
948
  }
845
949
  } catch (e) {
846
- console.error(chalk.red('\n[FATAL] Playwright core not found! Make sure playwright is properly installed.'));
950
+ console.error(chalk.red('\n[FATAL] Playwright core not found. Make sure @playwright/test is installed.'));
847
951
  console.error(chalk.red(e.message));
848
- process.exit(1);
952
+ console.error(chalk.yellow('\nTip: Run `npm install @playwright/test` in your project directory.'));
953
+ _missionActive = false;
954
+ process.exit = _origProcessExit;
955
+ _origProcessExit(1);
849
956
  }
850
957
 
958
+ const postProcessSigint = () => console.log(chalk.yellow('\n⏳ Finalizando tareas de reporte, por favor espera...'));
851
959
  let missionResult = null;
852
960
  let finalFailReason = null;
853
961
  let finalUsageData = undefined;
854
962
  let finalReportUrl = null;
963
+ let finalFailReasoning = null;
855
964
 
856
965
  try {
857
966
  // No test file path is passed — playwright.config.js uses testMatch:'agentic-runner.spec.ts'
@@ -869,14 +978,19 @@ async function main() {
869
978
  onSpawn: (child) => { activeChildProcess = child; } // ← Capturar referencia para cancelHandler
870
979
  });
871
980
 
872
- // Eliminar handlers SIGINT ahora que el proceso terminó normalmente
873
- process.off('SIGINT', cancelHandler);
874
- process.off('SIGTERM', cancelHandler);
981
+ process.removeAllListeners('SIGINT');
982
+ process.removeAllListeners('SIGTERM');
983
+ process.on('SIGINT', postProcessSigint);
984
+ process.on('SIGTERM', postProcessSigint);
875
985
 
876
986
  if (userCanceledMission) {
877
987
  missionResult = 'canceled';
878
988
  finalFailReason = 'user_canceled';
879
- return; // El handler ya tomó el control, finally se ejecutará y luego saldrá del CLI
989
+ // Detener el spinner explícitamente antes de salir
990
+ if (!process.argv.includes('--debug')) {
991
+ try { s.stop(chalk.yellow('⚠️ Misión cancelada por el usuario.')); } catch { }
992
+ }
993
+ return; // El handler ya tomó el control, finally se ejecutará
880
994
  }
881
995
 
882
996
  missionResult = 'success';
@@ -932,15 +1046,10 @@ async function main() {
932
1046
  }
933
1047
  }
934
1048
  } catch (e) {
935
- // Eliminar handlers SIGINT en el path de error también
936
- process.off('SIGINT', cancelHandler);
937
- process.off('SIGTERM', cancelHandler);
938
-
1049
+ // SIGINT ya fue manejado por cancelHandler solo clasificamos el resultado
939
1050
  if (userCanceledMission) {
940
1051
  missionResult = 'canceled';
941
1052
  finalFailReason = 'user_canceled';
942
- // El cancelHandler ya registró la cancelación en el servidor.
943
- // Solo mostramos un mensaje y dejamos que el finally genere el reporte.
944
1053
  console.log(chalk.yellow('\n⚠️ Misión cancelada — generando reporte de evidencia...'));
945
1054
  } else {
946
1055
  missionResult = 'failed';
@@ -952,14 +1061,29 @@ async function main() {
952
1061
  }
953
1062
  }
954
1063
  } finally {
1064
+ // Desactivar el bloqueo de process.exit y restaurar el original
1065
+ _missionActive = false;
1066
+ process.exit = _origProcessExit;
1067
+ process.removeListener('SIGINT', cancelHandler);
1068
+ process.removeListener('SIGTERM', cancelHandler);
1069
+
955
1070
  if (userCanceledMission) {
956
1071
  missionResult = 'canceled';
957
1072
  finalFailReason = 'user_canceled';
1073
+ finalFailReasoning = 'La misión fue detenida manualmente por el usuario (Ctrl+C) antes de completarse. No se trata de un fallo técnico del sistema ni del portal.';
958
1074
  await new Promise(r => setTimeout(r, 800));
959
1075
  }
960
1076
 
1077
+ process.on('SIGINT', postProcessSigint);
1078
+ process.on('SIGTERM', postProcessSigint);
1079
+
961
1080
  const sRep = spinner();
962
- sRep.start('📊 Generating report...');
1081
+ if (!process.argv.includes('--debug')) {
1082
+ sRep.start('📊 Generating report...');
1083
+ } else {
1084
+ console.log('📊 Generating report...');
1085
+ }
1086
+
963
1087
  await run("node", [path.join(PROJECT_ROOT, "scripts/rebrand-report.mjs")]).catch(() => { });
964
1088
 
965
1089
  const reportDir = process.env.REPORTS_DIR || 'tests-report';
@@ -977,6 +1101,7 @@ async function main() {
977
1101
  const lastLog = path.join(ctxDir, logFiles[logFiles.length - 1]);
978
1102
  const logData = JSON.parse(fs.readFileSync(lastLog, 'utf8'));
979
1103
  finalUsageData = logData.usage || finalUsageData;
1104
+ finalFailReasoning = logData.fail_reasoning || null;
980
1105
  if (missionResult === 'failed' && !finalFailReason) {
981
1106
  finalFailReason = logData.fail_reason || "timeout_or_crash";
982
1107
  }
@@ -984,37 +1109,51 @@ async function main() {
984
1109
  }
985
1110
  } catch(e) {}
986
1111
 
987
- // ── Upload Evidence ──
988
- try {
989
- const { endMission, getEvidenceSasToken } = await import('../src/arcalityClient.mjs');
990
- const { uploadEvidence } = await import('../src/EvidenceUploader.mjs');
991
-
992
- const sasData = await getEvidenceSasToken(mission.mission_id, finalProjectId);
993
- if (process.argv.includes('--debug')) console.log(`[DEBUG] sasData received:`, sasData);
994
-
995
- // Soporte para camelCase y snake_case debido al serializador JSON del backend
996
- const actualSasUrl = sasData?.sasTokenUrl || sasData?.sasUrl || sasData?.sas_url;
997
- const actualBasePath = sasData?.basePath || sasData?.base_path;
998
-
999
- if (sasData && actualSasUrl && actualBasePath) {
1000
- if (!process.argv.includes('--debug')) sRep.start('☁️ Uploading evidence to Azure...');
1001
-
1002
- await uploadEvidence(reportDir, actualSasUrl, actualBasePath);
1003
-
1004
- // Construct the public URL
1005
- try {
1006
- const parsedSas = new URL(actualSasUrl);
1007
- finalReportUrl = `${parsedSas.origin}${parsedSas.pathname}/${actualBasePath}/index.html`;
1008
- } catch(e) {}
1009
-
1010
- if (!process.argv.includes('--debug')) sRep.stop(chalk.green('✅ Evidence uploaded successfully.'));
1112
+ // ── Upload Evidence (solo si NO fue cancelado por usuario) ──
1113
+ if (missionResult !== 'canceled') {
1114
+ try {
1115
+ const { getEvidenceSasToken } = await import('../src/arcalityClient.mjs');
1116
+ const { uploadEvidence } = await import('../src/EvidenceUploader.mjs');
1117
+
1118
+ const sasData = await getEvidenceSasToken(mission.mission_id, finalProjectId);
1119
+ if (process.argv.includes('--debug')) console.log(`[DEBUG] sasData received:`, sasData);
1120
+
1121
+ const actualSasUrl = sasData?.sasTokenUrl || sasData?.sasUrl || sasData?.sas_url;
1122
+ const actualBasePath = sasData?.basePath || sasData?.base_path;
1123
+
1124
+ if (sasData && actualSasUrl && actualBasePath) {
1125
+ if (!process.argv.includes('--debug')) {
1126
+ sRep.start('☁️ Uploading evidence to Azure...');
1127
+ }
1128
+ process.removeAllListeners('SIGINT');
1129
+ process.removeAllListeners('SIGTERM');
1130
+ process.on('SIGINT', postProcessSigint);
1131
+ process.on('SIGTERM', postProcessSigint);
1132
+
1133
+ await uploadEvidence(reportDir, actualSasUrl, actualBasePath);
1134
+
1135
+ try {
1136
+ const parsedSas = new URL(actualSasUrl);
1137
+ finalReportUrl = `${parsedSas.origin}${parsedSas.pathname}/${actualBasePath}/index.html`;
1138
+ } catch(e) {}
1139
+
1140
+ if (!process.argv.includes('--debug')) sRep.stop(chalk.green('✅ Evidence uploaded successfully.'));
1141
+ }
1142
+ } catch (e) {
1143
+ if (process.argv.includes('--debug')) console.log(chalk.yellow(`⚠️ Could not upload evidence: ${e.message}`));
1144
+ if (!process.argv.includes('--debug')) sRep.stop(chalk.yellow('⚠️ Evidence upload failed.'));
1011
1145
  }
1146
+ } else {
1147
+ console.log(chalk.gray(' >> Skipping evidence upload (mission was canceled).'));
1148
+ }
1012
1149
 
1013
- // ── End Mission ──
1014
- await endMission(mission.mission_id, missionResult || 'failed', finalUsageData, finalFailReason, finalReportUrl);
1150
+ // ── End Mission — SIEMPRE se llama, independientemente de si la evidencia subió ──
1151
+ try {
1152
+ const { endMission } = await import('../src/arcalityClient.mjs');
1153
+ await endMission(mission.mission_id, missionResult || 'failed', finalUsageData, finalFailReason, finalReportUrl, finalFailReasoning);
1154
+ if (process.argv.includes('--debug')) console.log(chalk.gray(`[DEBUG] endMission called → result: ${missionResult}, failReason: ${finalFailReason}`));
1015
1155
  } catch (e) {
1016
- if (process.argv.includes('--debug')) console.log(chalk.yellow(`⚠️ Could not upload evidence or end mission: ${e.message}`));
1017
- if (!process.argv.includes('--debug')) sRep.stop(chalk.yellow('⚠️ Evidence upload or end mission failed.'));
1156
+ if (process.argv.includes('--debug')) console.log(chalk.yellow(`⚠️ Could not end mission in backend: ${e.message}`));
1018
1157
  }
1019
1158
 
1020
1159
  if (globalReportProcess) {
@@ -1047,9 +1186,19 @@ async function main() {
1047
1186
  process.env.ARCALITY_API_KEY
1048
1187
  );
1049
1188
 
1050
- await text({
1051
- message: 'Press Enter to return to menu...',
1052
- placeholder: 'Enter'
1189
+ // Esperar Enter para regresar al menú
1190
+ // Usamos escucha directa de stdin (raw mode) porque @clack/text
1191
+ // no procesa Enter correctamente cuando setRawMode(true) está activo.
1192
+ console.log(chalk.gray('\n Press Enter to return to menu...'));
1193
+ await new Promise(resolve => {
1194
+ const onKey = (key) => {
1195
+ if (key === '\r' || key === '\n' || key === '\u0003') {
1196
+ process.stdin.off('data', onKey);
1197
+ if (key === '\u0003') process.emit('SIGINT'); // Ctrl+C = salir
1198
+ else resolve();
1199
+ }
1200
+ };
1201
+ process.stdin.on('data', onKey);
1053
1202
  });
1054
1203
 
1055
1204
  if (skipMenu) {
@@ -1060,6 +1209,10 @@ async function main() {
1060
1209
  agentMode = false;
1061
1210
  prompt = "";
1062
1211
  firstRun = false;
1212
+
1213
+ // Limpiar handlers del post-proceso
1214
+ process.removeListener('SIGINT', postProcessSigint);
1215
+ process.removeListener('SIGTERM', postProcessSigint);
1063
1216
  }
1064
1217
  }
1065
1218
  firstRun = false;
@@ -250,7 +250,7 @@ export async function startMission(prompt, targetUrl) {
250
250
  * @param {'success'|'failed'|'cancelled'} result
251
251
  * @param {object} usage - Optional usage statistics directly from the AI
252
252
  */
253
- export async function endMission(missionId, result, usage = null, failReason = null, reportUrl = null) {
253
+ export async function endMission(missionId, result, usage = null, failReason = null, reportUrl = null, failReasoning = null) {
254
254
  const apiBase = getEffectiveApiBase();
255
255
  if (!apiBase || !missionId) return { ok: true };
256
256
 
@@ -262,7 +262,7 @@ export async function endMission(missionId, result, usage = null, failReason = n
262
262
  'Content-Type': 'application/json',
263
263
  'x-api-key': key
264
264
  },
265
- body: JSON.stringify({ result, usage, failReason, reportUrl })
265
+ body: JSON.stringify({ result, usage, failReason, failReasoning, reportUrl })
266
266
  });
267
267
  } catch { }
268
268
  return { ok: true };
@@ -2810,6 +2810,7 @@ function captureValidationRule(errorMessage, context) {
2810
2810
  let aiMarkedSuccess = false;
2811
2811
  let hasCriticalError = false;
2812
2812
  let failReason = "";
2813
+ let failReasoning = "";
2813
2814
  let resultsSaved = false;
2814
2815
  let isFinished = false;
2815
2816
  let stepCount = 0;
@@ -2902,6 +2903,7 @@ function captureValidationRule(errorMessage, context) {
2902
2903
  success: finalSuccess,
2903
2904
  error: hasCriticalError,
2904
2905
  fail_reason: failReason,
2906
+ fail_reasoning: failReasoning,
2905
2907
  timestamp: Date.now()
2906
2908
  };
2907
2909
  memories.push(missionData);
@@ -2912,7 +2914,7 @@ function captureValidationRule(errorMessage, context) {
2912
2914
  }
2913
2915
  try {
2914
2916
  const logPath = path2.join(contextDir, `agent-log-${Date.now()}.json`);
2915
- fs2.writeFileSync(logPath, JSON.stringify({ prompt, history, steps: stepCount, success: finalSuccess, error: hasCriticalError, fail_reason: failReason, usage: totalMissionUsage, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
2917
+ fs2.writeFileSync(logPath, JSON.stringify({ prompt, history, steps: stepCount, success: finalSuccess, error: hasCriticalError, fail_reason: failReason, fail_reasoning: failReasoning, usage: totalMissionUsage, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
2916
2918
  try {
2917
2919
  fs2.appendFileSync(path2.join(contextDir, "arcality-history.log"), JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), mission: prompt.substring(0, 150), success: finalSuccess, steps: stepCount, usage: totalMissionUsage }) + "\n");
2918
2920
  } catch (e) {
@@ -3248,6 +3250,8 @@ ${readableHistory}`;
3248
3250
  console.log(`>>ARCALITY_STATUS>> \u{1F6D1} Error interno del Portal/Red detectado: "${networkError}"`);
3249
3251
  hasCriticalError = true;
3250
3252
  failReason = "portal_network_error";
3253
+ failReasoning = `El portal web fall\xF3 al intentar comunicarse con su backend (ej. una petici\xF3n XHR o Fetch fall\xF3 o dio timeout). El agente QA no tiene control sobre las ca\xEDdas del servidor.
3254
+ Detalles t\xE9cnicos capturados en la red: ${networkError}`;
3251
3255
  isFinished = true;
3252
3256
  history.push(`Turno ${stepCount}: \u{1F6D1} ERROR CR\xCDTICO DE SISTEMA/RED: ${networkError}. El portal fall\xF3 internamente al ejecutar una petici\xF3n XHR/Fetch.`);
3253
3257
  saveMissionResults();
@@ -3258,6 +3262,8 @@ ${readableHistory}`;
3258
3262
  console.log(`>>ARCALITY_STATUS>> \u{1F6D1} Error Fatal de JavaScript detectado: "${jsError}"`);
3259
3263
  hasCriticalError = true;
3260
3264
  failReason = "portal_js_crash";
3265
+ failReasoning = `La aplicaci\xF3n web sufri\xF3 una excepci\xF3n no manejada de JavaScript en el navegador, provocando que la p\xE1gina dejara de responder o funcionara de manera err\xE1tica.
3266
+ Detalles de la excepci\xF3n en consola: ${jsError}`;
3261
3267
  isFinished = true;
3262
3268
  history.push(`Turno ${stepCount}: \u{1F6D1} ERROR CR\xCDTICO DE APLICACI\xD3N: ${jsError}. El portal sufri\xF3 una excepci\xF3n no manejada y dej\xF3 de responder.`);
3263
3269
  saveMissionResults();
@@ -3275,6 +3281,7 @@ ${readableHistory}`;
3275
3281
  console.error(` >> Tipo: portal_internal_error | El agente NO puede corregir errores del servidor.`);
3276
3282
  hasCriticalError = true;
3277
3283
  failReason = "portal_internal_error";
3284
+ failReasoning = `El servidor devolvi\xF3 un error sist\xE9mico irrecuperable que fue mostrado en la interfaz visual: "${sysErrTxt.trim()}". El agente QA no puede reparar bugs l\xF3gicos o de infraestructura del servidor, por lo que la misi\xF3n fue detenida preventivamente.`;
3278
3285
  isFinished = true;
3279
3286
  history.push(`Turno ${stepCount}: \u{1F6D1} ERROR SIST\xC9MICO DEL PORTAL: "${sysErrTxt.trim().substring(0, 200)}". Este es un fallo del servidor, no un error de datos del agente. El portal devolvi\xF3 HTTP 200 pero report\xF3 un error interno en la UI.`);
3280
3287
  saveMissionResults();
@@ -3359,7 +3366,24 @@ ${readableHistory}`;
3359
3366
  const effectivePrompt = patternContext ? `${prompt}
3360
3367
 
3361
3368
  ${patternContext}` : prompt;
3362
- response = await agent.askIA(effectivePrompt, history, stepCount);
3369
+ try {
3370
+ response = await agent.askIA(effectivePrompt, history, stepCount);
3371
+ } catch (err) {
3372
+ if (err.message?.includes("APPLICATION BUG:")) {
3373
+ const bugDesc = err.message.replace("APPLICATION BUG:", "").trim();
3374
+ console.log(`>>ARCALITY_STATUS>> \u{1F6D1} Application Bug reportado: ${bugDesc}`);
3375
+ aiMarkedSuccess = false;
3376
+ hasCriticalError = true;
3377
+ failReason = "application_bug";
3378
+ failReasoning = bugDesc;
3379
+ isFinished = true;
3380
+ history.push(`Turno ${stepCount}: \u{1F6D1} [APPLICATION BUG] ${bugDesc}`);
3381
+ saveMissionResults();
3382
+ break;
3383
+ } else {
3384
+ throw err;
3385
+ }
3386
+ }
3363
3387
  if (response.usage) {
3364
3388
  const inputs = response.usage.input_tokens || 0;
3365
3389
  const cacheCreates = response.usage.cache_creation_input_tokens || 0;
@@ -3381,13 +3405,17 @@ ${patternContext}` : prompt;
3381
3405
  aiMarkedSuccess = false;
3382
3406
  hasCriticalError = true;
3383
3407
  failReason = "cost_limit_reached";
3408
+ failReasoning = response.thought;
3384
3409
  }
3385
3410
  }
3386
3411
  }
3412
+ if (response && response.thought && response.thought.includes("DISCULPA:")) {
3413
+ failReasoning = response.thought.replace("DISCULPA:", "").trim();
3414
+ }
3387
3415
  console.log(`
3388
3416
  ======================================================`);
3389
3417
  console.log(`\u{1F916} [RAZONAMIENTO DEL AGENTE]`);
3390
- console.log(`${response.thought}`);
3418
+ console.log(`${response ? response.thought : "N/A"}`);
3391
3419
  console.log(`======================================================
3392
3420
  `);
3393
3421
  if (!response.finish && history.length >= 6) {
@@ -3417,6 +3445,8 @@ ${patternContext}` : prompt;
3417
3445
  aiMarkedSuccess = false;
3418
3446
  hasCriticalError = true;
3419
3447
  failReason = "loop_detected";
3448
+ failReasoning = `El agente QA entr\xF3 en un bucle l\xF3gico intentando ejecutar la misma acci\xF3n repetidamente sin observar cambios significativos en el DOM.
3449
+ Acci\xF3n bloqueada: Bucle de tipo ${loopType} sobre los elementos ${Array.from(uniqueTargets).join(", ")}.`;
3420
3450
  } else if ((isClickLoop || isFillLoop) && agentRecovered) {
3421
3451
  console.log(`>>ARCALITY_STATUS>> \u{1F504} Loop candidato detectado pero el agente se recuper\xF3 en este turno. Continuando misi\xF3n.`);
3422
3452
  }
@@ -3441,6 +3471,7 @@ ${patternContext}` : prompt;
3441
3471
  aiMarkedSuccess = false;
3442
3472
  hasCriticalError = true;
3443
3473
  failReason = "portal_rendering_failure";
3474
+ failReasoning = response.thought;
3444
3475
  history.push(`Turno ${stepCount}: \u{1F6D1} [PORTAL_RENDER_FAILURE] La p\xE1gina ${blankUrl} carg\xF3 en blanco (${consecutiveWaits} esperas sin progreso). Los widgets no se renderizaron. Bug del portal detectado por Arcality QA. Evidencia: ${blankEvidencePath}`);
3445
3476
  saveMissionResults();
3446
3477
  }
@@ -4027,7 +4058,21 @@ Evidence: ${JSON.stringify(v.evidence).substring(0, 400)}`,
4027
4058
  console.log(`>>ARCALITY_STATUS>> \u{1F4CA} Uso total de tokens esta misi\xF3n: IN=${totalMissionUsage.input_tokens} | OUT=${totalMissionUsage.output_tokens} | Cache writes=${totalMissionUsage.cache_creation_input_tokens} | Cache reads=${totalMissionUsage.cache_read_input_tokens}`);
4028
4059
  await persistCollectiveMemory(true).catch(() => {
4029
4060
  });
4030
- throw new Error(`Misi\xF3n no completada o finalizada con errores. Raz\xF3n: ${failReason}`);
4061
+ let finalErrorMsg = `Misi\xF3n no completada o finalizada con errores.
4062
+
4063
+ `;
4064
+ finalErrorMsg += `======================================================
4065
+ `;
4066
+ finalErrorMsg += `\u{1F3AF} TIPO DE FALLO: ${failReason}
4067
+ `;
4068
+ if (failReasoning) {
4069
+ finalErrorMsg += `\u{1F9E0} AN\xC1LISIS DEL AGENTE DE QA:
4070
+ ${failReasoning}
4071
+ `;
4072
+ }
4073
+ finalErrorMsg += `======================================================
4074
+ `;
4075
+ throw new Error(finalErrorMsg);
4031
4076
  } else {
4032
4077
  console.log(`>>ARCALITY_STATUS>> \u{1F4CA} Uso total de tokens esta misi\xF3n: IN=${totalMissionUsage.input_tokens} | OUT=${totalMissionUsage.output_tokens} | Cache writes=${totalMissionUsage.cache_creation_input_tokens} | Cache reads=${totalMissionUsage.cache_read_input_tokens}`);
4033
4078
  }