Commit graph

23 commits

Author SHA1 Message Date
Jason Monk
ad628ae545
Fix #347: Client-subject notification copy + deep-link taps + Manage action (#362)
* Fix #347: Client-subject notification copy + deep-link taps + Manage action

Rewrite per-tool-call and session-summary notification copy per the locked
#342 design, wire up notification tap deep-links, and add a Manage action
button.

Per-tool-call notifications now read "${clientLabel} used ${humanized(toolName)}"
with the integration display name as body and the call timestamp rendered
natively via setWhen(). Session-summary notifications partition by client
label and emit one notification per client titled "${clientLabel} used
${count} ${tool|tools}" with up to 3 unique humanized tool names in the
body (Oxford comma, "and N more" past the cap).

Tap deep-links route to the audit-history screen — scrolled to the
specific call (per-tool-call) or filtered to the session time window
(summary) — and a single Manage action button points at the integration
manage page (or home, when a summary's calls span multiple integrations).

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>

* Update integration tests for per-client summary notification ids

#347 changed SessionSummaryNotifier from a single-id summary to one
notification per client, keyed by the clientLabel hash. The existing
:app:integrationTest fixtures still asserted against the legacy
SessionSummaryNotifier.NOTIFICATION_ID constant, so they failed in CI.

Updated each affected test to compute the expected id via
SessionSummaryNotifier.idForClient(<clientLabel>) and dropped the
now-unused NOTIFICATION_ID companion constant. The InMemoryTokenStore
resolves clientName=null to the clientId, so for tests that pass
clientName=null the expected label is their clientId string (e.g.
"race-test", "multi-test", "runtime-mode-switch"). ToolCallHappyPathTest
passes clientName = "Test Client" explicitly.

Also retargeted the MultiToolSessionTest title-string assertion from the
old "{count} calls across {N} integrations" copy to the new
"{clientLabel} used {count} tools" title shape.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>

* Move summary notification id bucket below per-call range

The previous summary id bucket (6000..15999) overlapped with
PerToolCallNotifier.BASE_ID = 7000, so any clientLabel whose hash
.mod(10000) landed in 1000..9999 produced an id that collided with
per-call notifications. RuntimeNotificationModeSwitchTest failed in CI
because its clientLabel "runtime-mode-switch" hashes to
idForClient(...) = 7465, which the test's "id >= PerToolCallNotifier.BASE_ID"
probe flagged as a per-call notification during phase 1 (SUMMARY mode).

Narrow the bucket to 4000..4999 so summary ids stay clear of the per-call
range. Collisions inside the 1000-entry bucket remain acceptable for the
expected <100-clients-on-a-device cardinality.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4 (1M context) <noreply@anthropic.com>
2026-04-22 03:11:08 -04:00
Jason Monk
ece9639d0f
Fix #233: Wire up Firebase Crashlytics for release builds (#261)
Post-ship crashes now surface in the Firebase console so we can react to
regressions without waiting for users to file manual bug reports. Debug
builds skip collection so local repros don't pollute the dashboard.

- Add Crashlytics Gradle plugin + dependency (piggybacks on the existing
  google-services.json already shipped for FCM, so no new secrets).
- Release builds upload R8 mapping files via the plugin so stack traces
  in the console stay deobfuscated; debug builds skip that slow step.
- Add a thin CrashReporter interface in :api so :app, :work, and other
  feature modules can log caught exceptions without taking a direct
  dependency on Firebase. Production impl (FirebaseCrashReporter) lives
  in :app; all other modules see only the contract plus CrashReporter.NoOp.
- Initialize collection state in RouseApplication.configureCrashReporting
  gated on BuildConfig.DEBUG — release builds collect by default, debug
  builds stay silent.
- Surface previously-swallowed failures at the sites called out in the
  issue: cert provisioning storage/key-gen failures in IntegrationSetup,
  tunnel connect/session-handler errors in TunnelForegroundService,
  terminal KeyGenerationFailed + CnMismatch outcomes in CertRenewalWorker,
  and KeyInfo inspection failures in AndroidKeystoreDeviceKeyManager.
  Uncaught exceptions are still handled automatically by Crashlytics'
  default UncaughtExceptionHandler, so these explicit reports only cover
  the paths we'd otherwise lose.

Privacy note: enabling Crashlytics collects crash stacks + Firebase
installation IDs, which will change the Play Store data-safety
declaration when we list. A user-facing opt-out toggle was scoped out
of this PR (see follow-up candidates) — current behaviour is on-by-
default for release builds.

Co-authored-by: Claude Opus 4 (1M context) <noreply@anthropic.com>
2026-04-18 21:46:22 -04:00
Jason Monk
9af562452c
Fix #248: Add Kover coverage measurement + CI aggregated report (#254)
Applies the `org.jetbrains.kotlinx.kover` 0.9.8 plugin to every production
module and wires the root project as the merging target, so
`./gradlew koverHtmlReport` produces one aggregated HTML + XML report across
`:app`, `:core:{tunnel,mcp,bridge}`, `:api`, `:notifications`, `:work`, and
`:integrations`.

Key pieces:

- `:core:tunnel:integrationTest` (real-relay subprocess tier) is now
  registered as a `KotlinJvmTest`, not a plain `Test`. Kover's KMP locator
  only auto-discovers tasks whose superclass is `KotlinJvmTest`; without
  this change, coverage from integration tests (e.g.
  `LazyMtlsKeyManager.chooseClientAlias`, exercised only by the real-relay
  flow) would silently be dropped from the report.
- Android `testReleaseUnitTest` tasks are excluded via
  `instrumentation.disabledForTestTasks`. CI only runs debug unit tests, and
  some release-variant unit tests aren't configured to work (e.g. Roborazzi
  screenshot tests in `:notifications`).
