@exodus/xqa 1.14.0 → 1.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/xqa.cjs +52 -19
  2. package/package.json +3 -3
package/dist/xqa.cjs CHANGED
@@ -52349,6 +52349,7 @@ function createListAppsTool(udid = "booted") {
52349
52349
  }
52350
52350
  var DEFAULT_LONG_PRESS_DURATION_MS = 500;
52351
52351
  var MS_PER_SECOND = 1e3;
52352
+ var DEFAULT_SWIPE_DURATION_SECONDS = 0.1;
52352
52353
  var ENTER_KEY_CODE = "0x28";
52353
52354
  var TAP_SCHEMA = { x: external_exports.number(), y: external_exports.number() };
52354
52355
  var LONG_PRESS_SCHEMA = { x: external_exports.number(), y: external_exports.number(), duration: external_exports.number().optional() };
@@ -52356,7 +52357,10 @@ var SWIPE_SCHEMA = {
52356
52357
  x_start: external_exports.number(),
52357
52358
  y_start: external_exports.number(),
52358
52359
  x_end: external_exports.number(),
52359
- y_end: external_exports.number()
52360
+ y_end: external_exports.number(),
52361
+ duration: external_exports.number().positive().optional().describe(
52362
+ "Gesture duration in seconds. Smaller values (e.g. 0.1) produce a flick that scrolls lists and dismisses sheets; larger values (e.g. 0.5+) produce a slow drag suitable for reordering or panning. Defaults to 0.1s (flick)."
52363
+ )
52360
52364
  };
52361
52365
  var TYPE_TEXT_SCHEMA = { text: external_exports.string(), submit: external_exports.boolean().optional() };
52362
52366
  var PRESS_BUTTON_SCHEMA = {
@@ -52439,7 +52443,9 @@ function buildSwipeArguments(resolvedUdid, input) {
52439
52443
  String(input.xStart),
52440
52444
  String(input.yStart),
52441
52445
  String(input.xEnd),
52442
- String(input.yEnd)
52446
+ String(input.yEnd),
52447
+ "--duration",
52448
+ String(input.duration)
52443
52449
  ];
52444
52450
  }
52445
52451
  async function handleSwipe(input) {
@@ -52506,17 +52512,22 @@ function createLongPressTool(udid = "booted") {
52506
52512
  async ({ x, y: y6, duration: duration3 }) => handleLongPress({ udid, x, y: y6, duration: duration3 ?? DEFAULT_LONG_PRESS_DURATION_MS })
52507
52513
  );
52508
52514
  }
52515
+ function toSwipeInput(udid, params) {
52516
+ return {
52517
+ udid,
52518
+ xStart: params.x_start,
52519
+ yStart: params.y_start,
52520
+ xEnd: params.x_end,
52521
+ yEnd: params.y_end,
52522
+ duration: params.duration ?? DEFAULT_SWIPE_DURATION_SECONDS
52523
+ };
52524
+ }
52509
52525
  function createSwipeTool(udid = "booted") {
52510
52526
  return _x(
52511
52527
  "swipe",
52512
- "Swipe on the screen from one point to another.",
52528
+ "Swipe on the screen from one point to another. `duration` (seconds) controls gesture velocity: the default 0.1s produces a flick that scrolls lists, dismisses sheets and triggers paging; pass a larger value (e.g. 0.5+) for a slow drag suitable for reordering or panning. Omitting `duration` uses the flick default.",
52513
52529
  SWIPE_SCHEMA,
52514
- async ({
52515
- x_start,
52516
- y_start,
52517
- x_end,
52518
- y_end
52519
- }) => handleSwipe({ udid, xStart: x_start, yStart: y_start, xEnd: x_end, yEnd: y_end })
52530
+ async (params) => handleSwipe(toSwipeInput(udid, params))
52520
52531
  );
52521
52532
  }
