Buzz renders one card per `kind:30617`, so a project spanning several repositories has no representation — the relay, desktop app, and mobile app look like three unrelated things. This adds the spec for the container event that fixes that, plus the two shared fixture files that make it machine-checkable. Docs only; no code changes. Membership cannot live in the repository announcements themselves. A project spanning Alice's and Bob's repositories would need *both* of them to publish a tag naming the group, and Alice cannot sign for Bob's key. A project's own name, description, and channel binding likewise have no single writer when scattered across per-repository tags, and no deletion story. That is why multi-repo grouping is the one forge concept in Buzz that warrants a custom kind. ## `docs/nips/NIP-MP.md` `kind:30621`, an addressable event per NIP-01, addressed by `(pubkey, 30621, d)`. Members are `a` tags holding canonical `30617:<lowercase-64-hex-owner>:<repo-d>` coordinates, following NIP-01's 2-or-3-element grammar where the optional third element is a relay hint clients MAY use and whose content ingest does not parse. Metadata is `name`, `description`, `buzz-channel`, `buzz-visibility`. - **Authority stops at the container.** The signer can replace their own project and nothing else — no edit, delete, push, or admin over any member. Deletion additionally admits the signer's registered NIP-OA owner, because `validate_standard_deletion_event` (`crates/buzz-relay/src/handlers/side_effects.rs`) grants that platform-wide so a human can clean up events published by an agent they own; the spec documents it as a Buzz extension to NIP-09 rather than carving `kind:30621` out of it. `buzz-channel` on a project is metadata only; git push policy reads the repository's own `kind:30617` (`crates/buzz-relay/src/api/git/policy.rs`) and a project never becomes an input to it. - **Ingest validation contract**, with named rules the fixtures reference: `d-cardinality`, `d-empty`, `member-cap` (64, counting every `a` tag), `member-tag-arity`, `member-coordinate-malformed`, `member-duplicate`, `metadata-cardinality`, `metadata-length`. Arity is its own rule rather than part of coordinate parsing, because a four-element member tag can carry a valid coordinate — the tag's shape is what is wrong, and ignoring elements past the relay hint would admit unvalidated data no consumer reads. Duplicates are rejected rather than normalized — a relay cannot rewrite tags inside a signed event without invalidating its id and signature. - **Metadata interpretation is normative, not left to the reader.** Ingest bounds cardinality and length and interprets nothing; clients resolve absent `name` to the `d` value, any unrecognized `buzz-visibility` token to `listed` (a typo is not a privacy signal), and an unresolvable `buzz-channel` to a project rendered without a channel rather than dropped. `content` carries no meaning: writers SHOULD emit `""`, and readers and relays MUST ignore any value rather than reject it. - **Claim authority.** A project suppresses a member's standalone card only when it is listing eligible *and* its signer is that repository's owner or appears in the repository's own `maintainers` tag. Without this, anyone could publish a project naming your repository and pull it out of the collection into a container you never consented to. An unauthorized project still renders, and still renders its members — it just cannot remove a repository from where its owner expects to find it. - **Deterministic client fold**, seven steps, with a table of required cases: exhaustive enumeration (a fixed `limit: 200` makes repository 201 vanish), multiple membership, fallback to a standalone card, unresolvable members marked unavailable rather than dropped, and local hide of a container never hiding repositories. On a relay that provides no exhaustive mode, the conformant behavior is a persistently marked possibly-incomplete collection — not a violation of the enumeration requirement. - **Pagination is specified in two modes**, because exhaustive enumeration is not universally achievable. Both modes share an explicit three-condition relay contract: a relay must (1) apply the complete filter before enforcing any limit, (2) expose the exact effective page limit it enforces, and (3) saturate pages — return `min(effective limit, remaining matches)`, so a short page proves all remaining matches were returned. A relay satisfying any proper subset does not provide the guarantee, and absent it a client MUST mark the collection possibly incomplete. On a relay exposing a composite `(created_at, event id)` keyset cursor — Buzz does on its authenticated HTTP bridge endpoint, via `until` + `before_id`; the NIP-01 websocket REQ path silently discards `before_id`, so a websocket client against Buzz is in mode 2 — clients MUST page by it; within the relay contract the cursor's uniqueness means no skips or re-reads and a short page is an unambiguous end signal, but cursor uniqueness alone does not substitute for the relay contract. A vanilla NIP-01 filter has no id tiebreak, so `until` alone either skips a second's unread events or never advances; there a client MUST drain the boundary second explicitly. The spec also adds normative guidance on query shapes: a client MUST use only query shapes the relay applies completely before limiting, and where a needed constraint (such as `#a`) is post-applied, MUST widen to a pushable shape and match the rest client-side. - **Kind allocation** recorded with the checks performed: `30621` is unassigned in the upstream nostr NIPs kind table and has no nostrbook.dev entry, and it is the one free number between `30620` and `30622` locally. ## `docs/nips/NIP-MP.fixtures.json` The ingest contract: 31 cases — 11 accept, 20 reject — as unsigned templates consumers sign with their own test key. Coverage includes minimal and full projects, zero members, the 64-member boundary from both sides, cross-owner and same-`d`-different-owner members, colon-bearing repository `d` values, relay hints, non-empty `content`, and every rejection rule. Each of the two 256-byte `buzz-` bounds gets its own reject case so neither can hide behind the other's rejection, and duplicate detection is pinned to the coordinate alone by a case whose two identical coordinates carry different relay hints. A four-element member tag carrying an otherwise valid coordinate pins arity separately from coordinate parsing. Every rejection case names the rules that may fire, so an implementation cannot pass by rejecting a bad event for an unrelated reason. ## `docs/nips/NIP-MP.fold-fixtures.json` The fold oracle: 12 cases covering every row of the required-fold-cases table, including the discriminating case where one authorized and one unauthorized project list the same repository — an implementation that requires every listing project to be authorized emits a spurious implicit card, and one that lets any listing project suppress drops a card it owes the owner. Inputs are semantic rather than signed envelopes: a repository or project is named by its coordinate plus only what the fold reads — signer, members, `maintainers`, visibility, viewer-hidden, deletion. Every collection in `expect` is compared as a set, including each container's `members`, since the fold fixes placement and not order. Signing would re-test the ingest contract and obscure what is under test. The fold is where claim authority lives, so without a shared oracle two clients could each satisfy the prose and still render different collections from identical heads. ## `VISION_PROJECTS.md` Line 41's "zero custom kinds" now reads "no custom kind for the repo itself", with a new "One Project, Many Repos" section recording why the one exception is warranted. `30621` rows added to the kind and status tables. Related: #3171 (the `KIND_PROJECT` constant, relay ingest validation of this contract, and the inclusive `created_at <= tombstone` bound this spec's coordinate-deletion rule cites). Independent — either can merge first. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
15 KiB
🐝 Buzz Projects — A Nostr-Native Forge
Someone pushes a fix. Buzz creates a channel for the branch. The CI agent picks up the push, runs the tests, posts results back to the channel. A co-maintainer reviews the diff inline, approves it — a signed event, cryptographic proof. Merge. The workflow runs the integration. The channel archives into a permanent record of why that code exists.
Bug report to merged patch. One place. One search index. One identity system. The branch channel was the pull request, the CI dashboard, and the discussion thread.
This document is the software-forge slice of the broader Buzz platform. VISION.md covers the platform. VISION_SOVEREIGN.md covers the sovereign relay story — one domain, one relay, one project. This doc zooms in on what it looks like when that relay hosts code. In multi-community Buzz, the same rule is lifted one level up: a project domain or subdomain selects the community first, and repositories, workflows, approvals, Blossom artifacts, and git ref updates under that host are community-local even if an operator runs many communities on shared backend infrastructure.
The Project Model
A project lives on the relay. myproject.com in a browser shows the project home. Click a repo and you're at repoa.myproject.com — README rendered, file tree navigable, code syntax-highlighted, clone URL at the top. The same URL serves HTML to a browser and git protocol to git clone. Content negotiation. One URL, two audiences.
Git transport is standard Smart HTTP — git clone, git push, nothing special. Your npub signs pushes. Same domain, same auth, same identity as everything else on the relay. The host in the clone/push URL is also the community selector: the same owner/repo name may exist in two communities without sharing refs, branch protections, workflow runs, approvals, or repo announcements.
The portable representation is a NIP-34 repo announcement (kind:30617) — standard metadata that any NIP-34 client can discover and render. Buzz extends it with buzz- prefixed tags for channel binding and visibility:
{
"kind": 30617,
"tags": [
["d", "buzz"],
["name", "buzz"],
["clone", "https://repoa.myproject.com"],
["relays", "wss://myproject.com"],
["maintainers", "<co-maintainer-npub>"],
["buzz-channel", "<channel-uuid>"],
["buzz-visibility", "listed"],
["buzz-protect", "main", "push-allowed", "<alice-npub>", "<bob-npub>"],
["buzz-protect", "main", "require-approval", "2"],
["buzz-protect", "main", "no-force-push"]
]
}
Branch protections live in the same event — buzz-protect tags. The relay enforces them at the git transport layer. Only npubs listed in push-allowed can push to protected branches. Force pushes are blocked. Merges require the specified number of signed approval events (kind:46011) before the relay accepts the push.
Agents inherit access from their owner via NIP-OA. The relay checks: does the push carry a valid NIP-OA auth tag, and is the owner pubkey in that tag listed in push-allowed? If yes, the push is accepted — the agent's own pubkey doesn't need to be in the list. Add a maintainer, and all their authorized agents can push. Remove the maintainer, and all their agents lose access instantly. Agents without NIP-OA attestation are treated as their own identity and must be listed explicitly.
Standard NIP-34 clients see a normal repo. gitworkshop.dev renders it. ngit-cli works with it. Buzz clients read the buzz- tags and wire up the channel and project UI. One event, two audiences, no custom kind for the repo itself.
NIP-34 is the metadata and discovery layer. Git remains the transport. The transport is boring. The metadata is portable.
One Project, Many Repos
Real work spans repositories. The platform is a relay, a desktop app, and a mobile app — three repos, one project. Render one card per repo and they look like three unrelated things.
Grouping is the one forge semantic that per-repo tags cannot express, and it's worth being precise about why, because everything else here deliberately avoids a custom kind.
Put membership in each kind:30617 and a project spanning Alice's and Bob's repos needs both of them to publish a tag naming the group. Alice can't enroll Bob's repo — she can't sign for his key. Cross-owner grouping becomes impossible, and the project's own name, description, and channel end up scattered across events with no single writer and no deletion story: dropping a repo from the group would mean editing an event you don't control.
So there is exactly one custom kind — NIP-MP, kind:30621. One signer, one replaceable event, all group state in one place:
{
"kind": 30621,
"tags": [
["d", "platform"],
["name", "Platform"],
["a", "30617:<alice-npub-hex>:buzz"],
["a", "30617:<bob-npub-hex>:buzz-infra"],
["buzz-channel", "<channel-uuid>"],
["buzz-visibility", "listed"]
]
}
A project points at repos. That's all it does. The signer gets no authority over any member — no edit, no delete, no push, no admin. Adding Bob's repo to your project is your signed assertion that the two belong together, and it changes nothing about Bob's repo or who can push to it. Push policy reads the repo's own event, never the project's.
The cost is stated plainly: a third-party NIP-34 client sees the member repos individually and ignores the grouping. Nothing degrades — the repos are still standard, portable kind:30617 events. And a repo in no project still renders on its own, exactly as before.
Branches as Channels
A feature branch is a conversation.
When you create a branch, Buzz creates a channel. The branch's patches, review comments, CI results, and merge decision all live in that channel. When the branch merges, the channel archives. The conversation becomes the permanent record of why that code exists.
#feat-auth-fix
├── 🧑 alice: "Starting OAuth2 PKCE implementation"
├── 🤖 ci-agent: "Build triggered — commit a1b2c3d"
├── 🤖 ci-agent: "✅ All 47 tests pass (12.3s)"
├── 📎 kind:1617 patch — src/auth/pkce.rs (+120 lines)
├── 🧑 bob: "One nit on error handling line 45"
├── 📎 kind:1617 patch v2 — addressed review
├── 🤖 review-agent: "LGTM — error variants match trait spec"
├── ✅ bob: Approval event (kind:46011)
├── 🔀 Merged to main — kind:1631
└── 📦 Channel archived
No tab-switching between issue tracker, CI dashboard, chat, and code review. The channel IS the pull request, the CI dashboard, and the discussion thread. One stream. One search index.
The Merge Flow
Push to merge, fully traced. Every step is a signed event.
Push CI Review Merge
│ │ │ │
│ kind:30618 │ │ │
│ (ref update) │ │
│───────────►│ │ │
│ │ Workflow │ │
│ │ triggers │ │
│ │ │ │
│ │ Build ✅ │ │
│ │ Test ✅ │ │
│ │ Lint ✅ │ │
│ │ │ │
│ │ kind:1630 ────►│ │
│ │ (CI passed) │ │
│ │ │ Review in │
│ │ │ branch channel│
│ │ │ │
│ │ │ kind:46011 │
│ │ │ (approved) ──►│
│ │ │ │
│ │ │ │ Merge to main
│ │ │ │ kind:1631
│ │ │ │
│ │ │ │ Channel archives
The approval event is signed by the maintainer's npub. The merge status references the approval. The audit log chains them together. Cryptographic proof of who approved what.
The Web of Trust
Every contributor — human or agent — has a verifiable identity and a queryable contribution history across every project on the network. Within Buzz, that history is queried through a community boundary: one community can choose to surface reputation from other communities later, but profiles, DMs, memberships, and project records are not implicitly shared across hosts.
A new contributor submits a patch. Before you read the code:
- Query their npub — patches submitted, patches merged, projects contributed to.
- Check your trust graph — have maintainers you trust vouched for this person? Signed approval events are public and queryable.
- Assess risk — fresh npub with no history gets scrutiny. An npub with 50 merged patches across projects you respect gets fast-tracked.
This works because identity is cryptographic and portable. Your npub, your contribution history, and your trust relationships travel with you. No platform owns your reputation.
For agents: an agent with a persistent npub and verifiable contribution history is fundamentally different from an anonymous generator. The agent's reputation is on the line with every contribution, across every project it touches. See NIP-OA for the owner attestation mechanism that proves which human authorized which agent — independent keys, contained blast radius.
CI and Workflows
Workflows orchestrate. Agents perform the compute. The relay is the message bus, not the build server.
A push to a branch channel triggers the CI workflow. The workflow engine coordinates the steps — build, test, lint. Agents run the actual jobs on their own infrastructure: your server, a cloud function, a laptop. Results post back to the branch channel alongside the conversation.
Workflows live in the repo (.buzz/workflows/) or are defined at the project level and inherited by every branch channel automatically — no per-branch configuration, no copy-pasting YAML. Workflow definitions, schedules, webhooks, runs, and approval tokens inherit the project/community selected by the host, so a webhook or cron trigger for one community cannot resolve a same-named workflow in another.
name: CI
trigger:
on: diff_posted
steps:
- id: build
action: call_webhook
url: "https://ci.internal/build"
body: '{"commit": "{{trigger.commit}}"}'
- id: test
action: call_webhook
url: "https://ci.internal/test"
if: "steps.build.output.status == 'success'"
- id: gate
action: request_approval
message: "CI passed. Approve merge?"
if: "steps.test.output.status == 'success'"
Every step traced. Every trace a signed event. Change the project CI once and every branch gets it.
Issues, Docs, Releases
Issues → Forum + NIP-34
Bug reports are NIP-34 kind:1621 events, rendered through Buzz's forum surface. Threaded comments use NIP-22 kind:1111. Labels, assignees, milestones are nostr tags. Design discussions and RFCs use the forum's long-form async surface.
NIP-34 clients can discover and interact with issues. Buzz's forum gives them a home with threading, search, and agent triage.
Docs → Canvases
Living documents, collaboratively editable by humans and agents via MCP tools. Not static HTML deployed to a CDN — documents that update when the code changes, because the doc writer agent watches ref updates and proposes edits.
Releases → Agent + Workflow
An agent in #releases watches main. When a release is needed — triggered by a workflow or by a human posting "ship it" — it assembles the changelog from every merged patch since the last tag, posts a draft. The maintainer approves. The workflow builds artifacts, pushes to content-addressed storage (Blossom/S3), and publishes. Logged, signed, traceable.
Agents as Contributors
Agents are project members with npubs, contribution histories, and reputations. The protocol treats them identically to humans. Visual badges distinguish them in the UI.
| Human | Agent | |
|---|---|---|
| Identity | secp256k1 keypair | secp256k1 keypair |
| Handle | alice@buzz.dev |
triage-bot@buzz.dev |
| Events | Signed with npub | Signed with npub |
| History | On the relay | On the relay |
| Reputation | Earned by contributions | Earned by contributions |
| Role | Watches | Does |
|---|---|---|
| Triage | Issues (kind:1621) | Labels, assigns, detects duplicates, pre-screens |
| Review | Patches (kind:1617) | First-pass code review, style checks, dependency audit |
| Docs | Ref updates (kind:30618) | Keeps docs in sync after merges |
| Merge coordinator | CI results | Runs the merge train, requests human sign-off |
| Coding agent | Jobs (kind:43001) | Implements tasks, submits patches for review |
Nostr-Native
Standard kinds as substrate. Custom kinds only where genuinely novel.
| Layer | Standard NIP Kinds | Buzz Custom | Rationale |
|---|---|---|---|
| Git state | 30617, 30618, 1617, 1618, 1621, 1630-1633 (NIP-34) | — | Interop with ngit, gitworkshop.dev |
| Comments | 1111 (NIP-22) | — | Threaded replies everywhere |
| Channels | 9000-9022, 39000-39003 (NIP-29) | — | Project workspaces |
| HTTP auth | 27235 (NIP-98) | — | Git push authentication |
| Agent identity | 0 (NIP-01 profile) | — | Agents are npubs |
| Artifacts | 1063 (NIP-94) | — | Build outputs on Blossom/S3 |
| Workflows | — | 46001-46012 | No NIP equivalent |
| Job dispatch | — | 43001-43006 | Delegation trees |
| Project binding | 30617 (NIP-34) | buzz- tags |
Channel, visibility |
| Multi-repo projects | — | 30621 (NIP-MP) | Cross-owner grouping is unexpressible in per-repo tags |
| Audit | — | 48001 | Hash-chain tamper-evident log |
If Buzz disappears tomorrow, your repos still work on gitworkshop.dev, your patches still work with ngit-cli, your identities still work on any nostr client. Centralized deployment, decentralized protocol.
Status
| Capability | Status |
|---|---|
| Channels, forums, DMs, canvases | ✅ Ships today |
| Workflow engine (triggers, traces, conditional logic) | ✅ Ships today |
| MCP server + ACP agent harness | ✅ Ships today |
| Blossom media storage (SHA-256, S3) | ✅ Ships today |
| Approval gates | 🚧 Infrastructure exists; executor wiring in progress |
Project binding (kind:30617 + buzz- tags) |
📋 Designed |
| Multi-repo projects (kind:30621, NIP-MP) | 📋 Designed |
| Git hosting (smart HTTP + NIP-34) | ✅ Ships today |
| Merge coordinator | 📋 Designed |
| NIP-34 issues (kind:1621) | 📋 Designed |
| Web-of-trust reputation | 📋 Designed |
The collaboration platform is built, and git hosting ships today — git clone/git push over smart HTTP with NIP-34 manifests. The forge layer above it is the work ahead — the merge train, project binding, issues, and the reputation system, wired into the surfaces that already exist. See VISION.md for the platform and VISION_SOVEREIGN.md for the sovereign relay story.
Buzz 🐝 — the forge where identity is the foundation.