- Filters exclude generated code: `BuildConfig`, Compose `ComposableSingletons`
  + `*_ComposableKt`, kotlinx.serialization `$$serializer`, Room `*_Impl`,
  and Hilt/Koin glue for future-proofing.
- `:app`'s release-signing check is tightened to the exact signing-triggering
  task names so Kover's transitively-scheduled `packageReleaseResources` /
  `packageReleaseUnitTestForUnitTest` no longer trip the fail-fast guard.
- CI runs `koverHtmlReport koverXmlReport` after the existing test steps,
  uploads the HTML as `test-coverage` artifact, and writes a per-module line
  coverage summary to `$GITHUB_STEP_SUMMARY` (awk over each module's XML).

No `koverVerify` rule — report-only for the first pass as specified in #248.

Verified on the aggregated XML: `LazyMtlsKeyManager.chooseClientAlias` reports
`LINE covered=1 missed=0` (was unreachable from unit tests alone).

Co-authored-by: Claude Opus 4 (1M context) <noreply@anthropic.com>
2026-04-18 17:11:09 -04:00
Jason Monk
d1848db4a1 Show the actual AI client name in outreach launch notifications
"AI client wants to open Spotify" is now "Claude wants to open Spotify"
when the calling client supplied client_name during OAuth DCR. Falls
back to the generic "AI client" label when no name is set.

Plumbing:
 - TokenStore gains resolveClientName(integrationId, token). Both the
   in-memory and Room-backed implementations resolve from the stored
   DCR label (or null when that equals the opaque clientId).
 - McpRouting.handleMcp reads the name right after bearer validation
   and wraps the SDK dispatch in withContext(McpClientContext(...)),
   a tiny CoroutineContext.Element in :core:mcp.
 - LaunchRequestNotifierApi.postLaunchApp / postOpenLink take an
   optional clientName. LaunchRequestNotifier uses it to format the
   notification title (%1$s wants to open %2$s).
 - OutreachMcpProvider's launch_app and open_link tools read the name
   from coroutineContext[McpClientContext] and pass it to the notifier
   fallback.

Non-goals: this only touches the launch-app/open-link notifications.
Other user-facing prompts (auth approval, per-tool-call) already show
client identity via a different path and are unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 16:35:06 -04:00
Jason Monk
31090d1e98 Fix #114: Migrate notifier user-facing strings to :api resources
Extract all hard-coded user-facing strings from the seven notifier files
in :notifications into api/src/main/res/values/strings.xml so they are
audit-able in one place and can be referenced from any module that can
depend on :api. Follows the pattern established by the colors migration
(86b763ef): shared user-facing resources live in :api, notifiers look up
strings via context.getString(ApiR.string.*).

Covers:
- ForegroundNotifier (foreground service notification copy)
- FgsLimitNotifier (6-hour FGS limit copy)
- AuthRequestNotifier (approval request + Approve/Deny actions)
- LaunchRequestNotifier (AI-client launch / open-link + Open action)
- PerToolCallNotifier (tool-call title + body)
- SessionSummaryNotifier (session summary title with plurals)
- AndroidSecurityCheckNotifier (alert / info titles, check labels)

Compose UI strings across :app/ui/screens and dimens consolidation are
deferred to a follow-up pass under the same issue — notifier strings are
the well-defined first slice called out in the staged migration plan.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 22:34:18 -04:00
Jason Monk
0aa93f6ba8 Fix #136: Eliminate production runBlocking via reactive preferences
Replace the six synchronous DataStore reads that required runBlocking on
the main/Koin thread with suspending APIs and a live-snapshot holder.
This removes a long-standing violation of the project's coroutine rules
("test and main() only") and fixes the detail-screen staleness that
came from one-shot preference reads.

Changes:

- IntegrationStateStore, NotificationSettingsProvider, and
  IntegrationSettingsStore now expose suspend reads and
  Flow-returning observers; synchronous getters are gone.
- New PreferencesSnapshotHolder aggregates every UI-facing preference
  into a single StateFlow, collected WhileSubscribed via appScope.
  Compose and ViewModels read from .snapshot.value / observeX flows
  instead of blocking DataStore.
- IntegrationProviderRegistry caches enabled IDs in a StateFlow
  launched eagerly in appScope, preserving its synchronous
  ProviderRegistry contract for FCM and foreground-service callers.
- OutreachIntegration mirrors the DIRECT_LAUNCH toggle into a StateFlow
  so the canLaunchDirectly tool-call predicate stays non-suspending.
- DeviceRegistrationStatus starts false and transitions asynchronously
  once the cert store has been read.
- New OAuthHostnameProvider resolves the metadata hostname lazily in
  a suspend context; the McpSession starts with the "localhost"
  fallback that the Host header will override in real traffic.
- PerCallObserver.onToolCallRecorded is now suspend so
  PerToolCallNotifier can await NotificationSettingsProvider.settings().
- CI guardrail grep added to .github/workflows/ci.yml (lint job) that
  fails on any new production runBlocking. Commented KDoc references
  and the synchronous-API bridge in TlsAcceptor.kt (InputStream.read /
  OutputStream.write, non-suspending by java.io contract) are
  explicitly excluded.

Tests:

- New PreferencesSnapshotHolderTest exercises the snapshot flow.
- Existing store tests rewrap assertions in runBlocking (allowed in
  tests per project rules).
- MockK call sites for suspend methods migrated from every/verify
  to coEvery/coVerify as required.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:56:38 -04:00
Jason Monk
86b763ef78 Fix #114: Consolidate brand colors into :api resources (colors only)
Add api/src/main/res/values/colors.xml mirroring the Theme.kt palette so
:notifications and :work can reference brand colors via R.color.* instead
of inlining hex literals. Replace inline 0xFF0a1628 in FgsLimitNotifier
and ForegroundNotifier, and 0xFFea4335 in AuthRequestNotifier, with
ContextCompat.getColor(context, ApiR.color.rouse_*) lookups.

Theme.kt stays as-is (option a in the plan); palette is duplicated but
XML mirrors the Kotlin source of truth. Strings and dimens migration
will land in follow-up PRs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:06:14 -04:00
Jason Monk
4bbe3930c7 Fix #107: Wire SettingsScreen toggle + AuditHistoryViewModel for showAllMcpMessages
Follow-up to #105, which landed the backend (RoomAuditListener.onRequest,
mcp_request_entries table, NotificationSettings.showAllMcpMessages). This
wires the flag through the UI so users can actually see the extra entries.

