* ci: prevent concurrency cancellation on rapid main merges The concurrency group used github.ref which is static for main (refs/heads/main). When multiple PRs merged rapidly (~2 min apart), each merge commit cancelled the previous CI run before it could finish. Fix: use github.sha for push events to main (unique per commit) while keeping the ref-based group for PR branches (cancels on new commits to the same PR branch). * refactor(#82): remove dead SkeletonList/SkeletonCard composables --------- Co-authored-by: ronia <ronia@m57> Co-authored-by: M57 <hy4ri@users.noreply.github.com>
7.0 KiB
AGENTS.md
Guidance for AI coding agents working on this repository. This complements README.md (for humans) with agent-focused context.
Project Overview
Hermes Control is a Jetpack Compose Android app — a mobile control panel for Hermes Agent. It talks to the Hermes dashboard's REST API and WebSocket TUI Gateway (JSON-RPC 2.0) over a trusted LAN.
- Package:
com.m57.hermescontrol - Min SDK 26 / Target SDK 36 / Compile SDK 36
- Kotlin 2.3.20, KSP 2.3.9 (standalone versioning, NOT
kotlinVersion-kspVersion) - Jetpack Compose, Room 2.7.x, Navigation3, OkHttp WebSocket, Retrofit, Gson
- Auth:
EncryptedSharedPreferences(AES256-GCM), Bearer token (REST) +?token=(WS)
Build & Test Commands
⚠ No local Android SDK — CI is the only compilation truth engine
./gradlew assembleDebug, ./gradlew test, ./gradlew ktlintCheck all fail locally
(no Android SDK installed). CI verifies compilation, lint, and tests. Push small
and watch CI.
What you CAN run locally
# Standalone ktlint binary (no SDK needed) — the PRIMARY local check
curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.2.1/ktlint
chmod +x ktlint
./ktlint <file> # check one file
./ktlint --format <file> # auto-fix import ordering, indent, trailing commas
find app/src -name '*.kt' | xargs ./ktlint --format # fix all
CI pipeline (.github/workflows/android.yml)
| Job | Purpose |
|---|---|
ktlint |
ktlint 1.2.1 style check |
android-lint |
Android Lint |
unit-tests |
JUnit |
build |
assembleDebug (gated by the 3 above) |
ci-summary |
Aggregator (if: always()) — branch protection gates on THIS check |
Every Gradle job validates gradle-wrapper.jar and uses the remote build cache
(GRADLE_ENCRYPTED_KEY secret). concurrency.cancel-in-progress: true.
Releasing (.github/workflows/release.yml)
Triggers on git push tag v*. Release APK uses R8 minification + resource shrinking.
Requires permissions: contents: write on the build-release job.
Code Style
- ktlint 1.2.1 is enforced in CI. Run
./ktlint --formatbefore pushing. - Import ordering is the #1 CI failure: ktlint enforces ASCII-lexicographic order
(uppercase before lowercase:
LaunchedEffectbeforecollectAsState). const valmust use SCREAMING_SNAKE_CASE.- Trailing commas required. No trailing whitespace.
- 120 char max line length.
Architecture Conventions
Navigation (Navigation3 — NOT Navigation Compose)
Uses androidx.navigation3 (NavKey, NavBackStack, NavDisplay, entry<T>).
⚠ Always route navigation through NavigationController.navigateTo(). Never call
backStack.add(ScreenKey) directly from UI callbacks — the controller has a
deduplication guard. Bypassing it stacks duplicate screen entries that compete for
touch events and become unresponsive.
Shared Components
HermesScaffold— drawer-aware Scaffold + TopAppBar with refresh slot, pull-to-refresh, optional snackbar host. New screens MUST use this instead of rawScaffold. API:title: @Composable () -> Unit(notString— wrap in{ Text("...") }).StateViews—LoadingState,ErrorState,EmptyState. Every data screen must implement all three branches in itswhen { }block.
Room Persistence
ChatMessageEntity/ChatMessageDao/HermesDatabase— chat messages survive app kills.- Room 2.7.x requires
room { schemaDirectory("$projectDir/schemas") }DSL. ChatViewModelextendsAndroidViewModel(needs Application for DB access).
Theme
Theme.kt has ENABLE_DYNAMIC_COLOR = true (Material You on API 31+). Semantic
status colors (success/warning/error/info) are ALWAYS brand-defined via
LocalHermesStatusColors, never dynamic. Access them via
LocalHermesStatusColors.current.success, not MaterialTheme.colorScheme.
Git Workflow
PR-Always (ENFORCED)
Every change goes through a PR. Never push directly to main.
git checkout main && git pull origin main
git checkout -b fix/issue-N-description # or feat/...
# make changes, run ./ktlint --format
git commit -m "fix(#N): description"
git push -u origin HEAD
gh pr create --title "fix(#N): description" --body "Closes #N"
Commit Conventions
- NO co-author trailer. No
Co-Authored-By, noGenerated with .... Ever. - Short commits: subject line + max 2 lines of body. Split into atomic commits rather than writing long bodies.
- Conventional Commits types:
feat,fix,refactor,docs,test,ci,chore. - Bug fixes annotated inline:
// B7 (Jun 18 2026, kanban t_xxx): description.
Security Considerations
- Cleartext HTTP/WS is intentional — trusted LAN only.
usesCleartextTraffic="true"andhttp:///ws://URLs are by design. Don't "fix" this unless adding HTTPS support. HttpLoggingInterceptor.Level.BODYmust stay gated onBuildConfig.DEBUG— it prints the Authorization header. (B1)- AuthManager token is ephemeral — it becomes invalid when the Hermes dashboard restarts. Symptom: 401 on every call. Fix: re-extract the token from the running gateway, don't assume the stored one is valid.
- Release signing uses a keystore with env-var credentials (
KEYSTORE_PATH,KEYSTORE_PASSWORD,KEY_ALIAS,KEY_PASSWORD). Don't commit keystore files or hardcode passwords.
Things to Avoid
- Don't run
./gradlewtasks locally expecting them to work — no Android SDK. - Don't remove
@OptIn(ExperimentalMaterial3Api::class)from screens that useSegmentedButton,PullToRefreshBox, or other experimental M3 APIs. - Don't use
remember { AuthManager.getBottomNavItems() }without a key — SharedPreferences is not Compose state; the value goes stale. Read it directly. - Don't access
MaterialTheme.colorScheme.*inside non-@Composablelambdas (remember {},buildAnnotatedString,LaunchedEffect). Extract to a localvalat the composable scope first. - Don't add new screens without checking if an existing one already covers the functionality (19+ screens exist). Extend rather than duplicate.
Project Layout
com.m57.hermescontrol/
├── data/ local (AuthManager), remote (Retrofit), ws (WebSocket), model
├── notification/ ChatNotificationService (foreground service)
├── theme/ Color, Theme, Motion, Spacing, Shapes, Type
├── ui/ chat, settings, common (HermesScaffold, StateViews), + 17 screens
├── util/ CronExpressionFormatter, etc.
├── Navigation.kt Drawer + NavDisplay + entry wiring
├── NavigationController.kt Central navigation guard (dedup)
└── MainActivity.kt
Further Reading
- README.md — human-facing overview, features, screenshots, tech stack
- TEST_INFRA.md / TEST_READY.md — test infrastructure notes
- .github/workflows/android.yml — CI pipeline source of truth