Commit Graph

471 Commits

Author SHA1 Message Date
Ignacy Łątka 5ea8a9936a fix(boot-device): handle child spawn errors so tool-server doesn't crash
`attemptBoot` only listened on `child.on('exit', ...)`, so a `spawn` failure
(ENOENT — emulator binary missing/EACCES, transient FS hiccup) emitted an
`error` event with no listener and Node crashed the tool-server with an
uncaught exception while leaking the in-flight Map entry. Funnel the error
into the same `earlyExitError` race so the boot promise rejects cleanly and
`bootAndroid`'s `finally` clears the map.
2026-05-04 22:01:21 +02:00
Ignacy Łątka 074e6698c1 cleanup native profiler skill 2026-05-04 21:57:44 +02:00
Ignacy Łątka 02cef6d525 cleanup env inspector skill 2026-05-04 21:48:54 +02:00
Ignacy Łątka 5a5731c09b rename refs in tests 2026-05-04 21:45:19 +02:00
Ignacy Łątka e34cdf022a cleanup argent md 2026-05-04 21:45:04 +02:00
Ignacy Łątka ede247670d rename ios skill to match android 2026-05-04 21:44:52 +02:00
Ignacy Łątka b135b5e63a Revert "post-review fix: wrap iOS xcrun errors with verb prefix for parity"
This reverts commit 073c2195ea.
2026-05-04 21:33:46 +02:00
Ignacy Łątka 1b9de3f9f8 Merge remote-tracking branch 'origin/main' into feat/android-emulator-support 2026-05-04 21:17:57 +02:00
Ignacy Łątka fdc3e6d3e7 fix(test): prime adb in simulator-server blueprint test
CI Linux runners don't have `adb` on PATH, so the Android paths in
`simulatorServerBlueprint.factory` hit the real `ensureDep('adb')` probe
and threw `DependencyMissingError`. Locally the tests passed because
`adb` was on PATH. Prime the dep cache in `beforeEach` (matching the
existing pattern in boot-device.test.ts and describe-tool.test.ts) so
the dep check is a no-op and the test stays focused on dispatch
behaviour. Lazy-imported via dynamic `await import(...)` so check-deps.ts
does not load before the hoisted `vi.mock("node:child_process")` factory
has its `spawnMock` initialised.
2026-05-04 21:00:44 +02:00
Ignacy Łątka 6566ddae20 prettier 2026-05-04 20:54:15 +02:00
Ignacy Łątka fa7a4af17b post-review fix: skill docs catch up with cross-platform stop/describe
- argent-react-native-app-workflow Quick Reference still labelled
  stop-simulator-server / stop-all-simulator-servers as 'iOS only'
  (superseded by the cross-platform commits) and only listed the
  iOS run / log commands. Add Android counterparts.
- rules/argent.md session-end hint claimed stop-all-simulator-servers
  is 'for iOS targets'; the tool now tears down Android servers too.
- argent-device-interact described 'describe' as 'current simulator
  screen' — Android uses uiautomator; reword to be device-neutral and
  name both backends.
