Sam Rolfe 3ef11da808 fix: address the Maps search box by id, and re-prove Phase 5
The Maps omnibox hint rotates: it read "Search here" in the Phase 5 run and
"Try petrol stations, ATMs" in the AUD-128 re-verification, so the recipe's
text match on it failed. The resource id does not rotate.

- internal/ui: add Find/FindID and the "id:" selector; Find dispatches an
  id: selector to the resource id and everything else to text as before.
- internal/recipe: wait/tap/wait_gone/tap_if and the tap helper use Find, so a
  recipe may name a resource id. Guards stay text-only.
- recipes/maps_navigate.recipe: wait/tap id:search_omnibox_text_box.
- tests for the id selector; docs and README updated.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-17 09:12:12 +11:00
2026-10-16 11:36:03 +11:00

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 ✅ done — spotify_play plays the right song end to end (13 steps, 35s), see docs/phase-3-spotify-recipe.md
4 — Song radio ✅ done — spotify_radio starts a real radio (19 steps, 57s); the run log is now bounded at ~1 MB × 5, see docs/phase-4-song-radio.md
5 — Maps recipe ✅ done — maps_navigate opens a real search and the result list names the place (8 steps, 28s), see docs/phase-5-maps-recipe.md
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"
./bin/artemis run spotify_radio query="Rolling Stones Honky Tonk"
./bin/artemis run maps_navigate query="Melbourne Museum"

Read docs/phase-3-spotify-recipe.md for the file format, the verbs, and why list rows need their own step, docs/phase-4-song-radio.md for the context menu, menu_row, and the two ways a radio run fails on purpose, and docs/phase-5-maps-recipe.md for the Maps labels, the expect_change check, and the one ordering rule it depends on.

Rules that the code enforces

  • Find by text, never fixed coordinates. tap resolves a match to bounds and taps the centre; tap_row scores result rows against the query and taps the winning row's centre; menu_row opens the action menu of that same row, from the button inside it. Where a visible label rotates (the Maps search box), a step names the stable resource id with the id: selector — still a named match, still no coordinates.
  • 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, and a step may name its own failure with else "...".
  • Detect a wedged app. An app can stop consuming input while it keeps drawing: Android delivers the tap, the app never acts on it. expect_change fails with the app stopped consuming input when the screen does not change after the previous step, instead of blaming the label the next step waited for.
  • 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).
  • Confirm playback from the media session, not the screen. Spotify's transport button keeps the label "Play" while music plays; the playing step asks Android what is actually playing and checks the track title.
  • 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). The file rotates at about 1 MB, keeping the recent runs (FR-10 is an audit trail, so history is bounded, never deleted wholesale). Tune it with ARTEMIS_LOG_MAX_BYTES and ARTEMIS_LOG_KEEP.
Description
Remote phone screen control over Tailscale with ADB.
Readme 322 KiB
Languages
Go 100%