Files
amethyst/cli/ROADMAP.md
Claude d9827d85d6 feat(wot): providers write the 10040 rows; hand-written and copied rows as fallbacks
A provider knows which key serves which tag, so the kind 10040 rows come
from it:

- Brainstorm's guided sign-up now publishes the rows GET /setup/{pubkey}
  returns (30382:rank, followers, reporters, muters, hops, and 30392 for
  Trusted Lists), merged into the user's list: rows of the same names and
  the old score provider replaced, everything else kept. Without /setup it
  falls back to rank + followers under ta_pubkey, what brainstorm.world
  publishes.
- Each guided provider links its own setup page ("Use their website";
  brainstorm.world/setup/activate). Their site publishes the list and
  Amethyst reads it from the relays.

Fallbacks, collapsed under "Set up by hand":

- A row editor: tag name from a hardcoded list (30382:rank, followers,
  hops, reporters, muters), key and relay per row, a rank row required.
  "Keep private" moved here.
- Copy someone's view: another user's public rows (npub or NIP-05), or a
  provider's house observer (Brainstorm's _@brainstorm.world), land in the
  editor for review. Their provider computes cards for them, so the
  network shows as they see it.

"Stop using" now removes every row the rank provider's key serves.

CLI: `amy trust setup` publishes the provider's rows and reports them
(new `rows` key); new `amy trust copy USER [--private]`.

Checked live: /setup returns this account's six rows; copying the house
observer into a throwaway account (published only to a local relay) and
syncing downloaded its network (155,488 entries, 77,239 passing).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rkxh2PxVeRVuwb9kAMq34q
2026-10-08 18:06:46 +00:00

206 lines
21 KiB
Markdown

