#!/usr/bin/env bash # Stack-agnostic agent bootstrap for this repository. # # This script prepares any coding agent (Claude Code, Cursor, etc.) to work # on this repo. It handles: # 1. Nix flake input seeding (so `nix develop` works in cloud environments) # 2. bd (beads) installation and pin verification # 3. Issue tracker sync from the Dolt remote # 4. Optional cargo workspace warming (inside nix develop) # # Usage: # scripts/agent-bootstrap.sh [--skip-cargo-warm] [--skip-nix-seed] # # The script is idempotent and safe to re-run. Nothing here writes to tracked # files, stages anything, or creates a commit. # # IMPORTANT: This repo requires `nix develop` (or direnv) for builds/tests. # Bare system cargo that fails edition2024 is a misconfigured session. # # See also: `.claude/hooks/session-start.sh` (Claude Code wrapper), # `.cursor/environment.json` (Cursor cloud config) set -uo pipefail SKIP_CARGO_WARM=0 SKIP_NIX_SEED=0 while [[ $# -gt 0 ]]; do case "$1" in --skip-cargo-warm) SKIP_CARGO_WARM=1; shift ;; --skip-nix-seed) SKIP_NIX_SEED=1; shift ;; *) shift ;; esac done ROOT="${AGENT_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}}" cd "$ROOT" || exit 0 log() { printf '[agent-bootstrap] %s\n' "$*"; } nix_status=0 ensure_nix_daemon() { command -v nix >/dev/null 2>&1 || return 1 # Single-user Nix does not need a daemon; multi-user/cloud installs do. # If the store already answers, leave the environment alone. if nix --extra-experimental-features 'nix-command' store ping >/dev/null 2>&1; then return 0 fi if ! command -v nix-daemon >/dev/null 2>&1; then log "ERROR: Nix is installed, but the store is unavailable and nix-daemon is not on PATH" return 1 fi log "starting nix-daemon for bootstrap…" nix-daemon >/tmp/nix-daemon.log 2>&1 & # The daemon is started asynchronously; wait briefly for its socket before # invoking the seed/build steps that depend on it. for _ in {1..50}; do if nix --extra-experimental-features 'nix-command' store ping --store daemon >/dev/null 2>&1; then return 0 fi sleep 0.1 done log "ERROR: nix-daemon did not become ready; see /tmp/nix-daemon.log" return 1 } # Start the daemon during cloud-agent installation, before any required Nix # operation. This is harmless for single-user Nix because nix-store --ping # succeeds without starting a daemon. if [ "$SKIP_NIX_SEED" -eq 0 ] || [ "$SKIP_CARGO_WARM" -eq 0 ]; then if ! ensure_nix_daemon; then nix_status=1 fi fi # ── 0. Seed nix flake inputs (required for cloud environments) ───────────── # GitHub tarball endpoints are blocked in cloud containers. This seeds the # flake inputs over git so `nix develop` can build the dev shell. if [ "$SKIP_NIX_SEED" -eq 0 ]; then if [ "$nix_status" -ne 0 ]; then log "ERROR: cannot seed flake inputs because Nix is unavailable" elif ! command -v nix >/dev/null 2>&1; then log "ERROR: nix not available; required flake input seeding cannot run" nix_status=1 else # Set SSL cert for proxy environments if [ -z "${NIX_SSL_CERT_FILE:-}" ] && [ -r /root/.ccr/ca-bundle.crt ]; then export NIX_SSL_CERT_FILE=/root/.ccr/ca-bundle.crt fi if [ ! -f "$ROOT/scripts/nix-seed-flake-inputs.sh" ]; then log "ERROR: required scripts/nix-seed-flake-inputs.sh is missing" nix_status=1 else log "seeding nix flake inputs (GitHub tarballs blocked in cloud)…" if "$ROOT/scripts/nix-seed-flake-inputs.sh" >/tmp/nix-seed.log 2>&1; then log "flake inputs seeded" else log "ERROR: nix-seed-flake-inputs.sh failed; see /tmp/nix-seed.log" log " 'nix develop' is not validated for this session." nix_status=1 fi fi fi fi # ── 1. The bd on PATH must be the bd flake.nix pins ──────────────────────── beads_pin() { sed -n 's|.*beads\.url[[:space:]]*=[[:space:]]*"github:[^/]*/[^/]*/v\{0,1\}\([^"]*\)".*|\1|p' \ flake.nix 2>/dev/null | head -1 } bd_running_version() { bd version 2>/dev/null | sed -n 's/^bd version \([0-9][0-9.]*\).*/\1/p' | head -1 } bd_pin_banner() { { printf '\n[agent-bootstrap] ===== BEADS VERSION SKEW =====\n' printf '[agent-bootstrap] %s\n' "$@" printf '[agent-bootstrap] =============================\n\n' } >&2 } assert_bd_pin() { command -v bd >/dev/null 2>&1 || return 0 pinned=$(beads_pin) running=$(bd_running_version) case "$pinned" in [0-9]*.[0-9]*.[0-9]*) ;; *) bd_pin_banner "cannot read the beads pin out of flake.nix" \ "read: '${pinned:-}'" \ "The pin moved to a ref this hook cannot parse (a branch or a" \ "commit, perhaps). Teach beads_pin() to read it, or the check" \ "silently protects nothing." return 1 ;; esac if [ -z "$running" ]; then bd_pin_banner "cannot read the version of the bd on PATH" \ "bd: $(command -v bd)" \ "'bd version' did not answer in the expected shape. Until it does," \ "nothing here can tell whether that build matches the pin." return 1 fi [ "$running" = "$pinned" ] && return 0 bd_pin_banner "the bd on PATH is NOT the bd this repo pins" \ "pinned (flake.nix): $pinned" \ "running: $running ($(command -v bd))" \ "" \ "Reading is safe. Do NOT run a mutating bd command — create, update," \ "close, dep, dolt push. Version skew between builds empties the" \ "dependency graph in .beads/issues.jsonl and reports success." \ "" \ "Fix now: nix develop (in this directory), then re-run the" \ " command; or 'direnv allow' here." \ "Fix for good: worktrees are created fresh per dispatch, so allowing" \ " them one at a time never catches the next one. Put the" \ " repo root under [whitelist] prefix in" \ " ~/.config/direnv/direnv.toml." return 1 } pin_status=0 assert_bd_pin || pin_status=2 # Install a verified database at .beads/embeddeddolt. install_tracker() { staged=$1 live=.beads/embeddeddolt incoming=$live.incoming previous=$live.previous rm -rf "$incoming" "$previous" || return 1 cp -rf "$staged" "$incoming" || { rm -rf "$incoming"; return 1; } if [ -d "$live" ] && ! mv -f "$live" "$previous"; then rm -rf "$incoming" return 1 fi if mv -f "$incoming" "$live"; then rm -rf "$previous" return 0 fi if [ -d "$previous" ] && ! mv -f "$previous" "$live"; then log "ERROR: tracker left at $previous — move it back to $live by hand" fi rm -rf "$incoming" return 1 } # ── 2. bd on PATH ────────────────────────────────────────────────────────── export PATH="$PATH:/root/go/bin:$HOME/go/bin" BD_INSTALLER_REF=f45b249ce6b40ba62aecc03949e6371e8f7c79d8 # tag v1.3.0 BD_INSTALLER_URL="https://raw.githubusercontent.com/steveyegge/beads/$BD_INSTALLER_REF/scripts/install.sh" if ! command -v bd >/dev/null 2>&1; then log "installing bd (beads v1.3.0)…" if ! curl -fsSL "$BD_INSTALLER_URL" -o /tmp/bd-install.sh 2>/tmp/bd-install.log; then log "WARNING: could not download the bd installer; see /tmp/bd-install.log." elif bash /tmp/bd-install.sh >>/tmp/bd-install.log 2>&1; then log "bd installed" else log "WARNING: bd install failed; see /tmp/bd-install.log (the script that" log " ran is at /tmp/bd-install.sh). Issue tracking unavailable." fi fi # Export PATH for the calling environment if AGENT_ENV_FILE is set if [ -n "${AGENT_ENV_FILE:-${CLAUDE_ENV_FILE:-}}" ]; then env_file="${AGENT_ENV_FILE:-${CLAUDE_ENV_FILE}}" echo 'export PATH="$PATH:/root/go/bin:$HOME/go/bin"' >> "$env_file" fi if [ "$pin_status" -eq 0 ]; then assert_bd_pin || pin_status=2 fi # Do not run any tracker/config mutation through a bd binary whose version does # not match the repository pin. if [ "$pin_status" -eq 0 ] && command -v bd >/dev/null 2>&1; then bd metrics off >/dev/null 2>&1 || true fi # ── 3. Sync the issue tracker from the Dolt remote ───────────────────────── if [ "$pin_status" -eq 0 ] && command -v bd >/dev/null 2>&1; then DB_NAME="$(jq -r '.dolt_database // "improvise"' .beads/metadata.json 2>/dev/null || echo improvise)" ORIGIN="$(git remote get-url origin 2>/dev/null || true)" case "$ORIGIN" in git@github.com:*) ORIGIN="https://github.com/${ORIGIN#git@github.com:}" ;; esac ORIGIN="${ORIGIN%.git}" if [ -n "$ORIGIN" ] && TMP=$(mktemp -d) && [ -d "$TMP" ]; then log "syncing issue tracker from $ORIGIN …" STAGE="$TMP/$DB_NAME" mkdir -p "$STAGE" sync_ok=0 if ( cd "$STAGE" && git init -q . && BD_NON_INTERACTIVE=1 bd init --non-interactive --skip-agents --skip-hooks -q \ --remote "git+$ORIGIN" ) >/tmp/bd-sync.log 2>&1 && [ -d "$STAGE/.beads/embeddeddolt" ]; then listing="$( (cd "$STAGE" && bd list --all 2>/dev/null) || true )" case "$listing" in "" | "No issues found."*) ;; *) sync_ok=1 ;; esac fi if [ "$sync_ok" -eq 1 ] && install_tracker "$STAGE/.beads/embeddeddolt"; then log "issue tracker synced — 'bd ready' now reflects the remote, not issues.jsonl" elif [ -d .beads/embeddeddolt ]; then log "WARNING: tracker sync failed (see /tmp/bd-sync.log); kept the previous database." log " It may be out of date — verify any bead against the code." else log "WARNING: tracker sync failed; see /tmp/bd-sync.log." log " .beads/issues.jsonl is a STALE export — verify any bead against the code." fi rm -rf "$TMP" elif [ -n "$ORIGIN" ]; then log "WARNING: could not create a temporary directory; skipped the tracker sync." log " .beads/issues.jsonl is a STALE export — verify any bead against the code." fi fi # ── 4. Warm the build inside nix develop (optional) ──────────────────────── # Uses nix develop to ensure the correct toolchain (rust from flake, wasm target, etc.) if [ "$SKIP_CARGO_WARM" -eq 0 ]; then if [ "$nix_status" -ne 0 ]; then log "ERROR: skipping required cargo warm because Nix bootstrap already failed" elif ! command -v nix >/dev/null 2>&1; then log "ERROR: nix not available; required cargo warm cannot run" nix_status=1 else log "warming cargo build inside nix develop…" NIX_FLAGS=(--extra-experimental-features 'nix-command flakes') if nix "${NIX_FLAGS[@]}" develop --command cargo build --workspace --tests >/tmp/cargo-warm.log 2>&1; then log "workspace built (nix develop toolchain)" else log "ERROR: cargo build failed; see /tmp/cargo-warm.log" log " Required nix develop validation failed." nix_status=1 fi fi fi # ── 5. Environment notes ─────────────────────────────────────────────────── cat <<'NOTES' [agent-bootstrap] Environment notes (see context/cloud-agents.md): • This repo REQUIRES `nix develop` (or direnv) for builds and tests. Bare system cargo that fails edition2024 is a misconfigured session. • The bootstrap seeded flake inputs over git (GitHub tarballs are blocked in cloud). If nix develop still fails, check /tmp/nix-seed.log. • `bd dolt push` is blocked outbound (403). Bead status changes stay local. • Never run a bare `bd init` in this repo: it rewrites AGENTS.md, CLAUDE.md and .claude/settings.json, and commits. Use --skip-agents --skip-hooks. • Use `scripts/kb` for knowledge base operations, not Prolog MCP. • Run commands inside `nix develop` or prefix with `nix develop --command`. NOTES exit_status="$nix_status" if [ "$pin_status" -ne 0 ]; then exit_status="$pin_status" fi exit "$exit_status"