2026-05-04 18:46:43 +02:00
Ignacy Łątka 75f21880e7 post-review fix: agent docs recognize Kotlin Gradle DSL
Modern AGP templates default to *.gradle.kts (Kotlin DSL); the inspector
agent's classification rule and quality-control reference only listed
the Groovy variant. Update both to call out Kotlin DSL alongside the
Groovy form so a Kotlin-only project still classifies as native Android.
2026-05-04 18:46:18 +02:00
Ignacy Łątka 4a6e2fb47f post-review fix: cover ax-service blueprint Android-rejection gate
Test file covers nativeDevtoolsBlueprint and nativeProfilerSessionBlueprint
gates but axServiceBlueprint is the third iOS-only blueprint with the
same shape and was untested. Add the Android-rejection case and the
missing-options.device case for symmetry.
2026-05-04 18:45:50 +02:00
Ignacy Łątka 073c2195ea post-review fix: wrap iOS xcrun errors with verb prefix for parity
Android branches throw 'am start failed: …', 'adb install failed: …',
'open-url failed: …' — a uniform 'verb failed: detail' shape MCP
clients can pattern-match on. iOS branches let the raw 'Command failed:
xcrun simctl …' bubble out of execFile so the same client check works
on Android but not iOS. Add a small wrapXcrunError helper in
utils/format-error.ts and apply it to launch-app, restart-app,
reinstall-app, and open-url iOS handlers.
2026-05-04 18:45:27 +02:00
Ignacy Łątka 9abb6cff7f post-review fix: alwaysLoad + searchHint on list-devices and boot-device
Both are session-start essentials in the new Android workflow yet were
missing the discoverability flags peers have (screenshot, gesture-tap,
launch-app already set both). Add alwaysLoad and a searchHint covering
ios/android, simulator/emulator, avd, serial, udid keywords so
LLM-driven harnesses surface them when the user is choosing a target.
2026-05-04 18:44:23 +02:00
Ignacy Łątka aff4c534b6 post-review fix: restore .min(1) on run-sequence steps array
A pre-PR snapshot's schema rejected steps: [] up-front; somewhere in
this PR's restructuring the .min(1) was dropped, so an empty array now
silently no-ops with { completed: 0, total: 0, steps: [] } instead of
returning a clear schema error to the caller.
2026-05-04 18:43:48 +02:00
Ignacy Łątka 49e4bca558 post-review fix: split per-platform Services generic in dispatchByPlatform
LaunchAppServices and RestartAppServices declared { nativeDevtools }
as required even though Android's services() returns {} — a documented
lie that would type-check 'services.nativeDevtools.foo()' inside the
Android handler and crash at runtime. Extend dispatchByPlatform to
accept IosServices and AndroidServices as separate generics so each
branch types against its real shape. Reinstall-app/open-url/describe
keep the same shape on both sides; launch-app/restart-app split into
*IosServices (with nativeDevtools) and *AndroidServices (Record<string,
never>).
2026-05-04 18:43:29 +02:00
Ignacy Łątka bec8fd9d64 post-review fix: restart-app forwards optional Android activity
If the user launched a non-launcher activity (e.g. .SettingsActivity)
and then called restart-app, the Android branch silently bounced them
back to the package's default LAUNCHER activity because there was no
activity param at all — the resolver always picked the launcher. Add
the optional activity field to the schema/Params (mirroring launch-app's
shape and regex), and forward it through the same component-string
construction launch-app uses. Ignored on iOS.
2026-05-04 18:41:31 +02:00
Ignacy Łątka e705e26c13 post-review fix: reinstall-app Android uninstalls first for parity with iOS
iOS reinstall-app does 'simctl uninstall' then 'simctl install' so
app data and runtime permissions are wiped. Android did only
'adb install -r -d -g' which preserves data and previously-granted
permissions. Same tool name, divergent post-state. Run 'adb uninstall
${bundleId}' first (swallowing the not-installed case the way iOS
does), then the install. Update the description to match the new
unified semantics.
2026-05-04 18:40:54 +02:00
Ignacy Łątka ca8caf5581 post-review fix: gate adb at simulator-server blueprint factory for Android
The simulator-server binary shells out to adb internally for Android
targets. Without this preflight, a host without android-platform-tools
surfaces the failure as 'Timed out waiting for simulator-server to
become ready' (READY_TIMEOUT_MS = 30s) instead of the structured 424
DependencyMissingError other Android tools emit. The simulator-server-
backed tools (gesture-*, button, keyboard, screenshot, rotate, run-
sequence) share one handler so don't go through dispatchByPlatform's
branch-aware preflight; gating once at the blueprint factory covers
the whole family in a single place.
2026-05-04 18:40:16 +02:00
Ignacy Łątka cded9850d4 post-review fix: include Android emulators in /preview/simulators
The route was filtering /preview/simulators down to platform === 'ios'
so booted Android emulators were invisible to the preview UI even
though list-devices already classifies them and the downstream
/simulator-server/:udid route accepts the serial via resolveDevice.
Map Android entries to the same { udid, name, state, runtime }
shape the iOS branch emits (state 'device' becomes 'Booted', avdName
becomes the display name, sdkLevel formats the runtime label) so the
existing UI dropdown picks them up without UI changes.
2026-05-04 18:39:43 +02:00
Ignacy Łątka 070981d963 post-review fix: coalesce concurrent boots for the same AVD
Two simultaneous bootAndroid calls for the same AVD both passed the
'already running' fast path (no emulator yet) and both spawned QEMU.
The second collided on the AVD's exclusive on-disk lock and bailed
after the boot deadline with a misleading 'Running multiple emulators'
error. Coalesce per-AVD so a duplicate caller reuses the in-flight
result (or shares its rejection).

