Files
buzz/Justfile
ravarora2 6b1e45d2f6 Isolate S3 storage metrics from the relay (#7543)
## Why

**This PR moves the S3 storage scan out of the serving relay and into a
separate batch job.** The job saves a complete result in PostgreSQL. The
relay reads that result, exports the existing storage totals, and
reports the snapshot's freshness and database-read health.

```text
Today:         relay -> scan S3 -> calculate totals -> Datadog metrics
After rollout: CronJob -> scan S3 -> PostgreSQL snapshot -> relay -> Datadog metrics
```

The code in this PR is the new pipeline. It adds the worker, database
snapshot, relay read mode, and Helm CronJob template. It does **not**
enable the pipeline in production. The chart keeps the CronJob disabled,
and the relay keeps its current inline mode by default.
[squareup/builderbot-platform-core-infrastructure#244](https://github.com/squareup/builderbot-platform-core-infrastructure/pull/244)
will set the production limits, enable the job, and switch the relay to
the saved snapshot after this PR merges.

The relay currently lists the media bucket once an hour and calculates
storage totals inside the serving process. The production bucket now
exceeds the relay's one million object cap, so each sweep fails and the
last good per-community values remain stale. The Datadog dashboard can
no longer show current storage use by community.

Raising that cap inside the serving relay would move a larger,
memory-heavy scan into the process that owns live WebSocket connections.
The scan retains state for unique blob hashes and community bindings, so
its memory use grows with the bucket. A large scan could raise relay
memory use, trigger restarts, and drop user connections.

The goal is to restore current metrics without putting a
production-scale bucket scan in the relay's failure domain. No media
objects move or change. Only the process that calculates the totals and
the handoff to the relay change.

## What

- Add `buzz-admin storage-snapshot`, a run-once worker that scans S3 and
saves one complete accounting snapshot in PostgreSQL.
- Add `external` relay mode to read the saved snapshot. Keep the current
`inline` mode as the default, and add `off` as a fail-closed option.
- Add an opt-in Helm CronJob for the worker. The 10 million object cap
exists only on this Job.
- Keep the current fleet and per-community gauges, and add snapshot age,
generation duration, configured cap, cap utilization, and database-load
health gauges.
- Add an additive migration for the latest complete snapshot and its run
metadata.

## Overall plan

1. Merge this PR to add the worker, database handoff, relay modes, and
disabled-by-default chart support. This step does not change production
behavior.
2. Publish the relay image and Helm chart `0.1.9` from `main`.
3. Update
[squareup/builderbot-platform-core-infrastructure#244](https://github.com/squareup/builderbot-platform-core-infrastructure/pull/244)
to pin those final artifacts and rerender its manifests.
4. Use the infrastructure PR to deploy the CronJob with a 10 million
object cap, a one-hour deadline, and a 20 GiB memory limit.
5. Start one manual Job during a staffed window and verify its snapshot,
duration, memory use, relay stability, and Datadog output.
6. Keep the daily schedule and `external` relay mode only after that
gate passes, then add alerts for stale snapshots, snapshot-load failure,
failed jobs, cap pressure, duration, and worker memory.

The code and infrastructure remain separate so this PR can ship the
mechanism without silently enabling a production-scale scan. The
infrastructure PR owns the production settings and rollout gate.

## How

### Complete snapshot handoff

The worker takes a deployment-wide PostgreSQL advisory lock before it
lists S3. This prevents scheduled and manual workers from overlapping,
even if Kubernetes starts both.

The final database replacement runs through that same lock-owning
session. If the session dies during the scan, publication fails instead
of letting the stale worker overwrite its successor. A worker that no
longer owns the lock cannot publish through another pooled connection.

The worker reads S3 in pages of 1,000 objects and folds each page into
physical totals, logical per-community usage, and anomaly counts. It
stops before folding a page that would exceed `--max-objects` and emits
structured progress every 100,000 objects.

The worker replaces the singleton database row only after the full scan
succeeds. A listing error, malformed page, cap breach, serialization
error, lost lock session, or database error cannot publish a partial
snapshot. The last completed snapshot stays available. Each stored
result includes its completion time, duration, cap, and image or source
revision.

PostgreSQL is the handoff between the batch worker and the relay. The
worker writes one complete deployment-wide snapshot, and the relay reads
that snapshot on its normal leader metrics tick. No new network service
or worker endpoint is required.

### Relay behavior

`BUZZ_STORAGE_METRICS=inline` keeps the existing in-process sweep and
its one million object default. `external` loads the newest completed
worker snapshot on each leader metrics tick. `off` emits no
storage-family metrics. An unknown value fails closed to `off`.

External mode republishes the existing fleet, per-community, and anomaly
metric names, so those current queries remain valid. Snapshot handoff
health uses a separate family: `buzz_storage_snapshot_age_seconds`,
`buzz_storage_snapshot_duration_seconds`,
`buzz_storage_snapshot_max_objects`,
`buzz_storage_snapshot_cap_utilization`, and
`buzz_storage_snapshot_load_ok`.

The `buzz_storage_sweep_*` gauges remain inline-only because they
describe a scan attempted by the relay process. External mode does not
synthesize `sweep_ok=1` from the last successful database row: an old
row proves only that an earlier worker completed. Kubernetes CronJob/Job
status is authoritative for the latest worker result, including non-zero
exit, deadline expiry, and OOM termination.

The relay keeps the last complete snapshot available when a worker run
fails. Snapshot age shows that no replacement arrived on schedule, while
`buzz_storage_snapshot_load_ok` distinguishes staleness from a missing
row, database read failure, or invalid payload. If a community
disappears, changes host, or leaves the configured emission scope, the
relay zeroes the old labeled series instead of leaving stale values
behind.

Per-community bytes are logical referenced media usage. If two
communities reference the same blob, each community receives the blob's
bytes. Fleet physical totals still count each S3 object once. This PR
preserves those existing metric semantics.

### Deployment boundary

The CronJob is disabled by default. When enabled, it uses
`concurrencyPolicy: Forbid`, no retry, a configurable active deadline,
bounded history, explicit CPU and memory resources, a read-only root
filesystem, and no Istio sidecar by default.

The worker receives only `DATABASE_URL`, S3 settings, optional S3
credential references, and the code revision. It does not receive the
relay private key, Git hook HMAC secret, Redis URL, or the full relay
secret as environment variables. Operators may reuse the Buzz service
account or select a dedicated one.

### Non-goals

- This PR does not enable the worker or `external` mode in production.
- This PR does not change media uploads, object ownership, retention, or
deletion.
- This PR does not change the names or meaning of the existing fleet,
per-community, or anomaly storage gauges.
- This PR does not add a lease service or allow multiple workers to
publish concurrently.

## Risk

The application and chart defaults do not enable the new worker or raise
the serving relay's cap. The database change is an additive
deployment-global singleton table. The relay's default remains `inline`,
so merging this PR alone does not change the active metrics path.

The main rollout risk is worker memory use because the fold retains
state for distinct blob hashes and community bindings. The first
production-sized run must confirm peak memory and duration before the
schedule is trusted. The 10 million cap and one-hour deadline bound the
first deployment, but they do not replace measurement.

External mode also needs stale-snapshot, snapshot-load, and job-failure
monitors because the relay intentionally keeps publishing the last
complete result after a failed job or read error. This behavior avoids
gaps and partial data, while snapshot age and load health tell operators
when the result or PostgreSQL handoff needs attention. Job status
remains necessary for immediate failures and termination reasons that a
killed process cannot record itself.

## Testing

No production S3 scan was run from this PR.

Automated regressions verify that external mode keeps stale last-good
totals visible without emitting inline attempt-health gauges, reports
database-load failure while retaining the cache, and leaves inline
telemetry unchanged. Mutation checks also prove those tests fail if
external mode is routed through inline emission or a failed load is
reported as healthy.

After deployment, create one manual Job from the CronJob during a
staffed window. Require a completed Job, exactly one new snapshot, fewer
objects than the configured cap, acceptable duration and peak memory, no
relay restart regression, restored Datadog points, and no OpenMetrics
sample truncation.

## Next steps

1. Complete review and merge this PR.
2. Publish chart `0.1.9` and the final main-branch relay image.
3. Replace the provisional pins in
[squareup/builderbot-platform-core-infrastructure#244](https://github.com/squareup/builderbot-platform-core-infrastructure/pull/244),
rerender, and require clean CI.
4. Run the staffed manual Job gate and record object count, peak memory,
duration, snapshot revision, and relay stability.
5. Confirm the relay reads the new snapshot in `external` mode and the
existing `block.buzz_relay.*` storage series update again.
6. Enable the daily 03:17 UTC schedule and add dashboard panels and
monitors for snapshot age, snapshot-load health, Job failures, cap
pressure, duration, and worker memory.

If production must roll back to an older relay image, set
`BUZZ_STORAGE_METRICS=off` first. Older images do not understand
`external` and would resume the failing inline scan.

## Bigger picture

This is the application half of the storage-metrics repair. The paired
infrastructure PR owns the production resource limits, schedule,
artifact pins, secret boundary, and monitored rollout. Keeping those
changes separate lets this PR remain safe by default while still
providing a complete path to current per-community S3 totals.

Related issue: N/A. No matching issue or pull request exists in
`block/buzz`.

Generated with Codex

---------

Signed-off-by: Ravneet Arora <rarora@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-17 10:34:31 -04:00

1195 lines
55 KiB
Makefile

# Buzz — development task runner
set dotenv-load := true
desktop_dir := "desktop"
desktop_tauri_manifest := "desktop/src-tauri/Cargo.toml"
web_dir := "web"
# Opt-in mesh-llm. Off by default so `just dev`/`just staging`/`just production`
# skip ~420 extra crates + the llama.cpp native runtime build and stay fast to
# iterate on. Turn on to test mesh compute features: `just mesh=1 dev` /
# `just mesh=1 staging` / `just mesh=1 production`.
mesh := ""
# Reset only the current standalone desktop instance before launch.
# Usage: `just fresh=1 desktop-standalone`.
fresh := ""
# List all available tasks
default:
@just --list
# ─── Dev Environment ─────────────────────────────────────────────────────────
# Install required dev tools via Hermit and create .env (safe to re-run)
bootstrap:
#!/usr/bin/env bash
set -euo pipefail
export PATH="{{justfile_directory()}}/bin:$PATH"
# Hermit's bin/ symlinks auto-download pinned tool versions on first use.
# Running each tool once triggers the download if not already cached.
echo "Ensuring toolchain via Hermit..."
cargo --version &
node --version &
pnpm --version &
wait
if ! command -v docker &>/dev/null; then
echo "Error: Docker is required but not installed."
echo "Install it from https://docs.docker.com/get-docker/"
exit 1
fi
if [[ ! -f .env ]]; then
cp .env.example .env
echo "Created .env from .env.example — review it before running just dev."
fi
./scripts/ensure-local-relay-key.sh .env
# Start Docker services, run migrations, install desktop deps
setup: bootstrap
./scripts/dev-setup.sh
# Install git hooks via lefthook (dispatches from the shared .git/hooks dir so all
# linked worktrees inherit the same hooks without a worktree-relative .hooks path)
hooks:
#!/usr/bin/env bash
set -euo pipefail
# Use the Hermit-pinned lefthook (bin/lefthook self-downloads on first use):
# works with no pre-installed lefthook and guarantees the pinned version
# rather than whatever happens to be on PATH.
export PATH="{{justfile_directory()}}/bin:$PATH"
# --path-format=absolute guarantees an absolute path from every invocation context:
# without it, --git-common-dir returns ".git" from the main checkout and a
# relative hooksPath would break linked-worktree dispatch just like .hooks did.
HOOKS_DIR="$(git rev-parse --path-format=absolute --git-common-dir)/hooks"
git config --local core.hooksPath "$HOOKS_DIR"
lefthook install --force
# Wipe development state and recreate a clean environment. Installed Buzz is preserved.
[confirm("This will DELETE all development data and preserve installed Buzz. Continue? (y/N)")]
reset:
./scripts/dev-reset.sh --yes
# Stop all dev services (keep data)
down:
docker compose down
# Show dev service status
ps:
docker compose ps
# Tail all service logs
logs *ARGS:
docker compose logs -f {{ARGS}}
# ─── Build & Check ───────────────────────────────────────────────────────────
# Build the Rust workspace
build:
cargo build --workspace
# Build the Rust workspace in release mode
build-release:
cargo build --workspace --release
# Run repo lint, formatting, and repository policy checks
check: fmt-check clippy desktop-check desktop-tauri-fmt-check desktop-tauri-clippy web-check mobile-check security-review-check file-size-check
# Validate the trusted security-review workflow support and renderer contract.
security-review-check:
node --check .github/scripts/codex-security-review.js
node --test .github/scripts/codex-security-review.test.js
actionlint .github/workflows/codex-security-review.yml
# Run the repository-wide differential file-size ratchet and its policy tests.
# The ratchet inspects only files changed from the merge base, so this stays
# cheap enough to run unconditionally without duplicating path filters.
file-size-check:
node --test scripts/check-file-sizes-core.test.mjs
node desktop/scripts/check-file-sizes.mjs
node web/scripts/check-file-sizes.mjs
node mobile/scripts/check-file-sizes.mjs
# Format all Rust code
fmt:
cargo fmt --all
# Check formatting without modifying files
fmt-check:
cargo fmt --all -- --check
# Run clippy with warnings as errors
clippy:
cargo clippy --workspace --all-targets -- -D warnings
# Install JS dependencies (pnpm workspace — installs all packages from root)
desktop-install:
pnpm install
# Install JS dependencies reproducibly for CI (pnpm workspace)
desktop-install-ci:
pnpm install --frozen-lockfile
# Run desktop lint and format checks
desktop-check:
cd {{desktop_dir}} && pnpm check
# Fix desktop lint and format issues
desktop-fix:
cd {{desktop_dir}} && pnpm exec biome check --write .
# Run desktop TS helper unit tests
desktop-test:
cd {{desktop_dir}} && pnpm test
# Run desktop TypeScript checks
desktop-typecheck:
cd {{desktop_dir}} && pnpm typecheck
# Build desktop frontend assets
desktop-build:
cd {{desktop_dir}} && pnpm build
# Format desktop Tauri Rust code
desktop-tauri-fmt:
cargo fmt --manifest-path {{desktop_tauri_manifest}} --all
# Check desktop Tauri Rust formatting
desktop-tauri-fmt-check:
cargo fmt --manifest-path {{desktop_tauri_manifest}} --all -- --check
# Format all code (Rust + Tauri Rust + Dart)
fmt-all: fmt desktop-tauri-fmt mobile-fmt
# Fix all formatting and lint issues
fix-all: fmt desktop-tauri-fmt desktop-fix web-fix mobile-fix
# Ensure sidecar placeholder binaries exist (Tauri validates externalBin at compile time)
# Sidecar binary list must stay in sync with desktop-release-build below.
_ensure-sidecar-stubs:
#!/usr/bin/env bash
set -euo pipefail
TARGET=$(rustc -vV | sed -n 's|host: ||p')
mkdir -p desktop/src-tauri/binaries
SIDECARS=(buzz-acp buzz-agent buzz-dev-mcp git-credential-nostr buzz)
if [[ "$TARGET" != *windows* ]]; then
SIDECARS+=(buzz-backend-kubernetes)
fi
for bin in "${SIDECARS[@]}"; do
touch "desktop/src-tauri/binaries/${bin}-${TARGET}"
done
# Ensure Docker dev services (Postgres, Redis, etc.) are running and healthy
_ensure-services:
#!/usr/bin/env bash
set -euo pipefail
pg=$(docker inspect --format '{{"{{"}}.State.Health.Status{{"}}"}}' buzz-postgres 2>/dev/null || echo "not_found")
redis=$(docker inspect --format '{{"{{"}}.State.Health.Status{{"}}"}}' buzz-redis 2>/dev/null || echo "not_found")
if [[ "$pg" == "healthy" && "$redis" == "healthy" ]]; then
echo "Services already healthy"
exit 0
fi
echo "Starting services..."
docker compose up -d || true
echo -n "Waiting for services"
for i in $(seq 1 40); do
pg=$(docker inspect --format '{{"{{"}}.State.Health.Status{{"}}"}}' buzz-postgres 2>/dev/null || echo "not_found")
redis=$(docker inspect --format '{{"{{"}}.State.Health.Status{{"}}"}}' buzz-redis 2>/dev/null || echo "not_found")
if [[ "$pg" == "healthy" && "$redis" == "healthy" ]]; then
echo " ready"
exit 0
fi
echo -n "."
sleep 3
done
echo " timed out"
exit 1
# Apply database migrations and seed the local dev community if the dev database is running
_ensure-migrations: _ensure-services
cargo run -p buzz-admin -- migrate
./scripts/seed-local-community.sh
# Run clippy on the desktop Tauri Rust crate
# Features are additive, so a single invocation lints only one cfg graph.
# Both graphs ship (release-windows builds without mesh-llm), so lint both:
# the default graph covers the `#[cfg(not(feature = "mesh-llm"))]` arms and
# the feature-enabled graph covers the mesh code.
desktop-tauri-clippy: _ensure-sidecar-stubs
cargo clippy --manifest-path {{desktop_tauri_manifest}} --workspace --all-targets -- -D warnings
cargo clippy --manifest-path {{desktop_tauri_manifest}} --workspace --all-targets --features mesh-llm -- -D warnings
# Check the desktop Tauri Rust crate compiles
desktop-tauri-check: _ensure-sidecar-stubs
cargo check --manifest-path {{desktop_tauri_manifest}}
# Run desktop Tauri Rust unit tests
desktop-tauri-test: _ensure-sidecar-stubs
cd desktop/src-tauri && cargo test --workspace
# Run the native terminal latency gate explicitly on a known-idle host.
# This is intentionally excluded from shared CI: scheduler contention makes a
# wall-clock assertion flaky, and the release profile is the shipped shape.
desktop-terminal-performance-test:
cargo test --manifest-path desktop/src-tauri/crates/buzz-terminal/Cargo.toml --release --test latency g3_renderer_acquire_stays_within_frame_budget -- --ignored --exact --nocapture
# Verify compiled-flag behavior under both compile states (clean + capability set).
# Runs the auto-connect and owner-only access focused tests twice with
# independently supplied expected values; build.rs rerun-if-env-changed
# triggers recompilation.
desktop-tauri-test-compiled-flags: _ensure-sidecar-stubs
#!/usr/bin/env bash
set -euo pipefail
cd desktop/src-tauri
echo "=== Clean build (no flag) → expect false ==="
env -u BUZZ_BUILD_AUTO_CONNECT_DEFAULT_RELAY \
BUZZ_TEST_EXPECTED_AUTO_CONNECT_DEFAULT_RELAY=false \
cargo test compiled_flag_matches_expected -- --ignored --nocapture
env -u BUZZ_BUILD_AGENT_ACCESS_OWNER_ONLY \
BUZZ_TEST_EXPECTED_AGENT_ACCESS_OWNER_ONLY=false \
cargo test --lib
env -u BUZZ_BUILD_AGENT_ACCESS_OWNER_ONLY \
BUZZ_TEST_EXPECTED_AGENT_ACCESS_OWNER_ONLY=false \
cargo test compiled_policy_matches_expected -- --ignored --nocapture
echo "=== Internal build (flags set) → expect true ==="
BUZZ_BUILD_AUTO_CONNECT_DEFAULT_RELAY=1 \
BUZZ_TEST_EXPECTED_AUTO_CONNECT_DEFAULT_RELAY=true \
cargo test compiled_flag_matches_expected -- --ignored --nocapture
BUZZ_BUILD_AGENT_ACCESS_OWNER_ONLY=1 \
BUZZ_TEST_EXPECTED_AGENT_ACCESS_OWNER_ONLY=true \
cargo test --lib
BUZZ_BUILD_AGENT_ACCESS_OWNER_ONLY=1 \
BUZZ_TEST_EXPECTED_AGENT_ACCESS_OWNER_ONLY=true \
cargo test compiled_policy_matches_expected -- --ignored --nocapture
echo "=== Maximum accepted demo name reaches Rust build validation ==="
DEMO_CONFIG="$(node ../scripts/demo-build-config.mjs "$(printf 'x%.0s' {1..31})" /dev/null 1234567812345678)"
DEMO_SLUG="$(node -e 'console.log(JSON.parse(process.argv[1]).slug)' "$DEMO_CONFIG")"
BUZZ_BUILD_DEMO_SLUG="$DEMO_SLUG" \
BUZZ_TEST_EXPECTED_DEMO_SLUG="$DEMO_SLUG" \
cargo test compiled_demo_slug_matches_expected -- --ignored --nocapture
BUZZ_BUILD_DEMO_SLUG="$DEMO_SLUG" cargo test --workspace
if node ../scripts/demo-build-config.mjs "$(printf 'x%.0s' {1..32})" /dev/null 1234567812345678; then
echo "A 32-character demo name unexpectedly passed JavaScript validation" >&2
exit 1
fi
echo "Both compiled states and the accepted/rejected demo-name boundary verified."
# Build the full desktop Tauri app locally (unsigned, for testing)
# Sidecar binary list must stay in sync with _ensure-sidecar-stubs above.
# pnpm install is unconditional here: release builds must start from a clean dep tree.
desktop-release-build target="aarch64-apple-darwin":
#!/usr/bin/env bash
set -euo pipefail
TARGET={{target}}
mkdir -p desktop/src-tauri/binaries
touch "desktop/src-tauri/binaries/buzz-acp-$TARGET"
touch "desktop/src-tauri/binaries/buzz-agent-$TARGET"
if [[ "$TARGET" != *windows* ]]; then
touch "desktop/src-tauri/binaries/buzz-backend-kubernetes-$TARGET"
fi
touch "desktop/src-tauri/binaries/buzz-dev-mcp-$TARGET"
touch "desktop/src-tauri/binaries/git-credential-nostr-$TARGET"
touch "desktop/src-tauri/binaries/buzz-$TARGET"
pnpm install
cd {{desktop_dir}} && pnpm tauri build --features mesh-llm --target {{target}}
# Build an unsigned named macOS demo DMG with isolated app and runtime identities.
desktop-demo-build demo_name target="aarch64-apple-darwin":
#!/usr/bin/env bash
set -euo pipefail
TARGET={{target}}
[[ "$(uname -s)" == "Darwin" && "$TARGET" == *-apple-darwin ]] || { echo "Demo DMGs require a macOS Apple target" >&2; exit 2; }
CONFIG_PATH="$(mktemp "${TMPDIR:-/tmp}/buzz-demo-config.XXXXXX")"
trap 'rm -f "$CONFIG_PATH"' EXIT
DEMO_BUILD_ID="$(node -e 'console.log(require("node:crypto").randomBytes(8).toString("hex"))')"
DEMO_CONFIG="$(node desktop/scripts/demo-build-config.mjs {{quote(demo_name)}} "$CONFIG_PATH" "$DEMO_BUILD_ID")"
read_config() { node -e 'console.log(JSON.parse(process.argv[1])[process.argv[2]])' "$DEMO_CONFIG" "$1"; }
PRODUCT_NAME="$(read_config productName)"
DMG_VOLUME_NAME="$(read_config dmgVolumeName)"
DMG_FILE_STEM="$(read_config dmgFileStem)"
DEMO_SLUG="$(read_config slug)"
cargo build --release --target "$TARGET" \
-p buzz-acp -p buzz-agent -p buzz-backend-kubernetes -p buzz-dev-mcp \
-p git-credential-nostr -p buzz-cli
./scripts/bundle-sidecars.sh "$TARGET"
pnpm install
cd {{desktop_dir}}
BUZZ_BUILD_DEMO_SLUG="$DEMO_SLUG" pnpm tauri build --features mesh-llm --target "$TARGET" --bundles app --config "$CONFIG_PATH"
cd ..
VERSION="$(node -p "require('./desktop/package.json').version")"
DMG_ARCH="${TARGET%%-*}"; [[ "$DMG_ARCH" == "x86_64" ]] && DMG_ARCH=x64
APP_PATH="desktop/src-tauri/target/$TARGET/release/bundle/macos/$PRODUCT_NAME.app"
PLIST="$APP_PATH/Contents/Info.plist"
/usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName $PRODUCT_NAME" "$PLIST"
/usr/libexec/PlistBuddy -c "Set :CFBundleName $PRODUCT_NAME" "$PLIST"
codesign --force --deep --sign - "$APP_PATH"
VOL_NAME="$DMG_VOLUME_NAME" ./desktop/scripts/package-macos-dmg.sh "$APP_PATH" "desktop/src-tauri/target/$TARGET/release/bundle/dmg/${DMG_FILE_STEM}_${VERSION}_${DMG_ARCH}.dmg"
# Run desktop checks suitable for CI / pre-push
desktop-ci: desktop-check desktop-test desktop-tauri-fmt-check desktop-build desktop-tauri-check desktop-tauri-test
# Seed deterministic channel data for desktop Playwright tests
desktop-e2e-seed: _ensure-migrations
./scripts/setup-desktop-test-data.sh
# Run desktop browser smoke tests
desktop-e2e-smoke:
cd {{desktop_dir}} && pnpm test:e2e:smoke
# Run desktop relay-backed e2e tests
desktop-e2e-integration: _ensure-migrations
cd {{desktop_dir}} && pnpm test:e2e:integration
# Run the deterministic desktop correctness smoke against an isolated local relay
desktop-release-smoke:
./scripts/run-desktop-release-smoke.sh
# Run only the e2e specs changed vs origin/main (both projects) before pushing
desktop-e2e-pre-push: _ensure-migrations
git fetch origin main
cd {{desktop_dir}} && pnpm build:e2e && pnpm exec playwright test --only-changed=origin/main
# Run all checks suitable for CI / pre-push (no infra needed)
ci: check test-unit desktop-test desktop-build desktop-tauri-check desktop-tauri-test web-build mobile-test
# ─── Test ─────────────────────────────────────────────────────────────────────
# Run all tests (unit + integration)
test:
./scripts/run-tests.sh all
# Run unit tests only (no infra needed)
test-unit:
#!/usr/bin/env bash
set -euo pipefail
./scripts/test-ensure-local-relay-key.sh
if command -v cargo-nextest &>/dev/null; then
cargo nextest run -p buzz-core -p buzz-auth --lib
# buzz-auth NIP-FI verifier doctests. The sealed-authority
# `compile_fail` doctests prove the default-feature public API alone
# cannot forge the issuer→JWKS authority; nextest does not run
# doctests, hence this separate step. The verifier's regression suite
# lives in the in-crate `#[cfg(test)] mod tests`, so `--lib` above
# already runs it.
cargo test -p buzz-auth --doc
cargo nextest run -p buzz-voice --lib
cargo nextest run -p buzz-cli
# buzz-acp owns the relay-to-agent trust boundary. Run its tests here so
# forged relay events cannot regain a path into agent routing unnoticed.
cargo nextest run -p buzz-acp
# buzz-db migrator/lint tests: pure SQL-parsing unit tests (no infra).
# They guard the embedded-migrator invariant (the complete checked-in
# additive migration set; legacy cutover/backfill remains an operator
# script, not startup state) and the tenant-scoping lints. The
# Postgres-backed buzz-db tests are
# #[ignore]d, so --lib runs only the infra-free set. Without this gate a
# stray file in migrations/ or a broken lint ships green.
cargo nextest run -p buzz-db --lib
# Storage accounting crosses three crates whose focused regression
# suites are otherwise absent from the infra-free unit lane.
cargo nextest run -p buzz-media --lib \
-E 'test(=bucket_index::tests::bucket_snapshot_json_round_trip_preserves_community_keys)'
cargo nextest run -p buzz-admin \
-E 'test(=storage_snapshot_tests::failed_fold_never_invokes_snapshot_persistence)'
# Multi-tenant conformance gate (buzz-conformance): the independent
# replay checker + golden fixtures. No infra — pure in-process trace
# replay — so it belongs in the unit job. Run all targets (lib + the
# tests/replay_fixtures.rs integration test), not just --lib.
cargo nextest run -p buzz-conformance
# Gateway unit and black-box HTTP tests are infra-free. Postgres-backed
# contract/race tests run in the dedicated CI job below.
cargo nextest run -p buzz-push-gateway
# Kubernetes backend provider: the decision layers (state machine, GC
# planner, env precedence, naming, wire) are pure functions with a fake
# substrate, so they belong in the unit job. Enumerated explicitly
# because nothing in CI runs `cargo test --workspace` — workspace
# membership alone buys clippy/check, not a single executed test.
cargo nextest run -p buzz-backend-kubernetes
# buzz-agent: two infra-free concerns run together by executing the
# whole crate (lib + integration tests), because nothing in CI runs
# `cargo test --workspace`, so without this stanza neither the crate's
# library tests nor its integration tests execute remotely.
# * model-capabilities corpus (lib): the Rust half of the
# cross-language drift guard. `model_capabilities.rs` embeds
# scripts/model-capabilities.json + scripts/normative-corpus.json via
# include_str! and replays the full locked corpus as pure in-process
# tests; without it a manifest edit that diverges Rust from the
# corpus ships green.
# * OAuth auth coordinator (lib concurrency matrix + databricks
# integration tests): lock single-flight, cooldown, cross-process
# crash recovery — infra-free via a stub OIDC provider and an
# injected browser opener, no network or Postgres.
cargo nextest run -p buzz-agent
# Admin API auth-boundary tests (api::admin in buzz-relay): the NIP-98
# duplicate-tag rejections, the Host/Origin replay-ordering causal pair,
# the admin.localhost origin/advertisement/canonical-URL pins, and the
# host-oracle/credential-first checks. These are the regression guard for
# the /api/admin/v1 moderation auth surface. Enumerated explicitly because
# nothing in CI runs `cargo test --workspace`, `just test-unit` did not
# enumerate `buzz-relay --lib`, and Backend Integration selects only the
# #[ignore]d Postgres suites — so these non-ignored tests ran in no lane
# and a red one could ship green (exactly how a broken admin test slipped
# past every gate once). Scoped to api::admin, not the whole buzz-relay
# --lib, because api::media has non-ignored tests that require Postgres.
# Two api::admin tests are excluded: both exercise a read-route DB
# fallthrough and pass without a database only by waiting out the sqlx
# acquire timeout (~30s each), so they do not belong in the infra-free
# unit job. nip98_mode_unrostered_signer_does_not_consume_a_replay_slot
# asserts a unique replay-guard invariant, so it is wired into the
# Postgres-backed Backend Integration job (see ci.yml "Admin API
# unrostered-signer replay invariant"). disabled_mode_allows_
# unauthenticated_requests_on_the_admin_host has no unique invariant:
# disabled-mode unauthenticated success is covered by
# disabled_mode_regression_pin_unauthenticated_request_is_served on the
# DB-free /probe route, and its Host/Origin gating is covered here by
# disabled_mode_still_requires_the_correct_host / _a_matching_origin.
# The second clause adds the relay's pure authorization-decision tests:
# the NIP-29 channel membership grid (handlers::channel_authz), the
# moderation capability grid (handlers::moderation_authz), and the pure
# helpers in handlers::side_effects. They ran in NO lane before —
# `test(/^api::admin::/)` never matched them, and the PostgreSQL lane
# pairs `--run-ignored ignored-only` with a `postgres_tests::`
# default-filter — so a red one shipped green, exactly the gap the
# api::admin clause above was added to close.
# Deliberately scoped to these three modules instead of all of
# `handlers::`: the wider set is mostly Postgres-backed, and five of its
# non-postgres_tests cases only "pass" without a database by waiting out
# the ~30s sqlx acquire timeout, so they do not belong in the infra-free
# unit job either.
cargo nextest run -p buzz-relay --lib \
-E '(test(/^api::admin::/) - test(=api::admin::tests::disabled_mode_allows_unauthenticated_requests_on_the_admin_host) - test(=api::admin::tests::nip98_mode_unrostered_signer_does_not_consume_a_replay_slot)) + test(/^handlers::channel_authz::/) + test(/^handlers::moderation_authz::/) + test(/^handlers::side_effects::tests::/) + test(/^storage_sweep::tests::/)'
# ACP author-gate and queue tests protect the trust boundary between
# relay events and agent prompts. They are infra-free; ignored lifecycle
# tests remain excluded and run in their dedicated integration lanes.
cargo nextest run -p buzz-acp --lib
else
./scripts/run-tests.sh unit
fi
# Run integration tests only (starts services if needed)
test-integration:
./scripts/run-tests.sh integration
# Regenerate the model-capability normative corpus from the production Rust
# resolver. The corpus is a golden snapshot, never hand-edited: this runs the
# `#[ignore]`d writer test in buzz-agent, which serializes `resolve()` over the
# inputs-only question table to scripts/normative-corpus.json. Run this after
# any model-capabilities.json edit, then commit the regenerated file. The
# `corpus_matches_generated_snapshot` gate fails CI if the committed file drifts.
regen-model-corpus:
cargo test -p buzz-agent --lib model_capabilities::tests::regen_corpus_file -- --ignored --exact
# Buzz shared compute e2e: current desktop discovery/admission logic and
# Playwright UI coverage.
mesh-e2e:
cargo test --manifest-path {{desktop_dir}}/src-tauri/Cargo.toml --features mesh-llm mesh_llm --lib
cd {{desktop_dir}} && pnpm test:e2e:smoke -- mesh-compute.spec.ts
# Reset only development state, seed deterministic local channels, and launch
# the mesh-enabled desktop with the repository's public Tyler test identity.
# This is for local verification only; never point this identity at staging/prod.
[confirm("This will reset development data, preserve installed Buzz, then launch a seeded mesh dev app. Continue? (y/N)")]
mesh-dev-fresh:
#!/usr/bin/env bash
set -euo pipefail
./scripts/dev-reset.sh --yes
./scripts/setup-desktop-test-data.sh
export BUZZ_PRIVATE_KEY="3dbaebadb5dfd777ff25149ee230d907a15a9e1294b40b830661e65bb42f6c03"
export BUZZ_REQUIRE_RELAY_MEMBERSHIP=true
export BUZZ_ALLOW_NIP_OA_AUTH=true
export RELAY_OWNER_PUBKEY="e5ebc6cdb579be112e336cc319b5989b4bb6af11786ea90dbe52b5f08d741b34"
export BUZZ_RELAY_PRIVATE_KEY="0000000000000000000000000000000000000000000000000000000000000001"
export BUZZ_RECONCILE_CHANNELS=true
export BUZZ_RESET_WEBVIEW_STATE=1
exec just mesh=1 dev
# Real serve->client->inference on this machine (not CI).
mesh-e2e-hardware:
#!/usr/bin/env bash
set -euo pipefail
cargo run -p buzz-relay --example mesh_serve_client_smoke
# Three isolated node processes: trusted member joins and infers; stranger is rejected.
# Uses temp homes and explicit mesh owner keystores. Never reads the Buzz Keychain.
mesh-e2e-admission:
#!/usr/bin/env bash
set -euo pipefail
cargo run -p buzz-relay --example mesh_admission_smoke
# Full hardware confidence suite: routing, owner admission, and real agent inference.
mesh-e2e-confidence:
#!/usr/bin/env bash
set -euo pipefail
cargo build --release -p buzz-agent -p buzz-dev-mcp
cargo run -p buzz-relay --example mesh_serve_client_smoke
cargo run -p buzz-relay --example mesh_admission_smoke
cargo run -p buzz-relay --example mesh_agent_e2e
# Take desktop screenshots using the mock bridge
desktop-screenshot *ARGS:
#!/usr/bin/env bash
set -euo pipefail
pnpm -C {{desktop_dir}} build:e2e
cd {{desktop_dir}}
if ! curl -sf http://127.0.0.1:4173/ >/dev/null 2>&1; then
python3 -m http.server 4173 -d dist >/dev/null 2>&1 &
trap "kill $! 2>/dev/null || true" EXIT
for i in $(seq 1 20); do curl -sf http://127.0.0.1:4173/ >/dev/null && break; sleep 0.5; done
fi
node tests/helpers/screenshot.mjs {{ARGS}}
# ─── Run ──────────────────────────────────────────────────────────────────────
# Start the relay server (auto-starts Docker services if needed)
relay: bootstrap _ensure-migrations
#!/usr/bin/env bash
set -euo pipefail
export PATH="{{justfile_directory()}}/bin:$PATH"
set -o allexport
source .env
set +o allexport
cargo run -p buzz-relay
# Start the relay with the built web UI served from it
relay-web: bootstrap _ensure-migrations
#!/usr/bin/env bash
set -euo pipefail
export PATH="{{justfile_directory()}}/bin:$PATH"
set -o allexport
source .env
set +o allexport
[[ -d node_modules ]] || pnpm install
pnpm -C web build
BUZZ_WEB_DIR=./web/dist cargo run -p buzz-relay
# Build and run the private admin dashboard
admin: bootstrap _ensure-migrations
#!/usr/bin/env bash
set -euo pipefail
export PATH="{{justfile_directory()}}/bin:$PATH"
set -o allexport
source .env
set +o allexport
[[ -d node_modules ]] || pnpm install
pnpm -C admin-web build
export BUZZ_ADMIN_HOST="${BUZZ_ADMIN_HOST:-admin.localhost:3000}"
export BUZZ_ADMIN_WEB_DIR="${BUZZ_ADMIN_WEB_DIR:-{{justfile_directory()}}/admin-web/dist}"
# Default to disabled auth locally: localhost is the network boundary and a
# NIP-07 signer extension can't be assumed in dev. Override per run with
# BUZZ_ADMIN_AUTH=nip98 (plus RELAY_OPERATOR_PUBKEYS or RELAY_OWNER_PUBKEY)
# to exercise the authenticated path.
export BUZZ_ADMIN_AUTH="${BUZZ_ADMIN_AUTH:-disabled}"
echo "Admin dashboard: http://${BUZZ_ADMIN_HOST}/reports"
echo "Auth mode: ${BUZZ_ADMIN_AUTH} (set BUZZ_ADMIN_AUTH=nip98 to require a signed operator)"
cargo run -p buzz-relay
# Seed deterministic reports and product feedback for local admin dashboard review
admin-seed: _ensure-migrations
./scripts/seed-admin-dashboard.sh
# Run focused relay and browser checks for the admin dashboard
admin-check: fmt-check
cargo check -p buzz-relay --all-targets
cargo test -p buzz-relay api::admin
cargo test -p buzz-relay router::tests
pnpm -C admin-web check
pnpm -C admin-web test:e2e
# Start the relay server in release mode
relay-release: bootstrap _ensure-migrations
#!/usr/bin/env bash
set -euo pipefail
set -o allexport
source .env
set +o allexport
cargo run -p buzz-relay --release
# Run the desktop Tauri app in dev mode with a local relay (ports and identity derived from worktree)
dev *ARGS: bootstrap _ensure-sidecar-stubs _ensure-migrations
#!/usr/bin/env bash
set -euo pipefail
export PATH="{{justfile_directory()}}/bin:$PATH"
set -o allexport
source .env
set +o allexport
bind_addr="${BUZZ_BIND_ADDR:-0.0.0.0:3000}"
relay_port="${bind_addr##*:}"; [[ -n "$relay_port" ]] || relay_port=3000
health_port="${BUZZ_HEALTH_PORT:-8080}"
metrics_port="${BUZZ_METRICS_PORT:-9102}"
if command -v lsof >/dev/null 2>&1; then
for spec in "relay:$relay_port" "health:$health_port" "metrics:$metrics_port"; do
name="${spec%%:*}"; port="${spec##*:}"
if lsof -nP -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1; then
echo "Error: $name port $port is already in use; refusing to launch desktop against a stale relay." >&2
lsof -nP -iTCP:"$port" -sTCP:LISTEN >&2 || true
echo "Stop the process above (often a stale buzz-relay) and rerun: just dev" >&2
exit 1
fi
done
fi
cargo build -p buzz-acp -p buzz-agent -p buzz-backend-kubernetes -p buzz-dev-mcp -p buzz-cli -p git-credential-nostr -p buzz-relay
# Docker Desktop's forwarded MinIO port can stall under the deployment
# probe's 32 concurrent writers. Keep the gate enabled in local dev, using
# the bounded profile already used by the relay test launcher.
export BUZZ_GIT_PROBE_WRITERS="${BUZZ_GIT_PROBE_WRITERS:-8}"
export BUZZ_GIT_PROBE_ROUNDS="${BUZZ_GIT_PROBE_ROUNDS:-2}"
./target/debug/buzz-relay &
RELAY_PID=$!
cleanup() {
[[ -n "${INSTANCE_ID:-}" ]] && ../scripts/cleanup-instance-agents.sh "$INSTANCE_ID" || true
kill "$RELAY_PID" 2>/dev/null || true
}
trap cleanup EXIT
relay_ready=false
for _ in $(seq 1 120); do
if ! kill -0 "$RELAY_PID" 2>/dev/null; then
echo "Error: buzz-relay exited during startup; refusing to launch desktop." >&2
wait "$RELAY_PID" || true
exit 1
fi
if curl --silent --fail --max-time 1 "http://127.0.0.1:${health_port}/_readiness" >/dev/null; then
relay_ready=true
break
fi
sleep 0.5
done
if [[ "$relay_ready" != true ]]; then
echo "Error: buzz-relay did not become healthy within 60 seconds; refusing to launch desktop." >&2
exit 1
fi
cd {{desktop_dir}}
[[ -d node_modules ]] || pnpm install
source ../scripts/instance-env.sh
INSTANCE_ID=$(node -e "console.log(JSON.parse(process.env.BUZZ_TAURI_CONFIG).identifier)")
echo "Starting on Vite port ${BUZZ_VITE_PORT}, relay ${BUZZ_RELAY_URL}"
FEATURES=(); [[ -n "{{mesh}}" ]] && FEATURES=(--features mesh-llm)
pnpm exec tauri dev ${FEATURES[@]+"${FEATURES[@]}"} --config "$BUZZ_TAURI_CONFIG" {{ARGS}}
# Run only the desktop app. No relay, database, Docker, migrations, or .env are needed.
# The app opens normally and asks for a community before making a relay connection.
desktop-standalone *ARGS: _ensure-sidecar-stubs
#!/usr/bin/env bash
set -euo pipefail
export PATH="{{justfile_directory()}}/bin:$PATH"
cargo build -p buzz-acp -p buzz-agent -p buzz-backend-kubernetes -p buzz-dev-mcp -p buzz-cli -p git-credential-nostr
TARGET=$(rustc -vV | sed -n 's|host: ||p')
TARGET_DIR=$(cargo metadata --format-version 1 --no-deps | node -p "JSON.parse(require('fs').readFileSync(0, 'utf8')).target_directory")
for bin in buzz-acp buzz-agent buzz-backend-kubernetes buzz-dev-mcp git-credential-nostr buzz; do
cp "${TARGET_DIR}/debug/${bin}" "desktop/src-tauri/binaries/${bin}-${TARGET}"
chmod +x "desktop/src-tauri/binaries/${bin}-${TARGET}"
done
cd {{desktop_dir}}
[[ -d node_modules ]] || pnpm install
unset BUZZ_PRIVATE_KEY BUZZ_SHARE_IDENTITY
if [[ -n "{{fresh}}" ]]; then
export BUZZ_RESET_WEBVIEW_STATE=1
fi
source ../scripts/instance-env.sh
INSTANCE_ID=$(node -e "console.log(JSON.parse(process.env.BUZZ_TAURI_CONFIG).identifier)")
export BUZZ_DEV_KEYRING_SERVICE="buzz-desktop-dev.${BUZZ_INSTANCE_SLUG:-main}"
if [[ -n "{{fresh}}" ]]; then
../scripts/reset-desktop-standalone-state.sh "$INSTANCE_ID" "$BUZZ_DEV_KEYRING_SERVICE"
fi
trap '../scripts/cleanup-instance-agents.sh "$INSTANCE_ID" || true' EXIT
echo "Starting standalone desktop on Vite port ${BUZZ_VITE_PORT}; no relay services were started"
FEATURES=()
if [[ -n "{{mesh}}" ]]; then
FEATURES=(--features mesh-llm)
fi
pnpm exec tauri dev ${FEATURES[@]+"${FEATURES[@]}"} --config "$BUZZ_TAURI_CONFIG" {{ARGS}}
# Run the desktop app against the internal staging relay (installs deps + builds agent tools automatically)
staging *ARGS: bootstrap _ensure-sidecar-stubs
#!/usr/bin/env bash
set -euo pipefail
export PATH="{{justfile_directory()}}/bin:$PATH"
pnpm install # unconditional: staging must always start with a clean dep tree
cargo build --release -p buzz-acp -p buzz-agent -p buzz-backend-kubernetes -p buzz-dev-mcp -p buzz-cli -p git-credential-nostr
FEATURES=()
if [[ -n "{{mesh}}" ]]; then
FEATURES=(--features mesh-llm)
fi
# Replace 0-byte sidecar stubs with real binaries so tauri dev picks them up.
# buzz: the CLI sidecar. buzz-backend-kubernetes: provider discovery scans the
# exe dir for executable buzz-backend-* files, so the non-executable stub that
# tauri dev copies next to the exe would hide the provider from "Run on".
TARGET=$(rustc -vV | sed -n 's|host: ||p')
TARGET_DIR=$(cargo metadata --format-version 1 --no-deps | node -p "JSON.parse(require('fs').readFileSync(0, 'utf8')).target_directory")
STAGING_SIDECARS=(buzz)
if [[ "$TARGET" != *windows* ]]; then
STAGING_SIDECARS+=(buzz-backend-kubernetes)
fi
for bin in "${STAGING_SIDECARS[@]}"; do
cp "${TARGET_DIR}/release/${bin}" "desktop/src-tauri/binaries/${bin}-${TARGET}"
chmod +x "desktop/src-tauri/binaries/${bin}-${TARGET}"
done
cd {{desktop_dir}}
export BUZZ_RELAY_URL="wss://sprout-oss.stage.blox.sqprod.co"
source ../scripts/instance-env.sh
# Ctrl+C kills the Tauri app before its in-process sweep finishes, leaking
# agent workers. Reap this instance's agents on exit as a backstop.
INSTANCE_ID=$(node -e "console.log(JSON.parse(process.env.BUZZ_TAURI_CONFIG).identifier)")
trap '../scripts/cleanup-instance-agents.sh "$INSTANCE_ID" || true' EXIT
echo "Starting staging on Vite port ${BUZZ_VITE_PORT}, relay ${BUZZ_RELAY_URL}"
pnpm exec tauri dev ${FEATURES[@]+"${FEATURES[@]}"} --config "$BUZZ_TAURI_CONFIG" {{ARGS}}
# Run the desktop app against the production relay (installs deps + builds agent tools automatically)
production *ARGS: bootstrap _ensure-sidecar-stubs
#!/usr/bin/env bash
set -euo pipefail
export PATH="{{justfile_directory()}}/bin:$PATH"
pnpm install # unconditional: production must always start with a clean dep tree
cargo build --release -p buzz-acp -p buzz-agent -p buzz-backend-kubernetes -p buzz-dev-mcp -p buzz-cli -p git-credential-nostr
FEATURES=()
if [[ -n "{{mesh}}" ]]; then
FEATURES=(--features mesh-llm)
fi
# Replace 0-byte sidecar stubs with real binaries so tauri dev picks them up.
# buzz: the CLI sidecar. buzz-backend-kubernetes: provider discovery scans the
# exe dir for executable buzz-backend-* files, so the non-executable stub that
# tauri dev copies next to the exe would hide the provider from "Run on".
TARGET=$(rustc -vV | sed -n 's|host: ||p')
TARGET_DIR=$(cargo metadata --format-version 1 --no-deps | node -p "JSON.parse(require('fs').readFileSync(0, 'utf8')).target_directory")
PRODUCTION_SIDECARS=(buzz)
if [[ "$TARGET" != *windows* ]]; then
PRODUCTION_SIDECARS+=(buzz-backend-kubernetes)
fi
for bin in "${PRODUCTION_SIDECARS[@]}"; do
cp "${TARGET_DIR}/release/${bin}" "desktop/src-tauri/binaries/${bin}-${TARGET}"
chmod +x "desktop/src-tauri/binaries/${bin}-${TARGET}"
done
cd {{desktop_dir}}
export BUZZ_RELAY_URL="wss://buzz.block.builderlab.xyz"
source ../scripts/instance-env.sh
# Ctrl+C kills the Tauri app before its in-process sweep finishes, leaking
# agent workers. Reap this instance's agents on exit as a backstop.
INSTANCE_ID=$(node -e "console.log(JSON.parse(process.env.BUZZ_TAURI_CONFIG).identifier)")
trap '../scripts/cleanup-instance-agents.sh "$INSTANCE_ID" || true' EXIT
echo "Starting production on Vite port ${BUZZ_VITE_PORT}, relay ${BUZZ_RELAY_URL}"
pnpm exec tauri dev ${FEATURES[@]+"${FEATURES[@]}"} --config "$BUZZ_TAURI_CONFIG" {{ARGS}}
# Run the desktop frontend dev server (port derived from worktree)
desktop-dev:
#!/usr/bin/env bash
set -euo pipefail
cd {{desktop_dir}}
[[ -d node_modules ]] || pnpm install
source ../scripts/instance-env.sh
echo "Starting frontend dev server on Vite port ${BUZZ_VITE_PORT}, relay ${BUZZ_RELAY_URL}"
pnpm exec vite --port "${BUZZ_VITE_PORT}" --strictPort
# ─── Web ─────────────────────────────────────────────────────────────────────
# Run the web frontend dev server (port derived from worktree to avoid collisions)
web:
#!/usr/bin/env bash
set -euo pipefail
[[ -d node_modules ]] || pnpm install
source scripts/instance-env.sh
export VITE_PORT=$((BUZZ_VITE_PORT + 100))
export VITE_RELAY_URL="${BUZZ_RELAY_URL}"
echo "Starting web dev server on port ${VITE_PORT}, relay ${BUZZ_RELAY_URL}"
cd {{web_dir}}
pnpm exec vite --port "${VITE_PORT}" --strictPort
# Run web lint and format checks
web-check:
cd {{web_dir}} && pnpm check
# Fix web lint and format issues
web-fix:
cd {{web_dir}} && pnpm exec biome check --write .
# Run web TypeScript checks
web-typecheck:
cd {{web_dir}} && pnpm typecheck
# Build web frontend assets
web-build:
cd {{web_dir}} && pnpm build
# Run web browser smoke tests
web-e2e-smoke:
cd {{web_dir}} && pnpm test:e2e:smoke
# ─── Mobile ──────────────────────────────────────────────────────────────────
mobile_dir := "mobile"
# Install mobile Flutter dependencies
mobile-install:
unset GIT_DIR GIT_WORK_TREE; cd {{mobile_dir}} && flutter pub get
# Format all Dart code
mobile-fmt:
unset GIT_DIR GIT_WORK_TREE; cd {{mobile_dir}} && dart format .
# Fix mobile formatting and run analysis
mobile-fix:
unset GIT_DIR GIT_WORK_TREE; cd {{mobile_dir}} && dart format . && flutter analyze
# Run mobile lint and format checks
mobile-check:
unset GIT_DIR GIT_WORK_TREE; cd {{mobile_dir}} && dart format --output=none --set-exit-if-changed . && flutter analyze
# Run mobile tests
mobile-test:
/bin/bash ./scripts/test-mobile-gateway-recipes.sh
unset GIT_DIR GIT_WORK_TREE; cd {{mobile_dir}} && flutter test --dart-define=BUZZ_PUSH_GATEWAY_URL=https://push.example
unset GIT_DIR GIT_WORK_TREE; cd {{mobile_dir}} && flutter test test/shared/push/push_unconfigured_build_test.dart
# Regenerate the emoji dataset asset from desktop's emoji-mart install.
# Output is committed — rerun after bumping @emoji-mart/data.
mobile-emoji-data:
node {{mobile_dir}}/scripts/generate-emoji-data.mjs
# Compile an unsigned Android debug APK (worktree-aware debug identity)
mobile-build-android:
#!/usr/bin/env bash
set -euo pipefail
./scripts/mobile-worktree-overrides.sh
set -- build apk --debug --no-pub
if [[ -n "${BUZZ_PUSH_GATEWAY_URL:-}" ]]; then
set -- "$@" --dart-define="BUZZ_PUSH_GATEWAY_URL=${BUZZ_PUSH_GATEWAY_URL}"
fi
unset GIT_DIR GIT_WORK_TREE
cd {{mobile_dir}}
flutter "$@"
# Run the mobile app on iOS simulator (worktree-aware debug identity)
mobile-dev:
#!/usr/bin/env bash
set -euo pipefail
if ! pgrep -x Simulator &>/dev/null; then
open -a Simulator
sleep 3
fi
./scripts/mobile-worktree-overrides.sh
gateway_url="${BUZZ_PUSH_GATEWAY_URL:-}"
overrides_file="{{mobile_dir}}/ios/Flutter/AppOverrides.xcconfig"
if [[ -z "$gateway_url" && -f "$overrides_file" ]]; then
gateway_url="$(sed -nE 's/^[[:space:]]*BUZZ_PUSH_GATEWAY_URL[[:space:]]*=[[:space:]]*(.*[^[:space:]])[[:space:]]*$/\1/p' "$overrides_file" | tail -n 1 | sed 's/\$()//g')"
fi
set -- run
if [[ -n "$gateway_url" ]]; then
set -- "$@" --dart-define="BUZZ_PUSH_GATEWAY_URL=${gateway_url}"
fi
cd {{mobile_dir}}
unset GIT_DIR GIT_WORK_TREE
flutter "$@"
# Uninstall stale worktree-suffixed Buzz debug installs (production apps kept)
mobile-clean:
./scripts/mobile-worktree-clean.sh
# ─── Database ─────────────────────────────────────────────────────────────────
# Apply database migrations
migrate: _ensure-migrations
# ─── Utilities ────────────────────────────────────────────────────────────────
# Remove build artifacts
clean:
cargo clean
cargo clean --manifest-path desktop/src-tauri/Cargo.toml
# Check the Rust workspace compiles without producing binaries
check-compile:
cargo check --workspace --all-targets
# ─── Release ─────────────────────────────────────────────────────────────────
# Read the current desktop version from package.json
get-current-version:
@node -p "require('./desktop/package.json').version"
# Read the current relay version from its crate manifest
get-current-relay-version:
@grep -m1 '^version = ' crates/buzz-relay/Cargo.toml | sed -E 's/version = "(.*)"/\1/'
# Compute next minor version (e.g., 0.3.0 → 0.4.0)
get-next-minor-version:
@python3 -c "v='$(just get-current-version)'.split('.'); print(f'{v[0]}.{int(v[1])+1}.0')"
# Compute next patch version (e.g., 0.3.0 → 0.3.1)
get-next-patch-version:
@python3 -c "v='$(just get-current-version)'.split('.'); print(f'{v[0]}.{v[1]}.{int(v[2])+1}')"
# Compute next relay patch version (e.g., 0.3.0 → 0.3.1)
get-next-relay-patch-version:
@python3 -c "v='$(just get-current-relay-version)'.split('.'); print(f'{v[0]}.{v[1]}.{int(v[2])+1}')"
# Update version in desktop package manifests and regenerate lockfiles
bump-desktop-version version:
#!/usr/bin/env bash
set -euo pipefail
# desktop/package.json
cd desktop && npm pkg set "version={{ version }}" && cd ..
# desktop/src-tauri/tauri.conf.json
node -e "
const fs = require('fs');
const p = 'desktop/src-tauri/tauri.conf.json';
const c = JSON.parse(fs.readFileSync(p, 'utf8'));
c.version = '{{ version }}';
fs.writeFileSync(p, JSON.stringify(c, null, 2) + '\n');
"
# JSON.stringify expands arrays/objects in a way biome rejects; reformat to match.
(cd desktop && pnpm exec biome format --write src-tauri/tauri.conf.json)
# desktop/src-tauri/Cargo.toml — only first version line (under [package])
node -e "
const fs = require('fs');
const p = 'desktop/src-tauri/Cargo.toml';
let t = fs.readFileSync(p, 'utf8');
t = t.replace(/^version = \".*\"/m, 'version = \"{{ version }}\"');
fs.writeFileSync(p, t);
"
# Regenerate lockfiles
pnpm install --lockfile-only
cargo update -p buzz-desktop --manifest-path desktop/src-tauri/Cargo.toml
echo "Bumped desktop manifests to {{ version }} and regenerated lockfiles"
# Bump the relay crate version and regenerate the lockfile
bump-relay-version version:
#!/usr/bin/env bash
set -euo pipefail
# buzz-relay carries its own `version =` (not version.workspace), so the
# replace targets the package version line only.
perl -i -pe 's/^version = ".*"/version = "{{ version }}"/' crates/buzz-relay/Cargo.toml
cargo update -p buzz-relay
echo "Bumped buzz-relay to {{ version }} and regenerated Cargo.lock"
# Open or update the desktop release PR from an immutable origin/main snapshot
release-desktop *ARGS:
#!/usr/bin/env bash
set -euo pipefail
ARG="{{ ARGS }}"
if [[ -z "$ARG" || "$ARG" == "patch" ]]; then
VERSION=$(just get-next-patch-version)
else
VERSION="$ARG"
fi
scripts/prepare-desktop-release.sh "$VERSION"
# Open or update the relay release PR (ghcr.io/block/buzz image)
release-relay *ARGS:
#!/usr/bin/env bash
set -euo pipefail
ARG="{{ ARGS }}"
if [[ -z "$ARG" || "$ARG" == "patch" ]]; then
VERSION=$(just get-next-relay-patch-version)
else
VERSION="$ARG"
fi
just _release-pr relay "$VERSION"
# Shared release-PR engine for desktop and relay. Mobile publishes immutable
# candidate tags directly from remote main instead of using metadata-only PRs.
_release-pr lane version:
#!/usr/bin/env bash
set -euo pipefail
VERSION="{{ version }}"
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then
echo "Error: '$VERSION' is not valid semver (expected X.Y.Z)"
exit 1
fi
# Lane-specific identifiers. The bump command runs after the branch switch.
case "{{ lane }}" in
desktop)
BRANCH_PREFIX="version-bump"
TAG_FETCH='v*'
TAG_MATCH='v[0-9]*'
TAG_EXCLUDE='*-*'
TAG_PREFIX="v"
CHANGELOG="CHANGELOG.md"
ADD_FILES=(desktop/package.json desktop/src-tauri/tauri.conf.json desktop/src-tauri/Cargo.toml desktop/src-tauri/Cargo.lock pnpm-lock.yaml CHANGELOG.md)
LOG_PATHS=(desktop/ crates/buzz-core/ crates/buzz-persona/ crates/buzz-sdk/ crates/buzz-agent/)
ARTIFACT="Buzz Desktop" ;;
relay)
BRANCH_PREFIX="relay-release"
TAG_FETCH='relay-v*'
TAG_MATCH='relay-v[0-9]*'
TAG_EXCLUDE='relay-v*-*'
TAG_PREFIX="relay-v"
CHANGELOG="crates/buzz-relay/CHANGELOG.md"
ADD_FILES=(crates/buzz-relay/Cargo.toml Cargo.lock crates/buzz-relay/CHANGELOG.md)
LOG_PATHS=(crates/buzz-relay/ crates/buzz-core/ crates/buzz-db/ crates/buzz-auth/ crates/buzz-pubsub/ crates/buzz-search/ crates/buzz-audit/ crates/buzz-media/ crates/buzz-sdk/ crates/buzz-workflow/ crates/buzz-conformance/ migrations/)
ARTIFACT="Buzz Relay" ;;
*)
echo "Error: unknown release lane '{{ lane }}'"
exit 1 ;;
esac
echo "Preparing ${ARTIFACT} release v${VERSION}..."
# Must run on main with a clean, up-to-date tree.
CURRENT_BRANCH=$(git symbolic-ref --short HEAD)
if [[ "$CURRENT_BRANCH" != "main" ]]; then
echo "Error: must be on main branch (currently on '$CURRENT_BRANCH')"
exit 1
fi
git fetch origin refs/heads/main:refs/remotes/origin/main --no-tags
# Release tags are remote-owned state; sync only this lane's tags so stale
# local tags from older histories do not make release preflight fail.
git fetch origin "+refs/tags/${TAG_FETCH}:refs/tags/${TAG_FETCH}"
if [[ "$(git rev-parse HEAD)" != "$(git rev-parse origin/main)" ]]; then
echo "Error: local main is not up-to-date with origin/main. Run 'git pull' first."
exit 1
fi
if ! git diff --quiet || ! git diff --cached --quiet; then
echo "Error: working tree is dirty. Commit or stash changes first."
exit 1
fi
# Switch to the release branch (create, or reset to main if it exists).
BRANCH="${BRANCH_PREFIX}/${VERSION}"
if git rev-parse --verify "refs/heads/$BRANCH" >/dev/null 2>&1; then
echo "Branch '$BRANCH' already exists — resetting to origin/main..."
git switch "$BRANCH"
git reset --hard origin/main
elif git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then
echo "Branch '$BRANCH' exists on remote — checking out and resetting to origin/main..."
git switch -c "$BRANCH" --track "origin/$BRANCH"
git reset --hard origin/main
else
git switch -c "$BRANCH"
fi
# Lane-specific bump (the one diverging step).
case "{{ lane }}" in
desktop) just bump-desktop-version "$VERSION" ;;
relay) just bump-relay-version "$VERSION" ;;
esac
# Generate the changelog from commits since this lane's last release tag.
LAST_TAG=$(git describe --tags --abbrev=0 --match "$TAG_MATCH" --exclude "$TAG_EXCLUDE" 2>/dev/null || echo "")
REPO=$(git remote get-url origin | sed -E 's|.*github\.com[:/]||; s|\.git$||')
format_log() {
local range="$1"
git log "$range" --format="%h %H %s" --no-merges -- "${LOG_PATHS[@]}" | while IFS=' ' read -r short full rest; do
local pr subject
pr=$(printf '%s' "$rest" | grep -oE '\(#[0-9]+\)$' | grep -oE '[0-9]+' || true)
if [[ -n "$pr" ]]; then
subject=$(printf '%s' "$rest" | sed -E 's/ \(#[0-9]+\)$//')
printf -- '- %s ([#%s](https://github.com/%s/pull/%s)) ([`%s`](https://github.com/%s/commit/%s))\n' \
"$subject" "$pr" "$REPO" "$pr" "$short" "$REPO" "$full"
else
printf -- '- %s ([`%s`](https://github.com/%s/commit/%s))\n' \
"$rest" "$short" "$REPO" "$full"
fi
done
}
TMPFILE=$(mktemp)
{
echo "# Changelog"
echo ""
echo "## ${TAG_PREFIX}${VERSION}"
echo ""
if [[ -n "$LAST_TAG" ]]; then
format_log "${LAST_TAG}..HEAD"
else
echo "- Initial release"
fi
echo ""
if [[ -f "$CHANGELOG" ]]; then
tail -n +2 "$CHANGELOG"
fi
} > "$TMPFILE"
mkdir -p "$(dirname "$CHANGELOG")"
mv "$TMPFILE" "$CHANGELOG"
# Commit.
git add "${ADD_FILES[@]}"
RELEASE_MSG="chore(release): release ${ARTIFACT} version ${VERSION}"
if [[ "$(git log -1 --format='%s' 2>/dev/null)" == "$RELEASE_MSG" ]]; then
git commit --amend --no-edit
else
git commit -m "$RELEASE_MSG"
fi
# Push and open/update the PR.
git push --force-with-lease -u origin "$BRANCH"
PR_BODY="## ${ARTIFACT} release v${VERSION}"$'\n\n'
if [[ -n "$LAST_TAG" ]]; then
PR_BODY+="### Changes since ${LAST_TAG}:"$'\n\n'
CHANGELOG_BODY=$(format_log "${LAST_TAG}..HEAD~1")
MAX_LOG=62000
if (( ${#CHANGELOG_BODY} > MAX_LOG )); then
TRUNCATED=$(printf '%s' "$CHANGELOG_BODY" | awk -v max="$MAX_LOG" \
'BEGIN{n=0} {line_len=length($0)+1; if(n+line_len>max) exit; n+=line_len; print}')
SHOWN=$(printf '%s\n' "$TRUNCATED" | grep -c '^-' || true)
TOTAL=$(printf '%s\n' "$CHANGELOG_BODY" | grep -c '^-' || true)
SKIPPED=$(( TOTAL - SHOWN ))
CHANGELOG_BODY="${TRUNCATED}"$'\n'"_… and ${SKIPPED} more commits — [compare ${LAST_TAG}…${TAG_PREFIX}${VERSION}](https://github.com/${REPO}/compare/${LAST_TAG}...${TAG_PREFIX}${VERSION})_"
fi
PR_BODY+="${CHANGELOG_BODY}"$'\n\n'
else
PR_BODY+="Initial release."$'\n\n'
fi
PR_BODY+="**To release:** merge this PR. The tag and build will happen automatically."
PR_TITLE="chore(release): release ${ARTIFACT} version ${VERSION}"
EXISTING_PR=$(gh pr list --head "$BRANCH" --json url --jq '.[0].url' 2>/dev/null || true)
if [[ -n "$EXISTING_PR" ]]; then
gh pr edit "$BRANCH" --title "$PR_TITLE" --body "$PR_BODY"
PR_URL="$EXISTING_PR"
echo ""
echo "Updated existing release PR: ${PR_URL}"
else
PR_URL=$(gh pr create --title "$PR_TITLE" --body "$PR_BODY")
echo ""
echo "Release PR opened: ${PR_URL}"
fi
echo "Merge it to trigger the release build."
# ─── Agent Harness ────────────────────────────────────────────────────────────
# Run a goose agent connected to a Buzz relay (foreground)
goose relay="ws://localhost:3000" agents="1" heartbeat="0" prompt="" key="$BUZZ_PRIVATE_KEY":
#!/usr/bin/env bash
set -euo pipefail
export PATH="{{justfile_directory()}}/bin:$PATH"
source ./scripts/_goose-env.sh "{{relay}}" "{{key}}" "{{agents}}" "{{heartbeat}}" "{{prompt}}"
exec env "${env_args[@]}" ./target/release/buzz-acp
# Run a goose agent in the background (screen session named 'goose-agent-N')
goose-bg relay="ws://localhost:3000" agents="1" heartbeat="0" prompt="" key="$BUZZ_PRIVATE_KEY":
#!/usr/bin/env bash
set -euo pipefail
export PATH="{{justfile_directory()}}/bin:$PATH"
source ./scripts/_goose-env.sh "{{relay}}" "{{key}}" "{{agents}}" "{{heartbeat}}" "{{prompt}}"
screen -dmS goose-agent-{{agents}} bash -c "$(printf '%q ' env "${env_args[@]}") ./target/release/buzz-acp"
echo "Agent running in screen session 'goose-agent-{{agents}}'. Attach with: screen -r goose-agent-{{agents}}"
# ─── Benchmarking ─────────────────────────────────────────────────────────────
# Run the Buzz orchestra benchmark — TB defaults to leaderboard-eligible k=5; Buzz task layers default to regression k=1 and workflow k=3. Stands up its own Docker stack; --gui opens a live spectator desktop app; other flags pass to benchmark.py (--dataset/--path, --layer, --include-task, --attempts, --manifest, --dry-run, ...)
benchmark *ARGS:
#!/usr/bin/env bash
set -euo pipefail
export PATH="{{justfile_directory()}}/bin:$PATH"
uv run --project benchmarks/harbor-buzz-orchestra/testbed \
benchmarks/harbor-buzz-orchestra/scripts/benchmark.py {{ARGS}}
# Run the benchmark adapter + testbed gate exactly as CI does (pytest + ruff, pinned ruff from pyproject)
benchmark-check:
#!/usr/bin/env bash
set -euo pipefail
cd "{{justfile_directory()}}/benchmarks/harbor-buzz-orchestra"
# CI installs the dev extra with pip, so pyproject — not uv.lock — decides
# which ruff lints. Read the pin from there so this recipe cannot drift
# from the workflow (a floating specifier once meant CI failed on RUF100
# while the locked local ruff passed).
ruff_pin="$(grep -oE 'ruff==[0-9.]+' pyproject.toml | head -1 | cut -d= -f3)"
for project in . testbed; do
(
cd "$project"
echo "── harbor-buzz-orchestra/$project (ruff $ruff_pin)"
uv run --frozen pytest -q
uvx "ruff@$ruff_pin" check .
uvx "ruff@$ruff_pin" format --check .
)
done
# The task verifiers live in the sibling benchmarks/buzz-dataset, so they
# need the harness config passed explicitly to stay linted.
echo "── buzz-dataset (ruff $ruff_pin)"
uvx "ruff@$ruff_pin" check --config pyproject.toml ../buzz-dataset
uvx "ruff@$ruff_pin" format --check --config pyproject.toml ../buzz-dataset
# Stop the benchmark Docker stack (state and channels are kept)
benchmark-down:
docker compose --project-name buzz-benchmark down