@qawolf/cli 1.39.0 → 1.40.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.
@@ -1,662 +1,1213 @@
1
- # Driving a runner from the terminal
2
-
3
- An interactive runner is a live pod with a browser in it. You launch one, look
4
- at it, act on it, run flows on it, and read what it recorded. Everything is a
5
- plain request to one host, so there is no connection to hold open.
6
-
7
- ## Getting one
8
-
9
- Runner ids are yours to choose and are scoped to your team, so `agent-1` is a
10
- fine id. Launching an id that is already running attaches to that runner instead
11
- of starting and billing a second one, and the answer says which happened: read
12
- `alreadyRunning`. Reusing one id is therefore the
13
- cheap and safe pattern, and the same id with a different `--name` is refused
14
- rather than silently ignored.
15
-
16
- Either answer carries a `url`, which `qawolf runner launch` prints, as does a command that launched its own runner. It is a QA Wolf page showing what the runner is doing, where a person can also take over with their own mouse and keyboard. Hand it to a person who asks what your runner is up to. The page opens for anyone on the runner's team, however the runner was launched. There is nothing to see until the runner's first run starts its screen, so the page waits until then. You read the screen with `screenshot`, not with the page.
17
-
18
- Commands that target a runner find one in this order: `--runner`, then
19
- `QAWOLF_RUNNER_ID`, then the runner stored for the current directory (which
20
- `qawolf runner launch` sets). Setting the environment variable once is the most
21
- robust for a harness whose working directory may not be stable, but it comes
22
- with three catches worth knowing before you rely on it.
23
-
24
- `qawolf runner launch` is not in that order: it takes its id from `--id` and
25
- never reads `QAWOLF_RUNNER_ID`. Bare `qawolf runner launch` invents a random id,
26
- bills a pod under it and stores it, so a harness that exported the variable and
27
- then launched without `--id` ends up with a pod it is not addressing. Pass
28
- `--id` whenever you have an id in mind.
29
-
30
- And a runner id that is set is treated as found, whether or not anything is
31
- running under it. So exporting `QAWOLF_RUNNER_ID=agent-1` turns off the
32
- auto-launch described next: instead of starting `agent-1`, commands try to reach
33
- it and fail with exit code `8`, naming the id and saying the variable is what
34
- chose it. Launch that id once yourself and the rest follows.
35
-
36
- And launching an id that differs from `QAWOLF_RUNNER_ID` prints a warning on
37
- stderr naming both ids: the variable still outranks the directory default, so
38
- commands that omit `--runner` keep going to whatever it names, not the runner
39
- you just launched. Expected when you are launching an additional runner on
40
- purpose — address that one with `--runner` rather than re-exporting the
41
- variable, which would repoint every other runner-less command too.
42
-
43
- If nothing names a runner, the commands that change something will launch one
44
- and say so on stderr, naming it: `run`, `act` and `exec`. **Read that
45
- announcement.** The browser it just started is fresh: nothing has been run on it,
46
- nothing is signed in, and no page is open. Acting as though your earlier setup
47
- survived is the single most likely way to drive the wrong page.
48
-
49
- No `read` command ever launches a runner. `screenshot`, `events` and `keepalive`
50
- tell you there is no runner rather than quietly billing one, and so does
51
- `terminate`, since starting a pod in order to end it would be absurd.
52
-
53
- `--name` also chooses between a browser and a mobile device:
54
- `qawolf runner launch --name android` or `--name ios` starts an Appium session
55
- instead of a browser. A command built for the other kind answers a
56
- `failureReason` naming the mismatch rather than doing something approximate —
57
- `runner-is-not-mobile` from `inspect session`/`contexts`/`page-source`/`elements`
58
- on a browser runner, `runner-is-not-a-browser` from `inspect element-html`/
59
- `page-html` on a mobile one — so launch the right family up front rather than
60
- discovering it from a refusal mid-session.
61
-
62
- ## Knowing what you are holding
63
-
64
- `qawolf runner list` names the runners this directory has launched that are
65
- still running, and marks the one a command with no `--runner` would reach:
1
+ <!-- Generated by `bun run generate` from `qawolf help ref runner`, do not edit. Edit the runner commands' help under src/commands/runner instead. -->
66
2
 
67
- ```text
68
- id family default
69
- tester-abc-main playwright yes
70
- tester-abc-checkout playwright
71
- ```
3
+ # qawolf runner
72
4
 
73
- Every runner is looked up before it is listed, so a runner that idled out is
74
- absent rather than reported. The lookup neither starts a runner nor resets an
75
- inactivity clock, which is what separates `list` from `keepalive`: listing tells
76
- you what is there and changes nothing, and holding a runner open is still
77
- `keepalive`'s job. A lookup that cannot be answered fails the command rather
78
- than returning a shorter list, because a short list reads as the whole truth.
5
+ An interactive runner is a live pod with a browser or a mobile device in it. You launch one, look at it, act on it, run flows on it, and read what it recorded. Everything is a plain request to one host, so there is no connection to hold open. This guide covers what spans commands; each command's own help below covers the rest.
79
6
 
80
- Nothing is billed by listing, but everything in the list is billing. Terminate
81
- what you are done with.
7
+ ## Which runner a command reaches
82
8
 
83
- The list includes the runner named by `QAWOLF_RUNNER_ID` even though this
84
- directory did not launch it, so a harness handed a runner sees it alongside the
85
- ones it started itself. Use the `id` column with `--runner` to address any of
86
- them; addressing one does not make it the default.
9
+ Commands that target a runner find one in this order: `--runner`, then `QAWOLF_RUNNER_ID`, then the runner stored for the current directory, which `qawolf runner launch` sets. Setting the environment variable once is the most robust for a harness whose working directory may not be stable, with two catches:
87
10
 
88
- The table leaves out the page address, which beside a 63-character id outgrows a terminal. `--json` carries it as `url` on every runner:
11
+ - `qawolf runner launch` is not in that order: it takes its id from `--id` and never reads `QAWOLF_RUNNER_ID`. Pass `--id` whenever you have an id in mind.
12
+ - A runner id that is set is treated as found, whether or not anything is running under it. Exporting `QAWOLF_RUNNER_ID=agent-1` turns off the auto-launch described next: instead of starting `agent-1`, commands try to reach it and fail with exit code `8`, naming the id and saying the variable is what chose it. Launch that id once yourself and the rest follows.
89
13
 
90
- ```sh
91
- qawolf runner list --json | jq -r '.[] | [.id, .url] | @tsv'
92
- ```
14
+ If nothing names a runner, the commands that change something launch one and say so on stderr, naming it: `run`, `act` and `exec`. Read that announcement. The browser it just started is fresh: nothing has been run on it, nothing is signed in, and no page is open. Acting as though your earlier setup survived is the single most likely way to drive the wrong page.
15
+
16
+ No read command ever launches a runner. `screenshot`, `events` and `keepalive` tell you there is no runner rather than quietly billing one, and so does `terminate`.
93
17
 
94
18
  ## The order that matters
95
19
 
96
- A freshly launched runner has no screen. The virtual desktop starts with the
97
- runner's **first run** and nothing else starts it, so until you have run
98
- something:
20
+ A freshly launched runner has no screen. The virtual desktop starts with the runner's first run and nothing else starts it, so until you have run something:
99
21
 
100
- - `screenshot` and `act` fail with exit code `2`, except `navigate`, which
101
- fails with exit code `1` (`action-failed`): it skips the screen but still
102
- needs the runner to have run something
22
+ - `screenshot` and `act` fail with exit code `2`, except `navigate`, which fails with exit code `1` (`action-failed`): it skips the screen but still needs the runner to have run something
103
23
  - `exec` fails with exit code `2`
104
24
  - `events recorder` reads as empty
105
25
 
106
- None of that is a fault, and none of it clears on its own. **Only
107
- `qawolf runner run <flow>` starts the screen.** A bare navigate does not: it
108
- fails until the first run, however long you wait.
26
+ None of that is a fault, and none of it clears on its own. Only `qawolf runner run <flow>` starts the screen. A bare navigate does not: it fails until the first run, however long you wait.
109
27
 
110
- So the first thing you do to a new runner has to put a browser on it. That means
111
- a flow file and a `package.json` on disk, even if all you want is to drive the
112
- browser by hand; there is no "just give me a screen" call. Once one run has
113
- happened, the screenshot-and-act loop below works for the rest of the runner's
114
- life.
28
+ So the first thing you do to a new runner has to put a browser on it. That means a flow file and a `package.json` on disk, even if all you want is to drive the browser by hand; there is no "just give me a screen" call. Once one run has happened, the screenshot-and-act loop works for the rest of the runner's life. A `--lines` selection is the one call that does not need a run first.
115
29
 
116
- A `--lines` selection is the one call that does not need a run first. Sent to a
117
- runner with no browser, the runner starts one and runs your lines against it,
118
- and says so on stderr. Everything else on this page waits for a run.
30
+ ## Exit codes
119
31
 
120
32
  Retry on the exit code, not on the message text:
121
33
 
122
- - `4` is transient, with one exception. The screen is up but cannot serve this instant: restarting after a display-size change, or busy with another request. Retry in a second or two, and bound the retries. The exception is a command that changes something, where a `4` can instead mean the answer was lost with the work in flight. For `act` and `actions`, take a screenshot first and repeat only what the screen says did not happen. For `run`, poll `run-status` instead of submitting again, since a second submission risks a second billed run (see Running a flow). For `exec`, check the effect the snippet was meant to have before running it again.
123
- - `6` means the work ran out of the time it is given. A `runner actions` sequence leaves by the worst thing that happened to any one of its actions — an effect nobody can confirm (`4`) first, then running out of time (`6`), then a refusal (`2`), then an action that plainly did not happen (`1`) — so a sequence that did not reach every action exits `6` only when nothing worse also went wrong. Whatever the code, the message names the last action that took effect, and only what follows it goes in a new request.
124
- - `8` means there is no such runner. It was never launched, or it was
125
- terminated, or it idled out. Retrying never brings one back, so stop and
126
- launch the id or name one that is running. The message says which runner was
127
- meant and whether `--runner`, `QAWOLF_RUNNER_ID` or this directory's stored
128
- default chose it — read that line before you pick an id to launch.
129
- - `2` will not clear on its own. Nothing has run on this runner yet, so run a flow; or the runner has no browser at all, so launch with `--name playwright` instead; or the action belongs to the other runner family, so send `tap`, `swipe` or `fill` to a mobile runner and the browser actions to a browser runner. A `runner actions` sequence also exits `2` before it sends anything, for an argument that is not a JSON array, an array of more than ten actions, and `--screenshot` flags that contradict each other. The message says which.
130
-
131
- ## Seeing and acting: the loop is yours
132
-
133
- Two primitives, and you close the loop with your own model. There is no hosted
134
- vision loop on this surface.
135
-
136
- `qawolf runner screenshot --out page.jpg` writes a real JPEG to disk, decoded,
137
- because every coding harness can open an image file. Read it with whatever
138
- vision you have. `--out -` writes the JPEG bytes to stdout instead, on their own,
139
- for a caller that is a process rather than an agent: the confirmation, and the
140
- JSON line under `--json`, goes to stderr so nothing follows the image on stdout.
141
- A terminal on stdout is refused: redirect or pipe it.
142
-
143
- `qawolf runner act <action>` performs exactly one action per call, in the
144
- computer-use tool vocabulary a vision model already emits: `click`,
145
- `double_click`, `scroll`, `move`, `drag`, `keypress`, `navigate`, `type`. The
146
- names and the field names are unchanged from that vocabulary on purpose, so you
147
- can forward a tool call rather than translate it:
34
+ - `4` is transient, with one exception. The screen is up but cannot serve this instant: restarting after a display-size change, or busy with another request. Retry in a second or two, and bound the retries. The exception is a command that changes something, where a `4` can instead mean the answer was lost with the work in flight. For `act` and `actions`, take a screenshot first and repeat only what the screen says did not happen. For `run`, poll `run-status` instead of submitting again, since a second submission risks a second billed run. For `exec`, check the effect the snippet was meant to have before running it again.
35
+ - `6` means the work ran out of the time it is given.
36
+ - `8` means there is no such runner. It was never launched, or it was terminated, or it idled out. Retrying never brings one back, so stop and launch the id or name one that is running. The message says which runner was meant and whether `--runner`, `QAWOLF_RUNNER_ID` or this directory's stored default chose it; read that line before you pick an id to launch.
37
+ - `2` will not clear on its own. Nothing has run on this runner yet, so run a flow; or the runner has no browser at all, so launch with `--name playwright` instead; or the action belongs to the other runner family, so send `tap`, `swipe` or `fill` to a mobile runner and the browser actions to a browser runner. The message says which.
148
38
 
