# Ramus — for AI agents

This file helps AI agents use Ramus. Fetch it again whenever you start a task
involving Ramus; it is the source of truth for what exists.

Ramus (https://ramus.dev) builds Android pull requests and runs them in live
emulators. Humans get a preview link on every PR. Agents get a CLI that can
create emulator sessions and drive them: tap, swipe, type, read the UI tree,
take screenshots, read logcat.

Do not assume API, CLI, or MCP support unless it is listed in this file.

## Authentication

Two paths:

- **No account — `npx ramus-cli trial`.** Mints a 60-minute sandbox key and
  stores it locally (`~/.ramus`, 0600); every later command picks it up.
  Nothing to export, nothing to paste. Limits: 1 session at a time, 30-minute
  idle timeout, 150 MB APK, no GitHub features, 3 trials per IP per day, and a
  fleet-wide cap on concurrent trials (retry later if full).
- **Real key** in the `RAMUS_API_KEY` environment variable. A human creates
  one at https://ramus.dev/settings (API keys tab). Keys act as their user
  and can be revoked there.

- Never ask the user to paste a key or any token into chat. Ask them to set
  `RAMUS_API_KEY` in your environment.
- Do not send keys as URL parameters. The CLI sends them as headers.

## Quick start (no GitHub required)

You do not need GitHub for anything here:

```
npx ramus-cli trial                 # or: export RAMUS_API_KEY=... for a real key
npx ramus-cli session start --apk app-debug.apk --wait   # -> watchUrl
npx ramus-cli session start --demo --wait                # no APK? the demo app
npx ramus-cli snapshot             # UI tree with ref handles + a gen counter
npx ramus-cli tap e3 --gen 1       # act by ref (gen from that same snapshot)
npx ramus-cli screenshot --out screen.jpg
npx ramus-cli session end          # -> watchUrl again, for the wrap-up
```

**`session start` returns a `watchUrl`. Give it to the user immediately** —
it opens the live emulator in their browser, no account needed, and they can
watch you work or take over and tap themselves. **The link works for 72
hours**: while the session is live it shows the live screen; after the
session ends (the session itself idles out ~30 minutes after the last
input), opening the link relaunches the app fresh — so the user can come
back later and still try the app. **Repeat the watchUrl at the end of your
task** so it is the last thing the user sees.

`session start` prints a session id; other commands use the most recent
session by default or accept `--session <id>`. All commands emit JSON on
stdout; errors are JSON with exit code 1.

## The driving loop

Observe → act → observe. Never act on a stale observation.

1. `snapshot` returns visible elements, each
   `{ref, text?, contentDesc?, resourceId?, class, clickable?, x, y, w, h}`
   with `x y w h` normalized to [0,1] (top-left origin), plus `gen`. Only
   on-screen elements are returned (`--include-offscreen` adds scrolled-away
   ones, flagged `offscreen: true`). Layout containers and decoration are
   pruned, a tappable thing and its caption are one element, and WebView
   prose comes back as **paragraphs** with the links inside them also listed
   separately so they stay tappable. Prefer acting by `ref`, and prefer
   elements marked `clickable: true`. Coordinates are display-normalized even
   inside a dialog or popup (`screen` is always the full display).
   - `snapshot --find "text"` filters to elements whose text, contentDesc, or
     resourceId contain the substring (case-insensitive) — use it instead of
     reading whole screens. FABs and icon buttons carry their label in
     `contentDesc`, not `text`.
   - `snapshot --raw` returns the unparsed uiautomator XML if the tree looks
     wrong or incomplete.
   - To read text (an article, a message), read the paragraph elements'
     `text` in order — `snapshot --find "<a phrase you expect>"` narrows a
     long page to the paragraph you want. Long pages return many elements;
     `--find` is the normal way to read them. Confirm with `screenshot` if
     anything looks off.
2. Prefer **act-and-verify**: `tap e5 --gen 3 --expect "Items: 1"` (or
   `--expect-gone "Add a grocery item"`) re-snapshots after the tap and exits
   1 if the outcome didn't happen — a tap that lands but changes nothing is
   the failure mode you can't see otherwise. `type --ref` reads the field
   back after typing and errors on IME autocorrect mangling (`--no-verify`
   to skip). Otherwise, after every `tap`/`type`/`press`, take a fresh
   `snapshot` before deciding the next action. Refs go stale when the screen changes; a stale ref returns
   an error telling you to re-snapshot. `tap` **refuses** refs that are not
   `clickable` (a container's center lands on whichever child sits there) and
   names the clickable element with the same label; `--force` overrides. It
   refuses off-screen refs too. `tap` echoes the element it hit.
3. `wait --stable` (two consecutive identical snapshots) after navigation;
   `wait --text "Welcome"` for a specific state (substring over text AND
   contentDesc). Both return a `gen` valid right then; `wait --text` also
   returns the matching refs (with `clickable` and `matchedField`) so you can
   tap without another snapshot. Any later `snapshot` supersedes them.
   To close the soft keyboard, `ramus press IME_HIDE` (a second `press BACK`
   would leave the app).
4. If the snapshot is empty or missing what you can see must be there (games,
   some WebView/Flutter screens), fall back to `screenshot` and tap by
   normalized coordinates: `tap 0.5,0.72`.
5. If the app crashes or hangs, `logcat --limit 200` before anything else.
6. Tap a text field (focus it), wait ~300ms for the keyboard, then `type`.

## Sharing a session with a human

The `watchUrl` from `session start` already lets anyone with the link watch
AND control the session in a browser — no account, and it keeps working for
72 hours (after the session ends it relaunches the app on click). You
normally need nothing else. `ramus session share` mints an extra link
(`--ttl 7d` for a longer one; trial keys are capped at 72h). Sharing also
guarantees the session's full 30-minute idle window — it never holds a
device longer than that, because a later click just relaunches the app.
`--revoke` cuts off every link. Do not post these links anywhere public.

(MCP: `android_share_session`.)

## Hot reload: the emulator as a local adb device

`npx ramus-cli adb` tunnels the session's adbd to `127.0.0.1:<port>` on the
machine running the CLI and `adb connect`s it. The cloud emulator then shows
up in `adb devices`, and every local toolchain works as if it were plugged
in — `adb install`, `adb logcat`, `adb reverse` (so Metro on this machine is
`localhost:8081` inside the emulator), `flutter run`, `gradlew installDebug`,
Android Studio.

`npx ramus-cli dev` is the shape-aware loop: it detects Expo / React Native /
Flutter / Gradle in the current directory, starts a session if none is live,
opens the tunnel, and runs the framework's dev process against the emulator.
Edits are applied live (Expo/RN Fast Refresh, Flutter hot reload via
`dev reload`); Gradle apps reinstall on `dev reload`.

```
npx ramus-cli dev [--apk app-debug.apk] [--shape expo|react-native|flutter|gradle] [--app-dir d]
npx ramus-cli dev reload [--full]
npx ramus-cli dev status | stop
npx ramus-cli adb [--port N] | --status | --close
```

Requires a local `adb` (Android platform-tools). The first Expo/RN/Flutter
run builds locally and takes minutes — poll `dev status`. Never run
`adb root`, `adb unroot`, `adb reboot` or `adb reverse --remove-all` against
the tunneled device: adbd restarts and the session ends. (MCP: not exposed —
a tunnel needs a local process.)

## PR previews (GitHub required, human-gated)

Verifying a pull request build requires the Ramus GitHub App on the repo.
**You cannot install it — that requires a human in a browser.**

- `npx ramus-cli status` reports key validity, whether the current repo
  (from `git remote`) has the app installed, and your live sessions. It
  always exits 0 with a state report and never prompts. `ramus --version`
  prints the CLI version.
- If the app is missing: `npx ramus-cli install` prints an install URL.
  Show it to the user and ask them to open it. Then
  `npx ramus-cli install --wait` polls until the installation lands
  (safe to re-run; reports `pending_org_approval` when an org owner still
  has to approve).
- Once installed: `npx ramus-cli session start --pr owner/repo#123` attaches
  to that PR's latest ready build.

Ramus PR comments contain a machine-readable marker line
(`<!-- ramus:api pull=... docs=... -->`) pointing at the pull's API resource.

## Do NOT use Ramus for

- Unit-testable logic — run the project's own test suite instead.
- Building release APKs from local changes — build locally (gradle/expo/
  flutter), then hand the APK to `session start --apk`; for iterating, use
  `dev` (hot reload) instead of rebuilding per edit.
- Installing the GitHub App or completing any OAuth flow — human-only.
- Long soak tests — sessions are ephemeral and idle out (~30 min without
  input).

## Limits

- Concurrent ad-hoc sessions per user: 2. `session start` returns an error
  naming the session to end when you hit it.
- Single-finger input only (no pinch/multitouch yet).
- Session tokens are short-lived and handled by the CLI; you never see them.

## Claude Code skill

A packaged skill with the full driving playbook is served at
https://ramus.dev/skill.md — save it into a project as
`.claude/skills/ramus/SKILL.md` and Claude Code triggers it automatically
when emulator verification comes up.

## Surfaces

- CLI: `ramus-cli` on npm (this page's commands).
- HTTP API: used by the CLI; not yet a stable public contract.
- MCP: remote server at `https://api.ramus.dev/api/mcp` (Streamable HTTP,
  stateless; `Authorization: Bearer` with your API key) exposing the same
  verbs as the CLI (`android_*` tools) for clients that cannot shell out.
  Note: local APK upload needs the CLI; MCP sessions start from a PR or a
  known APK URI.
