Commit Graph

6 Commits

Author SHA1 Message Date
a7c07c645e feat: Phase 5 — maps_navigate, and read the screen before the input
The Maps recipe: open the search box, type the place, and confirm the
result list names it. Every label comes from a live uiautomator dump
(search_omnibox_text_box, typed_suggest_container, the account sheet),
never from a guess. Text only, one step_timeout/total_timeout, guards for
the account sheet.

The first live run exposed an ordering bug in expect_change. A dump is not
instant, so a baseline taken after the input already showed the app's
response and the check could never see a wedge; it failed against a
healthy Maps. The engine now reads the screen before the step above runs
and hands that signature to the check. Each retry re-reads it.

Live: maps_navigate ok in 27.6s, result list names "Melbourne Museum".
The wedge reason and its bounded retry are demonstrated with a probe
recipe whose input changes nothing; a real wedge could not be forced, and
docs/phase-5-maps-recipe.md says so plainly.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-16 16:18:06 +11:00
36b4dd2391 feat: Phase 4 — spotify_radio, and a bound on logs/runs.jsonl
spotify_radio opens a track's own context menu and starts its song radio.
The labels came from a fresh uiautomator dump on the phone, not from a guess:
the entry is "Go to song radio" and it sits below the fold, so the recipe
scrolls before it matches.

Recipe format:
- menu_row "{query}" finds the row like tap_row, then presses the action
  button inside that row's own subtree (ui.Row.Action), so the More options
  button of any other row is never a candidate. It records the row as {row}.
- a step can name its own failure: wait ... else "reason". The named reason
  comes first and the screen's labels still follow (FR-8).
- scroll up|down moves the current list.
- playing with no title waits for the media session to be PLAYING. A radio
  changes track every few minutes, so a title match would fail on a working
  radio; spotify_play still passes the title it wants.

ui:
- pickBestMatch ignores a match with no usable bounds. An element scrolled out
  of its list reports [0,0][0,0], and tapping its "centre" would be a blind tap
  in the corner. A wait now only succeeds once the element is on screen.

Run log (FR-10): appendRunLog rotates runs.jsonl when a write would pass
ARTEMIS_LOG_MAX_BYTES (default 1 MiB), keeping ARTEMIS_LOG_KEEP (default 5)
files. History is bounded, never deleted wholesale, and the per-entry cap of
6 screen labels of 60 characters is unchanged.

Live evidence in docs/phase-4-song-radio.md: ok:true in 56.6s, the menu that
opened belonged to the pressed row, the station name "Honky Tonk Women Radio"
on the phone, and the radio playing past its seed track (Layla) per the media
session and dumpsys audio. Both new failure modes were demonstrated failing
with distinct reasons. The mini-bar's transport label stays "Play" while
playing on this build, which is why playback is confirmed from the media
session.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-16 15:25:50 +11:00
c178f92851 feat: confirm playback from the media session, and stop demoting the top row
Phase 3 DoD reached end to end: `artemis run spotify_play query="Rolling
Stones Honky Tonk"` played the right song in 13 steps / 35.3s, with playback
paused first so the pass proves the row tap started it.

Two things stopped it before that, both found on the real phone:

- The playback check asserted on an on-screen "Pause" label that Spotify
  never shows: its transport button keeps the label "Play" while music is
  playing. The recipe now ends with a `playing` step that reads the app's
  media session (state PLAYING) and confirms the track title contains the
  query words. A failure reports the state and the track it found.
- tap_row pressed the wrong row. Once a track is loaded its row carries a
  `Paused` state indicator, and counting that content description as row text
  dropped the top hit below an identical duplicate further down, so the tap
  went to y=1443 instead of y=878. Rows are now scored on their title and
  subtitle only (state lives in content descriptions), and scores within
  rowScoreTolerance are treated as equal so the higher row wins, because
  Spotify already ranks by relevance.

internal/ui/rows_test.go reproduces that results screen, including the state
indicator and the duplicate row.

docs/phase-3-spotify-recipe.md records the successful run, the independent
media-session and audio-output reads, and the failure that preceded it.
2026-10-16 14:32:24 +11:00
ab69f37aa4 feat: bounded retry for a dropped tap, and poll the screen more gently
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.
2026-10-16 14:12:01 +11:00
767844cacc fix: record step durations, and pin the foreground app before matching text
Two defects found by running spotify_play against the real phone.

- Step durations were always zero in the run log: executeStep returned a copy,
  so the deferred assignment never reached the caller (FR-10). It now returns a
  named value, with a regression test.
- The phone was sitting on the Android Settings app, and wait "Search" was
  satisfied by Settings' own "Search Settings" entry, so the recipe tapped the
  Settings search bar. New "foreground" step waits until the recipe's app really
  is in front; spotify_play uses it right after launch.

docs/phase-3-spotify-recipe.md now logs all six live attempts and states
plainly that the end-to-end playback run has not completed: the phone is
showing a Settings "Verify that it's you" biometric prompt, which needs Sam and
is left untouched.
2026-10-16 13:29:07 +11:00
2604fea483 feat: Phase 3 recipes file, spotify_play, and locked-screen refusal
Phase 3 of AGENTS.md section 11.

Recipes file (FR-5)
- recipes/ holds one hand-editable file per task; recipes/spotify_play.recipe
  is the first one.
- internal/recipe parses directives, guards, parameters and steps, and runs
  them with a per-step and a whole-run timeout (FR-11).
- Steps name text ("tap the element whose text is Search"), never coordinates.
- guards name screens Artemis must never tap on, so a signed-out or
  first-run Spotify fails with its own reason (FR-8).
- Every run appends task, params, steps, result and duration to
  logs/runs.jsonl (FR-10).

spotify_play
- launch, search, play, then wait for the transport button to say "Pause".
- internal/ui/rows.go adds tap_row: a result row is a clickable container with
  a title and a subtitle, so a one-line search suggestion is not a result, and
  the tap lands on the row's centre rather than on its "More options" button.
  This fixes a real wrong tap: a plain text tap hit a 126x12 pixel
  "More options for song Honky Tonk Women - Live" button.

Locked screen (AGENTS.md section 13 Q2, decided)
- Artemis fails loudly on a secure keyguard and never types a PIN. No secret is
  stored. A non-secure keyguard is still dismissed.
- internal/ui/keyguard.go reads the KeyguardServiceDelegate block plus
  cmd lock_settings is-user-secure.
- Every acting command wakes the screen before it checks the keyguard: a dozing
  phone reports showing=false, because the lock screen is not drawn yet.
- AGENTS.md records the decision and that storing a PIN for auto-unlock is
  rejected for now.

Verified live: the phone locked itself mid-session, and keyguard, tap and
run spotify_play all refused with "phone is locked; unlock it and retry".
The live end-to-end playback run still needs an unlocked phone.
2026-10-16 13:24:22 +11:00