@arcadialdev/arcality 3.0.4 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -980,6 +980,34 @@ var sendKeyboardEventTool = {
980
980
  required: ["key"]
981
981
  }
982
982
  };
983
+ var typeSequentiallyTool = {
984
+ name: "type_sequentially",
985
+ description: "QA Skill: Type a full text string character by character into a specific field. Use this for masked numeric inputs, OTP/PIN/CLABE/identification codes, virtual-keyboard flows, or whenever the mission explicitly says to type manually, digit by digit, or character by character. Prefer this over repeated clicks when the field is already visible but normal fill may not reflect the mask correctly.",
986
+ input_schema: {
987
+ type: "object",
988
+ properties: {
989
+ idx: {
990
+ type: "number",
991
+ description: "The IDX of the input-like element to focus before typing"
992
+ },
993
+ text: {
994
+ type: "string",
995
+ description: "The exact text to type sequentially"
996
+ },
997
+ delay_ms: {
998
+ type: "number",
999
+ description: "Delay between keystrokes in milliseconds (default: 80)",
1000
+ default: 80
1001
+ },
1002
+ clear_first: {
1003
+ type: "boolean",
1004
+ description: "Whether to clear any existing value before typing (default: true)",
1005
+ default: true
1006
+ }
1007
+ },
1008
+ required: ["idx", "text"]
1009
+ }
1010
+ };
983
1011
  var interactNativeControlTool = {
984
1012
  name: "interact_native_control",
985
1013
  description: "QA Skill: Interact with native HTML controls that standard click/fill may not handle correctly. This includes: <input type='time'>, <input type='date'>, <input type='datetime-local'>, <input type='color'>, <input type='range'>, <select> with optgroup, and contenteditable elements. This tool uses Playwright's specialized methods (selectOption, fill with format coercion, press sequences) to reliably set values. ALWAYS prefer this tool over manual fill for time/date inputs.",
@@ -1035,6 +1063,7 @@ var qaAdvancedTools = [
1035
1063
  restoreNavigationCheckpointTool,
1036
1064
  createTestEvidenceTool,
1037
1065
  sendKeyboardEventTool,
1066
+ typeSequentiallyTool,
1038
1067
  interactNativeControlTool,
1039
1068
  scrollPageTool
1040
1069
  ];
@@ -1679,6 +1708,112 @@ function isPlaceholderRule(rule) {
1679
1708
  function isRecognizedSection(title) {
1680
1709
  return RECOGNIZED_SECTION_PATTERNS.some((pattern) => pattern.test(title));
1681
1710
  }
1711
+ function asTrimmedString(value) {
1712
+ if (typeof value !== "string")
1713
+ return null;
1714
+ const trimmed = value.trim();
1715
+ return trimmed ? trimmed : null;
1716
+ }
1717
+ function asStringList(value) {
1718
+ if (!Array.isArray(value))
1719
+ return [];
1720
+ return value.map((item) => asTrimmedString(item)).filter((item) => Boolean(item));
1721
+ }
1722
+ function loadQaContextGovernance(projectRoot, contextExists) {
1723
+ const metadataPath = path.join(projectRoot, ".arcality", "qa-context.meta.json");
1724
+ const warnings = [];
1725
+ const emptyResult = {
1726
+ path: metadataPath,
1727
+ exists: false,
1728
+ isValid: false,
1729
+ enforcementMode: getQaContextGovernanceMode(),
1730
+ version: null,
1731
+ updatedBy: null,
1732
+ approvedBy: null,
1733
+ ownerTeam: null,
1734
+ changeSummary: null,
1735
+ effectiveFrom: null,
1736
+ tags: [],
1737
+ warnings
1738
+ };
1739
+ if (!fs.existsSync(metadataPath)) {
1740
+ if (contextExists) {
1741
+ warnings.push("No se encontro qa-context.meta.json. Agrega metadata de gobernanza para versionar responsables y cambios.");
1742
+ }
1743
+ return emptyResult;
1744
+ }
1745
+ try {
1746
+ const raw = fs.readFileSync(metadataPath, "utf8");
1747
+ const parsed = JSON.parse(raw);
1748
+ const metadata = {
1749
+ path: metadataPath,
1750
+ exists: true,
1751
+ isValid: true,
1752
+ enforcementMode: getQaContextGovernanceMode(),
1753
+ version: asTrimmedString(parsed?.version),
1754
+ updatedBy: asTrimmedString(parsed?.updated_by ?? parsed?.updatedBy),
1755
+ approvedBy: asTrimmedString(parsed?.approved_by ?? parsed?.approvedBy),
1756
+ ownerTeam: asTrimmedString(parsed?.owner_team ?? parsed?.ownerTeam),
1757
+ changeSummary: asTrimmedString(parsed?.change_summary ?? parsed?.changeSummary),
1758
+ effectiveFrom: asTrimmedString(parsed?.effective_from ?? parsed?.effectiveFrom),
1759
+ tags: asStringList(parsed?.tags),
1760
+ warnings
1761
+ };
1762
+ if (!metadata.version)
1763
+ warnings.push('qa-context.meta.json no define "version".');
1764
+ if (!metadata.updatedBy)
1765
+ warnings.push('qa-context.meta.json no define "updated_by".');
1766
+ if (!metadata.ownerTeam)
1767
+ warnings.push('qa-context.meta.json no define "owner_team".');
1768
+ if (!metadata.changeSummary)
1769
+ warnings.push('qa-context.meta.json no define "change_summary".');
1770
+ metadata.isValid = warnings.length === 0;
1771
+ return metadata;
1772
+ } catch (error) {
1773
+ warnings.push("qa-context.meta.json no se pudo leer o parsear correctamente.");
1774
+ return {
1775
+ ...emptyResult,
1776
+ exists: true,
1777
+ warnings
1778
+ };
1779
+ }
1780
+ }
1781
+ function getQaContextGovernanceMode() {
1782
+ const raw = String(process.env.ARCALITY_QA_CONTEXT_GOVERNANCE || "").trim().toLowerCase();
1783
+ if (raw === "off")
1784
+ return "off";
1785
+ if (raw === "strict" || raw === "enforce" || raw === "required")
1786
+ return "strict";
1787
+ if (raw === "warn")
1788
+ return "warn";
1789
+ if (process.env.CI === "true")
1790
+ return "strict";
1791
+ return "warn";
1792
+ }
1793
+ function getQaContextGovernanceIssues(analysis) {
1794
+ if (!analysis.exists)
1795
+ return [];
1796
+ const issues = [];
1797
+ if (!analysis.governance.exists) {
1798
+ issues.push("Falta el archivo .arcality/qa-context.meta.json para gobernanza del contexto.");
1799
+ return issues;
1800
+ }
1801
+ if (!analysis.governance.version)
1802
+ issues.push("La metadata no define version.");
1803
+ if (!analysis.governance.updatedBy)
1804
+ issues.push("La metadata no define updated_by.");
1805
+ if (!analysis.governance.ownerTeam)
1806
+ issues.push("La metadata no define owner_team.");
1807
+ if (!analysis.governance.changeSummary)
1808
+ issues.push("La metadata no define change_summary.");
1809
+ return issues;
1810
+ }
1811
+ function shouldBlockOnQaContextGovernance(analysis) {
1812
+ const mode = analysis.governance.enforcementMode;
1813
+ if (mode !== "strict")
1814
+ return false;
1815
+ return getQaContextGovernanceIssues(analysis).length > 0;
1816
+ }
1682
1817
  function analyzeQaContext(projectRoot = process.cwd()) {
1683
1818
  const filePath = path.join(projectRoot, ".arcality", "qa-context.md");
1684
1819
  const emptyResult = {
@@ -1690,7 +1825,12 @@ function analyzeQaContext(projectRoot = process.cwd()) {
1690
1825
  ruleCount: 0,
1691
1826
  sectionCount: 0,
1692
1827
  recognizedSectionCount: 0,
1828
+ unrecognizedSectionCount: 0,
1693
1829
  ignoredExampleCount: 0,
1830
+ duplicateRuleCount: 0,
1831
+ lastModifiedAt: null,
1832
+ fingerprint: null,
1833
+ governance: loadQaContextGovernance(projectRoot, false),
1694
1834
  warnings: [],
1695
1835
  status: "missing",
1696
1836
  isValid: false
@@ -1699,12 +1839,16 @@ function analyzeQaContext(projectRoot = process.cwd()) {
1699
1839
  return emptyResult;
1700
1840
  }
1701
1841
  const rawContent = fs.readFileSync(filePath, "utf8");
1842
+ const stat = fs.statSync(filePath);
1702
1843
  const cleanedContent = stripHtmlComments(rawContent);
1703
1844
  const lines = cleanedContent.split(/\r?\n/);
1704
- const warnings = [];
1845
+ const governance = loadQaContextGovernance(projectRoot, true);
1846
+ const warnings = [...governance.warnings];
1705
1847
  const sections = [];
1706
1848
  let currentSection = null;
1707
1849
  let ignoredExampleCount = 0;
1850
+ let duplicateRuleCount = 0;
1851
+ const seenRules = /* @__PURE__ */ new Set();
1708
1852
  const openSection = (title) => {
1709
1853
  currentSection = {
1710
1854
  title: title.trim(),
@@ -1731,6 +1875,12 @@ function analyzeQaContext(projectRoot = process.cwd()) {
1731
1875
  ignoredExampleCount += 1;
1732
1876
  continue;
1733
1877
  }
1878
+ const dedupeKey = rule.toLowerCase();
1879
+ if (seenRules.has(dedupeKey)) {
1880
+ duplicateRuleCount += 1;
1881
+ continue;
1882
+ }
1883
+ seenRules.add(dedupeKey);
1734
1884
  if (currentSection) {
1735
1885
  currentSection.rules.push(rule);
1736
1886
  }
@@ -1738,6 +1888,7 @@ function analyzeQaContext(projectRoot = process.cwd()) {
1738
1888
  const nonEmptySections = sections.filter((section) => section.rules.length > 0);
1739
1889
  const ruleCount = nonEmptySections.reduce((total, section) => total + section.rules.length, 0);
1740
1890
  const recognizedSectionCount = nonEmptySections.filter((section) => section.recognized).length;
1891
+ const unrecognizedSectionCount = nonEmptySections.length - recognizedSectionCount;
1741
1892
  if (!rawContent.trim()) {
1742
1893
  warnings.push("El archivo existe pero esta vacio.");
1743
1894
  }
@@ -1754,6 +1905,12 @@ function analyzeQaContext(projectRoot = process.cwd()) {
1754
1905
  if (ruleCount > 0 && recognizedSectionCount === 0) {
1755
1906
  warnings.push("Hay reglas, pero ninguna cae en las secciones sugeridas. Revisa la estructura del archivo.");
1756
1907
  }
1908
+ if (duplicateRuleCount > 0) {
1909
+ warnings.push(`Se omitieron ${duplicateRuleCount} regla(s) duplicadas del QA Context.`);
1910
+ }
1911
+ if (unrecognizedSectionCount > 0) {
1912
+ warnings.push(`Hay ${unrecognizedSectionCount} seccion(es) con reglas que no coinciden con la taxonomia sugerida.`);
1913
+ }
1757
1914
  const sanitizedContent = nonEmptySections.map((section) => {
1758
1915
  const rules = section.rules.map((rule) => `- ${rule}`).join("\n");
1759
1916
  return `## ${section.title}
@@ -1775,7 +1932,12 @@ ${rules}`;
1775
1932
  ruleCount,
1776
1933
  sectionCount: nonEmptySections.length,
1777
1934
  recognizedSectionCount,
1935
+ unrecognizedSectionCount,
1778
1936
  ignoredExampleCount,
1937
+ duplicateRuleCount,
1938
+ lastModifiedAt: stat.mtime.toISOString(),
1939
+ fingerprint: `${stat.size}:${stat.mtimeMs}`,
1940
+ governance,
1779
1941
  warnings,
1780
1942
  status,
1781
1943
  isValid: ruleCount > 0
@@ -1793,10 +1955,28 @@ function formatQaContextSummary(analysis) {
1793
1955
  lines.push("QA Context Summary");
1794
1956
  lines.push(`Status: ${analysis.status}`);
1795
1957
  lines.push(`Path: ${analysis.filePath}`);
1958
+ lines.push(`Priority: local qa-context > backend collective memory > base heuristics`);
1796
1959
  lines.push(`Rules loaded: ${analysis.ruleCount}`);
1797
1960
  lines.push(`Sections with rules: ${analysis.sectionCount}`);
1798
1961
  lines.push(`Recognized sections: ${analysis.recognizedSectionCount}`);
1962
+ lines.push(`Unrecognized sections: ${analysis.unrecognizedSectionCount}`);
1799
1963
  lines.push(`Ignored examples/placeholders: ${analysis.ignoredExampleCount}`);
1964
+ lines.push(`Duplicate rules removed: ${analysis.duplicateRuleCount}`);
1965
+ lines.push(`Last modified: ${analysis.lastModifiedAt || "N/A"}`);
1966
+ lines.push(`Fingerprint: ${analysis.fingerprint || "N/A"}`);
1967
+ lines.push(`Governance metadata: ${analysis.governance.exists ? analysis.governance.isValid ? "valid" : "warning" : "missing"}`);
1968
+ lines.push(`Governance enforcement: ${analysis.governance.enforcementMode}`);
1969
+ if (analysis.governance.exists) {
1970
+ lines.push("");
1971
+ lines.push("Governance metadata:");
1972
+ lines.push(`- Version: ${analysis.governance.version || "N/A"}`);
1973
+ lines.push(`- Updated by: ${analysis.governance.updatedBy || "N/A"}`);
1974
+ lines.push(`- Approved by: ${analysis.governance.approvedBy || "N/A"}`);
1975
+ lines.push(`- Owner team: ${analysis.governance.ownerTeam || "N/A"}`);
1976
+ lines.push(`- Effective from: ${analysis.governance.effectiveFrom || "N/A"}`);
1977
+ lines.push(`- Change summary: ${analysis.governance.changeSummary || "N/A"}`);
1978
+ lines.push(`- Tags: ${analysis.governance.tags.length > 0 ? analysis.governance.tags.join(", ") : "N/A"}`);
1979
+ }
1800
1980
  if (analysis.sections.length > 0) {
1801
1981
  lines.push("");
1802
1982
  lines.push("Sections applied:");
@@ -1811,12 +1991,78 @@ function formatQaContextSummary(analysis) {
1811
1991
  lines.push(`- ${warning}`);
1812
1992
  }
1813
1993
  }
1994
+ const governanceIssues = getQaContextGovernanceIssues(analysis);
1995
+ if (governanceIssues.length > 0) {
1996
+ lines.push("");
1997
+ lines.push("Governance issues:");
1998
+ for (const issue of governanceIssues) {
1999
+ lines.push(`- ${issue}`);
2000
+ }
2001
+ }
1814
2002
  if (!analysis.exists) {
1815
2003
  lines.push("");
1816
2004
  lines.push("No local QA Context file was found for this mission.");
1817
2005
  }
1818
2006
  return lines.join("\n");
1819
2007
  }
2008
+ function buildEffectiveContextBlock(params) {
2009
+ const { localAnalysis, localPromptBlock, backendContext } = params;
2010
+ const backendRules = backendContext?.rules || [];
2011
+ const backendFields = backendContext?.fields || [];
2012
+ const blocks = [];
2013
+ if (localPromptBlock) {
2014
+ const governanceLines = [
2015
+ `Version: ${localAnalysis.governance.version || "N/A"}`,
2016
+ `Updated by: ${localAnalysis.governance.updatedBy || "N/A"}`,
2017
+ `Approved by: ${localAnalysis.governance.approvedBy || "N/A"}`,
2018
+ `Owner team: ${localAnalysis.governance.ownerTeam || "N/A"}`,
2019
+ `Change summary: ${localAnalysis.governance.changeSummary || "N/A"}`
2020
+ ].join("\n");
2021
+ blocks.push(
2022
+ `<LOCAL_QA_CONTEXT>
2023
+ Priority: Highest.
2024
+ If any local rule conflicts with backend collective memory, follow the local rule.
2025
+ Governance metadata:
2026
+ ${governanceLines}
2027
+ ${localPromptBlock}
2028
+ </LOCAL_QA_CONTEXT>`
2029
+ );
2030
+ }
2031
+ if (backendRules.length > 0 || backendFields.length > 0) {
2032
+ const backendLines = [];
2033
+ backendLines.push(`<BACKEND_COLLECTIVE_MEMORY>`);
2034
+ backendLines.push(`Priority: Secondary. Use this when local qa-context does not define the rule explicitly.`);
2035
+ if (backendRules.length > 0) {
2036
+ backendLines.push(`Rules:`);
2037
+ for (const rule of backendRules) {
2038
+ backendLines.push(`- [${rule.severity}] ${rule.title}: ${rule.description}`);
2039
+ }
2040
+ }
2041
+ if (backendFields.length > 0) {
2042
+ backendLines.push(`Known fields:`);
2043
+ for (const field of backendFields) {
2044
+ backendLines.push(`- ${field.field_identifier} (${field.field_type}) - Required: ${field.is_required}`);
2045
+ }
2046
+ }
2047
+ backendLines.push(`</BACKEND_COLLECTIVE_MEMORY>`);
2048
+ blocks.push(backendLines.join("\n"));
2049
+ }
2050
+ if (blocks.length === 0)
2051
+ return "";
2052
+ return `
2053
+ <CUSTOMER_CONTEXT_POLICY>
2054
+ Use customer context with this precedence order:
2055
+ 1. Local qa-context file
2056
+ 2. Backend collective memory
2057
+ 3. Base QA heuristics
2058
+ Never ignore a valid local qa-context rule because of a conflicting backend memory entry.
2059
+ Local context status: ${localAnalysis.status}
2060
+ Local context fingerprint: ${localAnalysis.fingerprint || "N/A"}
2061
+ </CUSTOMER_CONTEXT_POLICY>
2062
+
2063
+ ` + blocks.join("\n\n") + `
2064
+ `;
2065
+ }
1820
2066
 
1821
2067
  // tests/_helpers/ai-agent-helper.ts
1822
2068
  var qaSecurityTools = [
@@ -1873,7 +2119,7 @@ var AIAgentHelper = class {
1873
2119
  qaContextAnalysisCache = null;
1874
2120
  qaContextPromptCache = "";
1875
2121
  qaContextStatusLogged = false;
1876
- constructor(page, contextDir = "out", testInfo, knowledgeService) {
2122
+ constructor(page, contextDir, testInfo, knowledgeService) {
1877
2123
  this.page = page;
1878
2124
  this.contextDir = contextDir;
1879
2125
  this.testInfo = testInfo;
@@ -2707,7 +2953,8 @@ ${JSON.stringify(diagnosticPayload, null, 2)}`
2707
2953
  let projectInfo = "";
2708
2954
  try {
2709
2955
  const { getProjectContext } = require("../src/projectInspector");
2710
- const ctx = getProjectContext(process.cwd());
2956
+ const runtimeProjectRoot = process.env.ARCALITY_PROJECT_ROOT || process.cwd();
2957
+ const ctx = getProjectContext(runtimeProjectRoot);
2711
2958
  projectInfo = `
2712
2959
  \u{1F3D7}\uFE0F PROJECT CONTEXT:
2713
2960
  - Name: ${ctx.name}
@@ -2717,51 +2964,22 @@ ${JSON.stringify(diagnosticPayload, null, 2)}`
2717
2964
  `;
2718
2965
  } catch {
2719
2966
  }
2720
- let memoryBackendContext = "";
2967
+ let backendContextData = null;
2721
2968
  try {
2722
- const contextData = await this.knowledgeService.getContext(state.url);
2723
- if (contextData && (contextData.rules?.length > 0 || contextData.fields?.length > 0)) {
2724
- memoryBackendContext = `
2725
- \u{1F9E0} BACKEND COLLECTIVE MEMORY FOR THIS PAGE:
2726
- `;
2727
- if (contextData.rules?.length > 0) {
2728
- memoryBackendContext += `BUSINESS RULES:
2729
- `;
2730
- contextData.rules.forEach((r) => memoryBackendContext += `- [${r.severity}] ${r.title}: ${r.description}
2731
- `);
2732
- }
2733
- if (contextData.fields?.length > 0) {
2734
- memoryBackendContext += `KNOWN FIELDS:
2735
- `;
2736
- contextData.fields.forEach((f) => memoryBackendContext += `- ${f.field_identifier} (${f.field_type}) - Required: ${f.is_required}
2737
- `);
2738
- }
2969
+ backendContextData = await this.knowledgeService.getContext(state.url);
2970
+ if (backendContextData && (backendContextData.rules?.length > 0 || backendContextData.fields?.length > 0)) {
2739
2971
  if (process.env.DEBUG)
2740
- console.log(`>>ARCALITY_STATUS>> \u{1F9E0} Contexto de Memoria Colectiva inyectado (${contextData.rules?.length || 0} reglas, ${contextData.fields?.length || 0} campos).`);
2972
+ console.log(`>>ARCALITY_STATUS>> \u{1F9E0} Contexto de Memoria Colectiva inyectado (${backendContextData.rules?.length || 0} reglas, ${backendContextData.fields?.length || 0} campos).`);
2741
2973
  }
2742
2974
  } catch (e) {
2743
2975
  if (process.env.DEBUG)
2744
2976
  console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error en hook de contexto: ${e.message}`);
2745
2977
  }
2746
2978
  let customUserContext = "";
2747
- try {
2748
- const fs7 = require("fs");
2749
- const path7 = require("path");
2750
- const customContextPath = path7.join(process.cwd(), ".arcality", "qa-context.md");
2751
- if (false) {
2752
- const content = fs7.readFileSync(customContextPath, "utf8");
2753
- customUserContext = `
2754
- <CUSTOMER_BUSINESS_RULES>
2755
- El Ing. de QA o Cliente final ha provisto las siguientes reglas estrictas para este negocio/dominio. DEBES priorizar esta informaci\xF3n sobre todas tus skills base:
2756
- ${content}
2757
- </CUSTOMER_BUSINESS_RULES>
2758
- `;
2759
- }
2760
- } catch (e) {
2761
- }
2762
2979
  try {
2763
2980
  if (!this.qaContextAnalysisCache) {
2764
- const qaContextState = loadQaContextForPrompt(process.cwd());
2981
+ const runtimeProjectRoot = process.env.ARCALITY_PROJECT_ROOT || process.cwd();
2982
+ const qaContextState = loadQaContextForPrompt(runtimeProjectRoot);
2765
2983
  this.qaContextAnalysisCache = qaContextState.analysis;
2766
2984
  this.qaContextPromptCache = qaContextState.promptBlock;
2767
2985
  }
@@ -2777,18 +2995,16 @@ ${content}
2777
2995
  console.log(`>>ARCALITY_STATUS>> QA Context warning: ${warning}`);
2778
2996
  }
2779
2997
  }
2998
+ if (qaContextAnalysis.exists || backendContextData?.rules?.length || backendContextData?.fields?.length) {
2999
+ console.log(`>>ARCALITY_STATUS>> Pol\xEDtica efectiva de contexto: local qa-context > memoria backend > heur\xEDsticas base.`);
3000
+ }
2780
3001
  this.qaContextStatusLogged = true;
2781
3002
  }
2782
- if (this.qaContextPromptCache) {
2783
- customUserContext = `
2784
- <CUSTOMER_BUSINESS_RULES>
2785
- El Ing. de QA o Cliente final ha provisto reglas locales para este negocio/dominio. Debes priorizar esta informacion sobre tus heuristicas base cuando exista conflicto.
2786
- ${this.qaContextPromptCache}
2787
- </CUSTOMER_BUSINESS_RULES>
2788
- `;
2789
- } else {
2790
- customUserContext = "";
2791
- }
3003
+ customUserContext = buildEffectiveContextBlock({
3004
+ localAnalysis: qaContextAnalysis,
3005
+ localPromptBlock: this.qaContextPromptCache,
3006
+ backendContext: backendContextData
3007
+ });
2792
3008
  } catch (e) {
2793
3009
  }
2794
3010
  const systemPromptBlocks = [
@@ -2851,6 +3067,7 @@ These are NOT optional. You MUST use these tools in these situations:
2851
3067
  | Bug found in application | \`create_test_evidence\` | To document with annotated screenshot |
2852
3068
  | Mission complete | \`create_test_evidence\` | To document the final successful state |
2853
3069
  | Modal/popup blocking page | \`send_keyboard_event\` (Escape) | To close the overlay |
3070
+ | Masked numeric/code input, OTP/PIN/CLABE/INE identifier, or prompt says "manualmente", "d\xEDgito por d\xEDgito", "car\xE1cter por car\xE1cter" | \`type_sequentially\` | After ONE focus attempt on the field; do not keep clicking |
2854
3071
  | Time/Date/Color picker field | \`interact_native_control\` | INSTEAD of fill \u2014 native inputs need special handling |
2855
3072
  | Element not found on screen | \`scroll_page\` | To reveal off-screen content |
2856
3073
  | Need to close dropdown/menu | \`send_keyboard_event\` (Escape) | IMMEDIATELY after selecting any option \u2014 MANDATORY |
@@ -2895,7 +3112,13 @@ These are NOT optional. You MUST use these tools in these situations:
2895
3112
  13. **NATIVE CONTROLS**: For \`<input type="time">\`, \`<input type="date">\`, or similar native pickers, NEVER use \`fill\` repeatedly. Use \`interact_native_control\` or \`send_keyboard_event\` instead. If \`fill\` fails once, switch strategy immediately.
2896
3113
  14. **USER-DEFINED BUGS (MANDATORY & IMMEDIATE)**: If the prompt explicitly defines a condition as a bug (e.g., "Si no te deja eliminarla, es un bug de la aplicaci\xF3n", or "If X fails it's a bug"), and you encounter that condition, you MUST call \`report_application_bug\` IMMEDIATELY in that exact same turn. DO NOT perform any intermediate UI actions (such as clicking "Cancelar", closing modals, or navigating away) before calling the tool. Any intermediate UI action will cause you to lose your reasoning context in the next turn and enter an infinite loop. The call to \`report_application_bug\` must take absolute priority over any UI cleanup or close actions. DO NOT dismiss it as "expected business logic" or "referential integrity". The user's definition of a bug OVERRIDES all general reasoning.
2897
3114
 
2898
- 15. **FILE UPLOAD FIELDS (MANDATORY PROTOCOL)**: When the current step requires uploading an image or file (a dropzone component with text like "arrastra", "explora", "formatos", "JPEG", "PNG", or an input with aria-label containing "imagen" or "file"):
3115
+ 15. **MASKED / SEQUENTIAL INPUTS (MANDATORY PROTOCOL)**: If the field is a masked identifier/code input (examples: placeholders like \`000-000-000-0000\`, OTP/PIN boxes, CLABE/account identifiers, ID/INE codes) OR the mission explicitly says "manualmente", "n\xFAmero por n\xFAmero", "d\xEDgito por d\xEDgito", or "car\xE1cter por car\xE1cter":
3116
+ - You get EXACTLY ONE focus attempt: click the field once, or click a nearby keyboard button/icon ONCE if the UI shows one (e.g. "Teclado").
3117
+ - Then you MUST use \`type_sequentially\` with the full string. Do NOT simulate manual typing by repeatedly clicking the field.
3118
+ - If the field still does not accept input after that, use \`inspect_element_details\` on the field or the adjacent keyboard trigger. Never click the same input 3+ times in a row.
3119
+ - For numeric codes, preserve exact length. Count the digits from the mission and type all of them.
3120
+
3121
+ 16. **FILE UPLOAD FIELDS (MANDATORY PROTOCOL)**: When the current step requires uploading an image or file (a dropzone component with text like "arrastra", "explora", "formatos", "JPEG", "PNG", or an input with aria-label containing "imagen" or "file"):
2899
3122
  - You MUST use the \`fill\` action on the file input with one of the values from the \`# AVAILABLE UPLOAD ASSETS\` section below (format: \`arcasset://<asset_id>\`).
2900
3123
  - If no asset matches the required type, use the value \`"placeholder:image/banner"\` \u2014 the runner will automatically generate a valid placeholder PNG image.
2901
3124
  - **NEVER** report a bug or inability to proceed just because you see a file upload field. You ALWAYS have a fallback.
@@ -2941,7 +3164,9 @@ When a form is divided into numbered steps (e.g., Step 1 \u2192 Step 2 \u2192 St
2941
3164
 
2942
3165
  11. **COMBOBOX KEYBOARD FALLBACK**: If you click a combobox/select field and the dropdown options (\`[li]\`, \`[OPTION]\`) are NOT visible after 1 turn, use this keyboard sequence: a) Click the combobox field ONCE. b) Use \`send_keyboard_event\` with key "ArrowDown" to open the options and highlight the first one. c) Use \`send_keyboard_event\` with key "Enter" to select it. This avoids the toggle problem and works with all dropdown implementations.
2943
3166
 
2944
- 12. **ERROR STOP PROTOCOL (CRITICAL)**: If you see a message starting with \`\u{1F6D1} [ERROR_CR\xCDTICO]\` or \`\u{1F6D1} ERROR POST-GUARDADO\`, do NOT assume it is a transient error. You MUST analyze the text. If it says "ya existe" or "duplicado", you MUST change the offending field value (e.g., add a random number) AND click the save button in the SAME turn. Do NOT waste a turn closing the error toast. If you retry without changing any data and the error persists, you MUST use \`report_inability_to_proceed\` immediately. Looping on the same error toast is FORBIDDEN.`,
3167
+ 12. **SEQUENTIAL CODE INPUT FALLBACK (CRITICAL)**: If you are on a code/identifier step and the UI shows a field like "Identificador", an adjacent "Teclado" button, or a mask such as \`000-000-000-0000\`, NEVER spend multiple turns trying to focus the field. One click is enough. After that, use \`type_sequentially\` with the complete code string. If the keyboard button exists and the first direct focus did not work, click the keyboard button ONCE and then use \`type_sequentially\`.
3168
+
3169
+ 13. **ERROR STOP PROTOCOL (CRITICAL)**: If you see a message starting with \`\u{1F6D1} [ERROR_CR\xCDTICO]\` or \`\u{1F6D1} ERROR POST-GUARDADO\`, do NOT assume it is a transient error. You MUST analyze the text. If it says "ya existe" or "duplicado", you MUST change the offending field value (e.g., add a random number) AND click the save button in the SAME turn. Do NOT waste a turn closing the error toast. If you retry without changing any data and the error persists, you MUST use \`report_inability_to_proceed\` immediately. Looping on the same error toast is FORBIDDEN.`,
2945
3170
  cache_control: { type: "ephemeral" }
2946
3171
  },
2947
3172
  {
@@ -2955,7 +3180,6 @@ ${skillsContext}
2955
3180
  ${customUserContext}
2956
3181
  ${memoryContext}
2957
3182
  ${credentialsContext}
2958
- ${memoryBackendContext}
2959
3183
  ${assetContext}`
2960
3184
  }
2961
3185
  ];
@@ -3534,6 +3758,45 @@ ${report}` });
3534
3758
  } catch (e) {
3535
3759
  toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error sending key: ${e.message}` });
3536
3760
  }
3761
+ } else if (toolName === "type_sequentially") {
3762
+ const { idx, text, delay_ms = 80, clear_first = true } = toolInput;
3763
+ console.log(`>>ARCALITY_STATUS>> \u2328\uFE0F\u270D\uFE0F Type sequentially IDX:${idx} \u2192 "${text}"`);
3764
+ const c = state.components.find((comp) => comp.idx === idx);
3765
+ if (!c) {
3766
+ toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: "Element not found." });
3767
+ } else {
3768
+ try {
3769
+ const frame = this.page.frames()[c.frameIdx];
3770
+ const loc = frame.locator(c.selector).first();
3771
+ await loc.scrollIntoViewIfNeeded({ timeout: 5e3 }).catch(() => {
3772
+ });
3773
+ await loc.focus({ timeout: 3e3 }).catch(async () => {
3774
+ await loc.click({ timeout: 5e3, force: true });
3775
+ });
3776
+ if (clear_first) {
3777
+ await this.page.keyboard.press("Control+a").catch(() => {
3778
+ });
3779
+ await this.page.keyboard.press("Delete").catch(() => {
3780
+ });
3781
+ await this.page.waitForTimeout(100);
3782
+ }
3783
+ await this.page.keyboard.type(String(text), { delay: Math.max(10, Math.min(Number(delay_ms) || 80, 400)) });
3784
+ const finalValue = await loc.evaluate((el) => {
3785
+ if (typeof el.value === "string")
3786
+ return el.value;
3787
+ if (el.isContentEditable)
3788
+ return el.innerText || "";
3789
+ return document.activeElement?.value || "";
3790
+ }).catch(() => "");
3791
+ toolResults.push({
3792
+ type: "tool_result",
3793
+ tool_use_id: toolUse.id,
3794
+ content: `\u2705 Sequential typing completed. Current value: "${finalValue}".`
3795
+ });
3796
+ } catch (e) {
3797
+ toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error typing sequentially: ${e.message}` });
3798
+ }
3799
+ }
3537
3800
  } else if (toolName === "interact_native_control") {
3538
3801
  const { idx, control_type, value } = toolInput;
3539
3802
  const c = state.components.find((comp) => comp.idx === idx);
@@ -3944,6 +4207,33 @@ async function readLocatorText(locator) {
3944
4207
  const innerText = await locator.innerText().catch(() => "");
3945
4208
  return typeof innerText === "string" ? innerText.trim() : "";
3946
4209
  }
4210
+ async function readHiddenPortalResult(page) {
4211
+ const selectors = [
4212
+ "input#result",
4213
+ 'input[name="result"]',
4214
+ 'input[readonly][id*="result" i]',
4215
+ 'input[readonly][name*="result" i]'
4216
+ ];
4217
+ for (const selector of selectors) {
4218
+ try {
4219
+ const locator = page.locator(selector).first();
4220
+ if (!await locator.count().catch(() => 0))
4221
+ continue;
4222
+ const rawValue = await locator.inputValue().catch(async () => await locator.getAttribute("value"));
4223
+ const raw = typeof rawValue === "string" ? rawValue.trim() : "";
4224
+ if (!raw)
4225
+ continue;
4226
+ const parsed = JSON.parse(raw);
4227
+ const status = String(parsed?.status || parsed?.Status || "").trim().toLowerCase();
4228
+ const message = String(parsed?.message || parsed?.Message || parsed?.detail || parsed?.error || "").trim();
4229
+ if (status) {
4230
+ return { status, message, raw };
4231
+ }
4232
+ } catch {
4233
+ }
4234
+ }
4235
+ return null;
4236
+ }
3947
4237
  function writeBatchMissionResult(payload) {
3948
4238
  const runDomainDir = process.env.ARCALITY_RUN_DOMAIN_DIR;
3949
4239
  if (!runDomainDir)
@@ -3961,7 +4251,10 @@ function writeBatchMissionResult(payload) {
3961
4251
  }
3962
4252
  (0, import_test.test)("Arcality AI Runner", async ({ page }, testInfo) => {
3963
4253
  import_test.test.setTimeout(12e5);
3964
- const contextDir = process.env.CONTEXT_DIR || "out";
4254
+ const contextDir = process.env.CONTEXT_DIR;
4255
+ if (!contextDir) {
4256
+ throw new Error("Missing CONTEXT_DIR. Arcality must run through the CLI wrapper so outputs stay inside the user project .arcality directory.");
4257
+ }
3965
4258
  const activeConfig = process.env.ACTIVE_CONFIG || "Default";
3966
4259
  if (!process.env.BASE_URL && process.env[`${activeConfig}_URL`])
3967
4260
  process.env.BASE_URL = process.env[`${activeConfig}_URL`];
@@ -3988,7 +4281,8 @@ function writeBatchMissionResult(payload) {
3988
4281
  }
3989
4282
  const agent = new AIAgentHelper(page, contextDir, testInfo);
3990
4283
  const prompt = process.env.SMART_PROMPT || "Navega al inicio y verifica que el sitio cargue";
3991
- const qaContextAnalysis = analyzeQaContext(process.cwd());
4284
+ const runtimeProjectRoot = process.env.ARCALITY_PROJECT_ROOT || process.cwd();
4285
+ const qaContextAnalysis = analyzeQaContext(runtimeProjectRoot);
3992
4286
  if (qaContextAnalysis.exists && qaContextAnalysis.isValid) {
3993
4287
  console.log(`>>ARCALITY_STATUS>> QA Context validado al arranque (${qaContextAnalysis.ruleCount} reglas, ${qaContextAnalysis.sectionCount} secciones).`);
3994
4288
  } else if (qaContextAnalysis.exists) {
@@ -3999,10 +4293,24 @@ function writeBatchMissionResult(payload) {
3999
4293
  } else {
4000
4294
  console.log(`>>ARCALITY_STATUS>> QA Context local no configurado para esta mision.`);
4001
4295
  }
4296
+ if (qaContextAnalysis.governance.exists) {
4297
+ console.log(`>>ARCALITY_STATUS>> QA Context metadata: version=${qaContextAnalysis.governance.version || "N/A"} | updated_by=${qaContextAnalysis.governance.updatedBy || "N/A"} | owner_team=${qaContextAnalysis.governance.ownerTeam || "N/A"}`);
4298
+ }
4002
4299
  await testInfo.attach("qa_context_summary", {
4003
4300
  body: Buffer.from(formatQaContextSummary(qaContextAnalysis), "utf-8"),
4004
4301
  contentType: "text/plain"
4005
4302
  });
4303
+ const governanceIssues = getQaContextGovernanceIssues(qaContextAnalysis);
4304
+ if (governanceIssues.length > 0) {
4305
+ for (const issue of governanceIssues) {
4306
+ console.log(`>>ARCALITY_STATUS>> QA Context governance issue: ${issue}`);
4307
+ }
4308
+ }
4309
+ if (shouldBlockOnQaContextGovernance(qaContextAnalysis)) {
4310
+ throw new Error(
4311
+ `QA Context governance is required for this run (${qaContextAnalysis.governance.enforcementMode}) and is incomplete. Fix .arcality/qa-context.meta.json before continuing.`
4312
+ );
4313
+ }
4006
4314
  const successKeywords = /exitosamente|creado correctamente|guardado correctamente|misión cumplida|registered successfully|created successfully|saved successfully|operación exitosa|registro guardado|successfully|correctly/i;
4007
4315
  const failureKeywords = /\berror\b|falló|failed|no se puede|cannot|unable|ya existe|already exists|existente|exist|inválido|invalid|incorrecto|incorrect|ligado|linked|depende|depends|duplicado|duplicate|repetido|repeated|conflict|reintentar|retry|missing|required|obligatorio|bad request|400|500/i;
4008
4316
  const systemErrorKeywords = /error interno|internal error|intentarlo más tarde|try again later|inténtelo más tarde|servicio no disponible|service unavailable|something went wrong|algo salió mal|ha ocurrido un error inesperado|unexpected error|server error|no se pudo procesar|could not be processed|comuníquese con soporte|contact support/i;
@@ -4150,7 +4458,10 @@ function writeBatchMissionResult(payload) {
4150
4458
  const logPath = path6.join(contextDir, `agent-log-${timestampMs}.json`);
4151
4459
  fs6.writeFileSync(logPath, JSON.stringify(summaryPayload, null, 2));
4152
4460
  missionSummaryPaths.add(logPath);
4153
- const runDomainDir = process.env.ARCALITY_RUN_DOMAIN_DIR || path6.dirname(contextDir);
4461
+ const runDomainDir = process.env.ARCALITY_RUN_DOMAIN_DIR;
4462
+ if (!runDomainDir) {
4463
+ throw new Error("Missing ARCALITY_RUN_DOMAIN_DIR. Arcality must run through the CLI wrapper so durable logs stay inside the user project .arcality directory.");
4464
+ }
4154
4465
  const durableLogsDir = path6.join(runDomainDir, "logs");
4155
4466
  fs6.mkdirSync(durableLogsDir, { recursive: true });
4156
4467
  const durableLogPath = path6.join(durableLogsDir, `mission-log-${timestampMs}.json`);
@@ -4187,9 +4498,11 @@ function writeBatchMissionResult(payload) {
4187
4498
  const pid = process.env.ARCALITY_PROJECT_ID || "";
4188
4499
  const apiUrl = process.env.ARCALITY_API_URL || "";
4189
4500
  const apiKey = process.env.ARCALITY_API_KEY || "";
4190
- if (!pid || !apiUrl || !apiKey) {
4501
+ const orgId = process.env.ARCALITY_ORG_ID || "";
4502
+ if (!pid || !apiUrl || !apiKey || !orgId) {
4503
+ console.log(`>>ARCALITY_STATUS>> \u{1F9E0} Memoria Colectiva omitida por configuraci\xF3n incompleta.`);
4191
4504
  if (process.env.DEBUG)
4192
- console.warn(`[CollectiveMemory] \u26A0\uFE0F Vars faltantes \u2014 project_id='${pid}' api_url='${apiUrl}' api_key='${apiKey ? "OK" : "MISSING"}' \u2192 NO persiste.`);
4505
+ console.warn(`[CollectiveMemory] \u26A0\uFE0F Vars faltantes \u2014 project_id='${pid}' api_url='${apiUrl}' api_key='${apiKey ? "OK" : "MISSING"}' org_id='${orgId || "MISSING"}' \u2192 NO persiste.`);
4193
4506
  return;
4194
4507
  }
4195
4508
  if (collectiveMemoryPersisted && !isFailed) {
@@ -4336,8 +4649,7 @@ function writeBatchMissionResult(payload) {
4336
4649
  const matches = promptKeywords.filter((kw) => patternKeywords.some((pkw) => pkw.includes(kw) || kw.includes(pkw))).length;
4337
4650
  const isVectorMatch = true;
4338
4651
  if (isVectorMatch) {
4339
- if (process.env.DEBUG)
4340
- console.log(`>>ARCALITY_STATUS>> \u{1F9E0} Memoria Colectiva detectada (id: ${bestMatch.id}). Descargando a memoria local...`);
4652
+ console.log(`>>ARCALITY_STATUS>> \u{1F9E0} Memoria Colectiva detectada (id: ${bestMatch.id}). Descargando a memoria local...`);
4341
4653
  reusedPatternId = bestMatch.id || "unknown";
4342
4654
  let patternJsonObj = {};
4343
4655
  try {
@@ -4574,6 +4886,35 @@ Detalles de la excepci\xF3n en consola: ${jsError}`;
4574
4886
  }
4575
4887
  if (isFinished)
4576
4888
  break;
4889
+ try {
4890
+ const hiddenResult = await readHiddenPortalResult(page);
4891
+ if (hiddenResult) {
4892
+ const hiddenStatus = hiddenResult.status.toLowerCase();
4893
+ const hiddenMessage = hiddenResult.message || hiddenResult.raw;
4894
+ if (/^(failed|error|invalid|rejected)$/.test(hiddenStatus)) {
4895
+ console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Resultado oculto detectado: "${hiddenStatus}" - ${hiddenMessage.substring(0, 120)}`);
4896
+ hasCriticalError = true;
4897
+ failReason = "portal_hidden_result_error";
4898
+ failReasoning = `El portal public\xF3 un resultado interno oculto indicando fallo: "${hiddenMessage}". Arcality detuvo la misi\xF3n para evitar loops sobre una operaci\xF3n ya rechazada.`;
4899
+ aiMarkedSuccess = false;
4900
+ isFinished = true;
4901
+ history.push(`Turno ${stepCount}: \u{1F6D1} [HIDDEN_RESULT_ERROR] El portal devolvi\xF3 status="${hiddenStatus}" con mensaje "${hiddenMessage.substring(0, 200)}".`);
4902
+ saveMissionResults();
4903
+ break;
4904
+ }
4905
+ if (/^(success|succeeded|ok|completed)$/.test(hiddenStatus)) {
4906
+ console.log(`>>ARCALITY_STATUS>> \u2728 Resultado oculto de \xE9xito detectado: "${hiddenStatus}"`);
4907
+ aiMarkedSuccess = true;
4908
+ hasCriticalError = false;
4909
+ isFinished = true;
4910
+ history.push(`Turno ${stepCount}: \u2705 [HIDDEN_RESULT_SUCCESS] El portal devolvi\xF3 status="${hiddenStatus}".`);
4911
+ await persistCollectiveMemory();
4912
+ saveMissionResults();
4913
+ break;
4914
+ }
4915
+ }
4916
+ } catch {
4917
+ }
4577
4918
  let hasVisibleError = false;
4578
4919
  try {
4579
4920
  const errorEls = await page.locator('.toast, .alert, [role="alert"], [role="status"], .snackbar, .notification, .error-message, .mat-snack-bar-container, [class*="alert"], [class*="toast"]').all();
@@ -4727,7 +5068,7 @@ ${patternContext}` : prompt;
4727
5068
  const loopType = isClickLoop ? "click" : "fill";
4728
5069
  const uniqueTargets = isClickLoop ? uniqueClicks : uniqueFills;
4729
5070
  console.warn(`
4730
- \u26A0 [OUROBOROS SUBSYSTEM: Repetitive ${loopType} loop anomaly detected]:`);
5071
+ \u26A0 Bucle repetitivo de ${loopType} detectado:`);
4731
5072
  console.warn(` \xDAltimas acciones: ${lastActions.join(" \u2192 ")}`);
4732
5073
  response.thought = `\u{1F6D1} ERROR: Bucle repetitivo en ${loopType} sobre: ${Array.from(uniqueTargets).join(", ")}.`;
4733
5074
  response.finish = true;
@@ -4844,7 +5185,7 @@ Acci\xF3n bloqueada: Bucle de tipo ${loopType} sobre los elementos ${Array.from(
4844
5185
  }
4845
5186
  console.log(`
4846
5187
  \u{1F6D1} [AUTO-STOP] Bloqueando click repetido en "${desc}" (${sameActionCount} veces). Forzando re-evaluaci\xF3n.`);
4847
- history.push(`Turno ${stepCount}: \u{1F6D1} [AUTO-STOP] El click en "${desc}" fue bloqueado despu\xE9s de ${sameActionCount} repeticiones. Si es un dropdown/combobox que no abre sus opciones, usa send_keyboard_event con key "ArrowDown" para navegar opciones y "Enter" para seleccionar. NO vuelvas a hacer click en el mismo elemento.`);
5188
+ history.push(`Turno ${stepCount}: \u{1F6D1} [AUTO-STOP] El click en "${desc}" fue bloqueado despu\xE9s de ${sameActionCount} repeticiones. Si es un dropdown/combobox que no abre sus opciones, usa send_keyboard_event con key "ArrowDown" y luego "Enter". Si es un input enmascarado o un c\xF3digo/identificador, usa type_sequentially con el valor completo despu\xE9s de UN solo enfoque. NO vuelvas a hacer click en el mismo elemento.`);
4848
5189
  await page.waitForTimeout(800);
4849
5190
  break;
4850
5191
  }