Files
amethyst/CONTRIBUTING.md
T
Claude 9afa8127da refactor!: take quartz from vitorpamplona/quartz
Quartz moves to its own repo, the way the crypto primitives moved to
crypto-kmp. Every module depends on com.vitorpamplona.quartz:quartz
2.0.0 (libs.quartz) instead of :quartz, and -PquartzDir=../quartz
builds a local checkout instead of the artifact, for every module and
for quic's own quartz dependency (which replaces the root build's
substitution of the published quartz with :quartz).

What moved with quartz: its CI jobs (lint tasks, test-quartz-linux-native,
test-quartz-ios), the Maven Central publish step of the release
workflow, jitpack.yml, tools/unicode-nfkc, the nostr-expert and
event-store-semantics skills, the agent-skill check, and the README's
library usage docs.

What stays here:
- The four quartz client tests that need a real Ktor socket relay
  (TrustNetworkSync*, GiantReqStreamTest, BulkDownloadBenchmark) move to
  geode's tests; geode forwards -PprodRelayBench for the latter. quartz's
  other relay-client tests now use a test relay of its own, which ends
  quartz's test dependency on geode.
- The benchmark module's resources (previously read from quartz's
  androidDeviceTest resources) move into benchmark/; relayBench --real
  reads the corpus from there.
- NoteComposeKindCoverageTest reads EventFactory.kt from quartz's JVM
  sources jar, or from the -PquartzDir checkout, passed by commonsUI's
  build.
- commons and commonsUI declare crypto-kmp (and commons mls-kmp), which
  they import directly. Through quartz's api alone, a -PcryptoKmpDir
  checkout never reached the common metadata compile.
- The catalog drops what only quartz used (kchesslib, negentropy,
  jackson-databind, sqlite-bundled, secp256k1-kmp common, compose
  runtime-annotation, the maven-publish plugin).

agent_config_check checks a skill's quartz/... paths against a ../quartz
checkout when there is one and skips them otherwise. AGENTS.md, the
skills, CONTRIBUTING, BUILDING, RELEASE_OPS, PLANS and the READMEs point
at the new repo.

Needs quartz 2.0.0 (and crypto-kmp 1.0.0) on Maven Central before CI can
resolve it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEkJaDWYrcTbGvT8JLQCrG
2026-10-21 16:32:25 +00:00

16 KiB
Raw Permalink Blame History

Contributing to Amethyst

Thanks for your interest in improving Amethyst. This document captures the expectations, conventions, and review rules for code, documentation, and translation contributions across all modules in this repository (amethyst/, desktopApp/, commons/, commonsUI/, cli/, nestsClient/). The Quartz library has its own repo and its own contributing rules: vitorpamplona/quartz.

By contributing, you agree to license your work under the MIT license. Any work contributed where you are not the original author must contain its license header with the original author(s) and source.


Ways to contribute

  • Fix bugs or implement features (see issues and the bounty notes in the issue templates).
  • Improve documentation in README.md, BUILDING.md, SECURITY.md, the per-module plans/ folders, and the .claude/ skill docs.
  • Add or improve translations on Crowdin.
  • Report bugs, suggest features, or open issues at github.com/vitorpamplona/amethyst/issues or the Nostr mirror at gitworkshop.dev/repo/amethyst.
  • Send patches over Nostr using GitStr — see the address at the bottom of the README.

Proof of testing (new / occasional contributors)

We accept pull requests authored by humans and pull requests authored with help from AI coding assistants (Claude Code, Copilot, Cursor, Codex, etc.) under the same rules:

  • You are the author of record. You are responsible for understanding, testing, and defending every line — including code an assistant wrote for you. "The AI did it" is not a valid response to review feedback.
  • No hallucinated APIs or imports. Don't submit code that calls functions, classes, or libraries that don't exist in this repo or its declared dependencies. Build and run it before you push.
  • No machine-translated locale files. Translations go through Crowdin so native speakers can review them.
  • Disclose substantial AI involvement with a one-line note in the PR description. Courtesy, not a gate.

