The 33 descriptions (~16k chars, ~19k with paths) overran Codex's skill-list budget (2% of the context window, or 8,000 chars when unknown), and a review found most bodies stale: paths from before the commons/commonsUI moves, the deleted legacy desktop app, a callback signer API that no longer exists, and example APIs that were never in the tree. Kept, rewritten and checked against the code (every path, class and command): account-state (+feeds), amy-expert, android-expert, auth-signers, compose-expert (+API style, Navigation 3 back stacks), desktop-expert (+run/package), event-store-semantics (+NIP-50 search), find-missing- translations, kotlin-coroutines (+structured concurrency, flow modeling), kotlin-multiplatform (+Gradle, composable extraction), ngit-pr, nip85-trusted-assertions, nostr-expert (+NIP lookup, generated NIP catalog), relay-client. Dropped: textbook Compose/Kotlin technique skills, kotlin-expert, kotlin-types-value-class, gradle-expert, quartz-integration (external-facing; see the Quartz docs commit), find-non-lambda-logs (audit finished; the rule is now in AGENTS.md Kotlin Style), and the /nip, /desktop-run, /extract commands. The skill list is now ~4k chars. Also drops .claude/core-skills-plan.md (stale history) and repoints SearchableEvent's KDoc to event-store-semantics. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5.5 KiB
Extract-from-Android recipe
The single most common reason an Amy feature request stalls: the
logic it needs lives in amethyst/ with Android-only imports. You
cannot call it from cli/. You have to move it first.
This recipe is how.
When to extract
Before writing a new command, ask:
-
Does the piece of logic I need exist in
quartz/orcommons/?- Yes → use it.
- No, but it's in
amethyst/→ extract. This file. - No, it doesn't exist anywhere → design it in
commons/directly. Write a plan doc undercommons/plans/if it's a new subsystem.
-
Never duplicate
amethyst/logic intocli/. That's a debt you will pay later when the Android caller drifts.
Recipe
Land this as its own commit, before the commit that adds the CLI command.
Step 1 — Find the class
grep -rn "class <ClassName>" amethyst/src/main/java/
Identify the minimum unit to move. Sometimes it's a whole file, sometimes one function. Prefer the smallest unit that makes the command possible.
Step 2 — List Android-only dependencies
Walk the imports. The usual offenders:
| Dependency | Treatment |
|---|---|
android.content.Context |
Often accidental — inline if only used for logging or preferences. Otherwise, invert as constructor arg. |
android.content.SharedPreferences |
Abstract behind an interface in commons/; Android actual uses SharedPreferences, JVM actual uses a JSON file. |
androidx.work.WorkManager |
Rarely shareable — if the CLI needs it, simplify the flow to not require background scheduling. |
android.util.Log |
Replace with Quartz Log (quartz/…/utils/Log.kt), lambda overload for interpolated messages. |
android.graphics.Bitmap |
Almost never needed by Amy. Keep in Android and split the function. |
android.net.Uri |
Replace with kotlinx.io path types or a plain String. |
androidx.compose.* |
Compose UI (ui/foundation/material3), Coil and Res must stay out of commons entirely — they belong in :commonsUI, which Amy never depends on. Only the Compose runtime (@Stable, snapshot state) is allowed in commons. |
Step 3 — Pick a migration strategy per dependency
- Inline-able. One call, trivial. Delete it.
- Platform-abstractable. Add
expectincommons/commonMain/+actualincommons/androidMain/+actualincommons/jvmMain/. See thekotlin-multiplatformskill for the mechanics and for thejvmAndroidsource-set pattern used throughout this repo. - Inversion-of-control. Take the Android dependency as a constructor arg with an interface type. Amy supplies a JVM flavour; Android supplies the Context-backed one.
Step 4 — Move the code
# Target location depends on what it is:
# - Protocol → quartz/src/commonMain/kotlin/…
# - Business logic → commons/src/commonMain/kotlin/…
# - UI → commonsUI/src/commonMain/… (needs Compose Multiplatform; never used by amy)
git mv amethyst/src/main/java/com/.../<ClassName>.kt \
commons/src/commonMain/kotlin/com/.../<ClassName>.kt
Update package declarations. Run ./gradlew spotlessApply.
Step 5 — Update the Android caller
The amethyst/ caller now imports from the new location. Often this is the only code change visible in the Android app.
If the Android caller was using a concrete Android-backed dependency, it now supplies that concrete dependency explicitly.
Step 6 — Add a JVM test
In commons/src/commonTest/kotlin/… or commons/src/jvmTest/kotlin/…
(depending on what the code exercises), add a test that runs on JVM.
This guards against Android-only imports sneaking back in, and it's
the only way to be sure Amy can now call the code.
Step 7 — Commit
Single commit, descriptive:
refactor(<area>): extract <ClassName> to commons for CLI reuse, with a
body naming the old and new location, how each Android dependency was
handled (inlined, expect/actual, or inverted behind an interface), and
"No behaviour change on Android."
Step 8 — Now add the CLI command
Separate commit. Follows "How to add a command" in cli/DEVELOPMENT.md.
Cautionary notes
- Don't extract speculatively. Only extract what the current command needs. A feature-complete port can happen later; right now the goal is to unblock one command without adding surface area you don't have a second caller for.
- Android is allowed to keep side-effects. Notifications,
background services, Intents, camera, permissions dialogs — those
stay in
amethyst/. Amy's job isn't to replicate UX, it's to exercise the protocol underneath. - Check the consumers. Sometimes the "logic" you want is already
partially in
commons/, and theamethyst/class is just a thin wrapper. In that case, re-use thecommons/class directly and delete the wrapper or keep it if Android genuinely needs it. - Tests first if you're nervous. Copy the existing
amethyst/-side test (if any), make it JVM-only by removing Android imports, and watch it pass after the move.
Red flags during extraction
Stop and reconsider if:
- The Android class is 1000+ lines. Extract only the piece the CLI needs; leave the rest for a follow-up.
- You need
Contextin 30 places. It's probably being used as a grab-bag; sort by actual use (strings, preferences, services, …) and abstract those individually. - You find yourself writing
expect classwith a dozen methods. A fine-grained interface is usually clearer than a monolithic expect-actual. - You're about to add a Compose import to
cli/. Stop.