SettingsScreen gains a SwitchRow in the Audit Notifications section. The
toggle binds to SettingsViewModel.showAllMcpMessages, which observes a new
NotificationSettingsProvider.observeSettings() Flow so toggle state reacts
to writes from either the Settings screen or the Audit tab.

AuditHistoryViewModel now takes an optional McpRequestDao and
NotificationSettingsProvider. When the toggle is off, only tool-call
entries surface (unchanged). When on, generic MCP request entries are
queried and merged with tool calls, sorted timestamp-desc within each
day group. The display type is a sealed AuditHistoryItem hierarchy with
ToolCall and Request variants so the view renders each shape correctly
(tool calls stay clickable; requests show a muted Api icon + method).

Sealed class chosen over a polymorphic AuditEntry because the two row
shapes differ meaningfully in interactivity (detail navigation vs none)
and visual treatment, and the compiler-enforced exhaustiveness prevents
missing a variant when future row types are added.

Screenshot goldens regenerated: existing Settings screens pick up the new
SwitchRow; a new 36a_audit_history_show_all_mcp_messages pair captures
the interleaved mixed view.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 06:35:37 -04:00
Jason Monk
87146cbab7 Fix #102: Outreach SYSTEM_ALERT_WINDOW opt-in + notification fallback for background launches
Android 14+ tightened background-activity-start rules: the sender of a
PendingIntent must hold BAL privilege at send() time. Rouse's
FOREGROUND_SERVICE_TYPE_DATA_SYNC does NOT grant BAL, so open_link and
launch_app silently no-op when the app is backgrounded on Pixel 10 Pro
and similar API 34+ devices.

