kotlin-multiplatform — Claude Code skill: KMP and Gradle
A skill that tells the agent which Amethyst module and source set a file belongs to, so the KMP build and its purity gate stay green.
npx skills add vitorpamplona/amethyst --skill kotlin-multiplatform -a claude-codeWhat kotlin-multiplatform does
The skill answers the two things agents get wrong in a Kotlin Multiplatform repo: where a file goes, and how the Gradle wiring should look. It maps the modules listed in settings.gradle.kts and explains when to reach for expect/actual, when for a port interface, and when for a composition-local slot. Then comes verifyKmpPurity, the gate applied by commons and commonsUI. It scans every iOS and native-compiled source set and fails on okhttp3, com.fasterxml.jackson, System.currentTimeMillis, Thread.sleep, java.util.UUID and synchronized(.
Build traps get their own section. Versions belong in gradle/libs.versions.toml and nowhere else, test resolves to each module's jvmTest, and testFixtures needs a top-level dependencies block with the configuration name as a string. The secp256k1 JNI artifacts differ per source set, so the Android variant on a JVM classpath throws UnsatisfiedLinkError at load.
Expect it to point at AGENTS.md, commons/ARCHITECTURE.md and tools/r8-verify/. Those files live in Amethyst and nowhere else, so dropping the skill into an unrelated repository leaves the agent with nothing to read — forking it and rewriting the references is the honest option. After touching commonMain, the author suggests running ./gradlew :commons:verifyKmpPurity :commonsUI:verifyKmpPurity.
What it helps with
- place a new class in
commonMaininstead ofjvmAndroid - pick
expect/actualover a port for a platform call - fix a
verifyKmpPurityfailure onjava.util.UUID - add a dependency through
libs.versions.toml - move an Android composable into
commonsUI
Example prompts for the agent
- Where should this class live:
commonMainorjvmAndroid? - Run
./gradlew :commons:verifyKmpPurity :commonsUI:verifyKmpPurityand explain the failures. - Extract this Android composable into
commonsUIfollowingreferences/extract-composable.md.
How to install kotlin-multiplatform
1Skills CLI (Vercel)
npx skills add vitorpamplona/amethyst --skill kotlin-multiplatform -a claude-code2Manually: put the files in place
~/.claude/skills/kotlin-multiplatform/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/kotlin-multiplatform/
1Skills CLI (Vercel)
npx skills add vitorpamplona/amethyst --skill kotlin-multiplatform -a codex2Manually: put the files in place
.agents/skills/kotlin-multiplatform/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add vitorpamplona/amethyst --skill kotlin-multiplatform -a cursor2Manually: put the files in place
.cursor/skills/kotlin-multiplatform/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install vitorpamplona/amethyst kotlin-multiplatform2Manually: put the files in place
.github/skills/kotlin-multiplatform/SKILL.md1Skills CLI (Vercel)
npx skills add vitorpamplona/amethyst --skill kotlin-multiplatform -a gemini-cli2Platform CLI
gemini skills install https://github.com/vitorpamplona/amethyst --path .claude/skills/kotlin-multiplatformManually: put the files in place
.devin/skills/kotlin-multiplatform/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add vitorpamplona/amethyst --skill kotlin-multiplatform -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about kotlin-multiplatform
How to install kotlin-multiplatform in Claude Code?
Run npx skills add vitorpamplona/amethyst --skill kotlin-multiplatform -a claude-code. The skill lands in ~/.claude/skills/kotlin-multiplatform/SKILL.md.
What does the kotlin-multiplatform skill do?
It covers module and source set placement in Amethyst, the choice between expect/actual, ports and slots, plus the Gradle and R8 traps that break KMP builds.
Does the skill work outside the Amethyst repository?
It leans on AGENTS.md, commons/ARCHITECTURE.md, commonsUI/ARCHITECTURE.md and the verifyKmpPurity task. Without those files the agent has no project rules to follow.
Source text
SKILL.md · MIT
---
name: kotlin-multiplatform
description: KMP placement and Gradle in Amethyst: which module or source set, expect/actual vs ports, verifyKmpPurity, extracting Android composables to commonsUI, build.gradle.kts/libs.versions.toml edits, secp256k1 JNI variants, R8.
---
# Kotlin Multiplatform and Gradle in Amethyst
Module roles, the "share vs keep native" rule and the CLI-safe boundary are in
`AGENTS.md` (Architecture, Feature Workflow). Package placement inside `commons` /
`commonsUI` is in `commons/ARCHITECTURE.md` ("Where does my code go?", "Source sets")
and `commonsUI/ARCHITECTURE.md` ("Platform slots"). Read those first; this skill
adds the source-set mechanics and the build traps.
## Modules
`settings.gradle.kts` includes 13: `amethyst`, `nappletHost`, `benchmark`,
`baselineprofile`, `geode`, `commons`, `commonsUI`, `nestsClient`,
`marmotQuic`, `desktopApp`, `cli`, `relayBench`, `marmotBench`. Quartz is the
external `libs.quartz` (vitorpamplona/quartz) and QUIC the external `libs.quic`
(vitorpamplona/quic-kmp); `-PquartzDir=../quartz` / `-PquicKmpDir=../quic-kmp` build a
local checkout instead (an included build, so quic's own quartz dependency follows).
## Source sets
| Source set | Holds | Targets |
|---|---|---|
| `commonMain` | Pure Kotlin. Compiled for iOS/native too, so no JVM APIs | all |
| `jvmAndroid` | JVM libraries shared by Android + Desktop: OkHttp, `nestsClient`, NWC/LNURL | android, jvm |
| `androidMain` / `jvmMain` | Platform actuals (Keystore/DataStore vs keyring, Coil bridges) | one each |
| `iosMain`, `appleMain`, `nativeMain`, `linuxMain`, `macosMain` | quartz only (plus `iosMain` in commons/commonsUI) | Apple / native |
| `skikoMain` | commonsUI code shared by Desktop and iOS | jvm, ios |
Targets: `quartz` builds android, jvm, iosArm64, iosSimulatorArm64, linuxX64,
macosArm64. `commons` and `commonsUI` build android, jvm, iosArm64,
iosSimulatorArm64. `nestsClient`, `marmotQuic` are android + jvm. There is
no `iosX64`.
`jvmAndroid` is a custom intermediate set created with `create("jvmAndroid")`
before `androidMain`/`jvmMain` depend on it (see `commons/build.gradle.kts`).
Prefer the most common set that compiles: `commonMain` → `jvmAndroid` → platform.
kotlinx.serialization is the house JSON stack and OkHttp the house HTTP stack
(`AGENTS.md` › Tech Stack). Jackson is left only inside quartz's JVM code and reaches
the app modules transitively; don't add new Jackson use here. Don't migrate JVM HTTP
to Ktor. Jackson and OkHttp are banned in iOS-compiled sets, which the purity gate
enforces.
## expect/actual vs ports and slots
- **expect/actual** for a small platform verb or value with no app state:
`KmpLock`, `ConcurrentSet`, `SecureKeyStorage`, `isDebug`, `formattedDateTime`
(commons), `rememberTextSharer`, `rememberShortNotice` (commonsUI). Put the actual
in `jvmAndroid` when Android and Desktop share it.
- **A port** (interface the shell implements) when the platform side needs app
state or services: `AccountViewModelHost`, `UserNoticeSink`, `LocalCacheHost`.
- **A slot** (CompositionLocal the front end installs) for things only a front end
can draw: `LocalNotePlatform`, `LocalRichTextPlatform`.
- Not every quartz singleton is expect: `Secp256k1Instance`, `Log` and
`LibSodiumInstance` are plain `object`s in `quartz/.../utils/`.
- Don't `expect` a whole screen or navigation; share it and put only the leaf
behind a port or slot.
## Purity gate: `verifyKmpPurity`
`gradle/kmp-purity.gradle.kts`, applied by `commons` and `commonsUI` (quartz keeps a copy) and
wired into `check`, scans every iOS/native-compiled source set (`commonMain`,
`iosMain`, `skikoMain`, …, and their tests) and fails on `com.fasterxml.jackson`,
`okhttp3`, `System.currentTimeMillis` (use `TimeUtils.now()`), `Thread.sleep`,
`java.util.UUID` (use `kotlin.uuid.Uuid`), `kotlin.jvm.Synchronized` or bare
`synchronized(` (use `KmpLock`), `kotlin.jvm.Volatile` (use
`kotlin.concurrent.Volatile`). CI runs it on Linux in the lint job; the macOS
`test-commons-ios` / `test-commonsui-ios` jobs are authoritative. Run
`./gradlew :commons:verifyKmpPurity :commonsUI:verifyKmpPurity`
after touching `commonMain`. Quartz has its own copy of the task and its own iOS CI.
## Gradle traps
- **Versions live only in `gradle/libs.versions.toml`.** Add the library there and
reference `libs.*`; never hardcode a version or copy one into docs. Check the
license first (`AGENTS.md` › Dependency Licensing).
- **`test` is an alias for each KMP module's `jvmTest`.** Other targets
(`:commonsUI:testAndroidHostTest`, the native ones) run only when named; see
`AGENTS.md` › Build Commands.
- **testFixtures in KMP:** the source-set `dependencies { }` block
(`KotlinDependencyHandler`) has no `testFixtures(...)`. Use a top-level block with
the configuration name as a string:
`dependencies { "jvmAndroidTestImplementation"(testFixtures(project(":geode"))) }`.
- **secp256k1 variants:** `secp256k1-kmp-common` in `commonMain`,
`secp256k1-kmp-jni-android` in `androidMain`, `secp256k1-kmp-jni-jvm` in `jvmMain`
and JVM tests. All three share one `version.ref`. A JVM classpath with the
android variant fails at load with `UnsatisfiedLinkError`.
- **R8:** library keep rules ship in `quartz/consumer-rules.pro`. Never add a blanket
`-keep class com.vitorpamplona.quartz.** { *; }`. R8 can't see reflection, so a
moved/renamed reflective class is a silent release break: before tagging, run
`python3 tools/r8-verify/verify_reflection_contract.py amethyst/build/outputs/mapping/<variant>/`
(the release workflow checks `playRelease` and `fdroidRelease`) and update
`tools/r8-verify/reflection-contract.txt` when you move a listed class.
## Extracting Android code to shared modules
Steps and the Android → multiplatform replacement table:
[references/extract-composable.md](references/extract-composable.md).
© vitorpamplona · MIT · vitorpamplona/amethyst