Make the on-device Termux install flow actually work end-to-end. Three workstreams from plans/atomic-wondering-sunrise.md plus a validation patch: Workstream A — F-Droid deep-link from WelcomeScreen - The 'Install locally (Termux)' card no longer renders a dead disabled Continue button when Termux is missing. It now shows two stacked CTAs: primary 'Open F-Droid' (existing termux_install_action string, now actually used) deep-linking https://f-droid.org/packages/com.termux/, and secondary 'Or get it from GitHub Releases' (new string) for side-loaders deep-linking github.com/termux/termux-app/releases. - Reuses existing HermesApi.openExternal; no new IPC method needed. Workstream B — HermesInstaller backend-aware shell dispatch - runShell / runHermesDoctor / runPipInstall in HermesInstaller now branch on currentBackend() and dispatch to TermuxRunner on Termux, bundled.runPython on Bundled. Pre-fix, every stage shelled through bundled.runPython regardless of backend, so Termux installs failed immediately at stage 3 with 'Bundled Python not available'. Workstream C — RUN_COMMAND_PENDING_INTENT for real exit codes - New TermuxResultReceiver.kt: BroadcastReceiver + TermuxResultRegistry process-singleton. Atomic session IDs, ConcurrentHashMap of Continuations, PendingIntent.FLAG_MUTABLE result intent builder (FLAG_MUTABLE is required — Termux's RunCommandService adds the result bundle to the PendingIntent before broadcasting it back). - New TermuxRunner.runAndWait(command, cwd): suspend fn using suspendCancellableCoroutine (modeled on GatewayClient.request), attaches the result PendingIntent via the com.termux.RUN_COMMAND_PENDING_INTENT intent extra. Resolves with a BundledPythonRunner.PythonResult carrying the real exit code + truncated stdout/stderr (~100KB cap imposed by Termux). - HermesInstaller.runShellViaTermux switches from termux.run to termux.runAndWait. The 4 call sites (stages 3/4/5/8 in runStages, plus runHermesUpdate) already gate on exitCode != 0 and consume stderr/stdout, so they become correct automatically. - applyPatches rewritten as suspend: dispatches git apply via Termux using a bash heredoc to write the patch content to Termux's $PREFIX/tmp/ (our app's process can't write into Termux's sandbox). Drops the now-stale TODO(workstream-B-followup) comment. - AndroidManifest gets a <queries> block declaring Termux package visibility — without it, on API 30+, PackageManager.getPackageInfo returns NameNotFoundException even when Termux is installed and HermesInstaller falls through to the bundled-Python backend. Also adds com.termux.permission.RUN_COMMAND (user-granted in Android Settings -> App info -> Additional permissions). - TermuxProbe.isRunCommandResultSupported() helper checks Termux versionName >= 0.109 (the floor for RUN_COMMAND_PENDING_INTENT). - InstallScreen surfaces a TermuxPermissionNeededCard when an install stage fails with a Termux-specific error pattern (RUN_COMMAND, allow-external-apps, plugin_action_disabled — NOT the bare word 'permission' which pip prints routinely). The card has an 'Open App Settings' button using a new HermesApi.openAppSettings() helper that fires Settings.ACTION_APPLICATION_DETAILS_SETTINGS. - setup-android.sh gains a queries-merge step. Anchored on start-of-line for awk and probes android:name="com.termux" for detect-and-skip (so a future library's unrelated <queries> block doesn't silently skip the Termux merge). Validation patch — markerDir + Termux sandbox awareness (B1–B5) - An adversarial review found that HermesInstaller assumed every path it computes (hermesPython, hermesRepo, hermesVenv, the verified marker) lives somewhere our app can read. On Termux backend hermesHome resolves to /data/data/com.termux/files/home/.hermes which is OFF-LIMITS to our process. Pre-Workstream-C the install never succeeded on Termux so these checks were never exercised against real state; Workstream C made the install actually work, so the broken file-existence checks now blocked every user from ever reaching AppState.Main. - Fix: backend-aware markerDir at context.filesDir/hermes-markers/ on Termux (hermesHome on Bundled). New sentinels hermesVerifiedMarker / hermesRepoClonedMarker / hermesVenvCreatedMarker live there. Stages 3 & 4 write their marker on success and check the marker (not the file) for skip-on-rerun. checkInstall().installed/configured/hasApiKey collapse to 'verified marker exists' on Termux (the marker IS the success signal of a completed install). - Stage 4 switches from python3.11 to python on Termux — Termux's pkg install python ships the binary as 'python' symlinked to the current 3.x; the literal 'python3.11' was a Bundled-path assumption that happened to work on some Termux setups but not others. - getHermesVersion becomes suspend + backend-aware (Termux dispatches the importlib.metadata.version call via runAndWait); HermesApi cascades. Plan + research artifacts - plans/atomic-wondering-sunrise.md is the live source-of-truth plan/postmortem (3 shipped workstreams + validation section). - docs/python-bundling-alternatives.md captures the research that showed Termux + RESULT_INTENT is the right v1 path and that the originally-planned Chaquopy workstream isn't needed. Alpine minirootfs + proot is the recommended Tier 2 alternative for users without Termux, downloaded on first run (no APK bloat, no GPL contagion, F-Droid compatible). Build status - ./gradlew :app:assembleDebug green (arm64-v8a, armeabi-v7a, x86_64, universal APKs, ~20MB each). - script/check-kotlin-drift.sh green (59 Kotlin files in sync). - graphify graph rebuilt (1681 nodes, 2866 edges). Untested on device. Verification steps for happy path, permission-denied regression, and cancellation hygiene are in the plan's Workstream C verification section. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
hermes-mobile
An Android port of hermes-desktop that runs
hermes-agent on the device. The UI is a
native Jetpack Compose port — no WebView, no Capacitor, no vendored React renderer. The
Hermes Kotlin sources live in android-runner/; the live apps/mobile/android/ tree is
regenerated by apps/mobile/scripts/setup-android.sh on every build. See
docs/architecture.md for the full layout and design rationale.
The parity roadmap is at ~/.claude/plans/groovy-fluttering-island.md (8 phases,
~16 weeks, full desktop parity). The Phase 0 cleanup is complete; Phase 1 (HermesApi
surface expansion) is the next milestone.
Layout
hermes-mobile/
├── android-runner/ # canonical Kotlin sources
│ ├── README.md # edit conventions
│ └── app/src/main/
│ ├── kotlin/com/nousresearch/hermes/ # 33 .kt files: HermesApi, MainActivity,
│ │ # GatewayClient, HermesInstaller, services,
│ │ # Compose UI (chat, sessions, memory, ...)
│ ├── AndroidManifest.xml
│ └── res/values/strings.xml
├── apps/mobile/
│ ├── android/ # generated Android project; Kotlin sources
│ │ # synced from android-runner/ on every build
│ ├── package.json # minimal stub with android:assemble:* scripts
│ └── scripts/
│ └── setup-android.sh # rsync --delete runner → live; merge strings
│ # and permissions
├── script/
│ ├── check-kotlin-drift.sh # CI: fail if android-runner/ and live/ diverge
│ ├── sign-and-zipalign.sh
│ ├── write-update-manifest.sh
│ └── audit-a11y.sh
├── f-droid/ # F-Droid metadata + build script
├── keystore/ # release.jks (RSA 4096, 25-year)
├── review/
│ ├── hermes-desktop/ # upstream fathah/hermes-desktop (visual spec)
│ └── hermes-agent/ # upstream NousResearch/hermes-agent (HTTP API)
├── docs/
│ └── architecture.md
└── .github/workflows/mobile-build.yml # CI: sync, drift check, build, sign, release
Build
# One-time setup
pnpm install
bash apps/mobile/scripts/setup-android.sh # rsync runner → live
bash script/check-kotlin-drift.sh # verify in sync
# Build
cd apps/mobile/android
./gradlew :app:assembleDebug # debug APK
./gradlew :app:assembleRelease # release APK (needs keystore)
./gradlew test # unit tests
./gradlew connectedAndroidTest # instrumented tests
Editing
Edit Kotlin sources in android-runner/, not in apps/mobile/android/. The live
tree is regenerated by setup-android.sh on every build. CI's
script/check-kotlin-drift.sh fails the build on any divergence.
See android-runner/README.md for the layout, edit
conventions, and "how to add a new Hermes source" walkthrough.
Distribution
| Channel | Build | Update mechanism |
|---|---|---|
| Sideload (GitHub Releases) | assembleRelease (signed) |
In-app updater reads latest.json from the release |
| F-Droid | f-droid/build.sh |
F-Droid client auto-update |
| Manual APK | assembleDebug |
None |
Parity status
| Desktop | Mobile (this repo) | |
|---|---|---|
| Screens | 20+ | 3 implemented (chat, sessions, memory); 13+ TODO |
| IPC methods | ~188 | 44 implemented; ~110 TODO (Phase 1) |
| Connection modes | Local + remote + SSH | All three implemented |
| Install | Curl install.sh | Termux + bundled Python (8 stages) |
| Gateway | Python subprocess | Python subprocess (same) |
| OAuth | All providers | OAuth loop broken; fix in Phase 1 |
| Updates | electron-updater | GitHub Releases (F-Droid: in-app disabled) |
| Crash reporting | Optional Sentry | Optional Sentry (TODO Phase 7) |
See ~/.claude/plans/groovy-fluttering-island.md for the full roadmap.