This change adds a two-part fix:

1. SYSTEM_ALERT_WINDOW opt-in in the Outreach setup screen. When the user
   enables it and grants the OS permission, the PendingIntent path keeps
   working as before. The new SwitchRow only appears on API 34+
   (`directLaunchApplicable`); older devices don't need the workaround.

2. Tap-to-launch notification fallback when the opt-in is off or denied.
   A new LaunchRequestNotifier in :notifications posts a notification
   whose tap fires the PendingIntent — notification-tap launches bypass
   BAL restrictions. The channel `rouse_outreach_launch` defaults to
   IMPORTANCE_DEFAULT so it shows in the shade without heads-up.

OutreachMcpProvider now takes a `canLaunchDirectly` predicate and an
optional `LaunchRequestNotifierApi`. OutreachIntegration wires both from
the IntegrationSettingsStore toggle and the overlay permission check,
so the decision is re-evaluated on every tool call.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:59:03 -04:00
Jason Monk
e9a7ff582f Fix #105: Audit all MCP JSON-RPC methods, not just tool calls
Previously, only `tools/call` invocations were surfaced to the audit
listener, so a connected client could enumerate the full tool menu
(`tools/list`, `resources/read`, etc.) without leaving any user-visible
trace.

Changes:
- Extend `AuditListener` with a generic `onRequest(event: McpRequestEvent)`
  that fires for every JSON-RPC method. `onToolCall` remains the
  specialized path for `tools/call` so the existing tool-call UI stays
  intact.
- `McpRouting.emitAuditEvent` now fires `onRequest` for every dispatched
  method, and additionally fires `onToolCall` for `tools/call` (helpers
  split for detekt complexity).
- Add Room `McpRequestEntry` entity + `McpRequestDao` parallel to the
  existing `AuditEntry`. DB version 2 -> 3 with a migration that creates
  the new `mcp_request_entries` table without touching existing data.
- `RoomAuditListener` accepts an optional `McpRequestDao` and persists
  every request when supplied, encrypting `paramsJson` at rest via the
  existing `FieldEncryptor` (covers `prompts/get` / `resources/read`
  which may carry user data).
- `NotificationSettingsProvider` + DataStore gain
  `showAllMcpMessages` (default false) so the UI can later gate
  visibility behind a user opt-in.

Tests:
- `McpProtocolTest`: `onRequest` fires for every method; `tools/call`
  fires both callbacks; params/duration/resultBytes captured;
  onToolCall does not fire for non-tool methods.
- `RoomAuditListenerTest`: `onRequest` persists to the request DAO;
  is a no-op when the DAO is absent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:51:13 -04:00