# Amy Roadmap
The live checklist for growing `amy` from a Marmot test-bed into a
full command-line mirror of Amethyst.
**How to use this file:** update it in the same PR that ships a
feature. Move rows between tables, adjust ordering, add non-goals.
This is the single source of truth for "what's left".
- What amy is + how to use it: [README.md](./README.md)
- The public contract + how to extend amy: [DEVELOPMENT.md](./DEVELOPMENT.md)
- Ongoing design plans: [plans/](./plans/)
- Shared work consumed here: [../commons/plans/](../commons/plans/)
---
## North-star goal
> For every feature of the Amethyst Android app, there is a way to
> exercise it through `amy`, with byte-identical on-relay behaviour.
Why:
- Interop tests against the ~100 other Nostr clients need a
reproducible harness that does not require running Android.
- Agents and LLMs can script real Amethyst flows without a GUI.
- Regressions in shared logic (signing, encryption, filter building,
event parsing) become shell-scriptable.
- Power users get a command-line Amethyst for free.
**Non-goal:** Amy is not a second Nostr client implementation. It is
a thin assembly layer over `quartz` + `commons`. Protocol and business
logic in `cli/` are bugs.
---
## Parity matrix
Status legend: ✅ shipped · 📦 logic lives in `commons/`, needs a command ·
🆕 needs extraction from `amethyst/` first · ⚠️ blocked (see Notes).
| Area | Status | Notes |
|---|---|---|
| Identity create / import (`nsec`, `ncryptsec`, mnemonic, `npub`, `nprofile`, hex, NIP-05) | ✅ | `LoginCommand` + Quartz NIP-05 / NIP-06 / NIP-49 |
| Account bootstrap (nine events) | ✅ | `commons/account/AccountBootstrapEvents.kt` |
| Account logoff (`amy logoff`) — delete key + per-account state + the account's events in the shared store | ✅ | `LogoffCommand`. `--yes`-gated; `--keep-events` skips the shared-cache purge. |
| Status overview (`amy status`) — who is signed in (profile name, npub, signer + can-sign), what each account has saved (follows, relay lists, own events, contacts, Marmot groups + messages, key package, Concord communities, Cashu wallet, DM cursor), whether an account is actually selected, and the machine operator key | ✅ | `StatusCommand` + `StatusReport` + `StatusText`. Cross-account and read-only: no keychain prompt, no network, and it never creates the event store it reads. Store size/backend is `amy store stat`. |
| Relay config — every relay-list bucket (nip65 10002 via `outbox`/`inbox`/`nip65` nouns with spec read/write merge, dm 10050, key-package 10051, search 10007, private-outbox 10013, blocked 10006, trusted 10089, proxy 10087, indexer 10086, broadcast 10088, favorite 10012) — noun-first `relay <noun> add/remove/set/clear/list` + fan-out `relay add/remove` + publish | ✅ | `RelayCommands`. Mirrors the Android relay-settings screen. Local relays (device pref) + relay sets (30002) intentionally out of scope. |
| MLS KeyPackage publish + fetch | ✅ | `commons/marmot/MarmotManager` |
| Marmot group create / add / rename / promote / demote / remove / leave | ✅ | `commons/marmot/` |
| Marmot message send / list | ✅ | `commons/marmot/` |
| `await` polling (KP / group / member / admin / message / rename / epoch) | ✅ | `AwaitCommands` |
| NIP-01 note publish (`amy notes post TEXT`) | ✅ | `PostCommand` — outbox via `RelayCommands` configured set. |
| NIP-13 proof of work (`amy notes post --pow N`, `amy pow check/mine/bench`) | ✅ | `PostCommand` + `PowCommands` — mines pre-signature via quartz `PoWMiner`; `pow mine --pubkey` covers delegated PoW; `pow check` applies the commitment cap. |
| NIP-01 feed read (`amy notes feed [--following \| --author NPUB \| --hashtag TAG]`) | ✅ | `FeedCommand`; rows carry the shared renderer's fields. Community feeds still pending. |
| Event view (`amy notes show EVENT`) | ✅ | `NoteShowCommand` over the shared `commons/rendering/` `EventRendererRegistry` + `JsonEventFormatter` (any kind; specialised 0/1/3/5/6/7/16/1111/9735/10002/10050/30023). |
| NIP-10 / NIP-22 replies (`amy notes reply EVENT TEXT`) | ✅ | `NoteActionCommands` → `commons` `ReplyActions.reply` (kind:1 → NIP-10, anything else → NIP-22 kind:1111), text through the composer's `NewMessageTagger` + `messageTags`. Every interaction is routed by `commons` `BroadcastRelayPlanner` — the app's `EventBroadcaster` routing — with the call the app makes for that action. |
| NIP-02 follow list add / remove / list | ✅ | `FollowCommand` — `amy follow USER` / `amy unfollow USER` (fetches the freshest kind:3 first). |
| NIP-09 event deletion (`amy delete EVENT…`) | ✅ | `DeleteCommand` → `commons` `DeletionActions` (own events only, 200 targets per kind:5). |
| NIP-17 DMs send / list / await | ✅ | `DmCommands` — reuses Quartz `NIP17Factory` + `RecipientRelayFetcher`; filter extracted to `commons/relayClient/nip17Dm/`. Plan: [`cli/plans/2026-04-23-nip17-dm.md`](./plans/2026-04-23-nip17-dm.md). |
| NIP-18 reposts / quotes (`amy notes repost` / `notes quote`) | ✅ | `commons` `RepostAction` (kind:6 / 16); a quote is the composer's own pre-filled text (`quoteMessage`) posted through the composer's tagging. |
| NIP-25 reactions | ✅ | `amy notes react EVENT` via `commons` `ReactionAction`; `marmot message react` covers MLS group reactions. |
| NIP-29 relay groups (`amy relaygroup`) | ✅ | `RelayGroupCommands` — list/browse/info/create/join/leave/message/edit/invite/put-user/remove-user against a host relay; kind:10009 joined-list kept in sync. |
| Buzz workspaces (`amy buzz`) | ✅ | `BuzzCommands` — post/read the kind:40002 stream timeline, `attest` (offline NIP-OA), `console` (decrypt+aggregate kind:44200 turn metrics via the shared `AgentFleetAggregator`), `personas` (kind:30175). Join/leave reuse `amy relaygroup` (Buzz workspaces are NIP-29 groups). |
| Buzz agent jobs (`amy buzz job` / `agent serve`) | ✅ | `BuzzJobCommands` (request/list/show/cancel, kinds 43001-43006) + `BuzzAgentCommands` (`agent serve` responder loop: gate on `--accept-from`, run `--exec`, report accept/progress/result/error). Correlation + state via the shared `BuzzJobAggregator` in `commons`. Schema provisional (43001-43006 reserved upstream). See `cli/plans/2026-07-25-buzz-agent-support-channel.md`. |
| NIP-51 lists (bookmarks, mute, follow sets) | 🆕 | `amethyst/model/nip51Lists/` |
| NIP-57 zaps (send) | ✅ partial | `ZapCommand` — `zap user`/`zap event` build the kind:9734 request and fetch the BOLT11 (zap splits honored, one invoice per recipient); `--with NDEBIT` auto-pays through a CLINK debit pointer. Receipt (kind:9735) verification still 🆕. |
| BOLT12 zaps (NIP-B1, kinds 9736/9737/10058) | ✅ partial | `Bolt12Commands` + `Bolt12SendCommands` over shared `commons` `Bolt12ZapActions` — `bolt12 decode` (offer/proof), `verify` (validate a kind:9736), `offer get/set` (kind:10058), and the two-step send `intent`→`zap` (out-of-band proof, since amy has no NWC rail). Interop harness + NWC-fetched proofs still 🆕. |
| NIP-65 outbox model queries | ✅ | `OutboxCommand` — `amy outbox USER [--refresh]`, cache-first. |
| CLINK offers + debits (`amy offer` / `amy debit`) | ✅ | `OfferCommands` + `DebitCommands` — pointer decode, NIP-05 discover, kind:21001/21002 round-trips, `offer pay --with NDEBIT` end-to-end settlement. `--timeout` is SECONDS. |
| Geochat (Bitchat geohash, ephemeral kind:20000) | ✅ | `GeochatCommands` — listen/send/keys with per-geohash throwaway identity + geo-nearest relay routing; doubles as the Bitchat interop harness. |
| Concord Channels (encrypted communities) | ✅ | `ConcordCommands` — 17 sub-verbs (create/list/import/channels/send/read/invite/revoke/join/recover/rekey/roles/role/grant/ban/unban/refound) over shared `commons` `ConcordActions`; secrets in `concord.json`. |
| NIP-5A nsites + NIP-5D napplets | ✅ | `NsiteCommands` + `NappletCommands` — fetch/publish/serve/list with sha256 + aggregate-hash verification and `requires` capability reporting. |
| Podcasting 2.0 / podstr (`amy podcast20`) | ✅ | `Podcast20Commands` — kind:30078 metadata, 30054 episodes, 30055 trailers, list. |
| Follows-of-follows (`amy fof get/list/sync`) | ✅ | `FofCommand` — single-hop social proof from the local store (`wot` kept as deprecation alias). |
| NIP-85 GrapeRank web-of-trust (`amy graperank`) | ✅ | `GrapeRankCommand` — outbox-model crawl + scoring engine in `commons/wot/` (`GrapeRank`, `TrustGraph`, `TrustGraphBuilder`); every score run persists kind:30382 `UserAssertionEvent` cards to the local store (diffed against prior ranks, kind:5 retractions), `publish` mirrors that set to the operator relays via NIP-77 up-sync, `rank` reads cards back, plus `register` / `unregister` / `providers` for the kind:10040 `TrustProviderListEvent` discovery layer. |
| NIP-85 Web of Trust network (`amy trust sync/status/check/setup/copy`) | ✅ | Thin layer over commons `TrustNetworkState` (the apps' index, sync and verdict), `wot/onboarding/` (Brainstorm sign-up, which publishes the rows Brainstorm serves) and `ScoreProviderEdits` (copying another user's rows); the consumer side of `graperank`. |
| NIP-72 communities | 🆕 | |
| NIP-78 app-specific data (settings sync) | 🆕 | |
| Long-form (NIP-23) publish / read | 🆕 | |
| Live activities / chess (NIP-53 / NIP-64) | 🆕 | |
| Simple Nostr Objects (DECK-0003) | ✅ read-only | `SnoCommands` — `sno parse` (§1.9 verdict + the rule a refusal broke), `sno work` (the §8.10 proof of work an avatar owes), `sno verify` (whether a kind:11333 avatar has paid). Thin over quartz `cyberspace/deck0003Sno/`. Conformance harness at `cli/tests/sno/` diffs all three against the cyberspace project's own `sno-reference.py` and `cyberspace-cli`. Publishing/authoring 🆕. |
| Blossom blobs (NIP-B7) | ✅ | `BlossomCommands` — upload/download/list/delete/check/mirror on shared `commons` `BlossomClient`; live-server harness at `cli/tests/blossom/`. |
| NIP-60 / 61 Cashu wallet + nutzaps | ✅ | Full surface: `cashu wallet {create,show,export-key,destroy}`, `mint {ping,info}`, `sync`, `balance`, `receive {ln,complete,resume,token,nutzap-sweep}`, `send {ln,token,nutzap}`, `maintenance {scrub,restore,migrate-keysets}`, `mint-rec {show,add,remove}` — all on shared `commons` `CashuWalletOps` + `CashuWalletReader` (the exact path the Android wallet runs). Reads project the local store; `cashu sync` (or `--sync`) is what fills it, paging every relay to exhaustion so a cap can't truncate the proof set. Interop harness pending. Plan: [`cli/plans/2026-05-28-cashu-cli.md`](./plans/2026-05-28-cashu-cli.md). |
| NIP-47 Wallet Connect | 🆕 | |
| NIP-46 bunker signer | ✅ | `BunkerCommand` + `NostrConnect` + `LoginCommand` — host (`amy bunker[ connect]`) and client (`amy login bunker://` / `--nostrconnect`) sides, `--perms`/`--interactive` gating, `auth_url` challenges. |
| Profile view (`amy profile show NPUB`) + edit | ✅ | `ProfileCommands`. Cache-first; `--refresh` forces a relay drain. |
| Thread view (`amy notes thread EVENT`) | ✅ | `ThreadCommand` → commons `relayClient/oneshot/ThreadLoader`, which runs the app's thread code over a per-run `EventCache` (`OneShotNoteCache`): the thread screen's relay filters, `ThreadAssembler`, `ThreadFeedFilter.thread` ordering, `ThreadLevelCalculator` depths. |
| Notifications feed (`amy notifications`) | ✅ | `NotificationsCommand` → commons `relayClient/oneshot/NotificationsLoader` over `NotificationKinds` (filter + "is it for me") + `NotificationItem.classify` (extracted from Desktop). DMs stay in `amy dm list`. |
| Search (NIP-50) | ✅ | `SearchCommand` — `search user` (kind:0) + `search note` (`--kind`, `--kinds` alias) over the kind:10007 search-relay list; default limit 50. |
| Namecoin NIP-05 resolve (`amy namecoin resolve .bit\|d/\|id/`) | ✅ | `NamecoinCommand` — reuses Quartz `NamecoinNameResolver` + `ElectrumXClient` + the default ElectrumX server set the Android/Desktop apps ship with. Stateless. On-chain `name_history` + Core RPC backend pending separate PRs. |
### `nak` parity — army-knife primitives
Tracking [`fiatjaf/nak`](https://github.com/fiatjaf/nak)'s command surface. amy
adapts the verbiage where its own conventions differ (`req` → one-shot `fetch`
vs streaming `subscribe`). Stateless verbs run with no account or network.
| nak command | amy verb | Status | Notes |
|---|---|---|---|
| `decode` | `amy decode` | ✅ | NIP-19/21 → JSON. Quartz `Nip19Parser`. |
| `encode` | `amy encode` | ✅ | npub/nsec/note/nevent/nprofile/naddr. |
| `verify` / `validate` | `amy verify` | ✅ | id-hash + signature, reported separately. |
| `key` | `amy key generate\|public\|encrypt\|decrypt\|validate` | ✅ | generate/derive + NIP-49 encrypt/decrypt (bidirectionally nak-verified) + `validate` (npub/hex parse check). `expand`/`combine`(MuSig2)/`default` still 🆕. |
| `event` | `amy event` | ✅ | build/sign an arbitrary event, optional `--publish`/`--relay`. |
| `publish` | `amy publish` | ✅ | broadcast a pre-made event JSON (verified first). |
| `req` (one-shot) | `amy fetch` | ✅ | filter → collect-until-EOSE, dedupe, sort, cap. Also accepts a nip19/nip05 code and resolves relays via the outbox model (code hints + author's NIP-65 write relays), like nak's `fetch`. |
| `req` (stream) | `amy subscribe` | ✅ | filter → live NDJSON stream to stdout. |
| `count` | `amy count` | ✅ | NIP-45, per-relay counts. |
| `encrypt` / `decrypt` | `amy encrypt\|decrypt` | ✅ | raw NIP-44 (default) / NIP-04. |
| `gift` | `amy gift wrap\|unwrap` | ✅ | NIP-59 seal+wrap / unwrap+unseal. |
| `relay` (NIP-11) | `amy relay info` | ✅ | stateless NIP-11 doc fetch. |
| `outbox` | `amy outbox` | ✅ | NIP-65 read/write relays, cache-first. |
| `filter` | `amy filter` | ✅ | stateless — assemble + print a filter JSON. |
| `blossom` | `amy blossom` | ✅ | upload/download/list/delete/check/mirror (reuses commons `BlossomClient`). |
| `nip` | `amy nip` | ✅ | repo-first lookup + Nostr fallback (NipText kind:30817, wiki:30818, long-form:30023); `nip list`. |
| `kind` | `amy kind` | ✅ | quartz `KindNames` registry (kind → English label + NIP) covering **every** event kind quartz defines (280 entries); number lookup + name search. |
| `sync` | `amy sync` | ✅ | NIP-77 Negentropy reconcile with the local store (down/up/both). |
| `git` | `amy git` | ✅ (events + read) | NIP-34: `init` bootstraps a repo from the local `git` checkout (announce + state, like `ngit init`); repo announce (30617) + state (30618), patches (1617), pull requests (1618/1619), issues (1621), NIP-22 comments (1111), NIP-32 labels (1985), status open/applied/closed/draft (1630-1633), GRASP server list (10317); `issues`/`patches`/`prs`/`thread` reads derive status; `apply` applies a fetched patch to the local tree (`git am`); `browse`/`cat`/`log` read git objects over smart-HTTP v2 (quartz `GitHttpClient`, the same shallow-clone path the Android browser uses). Only git-packfile **push** (writing objects to clone/GRASP servers) and NIP-34 cover notes (1624: quartz now types and builds them as `GitCoverNoteEvent`, but amy has no verb yet) are out of scope. Event tag shapes were verified byte-for-byte against the ngit reference implementation and the NIP-34 spec (`clone`/`web` as single multi-value tags, issue `p`-tag for maintainer routing, plain patch/PR `r` tags); the quartz readers stay tolerant of the legacy repeated form. See `quartz/…/nip34Git/GitNip34InteropTest`. |
| `podcast` | `amy podcast` | ✅ | NIP-F4 show metadata (10154) + episode publish (54) + list. |
| `bunker` | `amy bunker[ connect]` + `amy login bunker://`/`--nostrconnect` | ✅ | NIP-46 remote signer + login, both the `bunker://` and `nostrconnect://` flows, each direction, plus `auth_url` challenge handling (client surfaces the URL + keeps waiting). Interop-verified vs real `nak`. |
| `admin` | `amy admin RELAY METHOD` | ✅ | NIP-86 Relay Management over NIP-98 HTTP auth — full method set (ban/allow pubkey + event, NIP-43 roles + invite claims, kinds, IP block, change name/desc/icon, list-*). Reuses quartz `Nip86Client` + shared `commons` `Nip86Retriever`. Interop-verified against `amy serve`. |
| `serve` | `amy serve` | ✅ | Embeds **geode** (the standalone Ktor relay on quartz's relay-server code) — in-memory by default, `--db FILE` for SQLite, account is admin so `amy admin` works against it. NIP-86 + NIP-77 included. |
| `wallet` (NIP-60 Cashu) | `amy cashu` | ✅ | See the Cashu row above — full NIP-60/61 wallet + nutzaps. |
| `mcp` / `fs` / `spell` | — | 🆕 (niche) | MCP server, FUSE mount, MuSig2/FROST; some pull new deps. |
### Full nak comparison (introspected both binaries)
nak has 34 functional commands (introspected from `nak --help`). Coverage:
- **Full / equivalent (24):** `event`, `req`(→`fetch`+`subscribe`), `fetch`
(nip19/nip05-hint resolution), `filter`, `count`, `decode`, `encode`,
`verify`, `relay`, `bunker`(+nostrconnect+auth_url), `encrypt`, `decrypt`,
`gift`, `publish`, `sync`, `profile`, `podcast`, `nip`, `kind`, `blossom`,
`nsite`(NIP-5A), `admin`(NIP-86), `serve`(geode), `wallet`(NIP-60/61 Cashu).
Protocol-sensitive ones (`bunker`, `sync`, `key` NIP-49, `encode`/`decode`,
`admin`) are interop-verified against the real `nak` binary or `amy serve`.
- **Partial / adapted (3):** `key` (no `expand`/`combine`(MuSig2)/`default`),
`git` (full NIP-34 event surface — announce/state/patch/PR/issue/comment/status
+ status-deriving reads + GRASP list, plus `browse`/`cat`/`log` reading git
objects over smart-HTTP; only packfile **push** is out of scope), `outbox`
(NIP-65 model vs nak's local hints DB).
- **Missing (6):** `dekey` (NIP-4E), `mcp`, `curl` (NIP-98), `fs` (FUSE),
`spell` (MuSig2/FROST), and `validate` (event-schema validation).
**Design differences (not gaps):** amy is a *stateful client* (accounts,
`~/.amy/`, shared event store) with a stable JSON contract; nak is a *stateless*
per-invocation tool that prints bare values for shell substitution. amy also has
a large surface nak lacks: Marmot/MLS, NIP-29 relay groups (offered side by
side with MLS, not substituting for it), Concord communities, geochat, NIP-17
DMs, zaps, CLINK offer/debit, NIP-02 follow, NIP-50 search, nsites/napplets,
podcast20, GrapeRank/fof, profile edit, store management, account management.
**Cheap remaining wins:** the `key` `expand` (hex left-pad) and `default`
(print the active account's key) sub-verbs. `key combine` needs MuSig2.
---
## Order of operations
Proposed sequencing. Each step is one PR. Each step should extract
at least one file from `amethyst/` into `commons/`; if it doesn't
move anything, re-audit — you're probably duplicating logic.
1. **Event rendering core** in `commons/commonMain/.../rendering/` — ✅
shipped with renderers for kinds 0 / 1 / 3 / 5 / 6 / 7 / 16 / 1111 /
9735 / 10002 / 10050 / 30023 and the JSON formatter amy emits. The
Compose formatter and the Android/Desktop switch-over are the plan's
remaining steps. Design: `commons/plans/2026-04-21-event-renderer.md`.
2. **`amy notes post|show|reply|quote|react|repost|thread`, `amy delete`,
`amy notifications`** — ✅ shipped: the core social loop outside Marmot.
3. **`amy notes feed`** — `--following`, `--author NPUB`, `--hashtag TAG` ✅;
community feeds still pending.
4. **`amy follow|unfollow`** (NIP-02) — ✅ shipped. A standalone
`follow list` view is still pending.
5. **`amy dm send|list`** (NIP-17) — ✅ shipped. Reuses the gift-wrap
path also exercised by Marmot.
6. **`amy list bookmarks|mute|pin …`** (NIP-51).
7. **`amy zap send|verify`** (NIP-57) — send ✅ (invoice fetch +
optional CLINK auto-pay via `--with NDEBIT`); receipt verify pending.
8. **Distribution** — Homebrew + Scoop + `.deb` in the same release
pipeline as desktop. Plan: `cli/plans/2026-04-21-cli-distribution.md`.
9. **Test suite** — largely in place, two layers:
- **Shell harnesses** under `cli/tests/` — among them: `blossom`
(live servers), `cache`, `clink`, `dm`, `git` (NIP-34 vs `amy serve`),
`marmot` (vs MDK), `nests` (manual audio-rooms matrix), `notes` (the
social loop vs `amy serve`), `pow`, `relaygroup`, `sync`, plus the shared `headless/` helpers. See
`cli/tests/README.md`. Every relay-backed suite runs against the
embedded `amy serve` relay (geode) — no external relay binary.
None run in CI yet (the Marmot ones need Rust for MDK's `wn`).
- **JVM unit suite** at `cli/src/test/kotlin/` — `Args` parsing,
exit-code contract, and `--json` shape tests driving `runCli`
in-process via the `amy.home` isolation seam.
10. **Everything else in the matrix.**
---
## Non-goals
- Interactive TUI or REPL.
- Native image (GraalVM) until Quartz has a pure-Kotlin signer
fallback — `secp256k1-kmp-jni-*` needs JNI today.
- A Gradle dependency on `:amethyst` or `:desktopApp`. Ever.
- Re-implementing any Nostr protocol piece that's already in
`quartz/` or in another client's library.