149
- ```sh
150
- echo '{"type":"click","button":"left","x":480,"y":260}' | qawolf runner act -
151
- ```
39
+ `act`, `actions`, `run` and `exec` are the commands whose lost answer may still have taken effect.
152
40
 
153
- Every action in a see-and-act loop is followed by a look at the result, so ask
154
- for it in the same call: `act ... --screenshot step-1.jpg` (or `--screenshot -`
155
- for stdout) performs the action and writes the screen the runner answers with.
156
- Prefer it over `act` and then `screenshot`: each step is one call instead of
157
- two, with no delay to guess at between them. The screen the runner answers with
158
- is taken once it has changed from just before the action or half a second has
159
- passed, whichever comes first. An action that reached the screen and did not
160
- take effect answers with one too, so a `1` from `act --screenshot` still leaves
161
- you a picture of why the click missed. A `4` whose message starts with
162
- "Performed" means the action happened and only the picture is missing: take a
163
- `screenshot`, never send the action again to get it. As with
164
- `screenshot --out -`, a terminal on stdout is refused.
41
+ ## End to end
165
42
 
166
- Coordinates are pixels on the same screenshot you just read. The runner serves one see-or-act request at a time, so never have two in flight; when the next few steps are already known, send them as one `runner actions` sequence instead of one request after another. Bounds are checked before anything is sent, so an over-long `--text` or an out-of-range coordinate comes back immediately naming the limit instead of occupying the runner and then failing.
43
+ Run from a directory holding a flow and a `package.json`. The run is what starts the screen, so it is not optional even though the goal here is to drive by hand.
167
44
 
168
- `act`, `actions`, `run` and `exec` are the commands whose lost answer may still have taken effect. On a `4` from `act` or `actions`, take a screenshot before repeating a click. `exec`'s message says the snippet could not be evaluated, but a lost answer looks the same from outside, so treat a `4` from a snippet that changes something as "may have run" rather than "did not run".
45
+ ```sh
46
+ export QAWOLF_API_KEY=... # the only credential
47
+ export QAWOLF_RUNNER_ID=agent-1 # so no command below needs --runner
169
48
 
170
- A mobile runner has a touchscreen, not a mouse, so it has actions of its own:
49
+ qawolf runner launch --id agent-1 --json # --id, not the variable; read .alreadyRunning and .url
50
+ qawolf runner run flows/smoke.flow.ts --follow # starts the screen; exit 1 if it failed
171
51
 
172
- - `tap` touches a point (`--x`, `--y`) or the element `--selector` names. Check the selector first with `inspect elements --selector`, and pass `--strategy ios-predicate` or `shadow` when it is not XPath.
173
- - `swipe --from x,y --to x,y` moves in a straight line between two points. `--duration-ms` sets how long it takes, up to 10000: a slow swipe scrolls, a fast one flings.
174
- - `fill --selector ... --text ...` replaces the value of that field, and `--text ""` clears it. To add to what a field already holds, `tap` it and then `type`.
175
- - `type` types into whatever the last tap focused, the same as on a browser.
52
+ qawolf runner act navigate --url https://example.com/login --screenshot step-1.jpg # then read step-1.jpg yourself
53
+ qawolf runner act click --button left --x 480 --y 260 --screenshot step-2.jpg
54
+ qawolf runner act type --text "someone@example.com" --screenshot step-3.jpg
176
55
 
177
- The browser actions `double_click`, `scroll`, `move`, `keypress` and `navigate` answer `action-not-supported-on-mobile` rather than doing something approximate. `navigate` is the one to watch for, since it works on a browser runner without a run first but has no meaning on mobile at all. `click` with `button: "left"` and `drag` still tap and swipe on mobile, but they are deprecated there, so send `tap` and `swipe`. A browser runner answers `tap`, `swipe` and `fill` with `action-not-supported-on-browser`.
56
+ qawolf runner inspect element-html --selector "#email"
57
+ qawolf runner inspect variable --name cart | jq .total
178
58
 
179
- ### Several steps in one request: `runner actions`
59
+ qawolf runner run flows/smoke.flow.ts --lines 12-40 --follow # just those lines
60
+ qawolf runner events recorder --tail 5 | jq -r '[.locator] + (.alternates // []) | @tsv'
61
+ qawolf runner terminate
62
+ ```
180
63
 
181
- `qawolf runner actions '<json array>'` performs up to ten of the browser actions back to back in one request (not `tap`, `swipe` or `fill`), for the steps you already know: click the field, type into it, press Enter. One round trip instead of three, with no delay to guess at between them, and `-` reads the array from stdin the way `act -` reads one action.
64
+ On a mobile runner, launch with `--name android` or `--name ios`, and drive it with `tap`, `swipe`, `fill` and `type` instead of the browser actions.
182
65
 
183
- ```sh
184
- qawolf runner actions '[{"type":"click","button":"left","x":480,"y":260},{"type":"type","text":"me@example.com"},{"type":"keypress","keys":["Enter"]}]' --screenshot after-login.jpg
66
+ ```text
67
+ Usage: qawolf runner [options] [command]
68
+
69
+ Drive an interactive runner on the QA Wolf platform
70
+
71
+ Options:
72
+ -h, --help display help for command
73
+
74
+ Commands:
75
+ launch [options] Launch an interactive runner and make it this directory's
76
+ default
77
+ list [options] List the runners running on your team
78
+ terminate [options] End an interactive runner, and the pod it runs on with it
79
+ stop-run [options] Stop what a runner is currently executing, leaving the
80
+ runner up
81
+ keepalive [options] Reset a runner's inactivity clock, for a caller that
82
+ pauses between actions
83
+ run [options] <flowFile> Run a flow on an interactive runner, shipping the flow
84
+ and what it imports
85
+ events [options] <stream> Print a runner's journal, one entry per line. QA Wolf
86
+ writes console, recorder, run-events, run-logs,
87
+ run-status
88
+ screenshot [options] Save a JPEG of an interactive runner's screen to a file,
89
+ or write it to stdout with --out -
90
+ act [options] <action> Perform one raw action on a runner's screen. A browser
91
+ runner takes click, double_click, scroll, move, drag,
92
+ keypress, navigate and type; a mobile runner takes tap,
93
+ swipe, fill and type. A browser runner answers mobile
94
+ actions with action-not-supported-on-browser; a mobile
95
+ runner answers the other browser actions with
96
+ action-not-supported-on-mobile. Use - to read a whole
97
+ action as JSON from stdin
98
+ actions [options] <sequence> Perform a sequence of up to ten raw actions on a runner's
99
+ screen in one request, as a JSON array of the same
100
+ actions `runner act` takes. Use - to read the array from
101
+ stdin. Actions run back to back, so batch only steps
102
+ whose targets are on the screen you last saw, and end the
103
+ sequence at the step that changes the page. Each result
104
+ carries an effect: performed, not-performed, or unknown
105
+ when the runner stopped answering and the action may have
106
+ landed
107
+ exec [options] <file> Evaluate a snippet against a runner's live page. Use - to
108
+ read the snippet from stdin
109
+ inspect Read one thing off a runner's live page (browser) or
110
+ Appium session (mobile)
111
+ import-package [options] <name> Install a package into a runner's live run, so a snippet
112
+ or a selection can import it
113
+ highlight-selector [options] [selector] Highlight what a selector matches on a runner's live
114
+ page, so the next screenshot shows it. Omit the selector
115
+ to clear the highlight
116
+ promote-snapshot [options] Accept a run's screenshot as the new baseline for an
117
+ image diff, on the runner that produced it
118
+ record Control video recording on a Playwright runner
119
+ list-recordings [options] Read a page of published video recordings, including
120
+ after the runner terminates. Platform URLs persist; video
121
+ URLs expire
122
+ help [command] display help for command
185
123
  ```
186
124
 
187
- Batch only steps whose targets are all on the screen you last saw and are not moved by the steps before them, and make the step that changes the page — a submit, a navigation, opening a menu — the last one. Then read the frame and decide the next batch from it.
125
+ ## qawolf runner launch
188
126
 
189
- `--screenshot after.jpg` writes the screen after the last action, as `act --screenshot` does, and `-` puts those bytes on stdout with the confirmation on stderr. Stdout then carries the image and nothing else, so `-` trades the per-action results below for the frame: give `--screenshot` a file path whenever you need to read `effect` per action. `--screenshot-mode each --screenshot step.jpg` writes one frame per action instead, `step-0.jpg`, `step-1.jpg` and so on; each is a full image, so keep those sequences short.
127
+ ```text
128
+ Usage: qawolf runner launch [options]
129
+
130
+ Launch an interactive runner and make it this directory's default
131
+
132
+ Options:
133
+ --id <id> Id to launch under. Relaunching an id attaches to that runner
134
+ --name <family> Runner family to run, e.g. playwright
135
+ -h, --help display help for command
136
+
137
+ Examples:
138
+ $ qawolf runner launch
139
+ $ qawolf runner launch --name android
140
+ $ qawolf runner launch --id ci
141
+
142
+ Ids:
143
+ Runner ids are yours to choose and are scoped to your team, so agent-1 is a
144
+ fine id. Launching an id that is already running attaches to that runner
145
+ instead of starting and billing a second one, and the answer says which
146
+ happened: read `alreadyRunning`. Reusing one id is therefore the cheap and
147
+ safe pattern. The same id with a different --name is refused rather than
148
+ silently ignored.
149
+
150
+ launch takes its id from --id and never reads QAWOLF_RUNNER_ID. A bare `qawolf
151
+ runner launch` invents a random id, bills a pod under it and stores it, so a
152
+ harness that exported the variable and then launched without --id ends up with
153
+ a pod it is not addressing. Pass --id whenever you have an id in mind.
154
+
155
+ Launching an id that differs from QAWOLF_RUNNER_ID prints a warning on stderr
156
+ naming both ids: the variable still outranks the directory default, so
157
+ commands that omit --runner keep going to whatever it names, not the runner
158
+ you just launched. That is expected when you launch an additional runner on
159
+ purpose. Address that one with --runner rather than re-exporting the variable,
160
+ which would repoint every other runner-less command too.
161
+
162
+ The page:
163
+ Either answer carries a `url`, which launch prints, as does a command that
164
+ launched its own runner. It is a QA Wolf page showing what the runner is
165
+ doing, where a person can also take over with their own mouse and keyboard.
166
+ Hand it to a person who asks what your runner is up to. The page opens for
167
+ anyone on the runner's team, however the runner was launched. There is nothing
168
+ to see until the runner's first run starts its screen, so the page waits until
169
+ then. You read the screen with `screenshot`, not with the page.
170
+
171
+ Browser or mobile:
172
+ --name also chooses between a browser and a mobile device: --name android or
173
+ --name ios starts an Appium session instead of a browser. A command built for
174
+ the other kind answers a failureReason naming the mismatch rather than doing
175
+ something approximate: runner-is-not-mobile from inspect session, contexts,
176
+ page-source and elements on a browser runner, and runner-is-not-a-browser from
177
+ inspect element-html and page-html on a mobile one. Launch the right family up
178
+ front rather than discovering it from a refusal mid-session.
179
+ ```
190
180
 
191
- The answer holds one entry per action reached, and the field to read on each is `effect`:
181
+ ## qawolf runner list
192
182
 
193
- - `performed` means the runner did it.
194
- - `not-performed` means the runner answered that it did not take effect.
195
- - `unknown` means the runner stopped answering with the action in flight, or its screen went quiet mid-action, so it may have taken effect. Take a screenshot before repeating anything from that step on, and never send it again blind.
183
+ ```text
184
+ Usage: qawolf runner list [options]
196
185
 
197
- The sequence stops at the first action that fails. `--continue-on-failure` carries on past one that reached the runner and did not take effect, which is only safe for actions that do not depend on each other: a `type` after a failed `click` goes to whatever has focus. A runner that cannot be reached, a screen that cannot serve, or running out of time ends the sequence either way, and the message names every action that did not succeed.
186
+ List the runners running on your team
198
187
 