Jason Monk
2e9eee6908 Fix #103: Replace status bar icon PNGs with vector + alpha gradient
Regenerate ic_stat_rouse as a vector drawable (24dp viewport) with a
solid circle path and vertical alpha gradient (40% -> 100% opacity).
The alpha gradient survives Android's status-bar tinting (which
flattens color gradients) and evokes the halftone-density feel of the
brand icon at notification-shade size.

Deletes five PNG density variants in favor of the single vector — the
previous PNG conversion from #55 quantized the halftone dot pattern
into an unrecognizable silhouette at 24dp.

No code changes: AuthRequestNotifier, SessionSummaryPoster,
NotificationAdapter, and ForegroundNotification continue to reference
ApiR.drawable.ic_stat_rouse, which now resolves to the vector.

Launcher monochrome variant (for Android 13+ themed icons) deferred
as a follow-up — requires a matching foreground silhouette asset.
2026-04-13 22:08:50 -04:00
Jason Monk
b86fea6e45 Fix #69: Move notification mode to global onboarding
Adds a one-time NotificationPreferences step between the Welcome screen
and Home so users pick their audit notification mode (Summary / Each
usage / Suppress) once during onboarding rather than being re-prompted
in every per-integration setup. The setting remains editable from the
global Settings screen, and both surfaces now read/write through a
shared NotificationSettingsProvider backed by DataStore.

Per-integration setup screens (Notification, Outreach, Usage, Health
Connect) never surfaced the mode in the first place, so no UI removal
is required there — they keep their integration-specific options only.

- Adds NotificationSettingsProvider.setPostSessionMode() and implements
  persistence in DataStoreNotificationSettingsProvider.
- Wires SettingsViewModel.setPostSessionMode to actually persist (was a
  no-op TODO).
- Adds NotificationPreferencesViewModel seeded from the provider so the
  previously-chosen option is pre-selected if the screen is revisited.
- Adds Routes.NOTIFICATION_PREFERENCES as an onboarding route; Welcome
  "Get Started" now navigates there, and Continue on the preferences
  screen kicks off relay registration before popping to HOME.
- Covers new flow with ViewModel tests, SettingsViewModel persistence
  tests, and BackStack/NavigationBarVisibility tests for the new route.

Closes #69.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:53:30 -04:00
Jason Monk
304e40a8d7 Fix #18: Update Gradle wrapper and dependencies to latest stable
- Gradle: 8.12 -> 8.14
- AGP: 8.10.0 -> 8.13.2
- Compose BOM: 2025.02.00 -> 2026.03.00
- Firebase BOM: 33.10.0 -> 34.12.0 (migrate -ktx to main modules)
- Ktor: 3.1.3 -> 3.4.1
- Activity/Activity-Compose: 1.10.1 -> 1.12.4
- Lifecycle: 2.9.0 -> 2.10.0
- Navigation Compose: 2.8.9 -> 2.9.6
- Room: 2.7.1 -> 2.8.4
- WorkManager: 2.10.0 -> 2.11.2
- Koin: 4.0.0 -> 4.1.1
- DataStore: 1.1.1 -> 1.2.1
- OkHttp: 4.12.0 -> 5.3.0
- kotlinx-serialization-json: 1.9.0 -> 1.11.0
- kotlinx-io: 0.7.0 -> 0.9.0
- Health Connect: 1.1.0-alpha12 -> 1.1.0 (stable)
- Roborazzi: 1.36.0 -> 1.58.0
- Robolectric: 4.14.1 -> 4.16.1
- MockK: 1.13.16 -> 1.14.9
- JUnit Jupiter: 5.11.4 -> 5.13.4
- ktlint plugin: 12.2.0 -> 14.2.0
- detekt: 1.23.7 -> 1.23.8
- Splashscreen: 1.0.1 -> 1.2.0
- AppCompat: 1.7.0 -> 1.7.1
- Acme4j: 3.5.0 -> 3.5.1
- Google Services: 4.4.2 -> 4.4.4
- compileSdk: 35 -> 36

