@exodus/xqa 1.14.1 → 1.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +85 -16
  2. package/dist/xqa.cjs +75 -67
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -14,12 +14,20 @@ The tool manages configuration, project initialization, session state tracking,
14
14
 
15
15
  Initialize a new xqa project in the current directory.
16
16
 
17
- Creates a `.xqa/` directory with templates and subdirectories for specs, designs, and suites. Installs the `xqa-spec` skill for creating test specs.
17
+ Creates a `.xqa/` directory with `app.md` and `explore.md` templates plus subdirectories for specs, designs, and suites. Installs bundled xqa skills.
18
18
 
19
19
  ```bash
20
20
  xqa init
21
21
  ```
22
22
 
23
+ ### update
24
+
25
+ Update installed xqa skills to the current CLI version.
26
+
27
+ ```bash
28
+ xqa update
29
+ ```
30
+
23
31
  ### explore [prompt]
24
32
 
25
33
  Run the explorer agent; omit prompt for a full breadth-first sweep.
@@ -31,44 +39,73 @@ xqa explore # breadth-first exploration
31
39
  xqa explore "test the login flow" # focused exploration
32
40
  xqa explore -v prompt,screen # verbose output for categories
33
41
  xqa explore -v # verbose output for all categories
42
+ xqa explore -t 600 # override explorer timeout (seconds)
43
+ xqa explore --debug # log timing and event details to stderr
34
44
  ```
35
45
 
36
- Flag: `-v, --verbose [categories]` — Log categories (prompt, tools, screen, memory). Default: all if flag is present without value.
46
+ Flags:
47
+
48
+ - `-v, --verbose [categories]` — Log categories (prompt, tools, screen, memory). Default: all if flag is present without value.
49
+ - `-t, --timeout <seconds>` — Explorer timeout in seconds (overrides `QA_EXPLORE_TIMEOUT_SECONDS`).
50
+ - `--debug` — Log timing and event details to stderr.
37
51
 
38
52
  ### spec [spec-file]
39
53
 
40
54
  Run the explorer agent against a spec file.
41
55
 
42
- Loads a spec markdown file from `.xqa/specs/` (or an absolute path) and executes the agent against it. Spec files define entry points, steps, and optional timeouts. Omit the argument to pick from available specs interactively.
56
+ Loads a spec markdown file from `.xqa/specs/` (or an absolute path) and executes the agent against it. Omit the argument to pick from available specs interactively.
43
57
 
44
58
  ```bash
45
59
  xqa spec # interactive spec picker
46
- xqa spec .xqa/specs/authentication.test.md # explicit spec file
60
+ xqa spec .xqa/specs/authentication.test.md # explicit spec file
47
61
  xqa spec -v tools,memory # verbose output
62
+ xqa spec --debug # debug logging
48
63
  ```
49
64
 
50
- Flag: `-v, --verbose [categories]` — Same as explore.
65
+ Flags:
66
+
67
+ - `-v, --verbose [categories]` — Same as explore.
68
+ - `--debug` — Log timing and event details to stderr.
51
69
 
52
70
  Spec file format (YAML frontmatter + markdown):
53
71
 
54
72
  ```markdown
55
73
  ---
56
74
  feature: 'Feature Name'
57
- entry: 'Screen name or navigation path'
58
75
  timeout: 300
59
76
  ---
60
77
 
61
78
  # Spec content
