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
unsafecode - Do not introduce new
unwrap()orexpect()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-50searchfilters are routed tosprout-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 protocole2e_rest_api.rs— REST endpoint coveragee2e_mcp.rs— MCP tool surfacee2e_tokens.rs— auth token flowse2e_workflows.rs— workflow enginee2e_media.rs— media upload/download (Blossom)e2e_media_extended.rs— extended media scenariose2e_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
- Kind
39000for channel metadata, not41— kind 41 is NIP-01 (unused). All kinds defined insprout-core/src/kind.rs. - Relay queries must specify
kinds— omittingkindstriggers the p-gate (403). Always include explicit kind filters. messages searchmust include--kinds— an open-ended search (no kinds) hits the relay p-gate and returns 403. Pass at least--kinds 9,45001,45003to scope the query.- Worktrees:
cdin the same command — shell CWD doesn't persist between tool calls. Usecd /path && cargo buildas one command. - Desktop crate excluded from root workspace —
cargo testat repo root does NOT run desktop tests. Usecargo test --manifest-path desktop/src-tauri/Cargo.tomlexplicitly. - Desktop Tauri fmt fails in worktrees and blocks commits — the pre-commit hook runs
just desktop-tauri-fmt, which fails in git worktrees becausecargo fmtresolves workspace paths relative to the worktree root. Runjust desktop-tauri-fmtfrom 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 rejectionresetMediaCaches()— proxy port and relay origin cachesclearSearchHitEventCache()— search result event cacheclearAllDrafts()— 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 boundarydesktop/src/features/workspaces/useWorkspaceInit.ts—resetWorkspaceState(), applies config to Tauri backenddesktop/src/features/workspaces/useWorkspaces.tsx—WorkspacesProvidercontext (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 inlib/shared/ - Nostr models:
lib/shared/relay/nostr_models.dart— event kinds must stay in sync withdesktop/src/shared/constants/kinds.ts
Rules
- NEVER use
StatefulWidget— always useHookConsumerWidgetorConsumerWidgetwithflutter_hooksfor local state. - NEVER run
flutter run,flutter build,flutter clean, orflutter upgrade— onlyflutter test,flutter analyze, anddart formatare safe for agents to run. - Do NOT use
print()— usedebugPrint()or structured logging. - Prefer
context.colorsandcontext.textTheme(via theme extensions) over rawTheme.of(context)calls. - Keep widgets small and composable.
- Feature modules must not import from other feature modules — only from
shared/. - Use
Gridtokens for spacing,Radiifor 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 customProviderScope+MaterialAppwhen 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