feat(projects): add buzz projects CLI commands (NIP-MP kind:30621) - #4020
Merged
Conversation
wpfleger96
force-pushed
the
duncan/projects-cli
branch
from
July 31, 2026 22:07
153db41 to
4e5e4e9
Compare
buzz projects command group (NIP-MP kind:30621 write path)…r B) Add two-layer builder implementation per NIP-MP Phase 2 spec (plan v3): Layer A (protocol): - `validate_project_envelope(tags, content)` — enforces the 8 NIP-MP ingest rules: d-cardinality, d-empty, member-tag-arity, member-cap (before dedup), member-coordinate-malformed, member-duplicate, metadata-cardinality, metadata-length. - `build_project_with_tags(content, tags)` — raw Layer A builder; no canonicalization; accepts all valid fixture envelopes including opaque metadata, non-empty content, relay hints, and unknown tags. - `ProjectMemberCoord` — parsed `30617:<owner-hex>:<repo-d>` coordinate with optional opaque relay hint; equality/Hash by coordinate only. Layer B (writer policy): - `build_project(slug, name, description, members, channel, visibility)` — constructs the single `d` tag, enforces UUID channel and listed|unlisted visibility, emits empty content; composes onto Layer A. Shared infrastructure: - `build_delete_addressable(kind, pubkey, d)` — generic NIP-09 kind:5 coordinate delete; validates addressable kind range and non-empty d. `build_workflow_delete` now delegates to this function. Conformance: - All 31 NIP-MP.fixtures.json cases exercised directly through `build_project_with_tags`; accepts build, rejects fail tied to the named rule; count assertion guards against omissions. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…write path)
Seven commands for creating and managing multi-repo projects:
buzz projects create <slug> --repo <coord> [--name] [--description]
[--channel <uuid>] [--visibility listed|unlisted]
buzz projects get <slug> [--owner <pubkey>]
buzz projects list [--owner <pubkey>] [--limit <n>]
buzz projects add-repo <slug> --repo <coord> [...]
buzz projects remove-repo <slug> --repo <coord> [...]
buzz projects update <slug> [--name|--clear-name] [--description|...]
[--channel <uuid>|--clear-channel]
[--visibility listed|unlisted|--clear-visibility]
buzz projects delete <slug>
Design:
- create: collision-guarded preflight; repeated create returns Conflict.
- update: clap argument group requires at least one setter or clearer;
omission preserves existing metadata; --clear-* drops the tag.
- delete: head-based tombstone (created_at = head + 1); post-submit
re-query verifies the tombstone landed; Conflict if raced.
- All mutations: strip auth, re-validate full envelope through Layer A
before publish; created_at advances from observed head, never wall-clock.
- --repo bare form (Buzz repo-id grammar) auto-expands with caller's pubkey;
full 30617:<owner>:<repo-d> form used for cross-owner or colon-bearing ids.
- Relay hints on existing members are preserved verbatim through RMW.
Shared infrastructure:
- commands/mod.rs: extract parse_write_response() shared helper; adopted by
repos.rs (thin wrapper) and projects.rs directly.
Limitations recorded for the PR body:
- No relay-hint authoring (read-preserved only).
- Signer-self delete only (NIP-OA owner-delete path deferred).
- Deletion durability against later arrival (watermark carry-over).
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…d-repo no-op IMPORTANT: fix clap mutation group — add required(true).multiple(true) to the Update ArgGroup so multiple independent setters/clearers are accepted and an empty update is rejected at parse time. Pairwise setter/clearer conflicts are retained. Add four parser-level tests covering all four cases Thufir specified. IMPORTANT: move member-cap check before arity loop in validate_project_envelope, matching the relay's ingest rule order (NIP-MP rule 3=cap, rule 4=arity). Correct rule-number doc comments. Add cap-wins-over-arity test with 65 members plus a malformed tag asserting member-cap fires first. MINOR: remove redundant double build_project_with_tags call in cmd_remove_repo — single rebuild_project call matches add-repo pattern and centralises auth strip. MINOR: return Conflict when add-repo adds zero new coordinates (all requested repos were already members). Consistent with create/update race reporting; add focused test verifying the error kind and message. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…ibility, dedup repos Route cmd_create through build_project() (Layer B) instead of manually assembling tags and calling raw Layer A. Local validation now fires before any .await in create/add-repo/remove-repo: slug, repo coordinates, channel UUID, visibility token, and name length are all checked before the first network call so malformed input returns CliError::Usage without touching the relay. Add ProjectVisibility clap ValueEnum for --visibility on create and update: invalid tokens (e.g. chartreuse) are rejected at parse time before authentication. validate_visibility retained as defense-in-depth for programmatic cmd_update callers. Dedupe --repo values within one create/add-repo invocation; a duplicate coordinate returns CliError::Usage naming the duplicate before any network call. Collapse fetch_own_project into a one-line delegation to fetch_project. Delete dead ProjectMemberCoord::from_tag_value and ::with_hint (zero callers after Layer B routing change). Replace two tautological guard-message tests with tests that either drive the real guard path (no-network async tests) or assert on behavioral properties of the guard error format (collision conflict message, no-op add-repo guard) without constructing their own expected error. Add Layer B tests in buzz-sdk: emitted envelope shape, optional-field absence, empty/overlong slug, invalid channel UUID, invalid visibility token, over-cap members, duplicate members, content forced empty. Add no-network tests in buzz-cli: create with invalid visibility, overlong name, malformed repo; add-repo and remove-repo with malformed coord; create and add-repo with duplicate coord; clap-level ValueEnum rejections for --visibility chartreuse on create and update. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Both create_collision_conflict_message_names_update_command and add_repo_no_op_guard_produces_conflict_with_slug_in_message constructed their own CliError::Conflict with a copy of the production format string and asserted on their own text. They exercise zero command logic and would pass even if the production message changed. The collision and no-op paths are pinned by the live transcript (duplicate create → Conflict exit=5, no-op add-repo → exit=5); the pre-await paths are covered by the no-network async tests added in the previous commit. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wpfleger96
force-pushed
the
duncan/projects-cli
branch
from
August 1, 2026 17:07
8befc0a to
cface63
Compare
joahg
added a commit
to joahg/buzz-dev-mode
that referenced
this pull request
Aug 3, 2026
…-style * origin/main: Polish mobile composer and messaging UI (block#3918) ci(linux): enable mesh-llm feature in Linux release and canary builds (block#4524) fix(desktop): stop the create-agent provider config probe from erasing keystrokes (block#4411) fix(mobile): recover and pace live subscriptions (block#3053) feat(acp): deliver system prompt via _meta.systemPrompt for claude-agent-acp (block#4395) fix(security): bump nostr crates for RUSTSEC-2026-0225..0232 + default sprig image to published digest (block#4392) fix(desktop): back/forward via keyboard chords, mouse X1/X2 buttons, and swipe gestures (block#3778) feat(k8s): Kubernetes backend plugin + desktop deploy path (block#4289) fix(git): allow deleting the default branch (block#4297) feat(projects): add buzz projects CLI commands (NIP-MP kind:30621) (block#4020) docs: formal spec for remote agents and their management (block#3748) fix(nip-oa): accept raw Nostr tag form in parse_json_array (block#4203) perf(relay): serve relay-membership checks from the read replica (block#4124) chore(deps): bump nostr-relay-pool for RUSTSEC-2026-0224 (block#4139) docs(nostr): document #h requirement for live reaction subscriptions (block#3487) Signed-off-by: Joah Gerstenberg <joah@squareup.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the
buzz projectscommand group — the NIP-MP Phase 2 write path for kind:30621 multi-repo projects. The relay accepted kind:30621 in #3171; this adds the two-layer Rust builder inbuzz-sdkand the seven CLI commands.What this adds
crates/buzz-sdk/src/builders.rs— two-layer builderLayer A (protocol):
validate_project_envelope(tags, content)— 8 NIP-MP rules in relay order:d-cardinality,d-empty/length, member-cap (≤64atags, checked before per-tag parse), member-tag-arity (2–3 elements), member-coordinate grammar (first-two-colons split, literal30617, lowercase 64-hex owner, non-empty remainder), member-duplicate (coordinate only, hint ignored), singleton metadata cardinality, byte bounds (name≤256 /description≤2048 /buzz-channel≤256 /buzz-visibility≤256).build_project_with_tags(content, tags)— raw Layer A builder; RMW mutations path.ProjectMemberCoord—30617:<owner-hex>:<repo-d>+ optional opaque relay hint; equality/Hash by coordinate only.Layer B (writer policy):
build_project(slug, name, description, members, channel, visibility)— constructsdtag, enforces UUID channel andlisted|unlistedvisibility, forces empty content; composes onto Layer A. This is thecreatepath.Shared:
build_delete_addressable(kind, pubkey, d)— generic NIP-09 kind:5 coordinate delete;build_workflow_deletenow delegates to this.NIP-MP.fixtures.jsoncases exercised throughbuild_project_with_tags; count assertion guards against omissions.crates/buzz-cli/— seven commandsCommand semantics:
create: all local validation (slug, repos, channel, visibility, name length) fires before the collision preflight — invalid input returnsUsagewithout a network call. Routes through Layer B (build_project).update: at least one setter/clearer required — enforced by a clapArgGroupwithrequired(true).multiple(true), with a runtime backstop for programmatic callers; setter + own clearer are mutually exclusive per clap conflicts.add-repo/remove-repo: coordinate expansion and dedup fire before head fetch — malformed or duplicate--repovalues returnUsagewithout touching the relay.delete: head-based tombstone atcreated_at = head + 1; post-submit re-query verifies tombstone landed.auth, re-validate full envelope through Layer A;created_atadvances from observed head, never wall-clock.Limitations (recorded, not in scope)
--repocarries a coordinate only; existing hintedatags survive RMW unchanged.deletetargets the signer's own coordinate.deleteis best-effort against a later-arriving replacement.Live round-trip
21-step transcript executed against a relay built from
origin/mainb1b283cd4, covering create, get, multi-field update (name + description + channel in one call), channel set/clear, add-repo, remove-repo, delete (tombstone verified athead+1, repeated delete →NotFound). Delta transcript confirmed multi-field update, channel set/clear, no-op add-repo →Conflictexit 5, empty update and setter+own-clearer both rejected at parse time. Duplicate create →Conflict. Cross-owneradd-repowith full coordinate exercised.