Skip to content

feat(projects): add buzz projects CLI commands (NIP-MP kind:30621) - #4020

Merged
wpfleger96 merged 5 commits into
mainfrom
duncan/projects-cli
Aug 2, 2026
Merged

feat(projects): add buzz projects CLI commands (NIP-MP kind:30621)#4020
wpfleger96 merged 5 commits into
mainfrom
duncan/projects-cli

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Jul 31, 2026

Copy link
Copy Markdown
Member

Implements the buzz projects command 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 in buzz-sdk and the seven CLI commands.

What this adds

crates/buzz-sdk/src/builders.rs — two-layer builder

Layer A (protocol):

  • validate_project_envelope(tags, content) — 8 NIP-MP rules in relay order: d-cardinality, d-empty/length, member-cap (≤64 a tags, checked before per-tag parse), member-tag-arity (2–3 elements), member-coordinate grammar (first-two-colons split, literal 30617, 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.
  • ProjectMemberCoord30617:<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) — constructs d tag, enforces UUID channel and listed|unlisted visibility, forces empty content; composes onto Layer A. This is the create path.

Shared:

  • build_delete_addressable(kind, pubkey, d) — generic NIP-09 kind:5 coordinate delete; build_workflow_delete now delegates to this.
  • All 31 NIP-MP.fixtures.json cases exercised through build_project_with_tags; count assertion guards against omissions.

crates/buzz-cli/ — seven commands

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> [--repo <coord>]...
buzz projects remove-repo <slug> --repo <coord> [--repo <coord>]...
buzz projects update <slug> [--name|--clear-name] [--description|--clear-description] [--channel <uuid>|--clear-channel] [--visibility listed|unlisted|--clear-visibility]
buzz projects delete <slug>

Command semantics:

  • create: all local validation (slug, repos, channel, visibility, name length) fires before the collision preflight — invalid input returns Usage without a network call. Routes through Layer B (build_project).
  • update: at least one setter/clearer required — enforced by a clap ArgGroup with required(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 --repo values return Usage without touching the relay.
  • delete: head-based tombstone at created_at = head + 1; post-submit re-query verifies tombstone landed.
  • All mutations: strip auth, re-validate full envelope through Layer A; created_at advances from observed head, never wall-clock.
  • Relay hints on existing member tags preserved verbatim through RMW.

Limitations (recorded, not in scope)

  • No relay-hint authoring: --repo carries a coordinate only; existing hinted a tags survive RMW unchanged.
  • Signer-self delete only: NIP-OA owner-delete extension not exposed; delete targets the signer's own coordinate.
  • Deletion durability: watermark carry-over applies; delete is best-effort against a later-arriving replacement.

Live round-trip

21-step transcript executed against a relay built from origin/main b1b283cd4, covering create, get, multi-field update (name + description + channel in one call), channel set/clear, add-repo, remove-repo, delete (tombstone verified at head+1, repeated delete → NotFound). Delta transcript confirmed multi-field update, channel set/clear, no-op add-repo → Conflict exit 5, empty update and setter+own-clearer both rejected at parse time. Duplicate create → Conflict. Cross-owner add-repo with full coordinate exercised.

@wpfleger96
wpfleger96 requested a review from a team as a code owner July 31, 2026 21:50
@wpfleger96
wpfleger96 force-pushed the duncan/projects-cli branch from 153db41 to 4e5e4e9 Compare July 31, 2026 22:07
@wpfleger96 wpfleger96 changed the title feat(buzz-cli, buzz-sdk): add buzz projects command group (NIP-MP kind:30621 write path) feat(projects): add buzz projects CLI commands (NIP-MP kind:30621) Aug 1, 2026
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 and others added 5 commits August 1, 2026 13:05
…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
wpfleger96 force-pushed the duncan/projects-cli branch from 8befc0a to cface63 Compare August 1, 2026 17:07
@wpfleger96
wpfleger96 merged commit b7bb151 into main Aug 2, 2026
33 checks passed
@wpfleger96
wpfleger96 deleted the duncan/projects-cli branch August 2, 2026 16:48
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant