@arcadialdev/arcality 2.6.2 → 2.6.4

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.2",
3
+ "version": "2.6.4",
4
4
  "description": "El primer ingeniero QA Autónomo integrado al CI/CD. Creado por Arcadial.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -70,4 +70,4 @@
70
70
  "terminal-image": "^1.1.0",
71
71
  "tsx": "^4.21.0"
72
72
  }
73
- }
73
+ }
@@ -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}`));
@@ -785,6 +815,17 @@ async function main() {
785
815
  try { s.stop(chalk.yellow('⚠️ Misión cancelada.')); } catch { /* ignore */ }
786
816
  }
787
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
+ }
828
+
788
829
  try {
789
830
  const ctxDir = process.env.CONTEXT_DIR;
790
831
  if (ctxDir && fs.existsSync(ctxDir)) {
@@ -850,14 +891,24 @@ async function main() {
850
891
  const testFile = path.join(PROJECT_ROOT, 'tests', '_helpers', 'agentic-runner.spec.ts');
851
892
  const configFile = path.join(PROJECT_ROOT, 'playwright.config.js');
852
893
 
853
- // Resolve playwright CLI — CRITICAL: must use the same @playwright/test instance
854
- // that the agentic-runner.bundle.spec.js was compiled against. If the user's project
855
- // has its own @playwright/test, we must use THAT one to avoid the singleton conflict
856
- // ("Playwright Test did not expect test() to be called here").
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".
857
904
  //
858
- // Resolution order:
859
- // 1. ORIGINAL_CWD (user's project) preferred, avoids singleton mismatch
860
- // 2. PROJECT_ROOT (arcality package) — fallback for clean installs
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)
861
912
  let playwrightCli;
862
913
  try {
863
914
  const searchPaths = [ORIGINAL_CWD, PROJECT_ROOT];
@@ -875,14 +926,21 @@ async function main() {
875
926
 
876
927
  if (!testRunnerPath) throw resolveError;
877
928
 
878
- // Navigate from @playwright/test/index.js → playwright/cli.js (sibling package)
879
- playwrightCli = path.join(testRunnerPath, '..', '..', '..', 'playwright', 'cli.js');
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');
880
935
 
881
- // Some versions nest it differently — try alternate location
882
- if (!fs.existsSync(playwrightCli)) {
883
- const alt = path.join(testRunnerPath, '..', '..', 'playwright', 'cli.js');
884
- if (fs.existsSync(alt)) playwrightCli = alt;
885
- else throw new Error(`Playwright CLI not found. Tried:\n ${playwrightCli}\n ${alt}`);
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
+ );
886
944
  }
887
945
 
888
946
  if (process.argv.includes('--debug')) {
@@ -894,15 +952,15 @@ async function main() {
894
952
  console.error(chalk.yellow('\nTip: Run `npm install @playwright/test` in your project directory.'));
895
953
  _missionActive = false;
896
954
  process.exit = _origProcessExit;
897
- process.exit(1);
955
+ _origProcessExit(1);
898
956
  }
899
957
 
900
-
901
958
  const postProcessSigint = () => console.log(chalk.yellow('\n⏳ Finalizando tareas de reporte, por favor espera...'));
902
959
  let missionResult = null;
903
960
  let finalFailReason = null;
904
961
  let finalUsageData = undefined;
905
962
  let finalReportUrl = null;
963
+ let finalFailReasoning = null;
906
964
 
907
965
  try {
908
966
  // No test file path is passed — playwright.config.js uses testMatch:'agentic-runner.spec.ts'
@@ -928,7 +986,11 @@ async function main() {
928
986
  if (userCanceledMission) {
929
987
  missionResult = 'canceled';
930
988
  finalFailReason = 'user_canceled';
931
- 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á
932
994
  }
933
995
 
934
996
  missionResult = 'success';
@@ -1008,6 +1070,7 @@ async function main() {
1008
1070
  if (userCanceledMission) {
1009
1071
  missionResult = 'canceled';
1010
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.';
1011
1074
  await new Promise(r => setTimeout(r, 800));
1012
1075
  }
1013
1076
 
@@ -1038,6 +1101,7 @@ async function main() {
1038
1101
  const lastLog = path.join(ctxDir, logFiles[logFiles.length - 1]);
1039
1102
  const logData = JSON.parse(fs.readFileSync(lastLog, 'utf8'));
1040
1103
  finalUsageData = logData.usage || finalUsageData;
1104
+ finalFailReasoning = logData.fail_reasoning || null;
1041
1105
  if (missionResult === 'failed' && !finalFailReason) {
1042
1106
  finalFailReason = logData.fail_reason || "timeout_or_crash";
1043
1107
  }
@@ -1045,43 +1109,51 @@ async function main() {
1045
1109
  }
1046
1110
  } catch(e) {}
1047
1111
 
1048
- // ── Upload Evidence ──
1049
- try {
1050
- const { endMission, getEvidenceSasToken } = await import('../src/arcalityClient.mjs');
1051
- const { uploadEvidence } = await import('../src/EvidenceUploader.mjs');
1052
-
1053
- const sasData = await getEvidenceSasToken(mission.mission_id, finalProjectId);
1054
- if (process.argv.includes('--debug')) console.log(`[DEBUG] sasData received:`, sasData);
1055
-
1056
- // Soporte para camelCase y snake_case debido al serializador JSON del backend
1057
- const actualSasUrl = sasData?.sasTokenUrl || sasData?.sasUrl || sasData?.sas_url;
1058
- const actualBasePath = sasData?.basePath || sasData?.base_path;
1059
-
1060
- if (sasData && actualSasUrl && actualBasePath) {
1061
- if (!process.argv.includes('--debug')) {
1062
- sRep.start('☁️ Uploading evidence to Azure...');
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.'));
1063
1141
  }
1064
- process.removeAllListeners('SIGINT');
1065
- process.removeAllListeners('SIGTERM');
1066
- process.on('SIGINT', postProcessSigint);
1067
- process.on('SIGTERM', postProcessSigint);
1068
-
1069
- await uploadEvidence(reportDir, actualSasUrl, actualBasePath);
1070
-
1071
- // Construct the public URL
1072
- try {
1073
- const parsedSas = new URL(actualSasUrl);
1074
- finalReportUrl = `${parsedSas.origin}${parsedSas.pathname}/${actualBasePath}/index.html`;
1075
- } catch(e) {}
1076
-
1077
- if (!process.argv.includes('--debug')) sRep.stop(chalk.green('✅ Evidence uploaded successfully.'));
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.'));
1078
1145
  }
1146
+ } else {
1147
+ console.log(chalk.gray(' >> Skipping evidence upload (mission was canceled).'));
1148
+ }
1079
1149
 
1080
- // ── End Mission ──
1081
- 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}`));
1082
1155
  } catch (e) {
1083
- if (process.argv.includes('--debug')) console.log(chalk.yellow(`⚠️ Could not upload evidence or end mission: ${e.message}`));
1084
- 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}`));
1085
1157
  }
1086
1158
 
1087
1159
  if (globalReportProcess) {
@@ -1114,9 +1186,19 @@ async function main() {
1114
1186
  process.env.ARCALITY_API_KEY
1115
1187
  );
1116
1188
 
1117
- await text({
1118
- message: 'Press Enter to return to menu...',
1119
- 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);
1120
1202
  });
1121
1203
 
1122
1204
  if (skipMenu) {
@@ -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) {
@@ -3066,8 +3068,8 @@ function captureValidationRule(errorMessage, context) {
3066
3068
  const promptKeywords = normalize(prompt);
3067
3069
  const patternKeywords = normalize(bestMatch.prompt || bestMatch.original_prompt || bestMatch.originalPrompt || bestMatch.name || "");
3068
3070
  const matches = promptKeywords.filter((kw) => patternKeywords.some((pkw) => pkw.includes(kw) || kw.includes(pkw))).length;
3069
- const matchRatio = matches / Math.max(promptKeywords.length, 1);
3070
- if (matchRatio >= 0.85) {
3071
+ const isVectorMatch = true;
3072
+ if (isVectorMatch) {
3071
3073
  console.log(`>>ARCALITY_STATUS>> \u{1F9E0} Memoria Colectiva detectada (id: ${bestMatch.id}). Descargando a memoria local...`);
3072
3074
  reusedPatternId = bestMatch.id || "unknown";
3073
3075
  let patternJsonObj = {};
@@ -3117,7 +3119,7 @@ function captureValidationRule(errorMessage, context) {
3117
3119
  } catch (err) {
3118
3120
  console.warn(`Error al sincronizar memoria local: ${err}`);
3119
3121
  }
3120
- } else if (matchRatio >= 0.5) {
3122
+ } else if (!isVectorMatch) {
3121
3123
  let patternJsonObj = {};
3122
3124
  try {
3123
3125
  patternJsonObj = typeof bestMatch.pattern_json === "string" ? JSON.parse(bestMatch.pattern_json) : bestMatch.pattern_json || bestMatch.patternJson || {};
@@ -3129,7 +3131,7 @@ function captureValidationRule(errorMessage, context) {
3129
3131
  patternContext = `Existe un patr\xF3n similar previamente aprobado que logr\xF3 el \xE9xito. \xDAsalo como referencia para tu estrategia:
3130
3132
  ${readableHistory}`;
3131
3133
  } else {
3132
- console.log(`>>ARCALITY_STATUS>> \u2139\uFE0F Patr\xF3n de BD descartado por baja similitud (${matchRatio.toFixed(2)}).`);
3134
+ console.log(`>>ARCALITY_STATUS>> \u2139\uFE0F Patr\xF3n de BD descartado por baja similitud.`);
3133
3135
  }
3134
3136
  } else {
3135
3137
  console.log(`>>ARCALITY_STATUS>> \u2139\uFE0F No se encontraron patrones previos (no_match: true).`);
@@ -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
  }
@@ -3918,13 +3949,14 @@ ${patternContext}` : prompt;
3918
3949
  if (aiMarkedSuccess) {
3919
3950
  hasCriticalError = false;
3920
3951
  await persistCollectiveMemory();
3921
- if (!usedDirectReuse && stepsData.length > 0) {
3952
+ const finalUsedDirectReuse = reusedPatternId !== null && guideIdx !== 999999;
3953
+ if (!finalUsedDirectReuse && stepsData.length > 0) {
3922
3954
  const normalizedPrompt = prompt.toLowerCase().trim();
3923
3955
  const patternKey = crypto2.createHash("sha256").update(normalizedPrompt).digest("hex");
3924
3956
  if (!hasCriticalError) {
3925
- const isDuplicate = patternContext !== "" || usedDirectReuse;
3957
+ const isDuplicate = finalUsedDirectReuse;
3926
3958
  if (isDuplicate) {
3927
- console.log(`>>ARCALITY_STATUS>> \u2139\uFE0F No se guardar\xE1 el patr\xF3n. duplicate_skipped: true (ya existe uno similar o se us\xF3 directamente)`);
3959
+ console.log(`>>ARCALITY_STATUS>> \u2139\uFE0F No se guardar\xE1 el patr\xF3n. duplicate_skipped: true (se us\xF3 directamente con \xE9xito)`);
3928
3960
  }
3929
3961
  const stepsToSave = stepsData.length > 0 ? stepsData : stepsDataBackup;
3930
3962
  const hasCompleteSteps = stepsToSave.length >= stepsDataBackup.length;
@@ -4027,7 +4059,21 @@ Evidence: ${JSON.stringify(v.evidence).substring(0, 400)}`,
4027
4059
  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
4060
  await persistCollectiveMemory(true).catch(() => {
4029
4061
  });
4030
- throw new Error(`Misi\xF3n no completada o finalizada con errores. Raz\xF3n: ${failReason}`);
4062
+ let finalErrorMsg = `Misi\xF3n no completada o finalizada con errores.
4063
+
4064
+ `;
4065
+ finalErrorMsg += `======================================================
4066
+ `;
4067
+ finalErrorMsg += `\u{1F3AF} TIPO DE FALLO: ${failReason}
4068
+ `;
4069
+ if (failReasoning) {
4070
+ finalErrorMsg += `\u{1F9E0} AN\xC1LISIS DEL AGENTE DE QA:
4071
+ ${failReasoning}
4072
+ `;
4073
+ }
4074
+ finalErrorMsg += `======================================================
4075
+ `;
4076
+ throw new Error(finalErrorMsg);
4031
4077
  } else {
4032
4078
  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
4079
  }