## Summary
- `formatDescribeTree` was branching on `source === "uiautomator"` to
pick nested rendering; the newer `android-devtools` source fell through
to flat mode and lost every node below depth 1. Extends the predicate to
cover both Android sources so the helper-service path renders with the
same hierarchy as the legacy `uiautomator dump` path.
- Bumps `packages/argent-private` to pick up the persistent Android
describe service that emits `source: "android-devtools"`, plus its
build-and-sign CI workflow.
- Wires the Android helper APK + `manifest.json` into the published
`@swmansion/argent` package via `bundle-tools.cjs`, and adds
`pack:mcp:local` / `build:android-binaries` so a local tarball can be
produced without going through `download:native-binaries`.
- Minor: tightens a couple of comments in `android-helper-install.ts`
and `format-tree.ts`.
This is connected to argent-private PR:
https://github.com/software-mansion/argent-private/pull/8
## Summary
- Move the JSON tree out of the describe tool's response and add an
in-between layer (`format-tree.ts`) that converts it to a sectioned text
rendering. On a typical iOS screen the new `description` field is ~6×
cheaper on the wire than the previous nested tree once braces, keys, and
indentation are counted.
- Public response shape becomes `{ description, source, should_restart?
}`. All existing pruning (Android v2 trimmer in `uiautomator-parser.ts`,
iOS frame clamp in `ios-ax-adapter.ts`) is untouched — the new layer
only re-presents what the platform adapters already produced.
- Header now spells out the coordinate space and the tap-point formula
so an agent can act on a frame without further docs.
## How it renders
Two modes picked automatically from the tree shape:
- **flat** (iOS ax-service): bucket children by `frame.y` into Top bar /
Body / Composer / Bottom; split the bottom zone into keyboard rows by
rounded y and classify each row (letters / shift / return /
globe-dictate).
- **nested** (Android uiautomator, iOS native-devtools fallback):
iterative DFS with depth indentation, preserving `clickable` /
`scrollable` / `scrollHidden` / `disabled` / `password` flags inline.
Example (Messages with keyboard up):
```
Source: ax-service
Mode: flat
Coordinates are normalized [0,1] fractions of the screen (x, y, width, height), not pixels — pass them straight to gesture-tap / gesture-swipe / gesture-pinch, which expect this same space. To tap an element, use its centre: tap_x = frame.x + frame.width / 2, tap_y = frame.y + frame.height / 2.
ROOT AXGroup (0.000, 0.000, 1.000, 1.000)
— Top bar —
AXButton "Messages" (0.040, 0.071, 0.109, 0.050)
AXButton "+1 (888) 555-1212" (0.267, 0.134, 0.466, 0.037)
— Body / content —
AXStaticText "iMessage  Encrypted" (0.040, 0.156, 0.920, 0.030)
...
— Bottom row 1: letters —
AXImage "Q" (0.012, 0.675, 0.098, 0.062)
...
```
## Files
- `packages/tool-server/src/tools/describe/contract.ts` — split into
`DescribeTreeData` (internal) and `DescribeResult` (public)
- `packages/tool-server/src/tools/describe/index.ts` —
`withDescription(data)` wrapper applied to both platform handlers
- `packages/tool-server/src/tools/describe/format-tree.ts` — the
renderer (new)
-
`packages/tool-server/src/tools/describe/platforms/{ios,android}/index.ts`
— return `DescribeTreeData`
- `packages/tool-server/test/describe-tool.test.ts` — assertions
migrated to `result.description` / `result.source` /
`result.should_restart`
- `packages/tool-server/test/describe-format-tree.test.ts` — renderer
unit tests (new): flat zoning, nested indentation + flags,
embedded-newline escaping, empty tree
## Test plan
- [x] `npm test` in `packages/tool-server` — 49 files / 482 tests
passing locally
- [x] `tsc --build` at workspace root — clean
- [x] Live MCP sanity check: `argent run describe --udid <booted>
--json` returns the new shape; tested against Messages (full keyboard
surface), Safari (chrome-only, expected), and a minimal in-process
WKWebView host. Existing pruning behaviour unchanged in every case.