Three findings from the review of the Sonar literal-extraction pass. None were
introduced by that pass — it renamed or sat next to each one.
no_relays told the wrong story. `relaysFor` returns an empty *non-null* set when
--relays was given but every entry failed `normalizeGroupRelay`, so the elvis to
`outboxRelays()` never fires and the user was told to pass the flag they had just
passed. The message now names the real problem and echoes the rejected value; the
`no_relays` error code is unchanged.
`participants` was string-munged. `arr.toString().trim('[',']').split(",")` turns
any shape other than a flat string array into plausible-looking fake pubkeys, and
splits a comma-bearing string into two entries. Now decoded as a JsonArray, with
non-string elements skipped rather than stringified. Extracted to
`participantsOf` so it is testable; BuzzParticipantsTest covers the shapes and
was confirmed to fail (3 of 5 cases) against the old implementation.
Nothing kept the two copies of the buzz-agent wrappers in sync. They exist as
byte-identical trees under cli/src/main/resources/buzz-agent (what `agent up`
extracts) and tools/buzz-agent (what the README tells people to use), with no
build step or test pinning them — I had to hand-apply the same patch twice this
week. BuzzAgentWrapperSyncTest now asserts it. The tools/ tree is declared as a
`:cli:test` input, without which Gradle calls the task up-to-date after a
tools/-only edit and misses exactly the drift being guarded (verified both ways).
Collapses the operator setup from an 8-flag command + two hand-written scripts to
essentially two commands, without removing any of the safety.
- `buzz workflow run` gains `--accept-from-channel` (parity with the job
scheduler): scope intake to the channel's kind-39002 roster instead of pasting
every teammate key. `--worktree` now defaults to the current directory.
- Ship the gated reference wrappers (tools/buzz-agent/workflow-agent.sh →
agent+commit; workflow-ship.sh → push+PR after the gate), split around the
approval gate the way agent-exec.sh is the one-shot ungated version.
- `buzz agent up RELAY --repo DIR --approver NPUB` — one command: resolves the
channel (the relay's only one, or --channel), defaults worktree/intake, extracts
the bundled wrappers to ~/.amy/buzz-agent, and delegates to `workflow run`. The
only thing it can't default is the human approver.
- `buzz agent doctor [--repo DIR]` — preflight that turns the security checklist
into a green/red report: gh authenticated, token can write to the repo, default
branch protected against force-push, worktree clean. Exits non-zero if not.
- cli build: set duplicatesStrategy on processResources (the explicit
resources.srcDir re-adds the default root, which now doubles the bundled scripts).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011mApqAbr8vkLC7gUDjavu6
- Output.error now derives the exit code from the error code
(bad_args -> 2, timeout -> 124, else 1), so every
'return Output.error(...)' site honours the documented contract.
Previously ~225 bad_args sites exited 1 while the docs promised 2,
and two timeout paths (nostrconnect wait, namecoin lookup) exited 1
instead of 124.
- Args: literal '--' ends flag parsing (escape hatch for values that
start with '--'); intFlag/longFlag reject non-numeric values instead
of silently using the default; requireFlag/positional no longer
double-print to stderr; new rejectUnknown() turns typo'd flags into
bad_args failures; new 'help' detection.
- route() understands --help/-h/help (prints group usage, exit 0) and
names the expected verbs on an unknown sub-verb.
- Unknown or missing top-level subcommand now emits a proper bad_args
error (JSON-aware under --json) plus a one-screen verb list instead
of dumping the full 400-line usage.
- RawEventSupport: --relay/--kind/--author/--id/--since/--until/--limit
entries that do not parse are now bad_args errors; previously an
unresolvable --author was silently DROPPED and the query ran with a
weaker filter than requested. New shared publishGuard() reports
'rejected' (exit 1) when every relay refuses an event.
- runCli() seam extracted from main() plus an 'amy.home' system-property
override of DEFAULT_ROOT so the new JVM test suite can drive the CLI
in-process; first contract tests: ArgsTest, ExitCodeContractTest,
JsonContractTest (NIP-19 vector goldens).
BREAKING (--json): error code pow_timeout is now timeout; exit codes
for bad-argument failures move from 1 to 2 as documented.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CP4kfLCa3wWtE8Khy21Pkj
Reverts the Kover coverage aggregation (introduced in dca345212b) from every
module, the root aggregation block, the Sonar coverage import, the version
catalog, and BUILDING.md. With the plugin present the 16 GB CI runner OOM-killed
during the Android test+build job even with Kover opt-in disabled; removing it
entirely clears the OOM. No other CI changes — the diff versus main is exactly
the inverse of dca345212b.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Apply Kover (Apache-2.0, build-time only) to the KMP/JVM modules and aggregate at the root
- include amethyst playDebug in the aggregated Kover coverage
Two new nak-parity commands:
- `amy admin RELAY METHOD [args]` — NIP-86 Relay Management API over NIP-98
HTTP auth. Full method set: ban/unban + allow/unallow pubkey, ban/allow
event, allow/disallow kind, block/unblock IP, change name/description/icon,
and all list-* queries. Reuses quartz's Nip86Client (request build + NIP-98
auth + parse) and the Nip86Retriever HTTP path — extracted from amethyst to
commons/jvmAndroid so amy and the Android relay-management screen share it.
- `amy serve [--host --port --path --db --admin]` — runs a Nostr relay by
embedding geode (the standalone Ktor relay on quartz's relay-server code).
In-memory by default (ephemeral, like nak serve); --db FILE for SQLite. The
account's own pubkey is always an admin, so `amy admin` works against it out
of the box. cli gains a :geode dependency (geode depends only on :quartz) and
kotlinx-serialization-json (to render NIP-86 JSON results).
Verified end-to-end: `amy serve` + `amy admin ws://127.0.0.1:PORT
supported-methods|change-name|ban-pubkey|list-banned-pubkeys` round-trip
cleanly over real HTTP + NIP-98 against the live geode relay.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
amy is headless and compiles against zero Compose UI (the Compose deps are
`implementation` in :commons, so they never hit the CLI compile classpath),
but they still rode the runtime classpath into the shipped image — ~29 MB of
Compose desktop render stack, including skiko's native .dylibs that enlarged
the macOS notarization surface.
Exclude skiko + the org.jetbrains.compose UI groups (ui/foundation/material/
material3/animation) from :cli runtimeClasspath. Keep androidx.compose.runtime
(snapshot state + @Stable/@Immutable) — that IS CLI-safe and used by commons
models/state. This avoids the commons → commons/commons-ui module split: the
single-module, feature-cohesive design (commons/ARCHITECTURE.md §1/§3) is
preserved; only the runtime artifact is trimmed.
Result: amy image lib 77 MB -> 48 MB (-38%), and all 4 Compose/skiko notary
dylibs gone (only secp256k1/jna/sqlite natives remain — the ones actually
loaded). A create-release.yml assertion fails the build if the UI stack ever
leaks back.
Verified with the SDK hidden + an offline amy command battery (init/whoami/
--json/relay/marmot/login, plus a real 6-relay key-package round-trip): zero
NoClassDefFoundError/linkage errors; init derives a secp256k1 key cleanly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sso31DfSF9B6EFCVkEqWD
Phase 0 of the desktop image compression plan
(docs/plans/2026-06-08-feat-desktop-image-compression-plan.md).
- commons jvmMain gains net.coobird:thumbnailator:0.4.21 (pure-Java,
MIT) — to be consumed by the new ImageReencoder in Phase 1.
- amy CLI now sets -Djava.awt.headless=true via three paths so any
transitive ImageIO/AWT touch never spawns a GUI thread:
* applicationDefaultJvmArgs in cli/build.gradle.kts (covers the
installDist startup scripts and any future jpackage launcher),
* the amyImage custom Unix launcher in cli/build.gradle.kts,
* System.setProperty as the first line of cli Main.kt — belt-
and-braces for invocations that bypass the launcher scripts.
- commons:jvmTest forces -Djava.awt.headless=true for the same
reason during test runs.
Smoke tests (CompressionSmokeTest.kt) document Thumbnailator's
upscale-by-default behavior — ImageReencoder must gate the resize
itself in Phase 1.
`marmot message react "$gid" "$id" "🍕"` was producing a kind:7 whose
inner content was four `U+FFFD` replacement characters instead of the
emoji. Whitenoise's `message_aggregator::emoji_utils` rejected it with
`Invalid reaction content: ����`, and the openmls receiver retried the
event ten times before giving up — by which point the secret tree had
already consumed that generation, so the retry surfaced as a confusing
`Ciphertext generation out of bounds 1 / SecretReuseError`. That last
error was a downstream symptom; the root cause is argv decoding.
Why it happens: JEP 400 (Java 18+) pins `file.encoding` to UTF-8 but
deliberately leaves `sun.jnu.encoding` — the encoding the JVM uses to
decode argv, filenames, and native interop strings — tied to the OS
locale, and `sun.jnu.encoding` is read **before** the JVM parses any
`-D` flag, so it can't be overridden via `applicationDefaultJvmArgs`.
Under POSIX/C locales (no `LANG`/`LC_ALL` set, common on CI runners
and stripped-down containers) `sun.jnu.encoding` ends up at
`ANSI_X3.4-1968` — i.e. ASCII — and every byte > 0x7F in argv lands
as `U+FFFD` before our code ever sees it. The four UTF-8 bytes of 🍕
(F0 9F 8D 95) become four replacement chars, get signed into the kind:7
content, and the test fails on receipt.
The only place we can set the encoding from is the launching shell.
Patch the gradle-generated start scripts (POSIX `amy`, Windows
`amy.bat`) right after `:cli:startScripts`:
* POSIX: if neither `LANG` nor `LC_ALL` is set, `export LANG=C.UTF-8`
before invoking Java. Existing locale settings are respected.
* Windows: `chcp 65001` to switch the console to UTF-8 before Java
runs.
Tested manually: with `LANG=` (sandbox default) `amy` previously sent
`argv[0]=????` for an emoji argument; with the patch it sends `🍕` and
`sun.jnu.encoding=UTF-8`, and whitenoise accepts the kind:7 reaction.
The interop test 09 polling also needed a fix unrelated to the
encoding bug: wn aggregates kind:7 reactions onto the anchor message
under `.reactions.by_emoji.<emoji>` instead of surfacing them as
standalone messages whose `.content` is the emoji, so the previous
`wait_for_message B "$mls_gid" "🍕"` would have failed even with a
correctly-decoded reaction. The new poll inspects each message's
`.reactions.by_emoji` keys for the emoji literal.
Marmot interop score: 15/16 → **16/16** 🎉https://claude.ai/code/session_013VYkpz8P1mPh9Ejxy9anhJ
Adds a new build-cli matrix to create-release.yml so every tag push also
produces self-contained amy binaries:
- amy-<ver>-macos-x64.tar.gz (macos-13 runner)
- amy-<ver>-macos-arm64.tar.gz (macos-14 runner)
- amy-<ver>-linux-x64.tar.gz (ubuntu-latest)
- amy-<ver>-linux-x64.deb (ubuntu-latest)
- amy-<ver>-linux-x64.rpm (ubuntu-latest)
Each tarball is a flat tree (bin/amy + lib/*.jar + runtime/) with a
jlink'd JDK 21 embedded — no system Java required on the user machine.
The .deb / .rpm install the same tree under /opt/amy/.
Windows is intentionally deferred until cli/ is validated on Windows
(data-dir path handling, file locking on groups/<gid>.mls, identity.json
line endings).
Two follow-ups flagged in comments:
- :commons leaks Compose + Skiko (~40 MB) as transitive deps to :cli.
Tarball lands at ~98 MB instead of the plan's <80 MB target. Size
budget in the workflow is set to 200 MB until :commons is split into
core + ui modules.
- .deb/.rpm install to /opt/amy/bin/amy with no /usr/local/bin symlink.
Users must add to PATH or symlink manually after install.
Wire-up:
- cli/build.gradle.kts gains jlinkRuntime + amyImage (Sync task that
combines installDist output with the jlink runtime + a shell
launcher) + jpackageDeb + jpackageRpm.
- scripts/asset-name.sh gains cli_asset_name() + collect_cli_assets()
alongside the existing desktop collector, preserving the
<product>-<version>-<family>-<arch>.<ext> naming contract.
See cli/plans/2026-04-21-cli-distribution.md for the overall plan.
https://claude.ai/code/session_01Tbh6F7TtEeceb4K3stcUWp
New :cli JVM module producing an `amy` binary that drives Amethyst
functionality headlessly. Identity and relay configuration sit at the
root (`amy init`, `amy whoami`, `amy relay …`); Marmot/MLS lives under
`amy marmot …` so future verbs (dm, feed, profile) can slot in cleanly.
Marmot surface covered:
- key-package publish / check
- group create / list / show / members / admins / add / rename /
promote / demote / remove / leave
- message send / list (kind:9 inner events)
- await key-package / group / member / admin / message / rename / epoch
(non-interactive polling with --timeout; exit 124 on timeout)
Wiring:
- NostrClient + BasicOkHttpWebSocket.Builder, reusing the existing
publishAndConfirmDetailed and fetchFirst accessories
- MarmotManager from :commons with file-backed MlsGroupStateStore,
KeyPackageBundleStore, and MarmotMessageStore (unencrypted — scratch
harness use only)
- each invocation is one-shot: prepare → syncIncoming → run command →
persist → disconnect
All commands emit one JSON object on stdout; diagnostics on stderr.
Designed so shell harnesses can pipe through jq without bespoke parsing.
https://claude.ai/code/session_01M6dCKAF5Y1VyHGZPjzwDXq