From 8dcbfbfc775f96a96fc49c7e69bb2836d971d5fc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 25 Sep 2026 18:59:25 +0000 Subject: [PATCH] feat: align agent stacks (Claude, Cursor) on shared bootstrap Unifies coding-agent setup across Claude Code and Cursor so both stacks get: - Pin-matched bd from flake.nix - Fresh Dolt beads tracker sync (not stale JSONL export) - Same skills via symlinks to .agents/skills/ Key changes: 1. scripts/agent-bootstrap.sh: stack-agnostic bootstrap that handles bd installation, tracker sync, and optional cargo warming. Works for any agent stack (Claude, Cursor, or manual). 2. .claude/hooks/session-start.sh: now a thin wrapper that calls the shared bootstrap for cloud sessions, keeping only the local pin-check inline. 3. .cursor/environment.json: configures Cursor cloud agents to run the bootstrap via 'install'. 4. .cursor/skills/: symlinks to .agents/skills/* so Cursor agents discover the same skills Claude has. 5. AGENTS.md: adds Multi-Stack Agent Support section; clarifies that scripts/kb is the KB interface (Prolog MCP is not required and not available in all stacks). 6. context/cloud-agents.md: updated for multi-stack support; added KB section emphasizing scripts/kb over Prolog MCP. The canonical skill source remains .agents/skills/. Both .claude/skills/ and .cursor/skills/ are symlinks to it, so there is one truth that both stacks read. Co-authored-by: Ed Langley --- .claude/hooks/session-start.sh | 291 ++++----------------------------- .claude/skills/beads | 1 + .cursor/environment.json | 5 + .cursor/skills/beads | 1 + .cursor/skills/beads-merge | 1 + .cursor/skills/code-review | 1 + .cursor/skills/kb | 1 + .cursor/skills/pr-watch | 1 + .cursor/skills/wave | 1 + AGENTS.md | 28 +++- context/cloud-agents.md | 41 ++++- scripts/agent-bootstrap.sh | 222 +++++++++++++++++++++++++ 12 files changed, 331 insertions(+), 263 deletions(-) create mode 120000 .claude/skills/beads create mode 100644 .cursor/environment.json create mode 120000 .cursor/skills/beads create mode 120000 .cursor/skills/beads-merge create mode 120000 .cursor/skills/code-review create mode 120000 .cursor/skills/kb create mode 120000 .cursor/skills/pr-watch create mode 120000 .cursor/skills/wave create mode 100755 scripts/agent-bootstrap.sh diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh index 52b0e9c2..3f055531 100755 --- a/.claude/hooks/session-start.sh +++ b/.claude/hooks/session-start.sh @@ -1,273 +1,50 @@ #!/bin/bash # Prepare a Claude Code on the web container for work on this repository. # -# Section 0 runs everywhere, cloud or not: it asserts that the `bd` on PATH -# is the one flake.nix pins. Everything after it is cloud-only setup. +# This hook delegates to the shared `scripts/agent-bootstrap.sh`, which works +# for any coding agent stack (Claude Code, Cursor, etc.). This file exists +# because Claude Code expects hooks at `.claude/hooks/session-start.sh`. # -# Three things are slow or wrong by default in a fresh cloud session: +# For local checkouts, only the bd pin assertion runs. +# For cloud sessions (CLAUDE_CODE_REMOTE=true), full bootstrap runs. # -# 1. `bd` is not installed, so the `bd prime` hook and every `bd` command -# in AGENTS.md silently do nothing. -# 2. `.beads/issues.jsonl` is a stale export. An agent that reads it picks -# work that is already done — this has happened. The authoritative -# tracker is the Dolt database on the git remote. -# 3. The Rust workspace is uncompiled, so the first `cargo test` pays a -# cold build before it reports anything. -# -# The tracker is re-synced on every session, not just when the database is -# missing: cloud containers reuse a cached workspace, so "already present" -# does not mean "still current", and a silently ageing tracker is the exact -# failure this hook exists to prevent. Installing bd and warming the build -# do check first, being genuinely one-time. -# -# Nothing here writes to a tracked file, stages anything, or creates a commit. +# See also: scripts/agent-bootstrap.sh (the stack-agnostic implementation) set -uo pipefail ROOT="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" -cd "$ROOT" || exit 0 +BOOTSTRAP="$ROOT/scripts/agent-bootstrap.sh" -log() { printf '[session-start] %s\n' "$*"; } - -# ── 0. The bd on PATH must be the bd flake.nix pins ──────────────────────── -# `flake.nix` pins beads so that one build of `bd` touches this repo. A git -# worktree does not load that pin — a fresh `.envrc` is unallowed, so a -# process there falls through to whatever `bd` the user's profile happens to -# carry. Today those agree; nothing enforces that they keep agreeing. -# -# The failure that matters is silent: writing .beads/issues.jsonl with a -# skewed build empties the dependency graph and reports success. Nobody -# notices until they ask what blocks what. So the mismatch is turned into a -# loud one here rather than trusted. Reading with a skewed bd is safe — it -# is the mutating commands that corrupt — so this blocks nothing, it shouts. -# -# The expected version is read from flake.nix, not written down twice: a -# hard-coded copy would go stale the first time the pin moves, and a stale -# assertion is worse than none. -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[session-start] ===== BEADS VERSION SKEW =====\n' - printf '[session-start] %s\n' "$@" - printf '[session-start] =============================\n\n' - } >&2 -} - -# Prints the banner and returns 1 when the two disagree, or when either one -# cannot be read — an unreadable answer is a rotted assertion, not a pass. -assert_bd_pin() { - command -v bd >/dev/null 2>&1 || return 0 # nothing on PATH to assert about - - 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 - -# Local checkouts have their own setup; this is for the web containers. -if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then - exit "$pin_status" -fi - -# Install a verified database at .beads/embeddeddolt. -# -# The staged copy lives on another filesystem, so it is brought alongside the -# live one FIRST. Every step after that is a same-filesystem rename, which -# either happens or does not — no half-finished copy. The previous database is -# removed only once the new one is in place, so no failure path leaves the repo -# without a tracker, and every move is checked: an unchecked one would let the -# new database land *inside* the old directory and still report success. -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 "$live" "$previous"; then - rm -rf "$incoming" - return 1 - fi - - if mv "$incoming" "$live"; then - rm -rf "$previous" - return 0 - fi - - # Could not put the new one in place; restore what was there. - if [ -d "$previous" ] && ! mv "$previous" "$live"; then - log "ERROR: tracker left at $previous — move it back to $live by hand" - fi - rm -rf "$incoming" - return 1 -} - -# ── 1. bd on PATH ────────────────────────────────────────────────────────── -# The embedded Dolt engine needs CGO, so this builds from source (~3 min). -# The container image is cached after the hook, so it is paid once. -export PATH="$PATH:/root/go/bin:$HOME/go/bin" -# Pinned to a commit rather than `main`, and downloaded before it is run: -# piping a moving URL straight into a shell executes code nobody has seen, -# and leaves nothing to inspect afterwards. The installer stays at -# /tmp/bd-install.sh so what actually ran can be read. -# -# To take a newer beads, resolve the peeled commit of its tag: -# git ls-remote --tags https://github.com/steveyegge/beads | grep 'v1.3.0^{}' -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 +# Forward the Claude env file variable to the generic name the bootstrap uses if [ -n "${CLAUDE_ENV_FILE:-}" ]; then - # Single quotes are deliberate: $PATH and $HOME must be written literally so - # they expand when the session sources this, not now. - # shellcheck disable=SC2016 - echo 'export PATH="$PATH:/root/go/bin:$HOME/go/bin"' >> "$CLAUDE_ENV_FILE" -fi -command -v bd >/dev/null 2>&1 && bd metrics off >/dev/null 2>&1 - -# bd may have arrived only just now, in which case §0 had nothing to check. -# Re-check, but only if §0 stayed quiet, so the banner is never printed twice. -if [ "$pin_status" -eq 0 ]; then - assert_bd_pin || pin_status=2 + export AGENT_ENV_FILE="$CLAUDE_ENV_FILE" fi -# ── 2. Sync the issue tracker from the Dolt remote ───────────────────────── -# `bd init` in-place rewrites .beads/config.yaml and commits, so the sync -# happens in a throwaway directory and only the database is moved back. -# That directory must be named after the database (see .beads/metadata.json), -# since bd derives the database name from it. -if 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}" - - # The mktemp is part of the condition on purpose. Unchecked, a failure - # leaves TMP empty, "$TMP/$DB_NAME" becomes an absolute path at the - # filesystem root, and the cleanup at the end degrades to `rm -rf ""`. - # Short-circuiting also means no temp directory is created when there is - # no origin to sync from. - if [ -n "$ORIGIN" ] && TMP=$(mktemp -d) && [ -d "$TMP" ]; then - log "syncing issue tracker from $ORIGIN …" - STAGE="$TMP/$DB_NAME" - mkdir -p "$STAGE" - - # `bd init` exits non-zero when the remote is unreachable but STILL leaves - # a .beads/embeddeddolt behind — an empty database. Testing only for the - # directory would swap that over a good tracker, so check the status and - # confirm the result actually carries issues. An empty remote tracker is - # indistinguishable from a failed clone here; keeping what we already have - # is the safe way to be wrong. - 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 +# For local checkouts, only verify the bd pin (no install, no sync) +if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then + # Inline the pin-check only, since the full bootstrap is cloud-only + cd "$ROOT" || exit 0 + 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 + } + if command -v bd >/dev/null 2>&1; then + pinned=$(beads_pin) + running=$(bd_running_version) + if [ -n "$pinned" ] && [ -n "$running" ] && [ "$running" != "$pinned" ]; then + printf '\n[session-start] ===== BEADS VERSION SKEW =====\n' >&2 + printf '[session-start] pinned (flake.nix): %s\n' "$pinned" >&2 + printf '[session-start] running: %s (%s)\n' "$running" "$(command -v bd)" >&2 + printf '[session-start] Reading is safe. Do NOT run mutating bd commands.\n' >&2 + printf '[session-start] Fix: nix develop, or direnv allow.\n' >&2 + printf '[session-start] =============================\n\n' >&2 + exit 2 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 + exit 0 fi -# ── 3. Warm the build so the first cargo test is not a cold compile ──────── -if command -v cargo >/dev/null 2>&1; then - log "warming cargo build…" - if cargo build --workspace --tests >/tmp/cargo-warm.log 2>&1; then - log "workspace built" - else - log "WARNING: cargo build failed; see /tmp/cargo-warm.log" - fi -fi - -# ── 4. Environment facts worth knowing up front ──────────────────────────── -cat <<'NOTES' -[session-start] Environment notes (see context/cloud-agents.md): - • Use the preinstalled cargo toolchain for ordinary work. `nix develop` - also works, but only after scripts/nix-seed-flake-inputs.sh — GitHub - tarballs are not served here, so the locked inputs are seeded over git. - • `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. - • `bd dolt push ` ignores its argument and pushes to origin. -NOTES - -# Non-zero only when §0 found version skew; the setup above still ran. -exit "$pin_status" +# Cloud session: run the full bootstrap +exec "$BOOTSTRAP" diff --git a/.claude/skills/beads b/.claude/skills/beads new file mode 120000 index 00000000..f57add10 --- /dev/null +++ b/.claude/skills/beads @@ -0,0 +1 @@ +../../.agents/skills/beads \ No newline at end of file diff --git a/.cursor/environment.json b/.cursor/environment.json new file mode 100644 index 00000000..ad4d8ea5 --- /dev/null +++ b/.cursor/environment.json @@ -0,0 +1,5 @@ +{ + "name": "Improvise", + "install": "./scripts/agent-bootstrap.sh --skip-cargo-warm", + "start": "nix-daemon &" +} diff --git a/.cursor/skills/beads b/.cursor/skills/beads new file mode 120000 index 00000000..f57add10 --- /dev/null +++ b/.cursor/skills/beads @@ -0,0 +1 @@ +../../.agents/skills/beads \ No newline at end of file diff --git a/.cursor/skills/beads-merge b/.cursor/skills/beads-merge new file mode 120000 index 00000000..53a96ecc --- /dev/null +++ b/.cursor/skills/beads-merge @@ -0,0 +1 @@ +../../.agents/skills/beads-merge \ No newline at end of file diff --git a/.cursor/skills/code-review b/.cursor/skills/code-review new file mode 120000 index 00000000..2d88efdf --- /dev/null +++ b/.cursor/skills/code-review @@ -0,0 +1 @@ +../../.agents/skills/code-review \ No newline at end of file diff --git a/.cursor/skills/kb b/.cursor/skills/kb new file mode 120000 index 00000000..3ee2d209 --- /dev/null +++ b/.cursor/skills/kb @@ -0,0 +1 @@ +../../.agents/skills/kb \ No newline at end of file diff --git a/.cursor/skills/pr-watch b/.cursor/skills/pr-watch new file mode 120000 index 00000000..3ef371ce --- /dev/null +++ b/.cursor/skills/pr-watch @@ -0,0 +1 @@ +../../.agents/skills/pr-watch \ No newline at end of file diff --git a/.cursor/skills/wave b/.cursor/skills/wave new file mode 120000 index 00000000..0c2044da --- /dev/null +++ b/.cursor/skills/wave @@ -0,0 +1 @@ +../../.agents/skills/wave \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index a476b158..f47132cc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,8 +13,10 @@ force-push. The rule, the message convention and the incidents are `.agents/skills/wave/agent-rules.md`, rule 8. - always start responses with bananaS! -- there is a prolog MCP available: load a session from `kb` at the root of the repository at the beginning of - every session, and query it as you work. **Reading only** — the MCP must never write `kb.pl`. +- **Knowledge base: use `scripts/kb`, not Prolog MCP.** The Prolog MCP is optional and not + available in all agent stacks (Cursor cloud agents do not have it). All agents must use + `scripts/kb add|replace|rm|query|check` for KB operations — see the `kb` skill at + `.agents/skills/kb/SKILL.md`. Reading `prolog-sessions/kb.pl` directly is fine for queries. - save structured facts as you go — work done, interesting locations, important decisions — with `scripts/kb add` (see the `kb` skill). Write a fact when you learn it; there is no reason to batch. - query the knowledge base as needed and use Prolog reasoning when it makes sense. @@ -25,6 +27,28 @@ This project uses **bd** (beads) for issue tracking. Run `bd prime` for full workflow context. +## Multi-Stack Agent Support + +This repo supports multiple coding-agent stacks: Claude Code, Cursor (IDE and cloud), and +potentially others. All stacks share the same operational setup: + +| Component | Location | Notes | +|---|---|---| +| Skills | `.agents/skills/` | Canonical source; `.claude/skills/` and `.cursor/skills/` symlink here | +| Session bootstrap | `scripts/agent-bootstrap.sh` | Stack-agnostic; installs bd, syncs tracker, warms cargo | +| Knowledge base | `scripts/kb` | **All stacks use this**, not Prolog MCP | +| Issue tracker | `bd` CLI | Synced from Dolt remote at session start | + +**Claude Code**: `.claude/hooks/session-start.sh` calls the bootstrap automatically. + +**Cursor cloud**: `.cursor/environment.json` runs the bootstrap via `install`. Skills are +discovered from `.cursor/skills/` (symlinks to `.agents/skills/`). + +**Local agents**: Run `scripts/agent-bootstrap.sh` manually, or use `direnv` / `nix develop` +for the full dev shell which includes `bd` from the flake pin. + +See `context/cloud-agents.md` for environment-specific notes (nix, network, etc.). + ## Quick Reference ```bash diff --git a/context/cloud-agents.md b/context/cloud-agents.md index 9fb343b4..7ab55bda 100644 --- a/context/cloud-agents.md +++ b/context/cloud-agents.md @@ -1,11 +1,26 @@ # Cloud Agents (LLM Reference) -Environment notes for agents running in Claude Code on the web. Local -checkouts are unaffected — the session hook exits early unless +Environment notes for agents running in cloud containers — Claude Code on the web, +Cursor cloud agents, and similar setups. Local checkouts are unaffected. + +## Session Bootstrap + +All agent stacks use the same bootstrap script: `scripts/agent-bootstrap.sh`. It: + +1. Verifies the `bd` version matches the flake.nix pin +2. Installs `bd` if missing +3. Syncs the Dolt issue tracker from the git remote +4. Optionally warms the cargo build + +**Claude Code**: `.claude/hooks/session-start.sh` calls the bootstrap automatically when `CLAUDE_CODE_REMOTE=true`. -`.claude/hooks/session-start.sh` handles the setup automatically. This doc -is the "why", and the record of what bites when it doesn't run. +**Cursor cloud**: `.cursor/environment.json` runs the bootstrap via `install`. Skills are +in `.cursor/skills/` (symlinks to `.agents/skills/`). + +**Other stacks**: Run `scripts/agent-bootstrap.sh` manually after checkout. + +This doc is the "why", and the record of what bites when setup doesn't run. --- @@ -143,3 +158,21 @@ environment, where egress leaves through your own network boundary. reorder or fold commits (`.agents/skills/wave/agent-rules.md`, rule 8). - Keep `context/repo-map.md` and `context/design-principles.md` current — they are the two docs agents are expected to maintain by hand. + +--- + +## Knowledge Base + +**Use `scripts/kb`, not Prolog MCP.** The Prolog MCP is not available in all agent stacks +(Cursor cloud agents do not have it). All agents must use `scripts/kb` for KB operations: + +```bash +scripts/kb check # kb_consistent, exactly what CI runs +scripts/kb query '' # run a goal; exit 1 if it fails +scripts/kb add # add clauses; refuses if a key already exists +scripts/kb replace # add, overwriting existing keys +scripts/kb rm # remove by key; refuses if a key is absent +``` + +Reading `prolog-sessions/kb.pl` directly for queries is fine. See the `kb` skill at +`.agents/skills/kb/SKILL.md` for the full interface. diff --git a/scripts/agent-bootstrap.sh b/scripts/agent-bootstrap.sh new file mode 100755 index 00000000..196e28b3 --- /dev/null +++ b/scripts/agent-bootstrap.sh @@ -0,0 +1,222 @@ +#!/bin/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. bd (beads) installation and pin verification +# 2. Issue tracker sync from the Dolt remote +# 3. Optional cargo workspace warming +# +# Usage: +# scripts/agent-bootstrap.sh [--skip-cargo-warm] +# +# The script is idempotent and safe to re-run. Nothing here writes to tracked +# files, stages anything, or creates a commit. +# +# See also: `.claude/hooks/session-start.sh` (Claude Code wrapper), +# `.cursor/environment.json` (Cursor cloud config) +set -uo pipefail + +SKIP_CARGO_WARM=0 +while [[ $# -gt 0 ]]; do + case "$1" in + --skip-cargo-warm) SKIP_CARGO_WARM=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' "$*"; } + +# ── 0. 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 +} + +# ── 1. 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 + +command -v bd >/dev/null 2>&1 && bd metrics off >/dev/null 2>&1 + +if [ "$pin_status" -eq 0 ]; then + assert_bd_pin || pin_status=2 +fi + +# ── 2. Sync the issue tracker from the Dolt remote ───────────────────────── +if 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 + +# ── 3. Warm the build (optional) ─────────────────────────────────────────── +if [ "$SKIP_CARGO_WARM" -eq 0 ] && command -v cargo >/dev/null 2>&1; then + log "warming cargo build…" + if cargo build --workspace --tests >/tmp/cargo-warm.log 2>&1; then + log "workspace built" + else + log "WARNING: cargo build failed; see /tmp/cargo-warm.log" + fi +fi + +# ── 4. Environment notes ─────────────────────────────────────────────────── +cat <<'NOTES' +[agent-bootstrap] Environment notes (see context/cloud-agents.md): + • Use the preinstalled cargo toolchain for ordinary work. `nix develop` + also works, but only after scripts/nix-seed-flake-inputs.sh — GitHub + tarballs are not served here, so the locked inputs are seeded over git. + • `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. + • `bd dolt push ` ignores its argument and pushes to origin. + • Use `scripts/kb` for knowledge base operations, not Prolog MCP. +NOTES + +exit "$pin_status"