diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 18216612..01c0b7d2 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -1,72 +1,51 @@ # Rouse Context -Android app + relay server that turns a phone into a secure, on-demand MCP server. - -## Architecture - -Three distinct domains live in this repo: - -- **Relay server** (`relay/`) — TLS passthrough via SNI routing, `/wake` endpoint, ACME cert orchestration, DNS-01 challenges via Cloudflare API. Never terminates TLS, never sees payloads. -- **Tunnel client** (`core/tunnel/`) — Mux protocol, TLS client connecting back to relay, session lifecycle, CertificateStore interface. Bridges the relay protocol to on-device MCP. -- **Android app** (`app/`, `core/mcp/`, `health/`, `api/`, `notifications/`, `work/`) — MCP SDK integration, Health Connect server, audit UI, foreground service. Pure Android/Kotlin. +Android app + relay server that turns a phone into a secure, on-demand MCP server. For architecture, see `docs/design/overall.md` and `docs/design/android-app.md`. ## Build & Test -All Gradle commands require `JAVA_HOME=/usr/lib/jvm/java-21-openjdk`. +`JAVA_HOME=/usr/lib/jvm/java-21-openjdk` for every Gradle command. ```bash -# Build library modules -./gradlew :core:mcp:assembleDebug :core:tunnel:assembleDebug :health:assembleDebug +# Build a module +./gradlew :core:mcp:assembleDebug # Unit tests ./gradlew :core:mcp:testDebugUnitTest -# Lint & static analysis +# Lint & static analysis (whole tree) ./gradlew ktlintCheck detekt # Auto-fix formatting ./gradlew ktlintFormat + +# Relay (Rust, separate) +cd relay && cargo test && cargo clippy --all-targets --all-features -- -D warnings ``` +For module-specific recipes and common commands, see `docs/agent-quickstart.md`. + ## Workflow -MUST follow this sequence for any non-trivial feature: +Non-trivial change: design → approve → test → implement → verify. See `.claude/rules/workflow.md`. -1. **Design** — Enter plan mode. Explore the codebase, understand existing patterns, design the approach. Present the plan for approval. Do NOT write code yet. -2. **Approve** — Wait for explicit approval before proceeding. -3. **Test** — Write tests that define the expected behavior. Verify they fail. -4. **Implement** — Write the minimum code to pass the tests. Do not modify tests to match implementation. -5. **Verify** — Run `./gradlew testDebugUnitTest ktlintCheck detekt`. Fix any failures. -6. **Commit** — Stage specific files, write a clear commit message. +UX-affecting changes (onboarding, permission/credential timing, error surfaces) require explicit user approval and a `docs/ux-decisions.md` entry. See `.claude/rules/ux-changes.md`. -Skip to step 4 for trivial changes (typos, single-line fixes, obvious bugs). - -## Module Boundaries - -- `:core:mcp` — MCP session orchestrator, HTTP routing, OAuth device code flow, token management. Contract layer. -- `:core:tunnel` — Mux protocol, WebSocket client, TLS accept, CertificateStore interface. MUST NOT know about MCP. -- `:api` — `McpIntegration` interface, `IntegrationStateStore`. Contract for integration modules. -- `:health` — Health Connect MCP server. Depends on `:api` and `:core:mcp`. -- `:notifications` — Notification state machine, audit persistence (Room). Depends on `:core:tunnel` and `:core:mcp`. -- `:work` — Foreground service, FCM receiver, WorkManager, wakelock. Depends on `:core:tunnel` and `:notifications`. -- `:app` — Wires everything together via Koin. The ONLY module that depends on all others. -- `relay/` — Completely independent. Rust. No Android dependencies. Built with Cargo. +When launching a sub-agent, point it at `.claude/agent-preamble.md` for the standard rules. ## Code Style - ktlint with `android_studio` style (see `.editorconfig`) - detekt with coroutine rules enabled (see `config/detekt/detekt.yml`) - No wildcard imports -- Structured concurrency — no `GlobalScope`, no unscoped `CoroutineScope()`, no `runBlocking` in production code +- Structured concurrency — see `.claude/rules/coroutines.md` ## Signing Keystores -- Debug keystore: `.signing/debug.keystore` -- Release keystore: `.signing/release.keystore` -- Backups: `~/backups/rouse-context/` +`.signing/{debug,release}.keystore`, backups at `~/backups/rouse-context/`. -NEVER regenerate these keystores. Regenerating causes signature mismatch, which forces an uninstall (losing all app data) and wastes ACME certificate quota on re-registration. +NEVER regenerate. Regenerating forces an uninstall on every device (losing app data) and burns ACME quota. Critical. -## Domain: rousecontext.com +## Domain -Registered at Squarespace, nameservers pointed to Cloudflare. Each device gets a unique subdomain (e.g. `abc123.rousecontext.com`). Cloudflare DNS API for automated ACME challenge TXT records. +`rousecontext.com` registered at Squarespace, nameservers on Cloudflare. Each device gets a per-subdomain wildcard cert from GTS via DNS-01 (Cloudflare API). Per-integration secrets prefix the subdomain at the SNI layer (e.g. `-..rousecontext.com`). diff --git a/.claude/agent-preamble.md b/.claude/agent-preamble.md new file mode 100644 index 00000000..e7df758d --- /dev/null +++ b/.claude/agent-preamble.md @@ -0,0 +1,65 @@ +# Agent preamble + +Read this once when launched. Apply to every agent task in this repo. + +## Tooling + +- Use `Read`, `Grep`, `Glob`, `Edit`, `Write` over their `bash` equivalents (`cat`, `grep`, `find`, `sed`). +- Spawn sub-agents with model `opus` for any code work. +- Run agents in the background unless the user asks otherwise. + +## What to NOT touch + +- Anything under `.claude/` (rules, agent-preamble, project memory): triggers approval prompts and blocks autonomous work. If you genuinely need a rule change, surface it to the user first. +- Signing keystores at `.signing/`: regenerating forces a forced uninstall on every device + burns ACME quota. Critical. +- Build outputs (`build/`, `.gradle/`, etc). + +## Workflow + +For any non-trivial change: design → approve → test → implement → verify. See `.claude/rules/workflow.md`. + +UX-affecting changes (onboarding, permission timing, error surfaces, credential timing, etc) need explicit user approval AND an entry in `docs/ux-decisions.md`. See `.claude/rules/ux-changes.md` for what counts. + +## Build & verify + +`JAVA_HOME=/usr/lib/jvm/java-21-openjdk` for every Gradle command. See `docs/agent-quickstart.md` for module-specific recipes. + +Always before committing: +- `./gradlew ktlintFormat` (formats Kotlin) +- `./gradlew :module:testDebugUnitTest ktlintCheck detekt` for the modules you touched +- For relay (Rust): `cargo fmt && cargo test && cargo clippy --all-targets --all-features -- -D warnings` + +Match commit-message style — `git log --oneline -10` for recent examples. + +## Branches and PRs + +Branch name: `fix-NNN-short-description` for issue fixes. + +PR lifecycle (must close out fully — no strays): +1. Open PR with `gh pr create`. Body links the issue (`Closes #NNN`). +2. Wait for CI green. +3. `gh pr merge NNN --squash --delete-branch`. +4. If local-branch deletion fails because another worktree has it checked out: + ``` + cd /home/jmonk/git/rouse-context && git checkout --detach origin/main && git branch -d + ``` +5. Comment on the issue with the PR number; remove the `in-progress` label (the PR's `Closes` line auto-closes the issue). + +Don't leave open PRs or stale local branches behind. + +## When something goes wrong + +- If you can't make progress: file a NEW GitHub issue describing the blocker (don't bury it as a comment on the parent issue), then stop. Don't speculate-patch. +- If you find a related bug while working: file as separate issue, link it, don't expand scope mid-PR unless the original task is bounded. + +## Relay-specific + +NEVER build the relay on the GCP VPS. CI deploys via GitHub Actions. To verify a deploy: +``` +gcloud compute ssh relay --zone=us-central1-a --command='systemctl status rouse-relay | head -5' +``` + +## Devices + +- `adolin` Pixel 6 Pro (`ANDROID_SERIAL=1B151FDEE008EY`) — agent-controlled test device. Drive via `ssh adolin "ANDROID_SERIAL=... /opt/android-sdk/platform-tools/adb …"`. +- The user's personal phone is NOT for autonomous testing. Always confirm before targeting any other device. diff --git a/docs/agent-quickstart.md b/docs/agent-quickstart.md index df4f53d1..825cb408 100644 --- a/docs/agent-quickstart.md +++ b/docs/agent-quickstart.md @@ -1,60 +1,35 @@ -# Agent Quickstart +# Agent quickstart -A short reference for agents (and humans) writing code in this repo. For the -full architecture and module layout, see `docs/workflow.md` and the -project-root `CLAUDE.md`. +Distilled commands and paths agents reach for repeatedly. Pre-loading saves grep-then-discover cycles. -## JSON construction +## JSON construction (mandatory) -Tools that return structured data MUST use `@Serializable` data classes plus -`Json.encodeToString` (or `Json.encodeToJsonElement` if a tree is preferable). -Manual template literals — `"""{"field":"$value"}"""` — are NOT permitted in -production code. +Tools that return structured data MUST use `@Serializable` data classes plus `Json.encodeToString` (or `Json.encodeToJsonElement` if a tree is preferable). Manual template literals — `"""{"field":"$value"}"""` — are NOT permitted in production code. ### Why -Issue [#417](https://github.com/Monkopedia/rouse-context/issues/417) shipped -malformed JSON to user testing because a template-literal error helper -interpolated a raw exception message containing quotes, producing output the -connector proxy refused to parse. The `@Serializable` + `encodeToString` path -escapes embedded quotes/backslashes/control characters automatically; the -manual path does not. +`#417` shipped malformed JSON to user testing because a template-literal error helper interpolated a raw exception message containing quotes, producing output the connector proxy refused to parse. The `@Serializable` + `encodeToString` path escapes embedded quotes / backslashes / control characters automatically; the manual path does not. -### Enforcement +### Two enforcement gates -Two gates run in CI: - -1. **Test harness** (`McpToolTestHarness` in - `integrations/src/test/kotlin/com/rousecontext/integrations/testing/`) — - parse-asserts every `CallToolResult` body returned through the harness. - Any tool whose result body fails `Json.parseToJsonElement` causes the - test to fail with the offending tool name and body content. -2. **Static check** (`scripts/check-no-manual-json.sh`) — runs in the - `Android CI` workflow. Greps for `"""{` in `*/src/main/*.kt` and fails the - build if any match isn't allowlisted. +1. **Test harness** — `McpToolTestHarness` at `integrations/src/test/kotlin/com/rousecontext/integrations/testing/` parse-asserts every `CallToolResult` body returned through it. Any tool whose result body fails `Json.parseToJsonElement` causes the test to fail with the offending tool name and body content. +2. **Static check** — `scripts/check-no-manual-json.sh` runs in the Android CI workflow. Greps for `"""{` in `*/src/main/*.kt` and fails the build on unallowlisted matches. ### The `// allow-manual-json` marker -For the rare case where a template literal is genuinely safe (e.g. a -`@Preview`-only Compose fixture with hard-coded literals that never see -runtime input), append a marker comment to the line: +For the rare case where a template literal is genuinely safe (e.g. a `@Preview`-only Compose fixture with hard-coded literals that never see runtime input), append a marker comment to the line: ```kotlin val sample = """{"steps":7000}""" // allow-manual-json: @Preview fixture, no interpolation ``` -The reason text after the colon should describe **why this isn't a runtime -risk** — typically because no user-controlled value flows into the template. -A reviewer should treat the absence of a clear reason as a blocker. +The reason text after the colon should describe **why this isn't a runtime risk** — typically because no user-controlled value flows into the template. A reviewer should treat the absence of a clear reason as a blocker. -`AuditDetailScreen.kt` is implicitly allowlisted in the script because its -preview fixtures exist purely for IDE rendering and never reach runtime; new -files should use the inline marker rather than expanding the file allowlist. +`AuditDetailScreen.kt` is implicitly allowlisted in the script because its preview fixtures exist purely for IDE rendering; new files should use the inline marker rather than expanding the file allowlist. -## Test harness +### Test harness usage -When writing or refactoring an `*McpProvider*Test`, use -`McpToolTestHarness` rather than hand-rolling the mockk capture pattern: +When writing or refactoring an `*McpProvider*Test`, use `McpToolTestHarness` rather than hand-rolling the mockk capture pattern: ```kotlin private val harness = McpToolTestHarness() @@ -75,5 +50,151 @@ private val fakeConnection: ClientConnection = mockk(relaxed = true) } ``` -Pass `expectJsonBody = false` only for tools whose contract specifies a -plain-text body (rare). Document the reason inline if you do. +Pass `expectJsonBody = false` only for tools whose contract specifies a plain-text body (rare). Document the reason inline if you do. + +## Module → path map + +``` +:app app/ +:core:tunnel core/tunnel/ +:core:mcp core/mcp/ +:core:bridge core/bridge/ +:core:testfixtures core/testfixtures/ +:api api/ +:integrations integrations/ +:notifications notifications/ +:work work/ +:device-tests device-tests/ +:e2e e2e/ +relay (Rust) relay/ +``` + +Source-of-truth for the module list is `settings.gradle.kts:25-35`. + +## Build & test recipes + +All Gradle commands need `JAVA_HOME=/usr/lib/jvm/java-21-openjdk`. + +```bash +# Build a debug APK +./gradlew :app:assembleDebug +# → app/build/outputs/apk/debug/app-debug.apk + +# Build a release APK +./gradlew :app:assembleRelease +# → app/build/outputs/apk/release/app-release.apk + +# Unit tests for one module +./gradlew :core:mcp:jvmTest # KMP/JVM +./gradlew :app:testDebugUnitTest # Android-only +./gradlew :integrations:testDebugUnitTest # Android-only + +# Tunnel integration tests (against real relay binary) +./gradlew :core:tunnel:integrationTest --tests '*OAuthEndToEndTest*' + +# E2E (against a real device — requires adb host + serial) +./gradlew :e2e:e2eTest -Dadb.host=adolin -Dadb.serial=1B151FDEE008EY + +# Lint & analysis (whole tree) +./gradlew ktlintCheck detekt + +# Format +./gradlew ktlintFormat +``` + +## Relay (Rust) + +```bash +cd relay +cargo test +cargo clippy --all-targets --all-features -- -D warnings +cargo fmt +``` + +NEVER build the relay on the GCP VPS. The PR-to-main triggers `.github/workflows/relay-deploy.yml` (or similar) which cross-compiles + deploys. + +## GitHub CLI patterns + +```bash +# Concise CI status +gh pr checks NNN --json name,state --jq '.[] | "\(.name)\t\(.state)"' + +# Open PR +gh pr create --title "..." --body "..." + +# Merge + delete branch (squash) +gh pr merge NNN --squash --delete-branch + +# Issue management +gh issue create --title "..." --body "..." --label in-progress +gh issue edit NNN --add-label in-progress +gh issue edit NNN --remove-label in-progress +gh issue close NNN --comment "..." + +# Just the body of a comment thread +gh issue view NNN --comments --json comments --jq '.comments[].body' +``` + +## Local-branch-deletion gotcha + +If `gh pr merge --delete-branch` fails because another worktree has the branch checked out: + +```bash +cd /home/jmonk/git/rouse-context && git checkout --detach origin/main && git branch -d +``` + +## adb via SSH (test device) + +```bash +# adolin Pixel 6 Pro (agent-controlled) +ADB="ANDROID_SERIAL=1B151FDEE008EY /opt/android-sdk/platform-tools/adb" +ssh adolin "$ADB " + +# Common operations +ssh adolin "$ADB shell pidof com.rousecontext.debug" +ssh adolin "$ADB shell pm clear com.rousecontext.debug" +ssh adolin "$ADB shell am start -n com.rousecontext.debug/com.rousecontext.app.MainActivity" +ssh adolin "$ADB shell run-as com.rousecontext.debug ls files/" +ssh adolin "$ADB logcat -d -s 'Onboarding:*' 'TunnelClient:*' '*:S'" # filter at logcat level + +# Kill app cleanly (NOT am force-stop — that disables FCM until manual relaunch). +# Use cmd activity stop-app: kills the process including foreground services +# but leaves FCM reachable. See #394. +ssh adolin "$ADB shell cmd activity stop-app com.rousecontext.debug" + +# Install +scp app-debug.apk adolin:/tmp/ +ssh adolin "$ADB install -r /tmp/app-debug.apk" +``` + +## UI automation on device + +Drive Android UI via `uiautomator dump` XML, NOT screencap PNGs (resolution blows up context). See `/tmp/drive_onboard.sh` for the pattern (extract bounds via regex on `text="..."[^>]*bounds="[x1,y1][x2,y2]"`). + +## Relay over gcloud + +```bash +gcloud compute ssh relay --zone=us-central1-a --command='sudo journalctl -u rouse-relay -f' +gcloud compute ssh relay --zone=us-central1-a --command='systemctl status rouse-relay | head -5' +``` + +## Common files to know + +- `core/tunnel/src/jvmMain/.../OnboardingFlow.kt` — canonical onboarding sequence +- `app/src/main/.../OnboardingViewModel.kt` — canonical VM (post-#392 single-VM invariant) +- `app/src/main/.../navigation/AppNavigation.kt` + `Routes.kt` — nav graph + routes +- `core/tunnel/src/jvmMain/.../TunnelClientImpl.kt` — tunnel client +- `core/tunnel/src/jvmMain/.../MuxFrame.kt` / `MuxDemux.kt` — mux protocol +- `relay/src/api/` — relay HTTP/WS endpoints +- `relay/src/passthrough.rs` — TLS-passthrough decision + FCM wake +- `relay/src/rate_limit.rs` — rate limiters (FcmWakeThrottle, etc.) +- `docs/design/relay-api.md` — endpoint catalog (kept current as of #410) +- `docs/design/android-app.md` — module map + dep graph (kept current as of #408) +- `docs/design/overall.md` — system overview (kept current as of #409) +- `docs/ux-decisions.md` — UX flow decisions log + +## When in doubt + +- Code is the source of truth over docs. +- A `.claude/rules/*` rule is the source of truth over a memory entry. +- The user's most recent in-session direction overrides everything else.