199
- ## Video recording
188
+ Options:
189
+ --here Only the runners this directory launched
190
+ -h, --help display help for command
200
191
 
201
- Use `record` to control video capture on a Playwright runner. Start a flow first
202
- so the runner has a screen, then start a manual capture:
192
+ Examples:
193
+ $ qawolf runner list
194
+ $ qawolf runner list --here
195
+ $ qawolf runner list --json
203
196
 
204
- ```sh
205
- qawolf runner record start --runner ci --json
206
- # Keep result.state.active.id from the response and use it to stop this capture:
207
- qawolf runner record stop <recording-id> --runner ci
208
- qawolf runner record status --runner ci
209
- qawolf runner record auto on --runner ci
210
- qawolf runner record auto off --runner ci
211
- ```
197
+ What the list holds:
198
+ The list names the runners this directory has launched that are still running,
199
+ and marks the one a command with no --runner would reach. It also includes the
200
+ runner named by QAWOLF_RUNNER_ID even though this directory did not launch it,
201
+ so a harness handed a runner sees it alongside the ones it started itself. Use
202
+ the id column with --runner to address any of them; addressing one does not
203
+ make it the default.
212
204
 
213
- Start generates a UUID unless you pass `--recording-id <uuid>`. If a start
214
- response is lost, the error includes that UUID: check `record status` and reuse
215
- the UUID when retrying. Stop always names a specific recording, so retrying it
216
- cannot stop a later capture. Manual recordings span runs and suppress automatic
217
- capture until stopped. The auto setting affects subsequent full runs.
218
- An active automatic recording ends with its run; `record stop` cannot interrupt
219
- it. If a stop publishes a recording with `status: "failed"`, the CLI returns
220
- exit code 1 and still prints the manifest so callers can inspect it.
205
+ Every runner is looked up before it is listed, so a runner that idled out is
206
+ absent rather than reported. The lookup neither starts a runner nor resets an
207
+ inactivity clock, which is what separates list from keepalive: listing tells
208
+ you what is there and changes nothing. A lookup that cannot be answered fails
209
+ the command rather than returning a shorter list, because a short list reads
210
+ as the whole truth.
221
211
 
222
- Published recordings remain accessible after the runner terminates:
212
+ Nothing is billed by listing, but everything in the list is billing. Terminate
213
+ what you are done with.
223
214
 
224
- ```sh
225
- qawolf runner list-recordings --runner ci --json
226
- qawolf runner list-recordings --runner ci --recording-id <uuid> --json
227
- qawolf runner list-recordings --runner ci --page-token '<nextPageToken>' --json
215
+ The table leaves out the page address, which beside a 63-character id outgrows
216
+ a terminal. --json carries it as `url` on every runner:
217
+
218
+ $ qawolf runner list --json | jq -r '.[] | [.id, .url] | @tsv'
228
219
  ```
229
220
 
230
- Each response is one page with `recordings` and an optional `nextPageToken`.
231
- Follow that token for more; the order is storage-key order, not newest-first.
232
- Entries include status, run ids, a stable platform `url`, and an expiring
233
- `videoUrl` when video is available. An empty lookup means the recording has not
234
- been published or does not exist. Abrupt runner loss may leave video unavailable.
235
- Keep the runner id for history lookups after termination clears the local default.
221
+ ## qawolf runner terminate
236
222
 
237
- ## The recorder: what you cannot get from pixels
223
+ ```text
224
+ Usage: qawolf runner terminate [options]
225
+
226
+ End an interactive runner, and the pod it runs on with it
227
+
228
+ Options:
229
+ --runner <id> Runner to target. Defaults to QAWOLF_RUNNER_ID, then this directory's stored runner
230
+ -h, --help display help for command
231
+
232
+ Stopping a run or ending a runner:
233
+ Two different things, and the names are the only warning you get:
234
+ - stop-run stops what the runner is executing and leaves the runner up, its
235
+ browser on whatever page the run reached. The run settles as stopped rather
236
+ than passed or failed. Use it to abandon a run and keep the browser you were
237
+ working against.
238
+ - terminate ends the runner and the pod with it. Everything on it is gone, and
239
+ the next command under that id launches and bills a new one.
240
+
241
+ Both succeed when there was nothing to do, and say which: `wasRunning` is
242
+ false when no run was going, and when no runner was running. Neither is an
243
+ error, so a retry needs no special handling. terminate never launches a runner
244
+ in order to end it.
245
+ ```
238
246
 
239
- `qawolf runner events recorder` is the capability that has no equivalent in a
240
- screenshot. As you drive the browser, the runner records each interaction and
241
- publishes `locator` (the real Playwright locator it resolved), `alternates` (the
242
- others that matched the same element) and `code` (the generated Playwright call),
243
- alongside `type`, `sourceUrl` and `timestamp`.
247
+ ## qawolf runner stop-run
244
248
 
245
- ```sh
246
- qawolf runner events recorder --tail 5 | jq -r '.code // .type' # what happened
247
- qawolf runner events recorder --tail 5 | jq -r '[.locator] + (.alternates // []) | @tsv'
249
+ ```text
250
+ Usage: qawolf runner stop-run [options]
251
+
252
+ Stop what a runner is currently executing, leaving the runner up
253
+
254
+ Options:
255
+ --runner <id> Runner to target. Defaults to QAWOLF_RUNNER_ID, then this directory's stored runner
256
+ -h, --help display help for command
257
+
258
+ Stopping a run or ending a runner:
259
+ Two different things, and the names are the only warning you get:
260
+ - stop-run stops what the runner is executing and leaves the runner up, its
261
+ browser on whatever page the run reached. The run settles as stopped rather
262
+ than passed or failed. Use it to abandon a run and keep the browser you were
263
+ working against.
264
+ - terminate ends the runner and the pod with it. Everything on it is gone, and
265
+ the next command under that id launches and bills a new one.
266
+
267
+ Both succeed when there was nothing to do, and say which: `wasRunning` is
268
+ false when no run was going, and when no runner was running. Neither is an
269
+ error, so a retry needs no special handling. terminate never launches a runner
270
+ in order to end it.
248
271
  ```
249
272
 
250
- `code` is absent on events with no call of their own, such as a navigation, which
251
- is why the first line falls back to `type`. Use these to turn a session you drove
252
- by pixel coordinates into durable selectors, and to check that a click landed on
253
- the element you meant rather than near it. The stream is empty until the session
254
- has a browser context, so an early empty answer means "not yet", not "broken".
255
- Do not add `--json` here: it wraps each line in an envelope and these field paths
256
- stop matching.
273
+ ## qawolf runner keepalive
257
274
 
258
- ## Reading the page: `inspect`
275
+ ```text
276
+ Usage: qawolf runner keepalive [options]
277
+
278
+ Reset a runner's inactivity clock, for a caller that pauses between actions
279
+
280
+ Options:
281
+ --runner <id> Runner to target. Defaults to QAWOLF_RUNNER_ID, then this directory's stored runner
282
+ -h, --help display help for command
283
+
284
+ Examples:
285
+ $ qawolf runner keepalive
286
+ $ qawolf runner keepalive --runner ci
287
+
288
+ When to call it:
289
+ A runner is reaped after a period of inactivity, and every command that talks
290
+ to the runner counts as activity, including a journal read. keepalive exists
291
+ for the gap that creates: a harness that thinks, or waits on a human, for
292
+ minutes between actions would otherwise come back to a pod that is gone. It
293
+ resets the clock and tells you the runner is still there. It never launches a
294
+ runner.
295
+
296
+ It is a read with a cost: keeping the clock reset keeps a billed pod alive.
297
+ Call it while you are genuinely still working, not on a timer you forget, and
298
+ call `qawolf runner terminate` when you are done rather than leaving a pod to
299
+ time out. A loop that keeps a runner alive and never stops it bills until
300
+ someone notices.
301
+ ```
259
302
 
260
- `qawolf runner inspect` answers one question about the live page and prints the
261
- answer on stdout by itself, so you can redirect or pipe it:
303
+ ## qawolf runner run
262
304
 
263
- ```sh
264
- qawolf runner inspect element-html --selector "#email"
265
- qawolf runner inspect page-html > page.html
266
- qawolf runner inspect page-html --selector "#cart" # just that subtree
267
- qawolf runner inspect variable --name cart | jq .total
305
+ ```text
306
+ Usage: qawolf runner run [options] <flowFile>
307
+
308
+ Run a flow on an interactive runner, shipping the flow and what it imports
309
+
310
+ Options:
311
+ --follow Report the run's status until it settles: in progress, then passed or failed
312
+ (default: false)
313
+ --logs Stream every log line the run produces while following. Implies --follow
314
+ (default: false)
315
+ --run-events Stream the run's progress events as JSON lines while following. Implies
316
+ --follow (default: false)
317
+ --recorder-events Stream the browser actions the runner records as JSON lines while following,
318
+ from an anchor taken just before submission: the recorder is runner-wide, not
319
+ run-scoped. Implies --follow (default: false)
320
+ --env-id <env> QA Wolf environment whose variables the run is given, by id or alias.
321
+ Defaults to QAWOLF_ENVIRONMENT. QA Wolf reads and decrypts them itself, so
322
+ they never leave the server and no size limit applies to them
323
+ --env-file <path> Dotenv file on this machine whose variables the run is given. Pass this or
324
+ --env-id, not both
325
+ --lines <start-end> Run only these 1-indexed lines against the browser as it stands, instead of
326
+ the whole flow from a fresh one
327
+ --lines-file <path> File the --lines range lives in. Defaults to <flowFile>; pass it only when
328
+ the lines are in another file, such as a page object
329
+ --runner <id> Runner to target. Defaults to QAWOLF_RUNNER_ID, then this directory's stored
330
+ runner
331
+ --timeout <seconds> Give up following after this long. Following keeps the runner alive, so a run
332
+ that never settles would otherwise bill until the terminal closed (default:
333
+ "3600")
334
+ -h, --help display help for command
335
+
336
+ Examples:
337
+ $ qawolf runner run flows/checkout.flow.ts
338
+ $ qawolf runner run flows/checkout.flow.ts --follow
339
+ $ qawolf runner run flows/checkout.flow.ts --follow --logs
340
+ $ qawolf runner run flows/checkout.flow.ts --lines 12-40
341
+ $ qawolf runner run flows/checkout.flow.ts --lines 4-9 --lines-file pages/login.ts
342
+ $ qawolf runner run flows/checkout.flow.ts --env-id staging
343
+
344
+ What travels:
345
+ run ships the flow file, everything it imports, and your package.json and
346
+ tsconfig.json. Nothing else travels, so you can run from the root of a large
347
+ project without sending it. The runner holds no copy of your project, so what
348
+ runs is exactly what is on disk at that moment, uncommitted edits included.
349
+
350
+ Imports are followed the same way a run from the QA Wolf app follows them:
351
+ relative paths and tsconfig.json path aliases, resolving .ts and .js. An
352
+ `export ... from` re-export is not followed, and neither is require(), so a
353
+ barrel file does not pull in what it re-exports. A package.json has to be
354
+ there, since the run reads its npm dependencies from it, and the files may
355
+ carry at most 30 MiB in total. A missing file, a missing package.json and
356
+ files over the cap are all refused before any runner is resolved or launched,
357
+ so a typo costs nothing.
358
+
359
+ After the first run on a runner, later runs send only the files whose content
360
+ changed, so iterating on one flow costs a small request rather than the whole
361
+ graph again. --json reports which happened in `fileSync`: delta or full.
362
+ Nothing about this needs managing: the baseline lives in
363
+ .qawolf/runner-files.json, a switch to another runner ignores it, and a runner
364
+ that turns out not to hold what was claimed gets the whole set resent
365
+ automatically.
366
+
367
+ Waiting for the outcome:
368
+ The call answers with a run id as soon as the run is accepted. The outcome is
369
+ not in that answer; it is in the run-status stream, whose entries carry
370
+ `runId`, `status` and an `errorMessage` when there is one.
371
+
372
+ Pass --follow and let run wait for you. It reports the run's status, in
373
+ progress, then passed or failed, and ends on the settled status. Exit 1 means
374
+ the run did not pass. --logs, --run-events and --recorder-events mirror more
375
+ streams into the follow, and each implies --follow on its own. The recorder is
376
+ runner-wide rather than run-scoped, so --recorder-events carries whatever is
377
+ recorded after an anchor taken just before submission. Whatever mirrors are
378
+ on, the follow still ends on the status, never on them, so a run that prints
379
+ nothing still ends the follow and a run that dies mid-sentence still reports
380
+ how. Combining mirror flags interleaves their lines with nothing saying which
381
+ stream a line came from: fine for eyeballs; when parsing, follow one stream at
382
+ a time.
383
+
384
+ To submit and come back later, poll with the same rule the CLI uses: `status`
385
+ is in-progress while the run is going, and any other value means it has
386
+ settled. `events --follow` does not end when the run settles, so it cannot be
387
+ used to wait for one.
388
+
389
+ $ qawolf runner run flows/checkout.flow.ts --json
390
+ $ qawolf runner events run-status --run <runId> --tail 1 | jq -r '.status'
391
+
392
+ When the runner could not be reached:
393
+ If run reports that the runner could not be reached, that does not mean the
394
+ run did not start. The runner may have accepted it and been too slow to
395
+ answer, and resubmitting bills and journals a second run.
396
+
397
+ There is no clean recovery. The journal lives on the same pod, so while the
398
+ runner stays unreachable a run-status read fails the same way. Wait for the
399
+ runner to answer again, then read run-status without --run and look at the
400
+ newest `runId`. Nothing ties that id back to your submission, and a runner
401
+ takes work from anyone addressing it, so treat the newest id as your run only
402
+ if you know nothing else submits to this runner. An empty read is not proof
403
+ the run did not start either: run returns the moment the run is accepted, and
404
+ its first run-status entry may not be written yet. A resubmit always risks a
405
+ second billed run, so prefer polling run-status a while longer, and only
406
+ submit again once you are willing to accept that risk.
407
+
408
+ Running part of a flow:
409
+ --lines 12-40 runs those lines against the browser as it stands, so nothing is
410
+ re-navigated and nothing is signed in again. Use it to iterate on a step
411
+ without paying for the whole flow to reach it again.
412
+
413
+ The two file paths are the thing to get right, because getting them backwards
414
+ runs the wrong code and nothing reports it:
415
+ - the positional is always the flow file. It is the run's entry point, and it
416
+ is required for every run, selection or not.
417
+ - --lines-file is where the lines live. It defaults to the positional, so pass
418
+ it only when the range is in another file, typically a page object whose
419
+ method you want to run against the instance your last run left alive.
420
+
421
+ The lines-file has to be one of the files that travel, so it lives under the
422
+ directory you run from. A range whose file is not collected is refused before
423
+ a runner is addressed, naming the path.
424
+
425
+ A --lines selection is the one call that does not need a run first. If the
426
+ runner had no browser, one is started before your lines run, and the command
427
+ says so on stderr. Those lines then ran against a fresh page rather than the
428
+ one an earlier run left, which is worth reading before you act on what you
429
+ see.
430
+
431
+ Environment variables:
432
+ A run takes one of two. --env-id names a QA Wolf environment by id or alias,
433
+ the same reference `qawolf flows` takes as --env. --env-file .env gives the
434
+ run the variables in a dotenv file, in the format `qawolf flows pull` writes.
435
+ Passing both is refused: each gives the run its whole environment, so there is
436
+ no order in which they would combine.
437
+
438
+ A run with neither flag falls back to QAWOLF_ENVIRONMENT, the same variable
439
+ `qawolf flows` reads, so one export covers both. The run says on stderr which
440
+ environment it picked up, because those variables reach your flow's code and a
441
+ run should never be given an environment silently. --env-id wins over it, and
442
+ --env-file suppresses it, so a run reading a dotenv file is not handed a
443
+ second environment on top.
444
+
445
+ Prefer --env-id. QA Wolf reads and decrypts the environment itself, so the
446
+ values never leave the server, nothing has to be pulled to disk first, and no
447
+ size limit applies to them. It is the only way to run a flow whose environment
448
+ holds something large, such as a session cookie.
449
+
450
+ A run that sends its own variables with --env-file may carry at most 200 of
451
+ them, each value at most 16 KiB. Names follow what a shell accepts, and
452
+ QAWOLF_TEAM_ID is reserved because QA Wolf sets it from the key you
453
+ authenticated with. All of that is refused before a runner is addressed,
454
+ naming the variable at fault.
268
455
  ```
