verikun 0.19.0 → 0.21.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.
@@ -0,0 +1,535 @@
1
+ ---
2
+ name: verikun
3
+ description: >-
4
+ Drive and verify a connected Android device/emulator the way Puppeteer drives a
5
+ browser: tap, type, swipe, screenshot, and — most importantly — inspect the UI
6
+ hierarchy by semantic identifiers (resource-id, visible text, accessibility
7
+ label) to confirm what is on screen. Use whenever a task means interacting with
8
+ or asserting the state of a native app on a device/emulator: "tap the login
9
+ button", "type into the email field", "verify the screen shows X", "scroll down
10
+ and check Y", "automate this signup flow", "screenshot the current screen",
11
+ "is the spinner gone yet". Prefer this over raw adb. Selector commands auto-wait
12
+ ~5s for elements to appear, so you rarely need explicit waits (`--no-wait` opts
13
+ out). Recorded actions form a
14
+ test run you can archive to a JUnit + HTML report (`vk run archive`) — use when
15
+ asked to "test", "verify the flow", or "produce a report". Run a whole known
16
+ flow in one call with `vk batch` (commands piped on stdin or via --file); run a
17
+ directory of natural-language tests as a gated suite with `vk suite <dir>`
18
+ (overview report + non-zero exit on failure — the CI gate). A remote device is
19
+ reachable via `--server <url>` (ai/suite/install) against a `vk server` running
20
+ next to it. iOS (--ios): full parity via idb (tap/type/swipe/`ui` +
21
+ screenshot/launch/stop), on simulators and devices; install idb and see
22
+ `vk doctor --ios`.
23
+ ---
24
+
25
+ # verikun — drive & verify mobile apps
26
+
27
+ `vk` operates a connected Android device/emulator and reads its screen as
28
+ structured, **semantic** elements, so you can act and then *verify* — like
29
+ Puppeteer for native apps. Prefer it over raw `adb`.
30
+
31
+ The command is `vk` (after `npm install -g verikun`, or `npm link` from a source
32
+ clone) or, if not linked, `node dist/bin/verikun.js` from the repo root. All
33
+ examples below use `vk`.
34
+
35
+ ## Before you start (once per session)
36
+
37
+ Run `vk doctor` once before the first device command. Alongside the adb/device checks it
38
+ reports version staleness on stderr — it never fails on that, so treat it as information:
39
+
40
+ - **`claude-code-plugin: … behind this CLI`** → **this skill file is out of date.** Trust
41
+ `vk --help` over what you read here when they disagree, and tell the user to run
42
+ `claude plugin update verikun@verikun` and restart Claude Code.
43
+ - **`verikun: … npm has <newer>`** → tell the user `npm install -g verikun@latest`.
44
+
45
+ **Tell the user, don't run it.** Upgrading changes their machine, and the plugin path needs
46
+ a Claude Code restart to take effect. Mention it once and move on.
47
+
48
+ ## The loop: act → inspect → assert
49
+
50
+ 1. **See** the screen → `vk ui`
51
+ 2. **Act** by semantic selector → `vk tap @login_button`
52
+ 3. **Verify** the result → `vk assert text:"Welcome"` (or `vk ui` again)
53
+
54
+ Never guess coordinates. Reference elements by their identifiers and let `vk`
55
+ resolve the tap point. This is the whole point of the tool.
56
+
57
+ ## Inspect — the core capability
58
+
59
+ - `vk ui` — compact list of every interactive/labeled element, one per line:
60
+ ```
61
+ [3] Button "Sign in" @sign_in_btn (540,1020) tap
62
+ [4] EditText @email_input (540,720) focused
63
+ ```
64
+ Fields: `[index] Type "text" @resource-id (centerX,centerY) flags`. The `@id`
65
+ token can be pasted straight back into a selector.
66
+ - `vk ui --tree` — indented to show nesting. Add `--all` to include layout nodes.
67
+ - `vk ui --json` — structured output for parsing.
68
+ - `vk find <selector>` — print matching elements; exit 1 if none.
69
+ - `vk assert <selector> [--text S] [--gone]` — exit 0 pass / 1 fail. For checks.
70
+ - `vk wait <selector> [--gone] [--timeout ms] [--interval ms]` — poll until the
71
+ element appears (or disappears with `--gone`). Essential for async UI.
72
+ - `vk current` — foreground app/activity.
73
+ - `vk log [package] [-n N]` — recent device logs (Android `logcat` snapshot).
74
+ Reach for this **after a failure** to read the crash/stack trace the screen
75
+ can't show you. **Inside a run it defaults to logs since the run started**, so
76
+ you only see this session's output (not stale logs from before); `-n N` gives
77
+ the last N lines instead, `--since '<MM-DD HH:MM:SS.mmm>'` sets an explicit
78
+ start, `--full` dumps everything. A `package` scopes to that app (system-wide
79
+ if it has already crashed). Unlike other inspect commands it is *recorded*, so
80
+ during a run the logs are pulled **into the archived report** next to the step.
81
+
82
+ ## Act
83
+
84
+ Selector lookups **auto-wait up to 5s** (see [Auto-wait](#auto-wait)), so you
85
+ usually don't need a `wait` before an action — `vk tap @next` already polls for
86
+ `@next` to appear.
87
+
88
+ - `vk tap <selector|index>` · `vk tap --at x,y`
89
+ - `vk text <selector> "the text" [--clear] [--enter]` — focus the field, then type
90
+ - `vk type "text" [--enter]` — type into the already-focused field
91
+ - `vk swipe up|down|left|right [--on <selector>] [--distance f] [--duration ms]`
92
+ - `vk swipe --from x,y --to x,y [--duration ms]`
93
+ - `vk key <name|code>` · `vk back` · `vk home` · `vk enter`
94
+ - `vk screenshot [--out path] [--more] [--max px] [--full]` — saves a PNG (default
95
+ `./.verikun/screen.png`) and prints the path; then read that file to *see* the
96
+ screen. It's **downscaled to a 700px longest edge by default** to save tokens
97
+ (text stays legible); add `--more` if a screen reads too coarse, `--max px` for
98
+ an exact cap, or `--full` for the original.
99
+ - `vk launch <pkg> [--clear] [--no-restart]` · `vk stop <pkg>` · `vk clear <pkg>`
100
+ `vk launch` **restarts by default**: it force-stops the app first so a rerun starts
101
+ from a fresh launch instead of landing on whatever screen a still-running instance
102
+ was left on (re-issuing the launch intent to a live app just resurfaces its current
103
+ state). force-stop is a no-op if the app isn't running. `--no-restart` skips it (just
104
+ bring the existing instance forward).
105
+ `vk clear` (and `vk launch --clear`) additionally wipe the app's local data —
106
+ login/session, prefs, cache — so a flow starts from a clean, logged-out,
107
+ fresh-install state. Android only (`pm clear`, which also force-stops the app); iOS
108
+ has no per-app data reset, so `clear` exits 3 there.
109
+
110
+ ## Change the device, not just the app
111
+
112
+ Some behaviour only appears when the device changes underneath the app — the offline
113
+ banner, the retry path, dark theme, a layout that breaks at accessibility text sizes.
114
+
115
+ - `vk device set <key>=<value> ...` · `vk device get [key]` · `vk device reset` · `vk device caps`
116
+
117
+ | key | values | Android | iOS simulator |
118
+ |---|---|---|---|
119
+ | `airplane` | `on\|off` | yes | **no** — a simulator has no radio |
120
+ | `dark` | `on\|off` | yes | yes |
121
+ | `font-scale` | `0.5`–`3.0`, `default` | yes | yes (nearest Dynamic Type category) |
122
+ | `rotation` | `portrait\|landscape\|portrait-reverse\|landscape-reverse\|auto` | yes | **no** |
123
+ | `stay-awake` | `on\|off` | yes | no-op (simulators don't sleep) |
124
+
125
+ Set several at once: `vk device set dark=on font-scale=1.3`. Each change is **verified by
126
+ reading it back**, so success means it actually landed — these device commands silently
127
+ no-op on some OEM skins. An unsupported key exits **3 before touching the device** and
128
+ names the manual equivalent; `vk device caps` prints the live matrix for your platform.
129
+
130
+ **Always `vk device reset` when the scenario is done.** It restores what the run changed,
131
+ from the snapshot taken before each change. `batch`, `ai` and `suite` reset automatically
132
+ even when the flow *fails* — but a bare `vk device set` from a shell stays applied until
133
+ you reset it, so don't leave someone's phone in airplane mode.
134
+
135
+ Two traps:
136
+ - **`airplane=off` brings the radio back, not the internet.** Follow it with
137
+ `vk assert <selector> --wait 10s`, never an immediate `tap`.
138
+ - **Over wireless adb, `airplane=on` is refused** (exit 2) — it would cut the connection
139
+ carrying your next command. Use USB, or `--allow-wireless` if you accept losing it.
140
+
141
+ ## Selectors
142
+
143
+ | Form | Matches |
144
+ |---|---|
145
+ | `@login` | resource-id (full, `/suffix`, or short name) |
146
+ | `id:login` | same as `@login` |
147
+ | `text:Sign in` | visible text (case-insensitive; auto-heals) |
148
+ | `desc:Submit` | content-desc / accessibility label |
149
+ | `class:Button` | simplified type or full class name |
150
+ | `"Sign in"` | a bare string == `text:Sign in` |
151
+
152
+ **Matching auto-heals** — always case-insensitive, trying **exact → partial
153
+ (substring) → normalized** (ignore punctuation/whitespace/emoji), stopping at the
154
+ first tier that hits. So `text:sign up`, `text:SIGN UP`, and `text:signup` all
155
+ find a "Sign up" button. Exact always wins (a partial never shadows an exact
156
+ match); a non-exact hit is flagged in the output as `(healed: …)`. Ambiguity is
157
+ never auto-resolved — if the winning tier has >1 match, an action lists the
158
+ candidates and exits 2 rather than guess.
159
+
160
+ Modifiers: `--contains` forces substring (skips the exact tier); `--index N`
161
+ picks the Nth match (0-based) when a selector intentionally matches several;
162
+ `--no-scroll` stops an action scrolling its target into view (see below).
163
+
164
+ **State modifiers** require an a11y attribute, in both polarities. Unset means
165
+ *don't care*:
166
+
167
+ | Modifier | Matches | Negative |
168
+ |---|---|---|
169
+ | `--enabled` | actionable right now | `--not-enabled` |
170
+ | `--selected` | current option of a segmented control / tab / mode picker | `--not-selected` |
171
+ | `--checked` | ticked checkbox / switch / radio | `--not-checked` |
172
+ | `--focused` | holds input focus | `--not-focused` |
173
+
174
+ Reach for `--enabled` on any button the app keeps disabled until something else
175
+ is done — a Check/Submit/Continue that lights up only once an answer is picked
176
+ or a form validates. Such a button is *present* long before it is usable, so a
177
+ plain presence match taps a dead control, nothing happens, and the failure
178
+ surfaces several steps later as a puzzling timeout on whatever should have come
179
+ next. With auto-wait, `--enabled` means "wait until it is actually pressable".
180
+
181
+ Reach for the **negative** forms whenever tapping a control *toggles* it. A
182
+ segmented control whose options share one handler flips on any tap, so an
183
+ unconditional "tap the option I want" lands on the other one whenever it was
184
+ already chosen — exit 0, no warning, and the rest of the run exercises the wrong
185
+ mode. Check first, then act:
186
+
187
+ ```sh
188
+ vk find "@mode_video --not-selected" --no-wait && vk tap @mode_video
189
+ ```
190
+
191
+ A modifier is a flag **or** a suffix on the selector string (as above) — the
192
+ string form is what lets a `vk ai` control node carry one, since `if-present` /
193
+ `when` / `repeat` / `while-present` / `read` hold a bare selector with nowhere to
194
+ put a flag: `if-present "id:mode_video --not-selected" { tap id:mode_video }`.
195
+
196
+ **`--selected` and `--focused` do not exist on iOS.** `idb` emits no such key, so
197
+ using them with `--ios` exits **3** instead of matching nothing forever.
198
+ `--enabled` and `--checked` work on both platforms.
199
+
200
+ ## Auto-wait
201
+
202
+ Selector commands (`tap`, `text`, `find`, `assert`, `swipe --on`) **retry the
203
+ lookup for up to 5s** instead of failing on the first miss — they re-capture the
204
+ hierarchy until it resolves. The screen is usually still settling after the prior
205
+ action, so this lets you act/verify directly without a `wait` in between, saving
206
+ round-trips and tokens.
207
+
208
+ - **Default:** 5s. `--wait <dur>` overrides it (`--wait 8s`, `--wait 800ms`, or a
209
+ bare ms count like `--wait 3000`).
210
+ - **`--no-wait` (or `--wait 0`):** fail immediately if the lenient lookup misses.
211
+ Use it for a pure existence probe where you want the answer *now*, e.g.
212
+ `vk find @spinner --no-wait` to check "is it there this instant".
213
+ - **Ambiguity is never waited on** — a present-but-plural match exits 2 right
214
+ away (the elements are already there); add `--index N` or refine the selector.
215
+ - **`vk assert <sel> --gone` waits for *disappearance*** — it polls until the
216
+ element is absent, so you don't need a separate `wait --gone`.
217
+
218
+ When you *do* want to block on a condition as an explicit step (e.g. a long
219
+ network wait beyond 5s), the `wait` command is still there with its own
220
+ `--timeout`/`--interval`; or just bump the inline window with `--wait`.
221
+
222
+ ## Auto scroll-into-view
223
+
224
+ **You do not need to scroll before tapping.** `tap` and `text` bring their target
225
+ into the clear first — into its scroll container, and out from under anything
226
+ drawn over it (a sticky bottom bar, a floating button) — then act, reporting
227
+ `(scrolled into view: N swipes)`. So "scroll down to the card and tap it" is just
228
+ `vk tap @card`. Reach for an explicit `swipe` only when the scrolling itself is
229
+ the thing under test, or to make a lazy list build rows it has not built yet.
230
+
231
+ - `ui` / `find` / `assert` never scroll, and hide nothing: an element with no
232
+ pixel on screen is listed as usual with an `offscreen` marker.
233
+ - An element that cannot be reached is a **failure (exit 1)**, never a blind tap
234
+ on its coordinates. `--no-scroll` turns the scrolling off.
235
+ - Caveat worth knowing: a control covered by something the accessibility tree
236
+ does not contain is invisible to any tool reading that tree. verikun warns on
237
+ stderr when it presses an element it believes is covered — if a tap "succeeds"
238
+ and nothing happens, that warning is the first thing to look for.
239
+
240
+ ## Be frugal: text over images, and remember identifiers
241
+
242
+ **Perceive with text, not pixels.** `vk ui` / `vk find` / `vk assert` return a
243
+ few hundred bytes; a screenshot read back as an image costs far more tokens.
244
+ Default to the textual hierarchy to see and verify state. Reach for `vk
245
+ screenshot` (then read the PNG) only when you genuinely need pixels — visual
246
+ layout, rendering/spacing bugs, or content with no text/id/desc. One image can
247
+ outweigh dozens of `vk ui` calls. When you do, `vk` already downscales the PNG
248
+ (700px longest edge) so the read stays cheap while text remains legible — add
249
+ `--more` if a screen is too coarse to read, or `--full` when you need exact detail.
250
+
251
+ **Two uses of a screenshot — keep them apart.** The cost above is about *reading a
252
+ screenshot back into context* to decide your next move; that is what to avoid (perceive
253
+ and verify with the hierarchy instead). A screenshot taken purely as **report evidence
254
+ and never read back** costs no tokens. So when you drive a flow to produce a report, **do**
255
+ `vk screenshot` around each significant transition (and before a risky or verification step)
256
+ — then leave it in the report, don't read the PNG back. A visual trail makes post-run review
257
+ far easier, and a failing step already auto-captures its own screen.
258
+
259
+ It is not free in wall clock, though: a capture is ~1.1s on a physical Android phone. That is
260
+ cheap next to a hierarchy read (~2.4s — see below) but it is not zero, so screenshot the
261
+ transitions worth reviewing rather than every step.
262
+
263
+ **Most of a run's time is reading the UI hierarchy.** Every selector command (`tap`, `text`,
264
+ `find`, `assert`, `swipe --on`) costs one read; measured on a physical mid-range Android
265
+ phone, one read is ~2.4s, nearly all of it fixed per-invocation cost inside `uiautomator`
266
+ that does not depend on how complex the screen is. iOS is ~10x cheaper. So prefer one
267
+ `vk assert` over a `vk ui` you have to scan, batch a known flow with `vk batch` rather than
268
+ re-checking between every step, and don't add a redundant `vk ui` just to confirm what an
269
+ `assert` already proved.
270
+
271
+ On Android this is handled for you: verikun keeps an accessibility connection alive on the
272
+ device (the *companion*), which cuts a read to ~0.2s. The first read on a device costs ~5.8s
273
+ to set it up, then every read after is fast. Nothing to enable.
274
+
275
+ It holds the device's single `UiAutomation` connection while it runs, so Appium and Layout
276
+ Inspector cannot attach. If the user needs those, tell them `VERIKUN_COMPANION=0` or
277
+ `vk companion stop` — don't disable it pre-emptively. A failure never breaks a run: verikun
278
+ falls back to the slower stock read on its own.
279
+
280
+ **Remember identifiers across runs.** After a flow succeeds, save the selectors
281
+ you found to memory — the mapping from human intent to selector, plus the screen
282
+ and step order, e.g.:
283
+
284
+ > Signup flow: "Get Started" → `@get_started`; intro slides → `@continue_btn`
285
+ > (tap ×2); plan picker → `text:"Free trial"`; account form → `@email_input`,
286
+ > then submit with `text:"Create account"`.
287
+
288
+ Next time a similar request comes in, **reuse the remembered selectors directly**
289
+ instead of re-inspecting from scratch — fewer round-trips, fewer tokens, faster
290
+ runs. Re-verify cheaply with `vk assert` / `vk find`; only fall back to a full
291
+ `vk ui` when a remembered selector stops resolving (the app changed — then update
292
+ the memory). Auto-healing selectors make remembered identifiers resilient to
293
+ small label/casing changes.
294
+
295
+ ## Batch a known flow into one call
296
+
297
+ When you already know the steps (e.g. from a remembered flow), run them as a single
298
+ `vk batch` instead of one tool call per command — one process, far fewer
299
+ round-trips. Pipe newline-separated commands on **stdin**, or pass `--file <path>`:
300
+
301
+ ```sh
302
+ vk batch <<'EOF'
303
+ launch com.example.app
304
+ text @email_input "user@example.com"
305
+ text @password_input "hunter2" --enter
306
+ assert text:"Welcome back" --wait 8s
307
+ run archive login-smoke
308
+ EOF
309
+ ```
310
+
311
+ Each line runs **exactly as if called standalone** — same [auto-wait](#auto-wait),
312
+ same recording as a test-run step, same exit codes. The batch **streams each result
313
+ to stdout, then stops at the first non-zero exit and propagates that code**, so a
314
+ failed `tap`/`assert` halts the flow (its screenshot + hierarchy are captured in the
315
+ run, like any failed step). Blank lines and `#` comments are skipped, and the
316
+ `batch` call's `--device` / `--ios` / `--android` / `--json` apply to every line.
317
+
318
+ Reach for it once a flow is *known*; keep using single commands while you're still
319
+ **discovering** a screen (you need `vk ui` between steps anyway). If a batch halts,
320
+ read its stderr line (`batch stopped at line N (…)`) to see which command failed,
321
+ fix that selector, and re-run.
322
+
323
+ ## Run a natural-language test (vk ai)
324
+
325
+ `vk ai <file>` is the inverse of driving the device yourself: instead of you
326
+ issuing `tap`/`text`/`assert` and inspecting between them, it hands a plain-English
327
+ test file to a model that **compiles it once into a deterministic plan**, then
328
+ replays that plan with **no model calls on the happy path**. The model is woken only
329
+ to *repair* a step whose selector stops resolving, and a green run caches the repaired
330
+ plan so the next run is free. Reach for it when the task is "run this whole English
331
+ test and give me a report"; keep using single commands while you're still
332
+ *discovering* a screen.
333
+
334
+ ```sh
335
+ vk ai onboarding.md # compile (first run) or replay (cached)
336
+ vk ai onboarding.md --show-plan # print the compiled plan IR, do not run
337
+ vk ai onboarding.md --max-cost-usd 0.50 # tighten the spend cap (default $3)
338
+ vk ai onboarding.md --timeout 5m # tighten the run timeout (default 15m)
339
+ ```
340
+
341
+ - Needs `ANTHROPIC_API_KEY` (Claude models) or `OPENAI_API_KEY` (OpenAI models) — **or no key
342
+ at all** with `--model codex-cli` / `--model cursor-cli`, which drive an already-logged-in
343
+ `codex` or `cursor-agent` CLI off your ChatGPT / Cursor subscription (run `codex login` or
344
+ `cursor-agent login` once; verikun just needs the binary on PATH). Default model
345
+ `claude-sonnet-4-6`; `--model` switches model **and** provider — Anthropic
346
+ (`claude-haiku-4-5` · `claude-sonnet-4-6` · `claude-opus-4-8` · `claude-fable-5`),
347
+ OpenAI (`gpt-5.4-mini` · `gpt-5.4` · `gpt-5.5` · `gpt-4.1` — cheaper than the default sonnet,
348
+ and non-reasoning, so `--effort` does not apply to it), or a CLI backend (`codex-cli` · `cursor-cli`).
349
+ - For the CLI backends, spend is on your subscription, not per token, so the cost line reads `$0`
350
+ and `--max-cost-usd` / `--cost-override` are no-ops (the run is still bounded by repairs +
351
+ `--timeout`). Each CLI chooses its own underlying model, and is run read-only in a scratch
352
+ directory so it never touches your working tree. Plans are provider-agnostic, so one compiled
353
+ by any model replays for free under a CLI backend; use `--recompile` to force a fresh compile
354
+ when comparing providers.
355
+ - The plan expresses **conditions** (`if-present`, for optional interstitials like a
356
+ permission dialog) and **bounded loops** (`repeat … until`, e.g. scroll-until) —
357
+ control flow `vk batch` cannot, so a flaky popup or a scroll-to-find no longer breaks
358
+ the flow. An `if-present` guard **waits for its selector to settle** (at least two looks
359
+ at the screen) before deciding the optional UI is absent, so a dialog that animates in a
360
+ beat after the transition is still caught. An absent guard costs about one extra UI dump;
361
+ `VERIKUN_GUARD_SETTLE_MS=0` restores the old single-shot probe.
362
+ - **Progress streams to stderr** (never silent in CI); **stdout is the report path**
363
+ (`--json` for a structured summary). It records like any flow, so it ends with the
364
+ same JUnit + HTML report — including the token/cost line and **suggested improvements**
365
+ you can fold back into the English to stabilize the test and cut future tokens.
366
+ - **Review screenshots are automatic.** The compiler sprinkles `screenshot` steps around
367
+ transitions (and inside loops) so the report shows a before/after visual trail for
368
+ post-run review — dumped, never read back by the model, so they add no token cost on
369
+ replay and never gate the test (a capture that hiccups is logged and skipped, not a
370
+ failure).
371
+ - **Bounded by default:** the run aborts if the estimated spend crosses **$3**
372
+ (`--max-cost-usd`) or the wall-clock passes **15m** (`--timeout`), so a runaway
373
+ compile/repair loop can't spend or hang without limit.
374
+ - Exit `0` pass · `1` a step failed (or the budget/timeout was hit) · `2` usage · `3` environment
375
+ (e.g. the model's API key — `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` — unset, or the `codex` /
376
+ `cursor-agent` CLI missing / not logged in for `--model codex-cli` / `cursor-cli`).
377
+ - **`3` also means the device toolchain is broken**, checked *before* anything is compiled
378
+ (missing `adb`/`idb`, no device, an ambiguous target) and again if it breaks mid-run. Treat
379
+ it as "fix the machine", never as a failing test — the message carries the install hint.
380
+
381
+ ## Run a suite of tests (vk suite)
382
+
383
+ When the task is "run all the tests" / "run the test directory", use
384
+ `vk suite <dir>` instead of looping `vk ai` yourself:
385
+
386
+ ```sh
387
+ vk suite tests/ --app com.example.app # reset app data between tests
388
+ ```
389
+
390
+ - Runs every `*.md` in the directory (lexicographic — `01-…`, `02-…` sequences
391
+ them; `README.md` is skipped) through the `vk ai` engine; all `ai` flags apply.
392
+ - `--app <id>` clears the app's data before each test (iOS: force-stop only).
393
+ Without it, each test must self-isolate (e.g. start with `launch <pkg> --clear`).
394
+ - A failing test doesn't stop the suite. stdout is the suite directory
395
+ (`./.verikun/suites/<id>/` with `index.json` + `index.html` linking every
396
+ test's report); **exit 1 if any test failed** — so it gates CI directly.
397
+ - **`--retries N`** (default `0`) re-runs a failed test up to N times. A later
398
+ pass recovers the suite (exit `0`) and surfaces a warning — failed-attempt
399
+ archives stay linked via `attempts` / suite `warnings`. An **environment break is
400
+ retried too** (a dropped `--server` connection, a device re-enumerating), with a
401
+ short backoff and a warning per blip; only a **budget abort** and a **usage error**
402
+ (exit `2`) are never retried, since a rerun cannot change either.
403
+ - **A broken *environment* does stop it: exit `3`.** If a test dies from an environment
404
+ error the toolchain is re-probed; only if it's still broken — and no retries remain —
405
+ does the suite abort (so a one-off flaky dump doesn't kill the run). The tests that
406
+ never ran are listed in `index.json`'s `aborted.notRun` and in the HTML banner — they
407
+ are **not** counted as failures. So `3` = fix the machine and rerun; `1` = a real
408
+ regression to investigate.
409
+
410
+ ## Drive a remote device (--server)
411
+
412
+ If the device is attached to another machine running `vk server`, point
413
+ `vk ai` / `vk suite` / `vk install` at it — everything else works identically
414
+ (same reports, same exit codes; the server's device/platform apply):
415
+
416
+ ```sh
417
+ export VERIKUN_SERVER=http://100.64.0.7:8391
418
+ export VERIKUN_SERVER_AUTH_KEY=<key printed/configured by the server>
419
+ vk install ./app-debug.apk --server "$VERIKUN_SERVER" # server needs --allow-install
420
+ vk suite tests/ --app com.example.app --server "$VERIKUN_SERVER"
421
+ ```
422
+
423
+ A wrong URL/key fails fast with exit 3; `409` means another run holds the
424
+ device. To expose a device from THIS machine: `vk server --allow-install`
425
+ (add `--bind <addr>` to leave loopback; auth key auto-generates if unset).
426
+
427
+ ## Test runs & reports
428
+
429
+ Every action is **recorded into a test run** — one auto-starts on your first
430
+ action, no setup needed. Each command becomes a step with its timing, the
431
+ selector + identifier it resolved through, and pass/fail. When a step fails, `vk`
432
+ automatically captures a screenshot **and** the UI hierarchy of that page.
433
+ `vk run archive` also captures a session-scoped device-log dump into
434
+ `artifacts/logcat.txt` by default, plus an app-scoped dump into
435
+ `artifacts/logcat-app.txt` (shown in the HTML accordion) when the run launched
436
+ an app. You can still run `vk log <package>` mid-run to attach a snapshot to a
437
+ step. Opt out on green runs with `--no-logs` / `VERIKUN_NO_LOGS` (failures still
438
+ capture).
439
+
440
+ - `vk run status` — the current run's steps and outcomes
441
+ - `vk run archive [name] [--no-logs]` — finish the run: writes a **JUnit XML** + a
442
+ self-contained **HTML report** (screenshots, captured hierarchies, device log,
443
+ and the identifiers used) to `./.verikun/runs/<id>/`, and exits non-zero if any
444
+ step failed — so it gates CI
445
+ - `vk run clear` — discard the run, no report
446
+ - `vk run start [name]` — begin a fresh named run explicitly (optional)
447
+
448
+ An implicit run **rolls over automatically** when the context changes — a
449
+ different device, a different `VERIKUN_SESSION`, or 30 min idle
450
+ (`VERIKUN_RUN_IDLE_MIN`, 0 disables): the stale run is archived and a fresh one
451
+ starts, so unrelated sessions never merge into one report. A run you named with
452
+ `vk run start` is sticky to idle (only a device/session change rolls it over).
453
+
454
+ When the task is "run/verify flow X and give me a report", just drive the flow
455
+ and end with `vk run archive` — the report *is* the deliverable. **Drop a
456
+ `vk screenshot` around each significant transition** (before/after a navigation tap,
457
+ a submit, a screen change) so the report carries a visual trail for post-run review —
458
+ these are write-only evidence, so don't read the PNGs back (see [Be frugal](#be-frugal-text-over-images-and-remember-identifiers)).
459
+ A failing step already auto-captures its own screen + hierarchy on top of that. The
460
+ archived `run.json` records which selector resolved each step, so it doubles as the
461
+ identifier memory described above. Set `VERIKUN_NO_RUN=1` to disable recording.
462
+
463
+ ## Improve verikun (report friction upstream)
464
+
465
+ When **verikun itself** — not the app, not your selector — is the friction (a model heal on
466
+ a *cached* replay: `[ai] plan cache hit` + a repair, or `"cached": true` with
467
+ `modelRepairs > 0` in `--json`; a `drifted, not repaired` give-up; or a recurring gotcha in
468
+ vk's own operation), use the **`suggest-verikun-improvement`** skill. It drafts a light,
469
+ TL;DR-first suggestion — **redacted** of every app-under-test specific (package, on-screen
470
+ text, selector values, test prose, logs) — for you to **review before it's submitted** to
471
+ `ddikman/verikun`. Don't hand-roll the issue: that skill owns the redaction and the
472
+ draft-first flow.
473
+
474
+ ## Exit codes — rely on these for control flow
475
+
476
+ - `0` success / found / assertion passed
477
+ - `1` not found / assertion failed / wait timeout
478
+ - `2` usage error **or ambiguous selector** (refine it or add `--index N`)
479
+ - `3` environment error (no device, adb/idb missing, hierarchy dump failed) — for
480
+ `ai`/`suite`/`install`/`server` the toolchain is verified up front, so this arrives
481
+ immediately with an install hint rather than mid-flow
482
+
483
+ ## Gotchas
484
+
485
+ - **Disable animations once** for reliable dumps: `vk doctor --fix`. Live
486
+ animations can make `vk ui` flaky (it already retries 3×).
487
+ - **Ambiguous selector → exit 2**, never a random tap. `vk` prints the candidate
488
+ matches; add `--index N` or use a more specific selector.
489
+ - **Indexes are per-snapshot.** `vk tap 3` taps `[3]` from the *latest* dump;
490
+ prefer `@id` / `text:` selectors for stability across screens.
491
+ - **Text starting with `-`:** put `--` first → `vk type -- "-50% off"`.
492
+ - **`vk device set` from a plain shell stays applied.** Inside `batch`/`ai`/`suite` it is
493
+ restored automatically even if the flow dies, but a one-off `vk device set airplane=on`
494
+ is yours to `vk device reset` — don't strand someone's phone offline.
495
+ - **One device auto-resolves.** Multiple → pass `-d <serial>` or set `VERIKUN_DEVICE`.
496
+ - **`vk text` opens the keyboard.** Use `--enter` to submit, or `vk back` to
497
+ dismiss it before re-inspecting (it can cover elements).
498
+ - **Unicode/emoji** may not type via `adb input text` (an Android limitation);
499
+ ASCII is reliable.
500
+ - **`vk log` is a snapshot, not a stream** — it dumps recent lines and exits.
501
+ Scoping with a `package` filters to that app's live process; once the app has
502
+ **crashed** its process is gone, so `vk log <pkg>` falls back to system-wide
503
+ logs (where the crash trace still is). The logs are **raw device output** and
504
+ can contain anything the app logged — including secrets — so treat archived
505
+ reports accordingly (`VERIKUN_NO_RUN=1` disables recording).
506
+ - **Special characters type fine.** Emails and symbols (`@ . + _ - / = : , ; ! # % & …`)
507
+ go in verbatim — `vk` backslash-escapes every device-shell metacharacter before
508
+ `adb input text`, so `vk text @email "bob+tag@mail.com"` lands the whole address,
509
+ not just `bob`. **Quote the value** when you build the command in a shell (or feed
510
+ it via `vk batch`/stdin, which uses no host shell) so your *own* shell can't split
511
+ or drop the `@`/`#`/`&` before `vk` sees it.
512
+ - **iOS** (`--ios`): full parity with Android — `ui`/`find`, `tap`, `text`/`type`,
513
+ `swipe`, `key`, `assert`, `screenshot`, `launch`/`stop` — on simulators and
514
+ physical devices. Interaction + hierarchy come from `idb` (`brew install
515
+ idb-companion` + `pip install fb-idb`); simulator screenshots/launch/stop/logs
516
+ use `xcrun simctl`. Run `vk doctor --ios` to check the toolchain. Caveats: `clear`
517
+ is unsupported (no per-app reset), `current` is `(unknown)`, device logs are
518
+ simulator-only, and `device set` is partial — `dark`/`font-scale` work on a simulator
519
+ while `airplane`/`rotation` do not exist there at all (`vk device caps --ios`). iOS
520
+ accessibility ids are often unset, so prefer `text:`/`desc:` selectors there.
521
+
522
+ ## Worked example — verify a login flow
523
+
524
+ ```sh
525
+ vk doctor --fix # deterministic UI
526
+ vk launch com.example.app
527
+ vk text @email_input "user@example.com" # field lookup auto-waits up to 5s
528
+ vk text @password_input "hunter2" --enter
529
+ vk assert text:"Welcome back" --wait 8s # poll up to 8s, then assert → exit 0 = logged in
530
+ vk assert @error_banner --gone # exit 0 → no error banner shown
531
+ vk run archive login-smoke # -> ./.verikun/runs/<id>/report.html (+ report.xml)
532
+ ```
533
+
534
+ Note there's no explicit `wait @email_input` — `text` auto-waits for the field.
535
+ Check `$?` after `assert`/`wait`/`find` to branch on success vs failure.