Files
buzz/AGENTS.md
T

16 KiB

AGENTS.md — AI Agent Contributor Guide

This guide is for AI agents contributing to the Sprout codebase. It covers agent-specific context and conventions. For general contributor info (setup, code style, PR process, architecture), see CONTRIBUTING.md.


Ecosystem

Sprout spans five repos. This one (block/sprout) is the OSS source for the relay, desktop, mobile, and CLI. The others handle internal builds and deployment:

Repo Purpose
block/sprout OSS source — relay, desktop app, mobile app, CLI, agent harness
squareup/sprout-releases Buildkite pipeline producing Block-signed macOS + iOS builds with -block version suffix
squareup/sprout-oss CI pipeline building the relay Docker image and pushing to internal ECR
squareup/block-coder-tf-stacks Terraform + ArgoCD deploying the relay to the staging Kubernetes cluster
squareup/sprout-backend-blox Desktop backend provider script connecting Blox workstation agents to the relay
block/sprout (source)
  ├─► sprout-releases    (desktop + mobile builds → Artifactory, GitHub, Mobile Releases)
  ├─► sprout-oss         (relay Docker image → ECR)
  │     └─► block-coder-tf-stacks  (Helm chart → ArgoCD → staging cluster)
  └─── sprout-backend-blox         (Blox compute provider for Desktop agent launch)

See RELEASING.md for the desktop release flow across block/sprout and sprout-releases.


Repo Structure

crates/
  # Relay + core
  sprout-relay        # WebSocket relay server — main entry point; also hosts git + huddle audio
  sprout-core         # Core types, event verification, filter matching, kind registry
  sprout-db           # Postgres event store and data access layer
  sprout-auth         # Authentication and authorization
  sprout-pubsub       # Redis pub/sub fan-out, presence, typing indicators
  sprout-search       # Typesense-backed full-text search
  sprout-audit        # Hash-chain audit log
  sprout-media        # Blossom/S3 media storage
  # Agent surface
  sprout-mcp          # MCP server providing AI agent tools (being phased out in favor of the CLI)
  sprout-acp          # ACP harness bridging Sprout events to AI agents
  sprout-agent        # Minimal ACP-compliant agent (non-streaming, tool-calls-as-output)
  sprout-dev-mcp      # Developer MCP server — shell + file-edit tools
  sprout-persona      # Agent persona packs
  sprout-workflow     # YAML-as-code workflow engine (evalexpr conditions)
  # Clients + interop
  sprout-proxy        # Nostr client compatibility proxy (NIP-28)
  sprout-pair-relay   # Ephemeral sidecar relay for NIP-AB device pairing
  sprout-pairing-cli  # CLI for NIP-AB device pairing interop testing
  git-sign-nostr      # Sign git objects with a Nostr key
  git-credential-nostr # Git credential helper for Nostr-authed push/fetch
  # Tooling + shared
  sprout-cli          # Agent-first CLI
  sprout-sdk          # Typed Nostr event builders
  sprout-admin        # Operator CLI for relay administration
  sprout-test-client  # Integration test client and E2E test suite
  sprig               # All-in-one harness bundling ACP, agent, and dev MCP

desktop/              # Tauri 2 + React 19 desktop app
web/                  # Browser web client (repo browser, served by the relay)
mobile/               # Flutter mobile app
migrations/           # SQL migrations (auto-applied on relay startup)
scripts/              # Dev tooling
.env.example          # Config template — copy to .env before running

Getting Started

. ./bin/activate-hermit   # activate hermit toolchain (Rust, Node, etc.)
cp .env.example .env      # configure local environment
just setup                # install deps, run migrations
just relay                # start relay at ws://localhost:3000
just ci                   # run before any PR

See CONTRIBUTING.md for full setup details and dependency requirements.


Quality Gates

Run just ci before every PR — it runs fmt + clippy + desktop lint + unit tests + builds. Clippy passing does not mean fmt passes; run both.

Run just test for integration tests if you touched sprout-relay, sprout-db, or sprout-auth — these require a running Postgres and Redis.

Pre-commit hooks are installed automatically by just setup and auto-fix formatting via stage_fixed. Pre-commit runs fix variants in parallel (Rust fmt, Tauri Rust fmt, desktop biome fix, web biome fix, mobile dart format). Auto-fixable issues are fixed and re-staged; unfixable lint issues block the commit. Pre-push hooks run clippy (workspace + Tauri) and fast unit tests in parallel (Rust, desktop JS, Tauri Rust, mobile Flutter) — no overlap with pre-commit. Builds are CI-only. Run just fix-all to auto-fix all formatting in one shot. Run just ci for the full local gate. Run just hooks to re-install hooks after env changes.

Additional rules:

  • No unsafe code
  • Do not introduce new unwrap() or expect() in production paths — use ? and proper error types
  • New public API must have doc comments

