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.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
- Thin assembly layer. No protocol, filter assembly, state machines or encryption in
cli/. Protocol goes toquartz/; business logic tocommons/, extracted fromamethyst/first in its own commit if needed (references/extraction-recipe.md). Acommands/*.ktfile over ~200 lines is a smell.climay depend on:geode(foramy serve), never on:commonsUI,:amethystor:desktopApp. - Output goes through
Output(cli/src/main/kotlin/…/cli/Output.kt).Output.emit(mapOf(...)): the map is the--jsonshape (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_args2,timeout124, anything else 1). Errors go to stderr (error: code: detail, or a JSON object under--json).- Exit codes
0ok,1runtime,2bad args,124await timeout.runCliinMain.ktalso maps a thrownIllegalArgumentExceptionto 2 andAwaitTimeoutto 124. - Never
printlnresults;System.err.printlnis for progress logs only. - Adding a
--jsonkey is safe. Renaming or removing one is breaking: say so in the commit, and updatecli/README.mdand the fixtures undercli/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.mdbefore minting one.
- Non-interactive. Everything is a flag; a network wait is an explicit
awaitverb with--timeout. The only TTY fallback isPassphraseProvider(after--passphrase-fileand$AMY_PASSPHRASE); don't add more. ~/.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 byamy use NAME). Account selection is the global--account NAME;--namestays free for subcommands. New persisted state goes throughConfig.kt/stores/FileStores.kt/ a named helper likeAliases.kt.$HOME, notuser.home.DataDir.DEFAULT_ROOT(Config.kt) reads$HOME; JDK 21'suser.homecomes fromgetpwuidand ignores it, which would break harness isolation (HOME=$(mktemp -d) amy --account alice init). JVM tests use theamy.homesystem 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/, andcommands/(~110 files, one per verb or group, pluscashu/,graperank/,trust/). Runlsoncommands/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 inCliResult.kt). cli/tests/: shell harnesses (lib.sh,headless/helpers.sh, one directory per suite:dm/,marmot/,notes/,cache/,git/, …), most against an embeddedamy serverelay with a fresh$HOME. Seecli/tests/README.md.