269
456
 
270
- `page-html` is simplified for a model to read rather than being the browser's
271
- exact markup. `variable` reads a top-level variable of the running workflow and
272
- prints it as JSON, which is how you see what your own code computed rather than
273
- what the page shows.
457
+ ## qawolf runner events
274
458
 
275
- One failure covers three causes, because a runner cannot tell them apart: no
276
- live page, no element matching the selector, no variable under that name. All
277
- three exit `2` and none clears by waiting, so read the message, which carries
278
- whatever the runner said. An unreachable runner exits `4` and is worth retrying;
279
- a runner that is not running at all exits `8` and is not.
459
+ ```text
460
+ Usage: qawolf runner events [options] <stream>
461
+
462
+ Print a runner's journal, one entry per line. QA Wolf writes console, recorder, run-events,
463
+ run-logs, run-status
464
+
465
+ Options:
466
+ --follow Keep reading as new entries arrive. Reading counts as activity, so a follow
467
+ left open keeps the runner alive and billing (default: false)
468
+ --run <id> Restrict run-scoped streams to one run
469
+ --runner <id> Runner to target. Defaults to QAWOLF_RUNNER_ID, then this directory's stored
470
+ runner
471
+ --since <sequence> Read entries after this sequence
472
+ --tail <count> Read only the newest <count> entries
473
+ --timeout <seconds> Give up following after this long. Reading keeps the runner alive, so a
474
+ follow left open would otherwise bill until the terminal closed (default:
475
+ "3600")
476
+ -h, --help display help for command
477
+
478
+ Examples:
479
+ $ qawolf runner events recorder --tail 5
480
+ $ qawolf runner events run-logs --run <runId> --follow
481
+ $ qawolf runner events console --since 120 --json
482
+
483
+ Streams:
484
+ Everything observable is an append-only stream on the pod, read by cursor or
485
+ tail rather than subscribed to, so attaching late still gets you the history
486
+ that is still there. QA Wolf writes recorder, console, run-events, run-logs
487
+ and run-status; a stream nobody has written reads as empty rather than as an
488
+ error, and a stream this CLI version does not know about is still readable by
489
+ name. One payload per line, so shell tools compose:
490
+
491
+ $ qawolf runner events console --tail 20 | jq -r '.message'
492
+ $ qawolf runner events run-logs --run <runId> --follow > run.log
493
+
494
+ --tail N takes the newest N, --since <sequence> reads everything after a
495
+ cursor, and --run <id> narrows the run-scoped streams.
496
+
497
+ History is not unbounded: a size cap drops the oldest entries on a long-lived
498
+ runner, and a --tail N read can stop early and hand back fewer than N even
499
+ when more matched. Both are warned about on stderr, dropped entries only once
500
+ a read holds a cursor, a stopped-early read with a pointer at --since. Watch
501
+ stderr, treat a short answer as "at least this" rather than "all there was",
502
+ and read what you care about as you go rather than at the end.
503
+
504
+ The recorder:
505
+ `events recorder` is what has no equivalent in a screenshot. As you drive the
506
+ browser, the runner records each interaction and publishes `locator` (the real
507
+ Playwright locator it resolved), `alternates` (the others that matched the
508
+ same element) and `code` (the generated Playwright call), alongside `type`,
509
+ `sourceUrl` and `timestamp`:
510
+
511
+ $ qawolf runner events recorder --tail 5 | jq -r '.code // .type'
512
+ $ qawolf runner events recorder --tail 5 | jq -r '[.locator] + (.alternates // []) | @tsv'
513
+
514
+ `code` is absent on events with no call of their own, such as a navigation,
515
+ which is why the first line falls back to `type`. Use these to turn a session
516
+ you drove by pixel coordinates into durable selectors, and to check that a
517
+ click landed on the element you meant rather than near it. The stream is empty
518
+ until the runner's first run gives it a browser context, so an early empty
519
+ answer means "not yet", not "broken". Do not add --json here: it wraps each
520
+ line in an envelope and these field paths stop matching.
521
+
522
+ Following:
523
+ --follow polls and prints as entries arrive. It is tail -f with a bound: it
524
+ ends only at its --timeout (an hour by default, exit 6), because reading keeps
525
+ the runner alive and billing. It does not end when a run settles, so it cannot
526
+ be used to wait for a run: use `runner run --follow`, or poll run-status.
527
+ Redirect it to a file and stop it yourself, or use repeated --since reads when
528
+ you need the command to end sooner.
529
+
530
+ Where --follow wins is the cursor. The pod reports how far a read scanned
531
+ rather than how far it matched, and --follow carries that number, so a
532
+ filtered read that matched nothing still moves forward. A caller paging by
533
+ hand cannot see it, because the CLI does not print it, and the best available
534
+ substitute is the highest `sequence` you actually saw. So a narrow --run
535
+ filter over a busy stream stalls: with nothing matching, there is no new
536
+ `sequence` to move on to, and you re-read the same window until something
537
+ matches.
538
+
539
+ events never launches a runner.
540
+ ```
280
541
 
281
- Use `inspect` before reaching for `exec`. Reading a value through a snippet
282
- means printing it and then fishing it back out of the `console` stream, which is
283
- two calls and a marker; `inspect variable` is one call and the value.
542
+ ## qawolf runner screenshot
284
543
 
285
- ## Reading a mobile screen: `inspect session`/`contexts`/`page-source`/`elements`
544
+ ```text
545
+ Usage: qawolf runner screenshot [options]
546
+
547
+ Save a JPEG of an interactive runner's screen to a file, or write it to stdout with --out -
548
+
549
+ Options:
550
+ --out <path> File to write the image to. - writes the JPEG bytes to stdout on their own and
551
+ moves the confirmation, JSON included, to stderr (default: "screenshot.jpg")
552
+ --runner <id> Runner to target. Defaults to QAWOLF_RUNNER_ID, then this directory's stored runner
553
+ -h, --help display help for command
554
+
555
+ Examples:
556
+ $ qawolf runner screenshot
557
+ $ qawolf runner screenshot --out screens/step-3.jpg
558
+ $ qawolf runner screenshot --out - > step-3.jpg
559
+ $ qawolf runner screenshot --out - | my-vision-tool
560
+
561
+ Reading the screen:
562
+ The image is a real JPEG on disk, decoded, because every coding harness can
563
+ open an image file. Read it with whatever vision you have. --out - writes the
564
+ JPEG bytes to stdout instead, on their own, for a caller that is a process
565
+ rather than an agent: the confirmation, and the JSON line under --json, goes
566
+ to stderr so nothing follows the image on stdout. A terminal on stdout is
567
+ refused: redirect or pipe it.
568
+
569
+ A runner has no screen until its first run, so until then screenshot exits 2.
570
+ It never launches a runner. When you are about to act anyway, `act
571
+ --screenshot` performs the action and returns the screen in one call.
572
+ ```
286
573
 
287
- `element-html`, `page-html` and `variable` are a browser's shapes. A mobile
288
- runner answers four different ones instead, one subcommand per question:
574
+ ## qawolf runner act
289
575
 
