324 lines
12 KiB
Bash
Executable File
324 lines
12 KiB
Bash
Executable File
#!/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:-<nothing>}'" \
|
|
"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:}"
|
|
git remote add gh "$ORIGIN"
|
|
;;
|
|
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"
|