Key Patterns

Dual API surface: Sprout exposes both a REST API and a NIP-29 WebSocket relay. Both paths converge on shared DB functions in sprout-db. When adding a feature, implement the shared DB logic first, then wire up both surfaces.

Prefer Nostr events over new REST endpoints: For new feature work, model the operation as a Nostr event (new kind in sprout-core/src/kind.rs, handler in sprout-relay) rather than adding a new REST endpoint. REST is reserved for things that genuinely need an HTTP-only surface: media upload/download (Blossom), OAuth callbacks, health checks, and the existing read endpoints that proxy DB queries. Two helpful endpoints already exist and rarely need to be duplicated:

  • POST /events — submit any signed event (same path the WebSocket uses).
  • POST /query — Nostr REQ filters over HTTP. NIP-50 search filters are routed to sprout-search (Typesense-backed) automatically.
  • POST /count — Nostr COUNT filters over HTTP.

If you find yourself reaching for a new REST endpoint, first check whether an event kind would do the job — it usually will, and you get realtime fan-out, NIP-29 scoping, and the existing auth pipeline for free.

Reference https://github.com/nostr-protocol/nips

Event kinds: All event kind integers are defined in sprout-core/src/kind.rs. New features get new kind integers — add them here first, then implement handling in the relay.

Channel scoping: Channels use h tags (NIP-29 group tag), not e tags. Filters and queries must scope to h tags when operating within a channel.

Agent-facing operations go in sprout-cli, not sprout-mcp: sprout-mcp is being phased out. New agent-facing features belong in sprout-cli — add a subcommand there first, then wire the REST/WebSocket call in client.rs. Do not add new tools to sprout-mcp unless specifically required for backward compatibility. sprout-dev-mcp (shell + file tools for sprout-agent) is separate and not being phased out.

Workflow conditions: sprout-workflow uses evalexpr for condition evaluation. Keep expressions simple and testable.

Thread counters: reply_count and descendant_count are materialized on thread root events. Any code that inserts replies must update these counters — check existing reply handlers for the pattern.


Agent CLI (sprout-cli)

sprout is the agent-first CLI replacing sprout-mcp. Auth env vars (SPROUT_RELAY_URL, SPROUT_PRIVATE_KEY, SPROUT_AUTH_TAG) are auto-injected by the ACP harness into managed agent subprocesses.

All reads return sig-stripped JSON arrays; all writes return {event_id, accepted, message}; creates add the entity ID. Exit codes: 0=ok, 1=input error, 2=network/relay, 3=auth, 4=other, 5=write conflict (NIP-33 LWW).

--format compact is a global flag — it goes before the subcommand: sprout --format compact channels list, NOT sprout channels list --format compact.

See crates/sprout-cli/TESTING.md for the full live-testing runbook.


Testing

just test-unit    # unit tests, no infrastructure needed
just test         # full integration suite (requires Postgres + Redis)

E2E tests live in crates/sprout-test-client/tests/:

  • e2e_relay.rs — WebSocket relay protocol
  • e2e_rest_api.rs — REST endpoint coverage
  • e2e_mcp.rs — MCP tool surface
  • e2e_tokens.rs — auth token flows
  • e2e_workflows.rs — workflow engine
  • e2e_media.rs — media upload/download (Blossom)
  • e2e_media_extended.rs — extended media scenarios
  • e2e_nostr_interop.rs — Nostr interop (NIP-50 search, NIP-10 threads, NIP-17 gift wraps)

Desktop E2E: cd desktop && pnpm exec playwright test

See TESTING.md for the full multi-agent E2E guide.

Desktop Screenshots (Playwright)

The desktop app is a Tauri app that cannot render in a plain browser without the E2E mock bridge. A standalone screenshot helper at desktop/tests/helpers/screenshot.mjs wraps the same mock bridge setup the E2E tests use (addInitScript + window.__SPROUT_E2E__) into a CLI tool.

just desktop-build   # build the frontend first
cd desktop
node tests/helpers/screenshot.mjs --name home
node tests/helpers/screenshot.mjs --name channel --route /channels/general
node tests/helpers/screenshot.mjs --name search --click open-search
node tests/helpers/screenshot.mjs --name settings --click open-settings

Options: --name (filename), --route (client route), --click (data-testid or CSS selector), --wait (ms, default 2000), --viewport (WxH, default 1280x720), --outdir (default test-results/screenshots). Screenshots are saved as PNGs and the path is printed to stdout.

The Playwright MCP browser (@playwright/mcp) is also configured but cannot drive the desktop app directly because it evaluates JS after page load — too late for the mock bridge. Use the MCP browser for non-Tauri pages only.