62
79
  ```
63
80
 
81
+ Frontmatter fields: `feature` (required), `timeout` (optional, seconds).
82
+
83
+ ### run
84
+
85
+ Run a test suite or a set of spec files in parallel across booted simulators.
86
+
87
+ Exactly one of `--suite` or `--spec` is required.
88
+
89
+ ```bash
90
+ xqa run --suite smoke # run .xqa/suites/smoke.suite.json
91
+ xqa run --spec 'specs/**/*.test.md' # run matching spec files
92
+ xqa run --suite smoke --debug # debug logging
93
+ ```
94
+
95
+ Flags:
96
+
97
+ - `--suite <name>` — Name of the suite (`<name>.suite.json`) under `.xqa/suites/`.
98
+ - `--spec <globs...>` — Glob patterns matching spec files, resolved from the xqa directory.
99
+ - `--debug` — Log timing and event details to stderr.
100
+
64
101
  ### review [findings-path]
65
102
 
66
103
  Review findings and mark false positives.
67
104
 
68
- Interactive session for triaging findings generated by explore or spec runs. Displays findings with confidence scores, steps, and screenshots. Mark findings as false positives (with optional reason) or undo previous dismissals. Saves dismissals to `.xqa/dismissals.json`. Defaults to the last findings path if omitted.
105
+ Interactive session for triaging findings generated by explore or spec runs. Mark findings as dismissed (with optional reason) or undo previous dismissals. Dismissals are written to `dismissals.json` next to the `.xqa` directory (override with `QA_DISMISSALS_PATH`). Defaults to the last findings path if omitted.
69
106
 
70
107
  ```bash
71
- xqa review # use last findings file
108
+ xqa review # use last findings file
72
109
  xqa review .xqa/output/findings-abc123.json # explicit path
73
110
  ```
74
111
 
@@ -93,26 +130,35 @@ xqa completion bash # generate bash completions
93
130
  xqa completion zsh # generate zsh completions
94
131
  ```
95
132
 
96
- ### Suite hooks
133
+ ### Suite config
97
134
 
98
- Suite config files may declare a `beforeEach` hook that runs before every work item on every simulator. Use this for project-owned setup (wallet provisioning, cache warming, login seeding).
135
+ Suite files live at `.xqa/suites/<name>.suite.json` and declare the work items plus optional hooks.
99
136
 
100
137
  ```json
101
138
  {
102
139
  "specs": ["specs/send.test.md"],
140
+ "freestyle": [{ "prompt": "explore settings", "timeoutSeconds": 300 }],
103
141
  "hooks": {
104
142
  "beforeEach": {
105
143
  "script": "qa/prepare-sim.mjs",
106
- "env": { "APP_PROFILE": "funded" }
144
+ "env": { "APP_PROFILE": "funded" },
145
+ "timeoutSeconds": 120
107
146
  }
108
147
  }
109
148
  }
110
149
  ```
111
150
 
151
+ Fields:
152
+
153
+ - `specs` (optional) — glob patterns resolved from the xqa directory.
154
+ - `freestyle` (optional) — either a positive integer (N empty entries) or an array of `{ prompt?, timeoutSeconds }` entries.
155
+ - At least one of `specs` or `freestyle` must resolve to a work item.
156
+ - `hooks.beforeEach` (optional) — runs before every work item on every simulator. Use for project-owned setup (wallet provisioning, cache warming, login seeding).
157
+
112
158
  The hook script is invoked as a Node child process. It receives:
113
159
 
114
160
  - Inherited `process.env`
115
- - Suite-declared `env` (merged below reserved keys)
161
+ - Suite-declared `env` overlaid with reserved keys (reserved wins)
116
162
  - Reserved xqa-owned keys: `XQA_SIM_UDID`, `XQA_ITEM_ID`, `XQA_ITEM_TYPE`, `XQA_ITEM_NAME`, `XQA_SUITE`, and (when item type is `spec`) `XQA_SPEC_PATH`
117
163
 
118
164
  Suite-declared `env` cannot override reserved keys — the parser rejects such configs.
@@ -121,7 +167,7 @@ Contract:
121
167
 
122
168
  - Exit 0 → proceed with item.
123
169
  - Non-zero exit → item marked failed, `executeItem` skipped, counts toward simulator-unhealthy threshold.
124
- - 120s timeout (not configurable in v1).
170
+ - Default 120s timeout, overridable via `hooks.beforeEach.timeoutSeconds`.
125
171
  - Honors the suite abort signal.