290
- ```sh
291
- qawolf runner inspect session
292
- qawolf runner inspect contexts
293
- qawolf runner inspect page-source
294
- qawolf runner inspect page-source --context WEBVIEW_1 # a specific context, not the current one
295
- qawolf runner inspect elements --x 240 --y 480
296
- qawolf runner inspect elements --text "Sign in" --partial
297
- qawolf runner inspect elements --selector "//android.widget.Button[@text='Sign in']"
298
- qawolf runner inspect elements --selector "@label == 'Sign in'" --strategy ios-predicate
576
+ ```text
577
+ Usage: qawolf runner act [options] <action>
578
+
579
+ Perform one raw action on a runner's screen. A browser runner takes click, double_click, scroll,
580
+ move, drag, keypress, navigate and type; a mobile runner takes tap, swipe, fill and type. A browser
581
+ runner answers mobile actions with action-not-supported-on-browser; a mobile runner answers the
582
+ other browser actions with action-not-supported-on-mobile. Use - to read a whole action as JSON from
583
+ stdin
584
+
585
+ Options:
586
+ --button <button> click: left, right, wheel, back or forward
587
+ --duration-ms <ms> swipe: how long it takes, up to 10000. Slow scrolls, fast flings
588
+ --from <x,y> swipe: the point it starts at
589
+ --keys <keys...> keypress: modifiers and the key, e.g. Control a
590
+ --path <json> drag: JSON array of points to drag through
591
+ --runner <id> Runner to target. Defaults to QAWOLF_RUNNER_ID, then this directory's
592
+ stored runner
593
+ --screenshot <path> Also save a JPEG of the screen, taken after the action, to this file, in
594
+ place of a separate screenshot. An action that did not take effect answers
595
+ with one too. - writes it to stdout and moves the confirmation, JSON
596
+ included, to stderr
597
+ --scroll-x <delta> scroll: horizontal wheel delta
598
+ --scroll-y <delta> scroll: vertical wheel delta
599
+ --selector <selector> tap or fill: the element to act on, as a screen object would find it. tap
600
+ takes it in place of --x and --y
601
+ --strategy <strategy> how --selector is resolved: xpath (default), ios-predicate or shadow
602
+ --text <text> type: the text to type into what has focus. fill: the field's new value
603
+ --to <x,y> swipe: the point it ends at
604
+ --url <url> navigate: the http or https URL to go to
605
+ --x <pixels> click, tap and the like: x, in screenshot pixels
606
+ --y <pixels> click, tap and the like: y, in screenshot pixels
607
+ -h, --help display help for command
608
+
609
+ Examples:
610
+ $ qawolf runner act click --button left --x 480 --y 260
611
+ $ qawolf runner act type --text "hello@example.com"
612
+ $ qawolf runner act keypress --keys Control a
613
+ $ qawolf runner act navigate --url https://example.com
614
+ $ qawolf runner act drag --path '[{"x":10,"y":20},{"x":80,"y":90}]'
615
+ $ qawolf runner act click --button left --x 480 --y 260 --screenshot step-4.jpg
616
+ $ echo '{"type":"click","button":"left","x":1,"y":2}' | qawolf runner act -
617
+ $ echo '{"type":"click","button":"left","x":1,"y":2}' | qawolf runner act - --screenshot - > step-5.jpg
618
+
619
+ Mobile:
620
+ $ qawolf runner act tap --x 540 --y 1200
621
+ $ qawolf runner act tap --selector '//*[@content-desc="Continue"]'
622
+ $ qawolf runner act fill --selector 'name == "Postal code"' --strategy ios-predicate --text 94107
623
+ $ qawolf runner act swipe --from 540,1600 --to 540,600 --duration-ms 1500
624
+
625
+ Seeing and acting:
626
+ There is no hosted vision loop: you look with screenshot, decide with your own
627
+ model, and act. act performs exactly one action per call, in the computer-use
628
+ tool vocabulary a vision model already emits. The names and the field names
629
+ are unchanged from that vocabulary on purpose, so you can forward a tool call
630
+ rather than translate it with `act -`.
631
+
632
+ Every action in a see-and-act loop is followed by a look at the result, so ask
633
+ for it in the same call with --screenshot. Prefer it over act and then
634
+ screenshot: each step is one call instead of two, with no delay to guess at
635
+ between them. The screen the runner answers with is taken once it has changed
636
+ from just before the action or half a second has passed, whichever comes
637
+ first. An action that reached the screen and did not take effect answers with
638
+ one too, so an exit 1 still leaves you a picture of why the click missed. A 4
639
+ whose message starts with "Performed" means the action happened and only the
640
+ picture is missing: take a screenshot, never send the action again to get it.
641
+ As with `screenshot --out -`, a terminal on stdout is refused.
642
+
643
+ Coordinates are pixels on the same screenshot you just read. The runner serves
644
+ one see-or-act request at a time, so never have two in flight; when the next
645
+ few steps are already known, send them as one `runner actions` sequence
646
+ instead. Bounds are checked before anything is sent, so an over-long --text or
647
+ an out-of-range coordinate comes back immediately naming the limit.
648
+
649
+ On a 4, the answer may have been lost with the action in flight: take a
650
+ screenshot before repeating a click, and repeat only what the screen says did
651
+ not happen.
652
+
653
+ Mobile:
654
+ A mobile runner has a touchscreen, not a mouse, so it has actions of its own:
655
+ - tap touches a point (--x, --y) or the element --selector names. Check the
656
+ selector first with `inspect elements --selector`, and pass --strategy
657
+ ios-predicate or shadow when it is not XPath.
658
+ - swipe --from x,y --to x,y moves in a straight line between two points.
659
+ --duration-ms sets how long it takes, up to 10000: a slow swipe scrolls, a
660
+ fast one flings.
661
+ - fill --selector ... --text ... replaces the value of that field, and --text
662
+ "" clears it. To add to what a field already holds, tap it and then type.
663
+ - type types into whatever the last tap focused, the same as on a browser.
664
+
665
+ On mobile, send tap and swipe, never click or drag. The other browser actions,
666
+ double_click, scroll, move, keypress and navigate, answer
667
+ action-not-supported-on-mobile rather than doing something approximate.
668
+ navigate is the one to watch for, since it works on a browser runner without a
669
+ run first but has no meaning on mobile at all. A browser runner answers tap,
670
+ swipe and fill with action-not-supported-on-browser.
671
+
672
+ Before the first run:
673
+ A runner has no screen until its first run, so act exits 2 until then, except
674
+ navigate, which exits 1 (action-failed): it skips the screen but still needs
675
+ the runner to have run something. With no runner named at all, act launches
676
+ one and says so on stderr.
299
677
  ```
300
678
 
301
- `session` prints one summary line — ready, or why not — because that line is
302
- the whole answer. `contexts`, `page-source` and `elements` instead print their
303
- answer as JSON on stdout, on its own like `inspect element-html`, so you can
304
- redirect or pipe it (`| jq .current`, `| jq .matches`, `| jq .pageSource`)
305
- rather than getting a count with no way to see what was actually found.
306
-
307
- `session` is also the one subcommand that never answers `screen-needs-a-run`:
308
- readiness is the question it exists to answer, so it reports one of `ready`
309
- (with the platform, device and session id), `unreachable`, `ambiguous` (more
310
- than one session is somehow live) or `no-session`, rather than refusing until a
311
- run has happened. The other three need a live session first and share the same
312
- readiness contract `act` and `screenshot` use on a browser runner:
313
- `screen-needs-a-run` exits `2` and means no Appium session has started on this
314
- runner yet — run a flow that opens one, then inspect again; `screen-not-ready`
315
- exits `4` and means the session exists but did not answer this instant, or more
316
- than one is somehow live — retry once, and relaunch the runner if it persists.
317
-
318
- `elements` takes one of three ways to search: whole-pixel `--x`/`--y` on the
319
- device's own screen, the same coordinates a screenshot is measured in;
320
- `--text`, which matches exactly unless `--partial` is passed; or `--selector`,
321
- resolved the same way a screen object's own selector is, with `--strategy`
322
- naming how (`xpath`, `ios-predicate` or `shadow`; defaults to `xpath`). Prefer
323
- `--selector` when checking a selector you are about to write: it answers what
324
- that exact string resolves to, where `--text`/`--x`/`--y` only approximate it.
325
- The three do not mix — passing flags from more than one at once is refused
326
- before a runner is addressed rather than silently searching by whichever one
327
- it picked. An unparseable selector answers `invalid-selector`, exit `2`,
328
- distinctly from a selector that parsed fine but matched nothing, which answers
329
- an empty `matches` list, exit `0` — the same distinction `highlight-selector`
330
- draws on a browser runner. `--context` on `page-source` or `elements` reads a
331
- context other than the current one — useful once `contexts` has told you which
332
- are available.
333
-
334
- A browser runner answers `runner-is-not-mobile` to all four, exit `2`, since
335
- retrying never helps: launch with `--name android` or `--name ios` instead.
336
- `runner-unreachable` exits `4` and is worth retrying, same as everywhere else
337
- on this surface.
338
-
339
- ## Checking a selector: `highlight-selector`
340
-
341
- `inspect element-html` tells you what a selector matched. `highlight-selector`
342
- shows you _where_ it is, by drawing on the page itself:
679
+ ## qawolf runner actions
343
680
 
344
- ```sh
345
- qawolf runner highlight-selector "text=Sign in"
346
- qawolf runner screenshot # the highlight is in this
347
- qawolf runner highlight-selector # omit the selector to clear
681
+ ```text
682
+ Usage: qawolf runner actions [options] <sequence>
683
+
684
+ Perform a sequence of up to ten raw actions on a runner's screen in one request, as a JSON array of
685
+ the same actions `runner act` takes. Use - to read the array from stdin. Actions run back to back,
686
+ so batch only steps whose targets are on the screen you last saw, and end the sequence at the step
687
+ that changes the page. Each result carries an effect: performed, not-performed, or unknown when the
688
+ runner stopped answering and the action may have landed
689
+
690
+ Options:
691
+ --continue-on-failure Carry on past an action that reached the runner and did not take effect;
692
+ a runner that cannot be reached or a screen that cannot serve still ends
693
+ the sequence
694
+ --runner <id> Runner to target. Defaults to QAWOLF_RUNNER_ID, then this directory's
695
+ stored runner
696
+ --screenshot <path> Save a JPEG of the screen after the last action to this file. With
697
+ --screenshot-mode each, one file per action, with the action's index
698
+ before the extension. - writes the final frame to stdout and moves the
699
+ confirmation to stderr
700
+ --screenshot-mode <mode> Defaults to final when --screenshot is given, none otherwise (choices:
701
+ "none", "final", "each")
702
+ -h, --help display help for command
703
+
704
+ Examples:
705
+ $ qawolf runner actions '[{"type":"click","button":"left","x":480,"y":260},{"type":"type","text":"hello@example.com"},{"type":"keypress","keys":["Enter"]}]' --screenshot after-login.jpg
706
+ $ echo '[{"type":"click","button":"left","x":1,"y":2},{"type":"type","text":"hi"}]' | qawolf runner actions -
707
+ $ qawolf runner actions '[{"type":"click","button":"left","x":480,"y":260},{"type":"type","text":"hello"}]' --screenshot-mode each --screenshot step.jpg
708
+ $ qawolf runner actions '[{"type":"scroll","x":480,"y":260,"scroll_x":0,"scroll_y":600},{"type":"click","button":"left","x":120,"y":700}]' --continue-on-failure
709
+
710
+ Mobile:
711
+ $ qawolf runner actions '[{"type":"tap","selector":"//*[@content-desc=\"Email\"]"},{"type":"type","text":"hello@example.com"},{"type":"tap","x":540,"y":1650}]' --screenshot after-login.jpg
712
+ $ qawolf runner actions '[{"type":"fill","selector":"name == \"Postal code\"","strategy":"ios-predicate","text":"94107"},{"type":"swipe","from":{"x":540,"y":1600},"to":{"x":540,"y":600}}]'
713
+
714
+ Batching:
715
+ Use a sequence for the steps you already know: click the field, type into it,
716
+ press Enter. One round trip instead of three, with no delay to guess at
717
+ between them. On a mobile runner the steps are tap, swipe, fill and type, the
718
+ same as act, and a step the runner does not take fails with the same reason
719
+ act would give, action-not-supported-on-mobile or
720
+ action-not-supported-on-browser.
721
+
722
+ Batch only steps whose targets are all on the screen you last saw and are not
723
+ moved by the steps before them, and make the step that changes the page, a
724
+ submit, a navigation, opening a menu, the last one. Then read the frame and
725
+ decide the next batch from it.
726
+
727
+ Screenshots:
728
+ --screenshot after.jpg writes the screen after the last action, as `act
729
+ --screenshot` does, and - puts those bytes on stdout with the confirmation on
730
+ stderr. Stdout then carries the image and nothing else, so - trades the
731
+ per-action results for the frame: give --screenshot a file path whenever you
732
+ need to read `effect` per action. --screenshot-mode each --screenshot step.jpg
733
+ writes one frame per action instead, step-0.jpg, step-1.jpg and so on; each is
734
+ a full image, so keep those sequences short.
735
+
736
+ Reading the answer:
737
+ The answer holds one entry per action reached, and the field to read on each
738
+ is `effect`:
739
+ - performed means the runner did it.
740
+ - not-performed means the runner answered that it did not take effect.
741
+ - unknown means the runner stopped answering with the action in flight, or its
742
+ screen went quiet mid-action, so it may have taken effect. Take a screenshot
743
+ before repeating anything from that step on, and never send it again blind.
744
+
745
+ The sequence stops at the first action that fails. --continue-on-failure
746
+ carries on past one that reached the runner and did not take effect, which is
747
+ only safe for actions that do not depend on each other: a type after a failed
748
+ click goes to whatever has focus. A runner that cannot be reached, a screen
749
+ that cannot serve, or running out of time ends the sequence either way, and
750
+ the message names every action that did not succeed.
751
+
752
+ Exit codes:
753
+ A sequence exits by the worst thing that happened to any one of its actions:
754
+ an effect nobody can confirm (4) first, then running out of time (6), then a
755
+ refusal (2), then an action that plainly did not happen (1). Whatever the
756
+ code, the message names the last action that took effect, and only what
757
+ follows it goes in a new request. A sequence also exits 2 before it sends
758
+ anything for an argument that is not a JSON array, an array of more than ten
759
+ actions, and --screenshot flags that contradict each other.
348
760
  ```
