R2D2-MERIDIAN/Justfile
Joshua Belke 338c988032
Some checks failed
CI / Detect Changed Paths (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
Meridian Harness / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Meridian Harness / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Isolated DB Gate (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Admin Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Server Cross-Compile (push) Has been cancelled
CI / Server Cross-Compile-1 (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Meridian Harness / Publish rolling release (push) Has been cancelled
Meridian Harness / Publish tagged release (push) Has been cancelled
docs: the ignored-relay-test count is 53, and a fourth author has now got it wrong
a9b30ea27 added two Postgres-gated tenancy tests and left the number at 51
in both the Justfile and the CI comment beside the same selection. Derived
from the binary rather than from grep: 53.

The Justfile comment already told the next reader to re-derive rather than
trust it, and listed three authors who had not. It now lists four,
including this campaign. Leaving the tally visible is the point -- a
hand-maintained number that keeps being wrong is evidence the comment
wants a gate, not a bigger warning.

Signed-off-by: Joshua Belke <joshua@innovationhub-act.org>
2026-08-21 04:53:55 -04:00

2094 lines
108 KiB
Makefile
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Meridian — development task runner
set dotenv-load := true
# dotenv-load above reads .env only. Compose reads .env.local ONLY via
# COMPOSE_ENV_FILES, and ONLY when it comes from the shell — setting it inside
# .env is silently ignored. Exporting it here means every recipe that shells out
# to `docker compose` honours .env.local automatically, with no per-recipe flag.
# Recipes that run the relay/control-plane source .env.local explicitly, since
# those read the environment rather than going through Compose.
export COMPOSE_ENV_FILES := if path_exists(".env.local") == "true" { ".env,.env.local" } else { ".env" }
desktop_dir := "REMAPPING/meridian-desktop"
desktop_tauri_manifest := "REMAPPING/meridian-desktop/src-tauri/Cargo.toml"
web_dir := "REMAPPING/meridian-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 &
git --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
# 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
# Beads chains its section onto the lefthook shims, so it must run AFTER
# `lefthook install --force` — which clobbers what beads writes if the order
# is reversed. Skipped silently when bd is absent; check-git-hooks reports a
# missing beads section anyway.
if command -v bd >/dev/null 2>&1; then bd hooks install >/dev/null 2>&1 || true; fi
# LAST, because both installers above rewrite the files and drop it: make
# lefthook's exit status reach git. Without this a shell script exits with
# its last command's status — beads' — and every local gate is advisory
# (meridian-qqct). Pinned by check-git-hooks.
node scripts/patch-hook-exit-propagation.mjs
# Wipe development state and recreate a clean environment. Installed Meridian is preserved.
[confirm("This will DELETE all development data and preserve installed Meridian. Continue? (y/N)")]
reset:
./scripts/dev-reset.sh --yes
# Start dev services. Honours COMPOSE_PROFILES and .env.local (via the exported
# COMPOSE_ENV_FILES above), unlike a bare `docker compose up -d`.
up *ARGS:
docker compose up -d {{ARGS}}
# Set MERIDIAN_DEV_FILE_SECRETS=1 (in .env.local) and run.sh builds the desktop
# with --no-default-features, so it never opens the keychain. This migrates the
# secrets into the 0600 file stores that build reads. --check reports only.
# The keychain items are left untouched, so it is reversible.
# macOS: move dev secrets out of the login keychain so rebuilds stop prompting
dev-secrets-to-files *ARGS:
./scripts/dev-secrets-to-files.sh {{ARGS}}
# Unattended equivalent of the Keychain Access → Certificate Assistant steps in
# REMAPPING/meridian-desktop/src-tauri/AGENTS.md. Needed for CI and any machine nobody can click
# through. Idempotent; set MERIDIAN_DEV_SIGN_IDENTITY afterwards to use it.
# macOS: create the self-signed identity that keeps dev builds' code identity stable
dev-signing-identity *ARGS:
./scripts/dev-signing-identity.sh {{ARGS}}
# Show which env files and profiles are in effect, and the resolved host ports.
# Run this first when a service is reachable on an unexpected port.
#
# Layer provenance: this recipe should report which layer supplied each
# resolved value (`.env`, `.env.local`, shell, compose override) — not only
# the winner — so a wrong port is diagnosable without re-reading every file.
# Caller overrides for test recipes use dedicated MERIDIAN_TEST_* names
# (MERIDIAN_TEST_DATABASE_URL, MERIDIAN_TEST_REDIS_URL, MERIDIAN_TEST_RELAY_URL)
# because those names appear in no env file; see CONTRIBUTING.md § Running Tests.
env-doctor:
#!/usr/bin/env bash
set -euo pipefail
# COMPOSE_PROFILES usually arrives from .env.local, which Compose reads via
# COMPOSE_ENV_FILES but this recipe's shell does not — so source it before
# reporting, or the value shown contradicts the service list below.
if [[ -f .env.local ]]; then set -a; . ./.env.local; set +a; fi
echo "COMPOSE_ENV_FILES = ${COMPOSE_ENV_FILES:-<unset>}"
# This recipe exports COMPOSE_ENV_FILES=.env, but .env is gitignored — so
# on a fresh clone Compose fails closed with "couldn't find env file" and
# env-doctor, the tool you reach for BECAUSE the layers are confusing,
# dies before printing a single resolved port. Report what was asked for
# on the line above, then drop the names that do not exist.
_cef_keep=""
if [[ -n "${COMPOSE_ENV_FILES:-}" ]]; then
IFS=',' read -ra _cef_parts <<<"$COMPOSE_ENV_FILES"
for _cef in "${_cef_parts[@]}"; do
[[ -f "$_cef" ]] && _cef_keep="${_cef_keep:+${_cef_keep},}${_cef}"
done
if [[ "$_cef_keep" != "$COMPOSE_ENV_FILES" ]]; then
echo " (using ${_cef_keep:-<none>} — the rest do not exist)"
fi
if [[ -n "$_cef_keep" ]]; then export COMPOSE_ENV_FILES="$_cef_keep"; else unset COMPOSE_ENV_FILES; fi
fi
echo "COMPOSE_PROFILES = ${COMPOSE_PROFILES:-<unset> (core services only)}"
for f in .env .env.local docker-compose.override.yml; do
[[ -f "$f" ]] && echo "present: $f" || echo "absent : $f"
done
# The bus is reported separately from the port table because its failure is
# a MISMATCH, not a missing value: a relay on a Zenoh mode with no endpoint
# list, or an endpoint list pointing at a port Compose is not publishing,
# both look completely healthy from `docker compose ps`. Neither multicast
# nor gossip scouting is enabled, so nothing discovers its way out of it.
echo "--- event bus ---"
bus_mode="${MERIDIAN_BUS:-redis (default)}"
bus_peers="${MERIDIAN_ZENOH_ENDPOINTS:-<none>}"
echo " MERIDIAN_BUS ${bus_mode}"
echo " MERIDIAN_ZENOH_ENDPOINTS ${bus_peers}"
if [[ "${MERIDIAN_BUS:-redis}" != "redis" && "${MERIDIAN_ZENOH_ENDPOINTS:-}" == "" ]]; then
echo " !! MERIDIAN_BUS=${MERIDIAN_BUS} with no endpoints: discovery is off on both"
echo " mechanisms, so the relay has no way to reach a router."
fi
if [[ ",${COMPOSE_PROFILES:-}," == *,bus,* ]]; then
echo " profile 'bus' is ON → zenohd on ${ZENOH_HOST_PORT:-7447}, image eclipse/zenoh:${ZENOH_IMAGE_TAG:-1.8.0}"
[[ "${MERIDIAN_BUS:-redis}" == "redis" ]] && \
echo " note: MERIDIAN_BUS=redis — the router will start with nothing dialing it."
else
echo " profile 'bus' is OFF → no zenohd router in this stack"
fi
echo "--- resolved services and host ports ---"
docker compose config --format json > /tmp/meridian-env-doctor.json
python3 - /tmp/meridian-env-doctor.json <<'PY'
import json, sys
services = json.load(open(sys.argv[1])).get("services", {})
for name, spec in sorted(services.items()):
published = [str(p.get("published")) for p in spec.get("ports", [])]
print(f" {name:24} {', '.join(published) or '-'}")
PY
rm -f /tmp/meridian-env-doctor.json
# 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 and formatting checks.
#
# `desktop-check` / `web-check` are biome plus the guard scripts — neither runs
# `tsc`. Without the typecheck recipes here, this whole gate passes on a tree
# that does not compile, and the first thing to notice is CI's `desktop-build`.
check: fmt-check clippy desktop-check desktop-typecheck desktop-tauri-fmt-check desktop-tauri-clippy web-check web-typecheck mobile-check compose-check scoped-commit-check launcher-check check-docker-context check-git-hooks backup-restore-check git-pointer-repair-contract nip34-search-rollout-check reply-persistence-benchmark-contract capacity-contract-check check-kinds perf-check zenoh-check check-skills check-stellar check-gauntlet check-architecture-map check-alert-runbooks check-chart-version-bump check-brand check-feature-specs check-mips check-unwrap-budget check-relay-e2e-inventory check-frozen-migrations check-legacy-namespaces check-relay-terminology check-npm-supply-chain check-external-copy check-reductstore-zenoh
# Guard run.sh's env layering and its process teardown.
#
# Both suites existed as evidence for a specific defect and neither ran in any
# gate — `test-run-env-layers.sh` was cited in the root contract as the thing
# that pins `.env` precedence while being reachable only by typing its path,
# which is the meridian-h73s shape (a test that runs nowhere proves nothing).
#
# Teardown is guarded because its two failure modes both report success:
# an EXIT trap scoped by binary path reaps a second agent's relay from the same
# checkout (meridian-mth0), and a stop that kills the tracked wrapper without
# checking the socket leaves an orphan answering health probes for the next
# build (meridian-we5n). No Docker, no database, no network; binds only
# ephemeral ports it asks the kernel for.
launcher-check:
bash scripts/test-run-env-layers.sh
bash scripts/test-run-process-teardown.sh
bash scripts/test-git-shim-recursion.sh
# Verify the local backing stack contract (Dragonfly swap, database isolation,
# and every caller that names those services). No Docker required — the
# resolved-config assertions self-skip when Docker is unavailable.
compose-check:
bash scripts/test-compose-stack-contract.sh
# Guard the shared-index commit contract. This branch is shared by construction
# — the root contract forbids branching so parallel agents interleave on `main`
# — so they share one working tree and one git index, and a bare `git commit -m`
# takes the whole index including another agent's staged work (finding F077).
# Runs in throwaway repos; no Docker, no database, no network.
scoped-commit-check:
bash scripts/test-git-commit-scoped.sh
# Guard the Docker build context against pruning a file the image then compiles.
# Every other check sees the full worktree; only `docker build` sees the pruned
# context, so a `.dockerignore` rule that excludes a transitively-imported file
# fails nowhere until the image build (meridian-xdl5: a published relay image was
# unbuildable on TS2307). Resolves each package's aliased imports and asserts that
# every one escaping its own package survives the prune. Static, milliseconds, no
# Docker required.
check-docker-context:
node scripts/check-docker-build-context.mjs
node --test scripts/check-docker-build-context-core.test.mjs
# Guard that git hooks are served by THIS repo and that both systems survive.
# core.hooksPath is consulted exclusively when set and prints nothing when it is
# wrong: on 2026-08-07 it pointed into an unrelated project, so every hook here
# was served from a tree this repo does not own, and a lefthook install followed
# the redirect and overwrote that project's hooks (meridian-89vi). Asserts
# resolved config and file content, never an installer's exit code — `lefthook
# install` has been observed succeeding while installing nothing.
check-git-hooks:
node scripts/check-git-hooks.mjs
node --test scripts/check-git-hooks-core.test.mjs
# Contract-test backup failure cleanup, signed-set integrity, restore gates,
# semantic probes, matching-image startup, and isolated resource cleanup.
backup-restore-check:
bash scripts/test-verify-backup-restore.sh
# Prove the quiesced Git watermark roll-forward validates every referenced
# content-addressed object and never repairs by lowering the durable fence.
git-pointer-repair-contract:
bash scripts/test-repair-git-pointer-from-watermark.sh
# Guard the online NIP-34 backfill/index procedure. The events table is
# partitioned, so PostgreSQL requires concurrent leaf builds plus attachment to
# a non-concurrently-created partitioned parent index.
nip34-search-rollout-check:
bash scripts/test-backfill-nip34-search-contract.sh
# Assert every kind constant in ALL_KINDS has a reader somewhere. A declared
# and unread kind looks like progress in a diff and is indistinguishable from
# nothing at runtime. Deliberate reservations live in the script's allowlist.
check-kinds:
node scripts/check-kind-readers.mjs
node --test scripts/check-kind-readers-core.test.mjs
# The bus-scaling harness produces the 64x interest-scoping figure that README,
# TASKS and DIAGRAM all publish as [MEASURED] — and DIAGRAM says the harness
# "fails below 95% of ideal, so the claim is load-bearing rather than
# decorative". Nothing ran it. It was not wired into any recipe, so the guard
# behind four published claims never executed in CI, and `assert_scaling`
# separately let a scoped arm delivering ZERO score an infinite reduction and
# pass. Both are the same failure the root contract names: a claim whose
# enforcement point is never reached. This runs the harness's own tests only —
# the measurement itself is a deliberate, quiet-machine act, never a CI job,
# because a throughput number taken under load is worse than no number.
perf-check:
python3 -m unittest discover -s perf -p 'test_*.py'
# Prove the pinned zenoh config survives contact with the DAEMON, not just the
# library. zenohd is not a passive reader of its own file: it re-enables
# adminspace and plugins_loading after loading it, and reads an ABSENT
# multicast-scouting key as consent. So a test that only shows the file parsed
# shows nothing -- this asserts the effective configuration zenohd reports in its
# own `Initial conf` line, and fails if zenohd ever stops force-enabling them, so
# the workaround cannot outlive the quirk.
#
# NOT in `just check`: it needs Docker and the pinned eclipse/zenoh:1.8.0 image.
# Wiring it in behind a compose-check-style self-skip is meridian-vt1 follow-up
# work -- a gate in `check` that has never been proven to skip cleanly is how a
# green CI stops meaning anything.
zenoh-config-check:
MERIDIAN_ZENOH_DAEMON_CHECK=1 cargo test -p meridian-pubsub --features zenoh zenoh_config
# Prove the bus DELIVERS in the topology it is deployed in: two relay sessions,
# one real zenohd, an event across. Nothing else does.
#
# `tests/zenoh_bus.rs` has 21 green tests and every one of them cross-connects
# two peers DIRECTLY -- a shape that exercises none of the router's routing
# tables. All 21 stayed green through meridian-2m45, where the documented
# default mode delivered NOTHING through a router while both sessions reported
# `zenoh_ready`. A test suite that never stands up the deployed topology cannot
# see a defect that only exists in it, however many assertions it carries.
#
# Same gate as `zenoh-config-check` above and for the same reason: it needs
# Docker and the pinned eclipse/zenoh:1.8.0 image, so it is opt-in behind
# MERIDIAN_ZENOH_DAEMON_CHECK=1 and NOT in `just check`.
zenoh-router-check:
MERIDIAN_ZENOH_DAEMON_CHECK=1 cargo test -p meridian-pubsub --features zenoh --test zenoh_router -- --test-threads=1
# Compile and run the Zenoh backend. Nothing else does.
#
# `just clippy` is `--workspace --all-targets` with no `--all-features`, and
# `just test-unit` names crates without `--features zenoh`, so every line of
# crates/meridian-pubsub/src/zenoh/, the shadow adapter, and the whole
# tests/zenoh_bus.rs suite were compiled and executed by NO gate -- not CI, not
# pre-push, not `just ci`. That included `ZenohConfigError::NonLocalEndpoint`,
# the single refusal standing between this build and unauthenticated
# federation: a compile break in it would have shipped undetected, and every
# "landed and tested" claim rested on an agent typing a local command in a
# worktree. Found by the Provenance Auditor seat, 2026-08-19.
#
# This is the same defect the repo already paid for twice this week -- perf/ in
# no recipe while four documents cited its number as [MEASURED], and cargo-deny
# reporting `licenses ok` with zenoh absent from the graph entirely. A gate that
# does not run is indistinguishable from one that passes.
#
# The daemon check above stays separate: it needs Docker and a pulled image.
# This one needs neither.
zenoh-check:
cargo clippy -p meridian-pubsub -p meridian-relay --all-targets --features zenoh -- -D warnings
cargo test -p meridian-pubsub --features zenoh
# Report the STELLAR documentation delta: which AGENTS.md subtrees moved since
# the committed snapshot, and what a regeneration has already dropped. Read this
# before editing a doc — rewriting one whose subtree did not move is how durable
# contracts get paraphrased away.
stellar-scan:
node scripts/stellar-scan.mjs
# Gate the chain. Fails on an orphaned doc, a child-index row pointing at
# nothing, or a loss: a section dropped or gutted, a child row removed, a doc
# shrunk past 60% of its bytes. The snapshot is tracked precisely so accepting
# a loss is a reviewable diff rather than a silent regeneration.
check-stellar:
node scripts/stellar-scan.mjs --check
node --test scripts/stellar-scan-core.test.mjs
bash scripts/test-stellar-scan-contract.sh
# Accept the current chain state and refresh .settings/stellar/reference-index.md.
# Commit the snapshot in the same commit as the doc edits it describes.
stellar-snapshot:
node scripts/stellar-scan.mjs --write
# Validate project skill frontmatter, UI metadata, and suite-index coverage.
# Guard text that leaves this repo for an external tracker or wiki.
#
# An external ticket states WORK TO BE DONE. Internal decision provenance
# (advisory-seat deliberation, automated review passes, model names) and prior
# product identities (Codebase Chat, Buzz, Sprout) are not work, and publishing
# them to a customer's GitLab is not recoverable by editing afterwards — the
# notification email already went out.
#
# Both failure modes already happened, which is why this is a gate and not a
# convention. The guard is the BACKSTOP: a generator must build external copy
# from a work statement, because a blocklist over freeform prose fails open on
# the phrasing nobody predicted. Widening the pattern list is the wrong repair
# for a failure — rewrite the copy.
check-external-copy:
node --test scripts/check-external-copy-core.test.mjs
node scripts/check-external-copy.mjs
check-skills:
node scripts/check-project-skills.mjs
node --test scripts/check-project-skills-core.test.mjs
# The gauntlet register may not claim a state its evidence cannot support. The
# maturation brief forbids an agent grading itself complete, secure, or
# production-ready; a prose rule cannot bind the agent it constrains, so the
# state machine, the evidence shape, the bead references, and the loop's own
# refusal list are parsed and asserted instead.
check-gauntlet:
node scripts/check-gauntlet.mjs
node --test scripts/check-gauntlet-core.test.mjs
# The G01 baseline inventory annotates the Cargo workspace with three facts the
# workspace cannot state about itself — maturity, test coverage, and whether the
# root architecture map names the crate at all. An annotation that drifts from
# what it annotates is worse than none, because it reads as verified. Every
# column is recomputed and compared bidirectionally, so a crate added or removed
# without touching the inventory fails here rather than at the next audit.
# The README's crate table is held to the same contract, in its own marker block:
# it is the most-read file in the repo and the least likely to be revisited when
# a crate lands, so an unguarded enumeration there would drift the fastest. Its
# Rust badge is pinned to rust-toolchain.toml for the same reason: a stale badge
# sends a contributor to the wrong toolchain and surfaces as a build error.
check-architecture-map:
node scripts/check-architecture-map.mjs
node --test scripts/check-architecture-map-core.test.mjs
# Two halves of the alerting surface fail silently. An alert whose expression
# names a metric no crate declares can never fire, and reads as coverage on
# every dashboard; an alert with no runbook is invisible until it pages. Both
# are asserted against the G17 baseline, bidirectionally, so an alert added or
# a metric renamed fails here rather than at 3am.
check-alert-runbooks:
node scripts/check-alert-runbooks.mjs
node --test scripts/check-alert-runbooks-core.test.mjs
# Assert every copy of the brand mark IS the brand mark, by measuring the
# rendered pixels rather than trusting the generator that produced them. The
# rebrand was called done three times over surfaces still carrying the old
# artwork; each miss was a copy whose generator was correct and whose committed
# artefact was not. See `.settings/rebrand/README.md` § Verifying a brand change.
check-brand:
node scripts/check-brand-marks.mjs
node --test scripts/check-brand-marks-core.test.mjs
# The control plane inlines the app's globe + wordmark from a committed
# bundle. Nothing fails when it goes stale — the pages keep serving the
# previous build — so freshness has to be asserted rather than noticed.
node scripts/generate-control-plane-brand.mjs --check
# Re-vendor the Scalar renderer both API references are served with.
#
# Downloads the pinned @scalar/api-reference tarball, verifies the registry's
# own sha512 integrity digest, and extracts one file. Bumping the version also
# means bumping SCALAR_VERSION in crates/meridian-openapi/src/lib.rs — a unit
# test there fails if the two disagree.
vendor-scalar:
node scripts/vendor-scalar.mjs
# Assert the committed renderer is the pinned release, byte for byte.
#
# Not in `check`: it reaches the npm registry, and a check that fails on a
# flaky network teaches people to ignore it. Run it when bumping Scalar, and
# in any job that already has egress.
check-scalar:
node scripts/vendor-scalar.mjs --check
# Keep preview manifest metadata and the specs' authoritative gap ledgers in sync.
# Assert `just test-unit`'s two branches run the same package set.
#
# CI takes the cargo-nextest lane; a developer without it takes the fallback.
# A package added to one and not the other makes them run different test sets,
# and neither goes red — CI is green because it never ran the missing package,
# and the developer is green for the same reason (meridian-1pmm).
check-test-unit-parity:
node scripts/check-test-unit-parity.mjs
node --test scripts/check-test-unit-parity-core.test.mjs
check-feature-specs:
node scripts/check-feature-specs.mjs
node --test scripts/check-feature-specs-core.test.mjs
# `docs/mips/README.md` is the one MIP register. The desktop capability view
# reads a generated copy; this fails when the two drift. Pass `--write` to
# regenerate after editing the register.
check-mips *ARGS:
node scripts/check-mip-registry.mjs {{ARGS}}
node --test scripts/mip-registry-core.test.mjs
# The root contract forbids new `unwrap()`/`expect()` in production paths, and
# until now nothing read that sentence. A per-crate ratchet fails on growth, so
# a new panic has to be argued for in a commit message rather than merged in
# silence. Rows only move down; `--write` regenerates after a real reduction.
check-unwrap-budget *ARGS:
node scripts/check-unwrap-budget.mjs {{ARGS}}
node --test scripts/check-unwrap-budget-core.test.mjs
# ReductStore's Zenoh integration is two environment variables away from being
# something the architecture refuses, and both wrong values WORK — records land,
# queries answer, and nothing downstream notices. Pointing the subscriber or the
# queryable at `meridian/v1/**` buys a second durable log and a second read path
# in one string (DIAGRAM.md R2/R3, the arrival R9 names). Omitting `mode=client`
# inherits Zenoh's default of `peer`, so a storage box becomes a routing peer
# that can transit between communities (DIAGRAM.md R4, Law 2). Lands BEFORE any
# Zenoh wiring, which is why its own fixtures carry the evidence: nothing in
# tree sets RS_ZENOH_* yet, so a scan set that reached nothing would pass
# forever. Static, no Docker, no network.
check-reductstore-zenoh:
node --test scripts/check-reductstore-zenoh-core.test.mjs
node scripts/check-reductstore-zenoh.mjs
bash scripts/test-reductstore-zenoh-guard.sh
# npm half of deny.toml: pnpm audit findings need an ignore entry with a reason,
# and every licence expression from `pnpm licenses list` must sit on the SPDX
# allow-list (or a named package exception with a reason). Policy lives in
# scripts/npm-supply-chain.json. Reaches the registry for audit metadata.
check-npm-supply-chain:
node scripts/check-npm-supply-chain.mjs
node --test scripts/check-npm-supply-chain-core.test.mjs
# Every ignored relay integration binary must have an executable or reviewed lane.
check-relay-e2e-inventory:
node scripts/check-relay-e2e-inventory.mjs
node --test scripts/check-relay-e2e-inventory-core.test.mjs
# ArgoCD tracks chart versions, so a template edited without a `Chart.yaml`
# `version` bump deploys the PREVIOUS templates — silently, and remotely: the
# chart renders, helm-unittest passes, CI is green. Commit b6443a823 added an
# entire alert group at 0.1.13 exactly that way. The contract is differential
# but this repo's guards run on a working tree with no trustworthy baseline
# (`main` is pushed to directly and rebased, so a merge-base is not a release
# boundary), so the baseline is checked in: scripts/chart-version-manifest.json
# records each chart's rendered bytes against the version they were recorded at.
# After a legitimate bump, `just check-chart-version-bump --write`.
check-chart-version-bump *ARGS:
node scripts/check-chart-version-bump.mjs {{ARGS}}
node --test scripts/check-chart-version-bump-core.test.mjs
bash scripts/test-chart-version-bump-guard.sh
# Applied migrations are immutable. Pin exact bytes to the attested schema-26
# N-1 image so comments or branding edits cannot break SQLx rollout checksums.
check-frozen-migrations:
node scripts/check-frozen-migrations.mjs
node --test scripts/check-frozen-migrations-core.test.mjs
bash scripts/test-n-minus-one-candidate-contract.sh
# A product rename moves four persisted namespaces; Rust declares each chain
# once and derives its readers, but the shell scripts hardcode. That is how the
# Meridian rename moved the current entry in `instance-env.sh` and
# `reset-desktop-dev-state.sh` and dropped both middle generations while
# `reset.rs` stayed correct — and both failures are silent. Enforces
# prepend-never-replace across every copy.
check-legacy-namespaces:
node scripts/check-legacy-namespaces.mjs
node --test scripts/check-legacy-namespaces-core.test.mjs
# The user-facing tenancy unit is a Relay, and the product is Meridian. The
# retired words survive in identifiers, test IDs and CSS custom properties,
# where they are correct and must stay — so a grep cannot tell a stale screen
# from working code, and the rename kept being declared done with copy like
# "Add a community" still on screen. Checks only strings that reach a user.
check-relay-terminology:
node scripts/check-relay-terminology.mjs
node --test scripts/check-relay-terminology-core.test.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 . && pnpm check:file-sizes
# 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 REMAPPING/meridian-desktop/src-tauri/binaries
for bin in meridian-acp meridian-agent meridian-dev-mcp git-credential-nostr meridian; do
touch "REMAPPING/meridian-desktop/src-tauri/binaries/${bin}-${TARGET}"
done
# Ensure Docker dev services (Postgres, Dragonfly, etc.) are running and healthy
_ensure-services:
#!/usr/bin/env bash
set -euo pipefail
pg=$(docker inspect --format '{{"{{"}}.State.Health.Status{{"}}"}}' meridian-postgres 2>/dev/null || echo "not_found")
dragonfly=$(docker inspect --format '{{"{{"}}.State.Health.Status{{"}}"}}' meridian-dragonfly 2>/dev/null || echo "not_found")
if [[ "$pg" == "healthy" && "$dragonfly" == "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{{"}}"}}' meridian-postgres 2>/dev/null || echo "not_found")
dragonfly=$(docker inspect --format '{{"{{"}}.State.Health.Status{{"}}"}}' meridian-dragonfly 2>/dev/null || echo "not_found")
if [[ "$pg" == "healthy" && "$dragonfly" == "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
#!/usr/bin/env bash
set -euo pipefail
export PATH="{{justfile_directory()}}/bin:$PATH"
# dotenv-load already applied .env. Layer .env.local on top for the same
# reason `relay` does: when docker-compose.override.yml relocates the
# Postgres host port, only .env.local carries the matching DATABASE_URL.
# Without this the migrator dials .env's port, which on a machine running
# other stacks is a DIFFERENT database that happens to answer — it then
# fails with "migration N was previously applied but has been modified",
# which reads like a broken migration but is the checksum guard refusing to
# migrate someone else's data.
if [[ -f .env.local ]]; then set -a; . ./.env.local; set +a; fi
cargo run -p meridian-admin -- migrate
./scripts/seed-local-community.sh
# Run clippy on the desktop Tauri Rust crate
desktop-tauri-clippy: _ensure-sidecar-stubs
cargo clippy --manifest-path {{desktop_tauri_manifest}} --all-targets -- -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 REMAPPING/meridian-desktop/src-tauri && cargo test
# Verify compiled-flag behavior under both compile states (clean + internal).
# Runs the observer_archive focused test 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 REMAPPING/meridian-desktop/src-tauri
echo "=== Clean build (no flag) → expect false ==="
env -u MERIDIAN_BUILD_OBSERVER_ARCHIVE_DEFAULT \
-u MERIDIAN_BUILD_AUTO_CONNECT_DEFAULT_RELAY \
MERIDIAN_TEST_EXPECTED_OBSERVER_ARCHIVE_DEFAULT=false \
cargo test observer_archive_default_enabled_matches_expected -- --ignored --nocapture
env -u MERIDIAN_BUILD_AUTO_CONNECT_DEFAULT_RELAY \
MERIDIAN_TEST_EXPECTED_AUTO_CONNECT_DEFAULT_RELAY=false \
cargo test compiled_flag_matches_expected -- --ignored --nocapture
echo "=== Internal build (flags set) → expect true ==="
MERIDIAN_BUILD_OBSERVER_ARCHIVE_DEFAULT=1 \
MERIDIAN_TEST_EXPECTED_OBSERVER_ARCHIVE_DEFAULT=true \
cargo test observer_archive_default_enabled_matches_expected -- --ignored --nocapture
MERIDIAN_BUILD_AUTO_CONNECT_DEFAULT_RELAY=1 \
MERIDIAN_TEST_EXPECTED_AUTO_CONNECT_DEFAULT_RELAY=true \
cargo test compiled_flag_matches_expected -- --ignored --nocapture
echo "Both compiled states 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
export PATH="{{justfile_directory()}}/bin:$PATH"
TARGET={{target}}
# REAL sidecars, never `touch`ed stubs. Tauri's externalBin check only tests
# that the path exists, so zero-byte files build, bundle, install and launch
# cleanly — then the app dies the first time it shells out to an agent
# binary. Stubs are correct for check/clippy/test (`_ensure-sidecar-stubs`),
# and never correct for a bundle somebody will run.
cargo build --release --target "$TARGET" \
-p meridian-acp -p meridian-agent -p meridian-dev-mcp \
-p git-credential-nostr -p meridian-cli
./scripts/bundle-sidecars.sh "$TARGET"
# This recipe passes --features mesh-llm, so it also needs the native
# runtime the feature resolves at RUNTIME. Same contract as `just mesh=1
# dev`; see the recipe comment there.
export MESH_LLM_NATIVE_RUNTIME_CACHE_DIR="$(./scripts/ensure-mesh-native-runtime.sh)"
pnpm install
cd {{desktop_dir}} && pnpm tauri build --features mesh-llm --target {{target}}
# 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 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 web-e2e-smoke admin-web-e2e 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
if command -v cargo-nextest &>/dev/null; then
cargo nextest run -p meridian-core -p meridian-auth --lib
# Webhook execution and redaction are feature-gated in the reusable
# workflow crate; run the production feature explicitly so those tests
# cannot disappear from the default package selection.
cargo nextest run -p meridian-workflow --features reqwest
# meridian-db migrator/lint tests: pure SQL-parsing unit tests (no infra).
# They guard the embedded-migrator invariant (exactly the consolidated
# 0001; cutover/backfill stays an operator script, not startup state)
# and the tenant-scoping lints. The Postgres-backed meridian-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 meridian-db --lib
# Multi-tenant conformance gate (meridian-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 meridian-conformance
# Gateway unit and black-box HTTP tests are infra-free. Postgres-backed
# contract/race tests run in the `backend-integration` job of
# .github/workflows/ci.yml — not anywhere in this file. The comment used
# to say "the dedicated CI job below", which sends a reader looking
# through the Justfile for something that was never in it (meridian-1pmm).
cargo nextest run -p meridian-push-gateway
# Inter-pod QUIC mesh. Infra-free — iroh binds 127.0.0.1:0 and the two
# registry tests construct a Redis pool without dialing it. The crate
# ships in every relay image unconditionally (no Cargo feature gates it
# out), so these are shipped-code tests; before this line they ran in no
# recipe and no CI job. `run-tests.sh`'s `cargo test --test '*'` sweep
# does not reach them either — all of them are in-file `#[cfg(test)]`
# under src/, and that sweep selects integration targets only.
cargo nextest run -p meridian-relay-mesh
# Redis pub/sub fan-out — the mechanism that actually moves Nostr events
# between relay pods, and unconditional (unlike the mesh above). `--lib`
# runs the infra-free set; the 12 Dragonfly-backed tests are `#[ignore]`d,
# including the dedicated-subscriber fault drill, and still run in no gate
# (meridian-4z3.16). Wiring the runnable 27 is not that gate.
cargo nextest run -p meridian-pubsub --lib
# The row's primary crate, and until now the least-tested lane: 819
# infra-free lib tests that ran in no `just` recipe at all, while CI
# executed only two filtered selections of them. `--lib` is the
# infra-free set here by the same convention as meridian-db — the 10
# Postgres-backed api::media/api::admin tests and the nondeterministic
# api::mesh_demo round trip (meridian-4z3.12) are `#[ignore]`d, and the
# Postgres-backed ones run in the backend-integration CI job against a
# real database rather than ceasing to run.
cargo nextest run -p meridian-relay --lib
# The agent-facing CLI, and the same story one crate over: 271 infra-free
# tests that ran in NO `just` recipe and NO CI job — `ci.yml` does not
# mention meridian-cli at all. Two of them are `subcommand_names_are_stable`
# and `subcommand_counts_are_stable`, whose entire job is to make a CLI
# surface change deliberate; they had been failing since de4cfec61
# because nothing executed them to say so.
cargo nextest run -p meridian-cli
# Eight more crates whose src/ unit tests ran in NO recipe and NO CI job.
# Found by auditing every workspace member against the Justfile, ci.yml
# and run-tests.sh: `run_unit_tests` selects crates explicitly, and
# `run_integration_tests`'s `cargo test --test '*'` sweep only reaches
# tests/ targets — so #[cfg(test)] modules under src/ in an unnamed crate
# execute nowhere at all.
#
# meridian-sdk is the one that matters most: 241 tests over the event
# builders every client and the CLI construct events with, including
# build_set_canvas. All 508 are infra-free and finish in well under a
# second combined, so there is no argument for leaving them out.
#
# --lib by the same convention as meridian-db: meridian-media,
# meridian-audit and meridian-search each carry #[ignore]d
# infra-dependent tests that this does not and should not run.
cargo nextest run --lib \
-p meridian-sdk -p meridian-persona -p meridian-media \
-p meridian-audit -p meridian-openapi -p meridian-search \
-p meridian-ws-client -p meridian-test-db
else
./scripts/run-tests.sh unit
# Keep the fallback equivalent to the nextest lane: the reusable
# workflow crate's outbound execution/redaction tests are hidden unless
# the production HTTP feature is selected explicitly.
cargo test -p meridian-workflow --features reqwest
cargo test -p meridian-relay-mesh
cargo test -p meridian-pubsub --lib
cargo test -p meridian-relay --lib
cargo test -p meridian-cli
cargo test --lib \
-p meridian-sdk -p meridian-persona -p meridian-media \
-p meridian-audit -p meridian-openapi -p meridian-search \
-p meridian-ws-client -p meridian-test-db
fi
# Run integration tests only (starts services if needed)
test-integration:
./scripts/run-tests.sh integration
# The isolated-DB gate: the Postgres-backed meridian-db tests.
#
# These are the 157 `#[ignore = "requires Postgres"]` tests in meridian-db, and
# they are the entire safety argument for read-replica routing — every
# replica-fence and floor-guard test lives here
# (`created_at_floor_guard_aborts_old_channel_rows_at_commit`,
# `fence_probe_refuses_to_start_without_verified_floor_guard`,
# `channel_cursor_above_fence_stays_on_writer_preventing_middle_hole`, …).
#
# Before this recipe they ran in **no target at all**: `test-unit` runs
# `--lib` without `--ignored`, and `test`/`run-tests.sh integration` invokes
# `cargo test -p meridian-db` without it either. `run-tests.sh` said so itself
# and pointed at "a separate isolated-DB gate" that did not exist. This is that
# gate (meridian-4z3.16). Their existence read as coverage while nothing
# executed them, which is worse than not having them.
#
# `--test-threads=1` is required by what these tests do, not caution: the
# read-replica routing and fence-probe tests each CREATE and DROP their own
# scratch database and mutate singleton rows inside it, so they must not share.
#
# WALL-CLOCK, measured over fifteen full runs on an M3 Max against the compose
# Postgres (17.7, `fsync=on`, `shared_buffers=512MB`): 119–224s, all 157 tests
# executed every time. Teardown dominates — `DROP DATABASE` forces an immediate
# checkpoint, and sampling `pg_stat_activity` mid-run catches each one parked on
# `wait_event = CheckpointDone` for 2–12s — but that is seconds, not the "on the
# order of a minute" this comment previously recorded, and the selection is
# minutes rather than the ~2.5h on meridian-s6vo. An empty scratch database
# drops in 0.09–0.13s. Re-derive the number before quoting it; the earlier
# figure did not reproduce and nobody knows why it was 40× slower.
#
# A killed run leaks its scratch databases; `test-db-clean` drops them.
test-db: _ensure-migrations test-db-run
# The gate itself, with no service bootstrap.
#
# CI starts Postgres its own way and would fail on `_ensure-migrations`' docker
# compose path, so the selection lives here and both entry points share it. The
# alternative — a second copy of the cargo invocation in ci.yml — is the
# hand-synchronised shape meridian-1pmm already records against `test-unit`.
test-db-run:
#!/usr/bin/env bash
set -euo pipefail
export PATH="{{justfile_directory()}}/bin:$PATH"
# Same layering as _ensure-migrations, and for the same reason: when the
# override relocates the Postgres host port, only .env.local carries the
# matching DATABASE_URL. Without it we dial .env's port, which on a machine
# running several stacks is a different database that happens to answer.
if [[ -f .env.local ]]; then set -a; . ./.env.local; set +a; fi
# ONE name, no fallback. Every Postgres-backed test in the workspace now
# resolves through `meridian_test_db::disposable_url()` (34 call sites), so
# binding `MERIDIAN_TEST_DATABASE_URL` is sufficient and the old three-name
# workaround is gone — as is the hardcoded `localhost:5432` default, which
# on a multi-stack host was a neighbour's Postgres (meridian-8onc).
# THIS SUITE IS DESTRUCTIVE. Three of the migration tests call
# `reset_public_schema`, which is `DROP SCHEMA IF EXISTS public CASCADE`
# (crates/meridian-db/src/migration.rs:1275). The first version of this
# recipe defaulted to DATABASE_URL, so running it DROPPED THE DEVELOPER'S
# DEV DATABASE — observed: every community wiped, the desktop app left
# reporting "no community is configured for this host". A test suite that
# destroys the database you develop against must never be one env var's
# absence away from doing so.
#
# So the default is a SEPARATE, disposable database on the same server, and
# pointing this at the dev database requires saying so out loud.
dev_url="${DATABASE_URL:?DATABASE_URL is unset -- run 'just setup' or set it}"
default_test_url="$(printf '%s' "$dev_url" | sed -E 's#/[^/?]+(\?|$)#/meridian_test\1#')"
url="${MERIDIAN_TEST_DATABASE_URL:-${TEST_DATABASE_URL:-$default_test_url}}"
db_name="$(printf '%s' "$url" | sed -E 's#.*/([^/?]+)(\?.*)?$#\1#')"
dev_name="$(printf '%s' "$dev_url" | sed -E 's#.*/([^/?]+)(\?.*)?$#\1#')"
if [[ "$db_name" == "$dev_name" && "${MERIDIAN_TEST_DB_DESTROY_DEV:-}" != "1" ]]; then
echo "REFUSING: this suite runs DROP SCHEMA public CASCADE, and the target" >&2
echo " database (${db_name}) is the one DATABASE_URL develops against." >&2
echo " Point MERIDIAN_TEST_DATABASE_URL at a disposable database, or set" >&2
echo " MERIDIAN_TEST_DB_DESTROY_DEV=1 if you genuinely mean to wipe it." >&2
exit 1
fi
# Create the disposable database if it is not there yet. Do this through a
# server connection rather than `createdb "$url"` — createdb treats that URL
# as a database NAME, so it silently fails and the whole suite then dies on
# "database does not exist". CREATE DATABASE cannot run inside a
# transaction, hence the plain -c.
if ! psql "$dev_url" -tAc "SELECT 1 FROM pg_database WHERE datname = '${db_name}'" | grep -q 1; then
echo "Creating disposable test database ${db_name}"
psql "$dev_url" -c "CREATE DATABASE \"${db_name}\"" >/dev/null
fi
# DATABASE_URL still moves with it because `meridian-admin migrate` below
# reads it — but nothing in the TEST path does any more.
export MERIDIAN_TEST_DATABASE_URL="$url" DATABASE_URL="$url"
# Migrate the disposable database. `_ensure-migrations` above ran the
# migrator against the DEV database — that is its job and it must keep doing
# it — so without this the test target is an empty schema and 45 of the
# tests fail on missing tables. Measured, not predicted.
echo "Migrating ${db_name}"
cargo run -q -p meridian-admin -- migrate
# Stamp the disposability marker. This is the second half of the guard in
# `meridian-test-db`: the destructive tests refuse unless the database both
# is NAMED like a throwaway and CARRIES this row. A name alone is one
# environment variable away from pointing at real data, which is exactly how
# the dev database got dropped; the marker is what a real database will
# never have, because nothing but this line and `stamp_disposable` writes it.
psql "$url" -q -c "CREATE TABLE IF NOT EXISTS _meridian_disposable (
stamped_at timestamptz NOT NULL DEFAULT now(), note text NOT NULL)" >/dev/null
psql "$url" -q -c "INSERT INTO _meridian_disposable (note)
SELECT 'provisioned by just test-db'
WHERE NOT EXISTS (SELECT 1 FROM _meridian_disposable)" >/dev/null
# QUARANTINE — two tests are excluded BY NAME, and the exclusion is here
# rather than in ci.yml so the gate and a developer's box cannot disagree
# about what ran.
#
# Both are in `push::tests` and both are flaky against the shared database.
# Measured across nine full runs after the deterministic failures were
# repaired: 7 green, 2 red, and the two reds were these two tests. A gate
# that reds ~22% of the time on unrelated work teaches people to re-run it
# until it passes, which is worse than not having one — the same reason
# `check-scalar` is kept out of `check`.
#
# batch_claim_is_single_community_and_setwise_ops_honor_the_fence
# meridian-yuia. Failed twice, on two DIFFERENT assertions. Not pinned.
# exhausted_match_job_is_reaped_and_cannot_pin_retention
# meridian-woau. 1 failure / 30 isolated runs. The row it wants can
# only come from the lease-activation backfill, so this may be a
# PRODUCT race that drops a push wake — do not "fix" it by retrying.
#
# This is quarantine, not deletion, and it is the thing to delete first when
# either bead closes. `MERIDIAN_TEST_DB_ALL=1` runs the full 157 — use it
# when working those beads, and to re-measure the flake rate.
skips=(--skip push::tests::batch_claim_is_single_community_and_setwise_ops_honor_the_fence
--skip push::tests::exhausted_match_job_is_reaped_and_cannot_pin_retention)
if [[ "${MERIDIAN_TEST_DB_ALL:-0}" == "1" ]]; then
skips=()
echo "MERIDIAN_TEST_DB_ALL=1 — running the quarantined tests too (meridian-yuia, meridian-woau)"
else
echo "2 tests quarantined (meridian-yuia, meridian-woau); MERIDIAN_TEST_DB_ALL=1 to include them"
fi
echo "Isolated-DB gate against ${url}"
# `${skips[@]+...}` — macOS ships bash 3.2, where an empty array under
# `set -u` is an unbound-variable error rather than an empty expansion.
cargo test -p meridian-db --lib -- --ignored --test-threads=1 ${skips[@]+"${skips[@]}"}
# Drop scratch databases left behind by an interrupted `test-db` run.
#
# The routing and replica-fence tests create a database per test and drop it on
# the way out, so a killed or timed-out run leaks them. They are harmless but
# they accumulate, and a full disk is a confusing way to find that out.
test-db-clean:
#!/usr/bin/env bash
set -euo pipefail
# To point this sweep somewhere other than the dev database, set
# MERIDIAN_TEST_DATABASE_URL — NOT DATABASE_URL.
#
# `set -a; . ./.env.local` overwrites names that are already exported, so a
# caller's DATABASE_URL does not survive to the line below. That is not a
# bug to patch by capturing the value first: `just`'s dotenv-load has
# already applied `.env` by the time this recipe starts, so "the value
# before sourcing" is `.env`'s, not the caller's, and preferring it makes
# `.env` beat `.env.local` — inverting the documented order and pointing a
# recipe that DROPS DATABASES at whatever Postgres `.env` names, which on a
# multi-stack host is a neighbour's. That patch was tried and reverted.
#
# MERIDIAN_TEST_DATABASE_URL works because neither env file defines it, so
# nothing overwrites it. Same reason `test-db-run` binds that one name and
# no fallback.
if [[ -f .env.local ]]; then set -a; . ./.env.local; set +a; fi
url="${MERIDIAN_TEST_DATABASE_URL:-${TEST_DATABASE_URL:-${DATABASE_URL:?DATABASE_URL is unset}}}"
# Every scratch database is stamped `mdbtest_` by
# crates/meridian-db/src/test_scratch.rs. Anchor on that literal: this sweep
# runs against a developer's Postgres, which on a multi-stack host also holds
# neighbouring projects' databases, and a false positive here drops data.
#
# The prefix is the whole contract, and it is checked from the other side —
# `test_db_clean_sweeps_the_prefix_this_module_stamps` reads this line and
# fails if the two disagree. The previous shape-matching regex was not
# checked from anywhere and missed 13 of 32 prefixes, including every
# `fence_probe_*`, while still reporting success (meridian-5n3l).
#
# No `mapfile` — macOS ships bash 3.2 and this recipe runs on developer boxes.
count=0
while IFS= read -r db; do
[[ -z "$db" ]] && continue
echo "Dropping ${db}"
psql "$url" -c "DROP DATABASE IF EXISTS \"${db}\" WITH (FORCE)" >/dev/null
count=$((count + 1))
done < <(psql "$url" -tAc \
"select datname from pg_database where left(datname, 8) = 'mdbtest_'")
if [[ $count -eq 0 ]]; then echo "No leaked scratch databases."; else
echo "Dropped ${count} scratch database(s)."
fi
# The Redis-backed half of the infra gate: meridian-pubsub's #[ignore]d tests.
#
# Twelve tests across lib.rs (5), presence.rs (3) and nip98_replay.rs (4) — the
# fan-out, presence and NIP-98 replay paths — ran in no `just` target and no CI
# job, the other half of meridian-4z3.16. `test-unit` runs `-p meridian-pubsub
# --lib` without `--ignored`, so it covers the 27 infra-free ones and none of
# these.
#
# REDIS_URL is bound explicitly and verified with a PING before anything runs.
# The tests otherwise fall back to a hardcoded 127.0.0.1:6379, which on a
# multi-stack host is a neighbour's Redis — it answers, so the tests would pass
# against someone else's datastore while writing presence keys into it
# (meridian-7s3h is the same defect in meridian-relay). Unlike the Postgres
# gate this one is NOT destructive: no DROP, no schema reset.
# Split like `test-db` / `test-db-run`: the wrapper starts the local stack, the
# `-run` half is what CI invokes, so the selection lives in ONE place and a CI
# job and a developer's box cannot drift apart.
# The 53 `#[ignore]`d meridian-relay tests, which are the row's PRIMARY crate.
# Re-derive this rather than trusting it: FOUR authors have now written 46, 47,
# 49 and 51 beside the same selection, most recently while the tree held 53 —
# a9b30ea27 added two Postgres-gated tenancy tests and left the number behind. `grep -rc '#\[ignore'
# crates/meridian-relay/src/` overcounts — nine are the token inside comment
# prose — so count what the binary lists:
# cargo test -p meridian-relay --lib -- --ignored --list | grep -c ': test'
#
# Before this recipe, 27 of the crate's 48 ran in NO recipe and NO CI job: CI
# reached only `api::invites`, `api::media` and `api::admin` through two
# filtered nextest selections in backend-integration. Ungated were
# `api::operator` (12), `api::bridge` (6), `api::git::transport`'s
# `sec005_read_gate_tests` (4), `api::git::policy`, `api::git::access` and
# `workflow_sink` — a SECURITY READ GATE among them, executing nowhere
# (meridian-wxqw).
#
# They were also unrunnable-by-accident rather than merely unrun: `operator` and
# `bridge` hardcoded `localhost:5432` and returned `Option`, so every caller
# wrote `let Some(state) = ... else { return; }` and a missing database made all
# 12 operator tests report PASS having asserted nothing. Both now resolve
# through `meridian_test_db::disposable_url()` and fail loudly. Measured: with
# MERIDIAN_TEST_DATABASE_URL unset the group goes 0 passed / 12 FAILED, where it
# previously went 12 passed.
#
# NOT destructive — no DROP and no schema reset — but it writes communities, so
# it still targets the disposable database rather than the one you develop against.
test-relay-db: _ensure-services test-relay-db-run
# The gate itself, with no service bootstrap. Same split as `test-db` and
# `test-pubsub`, for the same reason: CI invokes the `-run` half, so the
# selection lives in one place and cannot drift from a developer's box.
test-relay-db-run:
#!/usr/bin/env bash
set -euo pipefail
export PATH="{{justfile_directory()}}/bin:$PATH"
# `.env.local` must win over `.env` — see `test-pubsub-run` for why that is
# load-bearing here and not cosmetic.
# `dev_url` is deliberately the LAYERED value, not the caller's: it is the
# database this checkout develops against, and the REFUSING guard below
# compares the test target to it. Preferring a caller's DATABASE_URL here
# would let someone move the goalposts of the very guard meant to stop them
# destroying their dev data. Override the TEST target with
# MERIDIAN_TEST_DATABASE_URL instead — see `test-db-clean` for why capturing
# a "caller value" before sourcing does not mean what it looks like.
if [[ -f .env.local ]]; then set -a; . ./.env.local; set +a; fi
dev_url="${DATABASE_URL:?DATABASE_URL is unset -- run 'just setup' or set it}"
default_test_url="$(printf '%s' "$dev_url" | sed -E 's#/[^/?]+(\?|$)#/meridian_test\1#')"
url="${MERIDIAN_TEST_DATABASE_URL:-$default_test_url}"
db_name="$(printf '%s' "$url" | sed -E 's#.*/([^/?]+)(\?.*)?$#\1#')"
dev_name="$(printf '%s' "$dev_url" | sed -E 's#.*/([^/?]+)(\?.*)?$#\1#')"
if [[ "$db_name" == "$dev_name" && "${MERIDIAN_TEST_DB_DESTROY_DEV:-}" != "1" ]]; then
echo "REFUSING: ${db_name} is the database DATABASE_URL develops against." >&2
echo " These tests write communities into their target. Point" >&2
echo " MERIDIAN_TEST_DATABASE_URL at a disposable database." >&2
exit 1
fi
if ! psql "$dev_url" -tAc "SELECT 1 FROM pg_database WHERE datname = '${db_name}'" | grep -q 1; then
echo "Creating disposable test database ${db_name}"
psql "$dev_url" -c "CREATE DATABASE \"${db_name}\"" >/dev/null
fi
export MERIDIAN_TEST_DATABASE_URL="$url" DATABASE_URL="$url"
echo "Migrating ${db_name}"
cargo run -q -p meridian-admin -- migrate
# Same override pattern as `test-pubsub-run`: MERIDIAN_TEST_REDIS_URL, not
# REDIS_URL — `.env` defines REDIS_URL and dotenv-load has already applied it.
REDIS_URL="${MERIDIAN_TEST_REDIS_URL:-${REDIS_URL:-}}"
export REDIS_URL
: "${REDIS_URL:?REDIS_URL is unset -- run 'just setup' or set it}"
# `api::mesh_demo` — the repo's only end-to-end cross-pod round trip — was
# quarantined out of this selection while it failed ~60% of runs. The cause
# was a real product defect, not infrastructure: `run_demo_echo`'s drain tick
# cancelled a non-cancel-safe `recv_validated` and ate the frame. Fixed, and
# measured 20/20 green (meridian-4z3.12), so it is back in the gate. There is
# no skip list here any more; keep it that way — a selection with an
# exclusion is one commit away from excluding the thing it was built to run.
echo "Relay DB/Redis gate against ${db_name} and ${REDIS_URL}"
cargo test -p meridian-relay --lib -- --ignored --test-threads=1
# Cross-pod mesh round trip against TWO LIVE RELAY PROCESSES.
#
# The `live`-gate evidence for G03: `api::mesh_demo` covers the same round trip
# with both endpoints inside one process, so it proves the transport and the
# fence and says nothing about whether two relays DISCOVER each other through
# the shared Redis registry and route a session between them. Green CI is not
# evidence here — that is the root Design Law, not a preference.
#
# Deliberately not part of `just ci`: it boots two relays against the local
# stack, which a lint gate has no business doing. Run it when the claim matters.
mesh-probe: _ensure-services
./scripts/mesh-cross-pod-probe.sh
test-pubsub: _ensure-services test-pubsub-run
test-pubsub-run:
#!/usr/bin/env bash
set -euo pipefail
export PATH="{{justfile_directory()}}/bin:$PATH"
# `.env.local` MUST win over `.env` — that is the documented precedence
# (root AGENTS.md) and here it is load-bearing, not cosmetic: `.env` carries
# `REDIS_URL=redis://localhost:6390`, which on this host is a NEIGHBOURING
# stack's Redis. It answers PING, so a run that honours it passes while
# writing presence keys into someone else's datastore — observed, not
# theorised.
#
# An earlier version of this recipe tried to let "the caller" override the
# file by capturing REDIS_URL before sourcing. That cannot work: `just`'s
# `dotenv-load` has already put `.env`'s value in the environment by the
# time the body runs, so a `.env` value and a real shell export are
# indistinguishable — and the capture handed the run straight back to 6390.
# Override the target with MERIDIAN_TEST_REDIS_URL — not REDIS_URL.
# MERIDIAN_TEST_REDIS_URL works because no env file defines it, so nothing
# overwrites it after `.env.local` is sourced.
if [[ -f .env.local ]]; then set -a; . ./.env.local; set +a; fi
REDIS_URL="${MERIDIAN_TEST_REDIS_URL:-${REDIS_URL:-}}"
export REDIS_URL
: "${REDIS_URL:?REDIS_URL is unset -- run 'just setup' or set it}"
# Pre-flight PING, but only when redis-cli exists: it is NOT in hermit's
# bin/, so a CI runner may not have it, and a missing diagnostic tool must
# not fail a gate that would otherwise pass. When it is absent the tests
# themselves surface an unreachable Redis — just less legibly.
if command -v redis-cli >/dev/null 2>&1; then
if ! redis-cli -u "$REDIS_URL" PING >/dev/null 2>&1; then
echo "REFUSING: ${REDIS_URL} did not answer PING." >&2
echo " These tests fall back to 127.0.0.1:6379 when unset, which on this" >&2
echo " host is usually another project's Redis. Start the stack first." >&2
exit 1
fi
fi
echo "Redis-backed pubsub gate against ${REDIS_URL}"
cargo test -p meridian-pubsub --lib -- --ignored --test-threads=1
# The canvas gate: kind 40100 read/write against a live relay.
#
# `crates/meridian-test-client/tests/e2e_canvas.rs` pins the rules that
# `meridian-acp/src/pool.rs` and the desktop's `canvasSync.ts` each implement
# separately — newest-wins, blank-means-cleared with no fallback, and the
# member/open-visibility split on writes. Two codebases agreeing by convention
# is not the same as a relay agreeing with both.
#
# Like `test-db`, this exists because the tests otherwise ran in NO target:
# `run-tests.sh integration` invokes `cargo test --test '*'` WITHOUT
# `--ignored`, so every `#[ignore]`d e2e case is silently skipped there. The
# whole `e2e_*` family has that problem; this recipe fixes it for one suite
# rather than turning on suites with known-failing cases (meridian-swcq,
# meridian-57j) and shipping a recipe that is red on arrival.
#
# It FAILS rather than skips when no relay is reachable. A gate that quietly
# passes when its subject is absent is the defect it was written to remove.
test-canvas:
#!/usr/bin/env bash
set -euo pipefail
export PATH="{{justfile_directory()}}/bin:$PATH"
# Override the target with MERIDIAN_TEST_RELAY_URL — not RELAY_URL.
#
# `.env` defines RELAY_URL and `just`'s dotenv-load applies it before this
# recipe starts, so a caller's RELAY_URL is indistinguishable from `.env`'s
# and cannot be preferred. Trying to (capture it before sourcing
# `.env.local`) makes `.env` beat `.env.local`, and this gate then looked
# for a relay on `.env`'s 3000 while the local one ran on `.env.local`'s
# 3002 — failing with "no relay reachable" on a machine where a relay was
# running. That patch was tried and reverted.
#
# MERIDIAN_TEST_RELAY_URL works because no env file defines it, so nothing
# overwrites it. Same reason `test-db-run` binds MERIDIAN_TEST_DATABASE_URL
# and no fallback.
if [[ -f .env.local ]]; then set -a; . ./.env.local; set +a; fi
url="${MERIDIAN_TEST_RELAY_URL:-${MERIDIAN_RELAY_URL:-${RELAY_URL:-ws://localhost:3000}}}"
host_port="${url#*://}"; host_port="${host_port%%/*}"
host="${host_port%%:*}"; port="${host_port##*:}"
[[ "$port" == "$host" ]] && port=80
if ! (exec 3<>"/dev/tcp/${host}/${port}") 2>/dev/null; then
echo "error: no relay reachable at ${url}" >&2
echo " start one with 'just relay', or set RELAY_URL to a running one." >&2
exit 1
fi
echo "canvas gate against ${url}"
RELAY_URL="$url" cargo test -p meridian-test-client --test e2e_canvas \
-- --ignored --test-threads=1
# Deterministically sever each dedicated Redis subscriber while pooled PING
# remains healthy, then prove fail-closed drain, ACL/ban freshness, and recovery.
subscriber-fault-matrix:
./scripts/run-subscriber-fault-matrix.sh
# Freeze the production user-visible capacity profile and evidence manifest.
capacity-contract-check:
node scripts/check-relay-capacity-contract.mjs
# Dump the local relay DB, restore into an isolated disposable DB, and boot the
# current relay with migration disabled before comparing durable inventories.
relay-postgres-restore-rehearsal:
./scripts/rehearse-relay-postgres-restore.sh
# Exact pinned N-1 / current binary quiesced rollout, rollback, and restore.
n-minus-one-rollout output:
N_MINUS_ONE_ALLOW_DISPOSABLE=1 ./scripts/run-n-minus-one-rollout.sh {{output}}
# Meridian 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 Meridian, 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 MERIDIAN_PRIVATE_KEY="3dbaebadb5dfd777ff25149ee230d907a15a9e1294b40b830661e65bb42f6c03"
export MERIDIAN_REQUIRE_RELAY_MEMBERSHIP=true
export MERIDIAN_ALLOW_NIP_OA_AUTH=true
export RELAY_OWNER_PUBKEY="e5ebc6cdb579be112e336cc319b5989b4bb6af11786ea90dbe52b5f08d741b34"
export MERIDIAN_RELAY_PRIVATE_KEY="0000000000000000000000000000000000000000000000000000000000000001"
export MERIDIAN_RECONCILE_CHANNELS=true
export MERIDIAN_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
export MESH_LLM_NATIVE_RUNTIME_CACHE_DIR="$(./scripts/ensure-mesh-native-runtime.sh)"
cargo run -p meridian-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 Meridian Keychain.
mesh-e2e-admission:
#!/usr/bin/env bash
set -euo pipefail
export MESH_LLM_NATIVE_RUNTIME_CACHE_DIR="$(./scripts/ensure-mesh-native-runtime.sh)"
cargo run -p meridian-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
export MESH_LLM_NATIVE_RUNTIME_CACHE_DIR="$(./scripts/ensure-mesh-native-runtime.sh)"
cargo build --release -p meridian-agent -p meridian-dev-mcp
cargo run -p meridian-relay --example mesh_serve_client_smoke
cargo run -p meridian-relay --example mesh_admission_smoke
cargo run -p meridian-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"
# dotenv-load already applied .env. Layer .env.local on top so a relocated
# REDIS_URL/port here matches what Compose published.
if [[ -f .env.local ]]; then set -a; . ./.env.local; set +a; fi
cargo run -p meridian-relay
# Start the hosted-community control plane (local replacement for the hosted one)
#
# Generates a relay-operator keypair on first run and stores it under
# .control-plane/. The relay must list that pubkey in RELAY_OPERATOR_PUBKEYS or
# every provisioning call is rejected, so the recipe prints the exact lines to
# add and refuses to start until they are present.
control-plane: bootstrap
#!/usr/bin/env bash
set -euo pipefail
export PATH="{{justfile_directory()}}/bin:$PATH"
cd {{justfile_directory()}}
# dotenv-load already applied .env; layer .env.local on top (see `relay`).
if [[ -f .env.local ]]; then set -a; . ./.env.local; set +a; fi
key_dir=".control-plane"
key_file="$key_dir/operator.key"
mkdir -p "$key_dir"
if [[ ! -f "$key_file" ]]; then
# 32 bytes of hex from the same CSPRNG the OS gives everything else.
openssl rand -hex 32 > "$key_file"
chmod 600 "$key_file"
echo "Generated a new relay-operator key at $key_file"
fi
secret="$(tr -d '[:space:]' < "$key_file")"
pubkey="$(MERIDIAN_CONTROL_RELAY_OPERATOR_SECRET_KEY="$secret" \
cargo run --quiet -p meridian-control-plane -- --print-operator-pubkey)"
if [[ -z "$pubkey" ]]; then
echo "error: could not derive the operator pubkey from $key_file" >&2
exit 1
fi
# `.env` is the single source both processes read; the relay half must be
# present before this service can provision anything.
if ! grep -q "^RELAY_OPERATOR_PUBKEYS=.*$pubkey" .env 2>/dev/null; then
cat >&2 <<EOF
The relay does not yet trust this control plane's operator key.
Add these two lines to .env, restart \`just relay\`, then re-run:
RELAY_OPERATOR_PUBKEYS=$pubkey
RELAY_OPERATOR_API_ORIGIN=http://127.0.0.1:3000
EOF
exit 1
fi
export MERIDIAN_CONTROL_DATABASE_URL="${MERIDIAN_CONTROL_DATABASE_URL:-postgres://meridian:meridian_dev@localhost:5432/meridian_control_plane}"
export MERIDIAN_CONTROL_PUBLIC_ORIGIN="${MERIDIAN_CONTROL_PUBLIC_ORIGIN:-http://127.0.0.1:8090}"
export MERIDIAN_CONTROL_RELAY_OPERATOR_API_ORIGIN="${MERIDIAN_CONTROL_RELAY_OPERATOR_API_ORIGIN:-http://127.0.0.1:3000}"
export MERIDIAN_CONTROL_RELAY_OPERATOR_SECRET_KEY="$secret"
export MERIDIAN_CONTROL_COMMUNITY_HOST_SUFFIX="${MERIDIAN_CONTROL_COMMUNITY_HOST_SUFFIX:-relays.meridian.localtest.me}"
export MERIDIAN_CONTROL_COMMUNITY_HOST_PORT="${MERIDIAN_CONTROL_COMMUNITY_HOST_PORT:-3000}"
export MERIDIAN_CONTROL_RELAY_SCHEME="${MERIDIAN_CONTROL_RELAY_SCHEME:-ws}"
export MERIDIAN_CONTROL_IDENTITY_PROVIDER="${MERIDIAN_CONTROL_IDENTITY_PROVIDER:-dev}"
export MERIDIAN_CONTROL_ALLOW_DEV_LOGIN="${MERIDIAN_CONTROL_ALLOW_DEV_LOGIN:-1}"
export MERIDIAN_CONTROL_RUNTIME_DATABASE_ROLE="${MERIDIAN_CONTROL_RUNTIME_DATABASE_ROLE:-meridian}"
cargo run -p meridian-control-plane -- --migrate-only
# Report the ports actually in effect. These were literals until ./run.sh
# started relocating the control plane off a busy 8090 — a hardcoded banner
# then sends you to a port nothing is listening on and reads like a crash.
base_path="${MERIDIAN_CONTROL_API_BASE_PATH:-/api/meridian}"
origin="${MERIDIAN_CONTROL_PUBLIC_ORIGIN:-http://127.0.0.1:8090}"
echo ""
echo "Control plane: ${origin}${base_path}"
echo "Operator key: $pubkey"
echo "Communities: <name>.${MERIDIAN_CONTROL_COMMUNITY_HOST_SUFFIX}:${MERIDIAN_CONTROL_COMMUNITY_HOST_PORT}"
echo ""
echo "Point the desktop app at it:"
echo " MERIDIAN_CONTROL_PLANE_URL=${origin}${base_path} \\"
echo " MERIDIAN_CONTROL_PLANE_ORIGIN=${origin} just dev"
echo ""
echo " (./run.sh desktop --attach exports both for you)"
echo ""
cargo run -p meridian-control-plane
# 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"
[[ -d node_modules ]] || pnpm install
pnpm -C REMAPPING/meridian-web build
MERIDIAN_WEB_DIR=./REMAPPING/meridian-web/dist cargo run -p meridian-relay
# Build and run the private read-only admin dashboard
admin: bootstrap _ensure-migrations
#!/usr/bin/env bash
set -euo pipefail
export PATH="{{justfile_directory()}}/bin:$PATH"
[[ -d node_modules ]] || pnpm install
pnpm -C REMAPPING/meridian-admin-web build
export MERIDIAN_ADMIN_HOST="${MERIDIAN_ADMIN_HOST:-admin.localhost:3000}"
export MERIDIAN_ADMIN_WEB_DIR="${MERIDIAN_ADMIN_WEB_DIR:-{{justfile_directory()}}/REMAPPING/meridian-admin-web/dist}"
echo "Admin dashboard: http://${MERIDIAN_ADMIN_HOST}/reports"
cargo run -p meridian-relay
# Seed deterministic reports and product feedback for local admin dashboard review
admin-seed: _ensure-migrations
./scripts/seed-admin-dashboard.sh
# Build admin-web and run its browser suite — the operator console's only tests.
# The build is not optional and must not be split from the run: the Playwright
# config serves `REMAPPING/meridian-admin-web/dist` through `vite preview`, and `dist/` is
# gitignored, so on a clean checkout a bare `playwright test` dies with
# "The directory \"dist\" does not exist" (F087). `pnpm test:e2e` is
# `pnpm build && playwright test`, which is why this delegates rather than
# calling playwright directly.
admin-web-e2e:
pnpm -C REMAPPING/meridian-admin-web test:e2e
# Run focused relay and browser checks for the read-only admin dashboard
admin-check: fmt-check admin-web-e2e
cargo check -p meridian-relay --all-targets
cargo test -p meridian-relay api::admin
cargo test -p meridian-relay router::tests
pnpm -C REMAPPING/meridian-admin-web check
# Start the relay server in release mode
relay-release: _ensure-migrations
cargo run -p meridian-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"
bind_addr="${MERIDIAN_BIND_ADDR:-0.0.0.0:3000}"
relay_port="${bind_addr##*:}"; [[ -n "$relay_port" ]] || relay_port=3000
health_port="${MERIDIAN_HEALTH_PORT:-8080}"
metrics_port="${MERIDIAN_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 meridian-relay) and rerun: just dev" >&2
exit 1
fi
done
fi
cargo build -p meridian-acp -p meridian-agent -p meridian-dev-mcp -p meridian-cli -p git-credential-nostr -p meridian-relay
if [[ -n "{{mesh}}" ]]; then
export MESH_LLM_NATIVE_RUNTIME_CACHE_DIR="$(./scripts/ensure-mesh-native-runtime.sh)"
fi
# 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 MERIDIAN_GIT_PROBE_WRITERS="${MERIDIAN_GIT_PROBE_WRITERS:-8}"
export MERIDIAN_GIT_PROBE_ROUNDS="${MERIDIAN_GIT_PROBE_ROUNDS:-2}"
./target/debug/meridian-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: meridian-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: meridian-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.MERIDIAN_TAURI_CONFIG).identifier)")
echo "Starting on Vite port ${MERIDIAN_VITE_PORT}, relay ${MERIDIAN_RELAY_URL}"
FEATURES=(); [[ -n "{{mesh}}" ]] && FEATURES=(--features mesh-llm)
if [[ "$(uname -s)" == "Darwin" && -n "${MERIDIAN_DEV_SIGN_IDENTITY:-}" ]]; then
# Stable dev code signing: OS grants survive rebuilds. See REMAPPING/meridian-desktop/src-tauri/AGENTS.md.
export "CARGO_TARGET_$(rustc -vV | sed -n 's|host: ||p' | tr 'a-z-' 'A-Z_')_RUNNER={{justfile_directory()}}/REMAPPING/meridian-desktop/src-tauri/scripts/macos-dev-sign-runner.sh"
fi
pnpm exec tauri dev ${FEATURES[@]+"${FEATURES[@]}"} --config "$MERIDIAN_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 meridian-acp -p meridian-agent -p meridian-dev-mcp -p meridian-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 meridian-acp meridian-agent meridian-dev-mcp git-credential-nostr meridian; do
cp "${TARGET_DIR}/debug/${bin}" "REMAPPING/meridian-desktop/src-tauri/binaries/${bin}-${TARGET}"
chmod +x "REMAPPING/meridian-desktop/src-tauri/binaries/${bin}-${TARGET}"
done
cd {{desktop_dir}}
[[ -d node_modules ]] || pnpm install
unset MERIDIAN_PRIVATE_KEY MERIDIAN_SHARE_IDENTITY
if [[ -n "{{fresh}}" ]]; then
export MERIDIAN_RESET_WEBVIEW_STATE=1
fi
source ../scripts/instance-env.sh
INSTANCE_ID=$(node -e "console.log(JSON.parse(process.env.MERIDIAN_TAURI_CONFIG).identifier)")
export MERIDIAN_DEV_KEYRING_SERVICE="meridian-desktop-dev.${MERIDIAN_INSTANCE_SLUG:-main}"
if [[ -n "{{fresh}}" ]]; then
../scripts/reset-desktop-standalone-state.sh "$INSTANCE_ID" "$MERIDIAN_DEV_KEYRING_SERVICE"
fi
trap '../scripts/cleanup-instance-agents.sh "$INSTANCE_ID" || true' EXIT
echo "Starting standalone desktop on Vite port ${MERIDIAN_VITE_PORT}; no relay services were started"
if [[ "$(uname -s)" == "Darwin" && -n "${MERIDIAN_DEV_SIGN_IDENTITY:-}" ]]; then
# Stable dev code signing: OS grants survive rebuilds. See REMAPPING/meridian-desktop/src-tauri/AGENTS.md.
export "CARGO_TARGET_$(rustc -vV | sed -n 's|host: ||p' | tr 'a-z-' 'A-Z_')_RUNNER={{justfile_directory()}}/REMAPPING/meridian-desktop/src-tauri/scripts/macos-dev-sign-runner.sh"
fi
pnpm exec tauri dev --config "$MERIDIAN_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 meridian-acp -p meridian-agent -p meridian-dev-mcp -p meridian-cli -p git-credential-nostr
FEATURES=()
if [[ -n "{{mesh}}" ]]; then
FEATURES=(--features mesh-llm)
export MESH_LLM_NATIVE_RUNTIME_CACHE_DIR="$(./scripts/ensure-mesh-native-runtime.sh)"
fi
# Replace the 0-byte sidecar stub with the real CLI binary so tauri dev picks it up.
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")
cp "${TARGET_DIR}/release/meridian" "REMAPPING/meridian-desktop/src-tauri/binaries/meridian-${TARGET}"
chmod +x "REMAPPING/meridian-desktop/src-tauri/binaries/meridian-${TARGET}"
cd {{desktop_dir}}
export MERIDIAN_RELAY_URL="wss://relay.meridian.r2d2.office.ilab.zone"
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.MERIDIAN_TAURI_CONFIG).identifier)")
trap '../scripts/cleanup-instance-agents.sh "$INSTANCE_ID" || true' EXIT
echo "Starting staging on Vite port ${MERIDIAN_VITE_PORT}, relay ${MERIDIAN_RELAY_URL}"
if [[ "$(uname -s)" == "Darwin" && -n "${MERIDIAN_DEV_SIGN_IDENTITY:-}" ]]; then
# Stable dev code signing: OS grants survive rebuilds. See REMAPPING/meridian-desktop/src-tauri/AGENTS.md.
export "CARGO_TARGET_$(rustc -vV | sed -n 's|host: ||p' | tr 'a-z-' 'A-Z_')_RUNNER={{justfile_directory()}}/REMAPPING/meridian-desktop/src-tauri/scripts/macos-dev-sign-runner.sh"
fi
pnpm exec tauri dev ${FEATURES[@]+"${FEATURES[@]}"} --config "$MERIDIAN_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 meridian-acp -p meridian-agent -p meridian-dev-mcp -p meridian-cli -p git-credential-nostr
FEATURES=()
if [[ -n "{{mesh}}" ]]; then
FEATURES=(--features mesh-llm)
export MESH_LLM_NATIVE_RUNTIME_CACHE_DIR="$(./scripts/ensure-mesh-native-runtime.sh)"
fi
# Replace the 0-byte sidecar stub with the real CLI binary so tauri dev picks it up.
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")
cp "${TARGET_DIR}/release/meridian" "REMAPPING/meridian-desktop/src-tauri/binaries/meridian-${TARGET}"
chmod +x "REMAPPING/meridian-desktop/src-tauri/binaries/meridian-${TARGET}"
cd {{desktop_dir}}
export MERIDIAN_RELAY_URL="wss://meridian.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.MERIDIAN_TAURI_CONFIG).identifier)")
trap '../scripts/cleanup-instance-agents.sh "$INSTANCE_ID" || true' EXIT
echo "Starting production on Vite port ${MERIDIAN_VITE_PORT}, relay ${MERIDIAN_RELAY_URL}"
if [[ "$(uname -s)" == "Darwin" && -n "${MERIDIAN_DEV_SIGN_IDENTITY:-}" ]]; then
# Stable dev code signing: OS grants survive rebuilds. See REMAPPING/meridian-desktop/src-tauri/AGENTS.md.
export "CARGO_TARGET_$(rustc -vV | sed -n 's|host: ||p' | tr 'a-z-' 'A-Z_')_RUNNER={{justfile_directory()}}/REMAPPING/meridian-desktop/src-tauri/scripts/macos-dev-sign-runner.sh"
fi
pnpm exec tauri dev ${FEATURES[@]+"${FEATURES[@]}"} --config "$MERIDIAN_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 ${MERIDIAN_VITE_PORT}, relay ${MERIDIAN_RELAY_URL}"
pnpm exec vite --port "${MERIDIAN_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=$((MERIDIAN_VITE_PORT + 100))
export VITE_RELAY_URL="${MERIDIAN_RELAY_URL}"
echo "Starting web dev server on port ${VITE_PORT}, relay ${MERIDIAN_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 . && pnpm check:file-sizes
# 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 := "REMAPPING/meridian-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 && node ./scripts/check-file-sizes.mjs
# Run mobile tests
mobile-test:
unset GIT_DIR GIT_WORK_TREE; cd {{mobile_dir}} && flutter test
# Compile an unsigned Android debug APK (worktree-aware debug identity)
mobile-build-android:
./scripts/mobile-worktree-overrides.sh
unset GIT_DIR GIT_WORK_TREE; cd {{mobile_dir}} && flutter build apk --debug --no-pub
# 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
cd {{mobile_dir}}
unset GIT_DIR GIT_WORK_TREE
flutter run
# Uninstall stale worktree-suffixed Meridian 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 REMAPPING/meridian-desktop/src-tauri/Cargo.toml
# Reclaim incremental-cache disk in both target trees without a cold rebuild
target-sweep *ARGS:
./scripts/target-sweep.sh {{ARGS}}
# 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('./REMAPPING/meridian-desktop/package.json').version"
# Read the current relay version from its crate manifest
get-current-relay-version:
@grep -m1 '^version = ' crates/meridian-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
# REMAPPING/meridian-desktop/package.json
(cd REMAPPING/meridian-desktop && npm pkg set "version={{ version }}")
# REMAPPING/meridian-desktop/src-tauri/tauri.conf.json
node -e "
const fs = require('fs');
const p = 'REMAPPING/meridian-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 REMAPPING/meridian-desktop && pnpm exec biome format --write src-tauri/tauri.conf.json)
# REMAPPING/meridian-desktop/src-tauri/Cargo.toml — only first version line (under [package])
node -e "
const fs = require('fs');
const p = 'REMAPPING/meridian-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 meridian-desktop --manifest-path REMAPPING/meridian-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
# meridian-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/meridian-relay/Cargo.toml
cargo update -p meridian-relay
echo "Bumped meridian-relay to {{ version }} and regenerated Cargo.lock"
# Open or update the desktop release PR (signed desktop app)
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
just _release-pr desktop "$VERSION"
# Open or update the relay release PR (registry.r2d2.office.ilab.zone/meridian 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=(REMAPPING/meridian-desktop/package.json REMAPPING/meridian-desktop/src-tauri/tauri.conf.json REMAPPING/meridian-desktop/src-tauri/Cargo.toml REMAPPING/meridian-desktop/src-tauri/Cargo.lock pnpm-lock.yaml CHANGELOG.md)
LOG_PATHS=(REMAPPING/meridian-desktop/ crates/meridian-core/ crates/meridian-persona/ crates/meridian-sdk/ crates/meridian-agent/)
ARTIFACT="Meridian 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/meridian-relay/CHANGELOG.md"
ADD_FILES=(crates/meridian-relay/Cargo.toml Cargo.lock crates/meridian-relay/CHANGELOG.md)
LOG_PATHS=(crates/meridian-relay/ crates/meridian-core/ crates/meridian-db/ crates/meridian-auth/ crates/meridian-pubsub/ crates/meridian-search/ crates/meridian-audit/ crates/meridian-media/ crates/meridian-sdk/ crates/meridian-workflow/ crates/meridian-conformance/ migrations/)
ARTIFACT="Meridian 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 Meridian relay (foreground)
goose relay="ws://localhost:3000" agents="1" heartbeat="0" prompt="" key="$MERIDIAN_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/meridian-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="$MERIDIAN_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/meridian-acp"
echo "Agent running in screen session 'goose-agent-{{agents}}'. Attach with: screen -r goose-agent-{{agents}}"
# ─── Benchmarking ─────────────────────────────────────────────────────────────
# Run the Meridian orchestra benchmark — leaderboard-eligible by default (TB 2.1, k=5, Sonnet+Haiku). Stands up its own Docker stack; --gui opens a live spectator desktop app; other flags pass to benchmark.py (--dataset/--path, --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-meridian-orchestra/testbed \
benchmarks/harbor-meridian-orchestra/scripts/benchmark.py {{ARGS}}
# Stop the benchmark Docker stack (state and channels are kept)
benchmark-down:
docker compose --project-name meridian-benchmark down
# Run the isolated, calibrated threaded-event PostgreSQL persistence gate.
reply-persistence-benchmark:
REPLY_BENCH_ALLOW_DATABASE_MUTATION=1 scripts/benchmark-reply-path.sh
# Verify benchmark safety, durability, invariant, and threshold wiring without Docker.
reply-persistence-benchmark-contract:
scripts/test-benchmark-reply-path.sh
# Run the criterion benches that back every published throughput figure.
#
# Before this recipe, `cargo bench` appeared in no recipe and no CI workflow
# (meridian-wpqw), while README.md, AGENTS.md and ARCHITECTURE.md all cite
# `event_cost.rs` for the verification ceiling and for the 125x/405x MAC ratios
# derived from it. AGENTS.md instructs "re-run the bench rather than trusting
# the extrapolation when the number matters" — and there was nothing to run.
#
# The profile header is the point, not decoration. The root Design Law refuses a
# throughput figure published without its traffic class, auth profile, and
# whether the payload is parsed and stored; printing those beside the numbers is
# what stops a bare figure being copied out of this output into a document.
#
# Pass criterion args through directly — `just bench schnorr_verify_nostr` to
# filter, `just bench --quick` for a fast smoke. Do NOT write `just bench --
# --quick`: this recipe already supplies the `--` that separates cargo's args
# from criterion's, and a second one reaches criterion as a positional filter
# ("error: unexpected argument found").
#
# `--quick` reads HIGH on this suite — measured ~34-40 Kelem/s on
# schnorr_verify_nostr against ~21-33 for full runs — so it is a smoke test that
# the benches still execute, and never a figure to publish.
#
# HOST LOAD IS THE DOMINANT VARIABLE, and it used to be missing from the profile
# entirely. Measured on one unchanged tree, `schnorr_verify_nostr/64`:
#
# load 1m ~9.8 42.6 Kelem/s load 24-43 15.5 Kelem/s
# load 20-24 27.5 Kelem/s load 43-81 21.2 Kelem/s
#
# A 2.75× swing with no code change (meridian-9fqe). This host runs 38
# containers for neighbouring projects, so a contended reading is the NORMAL
# case here, not the exception — which is why the recipe now measures load
# rather than asking a human to remember. A run that is not marked PUBLISHABLE
# below must not have its numbers copied into any document.
bench *ARGS:
#!/usr/bin/env bash
set -euo pipefail
export PATH="{{justfile_directory()}}/bin:$PATH"
if [[ "$(uname -s)" == "Darwin" ]]; then
ncpu=$(sysctl -n hw.ncpu)
else
ncpu=$(nproc)
fi
# 1-minute load average, portably: `uptime` is the one source present on
# both platforms, and its "load average:"/"load averages:" spelling differs.
load_start=$(uptime | sed 's/.*load averages*: *//' | awk '{print $1}' | tr -d ,)
echo "── Benchmark profile ──────────────────────────────────────────────"
if [[ "$(uname -s)" == "Darwin" ]]; then
echo " CPU: $(sysctl -n machdep.cpu.brand_string)"
echo " Cores: ${ncpu} logical, $(sysctl -n hw.perflevel0.logicalcpu 2>/dev/null || echo '?') performance"
echo " OS: macOS $(sw_vers -productVersion)"
else
echo " CPU: $(grep -m1 'model name' /proc/cpuinfo | cut -d: -f2- | sed 's/^ *//' || echo unknown)"
echo " Cores: ${ncpu} logical"
echo " OS: $(uname -sr)"
fi
echo " Build: release (criterion), single-threaded, one core"
echo " Load: ${load_start} at start (1m avg, ${ncpu} cores) — verdict below"
echo ""
echo " event_cost traffic class: signed NIP-01 ingest"
echo " auth: full BIP-340 per event · parsed: yes · stored: no"
echo " fanout_cost traffic class: NIP-01 fan-out to N subscribers"
echo " auth: none on this leg (verified upstream)"
echo " parsed: yes, re-serialized per frame · stored: no"
echo ""
echo " A figure from this run is publishable only with the lines above"
echo " attached, and as an interval no narrower than the spread you see"
echo " across repeated runs — one run is not a measurement (meridian-9fqe)."
echo "───────────────────────────────────────────────────────────────────"
echo ""
cargo bench -p meridian-core --bench event_cost -- {{ARGS}}
cargo bench -p meridian-relay --bench fanout_cost -- {{ARGS}}
# The verdict goes AFTER the numbers, deliberately: it is the last thing on
# screen when someone is about to copy a figure out of this output.
load_end=$(uptime | sed 's/.*load averages*: *//' | awk '{print $1}' | tr -d ,)
echo ""
echo "── Publishability ─────────────────────────────────────────────────"
verdict=$(awk -v a="$load_start" -v b="$load_end" -v n="$ncpu" 'BEGIN{
m = (a > b ? a : b);
printf "%s %.2f %.2f", (m/n <= 1.0 ? "PUBLISHABLE" : "CONTENDED"), m, m/n }')
state=${verdict%% *}; rest=${verdict#* }; peak=${rest%% *}; per=${rest#* }
echo " Load: ${load_start} start → ${load_end} end, peak ${peak} over ${ncpu} cores (${per}/core)"
if [[ "$state" == "PUBLISHABLE" ]]; then
echo " Verdict: PUBLISHABLE — the machine was not oversubscribed."
echo " Still needs N runs; one run is not a measurement."
else
echo " Verdict: CONTENDED — DO NOT PUBLISH THESE NUMBERS."
echo " Load exceeded one runnable thread per core, which"
echo " moved schnorr_verify_nostr by 2.75× on this host with"
echo " no code change. Re-run on a quiet machine."
if [[ "${MERIDIAN_BENCH_REQUIRE_QUIET:-0}" == "1" ]]; then
echo " MERIDIAN_BENCH_REQUIRE_QUIET=1 — failing."
exit 1
fi
fi
echo "───────────────────────────────────────────────────────────────────"