The :ammolite module contained no production Kotlin/Java sources (just
a manifest, build.gradle, and proguard stubs) and no module in the
codebase imports com.vitorpamplona.ammolite.*.
Removes:
- ammolite/ directory (5 files)
- :ammolite project include in settings.gradle
- implementation project(':ammolite') from :amethyst
- androidTestImplementation project(':ammolite') from :benchmark
- :ammolite:testDebugUnitTest from CI workflow and pre-push hook
- -keep class com.vitorpamplona.ammolite.** rules from
:amethyst, :commons, and :desktopApp proguard files
- Stale references in CONTRIBUTING.md, CLAUDE.md, and the
gradle-expert skill dependency-graph doc
Small build-graph win: one fewer module to configure, compile, lint,
and spotless-check on every build, and one fewer unit-test target in
both CI and the local pre-push hook.
15 KiB
Amethyst
Project Overview
Amethyst is a Nostr Client for Android that was made for Android-only and has been slowly switching
over to a Kotlin Multiplatform project. The main modules are: quartz, commons, amethyst,
desktopApp, cli, plus the audio-rooms transport stack quic + nestsClient. Quartz should
contain implementations of Nostr specifications and utilities to help implement them. Commons stores
shared code between Amethyst Android (amethyst) and Amethyst Desktop (desktopApp). The Desktop
App is designed to be mouse first and so uses a completely different screen and navigation
architecture while sharing the back end components with the android counterpart. cli ships amy,
a non-interactive JVM command-line client that drives the same quartz + commons code — used by
humans, agents, and interop tests. quic is a from-scratch pure-Kotlin QUIC v1 + HTTP/3 +
WebTransport client (no JNI, no BouncyCastle), built because no Android-compatible Java QUIC library
exists. nestsClient runs the audio-room protocol on top of :quic for the NIP-53
audio-rooms feature. It implements both IETF draft-ietf-moq-transport-17 (under
moq/) and moq-lite Lite-03 (kixelated's variant, under moq/lite/); the
production listener AND speaker paths both run on moq-lite to interop with the
nostrnests reference relay. The IETF code is kept as a reference + unit-test
implementation for any future IETF target; see
nestsClient/plans/2026-04-26-moq-lite-gap.md.
Architecture
amethyst/
├── quartz/ # Nostr KMP library (protocol only, no UI)
│ └── src/
│ ├── commonMain/ # Shared Nostr protocol, data models
│ ├── androidMain/ # Android-specific (crypto, storage)
│ ├── jvmMain/ # Desktop JVM-specific
│ └── iosMain/ # iOS-specific
├── commons/ # Shared UI components (convert to KMP)
│ └── src/
│ ├── commonMain/ # Shared composables, icons, state
│ ├── androidMain/ # Android-specific UI utilities
│ └── jvmMain/ # Desktop-specific UI utilities
├── quic/ # Pure-Kotlin QUIC v1 + HTTP/3 + WebTransport (audio-rooms transport)
│ └── src/
│ ├── commonMain/ # Protocol, frame/packet codecs, TLS state machine
│ ├── jvmAndroid/ # JCA-backed AEAD + UDP socket actuals
│ └── commonTest/ # RFC vector + adversarial tests
├── nestsClient/ # Audio-room client (IETF MoQ-transport today; moq-lite phase pending)
│ └── src/
│ ├── commonMain/ # MoQ session, NestsListener, audio glue
│ └── jvmAndroid/ # Opus encode/decode, AudioRecord/AudioTrack
├── desktopApp/ # Desktop JVM application (layouts, navigation)
├── amethyst/ # Android app (layouts, navigation)
└── cli/ # Amy — non-interactive CLI (JVM only, no Compose)
Sharing Philosophy:
quartz/= Nostr business logic, protocol, data (no UI)commons/= Shared UI components, icons, composables, flows and ViewModelsquic/= Transport library (QUIC + HTTP/3 + WebTransport); reusable for any KMP project that needs MoQ. Has no Android-framework dependencies.nestsClient/= MoQ + audio-rooms client; takes:quicas transport, Quartz for crypto,MediaCodec/AudioRecord/AudioTrackfor audio.amethyst/&desktopApp/= Platform-native layouts and navigationcli/= Thin assembly layer overquartz/+commons/(no new logic allowed)
Plans per module: design docs for new subsystems live in the owning
module's plans/YYYY-MM-DD-<slug>.md (e.g. cli/plans/, commons/plans/).
The global docs/plans/ folder is frozen — don't add new plans there.
Tech Stack
| Layer | Technology |
|---|---|
| Core | Quartz (Nostr KMP) |
| UI | Compose Multiplatform 1.10.3 |
| Async | kotlinx.coroutines + Flow |
| Network | OkHttp (JVM) |
| Serialization | Jackson |
| DI | Manual / Koin |
| Build | Gradle 8.x, Kotlin 2.3.20 |
Skills
Specialized skills provide domain expertise with bundled resources and patterns:
| Skill | Expertise | When to Use |
|---|---|---|
nostr-expert |
Nostr protocol (Quartz library) | Event types, NIPs, tags, signing, Bech32, NIP-44, LargeCache |
kotlin-expert |
Advanced Kotlin patterns | StateFlow, sealed classes, @Immutable, DSLs, common utilities |
kotlin-coroutines |
Advanced async patterns | supervisorScope, callbackFlow, relay pools, testing |
kotlin-multiplatform |
Platform abstraction | expect/actual, source sets, sharing decisions |
compose-expert |
Shared UI components | Material3, state hoisting, recomposition, rich-text parsing |
android-expert |
Android platform | Navigation, permissions, lifecycle, Material3, Coil image loading |
desktop-expert |
Desktop platform | Window, MenuBar, keyboard shortcuts, DeckLayout |
gradle-expert |
Build system | Dependencies, versioning, packaging, optimization |
account-state |
Account + LocalCache |
Per-user StateFlows, event store, adding account-scoped settings |
relay-client |
Subscriptions & filter assembly | ComposeSubscriptionManager, assemblers, preloaders, EOSE |
feed-patterns |
Feeds & DAL | FeedFilter, AdditiveComplexFeedFilter, FeedViewModel family |
auth-signers |
NostrSigner implementations |
Local, NIP-46 bunker, NIP-55 Android external signer |
quartz-integration |
Quartz as an external library | Gradle setup, NostrClient, KeyPair, for external projects |
amy-expert |
Amy CLI (cli/ module) |
Adding amy <verb> commands, JSON output contract, extracting logic from amethyst/ into commons/ so CLI can call it |
find-missing-translations |
Utility | Extract untranslated Android strings |
find-non-lambda-logs |
Utility | Audit Log calls for lambda overloads |
Technique-layer skills (Compose / Kotlin best practices)
These are general Compose/Kotlin decision-framework skills (vendored from
chrisbanes/skills). The skills above are codebase-oriented ("where is X in
Amethyst, what pattern do we use"); these are technique-oriented ("what is
the correct Compose/Kotlin design here"). They complement — not replace — the
codebase skills: e.g. compose-expert tells you where shared composables live,
compose-slot-api-pattern tells you how to shape their public API.
| Skill | Expertise | Complements |
|---|---|---|
compose-recomposition-performance |
Router: which recomposition axis is the problem | compose-expert |
compose-stability-diagnostics |
Compiler reports, strong skipping, ImmutableList at UI boundaries |
compose-expert, kotlin-expert |
compose-state-deferred-reads |
Phase-aware state reads, block-form modifiers, provider lambdas | compose-expert |
compose-slot-api-pattern |
@Composable slot design for reusable components |
compose-expert |
compose-modifier-and-layout-style |
modifier parameter conventions, chain construction, conditional hoisting |
compose-expert |
compose-side-effects |
LaunchedEffect/DisposableEffect/SideEffect, keys, rememberUpdatedState |
compose-expert |
compose-state-holder-ui-split |
State-holder vs plain-UI composable split | compose-expert, feed-patterns |
kotlin-flow-state-event-modeling |
StateFlow/SharedFlow/Channel choice, sentinels, stateIn, update {} |
kotlin-expert |
kotlin-coroutines-structured-concurrency |
Stored-scope anti-pattern, suspend boundaries, runBlocking, cancellation |
kotlin-coroutines |
kotlin-types-value-class |
@JvmInline value class vs data class, Compose stability |
kotlin-expert |
Workflow
When you ask for a feature:
- Quick skill assessment - I identify which skills are relevant
- Propose which skills - I present which skills I'll use for the task
- Get approval - You review and approve (or adjust) the skill selection
- Review plan using approved skills - I invoke the approved skills to create detailed implementation plan
- Execute with skills - Skills collaborate to implement the feature
Example:
You: "Add video support to notes"
Me: "I'll use:
- /nostr-expert (NIP-71 video events)
- /compose-expert (video player UI)
- /android-expert (platform video APIs)
Proceed?"
You: "yes"
Me: [invokes skills to create plan]
"Plan from skills:
1. nostr-expert: Use NIP-71 kind 34235 for video events...
2. compose-expert: Create VideoPlayer composable in commons...
3. android-expert: Use ExoPlayer for Android...
Proceed with implementation?"
You: "yes"
Me: [implements using skill guidance]
Commands
/desktop-run- Build and run desktop app/nip <number>- Get NIP implementation guidance
Feature Workflow
CRITICAL: Always check existing implementations first before creating new code!
When picking up a new task or feature, follow this process:
Step 0: Survey Existing Implementation (MANDATORY)
Before writing ANY code, thoroughly audit ALL modules:
-
Search for existing implementations across all modules:
# Search in quartz for protocol/business logic grep -r "class.*Manager\|object.*Cache\|class.*Filter" quartz/src/commonMain/ # Search in commons for UI components grep -r "@Composable.*Card\|@Composable.*View\|@Composable.*Dialog" commons/src/ # Search in amethyst for Android patterns grep -r "class.*ViewModel\|class.*Account\|class.*State" amethyst/src/main/java/ # Search for specific functionality grep -r "fun isFollowing\|fun subscribe\|fun getMetadata" {quartz,commons,amethyst}/src/ -
Understand existing architecture patterns:
- Event stores and caching systems
- State management patterns (StateFlow, mutable states)
- ViewModel patterns and lifecycle handling
- Filter builders and relay subscription patterns
- UI component hierarchies
-
Key principle: Most logic already exists! Your job is to:
- Reuse existing protocol/business logic from quartz
- Extract shareable UI components AND ViewModels from amethyst to commons
- Create platform-specific layouts/navigation for Desktop
- NOT duplicate existing managers, caches, or state systems
-
Document findings in implementation plan as a matrix:
File/Component Status Location Action FilterBuilders ✅ Exists quartz/relay/filters/ Reuse as-is NoteCard 📦 Extract amethyst/ui/note/ → commons/ Extract to commons HomeFeedViewModel 📦 Extract amethyst/ → commons/commonMain/viewmodels/ Extract to commons ProfileCache ⚠️ Avoid N/A Already in User/Account pattern Legend:
- ✅ Reuse - Exists and can be used directly
- 📦 Extract - Exists in Android, needs extraction to commons
- 🆕 New - Doesn't exist, needs creation (platform-specific only)
- ⚠️ Avoid - Duplicate functionality, use existing pattern instead
Step 1: Analyze Android Implementation
After surveying (Step 0), deeply examine the Android implementation:
- Find the relevant feature/component in
amethyst/module - Understand the current implementation patterns
- Identify dependencies and integrations
- Map out what code can be shared vs platform-specific
Step 2: Create Implementation Plan
Before coding, create a plan that categorizes work into three buckets:
| Category | Description | Location |
|---|---|---|
| Android-Specific | Platform-native layouts, navigation patterns | amethyst/, androidMain/ |
| Reusable (Shared) | Business logic, UI components, ViewModels, state management | quartz/commonMain/, commons/commonMain/ |
| Desktop-Specific | Desktop-native layouts, navigation patterns, platform APIs | desktopApp/, jvmMain/ |
Step 3: Code Sharing Strategy
Share:
- Business logic and data models →
quartz/commonMain/ - Major UI components (cards, lists, dialogs) →
commons/commonMain/ - ViewModels (state, business logic) →
commons/commonMain/viewmodels/ - Icons and visual assets →
commons/commonMain/
Keep Platform-Native:
- Screen composables (layout, scaffolding) - Desktop uses
Window, Android usesActivity - Navigation patterns (sidebar vs bottom nav)
- Platform-specific interactions (gestures, keyboard shortcuts)
- System integrations (notifications, file pickers)
Rationale: ViewModels contain platform-agnostic state management (StateFlow/SharedFlow) and business logic. Screens consume ViewModels but render differently (Desktop sidebar + content area vs Android bottom nav).
Step 4: Extract Shared Components
When extracting UI components:
- Identify reusable composables in Android code
- Move to
commons/commonMain/(consult/compose-expertfor patterns) - Create expect/actual declarations for platform-specific behavior (consult
/kotlin-multiplatform) - Update both Android and Desktop to use shared component
Note: quartz/ is protocol-only (no composables). Shared UI goes in commons/ after converting it to KMP.
Build Commands
# Run desktop app
./gradlew :desktopApp:run
# Run Android app
./gradlew :amethyst:installDebug
# Build Quartz for all targets
./gradlew :quartz:build
# Run tests
./gradlew test
# Format code
./gradlew spotlessApply
Quartz KMP Structure
The Quartz library uses expect/actual for platform-specific implementations:
// commonMain - shared protocol logic
expect class CryptoProvider {
fun sign(message: ByteArray, privateKey: ByteArray): ByteArray
fun verify(message: ByteArray, signature: ByteArray, publicKey: ByteArray): Boolean
}
// androidMain - uses secp256k1-kmp-jni-android
actual class CryptoProvider { /* Android implementation */ }
// jvmMain - uses secp256k1-kmp-jni-jvm
actual class CryptoProvider { /* JVM implementation */ }
Key Patterns
Platform Abstraction
// commonMain
expect fun openExternalUrl(url: String)
// androidMain
actual fun openExternalUrl(url: String) {
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
}
// jvmMain (Desktop)
actual fun openExternalUrl(url: String) {
Desktop.getDesktop().browse(URI(url))
}
Code Formatting
After completing any task that modifies Kotlin files, always run:
./gradlew spotlessApply
Do this before considering the task complete.
Kotlin Style
- Never write fully-qualified class names inline in function bodies. Add an
importfor the class and reference it by its simple name. WriteEvent(withimport com.vitorpamplona.quartz...Event), notcom.vitorpamplona.quartz...Eventin the middle of code. - The only acceptable inline fully-qualified names are: a genuine name
collision (prefer
import ... as Aliasinstead), or where the language requires it. Comments, KDoc, and string literals are exempt.
Navigation Shell
- Desktop: Sidebar + main content area
- Android: Bottom navigation
Git Workflow
- Branch:
feat/desktop-<feature>orfix/desktop-<issue> - Commits: Conventional commits (
feat:,fix:, etc.) - Never use
--no-verify