The two new boards were added as top-bar icons, which put a Buzz channel back to
four (Canvas, Backlog, Workflow runs, ⋮) and truncated the title row the bar is
there to show — "nosfabrica.commun…" instead of the full relay. That is the
crowding #3729 had just removed; this branch predates it and the merge stacked
them.
Canvas keeps the only icon: it is the channel's shared document, i.e. content.
Backlog and Workflow runs are two more views of the channel, reached
occasionally, so they join Threads and Share in the overflow — and pick up the
`!isDm` gate the icons never had.
Also gives the job board a string resource; the branch's i18n pass left
"Backlog" hardcoded in JobBoardScreen and in the icon's contentDescription.
Adds cli/tests/buzz/agent-exec.sh, which covers what the two loop harnesses
stub out. job-loop.sh proves the scheduler drives *an* --exec program; nothing
committed exercised the real one — the wrapper that turns a job into a PR. With
a stubbed `gh` and agent (no network, credentials, or Claude Code) it asserts
the happy path end to end (task on stdin → agent → commit → push the job branch
→ PR url on stdout) and, as importantly, the paths that must fail: an agent that
changed nothing becomes a job error, an empty task is rejected before the agent
runs, missing scheduler env is a hard error rather than a silent no-op, an agent
that committed for itself is not double-committed, and the default-branch guard
holds — asserting `main` on the remote is left untouched. 19/19.
Verified on emulator-5554: the bar is Canvas + ⋮ again with the full relay name
visible, the menu reads Threads / Backlog / Workflow runs / Share / Members /
Leave, and Backlog still opens from it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Switch the agent-support-channel prototype from the speculative agent-job
kinds (43001-43006, reserved with no upstream builder) to Buzz's real,
source-confirmed workflow primitive: 30620 def / 46020 trigger / 46001-46007
lifecycle / 46010 approval gate / 46030-46031 grant-deny (pinned against
buzz-relay's command_executor.rs). This bakes the human-approval gate into
the protocol — anyone in the channel can drive a run, but a human grants
before anything is pushed.
- commons WorkflowRunAggregator folds trigger + lifecycle + grant/deny into
per-run state (TRIGGERED/RUNNING/AWAITING_APPROVAL/APPROVED/COMPLETED/
FAILED/DENIED), correlating by run id (= trigger event id = approval token);
8-case test.
- cli `amy buzz workflow` — trigger/list/show/approve/deny plus the `run`
runner: per new trigger it does the agent work in a fresh worktree+branch,
posts the 46010 gate, and on a later poll runs --on-approve (push + PR) and
emits 46005 completed; a deny discards the worktree (run is DENIED).
On a real Buzz relay the relay executes the workflow YAML; self-hosted on
geode there is no engine, so amy is the runner and emits the lifecycle events
itself (documented divergence). Two store realities, both verified against
geode: decisions are fetched by author (quartz's store serves #d only for
addressable kinds, and 46030/46031 are regular), and the runner is
restart-safe (runs at the gate are rebuilt from the deterministic run id).
Also hardens the exec helper against a broken pipe when --exec doesn't read
stdin, and fixes worktree teardown to run git against the owning repo.
End-to-end headless harness (cli/tests/buzz/workflow-loop.sh) covers the
grant and deny paths through an embedded geode relay: 14/14 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011mApqAbr8vkLC7gUDjavu6
Prototype the "human drives an AI coding agent to develop Amethyst" support
channel on top of the existing block/buzz integration, all through amy.
- commons: BuzzJobAggregator (BuzzJobs.kt) — a pure, tested folder that
correlates the Buzz agent-job kinds (43001-43006) by their `e` request
reference into JobView records with a REQUESTED→ACCEPTED→IN_PROGRESS→
COMPLETED/FAILED/CANCELLED state machine (newest terminal wins). Shared so a
future mobile Jobs board reuses one correlation path. 9 unit tests.
- cli: `amy buzz job request|list|show|cancel` (requester side) and
`amy buzz agent serve --exec CMD` (the responder loop): polls for REQUESTED
jobs targeting my key, gates intake on `--accept-from` (allowlist) and
`--channel`, then accepts (43002) → progress (43003) → runs `sh -c CMD`
(task text on stdin; BUZZ_JOB_ID/REQUESTER/CHANNEL/RELAY/AGENT in env) →
result (43004) or error (43006). Point `--exec` at a coding agent to drive it.
- cli/tests/buzz/job-loop.sh — self-contained headless harness over an embedded
`amy serve` relay; asserts the full loop AND the permission gate (an allowlist
excluding the requester handles nothing).
- Design doc cli/plans/2026-07-25-buzz-agent-support-channel.md: the three-layer
permission model (Buzz scopes by identity, not capability flags — so
"can't merge/destroy main" lives in GitHub branch protection + the `--exec`
credential, not the relay), the MVP architecture, and the prioritized mobile
app gap list (approvals inbox, jobs board, diff/PR review, …).
Schema caveat: kinds 43001-43006 are reserved in Buzz with no upstream builder;
the tag layout is Quartz's best-effort model, to be reconciled upstream.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011mApqAbr8vkLC7gUDjavu6
Extend `amy git` from repo announce/list/show/issue to the complete
pure-Nostr surface of `ngit` and `nak git`, so every NIP-34 collaboration
flow is scriptable without a GUI.
New sub-verbs (all thin assembly over quartz's `nip34Git` builders):
- `git state` — kind:30618 repository state (branch/tag tips + HEAD)
- `git patch` — kind:1617 patch from `git format-patch` (--file or stdin),
with --root/--root-revision, --commit, --parent-commit,
and --in-reply-to for revision chains
- `git pr` / `git pr-update` — kind:1618 pull request + kind:1619 tip update
- `git comment` — NIP-22 kind:1111 reply on an issue/patch/PR/repo (the
modern replacement for the deprecated kind:1622 git reply)
- `git open|applied|close|draft` — kind:1630/1631/1632/1633 status events
(aliases `merged`/`resolved` for applied); applied carries
--merge-commit / --commit / --patch
- `git issues|patches|prs` — list a repo's items with status derived from the
newest authoritative (owner/maintainer/author) status event,
with --open/--applied/--closed/--draft/--status filters
- `git thread` — one item plus its status timeline and comments
Shared parsing/fetch/routing glue lives in `GitSupport`; the existing
announce/list/show/issue verbs now reuse it. The git *packfile* transport
(clone/fetch/push of real objects to clone/GRASP servers) stays out of
scope — it needs a git plumbing layer, not an event builder — and is
documented as such.
Adds `cli/tests/git/git-nip34-headless.sh` (21 assertions, drives the whole
flow against `amy serve` and checks the status-deriving reads) and updates
the README/ROADMAP command tables.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UKMaNoK5M2PQKCAxhxWzPr
Adds cli/tests/blossom/blossom-live.sh, an end-to-end harness that drives the
full amy blossom lifecycle against a REAL public Blossom server: upload → HEAD
check → download-and-verify-hash → list → cross-server mirror → delete. Verified
green (6/6) against files.sovbit.host with a mirror to blossom.primal.net, which
exercises the whole quartz/commons/CLI Blossom stack over the wire.
Server-side write rejections (whitelists, rate limits, payment) record as SKIP,
not FAIL — only a broken client contract (bad descriptor, hash mismatch,
unparseable list) fails — since public servers gate anonymous writes very
differently. Documented in cli/tests/README.md; state dir gitignored.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ckbnz1N94W1hnNC9xpsCNP
Drives the built `amy` binary against a real `amy serve` relay to prove NIP-77
deletion propagation end-to-end — the running answer to "does it actually work",
on top of the in-process geode tests:
T1 (up) we deleted a note the relay still has → `amy sync` sends our kind-5
up and the relay drops it (checked by an isolated third account that
reads the relay only, so no local tombstone masks the result).
T2 (off) `--no-sync-deletions` sends nothing and the relay keeps the note.
T3 (down) the relay deleted a note we still hold → `amy sync --up` pulls the
relay's kind-5 down and applies it locally; a second sync converges.
Each amy account gets its own $HOME (accounts under one $HOME share the file
store). Follows the cli/tests/*-headless.sh pattern; state dir gitignored.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgL1WTV4Hkp2uuXcUHCHGt
Adds a first-class `amy relaygroup` verb group so the CLI can drive the
same NIP-29 relay groups as the app. Thin assembly over quartz builders +
Context.publish/drain — no protocol logic in cli/.
Verbs:
- list / browse RELAY / info RELAY GID — reads (joined kind:10009 list with
private-item decryption; a relay's 39000-39003 directory; one group's
metadata + roster).
- create / join / leave / message — lifecycle. join/leave also maintain the
caller's kind:10009 list (add/remove private item) so `list` reflects them,
mirroring the app's follow/unfollow.
- edit / invite / put-user / remove-user — moderation (9002/9009/9000/9001).
All writes pin to the group's single host relay. Output follows amy's
text/--json contract with snake_case keys.
Verified end-to-end against an embedded relay (amy serve) with a new
self-contained harness, cli/tests/relaygroup/relaygroup-headless.sh:
create/message/join/list/browse all pass (5/5). browse/info return empty
against geode since it doesn't sign 39000-39003 — a relay capability, not a
client issue. README command table + ROADMAP updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
Local-only shell suite (no relay): asserts amy decodes the canonical CLINK
interop vectors (same fixtures as quartz ClinkInteropTest) to the right fields
for 'offer info' and 'debit info', plus the argument-error paths (bad pointer,
unknown budget frequency, missing --amount). The round-trip verbs need a live
service and stay out of scope.
12/12 assertions pass locally (amy init -> decode, no network).
Adds cli/tests/nests/nests-interop.sh — a 47-test operator-driven
interop script that walks a tester through every audio-room edge case
between Amethyst Android and the nostrnests.com NestsUI-v2 web client.
Coverage: bidirectional host/listener flows, audio round-trip on
moq-lite Lite-03, hand-raise + role promotion/demotion, mute, kind:7
reactions (including NIP-30 custom emoji + 30 s overlay clear), kind:1311
in-room chat (text + emoji + link + image upload + history backfill),
kind:4312 kick, room edit + close, scheduled rooms (status=planned),
multi-speaker, background audio + PIP, network drop reconnect, 10-min
JWT refresh, custom moq servers (kind:10112), naddr deep links, and
edge cases (long titles, empty rooms, leave-stage, dedupe, race).
The script follows the marmot/marmot-interop.sh pattern: shared
logging + result helpers, color-coded prompts (yellow=Amethyst,
magenta=web, cyan=optional 3rd identity), p/f/s confirms recorded to a
TSV, and a final summary table. Skip / only / keep-state flags are
supported for iteration.
Pure manual harness — amy does not yet ship `amy nests <verb>` so
nothing is automatable from the CLI side.
`tools/` is for libraries (arti-build); shell-based interop harnesses
that drive the `amy` binary belong with the CLI module. New layout:
cli/tests/
├── lib.sh # shared logging + result tracking
├── headless/helpers.sh # shared amy_a / amy_json / assertions
├── marmot/ # MLS group-messaging interop (vs whitenoise-rs)
│ ├── marmot-interop.sh
│ ├── marmot-interop-headless.sh
│ ├── setup.sh
│ ├── tests-create.sh
│ ├── tests-manage.sh
│ ├── tests-extras.sh
│ └── patches/
└── dm/ # NIP-17 DM interop (amy ↔ amy)
├── dm-interop-headless.sh
├── setup.sh
└── tests-dm.sh
Per-suite changes:
- Each suite owns its own setup.sh; previously the Marmot setup was at
headless/setup.sh and the DM setup at headless/setup-dm.sh, which
implied shared infra they don't actually share.
- `cli/tests/lib.sh` and `cli/tests/headless/helpers.sh` are the only
shared bits across suites.
- The DM harness still reuses Marmot's `start_local_relay` /
`stop_local_relay` (sourced via `../marmot/setup.sh`) — same relay
binary, no duplication.
- All sourcing paths updated; REPO_ROOT now climbs three levels (was
two when the harness lived under `tools/`); patches/ is now a sibling
of marmot/setup.sh rather than under marmot/headless/.
- `.gitignore` updated to cover both suites' state dirs.
cli/ROADMAP.md and cli/tests/README.md updated to point at the new paths.
No behavioural change — every test runs the same code; only the file
layout and source paths moved.