349
761
 
350
- The highlight stays until it is replaced or cleared, which is the point: you
351
- cannot see the runner's screen, so the only way to read the result is the next
352
- screenshot.
762
+ ## qawolf runner exec
353
763
 
354
- Three answers are worth telling apart. A selector that matched prints how many
355
- elements it hit and exits `0`. A selector the page read fine but that matched
356
- nothing also exits `0`, because the call did what was asked and the count is the
357
- answer; the message says the syntax was fine so you look at the page, not the
358
- locator. A selector the page could not read at all exits `2`, because that one
359
- is yours to correct and retrying will not change it.
764
+ ```text
765
+ Usage: qawolf runner exec [options] <file>
766
+
767
+ Evaluate a snippet against a runner's live page. Use - to read the snippet from stdin
768
+
769
+ Options:
770
+ --file <path> File whose scope the snippet is evaluated in; it and the directory's other files
771
+ travel with it
772
+ --runner <id> Runner to target. Defaults to QAWOLF_RUNNER_ID, then this directory's stored runner
773
+ -h, --help display help for command
774
+
775
+ Examples:
776
+ $ qawolf runner exec snippet.ts
777
+ $ echo 'console.log(await page.title())' | qawolf runner exec -
778
+ $ qawolf runner exec snippet.ts --file flows/checkout.flow.ts
779
+
780
+ Getting a value back:
781
+ exec does not return what the snippet evaluated to, only whether it ran. To
782
+ get a value back, print it and read the `console` stream with `qawolf runner
783
+ events console`. Print it behind a marker you chose, and match on that rather
784
+ than taking the newest line: the page logs to the same stream, so anything it
785
+ prints after your snippet would be what --tail 1 hands back. Entries carry
786
+ `source`, which is `serverConsole` for your snippet and `browserConsole` for
787
+ the page, so filtering on both is what pins the value down:
788
+
789
+ $ echo 'console.log("qw-title:", await page.title())' | qawolf runner exec -
790
+ $ qawolf runner events console --tail 20 | jq -r 'select(.source == "serverConsole" and (.message | contains("qw-title:"))) | .message'
791
+
792
+ To read a top-level variable of the running workflow, `qawolf runner inspect
793
+ variable` is one call and the value, where exec is two calls and a marker.
794
+
795
+ Scope:
796
+ The snippet imports nothing of yours by default. Pass --file <path> to
797
+ evaluate it in that file's scope, which also ships the directory's other
798
+ files, so the snippet can use your own page objects and helpers.
799
+
800
+ Failures:
801
+ exec exits 2 until the runner's first run, since there is no page to evaluate
802
+ against. On a 4 the message says the snippet could not be evaluated, but a
803
+ lost answer looks the same from outside, so treat a 4 from a snippet that
804
+ changes something as "may have run" rather than "did not run", and check its
805
+ effect before running it again. With no runner named at all, exec launches one
806
+ and says so on stderr.
807
+ ```
360
808
 
361
- `runner-cannot-highlight-selectors` exits `2` and means the runner has no
362
- browser to draw on. `no-answer` exits `4`: a highlight runs inside the page, so
363
- a page that is gone or mid-navigation does not answer at all rather than
364
- answering slowly.
809
+ ## qawolf runner inspect
365
810
 
366
- ## Accepting a new baseline: `promote-snapshot`
811
+ ```text
812
+ Usage: qawolf runner inspect [options] [command]
813
+
814
+ Read one thing off a runner's live page (browser) or Appium session (mobile)
815
+
816
+ Options:
817
+ -h, --help display help for command
818
+
819
+ Commands:
820
+ element-html [options] Print the HTML of the first element a selector matches
821
+ page-html [options] Print the page's HTML, simplified for a model to read
822
+ variable [options] Print a top-level variable's value from the running workflow
823
+ session [options] Print the Appium session's status: ready, or why not
824
+ contexts [options] List the WebView contexts available, and which is current
825
+ page-source [options] Print the current context's page source, as a tree
826
+ elements [options] Find elements at a screen point, carrying some text, or matching a
827
+ selector
828
+ help [command] display help for command
829
+
830
+ Examples:
831
+ $ qawolf runner inspect element-html --selector "#email"
832
+ $ qawolf runner inspect page-html > page.html
833
+ $ qawolf runner inspect variable --name cart | jq .total
834
+ $ qawolf runner inspect session
835
+ $ qawolf runner inspect contexts
836
+ $ qawolf runner inspect page-source --context WEBVIEW_1
837
+ $ qawolf runner inspect elements --x 200 --y 400
838
+ $ qawolf runner inspect elements --text "Sign in" --partial
839
+ $ qawolf runner inspect elements --selector "//android.widget.Button[@text='Sign in']"
840
+
841
+ Browser:
842
+ inspect answers one question about the live page and prints the answer on
843
+ stdout by itself, so you can redirect or pipe it. `page-html` is simplified
844
+ for a model to read rather than being the browser's exact markup, and
845
+ --selector narrows it to one subtree. `variable` reads a top-level variable of
846
+ the running workflow and prints it as JSON, which is how you see what your own
847
+ code computed rather than what the page shows. Use it before reaching for
848
+ exec: reading a value through a snippet means printing it and then fishing it
849
+ back out of the console stream, which is two calls and a marker.
850
+
851
+ One failure covers three causes, because a runner cannot tell them apart: no
852
+ live page, no element matching the selector, no variable under that name. All
853
+ three exit 2 and none clears by waiting, so read the message, which carries
854
+ whatever the runner said. An unreachable runner exits 4 and is worth retrying;
855
+ a runner that is not running at all exits 8 and is not.
856
+
857
+ Mobile:
858
+ element-html and page-html are a browser's shapes, and a mobile runner answers
859
+ them with runner-is-not-a-browser. It answers session, contexts, page-source
860
+ and elements instead, and variable works on both.
861
+
862
+ session prints one summary line, ready or why not, because that line is the
863
+ whole answer. contexts, page-source and elements print their answer as JSON on
864
+ stdout, on its own, so you can pipe it (| jq .current, | jq .matches, | jq
865
+ .pageSource) rather than getting a count with no way to see what was found.
866
+
867
+ session is the one subcommand that never answers screen-needs-a-run: readiness
868
+ is the question it exists to answer, so it reports ready (with the platform,
869
+ device and session id), unreachable, ambiguous (more than one session is
870
+ somehow live) or no-session. The other three need a live session first.
871
+ screen-needs-a-run exits 2 and means no Appium session has started on this
872
+ runner yet: run a flow that opens one, then inspect again. screen-not-ready
873
+ exits 4 and means the session exists but did not answer this instant, or more
874
+ than one is somehow live: retry once, and relaunch the runner if it persists.
875
+
876
+ elements takes one of three ways to search: whole-pixel --x/--y on the
877
+ device's own screen, the same coordinates a screenshot is measured in; --text,
878
+ which matches exactly unless --partial is passed; or --selector, resolved the
879
+ same way a screen object's own selector is, with --strategy naming how (xpath,
880
+ ios-predicate or shadow; defaults to xpath). Prefer --selector when checking a
881
+ selector you are about to write: it answers what that exact string resolves
882
+ to, where --text and --x/--y only approximate it. The three do not mix: flags
883
+ from more than one are refused before a runner is addressed. An unparseable
884
+ selector answers invalid-selector, exit 2, distinctly from a selector that
885
+ parsed fine but matched nothing, which answers an empty `matches` list, exit
886
+ 0. --context on page-source or elements reads a context other than the current
887
+ one, once contexts has told you which are available.
888
+
889
+ A browser runner answers runner-is-not-mobile to session, contexts,
890
+ page-source and elements, exit 2, since retrying never helps: launch with
891
+ --name android or --name ios instead. runner-unreachable exits 4 and is worth
892
+ retrying.
893
+ ```
367
894
 
368
- When a run's image diff fails and the new screenshot is the one you want, this
369
- replaces the baseline on the runner that produced it:
895
+ ### qawolf runner inspect element-html
370
896
 
371
- ```sh
372
- qawolf runner events run-events --tail 20 | jq 'select(.type == "imageDiffArtifact")'
373
- qawolf runner promote-snapshot --screenshot checkout-1-actual.png --baseline checkout-1.png
374
- ```
897
+ ```text
898
+ Usage: qawolf runner inspect element-html [options]
375
899
 
376
- Both paths are the ones the diff reported, and both are named rather than
377
- positional, because two paths with one unlabelled is easy to get backwards and
378
- swapping them promotes the wrong image. They are paths inside the run's own
379
- screenshot storage, not files on your machine.
900
+ Print the HTML of the first element a selector matches
380
901
 
381
- `snapshot-not-found` exits `2` and means the run wrote no screenshot at that
382
- path, which nearly always means the paths did not come from a diff this run
383
- produced. Nothing is changed, so correcting the path and repeating is safe.
384
- Promoting twice is also safe, so an unreachable runner is worth retrying.
902
+ Options:
903
+ --selector <selector> Playwright selector to inspect
904
+ --runner <id> Runner to target. Defaults to QAWOLF_RUNNER_ID, then this directory's
905
+ stored runner
906
+ -h, --help display help for command
907
+ ```
385
908
 
386
- ## Installing a package mid-session
909
+ ### qawolf runner inspect page-html
387
910
 
388
- `qawolf runner import-package <name>` installs a package into the runner's live
389
- run, so a snippet or a selection can import it without a whole run to reinstall
390
- dependencies:
911
+ ```text
912
+ Usage: qawolf runner inspect page-html [options]
391
913
 
392
- ```sh
393
- qawolf runner import-package dayjs
394
- qawolf runner import-package dayjs --package-version 1.11.13
914
+ Print the page's HTML, simplified for a model to read
915
+
916
+ Options:
917
+ --selector <selector> Limit the output to this subtree
918
+ --runner <id> Runner to target. Defaults to QAWOLF_RUNNER_ID, then this directory's
919
+ stored runner
920
+ -h, --help display help for command
395
921
  ```
396
922
 
397
- The version defaults to `latest`, and the flag is `--package-version` because
398
- `--version` belongs to the CLI itself. The install resolves against your
399
- project's own dependencies, read from `package.json`, so it needs a run already
400
- going: there is no live run on a runner that has not run anything. npm's own
401
- refusal comes back verbatim on an exit `2`, which is a name or a version to
402
- correct rather than something to retry.
923
+ ### qawolf runner inspect variable
924
+
925
+ ```text
926
+ Usage: qawolf runner inspect variable [options]
927
+
928
+ Print a top-level variable's value from the running workflow
403
929
 
404
- ## Reading the page: `exec`
930
+ Options:
931
+ --name <name> Name of the variable to read
932
+ --runner <id> Runner to target. Defaults to QAWOLF_RUNNER_ID, then this directory's stored runner
933
+ -h, --help display help for command
934
+ ```
405
935
 
406
- `qawolf runner exec <file>` evaluates a snippet against whatever the runner's
407
- browser is showing, which is how you read a value out of the page rather than
408
- looking at it. Two things to know, because neither is guessable:
936
+ ### qawolf runner inspect session
409
937
 
410
- It does not return what the snippet evaluated to, only whether it ran. To get a
411
- value back, print it and read the `console` stream. Print it behind a marker you
412
- chose, and match on that rather than taking the newest line: the page logs to the
413
- same stream, so anything it prints after your snippet would be what `--tail 1`
414
- hands back. Entries carry `source`, which is `serverConsole` for your snippet and
415
- `browserConsole` for the page, so filtering on both is what pins the value down.
938
+ ```text
939
+ Usage: qawolf runner inspect session [options]
416
940
 