Tests get a __resetInFlightBootsForTesting hook because some
intentionally abandon a half-started boot to assert orphan-cleanup —
without the reset that leaked promise contaminates the next test that
boots the same AVD.
2026-05-04 18:26:46 +02:00
Ignacy Łątka 45a7ff5737 post-review fix: bound listAndroidDevices enrichment getprops at 5s
Enrichment getprops used the runAdb default of 30s, so a single device
stuck mid-daemon-attach could stall every `adb devices`-driven loop
(boot-device's adb-register stage, list-devices) for 30s × 3 = up to
90s per call. boot-device's adb-register budget is 60s — so a single
hung device exhausts the whole stage.

Cap each enrichment getprop at 5s. Plenty for a healthy device; not
enough to mask the underlying issue.
2026-05-04 18:22:54 +02:00
Ignacy Łątka d52df0e35d post-review fix: waitForBootCompleted fails fast on terminal device states
The retry loop swallowed every getprop failure, so a device that came
up 'unauthorized' / 'offline' / 'not found' burned the full 2-minute
boot budget before producing a generic timeout message. The user
needs to authorise the prompt, reconnect the device, or pick a
different target — telling them only after 2 minutes is unhelpful.
Classify the rewrapped adb error and abort early on those states.
2026-05-04 18:22:15 +02:00
Ignacy Łątka 54e180ca05 post-review fix: react-profiler-status device_id description matches its peers
Every other react-profiler-* tool (start/stop/analyze/renders/fiber-tree/
cpu-summary) describes device_id as 'iOS simulator UDID or Android
logicalDeviceId'; only react-profiler-status said iOS-only. Align the
wording so agents don't conclude the status check is iOS-only.
2026-05-04 18:20:51 +02:00
Ignacy Łątka 91ede5f922 post-review fix: generalize cross-platform tool descriptions
screenshot, gesture-{tap,swipe,pinch,rotate}, button, keyboard, and
run-sequence all advertise an Android capability but their descriptions
and searchHints still said 'simulator screen' / 'simulator server is
not running' — implying iOS only. The simulator-server binary now
dispatches both iOS and Android internally, so the tools really are
cross-platform; the docs just hadn't caught up.
2026-05-04 18:20:14 +02:00
Ignacy Łątka 8d3550c134 post-review fix: argent run --help references the renamed tool
`list-simulators` was deleted and replaced by `list-devices` (which
lists both iOS simulators and Android emulators/AVDs). The CLI help
text still pointed at the old name, so anyone copy-pasting the example
got 'Tool not found.'
2026-05-04 18:18:19 +02:00
Ignacy Łątka c2f23cb853 post-review fix: open-url Android matches more failure shapes
am start can fail without an 'Error:' prefix — a deep link to a
permission-protected intent emits 'Permission Denial' /
'SecurityException' / 'requires permission' / 'denied'. Without these
in the matcher the tool returned { opened: true } while nothing
happened. Widen the regex.
2026-05-04 18:17:59 +02:00
Ignacy Łątka 132fce728d post-review fix: HTTP capability gate also reads parsedData.device_id
The gate keyed only on parsedData.udid, so debugger/profiler/network
tools (which spell the parameter `device_id`) silently bypassed
capability checks. An Android serial reaching, say, native-profiler-start
fell through to the deeper blueprint and surfaced as HTTP 500 instead
of the clean 400 the gate would have produced. Honour both names.
2026-05-04 18:17:42 +02:00
Ignacy Łątka 0c8dde4831 post-review fix: launch-app treats bare activity name as relative
A bare 'MainActivity' (no leading dot, no '/') was emitted as
${bundleId}/MainActivity, which Android's `am start -n` parses as
package=${bundleId} class='MainActivity' in the default package — and
rejects with 'Activity not found'. The user's obvious intent is the
class within their package, so resolve it to ${bundleId}/.MainActivity.
'pkg/X' and FQCN inputs continue to work unchanged.
2026-05-04 18:17:11 +02:00
Ignacy Łątka ca7d6a9b4d post-review fix: describe Android reports source: 'uiautomator'
The Android handler was returning source: 'native-devtools' even though
its tree comes from `uiautomator dump`, not the React Native devtools
injection layer. Agents that branch on `source` (to decide whether to
also call `native-find-views` for a richer tree, or to log which
provider answered) couldn't distinguish an iOS native-devtools fallback
from a normal Android describe.

Add 'uiautomator' to the DescribeResult.source union and emit it from
the Android branch.
2026-05-04 18:15:42 +02:00
Ignacy Łątka 05712caf59 post-review fix: drop android-screen size cache to fix rotation staleness
5s cache TTL outlived a rotation by orders of magnitude (rotation
completes in <500ms), so describes within the window divided
landscape-X by portrait-W and produced normalized frames with x>1 /
width>1 — every element collapsed onto the right edge. Drop the cache
entirely; one extra `adb shell wm size` per describe is cheap next to
the uiautomator-dump exec-out it sits beside.

Also reject non-positive dimensions explicitly so a malformed `wm size`
output fails clean instead of producing NaN frames downstream.
2026-05-04 18:15:03 +02:00
Ignacy Łątka 778214ff1d post-review fix: regex must exclude '/' so self-closing tags still parse
Follow-up to 8885fe2 — the new attribute charclass [^"'<>] also matched
the '/' that precedes self-close, swallowing it into the attr block and
treating <node ... /> as an opener. Exclude '/' from the unquoted class
and require the trailing \s*\/?> to recognise self-close.

Also: instead of dropping orphaned top-level elements when a stray
closing tag empties the stack, attach them to the existing root so
sibling content stays reachable in the dump tree.
2026-05-04 18:14:52 +02:00
Ignacy Łątka a20dca35b3 post-review fix: restart-app Android uses am start -W like launch-app
The /No activities found|Error:/ scrape on monkey output is the same
false-positive trap launch-app moved away from — class names like
com.example.ErrorReportingActivity in the Activity: line tripped it,
and monkey returns as soon as the intent is injected so describe/tap
race a still-forking process. Resolve the LAUNCHER activity (or use
the user-supplied one), then run am start -W -n <component> and
positive-match Status: ok, sharing the helpers exported from
launch-app/platforms/android.ts.
2026-05-04 18:13:27 +02:00
Ignacy Łątka 1c9533a204 post-review fix: preserve siblings under a bounds-less wrapper
A bounds-less node with 2+ children (a routine Compose pattern: an
unmeasured group around real components) used to be replaced with
`null`, dropping the entire subtree. The collapse rule only handled
the 0-child and 1-child cases — anything else lost its children
silently from the describe tree, so the agent never saw them.

Synthesise a frame that unions the children's frames so the wrapper
becomes a real node and every child remains reachable.
2026-05-04 18:12:30 +02:00
Ignacy Łątka 69973a3d5e post-review fix: clip off-screen uiautomator bounds to the screen rect
Independent clamping of x and width to [0,1] let the sum exceed 1 for
partially-off-screen views — e.g. a right-edge badge at bounds=[1090,0]
[1280,200] on a 1080-wide screen produced x=1, width=0.176, putting the
computed tap centre at 1.088 (off-screen). Same on the left for negative
origins from drawer/overlay states.

Add clipBoundsToScreen() and use the visible portion before normalising.
The describe-frame contract (x+w in [0,1]) now holds even for views the
device reports past the screen edge.
2026-05-04 18:11:58 +02:00
Ignacy Łątka 8885fe2e4b post-review fix: parser tolerates raw '>' + stray closing tags
Two robustness issues in parseUiAutomatorXml:

1. Tag regex used [^<>]*? for the attribute block, rejecting any tag
   whose attribute value contained a literal '>'. XML §2.4 only requires
   '<' and '&' to be escaped, and real Android dumps emit text='A > B',
   text='5 > 0', etc. unescaped. Switch to a charclass that consumes
   complete quoted strings or any non-quote non-'>' character — the '>'
   inside quotes is no longer a tag terminator.

2. A stray '</node>' with no matching opener called stack.pop() blindly,
   then the next opening tag became a second 'root' (overwriting the
   first) — silently dropping all earlier siblings. Guard the pop and
   make root assignment idempotent so malformed dumps degrade
   gracefully instead of losing entire subtrees.
2026-05-04 18:11:12 +02:00
Ignacy Łątka 56e8004743 post-review fix: stop-*-simulator-server is cross-platform
The simulator-server blueprint takes a 'platform' arg and disposes
purely via proc.kill(), so the xcrun preflight was a stale iOS-era
gate that would 424 a host that only used Android. Drop 'requires'
and reword the descriptions to mention both iOS UDIDs and Android
serials.
2026-05-04 18:10:01 +02:00
Ignacy Łątka 0a6d15ce2d post-review fix: declare iOS-only capability on native-devtools/profiler tools
These 10 tools have iOS-only blueprints under the hood, but their
zod schemas / ToolDefinitions never declared a capability matrix.
With nothing to gate against, an Android serial slipped past the HTTP
capability check and surfaced as a deep xctrace / dylib-injection
failure. Declaring 'apple: { simulator, device }' makes the gate
respond with 'Tool X is not supported on android emulator', matching
the contract paste / launch-app already use.
2026-05-04 18:09:29 +02:00
Ignacy Łątka ff27988296 post-review fix: parseAdbDevices rejects daemon banner + unknown states
When the adb background server isn't running yet, `adb devices`
prefixes its output with banner lines like:
  * daemon not running; starting now at tcp:5037 *
  * daemon started successfully *

The previous `^(\S+)\s+(\S+)` regex parsed these as a phantom device
{ serial: '*', state: 'daemon' }, which the boot loop then adopted as
'a new emulator I just booted.' Skip lines starting with '*' and reject
any state outside the canonical adb state set.
2026-05-04 18:09:21 +02:00
Ignacy Łątka 8f79a82d37 post-review fix: surface adb stderr + use SIGKILL on runAdb timeout
Two related issues in runAdb / runAdbBinary:

1. execFile rejection drops the underlying `adb` stderr. Callers saw
   a generic 'Command failed: adb …' instead of the real diagnostic
   ('device offline', 'more than one device', 'no devices/emulators
   found'), so a hung daemon or a serial typo looked identical to a
   transient failure. Catch + rethrow with stderr (or stdout, or the
   raw message) interpolated into the error text.

2. execFile's default killSignal on timeout is SIGTERM, which an adb
   client blocked on a wedged daemon socket can ignore — leaving the
   parent waiting past the deadline and breaking the per-stage budget
   that boot-device relies on. Switch to SIGKILL on both paths.
2026-05-04 18:08:38 +02:00
Ignacy Łątka e0f2c075b6 post-review fix: treat empty screencap output as failure, not success
assertScreencapAlive checked `out.trim() === "0"`, so an empty
output (screencap missing, pipe broken, exec-out drained nothing)
trimmed to "" which !== "0" and was silently treated as healthy —
the exact opposite of intent. Switch to positive match on "1" so
any unexpected reading triggers the cold-boot fallback.
2026-05-04 18:08:01 +02:00
Ignacy Łątka 494145ee7c post-review fix: cancel waitForEarlyExit timer + race stage 5
The recursive setTimeout chain in waitForEarlyExit kept firing forever
after Promise.race resolved with the adb winner, leaking a 500ms timer
across the entire process lifetime — real handle leak across many
boot/restart cycles. Switch to createEarlyExitRacer { promise, cancel }
and clear in a finally on every call site.

Same racer is now wired into stage 5 (pm path android), so a QEMU crash
between sys.boot_completed=1 and PackageManager replying surfaces as
the actual exit/signal error instead of the misleading 'PackageManager
did not respond' timeout.
2026-05-04 18:07:37 +02:00
Ignacy Łątka 6067c163b5 post-review fix: mark paste as iOS-only in skills
The paste tool's capability matrix is { apple: { simulator, device } }
(no android entry), so calls with an Android serial fail at the HTTP
capability gate. Skills told users to 'try paste first, fall back to
keyboard' uniformly; tighten the guidance to use keyboard directly on
Android.
2026-05-04 18:07:32 +02:00
Ignacy Łątka 63a5738346 post-review fix: setup skill claims cold-boot default — actually hot
boot-device probes default_boot via -check-snapshot-loadable, hot-boots
with -force-snapshot-load and falls back to cold on failure (commit
4c48223). The skill still told users 'cold boot by default'; reword
to match the implementation and document the coldBoot escape hatch.
2026-05-04 18:07:06 +02:00
Ignacy Łątka 60801f2767 post-review fix: drop nonexistent grantPermissions skill references
reinstall-app's zod schema only accepts { udid, bundleId, appPath };
the Android impl unconditionally passes -g, so 'grantPermissions: true'
was both unsupported by the schema and unnecessary in practice. Update
the four skills/rules that documented it.
2026-05-04 18:06:52 +02:00
Ignacy Łątka fc5ed6f692 post-review fix: prefix skill references in optimization SKILL.md
Bare names like 'react-native-profiler' / 'create-flow' / 'device-interact'
won't resolve via the registered slug; switch to the canonical
'argent-<short>' form.
2026-05-04 18:05:56 +02:00
Ignacy Łątka 7a6130dda3 post-review fix: prefix skill reference in rules/argent.md
Follow-up to prior commit — rules/argent.md still had a bare
'device-interact' reference that wouldn't resolve to a registered slug.
2026-05-04 18:04:23 +02:00
Ignacy Łątka 73c415b628 post-review fix: prefix skill references with 'argent-'
The registered slug for every shipped skill is 'argent-<short>'; bare
references like 'device-interact' or 'react-native-app-workflow' don't
resolve in MCP/Claude harnesses that look up by the canonical id.
2026-05-04 18:04:13 +02:00
Ignacy Łątka a73205b027 post-review fix: forbid leading dash in bundleId/activity regex
The old /^[A-Za-z0-9._-]+$/ accepted '--user' / '-X', which would
let an attacker masquerade as a flag inside on-device 'am start -n …',
'am force-stop …', or 'cmd package resolve-activity …'. Tighten the
head to letters or underscore (and dot for activity, so '.MainActivity'
still works). Add positive injection-hardening cases for '--user',
'-X', '-foo', and a leading-dash activity.
2026-05-04 18:03:54 +02:00