52522
52533
  function createTypeTextTool(udid = "booted") {
@@ -62942,15 +62953,17 @@ At every reasoning step, maintain a mental ledger:
62942
62953
  Consult the ledger before every action. Always prefer navigating to a QUEUE screen over a VISITED one.`;
62943
62954
  var SESSION_START_RULE = `Before taking any other action \u2014 including initializing the Working State ledger or emitting findings \u2014 call \`view_ui\` once to observe the starting screen`;
62944
62955
  var BACK_NAV_RULE = `After navigating forward to any new screen: attempt to return to the expected parent in PATH \u2014 consult App Knowledge first for the correct exit gesture on this screen, then try in order: (1) any visible back/close button, (2) OS back gesture, (3) swipe up, (4) swipe down, (5) swipe left, (6) swipe right \u2014 confirm return via \`screenshot\` if the parent is visually unambiguous, \`view_ui\` otherwise \u2014 only after ALL attempts fail emit a \`back-nav-failure\` finding, then navigate forward again to continue`;
62945
- var STUCK_LOOP_RULE = `Stuck loop: emit a \`stuck-loop\` finding when any of these occur: (1) \`view_ui\` returns the same \`<screen_id>\` across 3 or more consecutive \`view_ui\` calls, (2) the same element has been tapped more than twice with no screen change confirmed by \`view_ui\`, (3) PATH shows the same screen at two non-adjacent positions \u2014 before emitting, try one alternative action (scroll, long-press, swipe) to rule out a gesture mismatch \u2014 note: \`screenshot\`-only calls do not update the stuck-loop counter; only \`view_ui\` calls count`;
62956
+ var STUCK_LOOP_RULE = `Stuck loop: emit a \`stuck-loop\` finding when any of these signals occur: (1) \`view_ui\` returns the same \`<screen_id>\` across 3 or more consecutive \`view_ui\` calls, (2) the same element has been tapped more than twice with no screen change confirmed by \`view_ui\`, (3) PATH shows the same screen at two non-adjacent positions \u2014 i.e. you visited a screen, left it, and returned. Example: \`Portfolio \u2192 Asset \u2192 Swap \u2192 Asset\` (Asset revisited). Indicates circular navigation. NOT tripped by adjacent repeats like \`Portfolio \u2192 Asset \u2192 Asset\` (normal state refresh), (4) a scrollable list's element positions \u2014 x for horizontal, y for vertical \u2014 are identical across 2 or more consecutive swipe+\`view_ui\` cycles (zero-delta scroll stall) \u2014 before emitting for (1)\u2013(3), try one alternative action (scroll, long-press, swipe) to rule out a gesture mismatch \u2014 before emitting for (4), baseline element positions via \`view_ui\`, then vary one spatial parameter on the next attempt: shift swipe start coord by \u226540% of visible container dimension, OR reverse direction to probe opposite bound, OR extend throw to span \u226580% of viewport dimension \u2014 then \`view_ui\` and compare. Example: if tab bar positions \`Tokens(-31) ETH(65) ... Tron(352)\` are unchanged across 2 swipes starting near x=350, shift start to x=10 or x=395 and extend end past the opposite viewport edge. If delta still zero, emit \`stuck-loop\` with axis, before/after positions, and variation attempted \u2014 note: \`screenshot\`-only calls do not update the stuck-loop counter; only \`view_ui\` calls count. Zero-delta scroll stall is not a separate finding type \u2014 report as \`stuck-loop\`.`;
62946
62957
  var LOADING_STATE_RULE = `Transient loading state: when the screen shows spinners, skeleton screens, progress bars, "Loading..." text, or placeholder content NOT described in spec or app context \u2014 use \`screenshot\` to poll for resolution (up to 3 retries); switch to \`view_ui\` only on the final check or when you need element data to act \u2014 if loading persists after 3 retries, proceed with what is visible; if spec or app context explicitly describes a loading screen as a step, do not retry \u2014 call \`view_ui\` and assert normally`;
62947
62958
  var EXPECTED_CONTENT_MISSING_RULE = `Expected content missing: when \`view_ui\` shows no loading indicator yet omits an element named or strongly implied by spec or app context \u2014 and its absence is not semantically consistent with the current screen \u2014 call \`wait_seconds\` with 2\u20135 seconds and retry \`view_ui\` up to 2 times; if element remains absent, emit a \`missing-content\` finding stating what was expected and what was observed`;
62948
62959
  var CLIPPED_ELEMENT_RULE = `Never tap an element tagged \`[clipped-top]\`, \`[clipped-bottom]\`, \`[clipped-left]\`, or \`[clipped-right]\` \u2014 scroll to fully reveal it first, then re-call \`view_ui\` before tapping`;
62949
- var SCROLL_FOLD_RULE = `Scrollable lists: elements below the visible fold are absent from the a11y tree by design \u2014 scroll to reveal before asserting presence or absence, and never emit a finding solely because list items or rows are missing from the tree on a scrollable screen`;
62960
+ var SCROLL_FOLD_RULE = `Scrollable lists: elements outside the visible viewport are absent from the a11y tree by design \u2014 this applies to elements below the fold in vertical lists AND elements clipped off-left or off-right in horizontal lists \u2014 scroll or swipe in the appropriate axis to reveal before asserting presence or absence; never emit a finding solely because list items, rows, or tabs are missing from the tree on a scrollable screen; if swipe attempts yield no position change across 2+ cycles, apply the scroll-stall path in STUCK_LOOP_RULE.`;
62950
62961
  var FINDING_TAXONOMY_SECTION = `## Finding Types
62951
62962
 
62952
62963
  You may emit only these trigger types: \`back-nav-failure\`, \`dead-end\`, \`stuck-modal\`, \`stuck-loop\`, \`missing-a11y-element\`, \`missing-content\`, \`spec-deviation\`, \`destructive-only-exit\`. Do NOT emit \`design-system-violation\`, \`motion-regression\`, \`continuity-regression\`, \`interaction-regression\`, or \`loading-regression\` \u2014 those belong to other agents.`;
62953
62964
  var A11Y_FALLBACK_RULE = `Missing a11y element: if you intend to tap or interact with a UI element and that element is absent from the most recent \`view_ui\` a11y tree \u2014 visible in the screenshot does NOT imply interactable; the a11y tree is authoritative \u2014 do NOT estimate its coordinates from the screenshot, do NOT attempt any pixel-based tap, do NOT retry at different coordinates, do NOT long-press or swipe in the element's visual region as a fallback; a failed pixel tap is never an \`interaction-regression\` \u2014 it is a \`missing-a11y-element\` \u2014 instead, emit a \`missing-a11y-element\` finding that states: (1) your intent (what you were trying to do), (2) the approximate visual region where the element appeared (coords/size from the screenshot), (3) nearby labeled elements from the a11y tree that serve as landmarks \u2014 then continue: in freestyle mode keep exploring other reachable screens; in spec mode advance to the next step`;
62965
+ var OUTCOME_LITERAL_RULE = `When verifying a step outcome or assertion, interpret all quantifiers literally and apply them exhaustively: any keyword that imposes a universal or count-bound constraint \u2014 including but not limited to \`only\`, \`all\`, \`every\`, \`each\`, \`both\`, \`no\`, \`none\`, \`neither\`, \`exactly N\`, \`at least N\`, \`fewer than N\`, \`more than N\` \u2014 a single counter-example observed in \`view_ui\` or \`screenshot\` constitutes a failed constraint; this rule applies only when the outcome text contains a universal or count-bound quantifier; positive-existence outcomes (\`X appears\`, \`Y becomes active\`, \`screen is visible\`) are not subject to counter-example scanning and are governed by SPEC_STEP_READING_SECTION; on a scrollable list, scroll through the full list before concluding pass or fail on a universal quantifier \u2014 apply SCROLL_FOLD_RULE to reveal all items, then evaluate across the complete visible set; if the screen shows any loading indicator at the time of observation, apply LOADING_STATE_RULE first and re-verify after resolution before emitting; do NOT soften (\`mostly\`, \`essentially\`, \`largely\`) and do NOT narrow the target class post-hoc to exclude the counter-example; when the counter-evidence is an element absent from the a11y tree, A11Y_FALLBACK_RULE takes precedence and determines the finding type (\`missing-a11y-element\`) \u2014 OUTCOME_LITERAL_RULE governs unwanted-presence violations only; if one item violates the constraint, emit \`spec-deviation\` immediately with: (a) the literal outcome text, (b) the specific element(s) that violate it, (c) the constraint keyword that is broken.`;
62966
+ var ANTI_RATIONALIZATION_RULE = `During outcome verification, monitor your own reasoning for reconciliation hypotheses: if you generate any reasoning that re-frames, redefines, or reinterprets the observed counter-example or target class in order to produce agreement with the spec outcome \u2014 regardless of phrasing \u2014 treat that reasoning as a deviation signal, not a resolution; stop, do NOT mark the step complete, and emit \`spec-deviation\` with: (a) the literal outcome text, (b) the specific observation that triggered the hypothesis, (c) the reconciliation reasoning itself verbatim; before marking any quantifier-bearing outcome complete, state explicitly in your reasoning: \`No reconciliation hypothesis generated. Counter-examples found: [list or none].\` If you cannot make that statement honestly, a hypothesis exists \u2014 emit \`spec-deviation\`; when outcome verification is ambiguous and no reconciliation hypothesis is generated, still default to emitting \`spec-deviation\`; silence is not a pass.`;
62954
62967
  var WHAT_TO_TEST_SECTION = `## What to Test
62955
62968
 
62956
62969
  Test navigation elements first, interactions second.
@@ -63043,6 +63056,8 @@ var SPEC_RULES_SECTION = `## Rules
63043
63056
  - ${CLIPPED_ELEMENT_RULE}
63044
63057
  - ${SCROLL_FOLD_RULE}
63045
63058
  - ${A11Y_FALLBACK_RULE}
63059
+ - ${OUTCOME_LITERAL_RULE}
63060
+ - ${ANTI_RATIONALIZATION_RULE}
63046
63061
  - Each item in \`**Assertions**\` is a mandatory pass/fail check \u2014 verify using \`view_ui\` when the assertion targets an element attribute, label, or presence in the tree; use \`screenshot\` when the assertion is purely visual; if neither can confirm, emit a \`spec-deviation\` finding based on what is observable
63047
63062
  - Flag crash dialogs, unexpected system errors, or navigation failures that occur as a direct result of executing a spec step; if you observe a visibly broken element in passing while navigating, note it without interacting with it`;
63048
63063
  function buildSpecModeBody({
@@ -76151,6 +76166,9 @@ var DEFAULT_FREESTYLE_TIMEOUT_SECONDS2 = 300;
76151
76166
  function buildDeviceInstruction(simulatorUdid) {
76152
76167
  return `You MUST use device "${simulatorUdid}" for ALL mobile tool calls. The simulator UDID is already configured \u2014 use it directly.`;
76153
76168
  }
76169
+ function composeAppContext(parts) {
76170
+ return parts.filter((part) => part !== void 0 && part.length > 0).join("\n\n");
76171
+ }
76154
76172
  function ensureWorkerCwd(simulatorUdid) {
76155
76173
  const workerDirectory = import_node_path21.default.join(import_node_os4.default.tmpdir(), "xqa-workers", simulatorUdid);
76156
76174
  (0, import_node_fs11.mkdirSync)(workerDirectory, { recursive: true });
@@ -76164,7 +76182,7 @@ function resolveFreestyleTimeout(item, config3) {
76164
76182
  }
76165
76183
  function buildFreestylePipelineConfig(input) {
76166
76184
  const { item, context, signal, simulatorUdid, onEvent } = input;
76167
- const { config: config3, xqaDirectory, runId, date: date5 } = context;
76185
+ const { config: config3, xqaDirectory, runId, date: date5, appContext } = context;
76168
76186
  return {
76169
76187
  outputDir: import_node_path21.default.join(xqaDirectory, "output", item.id),
76170
76188
  runId,
@@ -76177,9 +76195,11 @@ function buildFreestylePipelineConfig(input) {
76177
76195
  mcpServers: createDefaultMcpServers(simulatorUdid),
76178
76196
  allowedTools: ALLOWED_TOOLS,
76179
76197
  timeoutMs: resolveFreestyleTimeout(item, config3) * MS_PER_SECOND4,
76180
- appContext: item.prompt ? `${buildDeviceInstruction(simulatorUdid)}
76181
-
76182
- ${item.prompt}` : buildDeviceInstruction(simulatorUdid),
76198
+ appContext: composeAppContext([
76199
+ buildDeviceInstruction(simulatorUdid),
76200
+ appContext,
76201
+ item.prompt
76202
+ ]),
76183
76203
  buildEnv: config3.QA_BUILD_ENV,
76184
76204
  cwd: ensureWorkerCwd(simulatorUdid)
76185
76205
  }
@@ -76190,7 +76210,7 @@ function executeFreestyleItem(input) {
76190
76210
  }
76191
76211
  function buildSpecPipelineConfig(input) {
76192
76212
  const { item, context, signal, simulatorUdid, onEvent } = input;
76193
- const { config: config3, xqaDirectory, runId } = context;
76213
+ const { config: config3, xqaDirectory, runId, appContext } = context;
76194
76214
  return {
76195
76215
  outputDir: import_node_path21.default.join(xqaDirectory, "output", item.id),
76196
76216
  runId,
@@ -76202,7 +76222,7 @@ function buildSpecPipelineConfig(input) {
76202
76222
  specFiles: [item.specPath],
76203
76223
  mcpServers: createDefaultMcpServers(simulatorUdid),
76204
76224
  allowedTools: ALLOWED_TOOLS,
76205
- appContext: buildDeviceInstruction(simulatorUdid),
76225
+ appContext: composeAppContext([buildDeviceInstruction(simulatorUdid), appContext]),
76206
76226
  buildEnv: config3.QA_BUILD_ENV,
76207
76227
  cwd: ensureWorkerCwd(simulatorUdid)
76208
76228
  }
@@ -76278,17 +76298,30 @@ function deriveSuiteIdFromMode(mode) {
76278
76298
  }
76279
76299
  return deriveSuiteId({ mode: "spec", globs: mode.globs });
76280
76300
  }
76301
+ async function loadAppContext(xqaDirectory) {
76302
+ const result = await readAppContext(xqaDirectory);
76303
+ if (result.isErr()) {
76304
+ const cause = result.error.cause instanceof Error ? result.error.cause.message : JSON.stringify(result.error.cause);
76305
+ process.stderr.write(`Failed to read app context: ${result.error.type}
76306
+ ${cause}
76307
+ `);
76308
+ return void 0;
76309
+ }
76310
+ return result.value;
76311
+ }
76281
76312
  async function buildSuiteRunContext(input) {
76282
76313
  const suiteId = deriveSuiteIdFromMode(input.mode);
76283
76314
  const date5 = (/* @__PURE__ */ new Date()).toISOString().slice(0, ISO_DATE_LENGTH3);
76284
76315
  const outputDirectory = import_node_path22.default.join(input.xqaDirectory, "output");
76285
76316
  const existingDirectories = await listRunDirectories({ outputDirectory, suiteId, date: date5 });
76286
76317
  const runId = computeNextRunId(existingDirectories);
76318
+ const appContext = await loadAppContext(input.xqaDirectory);
76287
76319
  const context = {
76288
76320
  config: input.config,
76289
76321
  xqaDirectory: input.xqaDirectory,
76290
76322
  runId,
76291
- date: date5
76323
+ date: date5,
76324
+ appContext
76292
76325
  };
76293
76326
  return { suiteId, date: date5, outputDirectory, runId, context };
76294
76327
  }
@@ -76482,7 +76515,7 @@ function resolveXqaDirectory() {
76482
76515
  return result.value;
76483
76516
  }
76484
76517
  var program2 = new Command();
76485
- program2.name("xqa").description("AI-powered QA agent CLI").version(`${"1.14.0"}${false ? ` (dev build +${"7c0bf90"})` : ""}`);
76518
+ program2.name("xqa").description("AI-powered QA agent CLI").version(`${"1.14.1"}${false ? ` (dev build +${"18d343b"})` : ""}`);
76486
76519
  program2.command("init").description("Initialize a new xqa project in the current directory").action(() => {
76487
76520
  runInitCommand();
76488
76521
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/xqa",
3
- "version": "1.14.0",
3
+ "version": "1.14.1",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -27,10 +27,10 @@
27
27
  "vitest": "^3.2.1",
28
28
  "zod": "^3.0.0",
29
29
  "@qa-agents/explorer": "0.0.0",
30
- "@qa-agents/eslint-config": "0.0.0",
31
- "@qa-agents/display": "0.0.0",
32
30
  "@qa-agents/mobile-ios": "0.0.0",
33
31
  "@qa-agents/pipeline": "0.0.0",
32
+ "@qa-agents/display": "0.0.0",
33
+ "@qa-agents/eslint-config": "0.0.0",
34
34
  "@qa-agents/shared": "0.0.0",
35
35
  "@qa-agents/typescript-config": "0.0.0"
36
36
  },