Files
amethyst/.claude/skills/amy-expert/SKILL.md
Vitor PamplonaandClaude Opus 5.5 395af4dc9c chore(skills): consolidate 33 skills into 14, verified against the tree
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>
2026-10-11 12:21:01 -04:00

5.3 KiB


name: amy-expert description: amy CLI in cli/: adding an amy <verb> command, Main.kt dispatch, Output.emit/--json contract, exit codes 0/1/2/124, ~/.amy layout, cli/tests harnesses. Use when touching cli/. Enforces the thin-assembly rule: logic goes to commons/quartz.

amy CLI

cli/ is a non-interactive JVM client over quartz + commons. Its --json output and exit codes are a public API that the interop harnesses depend on. The long-form contract is cli/DEVELOPMENT.md ("Public contract", "How to add a command", "Output-shape conventions", "Testing", "Full on-disk layout"); this skill is the short version plus the traps.

Rules

  1. Thin assembly layer. No protocol, filter assembly, state machines or encryption in cli/. Protocol goes to quartz/; business logic to commons/, extracted from amethyst/ first in its own commit if needed (references/extraction-recipe.md). A commands/*.kt file over ~200 lines is a smell. cli may depend on :geode (for amy serve), never on :commonsUI, :amethyst or :desktopApp.
  2. Output goes through Output (cli/src/main/kotlin/…/cli/Output.kt).
    • Output.emit(mapOf(...)): the map is the --json shape (one line, one object, stable snake_case keys); the default text render comes from the same map and has no contract.
    • return Output.error("code", "detail"): the return value is the exit code (bad_args 2, timeout 124, anything else 1). Errors go to stderr (error: code: detail, or a JSON object under --json).
    • Exit codes 0 ok, 1 runtime, 2 bad args, 124 await timeout. runCli in Main.kt also maps a thrown IllegalArgumentException to 2 and AwaitTimeout to 124.
    • Never println results; System.err.println is for progress logs only.
    • Adding a --json key is safe. Renaming or removing one is breaking: say so in the commit, and update cli/README.md and the fixtures under cli/tests/.
    • Publishes: RawEventSupport.publishGuard(ack, id) then + RawEventSupport.ackFields(ack) (published_to + rejected_by: [{relay, reason}]). Don't hand-roll the projection.
    • Reuse an error code from the canonical list in cli/DEVELOPMENT.md before minting one.
  3. Non-interactive. Everything is a flag; a network wait is an explicit await verb with --timeout. The only TTY fallback is PassphraseProvider (after --passphrase-file and $AMY_PASSPHRASE); don't add more.
  4. ~/.amy/ is the whole state, reloaded on every run; no in-process caches across runs. ~/.amy/shared/events-store/ (one event store for all accounts), ~/.amy/<account>/ (identity.json, state.json, aliases.json, marmot/), ~/.amy/current (written by amy use NAME). Account selection is the global --account NAME; --name stays free for subcommands. New persisted state goes through Config.kt / stores/FileStores.kt / a named helper like Aliases.kt.
  5. $HOME, not user.home. DataDir.DEFAULT_ROOT (Config.kt) reads $HOME; JDK 21's user.home comes from getpwuid and ignores it, which would break harness isolation (HOME=$(mktemp -d) amy --account alice init). JVM tests use the amy.home system property seam instead.

Adding a command

Copy the shape of a real one, e.g. commands/FollowCommand.kt: a USAGE constant, parse with Args (positional, flag, longFlag, …, then args.rejectUnknown()), Context.open(dataDir).use { ctx -> ctx.prepare(); … }, call into commons/quartz, publish with ctx.publish or read with ctx.drain, emit one map. Commands are suspend fun; main already runs runBlocking. Multi-verb groups dispatch through route(...) in commands/Router.kt.

Wire-up: a branch in Main.kt's dispatch (marmot sub-verbs go in marmotDispatch), lines in printUsage() and printVerbList(), a row in cli/README.md, the status in cli/ROADMAP.md, and a harness case under cli/tests/ when wire behaviour changes.

Don't re-invent what exists: user identifiers via ctx.requireUserHex (or quartz nip05DnsIdentifiers/resolveUserHexOrNull); rendering via commons rendering/ (EventRendererRegistry, json/JsonEventFormatter); event lookup and loading via commons relayClient/oneshot/ (OneShotNoteCache, EventLocator, ThreadLoader, NotificationsLoader, over OneShotRelayAccess; amy's adapter is NoteSupport.access(ctx)); builders in commons actions/ and model/.

Where things live

  • cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/: Main.kt (dispatch, global flags, usage), Args.kt, Output.kt, Config.kt (DataDir, identity, state), Context.kt (per-run wiring), stores/, secrets/, and commands/ (~110 files, one per verb or group, plus cashu/, graperank/, trust/). Run ls on commands/ rather than trusting a list.
  • cli/README.md (user tour), cli/DEVELOPMENT.md (contract), cli/ROADMAP.md, cli/plans/.
  • Build: ./gradlew :cli:installDist → cli/build/install/amy/bin/amy. JVM contract tests: ./gradlew :cli:test (ArgsTest, ExitCodeContractTest, JsonContractTest, harness in CliResult.kt).
  • cli/tests/: shell harnesses (lib.sh, headless/helpers.sh, one directory per suite: dm/, marmot/, notes/, cache/, git/, …), most against an embedded amy serve relay with a fresh $HOME. See cli/tests/README.md.