417
- ```sh
418
- echo 'console.log("qw-title:", await page.title())' | qawolf runner exec -
419
- qawolf runner events console --tail 20 \
420
- | jq -r 'select(.source == "serverConsole" and (.message | contains("qw-title:"))) | .message'
941
+ Print the Appium session's status: ready, or why not
942
+
943
+ Options:
944
+ --runner <id> Runner to target. Defaults to QAWOLF_RUNNER_ID, then this directory's stored runner
945
+ -h, --help display help for command
421
946
  ```
422
947
 
423
- And the snippet imports nothing of yours by default. Pass `--file <path>` to
424
- evaluate it in that file's scope, which also ships the directory's other files,
425
- so the snippet can use your own page objects and helpers.
426
-
427
- ## Running a flow
428
-
429
- `qawolf runner run <flowFile>` ships the flow file, everything it imports, and
430
- your `package.json` and `tsconfig.json`. Nothing else travels, so you can run
431
- from the root of a large project without sending it. The runner holds no copy of
432
- your project, so what runs is exactly what is on disk at that moment,
433
- uncommitted edits included.
434
-
435
- Imports are followed the same way a run from the QA Wolf app follows them:
436
- relative paths and `tsconfig.json` path aliases, resolving `.ts` and `.js`. An
437
- `export ... from` re-export is not followed, and neither is `require()`, so a
438
- barrel file does not pull in what it re-exports. A `package.json` has to be
439
- there, since the run reads its npm dependencies from it, and the files may carry
440
- at most 30 MiB in total. A missing file, a missing `package.json` and files over
441
- the cap are all refused before any runner is resolved or launched, so a typo
442
- costs nothing.
443
-
444
- After the first run on a runner, later runs send only the files whose content
445
- changed, so iterating on one flow costs a small request rather than the whole
446
- graph again. `--json` reports which happened as `fileSync`, `delta` or `full`.
447
- Nothing about this needs managing: the baseline lives in `.qawolf/runner-files.json`,
448
- a switch to another runner ignores it, and a runner that turns out not to hold
449
- what was claimed gets the whole set resent automatically.
450
-
451
- The call answers with a run id as soon as the run is accepted. **The outcome is
452
- not in that answer**, it is in the `run-status` stream, whose entries carry
453
- `runId`, `status` and an `errorMessage` when there is one.
454
-
455
- ### Running part of a flow
456
-
457
- `--lines 12-40` runs those lines against the browser as it stands, so nothing is
458
- re-navigated and nothing is signed in again. Use it to iterate on a step without
459
- paying for the whole flow to reach it again.
460
-
461
- The two file paths are the thing to get right, because getting them backwards
462
- runs the wrong code and nothing reports it:
463
-
464
- - the positional is **always the flow file**. It is the run's entry point, and it
465
- is required for every run, selection or not.
466
- - `--lines-file` is **where the lines live**. It defaults to the positional, so
467
- pass it only when the range is in another file, typically a page object whose
468
- method you want to run against the instance your last run left alive.
948
+ ### qawolf runner inspect contexts
469
949
 
470
- ```sh
471
- qawolf runner run flows/checkout.flow.ts --lines 12-40 # lines in the flow file
472
- qawolf runner run flows/checkout.flow.ts --lines 4-9 \
473
- --lines-file pages/login.ts # lines in a page object
950
+ ```text
951
+ Usage: qawolf runner inspect contexts [options]
952
+
953
+ List the WebView contexts available, and which is current
954
+
955
+ Options:
956
+ --runner <id> Runner to target. Defaults to QAWOLF_RUNNER_ID, then this directory's stored runner
957
+ -h, --help display help for command
474
958
  ```
475
959
 
476
- The lines-file has to be one of the files that travel, so it lives under the
477
- directory you run from. A range whose file is not collected is refused before a
478
- runner is addressed, naming the path.
960
+ ### qawolf runner inspect page-source
479
961
 
480
- ### Giving the run environment variables
962
+ ```text
963
+ Usage: qawolf runner inspect page-source [options]
481
964
 
482
- There are two ways, and a run takes one of them. `--env-id` names a QA Wolf
483
- environment by id or alias, the same reference `qawolf flows` takes as `--env`.
484
- `--env-file .env` gives the run the variables in a dotenv file, in the format
485
- `qawolf flows pull` writes.
965
+ Print the current context's page source, as a tree
486
966
 
487
- ```sh
488
- qawolf runner run flows/checkout.flow.ts --env-id staging
489
- qawolf runner run flows/checkout.flow.ts --env-file .env
967
+ Options:
968
+ --context <name> Read this context instead of the current one
969
+ --runner <id> Runner to target. Defaults to QAWOLF_RUNNER_ID, then this directory's stored
970
+ runner
971
+ -h, --help display help for command
490
972
  ```
491
973
 
492
- **A run with neither flag falls back to `QAWOLF_ENVIRONMENT`**, the same
493
- variable `qawolf flows` reads, so one export covers both. The run says on
494
- stderr which environment it picked up, because those variables reach your flow's
495
- code and a run should never be given an environment silently. `--env-id` wins
496
- over it, and `--env-file` suppresses it, so a run reading a dotenv file is not
497
- handed a second environment on top.
974
+ ### qawolf runner inspect elements
498
975
 
499
- ```sh
500
- export QAWOLF_ENVIRONMENT=staging
501
- qawolf runner run flows/checkout.flow.ts # runs against staging
976
+ ```text
977
+ Usage: qawolf runner inspect elements [options]
978
+
979
+ Find elements at a screen point, carrying some text, or matching a selector
980
+
981
+ Options:
982
+ --context <name> Read this context instead of the current one
983
+ --partial text: match text containing this, rather than exactly this
984
+ --runner <id> Runner to target. Defaults to QAWOLF_RUNNER_ID, then this directory's
985
+ stored runner
986
+ --selector <selector> Resolved the same way a screen object's own selector is
987
+ --strategy <strategy> selector: xpath, ios-predicate, or shadow (defaults to xpath)
988
+ --text <text> text: the text to match
989
+ --x <pixels> point: whole pixels on the device's own screen
990
+ --y <pixels> point: whole pixels on the device's own screen
991
+ -h, --help display help for command
502
992
  ```
503
993
 
504
- **Prefer `--env-id`.** QA Wolf reads and decrypts the environment itself, so the
505
- values never leave the server, nothing has to be pulled to disk first, and no
506
- size limit applies to them. It is the only way to run a flow whose environment
507
- holds something large, such as a session cookie.
508
-
509
- A run that sends its own variables with `--env-file` may carry at most 200 of
510
- them, each value at most 16 KiB. Names follow what a shell accepts, and
511
- `QAWOLF_TEAM_ID` is reserved because QA Wolf sets it from the key you
512
- authenticated with. All of that is refused before a runner is addressed, naming
513
- the variable at fault.
514
-
515
- Passing both flags is refused. They each give the run its whole environment, so
516
- there is no order in which they would combine.
517
-
518
- If the runner had no browser, one is started before your lines run, and the
519
- command says so on stderr. Those lines then ran against a fresh page rather than
520
- the one an earlier run left, which is worth reading before you act on what you
521
- see.
522
-
523
- **Pass `--follow` to `run` and let it wait for you.** It reports the run's
524
- status — in progress, then passed or failed — and ends on the settled status.
525
- Exit code `1` means the run did not pass. Three flags mirror more streams into
526
- the follow, and each implies `--follow` on its own: `--logs` streams every log
527
- line the run produces, `--run-events` streams the run's progress events as JSON
528
- lines, and `--recorder-events` streams the browser actions the runner records
529
- as JSON lines — the recorder is runner-wide rather than run-scoped, so that one
530
- carries whatever is recorded after an anchor taken just before submission. Whatever mirrors are on, the
531
- follow still ends on the status, never on them, so a run that prints nothing
532
- still terminates the follow and a run that dies mid-sentence still reports how.
533
- Combining mirror flags interleaves their lines with nothing saying which stream
534
- a line came from — fine for eyeballs; when parsing, follow one stream at a time.
994
+ ## qawolf runner import-package
535
995
 
536
- ```sh
537
- qawolf runner run flows/checkout.flow.ts --follow
538
- qawolf runner run flows/checkout.flow.ts --follow --logs
539
- qawolf runner run flows/checkout.flow.ts --follow --recorder-events
996
+ ```text
997
+ Usage: qawolf runner import-package [options] <name>
998
+
999
+ Install a package into a runner's live run, so a snippet or a selection can import it
1000
+
1001
+ Options:
1002
+ --package-version <version> Version to install (default: "latest")
1003
+ --runner <id> Runner to target. Defaults to QAWOLF_RUNNER_ID, then this directory's
1004
+ stored runner
1005
+ -h, --help display help for command
1006
+
1007
+ Examples:
1008
+ $ qawolf runner import-package dayjs
1009
+ $ qawolf runner import-package dayjs --package-version 1.11.13
1010
+
1011
+ Installing mid-session:
1012
+ The package goes into the runner's live run, so a snippet or a selection can
1013
+ import it without a whole run to reinstall dependencies. The flag is
1014
+ --package-version because --version belongs to the CLI itself. The install
1015
+ resolves against your project's own dependencies, read from package.json, so
1016
+ it needs a run already going: there is no live run on a runner that has not
1017
+ run anything. npm's own refusal comes back verbatim on an exit 2, which is a
1018
+ name or a version to correct rather than something to retry.
540
1019
  ```
541
1020
 
542
- If you would rather submit and come back later, note that `--follow` on `events`
543
- does not end when the run settles — it runs until its own `--timeout`, an hour
544
- by default — so it cannot be used to wait for a run. Poll instead, and decide
545
- with the same rule the CLI uses: `status` is `in-progress` while the run is
546
- going, and any other value means it has settled.
1021
+ ## qawolf runner highlight-selector
547
1022
 
