Validated against the production relay wss://amethyst.communities.buzz.xyz
by joining and running a full DM round-trip. Adds the join primitive and
fixes the interop gaps that live testing surfaced.
- quartz: BuzzInviteLink — parse `https://<host>/invite/<token>` (relay-signed
base64url payload → community/role/expiry). A Buzz invite is NOT a NIP-29
code; it is redeemed over HTTP against the tenant host. Unit-tested with a
real token; rejects the Concord `/invite/<naddr>#…` shape (no collision).
- cli: `amy buzz join <invite-url>` — the real 3-step claim: GET /api/join-policy,
POST /api/invites/accept-policy, then NIP-98-signed POST /api/invites/claim.
Proven live (status: joined, role: member).
- cli: Context.publish now authenticates-then-retries on an `auth-required`
relay (warm the connection with a pendingOnAuthRequired REQ, then re-publish)
— the write path had no NIP-42 handling, so every Buzz write was rejected.
- cli: Buzz reads (dm list / read / console / personas) use the auth-aware
drain (pendingOnAuthRequired).
- cli: `dm open` surfaces the relay's synchronous OK `response:{channel_id}` —
the authoritative DM channel id (the relay assigns it; it is not polled).
- cli: `dm list` rewritten to the relay's actual discovery — kind-44100
member-added notifications (#p=me) filtered to the kind-40099 `dm_created`
channels. The deployed relay does NOT emit kind-41001.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
A Buzz DM is a relay-authoritative NIP-29 group whose h/id is a
relay-generated UUID, so its timeline reuses the whole relay-group chat
stack unchanged. This adds the missing discovery + product layer:
- commons: BuzzDmRegistry — process-wide registry fed by LocalCache from
the relay-signed DmCreatedEvent (41001) and per-viewer DmVisibilityEvent
(30622); tracks conversations (channel id -> participants/relay) and the
viewer's hidden set. Unit-tested.
- LocalCache: record 41001/30622 into the registry on consume (was
store-only).
- Account: openBuzzDm (41010), hideBuzzDm (41012), addBuzzDmMember (41011).
The relay assigns the channel UUID and confirms via 41001 — we never
mint it.
- Android: BuzzDmListViewModel (two-phase fetch: discover 41001/30622 #p=me,
then fetch each DM's 39000-39003 roster so the shared composer's member
gate passes), BuzzDmListScreen (inbox), BuzzNewDmScreen (publish 41010,
await the 41001, jump into the shared RelayGroupChatScreen). Reached from
a Direct Messages card on the Workspaces tab.
- CLI: amy buzz dm list/open/hide/add-member, mirroring buzz-cli.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
- Workspace shell: BuzzWorkspacesScreen now groups joined Buzz-dialect groups
BY RELAY into workspace→channels (a Buzz workspace IS a relay/tenant, per
buzz-core relay_url_authority), each an expandable section with its channels
and a + to the relay's directory (Concord-style community→channels).
- Forum composer (45001): the Threads-tab FAB opens BuzzForumPostScreen on a
Buzz relay, publishing a ForumPostEvent (mirrors build_forum_post) instead of
the vanilla kind-11 thread a NIP-29 relay uses.
- Held-attestation persistence: BuzzAttestationPreferences mirrors
BuzzHeldAttestations to the device DataStore and reloads at startup,
re-verifying each credential against its agent key (drops tampered entries).
Deliberately NOT built: job/huddle composers — jobs (43xxx) and huddles (48xxx)
have no builder in buzz-sdk (reserved kinds), so a composer would encode an
unconfirmed schema. Presence (20001) skipped per request (EventFactory collision
with GeohashPresenceEvent).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
Thin assembly over quartz + commons (no new protocol in cli/):
- buzz post RELAY GID <text> — publish a kind-40002 stream message (h-scoped)
- buzz read RELAY GID — drain the recent human-visible timeline (9/40002/40099)
- buzz attest AGENT — sign a NIP-OA OwnerAttestation offline, print the auth tag
- buzz console [--relays] — drain kind-44200 turn metrics (#p=me), NIP-44-decrypt,
and aggregate via the shared commons AgentFleetAggregator
- buzz personas [--relays] — list my kind-30175 personas (newest per slug)
Join/leave/create reuse 'amy relaygroup' (Buzz workspaces are NIP-29 groups). Wired
into Main dispatch + usage; README command table + ROADMAP updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
Gives Buzz a first-class navigation identity instead of hiding inside the generic
Relay Groups list. NavBarItem.BUZZ (Route.BuzzWorkspaces) is a pinnable bottom-nav
destination (added to NavBarCatalog + BottomBarCategories + the preloader when).
BuzzWorkspacesScreen is a modern hub: an Agent-Console hero card up top, then the
user's joined groups filtered to Buzz-dialect relays as cards with a colored
monogram avatar, live name/host/member-count and an unread dot, plus an inviting
empty state that routes to Browse groups. It reuses the always-on relay-group state
subscription (no per-screen fetch) and the same RelayGroupChannel the chat opens.
Also fixes the open-chat-tail test for the added 20002 typing kind
(RELAY_GROUP_OPEN_TAIL_KINDS), and documents typing + the hub in the buzz README.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
Brings Buzz to typing-indicator parity with Concord, verified against buzz-core
kind.rs (KIND_TYPING_INDICATOR=20002, requires_h_channel_scope=false).
- commons BuzzTypingState: process-wide, lock-guarded channel->typist->heartbeat
registry with stale pruning + future-clamp (6 unit tests).
- LocalCache records 20002 heartbeats into it (still no feed row; own typing
filtered in the UI).
- RELAY_GROUP_OPEN_TAIL_KINDS requests 20002 on the open channel's live tail only
(ephemeral, scoped to the room on screen, never the joined fleet).
- Account.sendBuzzTyping fires a throttled heartbeat to the host relay; the
composer sends it on text change (gated to Buzz relays).
- BuzzTypingIndicator: an animated three-dot '… is typing' row above the composer
that slides in/out and ages typists out on a timer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
Earlier text wrongly said Buzz has no reaction kind. buzz-core/src/kind.rs ALL_KINDS
includes KIND_REACTION(7), KIND_DELETION(5), KIND_NIP29_DELETE_EVENT(9005)/GROUP(9008),
KIND_REPORT(1984), and emoji list/set (10030/30030); requires_h_channel_scope(7) is
false. So the shared chat action sheet's react/delete/report already work against Buzz.
Only zaps (no 9734/9735) aren't relay-stored (payment still works). Typing/presence are
accepted by Buzz but not yet rendered by Amethyst.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
Closes the edit loop: Amethyst already renders 40003 edit overlays but could
not create them. ChannelNewMessageViewModel gains a Buzz edit mode
(editBuzzMessage/clearBuzzEdit) — the next send publishes a kind-40003 edit
targeting the original instead of a new 40002, kept minimal to mirror Buzz's
build_edit (h + e + content). An 'Edit' action on the message sheet is gated to
my own 40002 messages (the kind alone marks it Buzz) and threads to the composer
via an optional onWantsToEditBuzz callback through the shared chat feed
(default-null, so no other chat surface is affected). EditFieldRow shows an
editing banner with a cancel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
Adds a read surface for the Buzz canvas, which was consumed into overlay state
but had no UI. BuzzWorkspaceState gains a canvasUpdates flow; BuzzCanvasScreen
(Route.BuzzCanvas) renders the newest 40100 markdown for a channel and recomposes
when a newer revision lands. Entry point is a Dashboard icon in the relay-group
top bar, shown only on a Buzz-dialect relay once a canvas has arrived.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
Tier 1 connect path. BuzzHeldAttestations (commons) stores the OwnerAttestations
this device received, keyed by the agent pubkey each authorizes (verify-passing
only). AuthCoordinator.buzzAugmented appends the owner-signed auth tag to an
account's NIP-42 AUTH event when that account's key has a held attestation and
the relay speaks the Buzz dialect — and only that account's AUTH, never the
Concord stream-key AUTHs sharing the template, and never on non-Buzz relays.
So an un-enrolled agent key gets virtual membership while its owner stays a member.
AgentAttestationScreen gains a 'Hold an attestation' section (paste the auth tag
JSON, verified against the current account, stored/removed) alongside the existing
owner-side issuance. Store is in-memory for now — persisting per-account is a
follow-up. 4 store tests added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
Tier 3 write flow. AgentPersonaEditScreen + AgentPersonaEditViewModel publish
a Buzz Agent Persona (NIP-AP kind:30175) to the workspace's Buzz-dialect relays
(falling back to the owner outbox). The Personas tab gains a 'New persona' row
and each card is tappable to edit.
On edit the existing PersonaEvent is loaded from LocalCache so fields the form
does not expose (name pool, respond-to allowlist, parallelism) are preserved
rather than dropped on republish; the slug (d tag) is locked once chosen and
validated against the relay's grammar (^[a-z0-9][a-z0-9_-]{0,63}$).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
Read-path interop. Two changes:
1. RelayGroupFilterBuilders: BUZZ_RELAY_GROUP_TIMELINE_EXTRA_KINDS now also
requests canvas (40100), forum posts/votes/comments (45001-45003), agent
jobs (43001-43006), and huddle lifecycle (48100-48103) — all h-scoped, so
the same #h group REQ returns them. Previously only 40002/40003/40008/40099
were requested, so LocalCache could consume these kinds but they never
arrived for a group feed.
2. RenderBuzzNotes: kind-aware rows dispatched from ChatMessageCompose —
RenderBuzzDiff (40008: monospace, horizontally-scrollable diff with a
repo/commit/file header instead of prose-wrapped gutters), RenderBuzzActivityRow
(job + huddle lifecycle as centered system narration — huddles MUST be caught
here since their content is JSON), and RenderBuzzForumVote (45002). Forum
posts stay as plain chat bubbles (their content is already plain text).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
Adds AgentAttestationScreen, reached from an "Attest" FAB on the console.
The owner enters an agent pubkey (npub/hex) and optional AttestationConditions
(kind, created_at before/after), and OwnerAttestation.sign() produces the
signed auth tag, displayed for copy to hand to the agent operator out-of-band.
Entirely offline (nothing is published) and gated on a raw private key: the
NIP-OA signature covers a hashed commitment rather than a Nostr event, so
NIP-46 bunker / NIP-55 external-signer accounts get an explanation instead of
the form. Inputs are validated (kind 0-65535, unix bounds 0-u32, npub/hex key,
no self-attestation) with human-readable errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
Adds a third Observer tab streaming ephemeral agent telemetry frames
(ObserverFrameEvent, kind:24200, p=owner) live from every Buzz-dialect
relay via subscribeAsFlow. Frames are decrypted (frame:telemetry only),
deduped across relays, sorted newest-first, and bounded to a 200-row ring.
The live REQ is opened only while the Observer tab is on screen (started
in a DisposableEffect, cancelled on dispose and in onCleared) because
observer frames are never persisted by relays or LocalCache.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
Read-only owner dashboard over the AI-agent fleet. AgentConsoleViewModel
fetches turn metrics (kind:44200, p=owner) and personas (kind:30175,
authored by owner) from the Buzz-dialect relays plus the owner outbox set,
decrypts the NIP-44 metric payloads with the owner signer (cached by event
id), and derives fleet + per-agent totals via the pure AgentFleetAggregator.
AgentConsoleScreen renders a Costs tab (fleet total, per-agent spend/tokens/
turns/sessions, estimated-total warning) and a Personas tab (display name,
model, runtime, provider, system prompt). Wired as Route.AgentConsole with a
settings-catalog entry under App settings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
Pure aggregation of decrypted NIP-AM turn metrics (kind:44200) into per-agent
and fleet token/cost totals — the data core of the agent-owner console.
Correctness that makes the numbers trustworthy: turns group by (agent,
sessionId); within a session `cumulative` is the monotonic running total, so the
max cumulative per field is the authoritative session total (robust to dropped
turns), falling back to summing per-turn deltas only where no cumulative exists —
per field, so a cumulative that omits costUsd still gets cost from deltas.
Delta-sums that include a deltaReliable=false turn flag the fleet estimate.
10 tests cover cumulative-not-summed, missing-turn recovery, delta fallback,
per-field mixing, multi-session/multi-agent rollup, sessionless singletons, and
the unreliable-estimate flag.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
Three-reviewer pass over the branch. Fixes, most-severe first:
HIGH — channel-instance swap froze open screens. The subclass-upgrade design
replaced a group's RelayGroupChannel with a Buzz-typed instance on dialect
discovery, but screens/feeds/composers capture the instance for life, so all of
them kept rendering the orphan (frozen feed; composer stuck on kind 9). Removed
BuzzWorkspaceChannel entirely; RelayGroupChannel is a single stable type again.
Buzz-only overlay state (edit + canvas) now lives in BuzzWorkspaceStates, a
registry keyed by the channel UUID — dialect discovery no longer touches object
identity. The swap also silently dropped all relay-signed state (name, members,
admin status, pins, threads) and demoted the confirmed host; gone with the swap.
HIGH — wrong thread-root got messages relay-rejected AFTER the draft was
destroyed. A reply to a direct reply derived root=parent (buzzThreadRoot null on
a collapsed reply), which Buzz's ancestry validator rejects. Fallback is now
buzzThreadRoot() ?: buzzThreadReply() ?: parent.id. Also: minichat (kind 1111)
replies in Buzz channels are relay-rejected, so they're gated off for Buzz relays.
HIGH — pre-create defeated stray-redirect and marked the dialect off unverified
input. consumeBuzzTimelineEvent no longer pre-creates the channel; attachment
goes through the shared NIP-29 path (with its stray protection), and the dialect
is marked only off a VERIFIED event (markBuzzIfVerified) — a hostile relay can no
longer flip what the composer sends.
MEDIUM — engram tombstone divergence: a memory body missing `value` decoded as a
null tombstone (a deletion) where Buzz rejects it. `value` is now required (no
default); added NIP-AE slug-grammar validation. Pinned by test.
MEDIUM — unbounded edit overlay: pruneOldMessagesChannel now prunes overlay
entries for reaped messages. Overlay is keyed by channel id so own offline edits
(null relay) apply too.
Also: 40002 inbound reply linkage in computeReplyTo (we emit thread markers we
couldn't read); registered-but-unhandled 40901/40902/48001 now consumed;
unconditional Buzz-kind widening (kills the history-cursor-skip); stream mention
dedup; persona empty-list omission for content-hash parity; thread-marker
positional guard; edit-note-loading blank-row fallback.
Full amethyst unit suite + affected quartz suites green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
Review feedback: the 40002 composer branch hand-rolled raw arrayOf tags. Now:
- New shared `buzz/threading` package: `buzzThread(root, parent)` builder verb
emitting Buzz's exact thread_tags wire form (["e",root,"","root"] +
["e",parent,"","reply"], collapsing when parent==root; the empty relay slot
is deliberate — MarkedETag.assemble's arrayOfNotNull would slide the marker
into the relay slot) and `buzzThreadRoot()/buzzThreadReply()` positional
readers. The forum verbs now delegate to it (streams and forum comments share
thread_tags in buzz-sdk), and the composer uses buzzThread + the typed
pTag(PTag(...)) verb instead of raw arrays.
- Dialect bootstrap fix: the single-group open-channel REQ now always includes
the Buzz timeline kinds. Without this, an undiscovered Buzz relay was a
chicken-and-egg: fleet subs only widen after BuzzRelayDialect marks the
relay, but the mark comes from consuming a Buzz kind no filter asked for.
Opening a channel is explicit one-group intent, so the wider ask is cheap and
matches nothing on vanilla relays; fleet-wide subs stay dialect-gated
(both behaviors pinned in BuzzTimelineKindsTest). The live relay's NIP-11
("Buzz Relay", supported_extensions=[nip-er,nip-pl]) is a future
connect-time marker.
- Live proof of the full workspace lifecycle against the running Buzz relay
(BuzzRelayLiveInteropTest, 3/3 green): discover the channel via its
relay-signed 39000 (queryable by #d), join with NIP-29 kind-9021 from a
second member, post a 40002 after joining, and leave with kind-9022 — the
exact Quartz events Amethyst's group UI sends.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
Completes the visible half of Buzz workspace support:
- Composer: BuzzWorkspaceChannel branch (checked before its RelayGroupChannel
parent) sends the native kind-40002 stream message with the group's h tag,
Buzz thread markers on replies (["e",root,"","root"] + ["e",parent,"","reply"],
collapsing when parent is the root — mirrors thread_tags in buzz-sdk) and a
p notify to the parent author, with the same hashtag/url/quote/emoji/imeta
enrichment as every other channel type.
- Rendering: kind-40099 system messages render as centered system lines (like
NIP-28 admin rows) from the relay-signed payload; kind-40003 edits render as
an overlay — the newest edit's content replaces the stale original with an
"(edited)" marker, recomposing via the channel's editUpdates flow.
- LocalCache correction pinned by test: edits are overlays, never timeline rows
(Buzz's own CHANNEL_TIMELINE_CONTENT_KINDS excludes 40003) — a 40003 is
stored and recorded in the channel's edit map but no longer attached to the
timeline, so it can't render as a duplicate message.
All Buzz unit tests green; app compiles.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
Surfaces block/buzz workspace channels inside the existing NIP-29 relay-group
experience — one group model, dialect-aware rendering, zero impact on vanilla
groups (kind-filtered feeds can never receive Buzz kinds by accident).
- BuzzRelayDialect (commons): per-relay capability registry. Event-shape
detection — the first Buzz-only kind consumed from a relay marks it; vanilla
relays never serve those kinds, so no false positives. NIP-11 marking can be
layered on later.
- BuzzWorkspaceChannel (commons): sibling of RelayGroupChannel (now `open`)
holding Buzz-only channel state: the kind-40003 edit overlay (never render
superseded text as current) and the newest kind-40100 canvas. Kind-9 chat and
kind-40002 stream messages share ONE timeline per group so mixed-dialect
conversations stay whole.
- LocalCache: consumes every registered Buzz kind (previously all fell into the
"Event Not Supported" branch). Timeline kinds attach to the group's channel,
materialized dialect-aware with an in-place upgrade (note migration) when the
dialect is discovered after the channel was first created as plain NIP-29.
Addressables store replaceably; the rest store as queryable regular events;
ephemeral signals (typing 20002, observer 24200, huddle reaction 24810,
pairing 24134) mark the dialect but are deliberately not persisted.
- RelayGroupFilterBuilders: group-chat REQs widen their timeline kind set with
40002/40003/40008/40099 only for marked relays; vanilla NIP-29 REQs unchanged.
Tests cover dialect detection + materialization, the plain-channel upgrade with
timeline migration, newest-edit-wins overlay ordering, and that filter builders
extend kinds only on marked relays. Full amethyst unit suite green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
Adds an env-gated jvmTest (BUZZ_RELAY_WS / BUZZ_MEMBER_SK / BUZZ_OWNER_SK; CI
skips it) that drives the real block/buzz relay booted from its own compose +
cargo build, with members enrolled via buzz-admin. Verified green against the
running relay:
- NIP-42 member auth, and NIP-OA/NIP-AA agent auth: a brand-new un-enrolled
agent key authenticates using only our owner-signed `auth` tag — the relay's
NIP-OA membership fallback accepts the Quartz-produced attestation.
- Channel lifecycle: kind:9007 create via the existing NIP-29 CreateGroupEvent
(Buzz channels are NIP-29 groups), kind:40002 publish, REQ round-trip with a
byte-identical echo (same id), EOSE.
Interop lessons encoded in the test + README: tenancy is host-bound (connect
with the bound Host or the WS upgrade 404s), `h` values must parse as UUIDs,
and the channel row must exist before channel-scoped kinds are accepted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
Adds Quartz-native models for the entire block/buzz custom kind space, each as
an idiomatic per-NIP package (Event + KIND companion + tags/ + write-DSL +
read accessors, mirroring nip88Polls) and registered in EventFactory. Standard
NIP-29/34/51/42/43 kinds Buzz reuses are left to their existing Quartz classes.
Coverage:
- Agent identity: Persona 30175, Team 30176, Managed Agent 30177, Agent Profile 10100
- Agent telemetry (encrypted): Observer 24200, Engram 30174 (HMAC d-tag derivation
pinned to Buzz reference vectors), Turn Metric 44200 (prior commit)
- Workspace overlays: Reminders 30300, Push Lease 30350, DM Visibility 30622,
Workspace Profile 9033, Identity Archival 9035/9036/8002/8003/13535,
Channel Window 39005/39006, relay admin 9030-9032, moderation 9040-9044, 42000
- Messaging/collab: stream 40002-40100 (+sidecars), DMs 41001/41010-41012,
jobs 43001-43006, forum 45001-45003, workflow 30620/46001-46031,
notifications 44100/44101, presence 20001/20002, huddles 24810/48100-48106,
pairing 24134, audit 48001, media 49001, read-state (NIP-RS helpers on 30078)
All schemas confirmed against the authoritative Rust in a local block/buzz
checkout (buzz-core / buzz-sdk / buzz-relay), not the outdated prose NIPs.
Kinds only reserved-but-unbuilt in Buzz (jobs, workflow lifecycle, some
stream/sidecar/audit kinds) are modeled tolerantly and flagged in KDoc + README.
Kind conflicts with existing Amethyst classes are implemented but deliberately
NOT registered in EventFactory (incumbent keeps dispatch): 9041 (GoalEvent),
20001 (GeohashPresenceEvent), 39005 (GroupPinnedEvent), plus 49001 (Buzz marks
it non-wire) and 30078 read-state (reuses AppSpecificDataEvent).
156 Buzz tests pass; full quartz jvmTest green (no regressions).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
Addresses review feedback on the initial Buzz commit:
- Drop the central `BuzzKinds` registry (un-idiomatic). Each event now
declares its own `const val KIND` on its companion and is wired into
`EventFactory`, matching the nip88Polls-style per-NIP layout used across
Quartz (Event + tags/ + TagArrayBuilderExt write-DSL + TagArrayExt readers).
- Implement Agent Turn Metric (NIP-AM, kind:44200): an encrypted per-turn
token-usage/cost record published by an agent to its owner. content is a
NIP-44 v2 ciphertext of AgentTurnMetricPayload (camelCase), between the
agent (author + `agent` tag) and owner (`p` tag); either party decrypts.
- Verify against a vector generated by Buzz's OWN code rather than a
transcribed schema: a small generator on the real buzz-core emits a signed
44200 event with deterministic keys; AgentTurnMetricVectorTest dispatches it
through EventFactory, NIP-44-decrypts it, and asserts the payload — proving
end-to-end interop. Fixture + generator committed under jvmTest resources.
Schemas confirmed against buzz-core/src/agent_turn_metric.rs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
Introduces a top-level `buzz` package modelling the block/buzz protocol in
Quartz. Buzz is a NIP-29-family relay-group workspace (relay is the source of
truth, plaintext content, server-side membership) that layers an agent +
workspace vocabulary on top, so this package models only the Buzz-custom
extensions and reuses the existing NIP-29/34/51/42/43 classes for the standard
kinds.
- BuzzKinds: the full kind registry mirroring buzz-core `kind.rs`, annotated
with where each standard kind already lives in Quartz.
- Owner Attestation (NIP-OA): the owner-signed `auth` tag that lets agents act
as first-class members. Commitment = SHA-256("nostr:agent-auth:" + agent +
":" + conditions), BIP-340 Schnorr-signed by the owner; conditions grammar
(kind=/created_at</created_at>, canonical decimals, u16/u32 bounds) matches
buzz-sdk `nip_oa.rs` exactly.
- Tests pin Buzz's own published known-answer vector as a cross-implementation
compliance check: our preimage hash equals Buzz's SHA-256 and our verifier
accepts Buzz's reference signature.
Confirmed against the authoritative Rust (buzz-core/buzz-sdk), not the prose
NIP drafts, which lag the implementation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KBSw6smQRyXLiWHeDsZ8
the files are now pure-ASCII and visually unambiguous:
- BlossomPaymentSafetyTest: raw U+202E/U+202C test payload -> escapes
- BlossomPaymentRequired: BIDI_OVERRIDES char array -> escapes
- Sanitizer: RTL_OVERRIDES and ZERO_WIDTH regex classes -> escapes
Verified by mutation: with BIDI_OVERRIDES stripping disabled,
reasonBidiOverridesAreRemoved fails, proving the escaped payload still
carries a real U+202E. Emoji ZWJ sequences in RichTextParserTest are
intentionally untouched (functional joiners, not bidi controls).
test(share): pin naddr encoding for addressable notes in QR payloads
feat(share): add strings for the QR share screen
feat(share): add fixed-height note card for the QR screen
feat(share): add display-only QR screen for notes
feat(share): register the ShareNoteAsQr destination
feat(share): add Share as QR to the note share sheet
fix(share): gate NSFW thumbnail with ContentWarningGate, prefer article title
fix(share): size the QR from available width, add a11y description
fix(share): add QR a11y strings, document QR row in ShareActionRows KDoc
fix(share): compact permanently-covered NSFW thumbnail, fix inert QR width cap
fix(share-qr): close sensitivity gate leaks and text/thumbnail bugs on the share-as-QR card
fix(share-qr): fix stale payload, unreachable controls, missing back button, and screen-wake handling
fix(share-qr): show image thumbnail for image-only notes, hide raw media URL
Merges nostr proposal 2ca4f8ae into main:
- feat(cli): full NIP-34 git collaboration parity for `amy git`
- feat(cli): `amy git grasp list|set` (NIP-34 GRASP server list, kind 10317)
- feat(cli): `amy git browse|cat|log` — read git objects over smart-HTTP
- feat(cli): `amy git init` — bootstrap a repo from the local git checkout
- feat(cli): `amy git label` (NIP-32) and `amy git apply` (patch -> working tree)
- fix(quartz): NIP-34 wire-format interop with ngit (clone/web, issue p, plain r)
- test(cli): live interop check against the real ngit-published amethyst repo
- fix(cli): shallow-clone euc, read truncation, process deadlock
- fix(cli): read routing, status perf, publish-ack + robustness
- fix: complete PR clone multi-value (kinds 1618/1620) + CLI robustness
- revert(amethyst): drop GitStatusIndex auth change; defer to separate proposal
clone/web now serialize as single multi-value tags per NIP-34 (ngit drops
repeated ones) for kinds 30617, 1618 and 1620; readers stay tolerant of the
legacy repeated form.
The GitStatusIndex status-authority guard was reverted before merge: it
authorized against the status event's own `a` tag, which the author controls,
so a forged ["a","30617:<attacker>:x"] defeated it — and unverifiable statuses
were dropped permanently (no re-reduce when the 30617 announcement arrives),
rendering closed issues as open. Refiling as its own proposal.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The isAuthoritative guard added to GitStatusIndex read the repository owner
from the status event's own `a` tag (GitStatusEvent.repositoryAddress() ->
first a tag, no kind filter, no cross-check against the target's repo). That
value is attacker-controlled: a forged kind:1632 carrying
`["a", "30617:<attacker-pubkey>:anything"]` makes `status.pubKey ==
repoAddress.pubKeyHex` pass, so the spoof it meant to block still succeeds.
It also regressed reads: reduceLatestByTarget only re-runs on a new
kind 1630-1633, so a status dropped while the 30617 was uncached stayed
dropped, leaving genuinely-closed items in the Open tab with wrong counts.
Keep this series focused on the quartz + cli NIP-34 parity work. The Android
status-authorization hardening (resolve the repo from the target item, load
the cached 30617, authorize against repo.pubKey + maintainers() + the target
author, and re-reduce when a 30617 arrives) will land as its own proposal.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UKMaNoK5M2PQKCAxhxWzPr
Addresses review findings from a merge-time audit.
- **Complete the headline multi-value `clone` fix for PRs** (was applied only to
kind:30617). GitPullRequestEvent (1618) and GitPullRequestUpdateEvent (1619)
carry `clone` with the same spec shape but still emitted repeated single-value
tags and read only the first value — so the exact interop bug this branch set
out to kill was still live for PRs, both directions (ngit keeps only the last
repeated tag; we lost every URL after the first from ngit's multi-value tag).
Now both emit one multi-value `["clone", …]` tag and read both forms. Verified
on the wire + GitNip34InteropTest + CLI harness (40 checks).
- **Android git-status spoofing (GitStatusIndex)**: newest-status-wins with no
author check meant anyone could publish a kind-1632 and make someone else's
issue render closed. Now filter statuses to the repository owner (from the
status's own `a` tag), declared maintainers (from the cached announcement), or
the target item's author — matching NIP-34 and the CLI's derivation. Pre-existing
on main; this branch made the CLI/Android divergence visible.
- **CLI robustness**: `git comment`/`git patch` no longer block forever reading
stdin on an interactive TTY (amy is non-interactive — error instead). The local
`git` subprocesses in `git init`/`git apply` now drain stdout on a side thread
under a bounded `waitFor` + `destroyForcibly`, so a wedged git can't hang the
CLI.
Left as a follow-up (cosmetic): GitBrowseCommands.candidateUrls duplicates
GitRepositoryBrowserViewModel's — worth lifting to shared code, not worth the
cross-module coupling here.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UKMaNoK5M2PQKCAxhxWzPr
Findings from a second review round (two independent reviewers), with fixes:
Read path (git issues/patches/prs/thread):
- **Reads ignored the repo's own relays** (correctness). They queried only the
account outbox/bootstrap (general relays); NIP-34 events live on the repo
announcement's advertised relays (often GRASP/git-specific), which general
relays don't mirror — so `amy git issues <repo>` with no --relay could return
empty. Now fetch the announcement once and read from queryTargets ∪ its
advertised `relays`. Verified live: `git issues`/`git prs` on the amethyst
repo now return real events (and derive `closed`) with NO --relay.
- **O(items × statuses) status rescan** with un-memoized `rootEventId()` reparse
→ pre-group statuses by root id once (O(1) lookup per item).
- **Status query could truncate / exceed relay caps**: statuses are now paged
(`drainAllPages`) and the `#e` id set is chunked to 50 (under the common
~100-value relay filter cap).
- **Latency regression**: capped the list `drainAllPages` idle timeout to 12s
(was the 30s default; `drain` had been 8s).
- **Nondeterministic status on same-second ties** → deterministic id tie-break.
- Reuse the fetched repo for the maintainer set (removes a redundant round-trip).
Write path:
- **`git init` silently reported success when the 30618 state publish failed**
— its ack was dropped. Now surfaced as `state_published_to`/`state_rejected_by`
with a stderr warning on total rejection.
- **`git apply`** feeds stdin as UTF-8 (was JVM default charset — corrupted
non-ASCII patches) and joins the stdin thread in `finally` (no leak on error).
- **`normalizeCloneUrl`** drops the port from `ssh://git@host:port/…` (it was
carried into the https URL, making it unreachable).
- **Delivery fallback** to the account outbox (repo unresolved / no advertised
relays) now warns to stderr instead of reporting silent success.
Known limitation (documented, not fixed): patch-revision-chain status derivation
follows only the root item, and `git thread` shows first-level replies only
(nested trees and 1619 PR-updates are out of scope). 38/38 harness green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UKMaNoK5M2PQKCAxhxWzPr
Findings from a review pass over the git-parity branch, with fixes:
- **`git init` announced a WRONG earliest-unique-commit on shallow clones**
(interop-critical). `git rev-list --max-parents=0 HEAD` returns the shallow
boundary commits, not the true root, so the repo would be announced under a
different cross-fork identity than ngit computes. Now: detect shallow clones
and omit the euc with a warning to pass `--earliest-commit`; on full clones
derive the deterministic `--first-parent` mainline root instead of an
arbitrary `tail -1`.
- **`git issues|patches|prs` silently truncated and mis-derived status** on
active repos: one single-page `drain` pulled items AND status events under a
shared cap, so status events (newer, more numerous) could crowd items out of
the window and the close-status that determines an item's state could fall
outside it → a closed item read as open. Now paginate the items
(`drainAllPages`) and fetch exactly the statuses that `e`-reference them.
Verified on the live amethyst repo: 51 PRs paginated, 19 correctly closed.
- **Pipe-buffer deadlocks** (latent): `GitInitCommand.git()` discards stderr to
the OS (a chatty command can no longer fill its stderr pipe and hang the
stdout read); `GitApplyCommand.runGit()` writes stdin on a background thread
while draining stdout, so a patch larger than the pipe buffer can't deadlock.
- Minor: `git cat` binary detection uses an index loop instead of boxing 8000
bytes; `GitRepositoryEvent.clones()/webs()` dedupe.
The harness `git init` test now runs against a fresh full checkout (this repo's
CI checkout is shallow) and adds a shallow-clone case asserting the euc is
omitted. 38/38.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UKMaNoK5M2PQKCAxhxWzPr
Adds a `--live` block that reads the actual amethyst repository ngit publishes
to relay.ngit.dev and asserts our reader parses ngit's real multi-value `clone`
tag (currently 4 URLs) plus its published issues. This is the real-world proof
of the multi-value interop fix: the pre-fix reader would have surfaced only the
first clone URL. Opt-in (needs network + the live relay), skipped by default.
Verified manually end-to-end against the live repo: repo announcement (4 clone
URLs), issues (1621), patches (1617), pull requests (1618, with a real `closed`
status derived from ngit's status event), and a NIP-22 comment via `git thread`
all read correctly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UKMaNoK5M2PQKCAxhxWzPr
Verified Amethyst's NIP-34 events byte-for-byte against the ngit reference
implementation (DanConwayDev/ngit-cli) and the spec, and fixed three real
interop divergences in quartz — so ngit/gitworkshop and Amethyst read each
other's git repos, issues, patches, and PRs without losing data.
- Repository announcement `clone`/`web` were emitted as REPEATED single-value
tags (`["clone", a]`, `["clone", b]`). The spec and ngit use ONE multi-value
tag (`["clone", a, b]`), and ngit's parser keeps only the LAST of repeated
known tags — so multi-URL repos silently lost every URL but one in both
directions. Now emitted as a single multi-value tag; `clones()`/`webs()` read
BOTH the spec form and the legacy repeated form, so old events still parse.
(`relays`/`maintainers` were already correct multi-value tags.)
- Issues (kind 1621) were missing the `["p", <repo-owner>]` tag that patches and
PRs already include — a maintainer watching `#p` wouldn't see them. The
builder now adds it (fixes both the CLI and the Android issue-creation path,
which both passed an empty notify list).
- Patch / PR / PR-update `r` tags carried the `"euc"` marker
(`["r", commit, "euc"]`). Per the spec and ngit that marker belongs only on
the kind-30617 announcement; other `r` tags are plain `["r", commit]`. A `#r`
filter matches either shape, so this is a spec-compliance/byte-parity fix.
`alt` (NIP-31) tags are intentionally still omitted — quartz treats the generic
alt client-hint as deprecated, and ngit/gitworkshop parse the structured tags,
so it isn't required for interop.
Adds `GitNip34InteropTest` (5 cases: multi-value write, tolerant read of both
forms, issue p-tag, plain patch r-tag) and 4 wire-format assertions to the CLI
git harness (37 offline). No regressions in the nip34 or Search suites.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UKMaNoK5M2PQKCAxhxWzPr
Close two more ngit/nak parity gaps:
- `git label TARGET LABEL[,LABEL]` — attach NIP-32 kind:1985 labels to an
issue/patch/PR (the `ngit pr label` / `issue label` surface), over quartz's
existing `LabelEvent`. Namespace defaults to `ugc`; `--namespace` overrides.
- `git apply PATCH_ID` — fetch a kind:1617 patch and apply it to the local
working tree via `git am` (the `nak git patch apply` / `ngit pr apply`
surface); `--check` dry-runs `git apply --check`, `--print` emits the patch.
Shells out to `git` like `git init`, since it operates on the local checkout.
Verified end-to-end: a patch published to a relay, fetched, and `git am`'d as a
real commit into a scratch repo; labels land as kind 1985. The harness gains 5
assertions (label + a full publish→apply round-trip), now 33 offline.
Remaining out-of-scope items are documented: git-packfile push (needs a git
write layer quartz lacks) and NIP-34 cover notes (kind 1624, no quartz builder).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UKMaNoK5M2PQKCAxhxWzPr
Match `ngit init` / `nak git init`: read the local git repository and publish a
NIP-34 repository announcement, deriving the fields instead of making the user
type them. Shells out to `git` to determine the name (top-level dir), clone URL
(origin remote, ssh→https normalized), earliest-unique-commit (root commit),
and — for the accompanying kind:30618 state — the branch/tag tips and HEAD.
Publishes the 30617 announcement and (unless `--no-state`) the 30618 state in
one shot. Every derived value is overridable with a flag; outside a git repo
the derivation is skipped and `--name`/`--clone` are supplied manually.
This is the one `amy git` verb that shells out to `git`, since it is inherently
about the local working tree — exactly like the tools it mirrors.
Verified against the amethyst checkout itself (derives name=amethyst, the origin
clone URL, the root commit as EUC, and a 30618 with the live branches + HEAD).
The harness gains 4 assertions driving `git init` against its own checkout.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UKMaNoK5M2PQKCAxhxWzPr
Give `amy git` the git-object read side of `nak git download` / a shallow
clone. `browse` lists a repo's tree, `cat` prints (or `--out` writes) a file at
a ref, and `log` shows recent commit history — all over the git smart-HTTP v2
protocol via quartz's `GitHttpClient` (the same shallow-clone path the Android
repo browser uses). REPO may be a NIP-34 coordinate/naddr (whose announcement
supplies the clone URL) or a raw http(s) clone URL; `--clone` and `--ref`
override the URL and branch/tag.
Read-only: pushing git objects back to clone/GRASP servers stays out of scope.
Verified live against a public repo (octocat/Hello-World) — browse/cat/log all
return correct trees, blobs, and history. The harness gains a `--live` block
(28 assertions with `--live`, 24 in the default offline run) exercising these
against `$LIVE_REPO`, skipped by default since it needs a reachable git host.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UKMaNoK5M2PQKCAxhxWzPr
Declare/read a user's preferred GRASP (Git-over-Nostr hosting) servers in
preference order — the NIP-65-style list `ngit`/`nak git` consult to decide
where PR tip branches (`refs/nostr/<pr-id>`) get pushed. `set` publishes a
kind:10317 to the outbox; `list` reads it back cache-first (anonymous-capable).
Thin assembly over quartz `UserGraspListEvent`. The git push itself stays out
of scope, as with the rest of the packfile transport.
Extends the git NIP-34 harness with a grasp round-trip (24 assertions) and
updates the README/ROADMAP/help tables.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UKMaNoK5M2PQKCAxhxWzPr