# 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 "───────────────────────────────────────────────────────────────────"