548
- ```sh
549
- qawolf runner run flows/checkout.flow.ts --json # -> {"runId":"...","runnerId":"..."}
550
- qawolf runner events run-status --run <runId> --tail 1 | jq -r '.status'
1023
+ ```text
1024
+ Usage: qawolf runner highlight-selector [options] [selector]
1025
+
1026
+ Highlight what a selector matches on a runner's live page, so the next screenshot shows it. Omit the
1027
+ selector to clear the highlight
1028
+
1029
+ Options:
1030
+ --runner <id> Runner to target. Defaults to QAWOLF_RUNNER_ID, then this directory's stored runner
1031
+ -h, --help display help for command
1032
+
1033
+ Examples:
1034
+ $ qawolf runner highlight-selector "text=Sign in"
1035
+ $ qawolf runner highlight-selector "#checkout" && qawolf runner screenshot
1036
+ $ qawolf runner highlight-selector
1037
+
1038
+ Reading the result:
1039
+ `inspect element-html` tells you what a selector matched; highlight-selector
1040
+ shows you where it is, by drawing on the page itself. The highlight stays
1041
+ until it is replaced or cleared, which is the point: you cannot see the
1042
+ runner's screen, so the only way to read the result is the next screenshot.
1043
+
1044
+ Three answers are worth telling apart. A selector that matched prints how many
1045
+ elements it hit and exits 0. A selector the page read fine but that matched
1046
+ nothing also exits 0, because the call did what was asked and the count is the
1047
+ answer; the message says the syntax was fine so you look at the page, not the
1048
+ locator. A selector the page could not read at all exits 2, because that one
1049
+ is yours to correct and retrying will not change it.
1050
+
1051
+ runner-cannot-highlight-selectors exits 2 and means the runner has no browser
1052
+ to draw on. no-answer exits 4: a highlight runs inside the page, so a page
1053
+ that is gone or mid-navigation does not answer at all rather than answering
1054
+ slowly.
551
1055
  ```
552
1056
 
553
- The one expensive mistake on this surface: **if `run` reports that the runner
554
- could not be reached, that does not mean the run did not start.** The runner may
555
- have accepted it and been too slow to answer, and resubmitting bills and journals
556
- a second run.
557
-
558
- There is no clean recovery here, so it is worth being plain about it. The journal
559
- lives on the same pod, so while the runner stays unreachable a `run-status` read
560
- fails the same way and cannot tell you whether a run is going. Wait for the
561
- runner to answer again, then read `run-status` without `--run` and look at the
562
- newest `runId`. Nothing ties that id back to your submission: `run` never
563
- answered, so you have no id to match it against, and a runner takes work from
564
- anyone addressing it. Treat the newest id as your run only if you know nothing
565
- else submits to this runner; otherwise follow it to see what it is before acting
566
- on it. An empty read is not proof the run did not start, though:
567
- `run` returns the moment the run is accepted, and its first `run-status` entry
568
- may not be written yet, so a run accepted just before the runner went quiet can
569
- still be in flight with nothing to show. `runFlow` has no idempotency key, so a
570
- resubmit always risks a second billed run. Prefer polling `run-status` a while
571
- longer over resubmitting; only submit again once you are willing to accept that
572
- risk.
573
-
574
- ## Reading history
575
-
576
- Everything observable is an append-only stream on the pod, read by cursor or
577
- tail rather than subscribed to, so attaching late still gets you the history that
578
- is still there. It is not unbounded: a size cap drops the oldest entries on a
579
- long-lived runner, and a `--tail N` read can stop early and hand back fewer than
580
- N even when more matched. Both are warned about on stderr — dropped entries only
581
- once a read holds a cursor, a stopped-early read with a pointer at `--since` —
582
- so watch stderr, treat a short answer as "at least this" rather than "all there
583
- was", and read what you care about as you go rather than at the end. QA Wolf writes `recorder`, `console`, `run-events`,
584
- `run-logs` and `run-status`; a stream nobody has written reads as empty rather
585
- than as an error, and a stream this CLI version does not know about is still
586
- readable by name.
587
-
588
- One payload per line, so shell tools compose:
1057
+ ## qawolf runner promote-snapshot
589
1058
 
590
- ```sh
591
- qawolf runner events console --tail 20 | jq -r '.message'
592
- qawolf runner events run-logs --run <runId> --follow > run.log
1059
+ ```text
1060
+ Usage: qawolf runner promote-snapshot [options]
1061
+
1062
+ Accept a run's screenshot as the new baseline for an image diff, on the runner that produced it
1063
+
1064
+ Options:
1065
+ --screenshot <path> The screenshot to promote, as the image diff named it
1066
+ --baseline <path> The baseline to replace, as the image diff named it
1067
+ --runner <id> Runner to target. Defaults to QAWOLF_RUNNER_ID, then this directory's stored
1068
+ runner
1069
+ -h, --help display help for command
1070
+
1071
+ Examples:
1072
+ $ qawolf runner promote-snapshot --screenshot checkout-1-actual.png --baseline checkout-1.png
1073
+ $ qawolf runner events run-events --tail 20 | jq 'select(.type == "imageDiffArtifact")'
1074
+
1075
+ Paths:
1076
+ Use it when a run's image diff fails and the new screenshot is the one you
1077
+ want. Both paths are the ones the diff reported in its imageDiffArtifact run
1078
+ event, and both are named rather than positional, because two paths with one
1079
+ unlabelled is easy to get backwards and swapping them promotes the wrong
1080
+ image. They are paths inside the run's own screenshot storage, not files on
1081
+ your machine.
1082
+
1083
+ snapshot-not-found exits 2 and means the run wrote no screenshot at that path,
1084
+ which nearly always means the paths did not come from a diff this run
1085
+ produced. Nothing is changed, so correcting the path and repeating is safe.
1086
+ Promoting twice is also safe, so an unreachable runner is worth retrying.
593
1087
  ```
594
1088
 
595
- `--tail N` takes the newest N, `--since <sequence>` reads everything after a
596
- cursor, and `--run <id>` narrows the run-scoped streams.
1089
+ ## qawolf runner record
597
1090
 
598
- `--follow` polls and prints as entries arrive. It is `tail -f` with a bound: it
599
- ends only at its `--timeout` (an hour by default, exit `6`), because reading
600
- keeps the runner alive and billing. Redirect it to a file and stop it yourself,
601
- or use repeated `--since` reads when you need the command to end sooner.
1091
+ ```text
1092
+ Usage: qawolf runner record [options] [command]
1093
+
1094
+ Control video recording on a Playwright runner
1095
+
1096
+ Options:
1097
+ -h, --help display help for command
1098
+
1099
+ Commands:
1100
+ start [options] Start manual video capture across runs. Requires a ready screen and
1101
+ suppresses automatic capture until stopped
1102
+ stop [options] <recording-id> Stop the recording with this UUID and publish it. Retrying cannot
1103
+ stop a later recording
1104
+ status [options] Show the active recording and automatic recording setting
1105
+ auto [options] <setting> Enable or disable automatic recording for subsequent full runs
1106
+ help [command] display help for command
1107
+
1108
+ Recording:
1109
+ Start a flow first so the runner has a screen, then start a manual capture and
1110
+ keep `result.state.active.id` from the `record start --json` answer to stop
1111
+ that capture:
1112
+
1113
+ $ qawolf runner record start --runner ci --json
1114
+ $ qawolf runner record stop <recording-id> --runner ci
1115
+ $ qawolf runner record status --runner ci
1116
+ $ qawolf runner record auto on --runner ci
1117
+
1118
+ Start generates a UUID unless you pass --recording-id <uuid>. If a start
1119
+ response is lost, the error includes that UUID: check `record status` and
1120
+ reuse the UUID when retrying. Stop always names a specific recording, so
1121
+ retrying it cannot stop a later capture. Manual recordings span runs and
1122
+ suppress automatic capture until stopped. The auto setting affects subsequent
1123
+ full runs. An active automatic recording ends with its run; `record stop`
1124
+ cannot interrupt it. If a stop publishes a recording with status "failed", the
1125
+ CLI exits 1 and still prints the manifest so callers can inspect it.
1126
+
1127
+ Published recordings stay readable after the runner terminates, with `qawolf
1128
+ runner list-recordings`.
1129
+ ```
602
1130
 
603
- Where it does win is the cursor. The pod reports how far a read scanned rather
604
- than how far it matched, and `--follow` carries that number, so a filtered read
605
- that matched nothing still moves forward. A caller paging by hand cannot see it,
606
- because the CLI does not print it, and the best available substitute is the
607
- highest `sequence` you actually saw. So a narrow `--run` filter over a busy
608
- stream stalls: with nothing matching, there is no new `sequence` to move on to,
609
- and you re-read the same window until something matches (NOVA-1397).
1131
+ ### qawolf runner record start
610
1132
 
611
- ## Staying alive
1133
+ ```text
1134
+ Usage: qawolf runner record start [options]
612
1135
 
613
- A runner is reaped after a period of inactivity, and every command that talks to
614
- the runner counts as activity, including a journal read.
615
- `qawolf runner keepalive` exists for the gap that creates: a harness that thinks,
616
- or waits on a human, for minutes between actions would otherwise come back to a
617
- pod that is gone. It resets the clock and tells you the runner is still there.
1136
+ Start manual video capture across runs. Requires a ready screen and suppresses automatic capture
1137
+ until stopped
618
1138
 
619
- It is listed as a `read`, but it is the one read with a cost: keeping the clock
620
- reset keeps a billed pod alive. Call it while you are genuinely still working, not
621
- on a timer you forget, and call `qawolf runner terminate` when you are done rather
622
- than leaving a pod to time out. A loop that keeps a runner alive and never stops
623
- it bills until someone notices.
1139
+ Options:
1140
+ --recording-id <uuid> Recording UUID. Generated when omitted; reuse it when retrying this start
1141
+ --runner <id> Runner to target. Defaults to QAWOLF_RUNNER_ID, then this directory's
1142
+ stored runner
1143
+ -h, --help display help for command
1144
+ ```
624
1145
 
625
- ## Stopping a run vs ending a runner
1146
+ ### qawolf runner record stop
626
1147
 
627
- Two different things, and the names are the only warning you get:
1148
+ ```text
1149
+ Usage: qawolf runner record stop [options] <recording-id>
628
1150
 
629
- - `qawolf runner stop-run` stops what the runner is executing and leaves the
630
- runner up, its browser on whatever page the run reached. The run settles as
631
- stopped rather than passed or failed. Use it to abandon a run and keep the
632
- browser you were working against.
633
- - `qawolf runner terminate` ends the runner and the pod with it. Everything on
634
- it is gone, and the next command under that id launches and bills a new one.
1151
+ Stop the recording with this UUID and publish it. Retrying cannot stop a later recording
635
1152
 
636
- Both succeed when there was nothing to do, and say which: `wasRunning` is
637
- `false` when no run was going, and when no runner was running. Neither is an
638
- error, so a retry needs no special handling.
1153
+ Options:
1154
+ --runner <id> Runner to target. Defaults to QAWOLF_RUNNER_ID, then this directory's stored runner
1155
+ -h, --help display help for command
1156
+ ```
639
1157
 
640
- ## End to end
1158
+ ### qawolf runner record status
641
1159
 
642
- Run from a directory holding a flow and a `package.json`. The run is what starts
643
- the screen, so it is not optional even though the goal here is to drive by hand.
1160
+ ```text
1161
+ Usage: qawolf runner record status [options]
644
1162
 
645
- ```sh
646
- export QAWOLF_API_KEY=... # the only credential
647
- export QAWOLF_RUNNER_ID=agent-1 # so no command below needs --runner
1163
+ Show the active recording and automatic recording setting
648
1164
 
649
- qawolf runner launch --id agent-1 --json # --id, not the variable; read .alreadyRunning and .url
650
- qawolf runner run flows/smoke.flow.ts --follow # starts the screen; exit 1 if it failed
1165
+ Options:
1166
+ --runner <id> Runner to target. Defaults to QAWOLF_RUNNER_ID, then this directory's stored runner
1167
+ -h, --help display help for command
1168
+ ```
651
1169
 
652
- qawolf runner act navigate --url https://example.com/login --screenshot step-1.jpg # then read step-1.jpg yourself
653
- qawolf runner act click --button left --x 480 --y 260 --screenshot step-2.jpg
654
- qawolf runner act type --text "someone@example.com" --screenshot step-3.jpg
1170
+ ### qawolf runner record auto
655
1171
 
656
- qawolf runner inspect element-html --selector "#email"
657
- qawolf runner inspect variable --name cart | jq .total
1172
+ ```text
1173
+ Usage: qawolf runner record auto [options] <setting>
658
1174
 
659
- qawolf runner run flows/smoke.flow.ts --lines 12-40 --follow # just those lines
660
- qawolf runner events recorder --tail 5 | jq -r '[.locator] + (.alternates // []) | @tsv'
661
- qawolf runner terminate
1175
+ Enable or disable automatic recording for subsequent full runs
1176
+
1177
+ Arguments:
1178
+ setting Automatic recording setting (choices: "on", "off")
1179
+
1180
+ Options:
1181
+ --runner <id> Runner to target. Defaults to QAWOLF_RUNNER_ID, then this directory's stored runner
1182
+ -h, --help display help for command
1183
+ ```
1184
+
1185
+ ## qawolf runner list-recordings
1186
+
1187
+ ```text
1188
+ Usage: qawolf runner list-recordings [options]
1189
+
1190
+ Read a page of published video recordings, including after the runner terminates. Platform URLs
1191
+ persist; video URLs expire
1192
+
1193
+ Options:
1194
+ --runner <id> Runner to target. Defaults to QAWOLF_RUNNER_ID, then this directory's
1195
+ stored runner
1196
+ --recording-id <uuid> Look up one recording; an empty result means it is not published or does
1197
+ not exist
1198
+ --page-token <token> Continue from nextPageToken returned by the previous page
1199
+ -h, --help display help for command
1200
+
1201
+ Reading pages:
1202
+ $ qawolf runner list-recordings --runner ci --json
1203
+ $ qawolf runner list-recordings --runner ci --recording-id <uuid> --json
1204
+ $ qawolf runner list-recordings --runner ci --page-token '<nextPageToken>' --json
1205
+
1206
+ Each response is one page with `recordings` and an optional `nextPageToken`.
1207
+ Follow that token for more; the order is storage-key order, not newest-first.
1208
+ Entries include status, run ids, a stable platform `url`, and an expiring
1209
+ `videoUrl` when video is available. An empty lookup means the recording has
1210
+ not been published or does not exist. Abrupt runner loss may leave video
1211
+ unavailable. Keep the runner id for history lookups after termination clears
1212
+ the local default.
662
1213
  ```