Skip to content

Latest commit

 

History

History
617 lines (482 loc) · 29.1 KB

File metadata and controls

617 lines (482 loc) · 29.1 KB

AGENTS.md — AI Agent Contributor Guide

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


Ecosystem

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

Repo Purpose
block/buzz 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/buzz (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 and CONTRIBUTING.md § Ecosystem for contributor access information.


Repo Structure

crates/
  # Relay + core
  buzz-relay          # WebSocket relay server — main entry point; also hosts git + huddle audio
  buzz-core           # Core types, event verification, filter matching, kind registry
  buzz-db             # Postgres event store and data access layer
  buzz-auth           # Authentication and authorization
  buzz-pubsub         # Redis pub/sub fan-out, presence, typing indicators
  buzz-search         # Postgres FTS full-text search
  buzz-audit          # Hash-chain audit log
  buzz-media          # Blossom/S3 media storage
  # Agent surface
  buzz-acp            # ACP harness bridging Buzz events to AI agents
  buzz-agent          # Minimal ACP-compliant agent (non-streaming, tool-calls-as-output)
  buzz-dev-mcp        # Developer MCP server — shell + file-edit tools
  buzz-persona        # Agent persona packs
  buzz-workflow       # YAML-as-code workflow engine (evalexpr conditions)
  # Clients + interop
  buzz-pair-relay     # Ephemeral sidecar relay for NIP-AB device pairing
  buzz-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
  buzz-cli            # Agent-first CLI
  buzz-sdk            # Typed Nostr event builders
  buzz-admin          # Operator CLI for relay administration
  buzz-ws-client      # Shared NIP-42 WebSocket client (connect, auth, publish)
  buzz-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.


Workspace and Disk Hygiene

  • Use ~/Developer/buzz as the single canonical clone.
  • Create issue branches as Git worktrees from that clone under ~/Developer/buzz-worktrees/. Never create another Buzz clone per issue.
  • Keep all Buzz work under ~/Developer, never ~/Documents.
  • Reuse the shared Cargo target at ~/Developer/.cargo-target. It is configured in ~/.cargo/config.toml and must apply across Buzz worktrees.
  • Run df -h ~ before build-heavy work. Do not start or continue builds that would leave the data volume below 15% free; clean derived artifacts first.
  • Once a branch is pushed and its PR is open, verify the worktree is clean and its HEAD is contained in the fork branch, run Cargo cleanup, remove the worktree with git worktree remove, and run git worktree prune --expire=now in the canonical clone.
  • Never remove a worktree with uncommitted source changes. Cargo build artifacts may be cleaned, but the checkout and source changes must be preserved.

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 buzz-relay, buzz-db, or buzz-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. Before agents run Git or hooks, activate the repo's Hermit environment (. ./bin/activate-hermit); do not rewrite hook commands to compensate for an unconfigured shell PATH.

Commit with git commit -s. The required DCO Check fails any PR with a commit missing a Signed-off-by trailer, and just hooks installs a commit-msg hook that adds it to commits you create locally (git rebase and git cherry-pick still need --signoff) — if you build commit commands programmatically, include -s every time. To repair a branch that already has unsigned commits: git rebase --signoff main, then force-push.

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

Nostr-first HTTP surface: Buzz's primary API is NIP-29 over WebSocket. The relay also exposes a narrow HTTP surface: NIP-11/NIP-05 metadata, POST /events, POST /query, POST /count, workflow webhooks at /hooks/{id}, Blossom media, git smart HTTP, git policy hooks, and health probes. These HTTP paths all preserve the same host-derived community boundary.

Prefer Nostr events over new HTTP endpoints: For new feature work, model the operation as a Nostr event (new kind in buzz-core/src/kind.rs, handler in buzz-relay) rather than adding endpoint-specific JSON APIs. HTTP is reserved for things that genuinely need an HTTP-only surface: media upload/download (Blossom), webhooks, git smart HTTP, NIP-11/NIP-05 metadata, health checks, and the generic Nostr bridge endpoints:

  • 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 buzz-search (Postgres FTS) automatically.
  • POST /count — Nostr COUNT filters over HTTP.

If you find yourself reaching for a new HTTP 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 buzz-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. This applies to events inside a channel. Addressable events that describe a channel carry its id in their d tag instead: kind:39000 (metadata), kind:39001, kind:39002 (membership). get_channels resolves a user's channels from the d tag of their kind:39002 events, not from h.

Agent-facing operations go in buzz-cli: New agent-facing features belong in buzz-cli — add a subcommand there first, then wire the REST/WebSocket call in client.rs. buzz-dev-mcp (shell + file tools for buzz-agent) is separate.

Workflow conditions: buzz-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 (buzz-cli)

buzz is the agent-first CLI. Auth env vars (BUZZ_RELAY_URL, BUZZ_PRIVATE_KEY, BUZZ_AUTH_TAG) are auto-injected by the ACP harness into managed agent subprocesses. In development, set BUZZ_PRIVATE_KEY and BUZZ_RELAY_URL in your environment manually.

Building the CLI

cargo build --release -p buzz-cli

Binary location: ./target/release/buzz. Add ./target/release to PATH or invoke with the full path.

Deep Links

buzz://message?channel=<uuid>&id=<hex> links reference a specific message thread. To read the linked thread:

buzz messages thread --channel <uuid> --event <hex> --format compact

Extract channel and id from the URL query parameters. The optional thread parameter (root event ID) can be ignored — messages thread resolves the full thread from the event ID alone.

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: buzz --format compact channels list, NOT buzz channels list --format compact.

See crates/buzz-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/buzz-test-client/tests/:

  • e2e_relay.rs — WebSocket relay protocol
  • 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.

PR Screenshots

Do NOT use buzz upload, the relay media endpoint, or any third-party image host for PR screenshots. Relay media URLs fail through GitHub's camo proxy. Always use scripts/post-screenshots.sh for PNGs before linking them from a PR body/comment. If you hand-edit PR markdown, run scripts/check-pr-image-urls.sh <markdown-file> first to catch relay URLs.

For mobile simulator screenshots, save the PNGs in a local directory and run ./scripts/post-screenshots.sh <PR-number> <png-dir> or use the third argument with a markdown template containing {{filename}} placeholders.

The desktop app requires the E2E mock bridge to render — it cannot run in a plain browser. Use just desktop-screenshot to capture screenshots (builds frontend, starts preview server, runs Playwright automatically):

just desktop-screenshot --name home
just desktop-screenshot --name channel --route /channels/general
just desktop-screenshot --name search --click open-search
just desktop-screenshot --name settings --click open-settings

Options: --name (filename), --route (client route), --active-channel (channel to view), --click (left-click data-testid or CSS selector), --right-click (right-click for context menus), --hover (hover before capture), --clip (crop region as x,y,w,h — e.g. 0,0,256,720 for sidebar only), --wait (ms, default 2000), --viewport (WxH, default 1280x720), --outdir (default test-results/screenshots), --messages (JSON file path). Output is a PNG path on stdout.

Use --messages to inject content into a channel before capture. The JSON file is an array of objects — channelName and content are required, all other fields are optional and passed through to __BUZZ_E2E_EMIT_MOCK_MESSAGE__:

[
  {
    "channelName": "random",
    "content": "Hey @tyler check this out",
    "pubkey": "953d...",
    "kind": 40002,
    "mentionPubkeys": ["deadbeef..."],
    "extraTags": [["broadcast", "1"], ["e", "some-root-id"]],
    "parentEventId": "abc123"
  }
]

Without --active-channel, all messages must target the same channel and the helper navigates to that channel (useful for showing message content). With --active-channel, messages can target multiple channels while the "camera" stays on the specified channel (useful for unread indicators, badges, etc.).

# Messages in the channel you're viewing (code blocks, formatting, etc.)
just desktop-screenshot --name code-blocks --messages /tmp/msgs.json

# Messages in OTHER channels to trigger unread state
just desktop-screenshot --name unread-dot \
  --active-channel general --messages /tmp/badge-msgs.json

# Cropped to sidebar only (256px wide)
just desktop-screenshot --name sidebar-unread \
  --active-channel general --messages /tmp/badge-msgs.json \
  --clip 0,0,256,720

# Context menu on an unread channel (wider crop to include popup)
just desktop-screenshot --name ctx-mark-read \
  --active-channel general --messages /tmp/badge-msgs.json \
  --right-click channel-random --clip 0,200,320,300

# Hover state (e.g. copy button reveal)
just desktop-screenshot --name copy-hover \
  --messages /tmp/code-msgs.json --hover "[data-testid='copy-code']"

Available mock channels: general, random, design, sales, engineering, agents, watercooler, announcements, alice-tyler, bob-tyler.

scripts/post-screenshots.sh hosts PNGs on a per-developer branch (agent-screenshots/<github-username>) and posts a PR comment with commit-SHA-based image URLs (immutable — safe from later overwrites):

./scripts/post-screenshots.sh 803 test-results/screenshots
./scripts/post-screenshots.sh 803 test-results/screenshots body.md  # custom body prepended

The body file supports {{filename}} placeholders (without .png) to inline images at specific positions. Images not referenced by any placeholder are appended at the end. Without placeholders, all images are appended (backward compatible).

### Unread dot
A message arrives in `#random`.

{{01-unread-dot}}

### Context menu
Right-click shows "Mark as read".

{{02-context-menu}}

Re-runs overwrite the image blobs on the agent-screenshots/<username> branch, but the script appends a new PR comment — it does not edit or delete the previous one. After reposting, delete the superseded comment so only the current set remains, otherwise reviewers still see the stale images:

# List screenshot comments to find the stale one's id
gh pr view <pr> --repo block/buzz --json comments \
  --jq '.comments[] | select(.body | test("pr-<pr>--")) | {id, url}'
gh api -X DELETE repos/block/buzz/issues/comments/<stale-comment-id>

Branch cleanup when fully done: git push origin --delete agent-screenshots/<username>.

Writing E2E Screenshot Specs

When screenshots need seeded state, live messages, or UI interaction before capture, write a Playwright spec instead of using just desktop-screenshot. Add specs to desktop/tests/e2e/ and register them in playwright.config.ts (smoke project testMatch). Every test calls installMockBridge(page) for mock Tauri IPC. Mock pubkey, channel names, and UUIDs live in e2eBridge.ts.

Always build with pnpm build:e2e, never pnpm run build. The mock Tauri bridge is compiled in only for --mode e2e (see installE2eBridgeIfConfigured in desktop/src/main.tsx). A plain pnpm run build strips it, so window.__TAURI_INTERNALS__ is never defined and every mock-mode spec fails with Cannot read properties of undefined (reading 'invoke') — the app renders "Community connection failed" instead of the UI under test. That looks exactly like a product bug rather than a build mistake, so it burns real time. pnpm test:e2e:smoke and pnpm test:e2e:integration run the right build for you; prefer them over a manual build plus playwright test.

Stale server: reuseExistingServer: true means a previous build's server serves old code. Kill port 4173 and re-run pnpm build:e2e before re-running tests after code changes.

addInitScript before bridge: page.addInitScript (localStorage seeding) must run BEFORE installMockBridge(page) — React reads state on mount, the bridge triggers mount.

Live messages: Call waitForMockLiveSubscription(page, channelName) before __BUZZ_E2E_EMIT_MOCK_MESSAGE__ — messages are silently dropped without a subscription. Navigate to the channel first (triggers subscription), then away (so unread indicators appear), then inject.

Animation timing: Radix components animate in via CSS. toBeVisible() resolves mid-animation — wait for completion before screenshotting. Use the shared helper (mandatory before any page.screenshot() or locator.screenshot() in specs):

import { waitForAnimations } from "../helpers/animations";

// ... after the element is visible but before capturing:
await waitForAnimations(page);
await page.screenshot({ path: "...", clip: { ... } });

The just desktop-screenshot path (screenshot.mjs) calls waitForAnimations automatically — no manual step needed there.

For per-element waits (rare — prefer the page-level helper above):

await menuItem.evaluate((el) =>
  Promise.all(
    el.closest("[data-state]")?.getAnimations().map((a) => a.finished) ?? [],
  ),
);

Cropping: Use clip — full-window (1280x720) screenshots are unreadable for sidebar features. Sidebar = 256px; context menus ~450px.

Distinct states — verify before posting: when one view renders many elements at once (e.g. all team cards in a single grid), an unscoped full-page page.screenshot() captures the same pixels for every shot, so multiple PNGs come out byte-identical. Scope each shot to its subject with locator.screenshot() (full-page clip only when an overlay like an open dropdown must be included). Then gate on hash distinctness before posting:

shasum -a 256 test-results/<dir>/*.png   # every hash must be unique

Identical hashes mean two shots captured the same state — fix the spec, do not post. This catches the most common screenshot regression.

general has pre-seeded messages making hasUnread always true. Use engineering for "muted + no unread" visual states.

PR comments: Use a body template (3rd arg to post-screenshots.sh) with {{filename}} placeholders. Each screenshot gets a ### heading + one-line description. See PR #803.


Common Gotchas

  1. Kind 39000 for channel metadata, not 41 — kind 41 is NIP-01 (unused). All kinds defined in buzz-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.
  7. React render perf: React.memo is all-or-nothing — it only skips a re-render when every prop is reference-stable; one unstable prop (inline arrow/JSX, or a hook returning a fresh {}/[]/Map each render) defeats it. Two repeat offenders: (a) React Query results (useMutation/useQuery) are a new object each render — depend on the stable method (mutation.mutateAsync), not the object; (b) derived Map/array state that recomputes on a version bump — wrap in a content-equality ref cache (shared/hooks/useStableReference.ts). When chasing interaction lag, measure with DevTools closed and no perf probes (an open Web Inspector + per-keystroke console.log inflate the numbers), and isolate by removing one suspect at a time rather than guessing.

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

Text sizing & zoom (use rem, never px)

The desktop app implements Cmd +/- zoom by scaling the root <html> font-size (desktop/src/app/useWebviewZoomShortcuts.ts) and pinning the native webview zoom. Only rem-based text scales with zoom — hardcoded px text sizes are frozen.

So for any readable text, reach for rem-based Tailwind tokens, never arbitrary px:

  • ✅ Stock rem tokens (text-base, text-sm, text-xs, …). Chat body/author text === text-base (16px) — chat is the app's base type size, and the surrounding timeline elements (timestamps, system rows, code, reactions) are deliberate steps on that same stock ramp.
  • ✅ The text-2xs (0.6875rem / 11px) and text-3xs (0.5rem / 8px) meta-text tokens (in desktop/tailwind.config.js under theme.extend.fontSize) for the sub-text-xs ramp — timestamps, count badges, tracking labels, tiny glyphs. These replaced the dozens of arbitrary text-[…rem] literals that had drifted apart pixel-by-pixel; keep meta text on these two tokens, not new arbitrary values.
  • text-[15px], text-[13px], CSS font-size: 15px — px froze against zoom and caused the message-timeline regression (PR #891).
  • ❌ Arbitrary rem literals too: text-[0.6875rem], text-[0.9rem], etc. They zoom fine but re-fragment the scale we consolidated. Use a named token.

Prefer stock tokens — they're rem and zoom-safe. Only if a design genuinely needs a size the stock/2xs/3xs scale can't express should you add a rem-based token (in desktop/tailwind.config.js under theme.extend.fontSize) rather than an arbitrary literal. A CI guard (pnpm check:px-text, in desktop/scripts/check-px-text.mjs) scans all of desktop/src and fails on any new arbitrary text-size literal — px or rem/em. Genuinely decorative glyphs (e.g. the text-[6rem] avatar emoji) are allowlisted by path:line in that script.

Community Switching

The desktop app supports multiple communities (each backed by a different relay). Switching communities does not reload the page — it uses React key-based remounting. <AppReady key={communityKey} /> in App.tsx forces the entire community-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 community-scoped singleton needs a reset function wired into resetCommunityState() in desktop/src/features/communities/useCommunityInit.ts.

Current singletons that are reset on relay boundary changes (same-relay reconnects preserve pending avatar verification work):

  • relayClient.disconnect() — WebSocket teardown + promise rejection
  • resetRateLimitGate() — clears any active rate-limit window from the old relay
  • clearAllDrafts() — message draft cache
  • resetAgentObserverStore() — agent observer relay store
  • resetActiveAgentTurnsStore() — active agent turn timers
  • resetAgentWorkingSignal() — agent working indicator signal
  • resetAvatarProfileSync() — pending verified-avatar profile writes
  • resetAvatarPresentations() — avatar probes, previews, and Retry toasts
  • resetSidebarRelayConnectionCardState() — sidebar relay card dismiss state
  • resetMediaCaches() — proxy port and relay origin caches
  • resetVideoPlayerState() — video player singleton
  • resetRenderScopedReactionHydration() — reaction hydration cache
  • clearSearchHitEventCache() — search result event cache
  • clearMarkdownNodeCache() — markdown parse-node cache

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

Key files:

  • desktop/src/app/App.tsx — community key, init gate, remount boundary
  • desktop/src/features/communities/useCommunityInit.tsresetCommunityState(), applies config to Tauri backend
  • desktop/src/main.tsx — provider hierarchy (QueryClientProvider > 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 — favor Riverpod for state and 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. One public widget per file; push private sub-widgets (_Foo) into sibling part files under a <page>/ folder rather than growing the page file. Hard ceiling: 1000 lines/file, enforced by mobile/scripts/check-file-sizes.mjs via just mobile-check (runs in just check + pre-push, mirroring desktop/web). If the guard trips, split the file — never bump the limit or add an override to slip under it.
  • 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

When run from a git worktree, just mobile-dev (and just mobile-build-android) give the debug build a per-worktree app identifier (keyed to the worktree directory name) and a branch-labelled app name via scripts/mobile-worktree-overrides.sh, so builds from multiple worktrees install side by side. Release builds are unaffected. just mobile-clean removes stale worktree-suffixed installs from simulators/emulators. See mobile/README.md for direct Xcode / Android Studio usage.

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 / HTTP endpoints
  • TESTING.md — multi-agent E2E test guide
  • ARCHITECTURE.md — system design and component relationships
  • RELEASING.md — release process: release-desktop, release-relay, scripts/mobile-release.sh, candidate tags, internal builds
  • README.md — project overview and quick start