@qawolf/cli 1.8.1 → 1.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qawolf/cli",
3
- "version": "1.8.1",
3
+ "version": "1.9.2",
4
4
  "description": "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
5
5
  "keywords": [
6
6
  "automation",
@@ -60,13 +60,12 @@
60
60
  "@clack/prompts": "1.5.1",
61
61
  "@napi-rs/keyring": "1.3.0",
62
62
  "@oxc-node/core": "0.1.0",
63
- "@qawolf/api-contracts": "0.24.0",
63
+ "@qawolf/api-contracts": "0.25.0",
64
64
  "@qawolf/emails": "1.1.1",
65
65
  "@qawolf/flow-targets": "1.0.0",
66
66
  "@qawolf/flows": "0.1.4",
67
67
  "@qawolf/testkit": "1.1.1",
68
68
  "appium": "2.11.3",
69
- "appium-uiautomator2-driver": "3.7.0",
70
69
  "commander": "14.0.3",
71
70
  "env-paths": "4.0.0",
72
71
  "expect-webdriverio": "5.6.5",
@@ -85,6 +84,7 @@
85
84
  "@tsconfig/strictest": "2.0.8",
86
85
  "@types/bun": "1.3.14",
87
86
  "@types/picomatch": "4.0.3",
87
+ "appium-uiautomator2-driver": "4.2.9",
88
88
  "knip": "6.16.1",
89
89
  "oxfmt": "0.54.0",
90
90
  "oxlint": "1.69.0",
@@ -1,13 +1,14 @@
1
1
  ---
2
2
  name: qawolf-cli
3
- description: Manage QA Wolf through the qawolf CLI. Use when asked which QA Wolf environment variables are available or to list, set, or delete them; manage environments, flows, runs, tags, or issues; authenticate; install; or perform other QA Wolf operations from a shell.
3
+ description: Manage QA Wolf through the qawolf CLI. Use when asked which QA Wolf environment variables are available or to list, set, or delete them; manage environments, flows, runs, tags, or issues; authenticate; install; run or list flows; or drive a live cloud browser (launch a runner, screenshot it, click and type on it, read its recorder) from a shell.
4
4
  license: Apache-2.0
5
5
  compatibility: Requires the qawolf CLI on PATH. Install it from @qawolf/cli or use a standalone binary from GitHub Releases.
6
6
  ---
7
7
 
8
8
  # QA Wolf CLI
9
9
 
10
- `qawolf` runs QA Wolf flows locally and calls the QA Wolf public API.
10
+ `qawolf` runs QA Wolf flows locally, calls the QA Wolf public API, and drives
11
+ interactive runners: live cloud pods holding a browser you can see and act on.
11
12
 
12
13
  This file is an overview, not a reference. Before first using a command whose
13
14
  flags are not shown here, run `qawolf <command> --help` once. The installed CLI
@@ -22,6 +23,11 @@ environment variable (or stored credentials from `qawolf auth login`).
22
23
  their table entry notes a flag that switches them to `read`.
23
24
  Verify with `qawolf auth whoami`. Never print or log the key.
24
25
 
26
+ A team API key in the environment is the whole credential, including for the
27
+ `runner` group. Nothing needs a browser login, a session token or a held
28
+ connection, so a sandbox that can set one environment variable and make
29
+ requests to one host can do everything below.
30
+
25
31
  Commands use `https://app.qawolf.com` by default. Set `QAWOLF_HOST_URL` to
26
32
  target another deployment host, for example
27
33
  `https://app.staging.example.com`. `QAWOLF_API_URL` is a separate API endpoint
@@ -41,6 +47,19 @@ the target, ask instead of guessing. Do not default to the newest environment.
41
47
  listed name is enough to reference `process.env.NAME` in flow code; do not ask
42
48
  for its value merely because the CLI does not return it.
43
49
 
50
+ ## Interactive runners cost money
51
+
52
+ An interactive runner is a live pod holding a browser, and it is billed while it
53
+ runs. `qawolf runner launch` starts one; `qawolf runner run` starts one too when
54
+ no runner is already available, and says so when it does. Reading a runner
55
+ counts as activity, so `qawolf runner events --follow` left open keeps it alive
56
+ and billing. `qawolf runner stop` is what ends it, so stop a runner you launched
57
+ rather than leaving it to time out.
58
+
59
+ `qawolf runner launch` remembers its runner as this directory's default, so the
60
+ commands that follow need no `--runner`. Override that default for one command
61
+ with `--runner <id>`, or for a whole session with `QAWOLF_RUNNER_ID`.
62
+
44
63
  ## Output
45
64
 
46
65
  When consuming output programmatically, always pass `--json` (or `--agent`).
@@ -48,6 +67,11 @@ Human-formatted output is not stable across versions. Errors go to stderr;
48
67
  a non-zero exit code means the command failed. Reuse successful read results
49
68
  within a task unless a relevant write or target change could make them stale.
50
69
 
70
+ One exception to know about: on `qawolf runner events`, `--json` also switches
71
+ each printed line from the payload alone to the whole envelope (`sequence`,
72
+ `recordedAt`, `payload`). Both are JSON. Pass it when you want to page by
73
+ sequence, omit it when you want the payloads themselves.
74
+
51
75
  ## Safety: reads vs writes
52
76
 
53
77
  Read commands do not change team data, but some have operational effects noted
@@ -58,6 +82,12 @@ successful write response is confirmation, so do not read immediately only to
58
82
  verify it. Never blind-retry a write on timeout: it may have reached the server
59
83
  the first time.
60
84
 
85
+ Two runner-specific costs to keep in mind. Launching a runner starts a billed
86
+ pod, so reuse one id rather than minting new ones per step, and stop a runner
87
+ when you are done. And `run`, `act` and `exec` may all have taken effect even
88
+ when their answer never arrives, so none of them is safe to blind-retry; `run`
89
+ is the expensive one, because a second submission bills a second run.
90
+
61
91
  ## Git-backed workflows
62
92
 
63
93
  Inspect `git status` before publishing. Stage and commit only files changed for
@@ -68,45 +98,48 @@ current branch.
68
98
 
69
99
  <!-- commands-table:start — generated by `bun run generate`, do not edit -->
70
100
 
71
- | Command | Kind | What it does |
72
- | -------------------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
73
- | `qawolf auth login` | local | Authenticate with your QA Wolf API key |
74
- | `qawolf auth logout` | local | Remove stored credentials |
75
- | `qawolf auth whoami` | read | Show authentication status |
76
- | `qawolf automate` | write | Request automation for draft flows. First create a named local .flow.ts draft for every requested journey that does not already have a matching draft; never reuse a generic starter or placeholder. Each new draft must start with a JSDoc Goal: description, import flow from @qawolf/flows/web, and use export default flow(...); a comment-only file or direct test(...) call is not a valid draft. Commit and push all changes with Git to publish them, then list remote drafts to resolve every selected ID. Do not use patch to create or rename a selected flow. Finally make one automation request containing all requested flow IDs. |
77
- | `qawolf doctor` | local | Diagnose problems running flows locally |
78
- | `qawolf environment create` | write | Create an environment on the caller's team and return it in the environment.get shape. |
79
- | `qawolf environment deleteVariable` | write | Remove one environment variable by name. Succeeds whether or not the variable existed. |
80
- | `qawolf environment find` | read | List the team's environments, newest first. |
81
- | `qawolf environment get` | read | Read a single environment's name, kind, health status, run concurrency limit, and termination state. |
82
- | `qawolf environment getVariable` | read | Read the values of named environment variables in one call. Values are secrets. Names that do not exist go to missingNames and do not fail the call. |
83
- | `qawolf environment listVariableNames` | read | Use this to answer which QA Wolf environment variables are available to test code. Returns names only; values never leave the server. |
84
- | `qawolf environment setVariable` | write | Create or replace an environment variable. If the user asks to create one for "my email" without naming it, use DEFAULT_ENVIRONMENT_EMAIL. The value is never returned. |
85
- | `qawolf environment update` | write | Update an environment owned by the caller's team and return it in the environment.get shape. Omitted fields remain unchanged. |
86
- | `qawolf flow addTag` | write | Assign an existing tag to the selected flows. Create tags with tag.create. |
87
- | `qawolf flow update` | write | Move a flow between draft and active readiness. The other statuses shown in the app are derived and cannot be set. |
88
- | `qawolf flows list` | local (read with --remote) | List flows matching [pattern] from the local project, or from a QA Wolf environment with --remote |
89
- | `qawolf flows pull` | read | Download an environment's flows into the local .qawolf/<env>/ cache |
90
- | `qawolf flows run` | local (read with --env) | Run flows matching [pattern], or every flow when omitted; with --env, pull missing flows from that QA Wolf environment |
91
- | `qawolf init` | local | Scaffold a QA Wolf project in the current directory |
92
- | `qawolf install` | local | Install every runtime dependency the project's flows need |
93
- | `qawolf install android` | local | Install Android system images, AVDs, and the Appium driver used by the project's Android flows |
94
- | `qawolf install browsers` | local | Install Playwright browsers used by the project's web flows |
95
- | `qawolf install clear` | local | Remove the managed runtime cache (all installed runtime versions) |
96
- | `qawolf issue create` | write | Create a bug or coverage request issue for the caller's team. Maintenance issues cannot be created through the public API. |
97
- | `qawolf issue find` | read | List the team's bug reports, maintenance reports, or coverage requests, newest first. |
98
- | `qawolf issue get` | read | Get an issue by id. |
99
- | `qawolf run create` | write | Create a run for the selected flows and/or tags in an environment. |
100
- | `qawolf run find` | read | List an environment's recent runs, newest first. |
101
- | `qawolf run get` | read | Get a run's status, per-flow results, and links. |
102
- | `qawolf runner evaluateSnippet` | write | Evaluate a snippet against whatever the runner's browser is showing right now. Answers whether the snippet ran, and its error if it threw — not the value it evaluated to, so read anything you want back out of the runner's journal (a snippet's `console.log` lands in the `console` stream). A snippet starts no run and leaves no run-scoped journal entries of its own. `runner-unreachable` if the runner could not be reached or could not evaluate: as well as a runner that is still starting, has terminated after inactivity, or is busy, this covers a runner with no live page to evaluate against — which will never clear, so check that the runner you launched is one that runs a browser before retrying. It is NOT proof the snippet did not run: a snippet that outlives the answer window is still executing when you read this, so do not blindly resubmit one that mutates what the page is looking at. |
103
- | `qawolf runner launch` | write | Launch an interactive runner on the caller's team under an id the caller chooses. Launching the same id again returns the runner already running rather than starting a second one, and the same id with a different runnerName is refused. A runner is not permanent: it terminates on its own after a period of inactivity, and launching the same id after that starts and bills a new runner — so read `outcome` to tell which happened. |
104
- | `qawolf runner readJournal` | read | Read a window of one of an interactive runner's journal streams — the newest few, everything after a cursor, or everything belonging to one run. This is how a flow run's outcome and output are followed: `run-status` settles it, `run-logs` and `run-events` carry what it produced, and `recorder` carries the browser actions the runner recorded. A read counts as activity, so working through history does not get the runner reaped underneath you. `runner-unreachable` if the runner could not be reached: it may still be starting, it may have terminated after inactivity, or it may be too busy to answer. Nothing was changed, so retrying is safe; if it persists, launch the runner again. |
105
- | `qawolf runner runFlow` | write | Run a flow on an interactive runner. Answers as soon as the run is accepted, with the id to follow it by — nothing waits for the run to finish. Which browser or device the run needs is read from the flow file's own execution target, so it is not supplied here; when it does not match what the runner is, the call answers `runner-target-mismatch` rather than failing partway through the run. `runner-unreachable` means the answer did not arrive, which is NOT the same as the run not having started: the runner may have accepted it and been too slow to say so, and resubmitting would start a second run that is billed and journalled alongside the first. Read the runner's `run-status` journal stream before resubmitting, and use the newest run id there if one appeared. |
106
- | `qawolf runner stop` | write | Stop an interactive runner on the caller's team. Stopping a runner that is not running succeeds and reports `not-running`, so a retry needs no special handling. |
107
- | `qawolf runner takeScreenshot` | read | Take one screenshot of an interactive runner's screen. The image is the runner's whole virtual desktop, browser window and all. `screen-needs-a-run` if the runner has not run anything yet, so its virtual desktop has never started. Waiting will not change this and retrying is pointless — call `runner.runFlow` on the runner, then ask for the screen again. Evaluating a snippet does not start the desktop. `screen-not-ready` if the runner has a screen that cannot serve this instant. Retry in a second or two: the desktop restarts when a run changes the display size, and it serves one see-or-act request at a time, so a screenshot or action already in flight is the usual reason. Do not submit a run to clear this — a run may restart the display and discard what is on it. `runner-has-no-screen` if this runner is not one that runs a browser on a virtual desktop. Nothing about it can be seen or driven, and retrying will never help — launch a `node20WithPlaywright` runner instead. `runner-unreachable` if the runner could not be reached: it may still be starting, it may have terminated after inactivity, or it may be too busy to answer. Nothing was changed, so retrying is safe; if it persists, launch the runner again. |
108
- | `qawolf tag create` | write | Create a tag on the caller's team. Tags select flows in run.create. |
109
- | `qawolf tag list` | read | List the team's tags, alphabetical by name. Tag names select flows in run.create. |
101
+ <!-- prettier-ignore -->
102
+ | Command | Kind | What it does |
103
+ | --- | --- | --- |
104
+ | `qawolf auth login` | local | Authenticate with your QA Wolf API key |
105
+ | `qawolf auth logout` | local | Remove stored credentials |
106
+ | `qawolf auth whoami` | read | Show authentication status |
107
+ | `qawolf automate` | write | Request automation for draft flows. First create a named local .flow.ts draft for every requested journey that does not already have a matching draft; never reuse a generic starter or placeholder. Each new draft must start with a JSDoc Goal: description, import flow from @qawolf/flows/web, and use export default flow(...); a comment-only file or direct test(...) call is not a valid draft. Commit and push all changes with Git to publish them, then list remote drafts to resolve every selected ID. Do not use patch to create or rename a selected flow. Finally make one automation request containing all requested flow IDs. |
108
+ | `qawolf doctor` | local | Diagnose problems running flows locally |
109
+ | `qawolf environment create` | write | Create an environment on the caller's team and return it in the environment.get shape. |
110
+ | `qawolf environment deleteVariable` | write | Remove one environment variable by name. Succeeds whether or not the variable existed. |
111
+ | `qawolf environment find` | read | List the team's environments, newest first. |
112
+ | `qawolf environment get` | read | Read a single environment's name, kind, health status, run concurrency limit, and termination state. |
113
+ | `qawolf environment getVariable` | read | Read the values of named environment variables in one call. Values are secrets. Names that do not exist go to missingNames and do not fail the call. |
114
+ | `qawolf environment listVariableNames` | read | Use this to answer which QA Wolf environment variables are available to test code. Returns names only; values never leave the server. |
115
+ | `qawolf environment setVariable` | write | Create or replace an environment variable. If the user asks to create one for "my email" without naming it, use DEFAULT_EMAIL. The value is never returned. |
116
+ | `qawolf environment update` | write | Update an environment owned by the caller's team and return it in the environment.get shape. Omitted fields remain unchanged. |
117
+ | `qawolf flow addTag` | write | Assign an existing tag to the selected flows. Create tags with tag.create. |
118
+ | `qawolf flow update` | write | Move a flow between draft and active readiness. The other statuses shown in the app are derived and cannot be set. |
119
+ | `qawolf flows list` | local (read with --remote) | List flows matching [pattern] from the local project, or from a QA Wolf environment with --remote |
120
+ | `qawolf flows pull` | read | Download an environment's flows into the local .qawolf/<env>/ cache |
121
+ | `qawolf flows run` | local (read with --env) | Run flows matching [pattern], or every flow when omitted; with --env, pull missing flows from that QA Wolf environment |
122
+ | `qawolf init` | local | Scaffold a QA Wolf project in the current directory |
123
+ | `qawolf install` | local | Install every runtime dependency the project's flows need |
124
+ | `qawolf install android` | local | Install Android system images, AVDs, and the Appium driver used by the project's Android flows |
125
+ | `qawolf install browsers` | local | Install Playwright browsers used by the project's web flows |
126
+ | `qawolf install clear` | local | Remove the managed runtime cache (all installed runtime versions) |
127
+ | `qawolf issue create` | write | Create a bug or coverage request issue for the caller's team. Maintenance issues cannot be created through the public API. |
128
+ | `qawolf issue find` | read | List the team's bug reports, maintenance reports, or coverage requests, newest first. |
129
+ | `qawolf issue get` | read | Get an issue by id. |
130
+ | `qawolf run create` | write | Create a run for the selected flows and/or tags in an environment. |
131
+ | `qawolf run find` | read | List an environment's recent runs, newest first. |
132
+ | `qawolf run get` | read | Get a run's status, per-flow results, and links. |
133
+ | `qawolf runner act` | write | Perform one raw action on a runner's screen: click, double_click, scroll, move, drag, keypress, navigate or type. Use - to read a whole action as JSON from stdin |
134
+ | `qawolf runner events` | read | Print a runner's journal, one entry per line. QA Wolf writes console, recorder, run-events, run-logs, run-status |
135
+ | `qawolf runner exec` | write | Evaluate a snippet against a runner's live page. Use - to read the snippet from stdin |
136
+ | `qawolf runner keepalive` | read | Reset a runner's inactivity clock, for a caller that pauses between actions |
137
+ | `qawolf runner launch` | write | Launch an interactive runner and make it this directory's default |
138
+ | `qawolf runner run` | write | Run a flow on an interactive runner, shipping the current directory's files with it |
139
+ | `qawolf runner screenshot` | read | Save a JPEG of an interactive runner's screen to a file |
140
+ | `qawolf runner stop` | write | Stop an interactive runner |
141
+ | `qawolf tag create` | write | Create a tag on the caller's team. Tags select flows in run.create. |
142
+ | `qawolf tag list` | read | List the team's tags, alphabetical by name. Tag names select flows in run.create. |
110
143
 
111
144
  <!-- commands-table:end -->
112
145
 
@@ -114,3 +147,18 @@ Kinds: `read` calls the QA Wolf API without changing anything; `write`
114
147
  changes team state; `local` only affects this machine. A parenthesized
115
148
  note like `local (read with --remote)` means that flag makes the command
116
149
  call the QA Wolf API and require auth.
150
+
151
+ ## Driving a browser: the `runner` group
152
+
153
+ The `runner` commands drive a live cloud browser: `launch` one, `screenshot` to
154
+ see it, `act` to click and type, `run` a flow on it, `exec` a snippet against its
155
+ page, `events` to read its journal (including the `recorder` stream, which turns
156
+ your actions into Playwright locators), `keepalive` to hold it open, and `stop`
157
+ when done. Everything is a plain request to one host, so a shell with an API key
158
+ and its own vision model can close the see-and-act loop with no other tooling.
159
+
160
+ The full workflow is its own guide: how a runner is billed, why the first call
161
+ must be a run, the order the commands go in, the see-and-act loop, `exec`, the
162
+ recorder, reading history, staying alive, and an end-to-end example. **Read
163
+ [`references/runner.md`](references/runner.md) before driving a runner for the
164
+ first time.**
@@ -0,0 +1,280 @@
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
+ `outcome` for `launched` or `already-running`. 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
+ Commands that target a runner find one in this order: `--runner`, then
17
+ `QAWOLF_RUNNER_ID`, then the runner stored for the current directory (which
18
+ `qawolf runner launch` sets). Setting the environment variable once is the most
19
+ robust for a harness whose working directory may not be stable, but it comes
20
+ with two catches worth knowing before you rely on it.
21
+
22
+ `qawolf runner launch` is not in that order: it takes its id from `--id` and
23
+ never reads `QAWOLF_RUNNER_ID`. Bare `qawolf runner launch` invents a random id,
24
+ bills a pod under it and stores it, so a harness that exported the variable and
25
+ then launched without `--id` ends up with a pod it is not addressing. Pass
26
+ `--id` whenever you have an id in mind.
27
+
28
+ And a runner id that is set is treated as found, whether or not anything is
29
+ running under it. So exporting `QAWOLF_RUNNER_ID=agent-1` turns off the
30
+ auto-launch described next: instead of starting `agent-1`, commands try to reach
31
+ it and fail with exit code `4`, which reads as "retry" and never succeeds.
32
+ Launch that id once yourself and the rest follows.
33
+
34
+ If nothing names a runner, the commands that change something will launch one
35
+ and say so on stderr, naming it: `run`, `act` and `exec`. **Read that
36
+ announcement.** The browser it just started is fresh: nothing has been run on it,
37
+ nothing is signed in, and no page is open. Acting as though your earlier setup
38
+ survived is the single most likely way to drive the wrong page.
39
+
40
+ No `read` command ever launches a runner. `screenshot`, `events` and `keepalive`
41
+ tell you there is no runner rather than quietly billing one, and so does `stop`,
42
+ since starting a pod in order to stop it would be absurd.
43
+
44
+ ## The order that matters
45
+
46
+ A freshly launched runner has no screen. The virtual desktop starts with the
47
+ runner's **first run** and nothing else starts it, so until you have run
48
+ something:
49
+
50
+ - `screenshot` and `act` fail with exit code `2`, except `navigate`, which
51
+ fails with exit code `1` (`action-failed`): it skips the screen but still
52
+ needs the runner to have run something
53
+ - `exec` fails with exit code `4`
54
+ - `events recorder` reads as empty
55
+
56
+ None of that is a fault, and none of it clears on its own. **Only
57
+ `qawolf runner run <flow>` starts the screen.** A bare navigate does not: it
58
+ fails until the first run, however long you wait.
59
+
60
+ So the first call on a new runner has to be a run. That means a flow file and a
61
+ `package.json` on disk, even if all you want is to drive the browser by hand;
62
+ there is no "just give me a screen" call. Once one run has happened, the
63
+ screenshot-and-act loop below works for the rest of the runner's life.
64
+
65
+ Retry on the exit code, not on the message text:
66
+
67
+ - `4` is usually transient. The screen is up but cannot serve this instant:
68
+ restarting after a display-size change, or busy with another request. Retry in
69
+ a second or two — but bound the retries, because `4` also covers a runner that
70
+ was reaped after inactivity, which no amount of retrying brings back. If `4`
71
+ persists past a few tries, relaunch the id.
72
+ - `2` will not clear on its own. Either nothing has run on this runner yet, so
73
+ run a flow, or the runner has no browser at all, so launch with
74
+ `--name node20WithPlaywright` instead. The message says which.
75
+
76
+ The one exception is `exec`, which reports both as `4`; read its message to tell
77
+ them apart.
78
+
79
+ ## Seeing and acting: the loop is yours
80
+
81
+ Two primitives, and you close the loop with your own model. There is no hosted
82
+ vision loop on this surface.
83
+
84
+ `qawolf runner screenshot --out page.jpg` writes a real JPEG to disk, decoded,
85
+ because every coding harness can open an image file. Read it with whatever
86
+ vision you have.
87
+
88
+ `qawolf runner act <action>` performs exactly one action per call, in the
89
+ computer-use tool vocabulary a vision model already emits: `click`,
90
+ `double_click`, `scroll`, `move`, `drag`, `keypress`, `navigate`, `type`. The
91
+ names and the field names are unchanged from that vocabulary on purpose, so you
92
+ can forward a tool call rather than translate it:
93
+
94
+ ```sh
95
+ echo '{"type":"click","button":"left","x":480,"y":260}' | qawolf runner act -
96
+ ```
97
+
98
+ Coordinates are pixels on the same screenshot you just read. The runner serves
99
+ one see-or-act request at a time, so decide what to do next from each answer
100
+ rather than firing several. Bounds are checked before anything is sent, so an
101
+ over-long `--text` or an out-of-range coordinate comes back immediately naming
102
+ the limit instead of occupying the runner and then failing.
103
+
104
+ `act`, `run` and `exec` are the three commands whose lost answer may still have
105
+ taken effect. On a `4` from `act`, take a screenshot before repeating a click.
106
+ `exec`'s message says the snippet could not be evaluated, but a lost answer
107
+ looks the same from outside, so treat a `4` from a snippet that changes something
108
+ as "may have run" rather than "did not run".
109
+
110
+ ## The recorder: what you cannot get from pixels
111
+
112
+ `qawolf runner events recorder` is the capability that has no equivalent in a
113
+ screenshot. As you drive the browser, the runner records each interaction and
114
+ publishes `locator` (the real Playwright locator it resolved), `alternates` (the
115
+ others that matched the same element) and `code` (the generated Playwright call),
116
+ alongside `type`, `sourceUrl` and `timestamp`.
117
+
118
+ ```sh
119
+ qawolf runner events recorder --tail 5 | jq -r '.code // .type' # what happened
120
+ qawolf runner events recorder --tail 5 | jq -r '[.locator] + (.alternates // []) | @tsv'
121
+ ```
122
+
123
+ `code` is absent on events with no call of their own, such as a navigation, which
124
+ is why the first line falls back to `type`. Use these to turn a session you drove
125
+ by pixel coordinates into durable selectors, and to check that a click landed on
126
+ the element you meant rather than near it. The stream is empty until the session
127
+ has a browser context, so an early empty answer means "not yet", not "broken".
128
+ Do not add `--json` here: it wraps each line in an envelope and these field paths
129
+ stop matching.
130
+
131
+ ## Reading the page: `exec`
132
+
133
+ `qawolf runner exec <file>` evaluates a snippet against whatever the runner's
134
+ browser is showing, which is how you read a value out of the page rather than
135
+ looking at it. Two things to know, because neither is guessable:
136
+
137
+ It does not return what the snippet evaluated to, only whether it ran. To get a
138
+ value back, print it and read the `console` stream. Print it behind a marker you
139
+ chose, and match on that rather than taking the newest line: the page logs to the
140
+ same stream, so anything it prints after your snippet would be what `--tail 1`
141
+ hands back. Entries carry `source`, which is `serverConsole` for your snippet and
142
+ `browserConsole` for the page, so filtering on both is what pins the value down.
143
+
144
+ ```sh
145
+ echo 'console.log("qw-title:", await page.title())' | qawolf runner exec -
146
+ qawolf runner events console --tail 20 \
147
+ | jq -r 'select(.source == "serverConsole" and (.message | contains("qw-title:"))) | .message'
148
+ ```
149
+
150
+ And the snippet imports nothing of yours by default. Pass `--file <path>` to
151
+ evaluate it in that file's scope, which also ships the directory's other files,
152
+ so the snippet can use your own page objects and helpers.
153
+
154
+ ## Running a flow
155
+
156
+ `qawolf runner run <file>` ships the current directory's runnable files with the
157
+ request. The runner holds no copy of your project, so what runs is exactly what
158
+ is on disk at that moment, uncommitted edits included. A `package.json` has to
159
+ be there, since the run reads its npm dependencies from it, and the files may
160
+ carry at most 4 MiB in total: run from a directory holding the flow and what it
161
+ imports rather than from the root of a large monorepo. A missing file, a missing
162
+ `package.json` and files over the cap are all refused before any runner is
163
+ resolved or launched, so a typo costs nothing.
164
+
165
+ The call answers with a run id as soon as the run is accepted. **The outcome is
166
+ not in that answer**, it is in the `run-status` stream, whose entries carry
167
+ `runId`, `status` and an `errorMessage` when there is one.
168
+
169
+ **Pass `--follow` to `run` and let it wait for you.** It streams the run's logs
170
+ and ends on the settled status, never on the logs, so a run that prints nothing
171
+ still terminates the follow and a run that dies mid-sentence still reports how.
172
+ Exit code `1` means the run did not pass.
173
+
174
+ ```sh
175
+ qawolf runner run flows/checkout.flow.ts --follow
176
+ ```
177
+
178
+ If you would rather submit and come back later, note that `--follow` on `events`
179
+ does not end when the run settles — it runs until its own `--timeout`, an hour
180
+ by default — so it cannot be used to wait for a run. Poll instead, and decide
181
+ with the same rule the CLI uses: `status` is `in-progress` while the run is
182
+ going, and any other value means it has settled.
183
+
184
+ ```sh
185
+ qawolf runner run flows/checkout.flow.ts --json # -> {"runId":"...","runnerId":"..."}
186
+ qawolf runner events run-status --run <runId> --tail 1 | jq -r '.status'
187
+ ```
188
+
189
+ The one expensive mistake on this surface: **if `run` reports that the runner
190
+ could not be reached, that does not mean the run did not start.** The runner may
191
+ have accepted it and been too slow to answer, and resubmitting bills and journals
192
+ a second run.
193
+
194
+ There is no clean recovery here, so it is worth being plain about it. The journal
195
+ lives on the same pod, so while the runner stays unreachable a `run-status` read
196
+ fails the same way and cannot tell you whether a run is going. Wait for the
197
+ runner to answer again, then read `run-status` without `--run` and look at the
198
+ newest `runId`. Nothing ties that id back to your submission: `run` never
199
+ answered, so you have no id to match it against, and a runner takes work from
200
+ anyone addressing it. Treat the newest id as your run only if you know nothing
201
+ else submits to this runner; otherwise follow it to see what it is before acting
202
+ on it. An empty read is not proof the run did not start, though:
203
+ `run` returns the moment the run is accepted, and its first `run-status` entry
204
+ may not be written yet, so a run accepted just before the runner went quiet can
205
+ still be in flight with nothing to show. `runFlow` has no idempotency key, so a
206
+ resubmit always risks a second billed run. Prefer polling `run-status` a while
207
+ longer over resubmitting; only submit again once you are willing to accept that
208
+ risk.
209
+
210
+ ## Reading history
211
+
212
+ Everything observable is an append-only stream on the pod, read by cursor or
213
+ tail rather than subscribed to, so attaching late still gets you the history that
214
+ is still there. It is not unbounded: a size cap drops the oldest entries on a
215
+ long-lived runner, and a `--tail N` read can stop early and hand back fewer than
216
+ N even when more matched. Both are warned about on stderr — dropped entries only
217
+ once a read holds a cursor, a stopped-early read with a pointer at `--since` —
218
+ so watch stderr, treat a short answer as "at least this" rather than "all there
219
+ was", and read what you care about as you go rather than at the end. QA Wolf writes `recorder`, `console`, `run-events`,
220
+ `run-logs` and `run-status`; a stream nobody has written reads as empty rather
221
+ than as an error, and a stream this CLI version does not know about is still
222
+ readable by name.
223
+
224
+ One payload per line, so shell tools compose:
225
+
226
+ ```sh
227
+ qawolf runner events console --tail 20 | jq -r '.message'
228
+ qawolf runner events run-logs --run <runId> --follow > run.log
229
+ ```
230
+
231
+ `--tail N` takes the newest N, `--since <sequence>` reads everything after a
232
+ cursor, and `--run <id>` narrows the run-scoped streams.
233
+
234
+ `--follow` polls and prints as entries arrive. It is `tail -f` with a bound: it
235
+ ends only at its `--timeout` (an hour by default, exit `6`), because reading
236
+ keeps the runner alive and billing. Redirect it to a file and stop it yourself,
237
+ or use repeated `--since` reads when you need the command to end sooner.
238
+
239
+ Where it does win is the cursor. The pod reports how far a read scanned rather
240
+ than how far it matched, and `--follow` carries that number, so a filtered read
241
+ that matched nothing still moves forward. A caller paging by hand cannot see it,
242
+ because the CLI does not print it, and the best available substitute is the
243
+ highest `sequence` you actually saw. So a narrow `--run` filter over a busy
244
+ stream stalls: with nothing matching, there is no new `sequence` to move on to,
245
+ and you re-read the same window until something matches (NOVA-1397).
246
+
247
+ ## Staying alive
248
+
249
+ A runner is reaped after a period of inactivity, and every command that talks to
250
+ the runner counts as activity, including a journal read.
251
+ `qawolf runner keepalive` exists for the gap that creates: a harness that thinks,
252
+ or waits on a human, for minutes between actions would otherwise come back to a
253
+ pod that is gone. It resets the clock and tells you the runner is still there.
254
+
255
+ It is listed as a `read`, but it is the one read with a cost: keeping the clock
256
+ reset keeps a billed pod alive. Call it while you are genuinely still working, not
257
+ on a timer you forget, and call `qawolf runner stop` when you are done rather
258
+ than leaving a pod to time out. A loop that keeps a runner alive and never stops
259
+ it bills until someone notices.
260
+
261
+ ## End to end
262
+
263
+ Run from a directory holding a flow and a `package.json`. The run is what starts
264
+ the screen, so it is not optional even though the goal here is to drive by hand.
265
+
266
+ ```sh
267
+ export QAWOLF_API_KEY=... # the only credential
268
+ export QAWOLF_RUNNER_ID=agent-1 # so no command below needs --runner
269
+
270
+ qawolf runner launch --id agent-1 --json # --id, not the variable; read .outcome
271
+ qawolf runner run flows/smoke.flow.ts --follow # starts the screen; exit 1 if it failed
272
+
273
+ qawolf runner act navigate --url https://example.com/login
274
+ qawolf runner screenshot --out page.jpg # then read page.jpg yourself
275
+ qawolf runner act click --button left --x 480 --y 260
276
+ qawolf runner act type --text "someone@example.com"
277
+
278
+ qawolf runner events recorder --tail 5 | jq -r '[.locator] + (.alternates // []) | @tsv'
279
+ qawolf runner stop
280
+ ```