@exodus/xqa 1.14.1 → 1.15.0

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 +84 -16
  2. package/dist/xqa.cjs +162 -237
  3. package/package.json +4 -4
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,38 @@ 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
+ display-factory.ts # Solo and suite display factories
305
+ debug-logger.ts # Debug event logging
306
+ debug-agent-events.ts # Agent event debug formatter
307
+ debug-suite-events.ts # Suite event debug formatter
308
+ debug-logger-core.ts # Pure logging helpers
309
+ trigger-abort.ts # Abort signal plumbing
310
+
311
+ suite/
312
+ types.ts # Suite work-item and findings types
313
+ core/ # Pure: config parser, item builders, hook env, queue
314
+ shell/ # I/O: worker pool, hook runner, findings writer
315
+ commands/ # run-command, execute-item, suite-run-context
248
316
 
249
317
  __tests__/
250
318
  *.test.ts # Test files co-located with src/
package/dist/xqa.cjs CHANGED
@@ -3733,7 +3733,7 @@ var require_index_cjs = __commonJS({
3733
3733
  }
3734
3734
  var fromPromise = ResultAsync14.fromPromise;
3735
3735
  var fromSafePromise2 = ResultAsync14.fromSafePromise;
3736
- var fromAsyncThrowable10 = ResultAsync14.fromThrowable;
3736
+ var fromAsyncThrowable9 = ResultAsync14.fromThrowable;
3737
3737
  var combineResultList = (resultList) => {
3738
3738
  let acc = ok21([]);
3739
3739
  for (const result of resultList) {
@@ -3950,7 +3950,7 @@ var require_index_cjs = __commonJS({
3950
3950
  exports2.ResultAsync = ResultAsync14;
3951
3951
  exports2.err = err20;
3952
3952
  exports2.errAsync = errAsync8;
3953
- exports2.fromAsyncThrowable = fromAsyncThrowable10;
3953
+ exports2.fromAsyncThrowable = fromAsyncThrowable9;
3954
3954
  exports2.fromPromise = fromPromise;
3955
3955
  exports2.fromSafePromise = fromSafePromise2;
3956
3956
  exports2.fromThrowable = fromThrowable15;
@@ -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",
@@ -52855,8 +52863,6 @@ function resolveRunPaths({ outputDirectory, runId, date: date5 }) {
52855
52863
  return (0, import_neverthrow8.ok)({
52856
52864
  baseDir: baseDirectory,
52857
52865
  videoPath: import_node_path2.default.join(baseDirectory, "recording.mp4"),
52858
- videoPath2x: import_node_path2.default.join(baseDirectory, "recording_2x.mp4"),
52859
- videoPath4x: import_node_path2.default.join(baseDirectory, "recording_4x.mp4"),
52860
52866
  findingsPath: import_node_path2.default.join(baseDirectory, "findings.json"),
52861
52867
  screenshotsDir: import_node_path2.default.join(baseDirectory, "screenshots")
52862
52868
  });
@@ -56115,24 +56121,13 @@ function makePollTick(onEvent) {
56115
56121
  });
56116
56122
  };
56117
56123
  }
56118
- function resolveVideoPath(artifacts, variant) {
56119
- if (variant === "2x") {
56120
- return artifacts.videoPath2x;
56121
- }
56122
- if (variant === "4x") {
56123
- return artifacts.videoPath4x;
56124
- }
56125
- return artifacts.videoPath;
56126
- }
56127
56124
  function buildRunContext(artifacts, config3) {
56128
- const variant = config3?.videoVariant ?? "4x";
56129
56125
  return {
56130
56126
  onEvent: config3?.onEvent === void 0 ? void 0 : safeHandler(config3.onEvent),
56131
56127
  apiKey: config3?.apiKey ?? "",
56132
56128
  model: config3?.model ?? DEFAULT_MODEL,
56133
- variant,
56134
56129
  start: Date.now(),
56135
- videoPath: resolveVideoPath(artifacts, variant),
56130
+ videoPath: artifacts.videoPath,
56136
56131
  prompt: buildAnalyserPrompt(artifacts.snapshots)
56137
56132
  };
56138
56133
  }
@@ -56162,11 +56157,11 @@ function runUploadPipeline(context) {
56162
56157
  }
56163
56158
  function runAnalyser(artifacts, config3) {
56164
56159
  const context = buildRunContext(artifacts, config3);
56165
- const { onEvent, variant, start, videoPath } = context;
56160
+ const { onEvent, start, videoPath } = context;
56166
56161
  onEvent?.({ type: "STAGE_START", agent: "analyser" });
56167
56162
  if (!videoPath) {
56168
56163
  emitStageEnd(onEvent, start);
56169
- return (0, import_neverthrow10.errAsync)({ type: "VIDEO_PATH_MISSING", variant });
56164
+ return (0, import_neverthrow10.errAsync)({ type: "VIDEO_PATH_MISSING" });
56170
56165
  }
56171
56166
  return runUploadPipeline(context);
56172
56167
  }
@@ -62790,17 +62785,6 @@ function runQuery(prompt, config3) {
62790
62785
  function collectAgentOutput(prompt, config3) {
62791
62786
  return runQuery(prompt, config3).mapErr((cause) => ({ type: "QUERY_FAILED", cause }));
62792
62787
  }
62793
- async function runFfmpeg(arguments_) {
62794
- const { promise: promise2, resolve, reject } = Promise.withResolvers();
62795
- (0, import_node_child_process4.execFile)("ffmpeg", arguments_, (error48) => {
62796
- if (error48) {
62797
- reject(error48);
62798
- } else {
62799
- resolve(true);
62800
- }
62801
- });
62802
- await promise2;
62803
- }
62804
62788
  function spawnRecorder(outputPath) {
62805
62789
  const safeMkdirSync = (0, import_neverthrow20.fromThrowable)(import_node_fs.mkdirSync, (cause) => cause);
62806
62790
  const safeSpawn2 = (0, import_neverthrow20.fromThrowable)(
@@ -62874,35 +62858,6 @@ function stopRecording(handle) {
62874
62858
  (cause) => ({ type: "STOP_FAILED", cause })
62875
62859
  );
62876
62860
  }
62877
- function speedUpVideo({
62878
- inputPath,
62879
- factor,
62880
- outputPath
62881
- }) {
62882
- const pts = String(1 / factor);
62883
- return (0, import_neverthrow20.fromAsyncThrowable)(
62884
- runFfmpeg,
62885
- (cause) => ({ type: "PROCESS_FAILED", cause })
62886
- )(["-i", inputPath, "-filter:v", `setpts=${pts}*PTS`, "-an", "-y", outputPath]).map(
62887
- () => outputPath
62888
- );
62889
- }
62890
- var SPEED_2X = 2;
62891
- var SPEED_4X = 4;
62892
- function applySpeedUpVariants({
62893
- result,
62894
- params,
62895
- toRecordingError
62896
- }) {
62897
- const { findings, snapshots } = result;
62898
- const { videoPath, videoPath2x, videoPath4x, signal } = params;
62899
- if (signal?.aborted) {
62900
- return (0, import_neverthrow19.okAsync)({ findings, snapshots });
62901
- }
62902
- return speedUpVideo({ inputPath: videoPath, factor: SPEED_2X, outputPath: videoPath2x }).mapErr(toRecordingError).andThen(
62903
- () => speedUpVideo({ inputPath: videoPath, factor: SPEED_4X, outputPath: videoPath4x }).mapErr(toRecordingError).map(() => ({ findings, snapshots }))
62904
- );
62905
- }
62906
62861
  function runWithRecording(handle, collectOutput) {
62907
62862
  const toRecordingError = (cause) => ({
62908
62863
  type: "RECORDING_FAILED",
@@ -62931,7 +62886,7 @@ function startAndRun(params) {
62931
62886
  }).mapErr((error48) => {
62932
62887
  signal?.removeEventListener("abort", onAbort);
62933
62888
  return error48;
62934
- }).andThen((result) => applySpeedUpVariants({ result, params, toRecordingError }));
62889
+ });
62935
62890
  });
62936
62891
  }
62937
62892
  var TOOL_SELECTION_SECTION = `## Tool Selection
@@ -63405,15 +63360,13 @@ function toArtifacts(result, runPaths) {
63405
63360
  return {
63406
63361
  findings: result.findings,
63407
63362
  videoPath: runPaths.videoPath,
63408
- videoPath2x: runPaths.videoPath2x,
63409
- videoPath4x: runPaths.videoPath4x,
63410
63363
  snapshots: result.snapshots
63411
63364
  };
63412
63365
  }
63413
63366
  function emitStageEnd3(safeConfig, start) {
63414
63367
  safeConfig.onEvent?.({ type: "STAGE_END", agent: "explorer", durationMs: Date.now() - start });
63415
63368
  }
63416
- var EMPTY_RUN_PATHS = { videoPath: "", videoPath2x: "", videoPath4x: "" };
63369
+ var EMPTY_RUN_PATHS = { videoPath: "" };
63417
63370
  function collectWithoutRecording({
63418
63371
  safeConfig,
63419
63372
  prompt,
@@ -63438,8 +63391,6 @@ function collectAndFinalize({
63438
63391
  }
63439
63392
  return startAndRun({
63440
63393
  videoPath: runPaths.videoPath,
63441
- videoPath2x: runPaths.videoPath2x,
63442
- videoPath4x: runPaths.videoPath4x,
63443
63394
  signal: safeConfig.signal,
63444
63395
  collectOutput: () => collectAgentOutput(prompt, safeConfig)
63445
63396
  }).map((result) => {
@@ -67433,7 +67384,6 @@ var ALLOWED_TOOLS = [...MOBILE_IOS_TOOLS];
67433
67384
  var MS_PER_SECOND4 = 1e3;
67434
67385
  var DEFAULT_ABORT_EXIT_CODE = 130;
67435
67386
  var IDB_INSTALL_MESSAGE = "idb is not installed. Run:\n brew install idb-companion\n pipx install fb-idb\n";
67436
- var FFMPEG_INSTALL_MESSAGE = "ffmpeg is not installed. Run:\n brew install ffmpeg\n";
67437
67387
 
67438
67388
  // src/shell/debug-logger-core.ts
67439
67389
  var ELAPSED_PAD = 8;
@@ -67715,7 +67665,7 @@ var JSON_INDENT = 2;
67715
67665
  var ITEM_ID = "analyse";
67716
67666
  var ITEM_NAME = "analyse";
67717
67667
  function buildArtifacts(videoPath) {
67718
- return { videoPath, videoPath2x: "", videoPath4x: videoPath, findings: [], snapshots: [] };
67668
+ return { videoPath, findings: [], snapshots: [] };
67719
67669
  }
67720
67670
  async function checkVideoPathExists(videoPath) {
67721
67671
  const safeAccess = (0, import_neverthrow36.fromAsyncThrowable)(import_promises17.access, () => ({ type: "FILE_NOT_FOUND" }));
@@ -67762,7 +67712,6 @@ async function runAnalysisAndExit(input) {
67762
67712
  const state = { identity, startedAt };
67763
67713
  const result = await runAnalysis(input.artifacts, {
67764
67714
  apiKey: input.apiKey,
67765
- videoVariant: "1x",
67766
67715
  onEvent
67767
67716
  });
67768
67717
  result.match(
@@ -67783,7 +67732,7 @@ function ensureApiKey(config3) {
67783
67732
  }
67784
67733
  return apiKey;
67785
67734
  }
67786
- async function resolveVideoPath2(videoPath) {
67735
+ async function resolveVideoPath(videoPath) {
67787
67736
  if (videoPath === void 0) {
67788
67737
  process.stderr.write('A video path is required. Pass the path printed by "xqa explore".\n');
67789
67738
  process.exit(1);
@@ -67802,7 +67751,7 @@ async function runAnalyseCommand(input) {
67802
67751
  if (apiKey === void 0) {
67803
67752
  return;
67804
67753
  }
67805
- const videoPath = await resolveVideoPath2(input.videoPath);
67754
+ const videoPath = await resolveVideoPath(input.videoPath);
67806
67755
  if (videoPath === void 0) {
67807
67756
  return;
67808
67757
  }
@@ -68001,35 +67950,8 @@ function readExploreContext(xqaDirectory) {
68001
67950
  return readContextFile(xqaDirectory, "explore.md");
68002
67951
  }
68003
67952
 
68004
- // src/shell/ffmpeg-check.ts
68005
- var import_node_child_process5 = require("node:child_process");
68006
- var import_neverthrow40 = __toESM(require_index_cjs(), 1);
68007
- async function whichFfmpeg() {
68008
- const { promise: promise2, resolve, reject } = Promise.withResolvers();
68009
- (0, import_node_child_process5.execFile)("which", ["ffmpeg"], (error48, stdout) => {
68010
- if (error48) {
68011
- reject(error48);
68012
- } else {
68013
- resolve(stdout);
68014
- }
68015
- });
68016
- return promise2;
68017
- }
68018
- var safeWhichFfmpeg = (0, import_neverthrow40.fromAsyncThrowable)(
68019
- whichFfmpeg,
68020
- () => ({ type: "FFMPEG_NOT_FOUND" })
68021
- );
68022
- function checkFfmpegAvailable() {
68023
- return safeWhichFfmpeg();
68024
- }
68025
-
68026
67953
  // src/shell/preflight.ts
68027
67954
  async function runPreflightChecks() {
68028
- const ffmpegCheck = await checkFfmpegAvailable();
68029
- if (ffmpegCheck.isErr()) {
68030
- process.stderr.write(FFMPEG_INSTALL_MESSAGE);
68031
- return 1;
68032
- }
68033
67955
  const idbCheck = await checkIdbAvailable();
68034
67956
  if (idbCheck.isErr()) {
68035
67957
  process.stderr.write(IDB_INSTALL_MESSAGE);
@@ -68217,7 +68139,7 @@ var import_node_fs6 = require("node:fs");
68217
68139
  var import_node_path12 = __toESM(require("node:path"), 1);
68218
68140
 
68219
68141
  // src/commands/install-skills.ts
68220
- var import_node_child_process6 = require("node:child_process");
68142
+ var import_node_child_process5 = require("node:child_process");
68221
68143
  var import_node_fs5 = require("node:fs");
68222
68144
  var import_node_path11 = __toESM(require("node:path"), 1);
68223
68145
  var import_node_url = require("node:url");
@@ -68228,7 +68150,7 @@ function resolveSkillsRoot() {
68228
68150
  function installSkills() {
68229
68151
  const skillsRoot = resolveSkillsRoot();
68230
68152
  for (const skill of (0, import_node_fs5.readdirSync)(skillsRoot)) {
68231
- (0, import_node_child_process6.spawnSync)("npx", ["skills", "add", import_node_path11.default.join(skillsRoot, skill), "-y"], {
68153
+ (0, import_node_child_process5.spawnSync)("npx", ["skills", "add", import_node_path11.default.join(skillsRoot, skill), "-y"], {
68232
68154
  stdio: "inherit"
68233
68155
  });
68234
68156
  }
@@ -68306,7 +68228,7 @@ function runInitCommand() {
68306
68228
  // src/commands/review-command.ts
68307
68229
  var import_node_fs7 = require("node:fs");
68308
68230
  var import_node_path14 = __toESM(require("node:path"), 1);
68309
- var import_neverthrow42 = __toESM(require_index_cjs(), 1);
68231
+ var import_neverthrow41 = __toESM(require_index_cjs(), 1);
68310
68232
 
68311
68233
  // ../../node_modules/.pnpm/@inquirer+core@10.3.2_@types+node@22.19.15/node_modules/@inquirer/core/dist/esm/lib/key.js
68312
68234
  var isUpKey = (key, keybindings = []) => (
@@ -70711,7 +70633,7 @@ var esm_default11 = createPrompt((config3, done) => {
70711
70633
  });
70712
70634
 
70713
70635
  // src/review-session.ts
70714
- var import_neverthrow41 = __toESM(require_index_cjs(), 1);
70636
+ var import_neverthrow40 = __toESM(require_index_cjs(), 1);
70715
70637
  var CONFIDENCE_PERCENT = 100;
70716
70638
  var FLOW_COL_WIDTH = 35;
70717
70639
  var TRIGGER_COL_WIDTH = 16;
@@ -70864,15 +70786,15 @@ async function runInteractiveLoop(findings, existing) {
70864
70786
  }
70865
70787
  return { staged: state.staged, undoneKeys: state.undoneKeys };
70866
70788
  }
70867
- var safeRunInteractiveLoop = (0, import_neverthrow41.fromAsyncThrowable)(
70789
+ var safeRunInteractiveLoop = (0, import_neverthrow40.fromAsyncThrowable)(
70868
70790
  runInteractiveLoop,
70869
70791
  (error48) => error48 instanceof Error && error48.name === "ExitPromptError" ? "exit-prompt" : "unexpected"
70870
70792
  );
70871
70793
 
70872
70794
  // src/commands/review-command.ts
70873
- var safeReadFile3 = (0, import_neverthrow42.fromThrowable)((filePath) => (0, import_node_fs7.readFileSync)(filePath, "utf8"));
70874
- var safeParseJson3 = (0, import_neverthrow42.fromThrowable)(JSON.parse);
70875
- var safeWrite = (0, import_neverthrow42.fromThrowable)((filePath, content) => {
70795
+ var safeReadFile3 = (0, import_neverthrow41.fromThrowable)((filePath) => (0, import_node_fs7.readFileSync)(filePath, "utf8"));
70796
+ var safeParseJson3 = (0, import_neverthrow41.fromThrowable)(JSON.parse);
70797
+ var safeWrite = (0, import_neverthrow41.fromThrowable)((filePath, content) => {
70876
70798
  (0, import_node_fs7.writeFileSync)(filePath, content);
70877
70799
  });
70878
70800
  function readLastPath(xqaDirectory) {
@@ -70889,13 +70811,13 @@ function isPipelineOutput(data) {
70889
70811
  function readFindings(filePath) {
70890
70812
  const readResult = safeReadFile3(filePath);
70891
70813
  if (readResult.isErr()) {
70892
- return (0, import_neverthrow42.err)("not-found");
70814
+ return (0, import_neverthrow41.err)("not-found");
70893
70815
  }
70894
70816
  return safeParseJson3(readResult.value).mapErr(() => "invalid").andThen((data) => {
70895
70817
  if (!isPipelineOutput(data)) {
70896
- return (0, import_neverthrow42.err)("invalid");
70818
+ return (0, import_neverthrow41.err)("invalid");
70897
70819
  }
70898
- return (0, import_neverthrow42.ok)(data);
70820
+ return (0, import_neverthrow41.ok)(data);
70899
70821
  });
70900
70822
  }
70901
70823
  function loadExistingDismissals(filePath) {
@@ -70968,7 +70890,7 @@ function resolveAndReadFindings(findingsPath, xqaDirectory) {
70968
70890
  "No findings path provided and no last path found. Run: xqa review <findings-path>\n"
70969
70891
  );
70970
70892
  process.exit(1);
70971
- return (0, import_neverthrow42.err)();
70893
+ return (0, import_neverthrow41.err)();
70972
70894
  }
70973
70895
  const resolvedPath = resolvedPathResult.value;
70974
70896
  const findingsResult = readFindings(resolvedPath);
@@ -70981,9 +70903,9 @@ function resolveAndReadFindings(findingsPath, xqaDirectory) {
70981
70903
  `);
70982
70904
  }