Common Gotchas

  1. Kind 39000 for channel metadata, not 41 — kind 41 is NIP-01 (unused). All kinds defined in sprout-core/src/kind.rs.
  2. Relay queries must specify kinds — omitting kinds triggers the p-gate (403). Always include explicit kind filters.
  3. messages search must include --kinds — an open-ended search (no kinds) hits the relay p-gate and returns 403. Pass at least --kinds 9,45001,45003 to scope the query.
  4. Worktrees: cd in the same command — shell CWD doesn't persist between tool calls. Use cd /path && cargo build as one command.
  5. Desktop crate excluded from root workspacecargo test at repo root does NOT run desktop tests. Use cargo test --manifest-path desktop/src-tauri/Cargo.toml explicitly.
  6. Desktop Tauri fmt fails in worktrees and blocks commits — the pre-commit hook runs just desktop-tauri-fmt, which fails in git worktrees because cargo fmt resolves workspace paths relative to the worktree root. Run just desktop-tauri-fmt from the main checkout to apply the fix, then re-stage and commit. CI is unaffected.

Desktop App

The desktop app is Tauri 2 + React 19 + Vite + Tailwind CSS. Features are organized under desktop/src/features/. Biome handles linting and formatting.

just desktop-dev   # web-only dev server (faster iteration)
just dev           # full Tauri app with native shell

Workspace Switching

The desktop app supports multiple workspaces (each backed by a different relay). Switching workspaces does not reload the page — it uses React key-based remounting. <AppReady key={workspaceKey} /> in App.tsx forces the entire workspace-scoped subtree to unmount and remount with fresh state.

Module-level singletons must be explicitly reset. React remounting only clears React state (useState, useRef, context). Module-level variables (Maps, class instances, cached promises) survive across remounts. Every workspace-scoped singleton needs a reset function wired into resetWorkspaceState() in desktop/src/features/workspaces/useWorkspaceInit.ts.

Current singletons that are reset on workspace switch:

  • relayClient.disconnect() — WebSocket teardown + promise rejection
  • resetMediaCaches() — proxy port and relay origin caches
  • clearSearchHitEventCache() — search result event cache
  • clearAllDrafts() — message draft cache

If you add a new module-level cache, Map, or class instance that holds workspace-scoped data, you must add its reset to resetWorkspaceState(). Failure to do so causes data from the old workspace to leak into the new one.

Key files:

  • desktop/src/app/App.tsx — workspace key, init gate, remount boundary
  • desktop/src/features/workspaces/useWorkspaceInit.tsresetWorkspaceState(), applies config to Tauri backend
  • desktop/src/features/workspaces/useWorkspaces.tsxWorkspacesProvider context (shared state for App + AppShell)
  • desktop/src/main.tsx — provider hierarchy (QueryClientProvider > WorkspacesProvider > App)

Mobile App (Flutter)

The mobile app lives in mobile/ — a Flutter app using Riverpod + Hooks.

Architecture

  • State management: Riverpod + flutter_hooks (HookConsumerWidget)
  • Theme: Catppuccin Latte (light) / Macchiato (dark) — matches desktop
  • Features: Isolated under lib/features/, shared code in lib/shared/
  • Nostr models: lib/shared/relay/nostr_models.dart — event kinds must stay in sync with desktop/src/shared/constants/kinds.ts

Rules

  • NEVER use StatefulWidget — always use HookConsumerWidget or ConsumerWidget with flutter_hooks for local state.
  • NEVER run flutter run, flutter build, flutter clean, or flutter upgrade — only flutter test, flutter analyze, and dart format are safe for agents to run.
  • Do NOT use print() — use debugPrint() or structured logging.
  • Prefer context.colors and context.textTheme (via theme extensions) over raw Theme.of(context) calls.
  • Keep widgets small and composable.
  • Feature modules must not import from other feature modules — only from shared/.
  • Use Grid tokens for spacing, Radii for border radius.

Quality Checks

cd mobile
dart format --output=none --set-exit-if-changed .
flutter analyze
flutter test

Or from repo root: just mobile-fmt (auto-fix), just mobile-check (lint + fmt check), just mobile-test (tests).

To run the app locally (starts Docker, relay, iOS simulator automatically):

just mobile-dev

Testing Conventions

  • Prefer widget tests over unit tests for UI components — test the whole widget tree, not individual methods.
  • Use ProviderScope(overrides: [...]) to inject fake notifiers.
  • Fake notifiers should extend the real notifier class and override build().
  • Use the WidgetHelpers.testable() wrapper for simple widget tests or build a custom ProviderScope + MaterialApp when you need specific overrides.

See Also

  • CONTRIBUTING.md — setup, code style, PR process, how to add event kinds / CLI subcommands / API endpoints
  • TESTING.md — multi-agent E2E test guide
  • ARCHITECTURE.md — system design and component relationships
  • RELEASING.md — release process: just release, auto-tag, internal builds
  • README.md — project overview and quick start