Three open TODOs from the v0.4 hand-off, plus an install.sh ergonomic
improvement that came up while planning on-device verification.
C4 — voice contact-name → phone resolution
- RealVoiceBridgeIntentHandler now resolves the spoken contact name to
a real phone number BEFORE dispatching /send_sms, instead of passing
the raw name (which the phone-side regex check rejected, surfacing
as a user-visible error every time).
- New private helper resolveContactPhone() calls
HermesAccessibilityService.instance.actionExecutor.searchContacts(
contactName, limit=5) directly. Same code path as the /search_contacts
bridge route but invoked locally — no response-correlation plumbing
needed because both the voice handler and the accessibility service
are in the same process.
- searchContacts returns phones as a comma-joined string ("+1555..., +1555...")
per the C2 wire shape; the resolver splits on `,`, trims whitespace,
and takes the first non-blank entry.
- Wrapped in withContext(Dispatchers.IO) — ContactsContract queries
hit the on-device content provider and can block.
- If the resolver returns null (service unbound, contacts permission
missing, no matching contact, or matched contact has no phone), the
intent surfaces as a friendly spoken response ("I couldn't find a
contact called Sam.") instead of dispatching a broken envelope.
- The destructive-verb confirmation modal still fires on the
BridgeCommandHandler side, so the resolver is purely additive — it
doesn't bypass any safety gate.
H5 — voice app-name → package resolution
- Same shape as C4 but for /open_app. New resolveAppPackage() helper
calls service.packageManager.queryIntentActivities(ACTION_MAIN +
CATEGORY_LAUNCHER) and fuzzy-matches the spoken app name against
the launchable-app inventory.
- Three-tier match (case-insensitive, first hit wins):
1. Exact label match — "spotify" → "Spotify"
2. Prefix match — "chro" → "Chrome Beta"
3. Substring match — "google" → "Google Maps"
Order matters: tier 1 + 2 prevent accidental substring hits like
"messages" matching "Google Messages" instead of the literal
Messages app.
- Requires the <queries><intent action=MAIN category=LAUNCHER/></queries>
manifest declaration that landed in the previous commit (7755851) —
without it Android 11+ silently returns a near-empty candidate list.
- Wrapped in withContext(Dispatchers.IO) — PackageManager queries
can be slow on devices with many apps.
- Same null/error surfacing pattern as C4. BridgeCommandHandler still
blocklist-checks the resolved target package before launching, so
voice-utterance intents to open blocked banking apps stay blocked.
buildSmsEnvelope() and buildOpenAppEnvelope() now take 2 args (the
intent + the resolved value). Both call sites in tryHandle updated.
install.sh --branch flag (+ HERMES_RELAY_INSTALL_URL on the shim)
- Adds a CLI flag to install.sh that overrides HERMES_RELAY_BRANCH.
Flag wins over env var wins over the default ("main"). Same
precedence pattern as HERMES_RELAY_HOME / HERMES_VENV_PY.
- The hermes-relay-update shim now reads HERMES_RELAY_INSTALL_URL to
override the install.sh source URL. Bootstrap caveat documented in
the shim header: switching to a feature branch BEFORE that branch
is merged to main needs the env-var override because the shim
normally curls install.sh from main, which won't have the
--branch flag yet.
- After the bootstrap install completes the host has the new
install.sh on disk + the shim handles all subsequent updates,
including switching back to main via `hermes-relay-update`
(no flag needed, defaults to main).
- Documented the flag and the bootstrap escape hatch in the shim
doc-comment + the install.sh header.
plugin/tests/test_android_read_screen.py
- 11 stdlib-unittest cases mirroring the test_android_search_contacts
pattern (no pytest, no responses, runs as
`python -m unittest plugin.tests.test_android_read_screen`).
- Coverage: happy path with default include_bounds=False, explicit
include_bounds=True/False, empty node list, service-not-connected
503 passthrough, ConnectionError network failure, requests.Timeout,
schema registration sanity, and handler dispatch from the
_HANDLERS dict with both default and explicit args.
- All 11 tests pass locally (`python -m unittest plugin.tests.test_android_read_screen`).
- Caught a documentation drift along the way: android_read_screen
in the integration branch sends `?include_bounds=` (wave 1/2/3
rename) but main still sends `?bounds=`. The test asserts the
current branch shape; the rename will land in main as part of the
v0.4 merge.
Out of v0.4 scope, NOT touched
- The original C4 doc-comment proposed a response-correlation rewrite
of ChannelMultiplexer to wire bridge.command → bridge.response
request_id matching. Local resolution sidesteps that requirement
entirely. The correlation pattern is still useful for other
cross-channel flows (e.g. a future agent-driven UI test framework)
and is tracked separately rather than deferred under the C4 label.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>