70983
70905
  process.exit(1);
70984
- return (0, import_neverthrow42.err)();
70906
+ return (0, import_neverthrow41.err)();
70985
70907
  }
70986
- return (0, import_neverthrow42.ok)({ resolvedPath, output: findingsResult.value });
70908
+ return (0, import_neverthrow41.ok)({ resolvedPath, output: findingsResult.value });
70987
70909
  }
70988
70910
  async function runReviewLoop({
70989
70911
  findings,
@@ -71057,40 +70979,40 @@ function stripExtensions(filename) {
71057
70979
  // src/commands/spec-resolver.ts
71058
70980
  var import_node_fs8 = require("node:fs");
71059
70981
  var import_node_path16 = __toESM(require("node:path"), 1);
71060
- var import_neverthrow44 = __toESM(require_index_cjs(), 1);
70982
+ var import_neverthrow43 = __toESM(require_index_cjs(), 1);
71061
70983
 
71062
70984
  // src/spec-frontmatter.ts
71063
- var import_neverthrow43 = __toESM(require_index_cjs(), 1);
70985
+ var import_neverthrow42 = __toESM(require_index_cjs(), 1);
71064
70986
  var FRONTMATTER_OPEN_LEN = 4;
71065
70987
  var FRONTMATTER_MARKER_LEN = 3;
71066
70988
  function extractFrontmatterBlock(content) {
71067
70989
  const normalized = content.replaceAll("\r\n", "\n");
71068
70990
  if (!normalized.startsWith("---")) {
71069
- return (0, import_neverthrow43.err)({ type: "MISSING_FRONTMATTER" });
70991
+ return (0, import_neverthrow42.err)({ type: "MISSING_FRONTMATTER" });
71070
70992
  }
71071
70993
  const end = normalized.indexOf("\n---", FRONTMATTER_MARKER_LEN);
71072
70994
  if (end === -1) {
71073
- return (0, import_neverthrow43.err)({ type: "MISSING_FRONTMATTER" });
70995
+ return (0, import_neverthrow42.err)({ type: "MISSING_FRONTMATTER" });
71074
70996
  }
71075
- return (0, import_neverthrow43.ok)(normalized.slice(FRONTMATTER_OPEN_LEN, end));
70997
+ return (0, import_neverthrow42.ok)(normalized.slice(FRONTMATTER_OPEN_LEN, end));
71076
70998
  }
71077
70999
  function parseTimeout(fields) {
71078
71000
  const raw = fields.get("timeout");
71079
71001
  if (raw === void 0) {
71080
- return (0, import_neverthrow43.ok)(raw);
71002
+ return (0, import_neverthrow42.ok)(raw);
71081
71003
  }
71082
71004
  const parsed = Number(raw);
71083
71005
  if (Number.isNaN(parsed) || parsed <= 0) {
71084
- return (0, import_neverthrow43.err)({ type: "PARSE_ERROR", cause: `invalid timeout: ${raw}` });
71006
+ return (0, import_neverthrow42.err)({ type: "PARSE_ERROR", cause: `invalid timeout: ${raw}` });
71085
71007
  }
71086
- return (0, import_neverthrow43.ok)(parsed);
71008
+ return (0, import_neverthrow42.ok)(parsed);
71087
71009
  }
71088
71010
  function parseSpecFrontmatter(content) {
71089
71011
  return extractFrontmatterBlock(content).andThen((block) => {
71090
71012
  const fields = parseYamlFields(block);
71091
71013
  const feature = fields.get("feature");
71092
71014
  if (feature === void 0) {
71093
- return (0, import_neverthrow43.err)({ type: "MISSING_FIELD", field: "feature" });
71015
+ return (0, import_neverthrow42.err)({ type: "MISSING_FIELD", field: "feature" });
71094
71016
  }
71095
71017
  return parseTimeout(fields).map((timeout) => ({ feature, timeout }));
71096
71018
  });
@@ -71112,12 +71034,12 @@ function parseYamlFields(block) {
71112
71034
  }
71113
71035
 
71114
71036
  // src/commands/spec-resolver.ts
71115
- var safeReadFile4 = (0, import_neverthrow44.fromThrowable)((filePath) => (0, import_node_fs8.readFileSync)(filePath, "utf8"));
71116
- var safeReaddir = (0, import_neverthrow44.fromThrowable)(
71037
+ var safeReadFile4 = (0, import_neverthrow43.fromThrowable)((filePath) => (0, import_node_fs8.readFileSync)(filePath, "utf8"));
71038
+ var safeReaddir = (0, import_neverthrow43.fromThrowable)(
71117
71039
  (directory) => (0, import_node_fs8.readdirSync)(directory, { recursive: true, encoding: "utf8" })
71118
71040
  );
71119
71041
  var CANCEL = "xqa:cancel";
71120
- var safeSelect = import_neverthrow44.ResultAsync.fromThrowable(
71042
+ var safeSelect = import_neverthrow43.ResultAsync.fromThrowable(
71121
71043
  esm_default11,
71122
71044
  (error48) => error48 instanceof Error && error48.name === "ExitPromptError" ? "cancelled" : "failed"
71123
71045
  );
@@ -71307,7 +71229,7 @@ function runUpdateCommand() {
71307
71229
  var import_node_path18 = __toESM(require("node:path"), 1);
71308
71230
  var import_node_url2 = require("node:url");
71309
71231
  var import_dotenv = __toESM(require_main(), 1);
71310
- var import_neverthrow45 = __toESM(require_index_cjs(), 1);
71232
+ var import_neverthrow44 = __toESM(require_index_cjs(), 1);
71311
71233
 
71312
71234
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
71313
71235
  var external_exports2 = {};
@@ -75368,10 +75290,10 @@ function loadConfig() {
75368
75290
  const messages = result.error.issues.map(
75369
75291
  (issue2) => ` - ${issue2.path.join(".")}: ${issue2.message}`
75370
75292
  );
75371
- return (0, import_neverthrow45.err)({ type: "INVALID_CONFIG", message: `Configuration error:
75293
+ return (0, import_neverthrow44.err)({ type: "INVALID_CONFIG", message: `Configuration error:
75372
75294
  ${messages.join("\n")}` });
75373
75295
  }
75374
- return (0, import_neverthrow45.ok)(result.data);
75296
+ return (0, import_neverthrow44.ok)(result.data);
75375
75297
  }
75376
75298
 
75377
75299
  // src/core/parse-timeout-seconds.ts
@@ -75404,7 +75326,7 @@ function parseVerboseOption(value) {
75404
75326
 
75405
75327
  // src/pid-lock.ts
75406
75328
  var import_node_fs9 = require("node:fs");
75407
- var import_neverthrow46 = __toESM(require_index_cjs(), 1);
75329
+ var import_neverthrow45 = __toESM(require_index_cjs(), 1);
75408
75330
  var PID_FILE = "/tmp/xqa.pid";
75409
75331
  var SIGINT_EXIT_CODE = 130;
75410
75332
  var SIGTERM_EXIT_CODE = 143;
@@ -75413,7 +75335,7 @@ var HARD_TIMEOUT_MS = 1e4;
75413
75335
  var cleanup = () => {
75414
75336
  (0, import_node_fs9.rmSync)(PID_FILE, { force: true });
75415
75337
  };
75416
- var checkProcessRunning = (0, import_neverthrow46.fromThrowable)(
75338
+ var checkProcessRunning = (0, import_neverthrow45.fromThrowable)(
75417
75339
  (pid) => {
75418
75340
  process.kill(pid, 0);
75419
75341
  return true;
@@ -75479,17 +75401,17 @@ function acquireLock() {
75479
75401
  // src/shell/xqa-directory.ts
75480
75402
  var import_node_fs10 = require("node:fs");
75481
75403
  var import_node_path19 = __toESM(require("node:path"), 1);
75482
- var import_neverthrow47 = __toESM(require_index_cjs(), 1);
75404
+ var import_neverthrow46 = __toESM(require_index_cjs(), 1);
75483
75405
  function findXqaDirectory(startDirectory) {
75484
75406
  let current = startDirectory;
75485
75407
  for (; ; ) {
75486
75408
  const candidate = import_node_path19.default.join(current, ".xqa");
75487
75409
  if ((0, import_node_fs10.existsSync)(candidate)) {
75488
- return (0, import_neverthrow47.ok)(candidate);
75410
+ return (0, import_neverthrow46.ok)(candidate);
75489
75411
  }
75490
75412
  const parent = import_node_path19.default.dirname(current);
75491
75413
  if (parent === current) {
75492
- return (0, import_neverthrow47.err)({ type: "XQA_NOT_INITIALIZED" });
75414
+ return (0, import_neverthrow46.err)({ type: "XQA_NOT_INITIALIZED" });
75493
75415
  }
75494
75416
  current = parent;
75495
75417
  }
@@ -75499,11 +75421,11 @@ function findXqaDirectory(startDirectory) {
75499
75421
  var import_promises21 = __toESM(require("node:fs/promises"), 1);
75500
75422
  var import_node_path23 = __toESM(require("node:path"), 1);
75501
75423
  var import_fast_glob = __toESM(require_out4(), 1);
75502
- var import_neverthrow55 = __toESM(require_index_cjs(), 1);
75424
+ var import_neverthrow54 = __toESM(require_index_cjs(), 1);
75503
75425
 
75504
75426
  // src/suite/core/suite-config-parser.ts
75427
+ var import_neverthrow47 = __toESM(require_index_cjs(), 1);
75505
75428
  var import_neverthrow48 = __toESM(require_index_cjs(), 1);
75506
- var import_neverthrow49 = __toESM(require_index_cjs(), 1);
75507
75429
  var RESERVED_HOOK_ENV_KEYS = [
75508
75430
  "XQA_SIM_UDID",
75509
75431
  "XQA_ITEM_ID",
@@ -75555,7 +75477,7 @@ var suiteConfigSchema = external_exports2.object({
75555
75477
  }).refine((data) => data.specs.length > 0 || data.freestyle.length > 0, {
75556
75478
  message: "Suite must declare at least one spec or freestyle entry"
75557
75479
  });
75558
- var safeJsonParse4 = (0, import_neverthrow48.fromThrowable)(
75480
+ var safeJsonParse4 = (0, import_neverthrow47.fromThrowable)(
75559
75481
  JSON.parse,
75560
75482
  (cause) => ({
75561
75483
  type: "INVALID_SUITE_CONFIG",
@@ -75589,16 +75511,16 @@ function parseSuiteConfig(raw) {
75589
75511
  return safeJsonParse4(raw).andThen((data) => {
75590
75512
  const parsed = suiteConfigSchema.safeParse(data);
75591
75513
  if (!parsed.success) {
75592
- return (0, import_neverthrow49.err)({ type: "INVALID_SUITE_CONFIG", cause: parsed.error });
75514
+ return (0, import_neverthrow48.err)({ type: "INVALID_SUITE_CONFIG", cause: parsed.error });
75593
75515
  }
75594
75516
  const hooks = normalizeHooks(parsed.data.hooks);
75595
75517
  if (hooks === void 0) {
75596
- return (0, import_neverthrow49.ok)({
75518
+ return (0, import_neverthrow48.ok)({
75597
75519
  specs: parsed.data.specs,
75598
75520
  freestyle: parsed.data.freestyle
75599
75521
  });
75600
75522
  }
75601
- return (0, import_neverthrow49.ok)({
75523
+ return (0, import_neverthrow48.ok)({
75602
75524
  specs: parsed.data.specs,
75603
75525
  freestyle: parsed.data.freestyle,
75604
75526
  hooks
@@ -75657,7 +75579,7 @@ function buildFreestyleItems(entries) {
75657
75579
  // src/suite/shell/suite-findings-writer.ts
75658
75580
  var import_promises19 = __toESM(require("node:fs/promises"), 1);
75659
75581
  var import_node_path20 = __toESM(require("node:path"), 1);
75660
- var import_neverthrow50 = __toESM(require_index_cjs(), 1);
75582
+ var import_neverthrow49 = __toESM(require_index_cjs(), 1);
75661
75583
  var INDENT_SPACES = 2;
75662
75584
  function writeSuiteFindings(findings, options) {
75663
75585
  const directory = import_node_path20.default.join(
@@ -75669,7 +75591,7 @@ function writeSuiteFindings(findings, options) {
75669
75591
  );
75670
75592
  const finalPath = import_node_path20.default.join(directory, "findings.json");
75671
75593
  const temporaryPath = `${finalPath}.tmp`;
75672
- const safeWriteAtomically = import_neverthrow50.ResultAsync.fromThrowable(
75594
+ const safeWriteAtomically = import_neverthrow49.ResultAsync.fromThrowable(
75673
75595
  async () => {
75674
75596
  await import_promises19.default.mkdir(directory, { recursive: true });
75675
75597
  await import_promises19.default.writeFile(temporaryPath, JSON.stringify(findings, void 0, INDENT_SPACES));
@@ -75682,7 +75604,7 @@ function writeSuiteFindings(findings, options) {
75682
75604
  }
75683
75605
 
75684
75606
  // src/suite/shell/worker-pool.ts
75685
- var import_neverthrow53 = __toESM(require_index_cjs(), 1);
75607
+ var import_neverthrow52 = __toESM(require_index_cjs(), 1);
75686
75608
 
75687
75609
  // src/suite/core/priority-queue.ts
75688
75610
  var PriorityQueue = class {
@@ -75866,7 +75788,7 @@ function recordHookFailure(input) {
75866
75788
  }
75867
75789
 
75868
75790
  // src/suite/shell/hook-invoker.ts
75869
- var import_neverthrow52 = __toESM(require_index_cjs(), 1);
75791
+ var import_neverthrow51 = __toESM(require_index_cjs(), 1);
75870
75792
 
75871
75793
  // src/suite/core/hook-env-builder.ts
75872
75794
  function buildReservedKeys(input) {
@@ -75892,8 +75814,8 @@ function buildHookEnv(input) {
75892
75814
  }
75893
75815
 
75894
75816
  // src/suite/shell/hook-runner.ts
75895
- var import_node_child_process7 = require("node:child_process");
75896
- var import_neverthrow51 = __toESM(require_index_cjs(), 1);
75817
+ var import_node_child_process6 = require("node:child_process");
75818
+ var import_neverthrow50 = __toESM(require_index_cjs(), 1);
75897
75819
  var noop2 = () => void 0;
75898
75820
  function cleanup2(context) {
75899
75821
  clearTimeout(context.timeoutHandle);
@@ -75922,22 +75844,22 @@ function collectStderr(child, stderrReference) {
75922
75844
  function buildOnAbort(context) {
75923
75845
  return () => {
75924
75846
  context.child.kill("SIGTERM");
75925
- settle(context, (0, import_neverthrow51.err)({ type: "HOOK_ABORTED" }));
75847
+ settle(context, (0, import_neverthrow50.err)({ type: "HOOK_ABORTED" }));
75926
75848
  };
75927
75849
  }
75928
75850
  function attachChildListeners(context) {
75929
75851
  const { child, stderrReference } = context;
75930
75852
  child.on("error", (cause) => {
75931
- settle(context, (0, import_neverthrow51.err)({ type: "HOOK_SPAWN_FAILED", cause }));
75853
+ settle(context, (0, import_neverthrow50.err)({ type: "HOOK_SPAWN_FAILED", cause }));
75932
75854
  });
75933
75855
  child.on("exit", (code) => {
75934
75856
  if (code === 0) {
75935
- settle(context, (0, import_neverthrow51.ok)());
75857
+ settle(context, (0, import_neverthrow50.ok)());
75936
75858
  return;
75937
75859
  }
75938
75860
  settle(
75939
75861
  context,
75940
- (0, import_neverthrow51.err)({
75862
+ (0, import_neverthrow50.err)({
75941
75863
  type: "HOOK_EXIT_NONZERO",
75942
75864
  code: code ?? -1,
75943
75865
  stderr: stderrReference.value
@@ -75949,14 +75871,14 @@ function attachTimeout(context, timeoutMs) {
75949
75871
  clearTimeout(context.timeoutHandle);
75950
75872
  context.timeoutHandle = setTimeout(() => {
75951
75873
  context.child.kill("SIGTERM");
75952
- settle(context, (0, import_neverthrow51.err)({ type: "HOOK_TIMEOUT", timeoutMs }));
75874
+ settle(context, (0, import_neverthrow50.err)({ type: "HOOK_TIMEOUT", timeoutMs }));
75953
75875
  }, timeoutMs);
75954
75876
  }
75955
75877
  function buildContext2(options) {
75956
75878
  const { script, cwd, env: env2, baseEnv, nodeExecPath } = options;
75957
75879
  const deferred = makeDeferred();
75958
75880
  const stderrReference = { value: "" };
75959
- const child = (0, import_node_child_process7.spawn)(nodeExecPath, [script], {
75881
+ const child = (0, import_node_child_process6.spawn)(nodeExecPath, [script], {
75960
75882
  cwd,
75961
75883
  env: { ...baseEnv, ...env2 },
75962
75884
  stdio: ["ignore", "inherit", "pipe"]
@@ -75984,7 +75906,7 @@ async function spawnHook(options) {
75984
75906
  attachChildListeners(context);
75985
75907
  return context.deferred.promise;
75986
75908
  }
75987
- var safeSpawn = import_neverthrow51.ResultAsync.fromThrowable(
75909
+ var safeSpawn = import_neverthrow50.ResultAsync.fromThrowable(
75988
75910
  spawnHook,
75989
75911
  (cause) => ({ type: "HOOK_SPAWN_FAILED", cause })
75990
75912
  );
@@ -76012,7 +75934,7 @@ async function invokeHook(input) {
76012
75934
  async function maybeInvokeHook(input) {
76013
75935
  const { hook } = input;
76014
75936
  if (hook === void 0) {
76015
- return (0, import_neverthrow52.ok)();
75937
+ return (0, import_neverthrow51.ok)();
76016
75938
  }
76017
75939
  return invokeHook({ ...input, hook });
76018
75940
  }
@@ -76151,7 +76073,7 @@ function runWorkerPool(config3) {
76151
76073
  const queue = setupQueue(config3);
76152
76074
  const results = [];
76153
76075
  const suiteStartMs = Date.now();
76154
- const safeRun = import_neverthrow53.ResultAsync.fromThrowable(
76076
+ const safeRun = import_neverthrow52.ResultAsync.fromThrowable(
76155
76077
  async () => runAllWorkers({ config: config3, queue, results, suiteStartMs }),
76156
76078
  (cause) => ({ type: "WORKER_POOL_FAILED", cause })
76157
76079
  );
@@ -76180,29 +76102,31 @@ function resolveFreestyleTimeout(item, config3) {
76180
76102
  }
76181
76103
  return config3.QA_EXPLORE_TIMEOUT_SECONDS ?? DEFAULT_FREESTYLE_TIMEOUT_SECONDS2;
76182
76104
  }
76105
+ function buildFreestyleExplorerConfig(input) {
76106
+ const { item, context, simulatorUdid } = input;
76107
+ const { config: config3, date: date5, appContext } = context;
76108
+ return {
76109
+ mode: "freestyle",
76110
+ date: date5,
76111
+ mcpServers: createDefaultMcpServers(simulatorUdid),
76112
+ allowedTools: ALLOWED_TOOLS,
76113
+ timeoutMs: resolveFreestyleTimeout(item, config3) * MS_PER_SECOND4,
76114
+ appContext: composeAppContext([buildDeviceInstruction(simulatorUdid), appContext, item.prompt]),
76115
+ buildEnv: config3.QA_BUILD_ENV,
76116
+ cwd: ensureWorkerCwd(simulatorUdid),
76117
+ record: true
76118
+ };
76119
+ }
76183
76120
  function buildFreestylePipelineConfig(input) {
76184
76121
  const { item, context, signal, simulatorUdid, onEvent } = input;
76185
- const { config: config3, xqaDirectory, runId, date: date5, appContext } = context;
76122
+ const { xqaDirectory, runId } = context;
76186
76123
  return {
76187
76124
  outputDir: import_node_path21.default.join(xqaDirectory, "output", item.id),
76188
76125
  runId,
76189
76126
  simulatorUdid,
76190
76127
  signal,
76191
76128
  onEvent,
76192
- explorer: {
76193
- mode: "freestyle",
76194
- date: date5,
76195
- mcpServers: createDefaultMcpServers(simulatorUdid),
76196
- allowedTools: ALLOWED_TOOLS,
76197
- timeoutMs: resolveFreestyleTimeout(item, config3) * MS_PER_SECOND4,
76198
- appContext: composeAppContext([
76199
- buildDeviceInstruction(simulatorUdid),
76200
- appContext,
76201
- item.prompt
76202
- ]),
76203
- buildEnv: config3.QA_BUILD_ENV,
76204
- cwd: ensureWorkerCwd(simulatorUdid)
76205
- }
76129
+ explorer: buildFreestyleExplorerConfig(input)
76206
76130
  };
76207
76131
  }
76208
76132
  function executeFreestyleItem(input) {
@@ -76224,7 +76148,8 @@ function buildSpecPipelineConfig(input) {
76224
76148
  allowedTools: ALLOWED_TOOLS,
76225
76149
  appContext: composeAppContext([buildDeviceInstruction(simulatorUdid), appContext]),
76226
76150
  buildEnv: config3.QA_BUILD_ENV,
76227
- cwd: ensureWorkerCwd(simulatorUdid)
76151
+ cwd: ensureWorkerCwd(simulatorUdid),
76152
+ record: true
76228
76153
  }
76229
76154
  };
76230
76155
  }
@@ -76244,7 +76169,7 @@ function makeExecuteItem(context) {
76244
76169
  // src/suite/commands/suite-run-context.ts
76245
76170
  var import_promises20 = __toESM(require("node:fs/promises"), 1);
76246
76171
  var import_node_path22 = __toESM(require("node:path"), 1);
76247
- var import_neverthrow54 = __toESM(require_index_cjs(), 1);
76172
+ var import_neverthrow53 = __toESM(require_index_cjs(), 1);
76248
76173
 
76249
76174
  // src/suite/core/run-id.ts
76250
76175
  var RUN_ID_PAD_LENGTH2 = 4;
@@ -76279,7 +76204,7 @@ function deriveSuiteId(input) {
76279
76204
 
76280
76205
  // src/suite/commands/suite-run-context.ts
76281
76206
  var ISO_DATE_LENGTH3 = 10;
76282
- var safeReaddir2 = import_neverthrow54.ResultAsync.fromThrowable(
76207
+ var safeReaddir2 = import_neverthrow53.ResultAsync.fromThrowable(
76283
76208
  async (directoryPath) => {
76284
76209
  const entries = await import_promises20.default.readdir(directoryPath, { withFileTypes: true });
76285
76210
  return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
@@ -76327,7 +76252,7 @@ async function buildSuiteRunContext(input) {
76327
76252
  }
76328
76253
 
76329
76254
  // src/suite/commands/run-command.ts
76330
- var safeReadFile5 = import_neverthrow55.ResultAsync.fromThrowable(
76255
+ var safeReadFile5 = import_neverthrow54.ResultAsync.fromThrowable(
76331
76256
  async (filePath) => import_promises21.default.readFile(filePath, "utf8"),
76332
76257
  () => "READ_FAILED"
76333
76258
  );
@@ -76515,7 +76440,7 @@ function resolveXqaDirectory() {
76515
76440
  return result.value;
76516
76441
  }
76517
76442
  var program2 = new Command();
76518
- program2.name("xqa").description("AI-powered QA agent CLI").version(`${"1.14.1"}${false ? ` (dev build +${"18d343b"})` : ""}`);
76443
+ program2.name("xqa").description("AI-powered QA agent CLI").version(`${"1.15.0"}${false ? ` (dev build +${"befb4fb"})` : ""}`);
76519
76444
  program2.command("init").description("Initialize a new xqa project in the current directory").action(() => {
76520
76445
  runInitCommand();
76521
76446
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/xqa",
3
- "version": "1.14.1",
3
+ "version": "1.15.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -26,12 +26,12 @@
26
26
  "typescript": "^5.8.3",
27
27
  "vitest": "^3.2.1",
28
28
  "zod": "^3.0.0",
29
- "@qa-agents/explorer": "0.0.0",
30
- "@qa-agents/mobile-ios": "0.0.0",
31
- "@qa-agents/pipeline": "0.0.0",
32
29
  "@qa-agents/display": "0.0.0",
33
30
  "@qa-agents/eslint-config": "0.0.0",
31
+ "@qa-agents/explorer": "0.0.0",
32
+ "@qa-agents/mobile-ios": "0.0.0",
34
33
  "@qa-agents/shared": "0.0.0",
34
+ "@qa-agents/pipeline": "0.0.0",
35
35
  "@qa-agents/typescript-config": "0.0.0"
36
36
  },
37
37
  "dependencies": {