126
172
 
127
173
  ## Configuration
@@ -133,15 +179,17 @@ Configuration is loaded from environment variables and `.env.local`:
133
179
  - `QA_RUN_ID` (optional) — Custom run identifier; defaults to auto-generated
134
180
  - `QA_EXPLORE_TIMEOUT_SECONDS` (optional) — Exploration timeout in seconds
135
181
  - `QA_BUILD_ENV` (optional) — Build environment: `dev` or `prod` (default: prod)
182
+ - `QA_DISMISSALS_PATH` (optional) — Override the dismissals file path used by `xqa review`
136
183
 
137
184
  ## Architecture
138
185
 
139
186
  Key files and directories:
140
187
 
141
188
  - `src/index.ts` — CLI entry point; wires commander commands and manages graceful shutdown via process locks
142
- - `src/commands/` — Command implementations (init, explore, spec, review, analyse, completion)
143
- - `src/core/` — Pure functions: spec parsing, completion generation, verbose option parsing, last-path tracking
144
- - `src/shell/` — I/O wrappers: file reading, device discovery, app context loading
189
+ - `src/commands/` — Command implementations (init, update, explore, spec, review, analyse, completion)
190
+ - `src/suite/` — Suite runner: config parsing, work-item building, worker pool, hooks
191
+ - `src/core/` — Pure functions: completion generation, verbose/timeout option parsing, last-path tracking
192
+ - `src/shell/` — I/O wrappers: app/explore context reading, debug logging, display factory, preflight, xqa directory discovery
145
193
  - `src/config.ts`, `src/config-schema.ts` — Configuration loading and validation with Zod
146
194
  - `src/review-session.ts` — Interactive finding review loop with dismissal tracking
147
195
  - `src/spec-frontmatter.ts` — Spec markdown frontmatter parsing (YAML)
@@ -157,6 +205,8 @@ Core error discriminated unions:
157
205
  - `XqaDirectoryError` — No .xqa directory found (XQA_NOT_INITIALIZED)
158
206
  - `SpecFrontmatterError` — Malformed spec markdown (MISSING_FRONTMATTER, MISSING_FIELD, PARSE_ERROR)
159
207
  - `LastPathError` — No findings path provided and no prior session (NO_ARG_AND_NO_STATE)
208
+ - `SuiteConfigError` — Suite config JSON malformed or schema-invalid (INVALID_SUITE_CONFIG)
209
+ - `HookError` — Suite hook failure (HOOK_SPAWN_FAILED, HOOK_EXIT_NONZERO, HOOK_TIMEOUT, HOOK_ABORTED)
160
210
 
161
211
  ## Development
162
212
 
@@ -231,20 +281,39 @@ src/
231
281
 
232
282
  commands/
233
283
  init-command.ts # Project initialization
284
+ update-command.ts # Skill updates
285
+ install-skills.ts # Bundled skill installer
234
286
  explore-command.ts # Breadth-first exploration
235
287
  spec-command.ts # Spec-based exploration
288
+ spec-resolver.ts # Spec file discovery and parsing
236
289
  review-command.ts # Finding triage workflow
237
290
  analyse-command.ts # Video analysis
238
291
  completion-command.ts # Shell completion generation
292
+ item-events.ts # Start/complete/fail event emitters
239
293
 
240
294
  core/
241
295
  parse-verbose.ts # Verbose flag parsing
296
+ parse-timeout-seconds.ts # Timeout flag parsing
242
297
  completion-generator.ts # Bash/zsh completion script generation
243
298
  last-path.ts # Last findings path tracking
244
299
 
245
300
  shell/
246
301
  app-context.ts # Read app.md and explore.md
247
302
  xqa-directory.ts # Locate .xqa directory
