/amy-expert
Patterns for extending `amy`, the Amethyst CLI in `cli/`. Use when adding an `amy <verb>` command, touching files under `cli/src/main/kotlin/…/cli/`, wiring a new subcommand into `Main.kt`, writing an interop test script that drives Amy, or extracting logic out of `amethyst/`
$ npx -y skills add vitorpamplona/amethyst --skill amy-expert --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/amy-expert
Context preview
The summary Claude sees to decide when to auto-load this skill.
Patterns for extending `amy`, the Amethyst CLI in `cli/`. Use when adding an `amy <verb>` command, touching files under `cli/src/main/kotlin/…/cli/`, wiring a new subcommand into `Main.kt`, writing an interop test script that drives Amy, or extracting logic out of `amethyst/`
SKILL.md
amy-expert.SKILL.mdname: amy-expert
description: Patterns for extending `amy`, the Amethyst CLI in `cli/`. Use when adding an `amy <verb>` command, touching files under `cli/src/main/kotlin/…/cli/`, wiring a new subcommand into `Main.kt`, writing an interop test script that drives Amy, or extracting logic out of `amethyst/` into `commons/` so a CLI command can call it. Enforces the thin-assembly-layer rule (no Nostr protocol or business logic inside `cli/`), the dual-output contract (text by default, single-line JSON object on stdout under `--json`, exit codes 0/1/2/124), and the extract-from-Android recipe. Complements `nostr-expert` (protocol in Quartz), `kotlin-multiplatform` (expect/actual for extraction), and `feed-patterns` / `account-state` / `relay-client` (where the business logic should end up). NOT for general Nostr or Kotlin work — those have their own skills.
Amy CLI Expert
Practical patterns for touching the `cli/` module without breaking its public contract.
When to use this skill
- Adding a new `amy <verb>` subcommand.
- Editing anything under `cli/src/main/kotlin/…/cli/`.
- Writing a shell script or test harness that drives Amy.
- Extracting code out of `amethyst/` so the CLI can call it (this is
the single most common reason an Amy feature request stalls).
- Deciding whether a piece of logic belongs in `cli/` vs `commons/`
vs `quartz/` (answer: almost never `cli/`).
**Not for:** general Nostr protocol work (`nostr-expert`), general Kotlin (`kotlin-expert`), Compose UI (`compose-expert`), Android-only flows (`android-expert`), gradle/build (`gradle-expert`).
The rules that matter
Amy has a small number of hard rules. Any change that breaks them is a breaking change to the CLI's public API, and breaks the interop- test harnesses that depend on it.
Rule 1 — `cli/` is a thin assembly layer
No new Nostr protocol, filter assembly, state machines, or encryption lives in `cli/`. Ever. If you need logic that doesn't exist yet:
- Protocol piece (event kind, tags, signing)? Add it to `quartz/`.
- Business logic (state, defaults, ordering, filter assembly)?
Add it to `commons/` — extract from `amethyst/` first if needed (see Rule 5).
A `commands/*.kt` file longer than ~200 lines is a code smell. Either the command is doing too many things, or the logic has leaked in from where it should have lived.
Rule 2 — text by default, `--json` is the machine contract
amy ships a dual-output contract:
- **Default stdout is human-readable text.** A YAML-ish render of the
result map. No shape promise — the renderer can change between releases.
- **`--json` switches stdout to one JSON object, one line.** Stable
snake_case keys; this shape is the public API.
- **stderr is for humans.** Progress logs, warnings, per-relay ACK
traces. Errors go here too — `error: <code>: <detail>` by default, JSON `{"error":"…","detail":"…"}` under `--json`.
- **Exit codes:** `0` success · `1` runtime · `2` bad args · `124`
await timeout.
- Adding a `--json` key is safe; renaming or removing one is a
breaking change and needs the commit message to say so.
Commands emit results via `Output.emit(mapOf(...))` and errors via `Output.error("code", "detail")`. The `Output` object (in `cli/src/main/kotlin/…/cli/Output.kt`) handles the text-vs-JSON branching automatically. Never `println(...)` user-facing output directly — `System.err.println(...)` is fine for progress logs only.
See `references/output-conventions.md`.
Rule 3 — Non-interactive, ever
No `readLine()`, no TTY prompts, no hidden interactive behaviour. Passwords, names, keys, anything — all flags. Any network wait is an explicit `await` verb with `--timeout`.
Rule 4 — `~/.amy/` is the whole world
State is reloaded from `~/.amy/` on every invocation. No singletons, no in-process caches that survive across runs. This is what lets 100 parallel interop scenarios share a harness safely.
The layout:
- `~/.amy/shared/events-store/` — one file-backed Nostr event store
per machine, shared across every account.
- `~/.amy/<account>/` — per-account dir: `identity.json`,
`state.json`, `aliases.json`, `marmot/`.
- `~/.amy/current` — marker file written by `amy use NAME` to pin
the active account.
Account selection is via the global `--account NAME` flag (required when more than one account exists; auto-picked when exactly one does). `--account` cannot collide with subcommand flags, so commands like `marmot group create --name "Group"` or `profile edit --name "Alice"` keep their own `--name` parameter.
Tests isolate by overriding `$HOME` for the amy subprocess (`HOME=$(mktemp -d) amy --account alice init`). amy reads `$HOME` directly (not `user.home`, which JDK 21 derives from `getpwuid` and ignores `$HOME`), so the same convention `git`/`gpg`/`npm`/`ssh` follow Just Works.
If you need new persisted state, add it to `Config.kt`, `stores/FileStores.kt`, or a new helper (e.g. `Aliases.kt`) with a named JSON schema. Don't smuggle state into `~/.amy/` outside the documented files.
Rule 5 — Extract before adding
If the command you're about to add needs logic from `amethyst/`, land the extraction first, in its own commit:
1. Identify the class in `amethyst/src/main/java/…/`. 2. List its Android-only dependencies (`Context`, `SharedPreferences`, `WorkManager`, `Log`, `Bitmap`, `Uri`, …). 3. For each, choose: inline, platform-abstract via expect/actual, or take-as-constructor-arg. 4. Move the file to `commons/commonMain/…`. 5. Update the Android caller to use the new location. Add a JVM test. 6. **Then** add the `cli/commands/…` file.
Full checklist: `references/extraction-recipe.md`.
Standard command shape
Every new command follows the same shape — parse args, open Context, prepare, call into commons/quartz, publish or drain, emit one result via `Output.emit`. The template is in `references/command-template.md`; copy it rather than re-deriving it.
Wire-up checklist: 1. New file in `cli/commands/` wit
Read more
name: amy-expert description: Patterns for extending `amy`, the Amethyst CLI in `cli/`. Use when adding an `amy <verb>` command, touching files under `cli/src/main/kotlin/…/cli/`, wiring a new subcommand into `Main.kt`, writing an interop test script that drives Amy, or extracting logic out of `amethyst/` into `commons/` so a CLI command can call it. Enforces the thin-assembly-layer rule (no Nostr protocol or business logic inside `cli/`), the dual-output contract (text by default, single-line JSON object on stdout under `--json`, exit codes 0/1/2/124), and the extract-from-Android recipe. Complements `nostr-expert` (protocol in Quartz), `kotlin-multiplatform` (expect/actual for extraction), and `feed-patterns` / `account-state` / `relay-client` (where the business logic should end up). NOT for general Nostr or Kotlin work — those have their own skills.
Amy CLI Expert
Practical patterns for touching the `cli/` module without breaking its public contract.
When to use this skill
- Adding a new `amy <verb>` subcommand.
- Editing anything under `cli/src/main/kotlin/…/cli/`.
- Writing a shell script or test harness that drives Amy.
- Extracting code out of `amethyst/` so the CLI can call it (this is
the single most common reason an Amy feature request stalls).
- Deciding whether a piece of logic belongs in `cli/` vs `commons/`
vs `quartz/` (answer: almost never `cli/`).
**Not for:** general Nostr protocol work (`nostr-expert`), general Kotlin (`kotlin-expert`), Compose UI (`compose-expert`), Android-only flows (`android-expert`), gradle/build (`gradle-expert`).
The rules that matter
Amy has a small number of hard rules. Any change that breaks them is a breaking change to the CLI's public API, and breaks the interop- test harnesses that depend on it.
Rule 1 — `cli/` is a thin assembly layer
No new Nostr protocol, filter assembly, state machines, or encryption lives in `cli/`. Ever. If you need logic that doesn't exist yet:
- Protocol piece (event kind, tags, signing)? Add it to `quartz/`.
- Business logic (state, defaults, ordering, filter assembly)?
Add it to `commons/` — extract from `amethyst/` first if needed (see Rule 5).
A `commands/*.kt` file longer than ~200 lines is a code smell. Either the command is doing too many things, or the logic has leaked in from where it should have lived.
Rule 2 — text by default, `--json` is the machine contract
amy ships a dual-output contract:
- **Default stdout is human-readable text.** A YAML-ish render of the
result map. No shape promise — the renderer can change between releases.
- **`--json` switches stdout to one JSON object, one line.** Stable
snake_case keys; this shape is the public API.
- **stderr is for humans.** Progress logs, warnings, per-relay ACK
traces. Errors go here too — `error: <code>: <detail>` by default, JSON `{"error":"…","detail":"…"}` under `--json`.
- **Exit codes:** `0` success · `1` runtime · `2` bad args · `124`
await timeout.
- Adding a `--json` key is safe; renaming or removing one is a
breaking change and needs the commit message to say so.
Commands emit results via `Output.emit(mapOf(...))` and errors via `Output.error("code", "detail")`. The `Output` object (in `cli/src/main/kotlin/…/cli/Output.kt`) handles the text-vs-JSON branching automatically. Never `println(...)` user-facing output directly — `System.err.println(...)` is fine for progress logs only.
See `references/output-conventions.md`.
Rule 3 — Non-interactive, ever
No `readLine()`, no TTY prompts, no hidden interactive behaviour. Passwords, names, keys, anything — all flags. Any network wait is an explicit `await` verb with `--timeout`.
Rule 4 — `~/.amy/` is the whole world
State is reloaded from `~/.amy/` on every invocation. No singletons, no in-process caches that survive across runs. This is what lets 100 parallel interop scenarios share a harness safely.
The layout:
- `~/.amy/shared/events-store/` — one file-backed Nostr event store
per machine, shared across every account.
- `~/.amy/<account>/` — per-account dir: `identity.json`,
`state.json`, `aliases.json`, `marmot/`.
- `~/.amy/current` — marker file written by `amy use NAME` to pin
the active account.
Account selection is via the global `--account NAME` flag (required when more than one account exists; auto-picked when exactly one does). `--account` cannot collide with subcommand flags, so commands like `marmot group create --name "Group"` or `profile edit --name "Alice"` keep their own `--name` parameter.
Tests isolate by overriding `$HOME` for the amy subprocess (`HOME=$(mktemp -d) amy --account alice init`). amy reads `$HOME` directly (not `user.home`, which JDK 21 derives from `getpwuid` and ignores `$HOME`), so the same convention `git`/`gpg`/`npm`/`ssh` follow Just Works.
If you need new persisted state, add it to `Config.kt`, `stores/FileStores.kt`, or a new helper (e.g. `Aliases.kt`) with a named JSON schema. Don't smuggle state into `~/.amy/` outside the documented files.
Rule 5 — Extract before adding
If the command you're about to add needs logic from `amethyst/`, land the extraction first, in its own commit:
1. Identify the class in `amethyst/src/main/java/…/`. 2. List its Android-only dependencies (`Context`, `SharedPreferences`, `WorkManager`, `Log`, `Bitmap`, `Uri`, …). 3. For each, choose: inline, platform-abstract via expect/actual, or take-as-constructor-arg. 4. Move the file to `commons/commonMain/…`. 5. Update the Android caller to use the new location. Add a JVM test. 6. **Then** add the `cli/commands/…` file.
Full checklist: `references/extraction-recipe.md`.
Standard command shape
Every new command follows the same shape — parse args, open Context, prepare, call into commons/quartz, publish or drain, emit one result via `Output.emit`. The template is in `references/command-template.md`; copy it rather than re-deriving it.
Wire-up checklist: 1. New file in `cli/commands/` wit
Other skills on amethyst.
- /account-state
Account state and in-memory event store patterns in Amethyst. Use when working with `Account.kt` (per-user state objects — `kind3FollowList`, `nip65RelayList`, `muteList`, `bookmarkState`, each exposing a `.flow` StateFlow), `LocalCache` (the object-level event store backed by
Open skill - /android-expert
Android platform patterns for the `amethyst/` module. Use when working with (1) Android navigation (Navigation Compose, type-safe routes, bottom nav), (2) runtime permissions (camera, notifications, biometrics), (3) platform APIs (Intent, Context, Activity, ContentResolver), (4)
Open skill - /auth-signers
Signer abstraction patterns in Amethyst. Use when working with event signing, choosing between a local keypair (`NostrSignerInternal`), a remote NIP-46 bunker signer (`NostrSignerRemote`), or a NIP-55 Android external-app signer (`NostrSignerExternal`). Covers the abstract
Open skill - /compose-expert
Advanced Compose Multiplatform UI patterns for shared composables. Use when working with visual UI components, state management patterns (remember, derivedStateOf, produceState), recomposition optimization (@Stable/@Immutable visual usage), Material3 theming, custom ImageVector
Open skill - /compose-modifier-and-layout-style
Use when writing or reviewing Jetpack Compose layout APIs, modifier parameters, modifier chain construction, hardcoded root layout decisions, or layout wrappers around a single conditional. Technique-layer skill — complements the codebase-specific compose-expert.
Open skill - /compose-recomposition-performance
Use when investigating Jetpack Compose recomposition performance, skippable/restartable composables, composables.txt or compiler reports, Layout Inspector recomposition counts, or frame-rate State reads in composition vs layout/draw, and it is not yet clear whether the cause is
Open skill

