Running spotify_play against the real phone showed a tap can be delivered to the app and then never acted on (the dispatcher lists the Spotify window as the touch target, and the screen does not move). The run log also showed step durations were printed as 0s while the recorded ones were correct. - wait steps accept "retry N": on timeout, re-run the step above and wait again, up to N extra times. Only a wait may use it, it needs a step above it, and it can only press something already matched by text. spotify_play uses it on the search box and on the playback check. - The success log line printed the duration before the deferred assignment ran. It now records the duration first. - uiautomator dump starts a JVM on the phone, and the wait helpers polled every 400ms. ui.PollInterval is now 1500ms, which cuts that load on a phone that is already short of memory. docs/phase-3-spotify-recipe.md logs attempts 7-10: the recipe now reaches tap_row and picks "Honky Tonk Women" at (540,878), but the app stops acting on input on this loaded phone, so the end-to-end run is still outstanding.
Artemis Control
Artemis is the phone's hands. It drives the Pixel 8a screen from .13 over
Tailscale using ADB wireless, so the Hub can do things no deep link allows.
The build spec is AGENTS.md. Follow the phases in section 11 in
order.
Status
| Phase | State |
|---|---|
| 1 — Prove the ADB link | ✅ proven — see docs/phase-1-adb-link.md |
| 2 — Act primitives | ✅ done — tap by text, type, scroll, back, home, launch |
| 3 — Spotify recipe | 🟡 built — recipes file, guards, spotify_play; the live end-to-end run is still to do, see docs/phase-3-spotify-recipe.md |
| 4 — Song radio | not started |
| 5 — Maps recipe | not started |
| 6 — Action API | not started |
| 7 — Resilience | not started |
| 8 — Documentation | not started |
Layout
cmd/artemis/ command line entry point
internal/adb/ timeout-bounded wrapper around the adb binary
internal/ui/ read the screen (uiautomator dump) and act on it (input)
internal/recipe/ the recipes file format and its runner
recipes/ one hand-editable file per task
logs/ run log (JSON lines, not committed)
docs/ phase evidence and notes
Build and test
Go is not installed system-wide on .13; use a one-off nix shell. Nothing is
added to nixos-rebuild and no system module changes.
nix shell nixpkgs#go -c go build -o bin/artemis ./cmd/artemis
nix shell nixpkgs#go -c go vet ./...
nix shell nixpkgs#go -c go test ./...
Use
The phone address defaults to 100.101.49.17. Override with ARTEMIS_PHONE,
pin a serial with ARTEMIS_ADB_SERIAL, point at a different adb with
ARTEMIS_ADB, point at another recipes directory with ARTEMIS_RECIPES, and
move the run log with ARTEMIS_LOG.
./bin/artemis connect # scan for the wireless-debugging port, then connect
./bin/artemis devices
./bin/artemis dump # text nodes and bounds
./bin/artemis keyguard # lock-screen state; exits 1 when locked
./bin/artemis launch com.spotify.music
./bin/artemis tap "Search" # find by text, tap its centre (FR-4)
./bin/artemis type "Rolling Stones Honky Tonk"
./bin/artemis scroll down
./bin/artemis back
./bin/artemis home
./bin/artemis demo # Phase 2 DoD: launch an app, tap a named element
Recipes
A recipe is a plain-text file in recipes/ that Sam edits by hand. Steps name
what to find on screen; there are no coordinates anywhere.
./bin/artemis recipes # list them
./bin/artemis run spotify_play query="Rolling Stones Honky Tonk"
Read docs/phase-3-spotify-recipe.md for the
file format, the verbs, and why list rows need their own step.
Rules that the code enforces
- Find by text, never fixed coordinates.
tapresolves a text match to bounds and taps the centre;tap_rowscores result rows against the query and taps the winning row's centre. - Never tap blindly. A missing or disabled element is a loud failure, not a
guessed coordinate. A named bad screen (
guard) fails with its own reason. - Never touch a locked phone. A secure keyguard stops Artemis with
phone is locked; unlock it and retry. No PIN is stored, read or typed (AGENTS.md section 13 Q2). - Wake first. Every acting command wakes the screen before it acts (FR-7), and it checks the keyguard only after the screen is on.
- Bound every command. No adb call runs without a timeout, and a recipe has a per-step and a whole-run timeout (FR-11).
- Log every run. Each run appends a JSON line to
logs/runs.jsonl(FR-10).