303
+ preflight.ts # Environment preflight checks
304
+ ffmpeg-check.ts # ffmpeg availability probe
305
+ display-factory.ts # Solo and suite display factories
306
+ debug-logger.ts # Debug event logging
307
+ debug-agent-events.ts # Agent event debug formatter
308
+ debug-suite-events.ts # Suite event debug formatter
309
+ debug-logger-core.ts # Pure logging helpers
310
+ trigger-abort.ts # Abort signal plumbing
311
+
312
+ suite/
313
+ types.ts # Suite work-item and findings types
314
+ core/ # Pure: config parser, item builders, hook env, queue
315
+ shell/ # I/O: worker pool, hook runner, findings writer
316
+ commands/ # run-command, execute-item, suite-run-context
248
317
 
249
318
  __tests__/
250
319
  *.test.ts # Test files co-located with src/
package/dist/xqa.cjs CHANGED
@@ -52351,27 +52351,13 @@ var DEFAULT_LONG_PRESS_DURATION_MS = 500;
52351
52351
  var MS_PER_SECOND = 1e3;
52352
52352
  var DEFAULT_SWIPE_DURATION_SECONDS = 0.1;
52353
52353
  var ENTER_KEY_CODE = "0x28";
52354
- var TAP_SCHEMA = { x: external_exports.number(), y: external_exports.number() };
52355
- var LONG_PRESS_SCHEMA = { x: external_exports.number(), y: external_exports.number(), duration: external_exports.number().optional() };
52356
- var SWIPE_SCHEMA = {
52357
- x_start: external_exports.number(),
52358
- y_start: external_exports.number(),
52359
- x_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
- )
52364
- };
52365
- var TYPE_TEXT_SCHEMA = { text: external_exports.string(), submit: external_exports.boolean().optional() };
52366
- var PRESS_BUTTON_SCHEMA = {
52367
- button: external_exports.enum(["HOME", "LOCK", "SIRI", "SIDE_BUTTON", "APPLE_PAY"])
52368
- };
52369
52354
  function errorResult2(error48) {
52370
52355
  return {
52371
52356
  content: [{ type: "text", text: `Error: ${String(error48.cause)}` }],
52372
52357
  isError: true
52373
52358
  };
52374
52359
  }
52360
+ var TAP_SCHEMA = { x: external_exports.number(), y: external_exports.number() };
52375
52361
  function buildTapArguments(resolvedUdid, input) {
52376
52362
  return ["ui", "tap", "--udid", resolvedUdid, String(input.x), String(input.y)];
52377
52363
  }
@@ -52406,6 +52392,23 @@ async function handleDoubleTap(input) {
52406
52392
  ]
52407
52393
  };
52408
52394
  }
52395
+ function createTapTool(udid = "booted") {
52396
+ return _x(
52397
+ "tap",
52398
+ "Tap on the screen at the given coordinates.",
52399
+ TAP_SCHEMA,
52400
+ async ({ x, y: y6 }) => handleTap({ udid, x, y: y6 })
52401
+ );
52402
+ }
52403
+ function createDoubleTapTool(udid = "booted") {
52404
+ return _x(
52405
+ "double_tap",
52406
+ "Double-tap on the screen at the given coordinates.",
52407
+ TAP_SCHEMA,
52408
+ async ({ x, y: y6 }) => handleDoubleTap({ udid, x, y: y6 })
52409
+ );
52410
+ }
52411
+ var LONG_PRESS_SCHEMA = { x: external_exports.number(), y: external_exports.number(), duration: external_exports.number().optional() };
52409
52412
  function buildLongPressArguments(resolvedUdid, input) {
52410
52413
  return [
52411
52414
  "ui",
@@ -52434,8 +52437,39 @@ async function handleLongPress(input) {
52434
52437
  ]
52435
52438
  };
52436
52439
  }