If you are not a regular contributor to this repository — first PR, or sporadic enough that maintainers wouldn't recognize your handle — the PR description must include proof that you actually ran the change:

  • Code/logic changes: paste the test output, CLI command, or log lines that show the new path executing. CI green is necessary but not sufficient.
  • UI changes (amethyst/, desktopApp/, or @Composable code in commons/): attach screenshots or a short recording from a real device or emulator. See the PR template for the exact checklist (light + dark, device / OS info, empty / loading / error states).
  • Build / Gradle / packaging changes: paste the ./gradlew command and the tail of its output.
  • Translation-only PRs: mention which locale and which strings you touched; screenshots not required.

Once you have an established track record, a short test plan is enough on subsequent PRs. Maintainers may still ask for screenshots on visual changes.

If you can't run a particular target locally (e.g. no macOS, but your change affects the DMG build), say so explicitly. Honest "I couldn't test this on Windows" beats a silent guess.

If you used an AI coding assistant for a substantial portion of the diff, also read CONTRIBUTING-WITH-AI.md. Its main rule: no PR may be opened until a human has reviewed the whole diff and tested the app by hand on a device. An agent must never open the PR itself.

Reporting bugs and requesting features

Use the GitHub issue templates at .github/ISSUE_TEMPLATE/ rather than opening a blank issue — they exist so triage can route the report without a follow-up round trip.

Bug report ([BUG] title prefix): describe the bug, give numbered repro steps from a fresh app launch, state expected behavior, attach video / screenshots for anything visual or timing-sensitive, list device info (phone brand/model, Android version, app version, flavour, Amber version if applicable), and include a bounty.

Feature request ([FEATURE] title prefix): describe the user-facing outcome and include a bounty. Skip implementation suggestions unless they're load-bearing.

Bounties

Maintainer time is allocated by bounty size. From the templates: "If no bounty is offered, not even a small one, this bug will not be worked on because it doesn't matter to you." Issues are prioritized by bounty ÷ effort, even a few hundred sats outrank a zero-bounty issue, and bounties aren't refunded. If you can fix it yourself, just open the PR — bounties exist to move issues nobody is currently working on. Security issues do not need a bounty and should not be filed publicly; see SECURITY.md.

A bounty is not, and will never be, a promise of payment. Even if the issue's author likes your solution and it is approved and merged, the author can still decide, for any reason, whether to pay you or not. If you take on a bounty, you accept the risk of not being paid even after the work is done.

Security issues

Do not file security vulnerabilities as public GitHub issues. Use GitHub private vulnerability reporting instead. See SECURITY.md for scope, expected response times, and disclosure policy.

Development setup

Prerequisites:

  1. JDK 21+ (Zulu or Temurin recommended)
  2. Android Studio (for Android development)
  3. Android 8.0+ phone or emulator (for installing the Android app)
  4. Xcode + iOS simulator if you're touching iOS code
  5. libsodium for full local builds (brew install libsodium on macOS)

