HIGH: - ntfy listener: replace blocking exhausted() loop with SSE read + capped exponential-backoff reconnect; 60s read timeout - GatewayClient.stop(): reset HTTP leg (http, httpCursor, sseFailures, usingLongPoll, lastAck) - non-atomic shared state -> synchronized/@Volatile/AtomicLong/ CopyOnWriteArrayList MEDIUM: - mediaId path-traversal guard (isValidMediaId) at network/app/fs boundaries - loadFromCache: move ts=0 pending bubbles to end, keep stored order - attachment placeholder tracked by identity, not filename - optimistic ChannelStore updates on favorite/icon/automation/default - secure-store caching (desktop map, android store) - secret passed to keyring via stdin (macOS + Linux) - SecureStore.clear() clears deviceId/syncCursor/fcmToken/ntfy* - random ids for system messages; SSE EOF reconnect delay - PowerShell $ escaping; dispose() cancels job before saving flows - wire up "Forget pairing" in Settings - move machine-specific org.gradle.java.home to user-level gradle.properties LOW + dead code + stale comments: - .aac->audio/aac; locale-fixed cost/size; 3-digit hex; hour+ latency - Backdrop.DEFAULT defined once; notification id 24-bit; channel id cap - remove dead FileSource, unused protocol/theme/media constants, empty onDispose, SDK_INT<O guard, hostFromUrl - fix stale WS/SSE, M1/M5, and milestone KDoc comments Verified: Kotlin desktop+android host tests, 100/100 Python gateway tests, LSP clean, installed & running on device.
4.8 KiB
4.8 KiB
AGENTS.md
Hard rules
hermes-agent/is a read-only research reference (git-ignored). Never commit, push, or modify it — a pre-commit hook (scripts/guard_hermes_agent.sh --staged) fails any commit that stages it. Never modify hermes core; we only install our plugin into the live hermes home.- Commit/push scope: when asked to "commit and push all changes," that means all changes in the working tree — it does NOT matter whether a change was made this session or earlier. Stage everything (
git add .) and commit; do not cherry-pick or second-guess which files are "yours." The only exception ishermes-agent/(git-ignored, never staged). - The plugin is installed by symlink:
~/.hermes/plugins/android→<repo>/gateway-plugin(already set up on this machine).
Layout
gateway-plugin/— Python hermes platform plugin (android). No build step, zero new deps (stdlib + hermes-providedwebsockets/httpx).protocol.pyis the frame source of truth, mirrored inapp/shared/.../protocol/Protocol.ktanddocs/protocol/frames.schema.json.app/— one Compose Multiplatform Gradle project::shared(KMP, most of the code;jvmMainis shared by the android and desktop targets since both are JVM-based),:androidApp(thin shell, packagedev.iris.app),:desktopApp(thin shell).docs/— numbered reference library; readdocs/00-overview.mdfirst. Locked decisions:docs/16-open-questions.md.
Commands
hermesis not on PATH: usehermes-agent/.venv/bin/hermes(venv fromcd hermes-agent && uv sync).- Gateway:
hermes gateway setup(one-time; generatesIRIS_TOKENin~/.hermes/.env, prints the token only once) →hermes gateway(run) →hermes gateway status. - Android: check the device is connected first (
adb devices→a5ca2a4blisted asdevice); thencd app && ./gradlew :androidApp:installDebugto install on the phone and live-verify changes (launch/screenshot: see ADB below). - Desktop:
cd app && ./gradlew :desktopApp:run; packaging::desktopApp:jpackage(app-image;-PjpackageType=debfor a .deb). - Python tests — never bare
pytest:cd hermes-agent && scripts/run_tests.sh tests/gateway/test_android.py(no args = full suite). - Kotlin tests:
cd app && ./gradlew :shared:testAndroidHostTest/:shared:desktopTest(host-side;jvmTestis the shared source set). - WS probe (gateway must be running):
hermes-agent/.venv/bin/python gateway-plugin/tests/ws_probe.py --token <IRIS_TOKEN> --send "hello"— assertion flags documented ingateway-plugin/tests/README.md. - E2E driver (gateway must be running; it never starts/stops it):
hermes-agent/.venv/bin/python gateway-plugin/tests/e2e.py.
Environment / pairing quirks
- Pairing token:
IRIS_TOKENin~/.hermes/.env. Pairing is manual URL + token entry; on Android there's also a QR-scan button (camera) that fills URL + token from the gateway's pairing QR. Desktop has no camera, so it's manual entry only. - WS default bind is
127.0.0.1; for a phone on the LAN setIRIS_WS_HOSTto the gateway's LAN IP. app/local.properties(sdk.dir) is git-ignored and required for Android builds.google-services.jsonis optional: without it FCM is inert and ntfy is the push path. Public ntfy.sh SSE is flaky — self-host ntfy.- JDK 21 is required (the desktop Markdown renderer ships Java-21 bytecode); no system Gradle — always the wrapper (
./gradlew). The JDK-21 home is machine-specific and set per machine (NOT committed): addorg.gradle.java.home=/path/to/jdk21to~/.gradle/gradle.properties, orexport JAVA_HOME=/path/to/jdk21before running./gradlew. - Desktop jpackage on Linux/JDK 21 prints a non-fatal
pure virtual method called(JDK-8348560); the app works.
Testing quirks
hermes-agent/tests/gateway/test_android.pyis a thin mirror that imports the livegateway-plugin/package from this repo (override withIRIS_PLUGIN_DIR); HERMES_HOME is sandboxed per-test by the conftest. Tests must never touch the real~/.hermes.- e2e scenarios 3 (reasoning) and 5 (commentary) are model-dependent → SKIP; 11 (push) and 12 (reconnect) are PARTIAL by design.
- ADB: launch
adb shell am start -n dev.iris.app/.MainActivity; reset pairing stateadb shell pm clear dev.iris.app; screenshotadb exec-out screencap -p > /tmp/shot.png. - ADB UI taps: never guess tap coordinates from a screenshot — dump the hierarchy and tap the element's real bounds:
adb shell uiautomator dump→adb pull /sdcard/window_dump.xml→ find the node bytext/content-desc/resource-id→adb shell input tapat the center of itsbounds="[x1,y1][x2,y2]". Re-dump after every navigation; if a tap misses, the dump is stale — re-dump, don't nudge coordinates. - Gateway logs:
~/.hermes/logs/gateway.logorhermes logs --follow.