52440
+ function createLongPressTool(udid = "booted") {
52441
+ return _x(
52442
+ "long_press",
52443
+ "Long-press on the screen at the given coordinates.",
52444
+ LONG_PRESS_SCHEMA,
52445
+ async ({ x, y: y6, duration: duration3 }) => handleLongPress({ udid, x, y: y6, duration: duration3 ?? DEFAULT_LONG_PRESS_DURATION_MS })
52446
+ );
52447
+ }
52448
+ var SWIPE_SCHEMA = {
52449
+ x_start: external_exports.number(),
52450
+ y_start: external_exports.number(),
52451
+ x_end: external_exports.number(),
52452
+ y_end: external_exports.number(),
52453
+ duration: external_exports.number().positive().optional().describe(
52454
+ "Seconds for the full gesture. Velocity = distance / duration \u2014 raise duration at fixed distance to slow the gesture and reduce momentum. Default 0.1s (flick) works for short lists; raise to 0.4\u20130.8 for controlled scrolling on long lists where the flick overshoots."
52455
+ ),
52456
+ delta: external_exports.number().positive().optional().describe(
52457
+ "Pixel distance between interpolated touch points along the swipe path. Smaller values (e.g. 5) produce a denser event stream \u2014 smoother motion and more controllable stop-velocity, recommended when combining with a raised duration to tame long-list overshoot. Larger values produce coarser strokes. Omit to use idb defaults."
52458
+ )
52459
+ };
52460
+ function toSwipeInput(udid, params) {
52461
+ return {
52462
+ udid,
52463
+ xStart: params.x_start,
52464
+ yStart: params.y_start,
52465
+ xEnd: params.x_end,
52466
+ yEnd: params.y_end,
52467
+ duration: params.duration ?? DEFAULT_SWIPE_DURATION_SECONDS,
52468
+ delta: params.delta
52469
+ };
52470
+ }
52437
52471
  function buildSwipeArguments(resolvedUdid, input) {
52438
- return [
52472
+ const arguments_ = [
52439
52473
  "ui",
52440
52474
  "swipe",
52441
52475
  "--udid",
@@ -52447,6 +52481,10 @@ function buildSwipeArguments(resolvedUdid, input) {
52447
52481
  "--duration",
52448
52482
  String(input.duration)
52449
52483
  ];
52484
+ if (input.delta !== void 0) {
52485
+ arguments_.push("--delta", String(input.delta));
52486
+ }
52487
+ return arguments_;
52450
52488
  }
52451
52489
  async function handleSwipe(input) {
52452
52490
  const result = await resolveUdid(input.udid).andThen(
@@ -52464,6 +52502,15 @@ async function handleSwipe(input) {
52464
52502
  ]
52465
52503
  };
52466
52504
  }
52505
+ function createSwipeTool(udid = "booted") {
52506
+ return _x(
52507
+ "swipe",
52508
+ "Swipe on the screen from one point to another. Default duration 0.1s produces a flick \u2014 use for scrolling lists, dismissing sheets, triggering paging. Use duration 0.5+ for slow drag (reorder, pan). For long lists where default flick overshoots: shorten swipe distance AND raise duration to 0.4\u20130.8 to lower velocity; optionally lower delta for denser touch events and a more controllable stop. Omitting duration uses the flick default.",
52509
+ SWIPE_SCHEMA,
52510
+ async (params) => handleSwipe(toSwipeInput(udid, params))
52511
+ );
52512
+ }
52513
+ var TYPE_TEXT_SCHEMA = { text: external_exports.string(), submit: external_exports.boolean().optional() };
52467
52514
  async function handleTypeText({ udid, text, submit }) {
52468
52515
  const result = await resolveUdid(udid).andThen((resolvedUdid) => {
52469
52516
  const typeResult = runCommand("idb", ["ui", "text", "--udid", resolvedUdid, text]);
@@ -52479,6 +52526,17 @@ async function handleTypeText({ udid, text, submit }) {
52479
52526
  }
52480
52527
  return { content: [{ type: "text", text: `Typed: ${text}` }] };
52481
52528
  }
52529
+ function createTypeTextTool(udid = "booted") {
52530
+ return _x(
52531
+ "type_text",
52532
+ "Type text into the currently focused element.",
52533
+ TYPE_TEXT_SCHEMA,
52534
+ async ({ text, submit }) => handleTypeText({ udid, text, submit: submit ?? false })
52535
+ );
52536
+ }
52537
+ var PRESS_BUTTON_SCHEMA = {
52538
+ button: external_exports.enum(["HOME", "LOCK", "SIRI", "SIDE_BUTTON", "APPLE_PAY"])
52539
+ };
52482
52540
  async function handlePressButton(udid, button) {
52483
52541
  const result = await resolveUdid(udid).andThen(
52484
52542
  (resolvedUdid) => runCommand("idb", ["ui", "button", "--udid", resolvedUdid, button])
@@ -52488,56 +52546,6 @@ async function handlePressButton(udid, button) {
52488
52546
  }
52489
52547
  return { content: [{ type: "text", text: `Pressed button: ${button}` }] };
52490
52548
  }
52491
- function createTapTool(udid = "booted") {
52492
- return _x(
52493
- "tap",
52494
- "Tap on the screen at the given coordinates.",
52495
- TAP_SCHEMA,
52496
- async ({ x, y: y6 }) => handleTap({ udid, x, y: y6 })
52497
- );
52498
- }
52499
- function createDoubleTapTool(udid = "booted") {
52500
- return _x(
52501
- "double_tap",
52502
- "Double-tap on the screen at the given coordinates.",
52503
- TAP_SCHEMA,
52504
- async ({ x, y: y6 }) => handleDoubleTap({ udid, x, y: y6 })
52505
- );
52506
- }
52507
- function createLongPressTool(udid = "booted") {
52508
- return _x(
52509
- "long_press",
52510
- "Long-press on the screen at the given coordinates.",
52511
- LONG_PRESS_SCHEMA,
52512
- async ({ x, y: y6, duration: duration3 }) => handleLongPress({ udid, x, y: y6, duration: duration3 ?? DEFAULT_LONG_PRESS_DURATION_MS })
52513
- );
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
- }
52525
- function createSwipeTool(udid = "booted") {
52526
- return _x(
52527
- "swipe",
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.",
52529
- SWIPE_SCHEMA,
52530
- async (params) => handleSwipe(toSwipeInput(udid, params))
52531
- );
52532
- }
52533
- function createTypeTextTool(udid = "booted") {
52534
- return _x(
52535
- "type_text",
52536
- "Type text into the currently focused element.",
52537
- TYPE_TEXT_SCHEMA,
52538
- async ({ text, submit }) => handleTypeText({ udid, text, submit: submit ?? false })
52539
- );
52540
- }
52541
52549
  function createPressButtonTool(udid = "booted") {
52542
52550
  return _x(
52543
52551
  "press_button",
@@ -76515,7 +76523,7 @@ function resolveXqaDirectory() {
76515
76523
  return result.value;
76516
76524
  }
76517
76525
  var program2 = new Command();
76518
- program2.name("xqa").description("AI-powered QA agent CLI").version(`${"1.14.1"}${false ? ` (dev build +${"18d343b"})` : ""}`);
76526
+ program2.name("xqa").description("AI-powered QA agent CLI").version(`${"1.14.2"}${false ? ` (dev build +${"5d87cb5"})` : ""}`);
76519
76527
  program2.command("init").description("Initialize a new xqa project in the current directory").action(() => {
76520
76528
  runInitCommand();
76521
76529
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/xqa",
3
- "version": "1.14.1",
3
+ "version": "1.14.2",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -26,11 +26,11 @@
26
26
  "typescript": "^5.8.3",
27
27
  "vitest": "^3.2.1",
28
28
  "zod": "^3.0.0",
29
+ "@qa-agents/display": "0.0.0",
30
+ "@qa-agents/eslint-config": "0.0.0",
29
31
  "@qa-agents/explorer": "0.0.0",
30
32
  "@qa-agents/mobile-ios": "0.0.0",
31
33
  "@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
  },