Desktop packaging prerequisites (only if you're building installers) are listed in BUILDING.md § Prerequisites.

Clone and import:

git clone https://github.com/vitorpamplona/amethyst.git
cd amethyst

Common Gradle entry points:

./gradlew :desktopApp:run         # Run desktop app
./gradlew :amethyst:installDebug  # Install Android debug build
./gradlew -PquartzDir=../quartz build   # Build against a local Quartz checkout
./gradlew build                   # Full build + tests

Where code belongs

Modules:

  • Quartz — Nostr KMP library (protocol, crypto, models), in its own repo (vitorpamplona/quartz) and on Maven Central. No UI.
  • commons/ — Shared headless layer: models, ViewModels, flows, relay client. No Compose UI (the CLI depends on it).
  • commonsUI/ — Shared Compose Multiplatform UI, icons, theme, Compose resources, on top of commons.
  • nestsClient/ — Audio-rooms client (NIP-53) built on quic-kmp and quartz. The pure-Kotlin QUIC v1 + HTTP/3 + WebTransport stack lives in its own repo, quic-kmp, and comes from Maven Central.
  • amethyst/ — Android app: Activity, layouts, navigation.
  • desktopApp/ — Desktop JVM app: Window, sidebar, keyboard shortcuts.
  • cli/ — amy, a non-interactive JVM CLI over quartz + commons.

Per-module design docs live in <module>/plans/YYYY-MM-DD-<slug>.md. The global docs/plans/ folder is frozen.

Before writing a new class or composable, check whether it already exists. Most logic is already implemented somewhere — duplicating it is the #1 cause of PR churn. Place new code by purpose:

What you're adding Goes in
Nostr event types, NIPs, tags, signing, crypto, Bech32 quartz (its own repo)
Shared ViewModels, StateFlows, relay subscriptions commons/commonMain/viewmodels/ or commons/commonMain/
Shared Composables, icons, theme commonsUI/commonMain/ (same packages as commons)
Android-only screen, navigation, system integration amethyst/
Desktop-only window, sidebar, menu bar, shortcut desktopApp/
amy <verb> subcommand (thin assembly only) cli/src/main/kotlin/.../cli/
QUIC / HTTP/3 / WebTransport protocol code quic-kmp (its own repo)
MoQ session / audio-room logic nestsClient/

Hard rules:

  • Quartz has no UI.
  • cli/ has no Nostr protocol or business logic — it's a thin assembly layer over quartz + commons. If your CLI command needs new behavior, extract it into commons/ first.
  • ViewModels belong in commons/commonMain/; shared composables in commonsUI/commonMain/. Only screens (the Composable that wires layout + navigation) stay in the platform module.
  • For platform-specific behavior in a shared file, use expect/actual.

Workflow

  1. Survey first. Search the codebase for existing implementations before writing new code. The AGENTS.md file has a recommended set of grep queries; running them routinely turns up the exact class you were about to re-invent.
  2. Open a small PR. One feature or one fix per PR. Refactors that support the change are fine; unrelated refactors are not.
  3. Branch off main.
  4. Run tests + spotless locally before pushing.
  5. Open the PR with a description that explains why, not just what.

Coding standards

  • Kotlin, formatted with Spotless. Always run ./gradlew spotlessApply before considering a task complete. CI runs spotlessCheck and will fail on unformatted code.
  • Never use --no-verify to bypass pre-commit hooks. If a hook fails, fix the cause.
  • Default to writing no comments. Add one only when the why is non-obvious (a workaround, a subtle invariant, behavior that would surprise a reader). Don't restate what the code does, and don't reference the current task or callers — that belongs in the PR description.

Tests

./gradlew test                 # Unit + KMP common tests, all modules
./gradlew connectedAndroidTest # Android instrumented tests (needs device)
./gradlew :commons:test        # Single module

Add tests when:

  • You fix a bug — a regression test that fails before your fix and passes after.
  • You add new protocol code in quartz — Nostr event parsing, NIP implementations, and crypto paths should have unit coverage there.
  • You add anything to commons/ with non-trivial state transitions.

UI changes don't need automated UI tests, but they do need the screenshots described in the proof-of-testing section.

Interoperability tests

Amethyst ships several cross-stack interop harnesses that drive our code against external reference implementations. They are not run in CI — they're slow, require Rust / bun / Docker / Chromium, and most PRs don't touch the code they cover. If your change does touch a covered area, you are expected to run the relevant suite locally and paste the result into the PR description. Reviewers may ask if you didn't.

Suite Path What it covers When you must run it
Marmot / MLS (Whitenoise) cli/tests/marmot/ NIP-EE MLS groups: KeyPackage publish, group create/invite/remove, admin promote/demote, leave, replay, KP rotation — Amethyst (or amy) ↔ whitenoise-rs Rust reference Changes to MLS / Marmot code in quartz/ or commons/, or to the Marmot UI / group-chat flow in amethyst/
NIP-17 DMs (amy ↔ amy) cli/tests/dm/ Text + file DM round-trip, strict kind:10050, --allow-fallback NIP-65 chain, dm list --since window slide Changes to DM, gift-wrap (NIP-59), or NIP-17 code paths in quartz/ or commons/
Audio rooms (manual) cli/tests/nests/ 47-test manual harness: Amethyst Android ↔ nostrnests.com reference web client. Host/listener flows, hand-raise, role promotion, kicks, schedule, reconnect, JWT refresh, PIP Changes to NIP-53 audio rooms (amethyst/ UI) or nestsClient/ that affect host/listener UX
MoQ-lite hang-tier nestsClient/tests/hang-interop/ Rust hang-listen / hang-publish ↔ Amethyst Kotlin through a real moq-relay 0.10.x subprocess. Wire-byte capture, FFT-on-PCM, mute / hot-swap / packet-loss / late-join / 60s broadcast / multi-listener fan-out Changes to nestsClient/.../moq/lite/, nestsClient/.../audio/, MoqLite*Speaker.kt / *Listener.kt, ReconnectingNests*.kt, or quartz/.../nip53
MoQ-lite browser-tier nestsClient/tests/browser-interop/ Headless Chromium with @moq/lite + @moq/hang via Playwright ↔ Amethyst Kotlin (forward + reverse). WebCodecs encode/decode, ALPN negotiation, browser-side reconnect Same as hang-tier, plus any quic-kmp version bump (WebTransport, packet header protection, key updates, stream demux)

Running them

Each suite has its own README; the non-obvious bits worth flagging up front:

  • MoQ-lite tiers are opt-in via -DnestsHangInterop=true and -DnestsBrowserInterop=true on :nestsClient:jvmTest. Cold first run is ~10–13 min per tier; cached runs ~3–7 min.
  • The QUIC interop-runner matrix moved with the QUIC stack to quic-kmp (quic/interop/ there).
  • CLI suites (cli/tests/README.md): every relay-backed suite boots the embedded amy serve relay (geode) — no external relay binary; only the Marmot suites additionally need cargo for MDK's wn/wnd. The interactive Marmot variant prompts a human to drive the Android UI.

If a change is documentation-only, UI-only, build-script-only, or otherwise cannot affect wire bytes / decoded audio / MLS state / DM envelopes, skip the interop suites and say so in the PR description.

Commits

  • Use Conventional Commits: feat:, fix:, refactor:, docs:, chore:, test:, etc.
  • One logical change per commit. Squashing during PR review is fine.
  • Keep subject lines under ~72 characters. Use the body to explain why the change was made when it isn't obvious.
  • Branch names follow feat/<scope>-<short-name> or fix/<scope>-<short-name> (e.g. feat/desktop-sidebar-resize, fix/android-notification-leak).

Pull requests

A good PR description has:

  1. Summary — 1–3 sentences on what changed and why.
  2. Test plan — exactly what you ran, on what platform, with what result. Include screenshots / recordings for UI changes (mandatory for non-regular contributors; expected for everyone on visual changes).

We aim to give first-pass feedback within a few days. PRs may sit longer if they're large, touch security-sensitive code, or arrive during a release window.

Translations

  • Submit translations via Crowdin, not as direct PRs to strings.xml. Crowdin pushes are integrated through the crowdin.yml workflow.
  • The /find-missing-translations skill can help reviewers spot untranslated keys for a target locale.

Releases

Release runbooks (Android AAB upload, desktop packaging on macOS / Windows / Linux, Homebrew cask, Winget manifest, Apple Developer signing, asset naming) live in BUILDING.md § Release runbook and BUILDING.md § Bootstrap runbook.

If your PR needs to be cut into a particular release, mention it in the PR description and the maintainers will coordinate.


Thanks again for contributing.