Also runs ktlintFormat to fix new class-signature rule violations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:36:46 -04:00
Jason Monk
bfba41c4a4 Fix #55: Convert notification icon to PNG for proper rendering
Vector drawable notification icons can render incorrectly in the
notification tray. Replace with density-specific PNGs (mdpi through
xxxhdpi) generated from the same sun-rising pattern, white on
transparent for proper Android tinting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 15:11:22 -04:00
Jason Monk
1c47ace4c2 Fix #47: Move shared icon to :api module
Move ic_stat_rouse drawable from :notifications to :api so that
:outreach can reference it without depending on :notifications.
Replace :notifications dependency with :api in outreach/build.gradle.kts.
Add :api dependency to :work for the same icon reference.
Update all R.drawable.ic_stat_rouse references to use ApiR import alias
in notifications module sources (required by nonTransitiveRClass=true).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 18:44:55 -04:00
Jason Monk
3ce208e232 Lower minSdk from 29 to 24 to support ~98% of active Android devices
- Enable core library desugaring for java.time and java.util.Base64 usage
  in app, core:mcp, health, and notifications modules
- Guard NotificationChannel creation (API 26+) in NotificationChannels
  and OutreachMcpProvider
- Guard FOREGROUND_SERVICE_TYPE_DATA_SYNC (API 29+) in
  TunnelForegroundService, fall back to type-less startForeground
- Use ContextCompat.startForegroundService() in FcmReceiver and
  TestWakeReceiver for pre-API-26 compatibility
- Guard Health Connect availability check for API 28+ in
  HealthConnectIntegration
- Add tools:overrideLibrary for Health Connect in health manifest
- Pin Robolectric to SDK 33 in outreach, work, and notifications test
  resources to match app module

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 12:15:09 -04:00
Jason Monk
6fe84536a8 Make dashboard reactive so integration and audit state updates without navigation
The dashboard was stale because it used a manual refreshTrigger instead of
reactive data sources. Now IntegrationStateStore exposes observeChanges()
and AuditDao exposes observeRecent() — both Flows that the ViewModel combines
so the UI updates immediately when integrations are toggled or new audit
entries arrive.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 19:47:58 -04:00
Jason Monk
44670a6696 Remove stale mcp-core/ and mcp-health/ directories, fix conflict markers in health build files 2026-04-05 12:42:38 -04:00
Jason Monk
1b357a3dc5 Add real Health Connect MCP tools: get_steps, get_heart_rate, get_sleep 2026-04-05 11:32:55 -04:00
Jason Monk
45c39f36ac Fix conflict marker in api/build.gradle.kts from T-16 merge 2026-04-05 10:01:32 -04:00
Jason Monk
535fb2e430 Merge T-16: Tunnel + relay integration tests with mock relay server 2026-04-05 10:00:46 -04:00
Jason Monk
78f0fc65d3 Define McpIntegration, IntegrationStateStore, and IntegrationState contracts
Implement the :api module provider contract (T-6):
- McpIntegration interface for integration modules to implement
- IntegrationStateStore interface for user enable/disable persistence
- IntegrationState enum with deriveIntegrationState() derivation function
- NotificationSettingsProvider and NotificationSettings for notification model
- Tests for state derivation logic and state store contract

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 07:31:56 -04:00
Jason Monk
65d71fba10 Restructure Gradle modules: flat layout to nested architecture
Rename modules to match target architecture:
- mcp-core/ -> core/mcp/ (MCP session, providers, audit)
- tunnel/ -> core/tunnel/ (mux protocol, TLS, CertificateStore)
- mcp-health/ -> health/ (Health Connect integration)

Create new empty modules:
- api/ (integration contract layer, depends on core:mcp)
- notifications/ (notification state machine + audit persistence)
- work/ (foreground service, FCM, WorkManager)

Update all module references in settings.gradle.kts, app/build.gradle.kts,
health/build.gradle.kts. Update .claude/CLAUDE.md with Java 21 and new
module paths. Fix .gitignore to cover all build/ directories.

Verified: ./gradlew assemble, :core:mcp:testDebugUnitTest, ktlintCheck
all pass with JAVA_HOME=/usr/lib/jvm/java-21-openjdk.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 07:18:03 -04:00