sync

ober

165dd32fa155c3fc31ddc5b14c53751f722e1548

diff --git a/.build.yml b/.build.yml
index 30c3e22..3292af2 100644
--- a/.build.yml
+++ b/.build.yml
@@ -11,7 +11,6 @@ packages:
   - unzip
   - zip
 sources:
-  - https://git.sr.ht/~lisp/jerboa
   - https://git.sr.ht/~lisp/jerboa-code
 tasks:
   - setup-rust: |
@@ -22,11 +21,6 @@ tasks:
       # install one via rustup and persist it onto PATH for every later task.
       curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable
       echo 'source "$HOME/.cargo/env"' >> ~/.buildenv
-  - install-jerbuild: |
-      cd jerboa
-      make jerboa
-      make install PREFIX=$HOME/.local
-      echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.buildenv
   - vendor-code: |
       cd jerboa-code
       mkdir -p vendor
diff --git a/Makefile b/Makefile
index 0db9748..d411fb5 100644
--- a/Makefile
+++ b/Makefile
@@ -1,17 +1,20 @@
 # jerbuild is the self-contained build tool: it bundles Chez Scheme + the
 # jerboa stdlib under ~/.cache/jerbuild/<sha>/, so the host dev loop and the
 # native `binary` need NO jerboa source checkout and NO separately-built Chez.
-JERBUILD ?= jerbuild
+JERBOA_VERSION ?= v0.2.0
+JERBOA_TOOL_DIR ?= $(CURDIR)/.jerboa/bin
+JERBUILD ?= $(shell if [ -x ./jerbuild ] && [ -x ./jerboa ]; then echo ./jerbuild; \
+	elif [ -x "$(JERBOA_TOOL_DIR)/jerbuild" ] && [ -x "$(JERBOA_TOOL_DIR)/jerboa" ]; then echo "$(JERBOA_TOOL_DIR)/jerbuild"; \
+	elif [ -x ../jerboa/dist/jerbuild ] && [ -x ../jerboa/dist/jerboa ]; then echo ../jerboa/dist/jerbuild; \
+	elif command -v jerbuild >/dev/null 2>&1 && command -v jerboa >/dev/null 2>&1; then command -v jerbuild; \
+	else echo "$(JERBOA_TOOL_DIR)/jerbuild"; fi)
 
 # Bundled stdlib path (Chez + jerboa libs live under $(JH)/lib). Resolved by
 # asking the jerbuild binary where it unpacked its bundle.
-JH := $(shell $(JERBUILD) --jerboa-home 2>/dev/null)
-ifeq ($(JH),)
-$(error jerbuild not found on PATH (or '$(JERBUILD) --jerboa-home' failed). Install jerbuild, or set JERBUILD=/path/to/jerbuild)
-endif
+JH = $(shell "$(JERBUILD)" --jerboa-home 2>/dev/null)
 
-LIBDIRS := --libdirs ./lib:vendor/jerboa-sqlite/lib:vendor/jerboa-websearch/src:$(JH)/lib
-JEXEC   := $(JERBUILD) exec $(LIBDIRS)
+LIBDIRS = --libdirs ./lib:vendor/jerboa-sqlite/lib:vendor/jerboa-websearch/src:$(JH)/lib
+JEXEC   = $(JERBUILD) exec $(LIBDIRS)
 
 # Library paths for FFI shared objects (macOS: dylib, Linux: so). The native
 # Rust lib now lives in ./lib (dropped by the native-rs target), matching the
@@ -46,12 +49,13 @@ else
 JCODE_OS_LIBS := -lm -ldl -lpthread -luuid -lncurses -lstdc++
 endif
 
-.PHONY: all help build gen run test test-providers clean repl binary install tui-shim run-tui native-rs sqlite-bundled linux linux-check linux-amd64 linux-arm64 jcode-linux-amd64 jcode-linux-arm64 test-linux test-linux-amd64 freebsd freebsd-amd64 jcode-freebsd-amd64 purge-stale sqlite-shim sqlite-lib android android-clean vendor-deps vendor-clean
+.PHONY: all help ensure-jerboa-tools build gen run test test-providers clean repl binary install tui-shim run-tui native-rs sqlite-bundled linux linux-check linux-amd64 linux-arm64 jcode-linux-amd64 jcode-linux-arm64 test-linux test-linux-amd64 freebsd freebsd-amd64 jcode-freebsd-amd64 purge-stale sqlite-shim sqlite-lib android android-clean vendor-deps vendor-clean
 
 all: help
 
 help:
 	@echo "Available targets:"
+	@echo "  ensure-jerboa-tools  Ensure project-local jerboa/jerbuild are available"
 	@echo "  build        Compile src/ → lib/ and pre-compile imports (jerbuild)"
 	@echo "  test         Run test suite (test/run.ss)"
 	@echo "  test-providers  Live 'say hello' smoke test per configured provider"
@@ -77,6 +81,24 @@ help:
 	@echo "  android      Build Android APK in Termux (android/build/*.apk)"
 	@echo "  android-clean  Remove android/build/"
 
+ensure-jerboa-tools:
+	@if [ -x ./jerbuild ] && [ -x ./jerboa ]; then \
+	  echo "=== Using project-local ./jerbuild ==="; \
+	elif [ -x "$(JERBOA_TOOL_DIR)/jerbuild" ] && [ -x "$(JERBOA_TOOL_DIR)/jerboa" ]; then \
+	  echo "=== Using downloaded Jerboa toolchain: $(JERBOA_TOOL_DIR) ==="; \
+	elif [ -x ../jerboa/dist/jerbuild ] && [ -x ../jerboa/dist/jerboa ]; then \
+	  echo "=== Using sibling Jerboa build: ../jerboa/dist/jerbuild ==="; \
+	elif command -v jerbuild >/dev/null 2>&1 && command -v jerboa >/dev/null 2>&1; then \
+	  echo "=== Using Jerboa toolchain from PATH: $$(command -v jerbuild) ==="; \
+	else \
+	  echo "=== Fetching Jerboa $(JERBOA_VERSION) release tools into $(JERBOA_TOOL_DIR) ==="; \
+	  sh support/ensure-jerboa.sh "$(JERBOA_VERSION)" "$(JERBOA_TOOL_DIR)"; \
+	fi
+	@"$(JERBUILD)" --jerboa-home >/dev/null || { \
+	  echo "ERROR: Jerboa toolchain is unavailable; set JERBUILD=/path/to/jerbuild or JERBOA_VERSION=<tag>"; \
+	  exit 1; \
+	}
+
 # ── Stale-artifact prevention ───────────────────────────────────────────────
 # Delete any compiled .so/.wpo whose source .ss/.sls is newer (or whose
 # source no longer exists). This makes "make build" idempotent and immune
@@ -130,7 +152,7 @@ sqlite-shim: vendor/jerboa-sqlite sqlite-bundled
 	    vendor/jerboa-sqlite/jerboa_sqlite_shim.c $(SQLITE_BUNDLED_A); \
 	fi
 
-sqlite-lib: vendor/jerboa-sqlite
+sqlite-lib: ensure-jerboa-tools vendor/jerboa-sqlite
 	@rm -rf $(SQLITE_STAGE)
 	@mkdir -p $(SQLITE_STAGE)
 	@sed '/^(import (jerboa prelude))$$/d' \
@@ -216,14 +238,14 @@ $(REPL_TOKEN_FILE):
 	@head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n' > $@
 	@echo "Generated debug-REPL auth token in $(REPL_TOKEN_FILE)"
 
-gen: vendor-deps purge-stale sqlite-shim sqlite-lib $(REPL_TOKEN_FILE)
+gen: ensure-jerboa-tools vendor-deps purge-stale sqlite-shim sqlite-lib $(REPL_TOKEN_FILE)
 	JCODE_REPL_TOKEN=$$(cat $(REPL_TOKEN_FILE)) $(JERBUILD) transpile src lib
 
-build: gen native-rs
+build: ensure-jerboa-tools gen native-rs
 	DYLD_LIBRARY_PATH=$(LDPATH) LD_LIBRARY_PATH=$(LDPATH) \
 	$(JERBUILD) compile $(LIBDIRS) support/build-check.ss < /dev/null
 
-run: native-rs
+run: ensure-jerboa-tools native-rs
 	DYLD_LIBRARY_PATH=$(LDPATH) LD_LIBRARY_PATH=$(LDPATH) \
 	$(JEXEC) main.ss
 
@@ -235,11 +257,11 @@ tui-shim: vendor/termbox2
 	    -o $(TUI_SHIM_DIR)/jcode_tui_shim.dylib \
 	    src/jcode/ui/jcode_tui_shim.c
 
-run-tui: build tui-shim native-rs
+run-tui: ensure-jerboa-tools build tui-shim native-rs
 	DYLD_LIBRARY_PATH=$(LDPATH) LD_LIBRARY_PATH=$(LDPATH) \
 	$(JEXEC) main.ss --tui
 
-repl:
+repl: ensure-jerboa-tools
 	DYLD_LIBRARY_PATH=$(LDPATH) LD_LIBRARY_PATH=$(LDPATH) \
 	$(JEXEC) support/repl.ss
 
@@ -259,7 +281,7 @@ test-providers: build
 # Self-contained: `jerbuild build` reads .jerbuild (entry, libdirs, ffi-symbols,
 # main-c, C shims, rust-crate), bundles Chez + stdlib, runs cargo for the native
 # .a, and links it all. Per-OS link flags come from --os-libs.
-binary: gen native-rs
+binary: ensure-jerboa-tools gen native-rs
 	DYLD_LIBRARY_PATH=$(LDPATH) LD_LIBRARY_PATH=$(LDPATH) \
 	JCODE_REPL_TOKEN=$$(cat $(REPL_TOKEN_FILE)) \
 	$(JERBUILD) build --config .jerbuild --os-libs "$(JCODE_OS_LIBS)"
@@ -293,7 +315,7 @@ linux: linux-amd64
 # Runs compile-program on main-binary.ss with the same flags the cross build
 # uses, but skips the C compile and link. Catches drift (new imports, removed
 # stdlib exports) in ~10s before burning minutes in the full cross build.
-linux-check: gen
+linux-check: ensure-jerboa-tools gen
 	@echo "=== Running linux-check (fast cross-build drift check) ==="
 	DYLD_LIBRARY_PATH=$(LDPATH) LD_LIBRARY_PATH=$(LDPATH) \
 	$(SCHEME) -q --libdirs "$(XC_LIBDIRS)" --script linux-check.ss
@@ -303,7 +325,7 @@ linux-check: gen
 #   - <arch>-linux-musl-gcc on PATH   (brew install FiloSottile/musl-cross/musl-cross)
 #   - $(JERBOA_HOME)/.chez-cross-<m>  (cd $(JERBOA_HOME) && make chez-cross[-tarm64le])
 #   - rustup target add <arch>-unknown-linux-musl
-linux-amd64: gen
+linux-amd64: ensure-jerboa-tools gen
 	@command -v x86_64-linux-musl-gcc >/dev/null 2>&1 || { \
 	  echo "ERROR: x86_64-linux-musl-gcc not found on PATH."; \
 	  echo "Install with: brew install FiloSottile/musl-cross/musl-cross"; \
@@ -319,7 +341,7 @@ linux-amd64: gen
 	@ls -lh jcode-linux-amd64
 	@file jcode-linux-amd64
 
-linux-arm64: gen
+linux-arm64: ensure-jerboa-tools gen
 	@command -v aarch64-linux-musl-gcc >/dev/null 2>&1 || { \
 	  echo "ERROR: aarch64-linux-musl-gcc not found on PATH."; \
 	  echo "Install with: brew install FiloSottile/musl-cross/musl-cross"; \
@@ -356,7 +378,7 @@ test-linux: test-linux-amd64
 #   - $(JERBOA_HOME)/.chez-cross-ta6fb          (cd $(JERBOA_HOME) && make binary)
 #   - FreeBSD sysroot at $(JERBOA_HOME)/.freebsd-sysroot/amd64
 #   - rustup target add x86_64-unknown-freebsd
-freebsd-amd64: gen
+freebsd-amd64: ensure-jerboa-tools gen
 	@command -v $(firstword $(FREEBSD_AMD64_CC)) >/dev/null 2>&1 || { \
 	  echo "ERROR: $(FREEBSD_AMD64_CC) not found or not executable" >&2; \
 	  echo "  See top of Makefile freebsd-amd64 target for setup notes." >&2; \
diff --git a/README.md b/README.md
index 54662f6..7b835fe 100644
--- a/README.md
+++ b/README.md
@@ -44,9 +44,10 @@ tools dependably.
 
 ## Quickstart
 
-You need the installed Jerboa toolchain (`jerbuild` on `PATH`), a C compiler,
-and Rust/Cargo. See **[docs/getting-started.md](docs/getting-started.md)** for
-the full setup.
+You need a C compiler and Rust/Cargo. `make build` and `make binary` use a
+local Jerboa toolchain when present, otherwise they fetch the configured
+release bundle into `.jerboa/bin`. See
+**[docs/getting-started.md](docs/getting-started.md)** for the full setup.
 
 ```bash
 git clone https://git.sr.ht/~lisp/jerboa-code && cd jerboa-code
diff --git a/docs/cli.md b/docs/cli.md
index 9cf452e..47ed4e6 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -147,7 +147,10 @@ so it doubles as a smoke test. Full semantics in [FORGE.md](FORGE.md).
 
 | Variable | Purpose |
 |---|---|
-| `JERBOA_HOME` | Legacy fallback build path only; normal builds use installed `jerbuild`. |
+| `JERBUILD` | Optional explicit path to `jerbuild`. |
+| `JERBOA_VERSION` | Jerboa release tag to download when no local toolchain is available; defaults to `v0.2.0`. |
+| `JERBOA_TOOL_DIR` | Project-local download directory for `jerboa`, `jerbuild`, `jpkg`, `jmcp`, and `jlsp`; defaults to `.jerboa/bin`. |
+| `JERBOA_HOME` | Legacy cross-build fallback path only; normal builds use `jerbuild`. |
 | `JCODE_TRACE` | Path to write a full trace file (same as `--trace`). |
 | `JCODE_PASSPHRASE` | Passphrase for the encrypted key store (non-interactive). |
 | `JERBOA_STATIC` | `1` → use the statically-linked termbox shim instead of the `.dylib`/`.so`. |
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 0fe5fb3..b79e888 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -5,21 +5,21 @@ prerequisites, building on each platform, configuration, and your first session.
 
 ## Prerequisites
 
-`jcode` builds with the installed Jerboa toolchain. The build needs three
-things:
+`jcode` builds with the Jerboa toolchain. The build needs three things:
 
-1. **`jerbuild` on `PATH`.** Install the Jerboa multicall toolchain so
-   `jerboa`, `jerbuild`, `jmcp`, and `jlsp` are available:
+1. **A Jerboa toolchain.** The Makefile checks, in order:
+   project-local `./jerbuild` + `./jerboa`, `.jerboa/bin`, a sibling
+   `../jerboa/dist` build, `PATH`, then downloads the configured SourceHut
+   release artifact into `.jerboa/bin`.
 
    ```bash
-   cd ~/mine/jerboa
-   make jerboa
-   make install
+   make ensure-jerboa-tools
    ```
 
-   After install, `jerbuild --jerboa-home` should print the extracted bundled
-   stdlib path. A jcode build does not need `JERBOA_HOME` or a separate Chez
-   install for the normal `build`, `binary`, `linux-*`, or `freebsd` targets.
+   Override `JERBOA_VERSION` to pin a different Jerboa release tag, or set
+   `JERBUILD=/path/to/jerbuild` to force a specific local tool. A jcode build
+   does not need `JERBOA_HOME` or a separate Chez install for the normal
+   `build`, `binary`, `linux-*`, or `freebsd` targets.
 
 2. **A Rust toolchain.** `jerbuild` builds the vendored
    `jerboa-native-rs` crate with `cargo` for the standalone binary, and the dev
diff --git a/src/jcode/ui/tui-ffi.ss b/src/jcode/ui/tui-ffi.ss
index 4d677bc..cdb8b2c 100644
--- a/src/jcode/ui/tui-ffi.ss
+++ b/src/jcode/ui/tui-ffi.ss
@@ -99,7 +99,14 @@
 (define-tb c-tb-width      "jcode_tb_width"      ()    int)
 (define-tb c-tb-height     "jcode_tb_height"     ()    int)
 (define-tb c-tb-clear      "jcode_tb_clear"      ()    void)
-(define-tb c-tb-present    "jcode_tb_present"    ()    void)
+;; __collect_safe: tb_present writes the frame diff to the TTY with a
+;; blocking write(). Under tmux/vterm backpressure that write can stall;
+;; without collect-safe it pins the GC rendezvous and freezes EVERY
+;; thread (the streaming worker included) until the terminal drains.
+(def c-tb-present
+  (if (and _shim-loaded (foreign-entry? "jcode_tb_present"))
+    (foreign-procedure __collect_safe "jcode_tb_present" () void)
+    (lambda args (error 'tui-ffi "termbox shim not loaded" "jcode_tb_present"))))
 (define-tb c-tb-set-cursor "jcode_tb_set_cursor" (int int) void)
 (define-tb c-tb-hide-cursor "jcode_tb_hide_cursor" ()  void)
 (define-tb c-tb-change-cell "jcode_tb_change_cell" (int int unsigned-32 unsigned-32 unsigned-32) void)
diff --git a/src/jcode/ui/tui-input.ss b/src/jcode/ui/tui-input.ss
index 683c85a..8947c25 100644
--- a/src/jcode/ui/tui-input.ss
+++ b/src/jcode/ui/tui-input.ss
@@ -39,25 +39,51 @@
 
 ;; ---- Slash commands ----
 
+;; Every command handle-slash-command! (tui.ss) accepts MUST be listed
+;; here -- test/run.ss scans both sources and fails the suite when a
+;; dispatcher command is missing from this popup list.
 (def *slash-commands*
   '(("/help"          . "Show help")
     ("/model"         . "Show or set model")
     ("/provider"      . "Show or set provider")
     ("/expert"        . "Force a prompt to the configured expert model")
     ("/tools"         . "List available tools")
+    ("/mcp"           . "MCP server status")
+    ("/skills"        . "List skills")
+    ("/agents"        . "List agent definitions")
+    ("/mode"          . "Show or set PLAN/BUILD mode")
+    ("/plan"          . "Switch to PLAN (read-only) mode")
+    ("/build"         . "Switch to BUILD (read+write) mode")
     ("/clear"         . "Start new session")
     ("/sessions"      . "List saved sessions")
+    ("/resume"        . "Resume a stored session in this tab")
     ("/search"        . "Search session history")
     ("/compact"       . "Compact conversation")
     ("/copy"          . "Copy last reply to a file")
-    ("/quit"          . "Exit jcode")
-    ("/themes"        . "List color themes")
-    ("/theme"         . "Switch color theme")
-    ("/sidebar"       . "Toggle sidebar")
+    ("/save"          . "Save conversation to a file")
+    ("/activity"      . "Live harness activity screen (Esc returns)")
+    ("/side"          . "Open a parallel jcode conversation tab")
+    ("/tab"           . "Switch to tab N (Ctrl-O cycles)")
+    ("/tabs"          . "List open tabs")
+    ("/jcode"         . "Switch back to the main jcode tab")
+    ("/main"          . "Switch back to the main jcode tab")
+    ("/close-tab"     . "Close the current tab")
+    ("/close"         . "Close the current tab")
+    ("/claude"        . "Open/focus a Claude Code tab")
+    ("/codex"         . "Open/focus a Codex tab")
+    ("/gemini"        . "Open/focus a Gemini CLI tab")
+    ("/opencode"      . "Open/focus an opencode tab")
+    ("/grok"          . "Open/focus a Grok CLI tab")
     ("/ask-claude"    . "Second opinion from claude CLI (sandboxed)")
     ("/ask-gemini"    . "Second opinion from gemini CLI (sandboxed)")
     ("/ask-codex"     . "Second opinion from codex CLI (sandboxed)")
-    ("/ask-opencode"  . "Second opinion from opencode CLI (sandboxed)")))
+    ("/ask-opencode"  . "Second opinion from opencode CLI (sandboxed)")
+    ("/ask-grok"      . "Second opinion from grok CLI (sandboxed)")
+    ("/refresh-models" . "Refresh provider model list")
+    ("/themes"        . "List color themes")
+    ("/theme"         . "Switch color theme")
+    ("/sidebar"       . "Toggle sidebar")
+    ("/quit"          . "Exit jcode")))
 
 ;; ---- Key handling ----
 ;; Returns: 'submit | 'cancel | 'continue | 'quit
diff --git a/support/ensure-jerboa.sh b/support/ensure-jerboa.sh
new file mode 100755
index 0000000..e96aed7
--- /dev/null
+++ b/support/ensure-jerboa.sh
@@ -0,0 +1,121 @@
+#!/bin/sh
+# Bootstrap a project-local Jerboa toolchain from SourceHut release artifacts.
+#
+# Usage:
+#   support/ensure-jerboa.sh v0.2.0 .jerboa/bin
+#
+# Override the artifact location with:
+#   JERBOA_RELEASE_BASE=https://example.org/releases/v0.2.0
+# or the SourceHut repo with:
+#   JERBOA_RELEASE_REPO=~lisp/jerboa
+# For testing or unusual hosts, override target detection with:
+#   JERBOA_RELEASE_TARGET=macos-arm64
+
+set -eu
+
+usage() {
+    echo "Usage: $0 VERSION [BINDIR]" >&2
+}
+
+[ "${1:-}" ] || { usage; exit 2; }
+
+version=$1
+bindir=${2:-.jerboa/bin}
+repo=${JERBOA_RELEASE_REPO:-~lisp/jerboa}
+origin=${JERBOA_RELEASE_ORIGIN:-https://git.sr.ht}
+
+if [ "${JERBOA_RELEASE_TARGET:-}" ]; then
+    target=$JERBOA_RELEASE_TARGET
+else
+    os=$(uname -s)
+    arch=$(uname -m)
+    case "$os-$arch" in
+        Darwin-arm64) target=macos-arm64 ;;
+        Linux-x86_64|Linux-amd64) target=linux-amd64 ;;
+        Linux-aarch64|Linux-arm64) target=linux-arm64 ;;
+        FreeBSD-amd64|FreeBSD-x86_64) target=freebsd-amd64 ;;
+        *)
+            echo "ERROR: unsupported platform for Jerboa release artifacts: $os $arch" >&2
+            echo "       set JERBUILD=/path/to/jerbuild or build Jerboa locally for this host" >&2
+            exit 1
+            ;;
+    esac
+fi
+
+case "$target" in
+    macos-arm64|linux-amd64|linux-arm64|freebsd-amd64) ;;
+    *)
+        echo "ERROR: unsupported release target: $target" >&2
+        exit 1
+        ;;
+esac
+
+file="jerboa-${version}-${target}.tar.gz"
+base=${JERBOA_RELEASE_BASE:-${origin}/${repo}/refs/download/${version}}
+url="${base%/}/${file}"
+sum_url="${url}.sha256"
+
+download() {
+    src=$1
+    dst=$2
+    if command -v curl >/dev/null 2>&1; then
+        curl -fsSL "$src" -o "$dst"
+    elif command -v fetch >/dev/null 2>&1; then
+        fetch -q -o "$dst" "$src"
+    elif command -v wget >/dev/null 2>&1; then
+        wget -q -O "$dst" "$src"
+    else
+        echo "ERROR: need curl, fetch, or wget to download Jerboa" >&2
+        exit 1
+    fi
+}
+
+sha256_file() {
+    path=$1
+    if command -v sha256sum >/dev/null 2>&1; then
+        sha256sum "$path" | awk '{print $1}'
+    elif command -v shasum >/dev/null 2>&1; then
+        shasum -a 256 "$path" | awk '{print $1}'
+    elif command -v sha256 >/dev/null 2>&1; then
+        sha256 -q "$path"
+    else
+        echo "ERROR: need sha256sum, shasum, or sha256" >&2
+        exit 1
+    fi
+}
+
+tmp=$(mktemp -d "${TMPDIR:-/tmp}/jerboa-bootstrap.XXXXXX")
+trap 'rm -rf "$tmp"' EXIT HUP INT TERM
+
+archive="$tmp/$file"
+sum_file="$tmp/$file.sha256"
+
+echo "fetching $url"
+download "$url" "$archive"
+download "$sum_url" "$sum_file"
+
+expected=$(awk '{print $1; exit}' "$sum_file")
+actual=$(sha256_file "$archive")
+if [ "$expected" != "$actual" ]; then
+    echo "ERROR: checksum mismatch for $file" >&2
+    echo "expected: $expected" >&2
+    echo "actual:   $actual" >&2
+    exit 1
+fi
+
+mkdir -p "$tmp/extract"
+tar -xzf "$archive" -C "$tmp/extract"
+root="$tmp/extract/jerboa-${version}-${target}"
+[ -x "$root/bin/jerboa" ] || {
+    echo "ERROR: archive did not contain bin/jerboa" >&2
+    exit 1
+}
+
+mkdir -p "$bindir"
+cp "$root/bin/jerboa" "$bindir/jerboa"
+chmod 0755 "$bindir/jerboa"
+for link in jmcp jlsp jerbuild jpkg; do
+    ln -sf jerboa "$bindir/$link"
+done
+
+echo "installed $bindir/{jerboa,jmcp,jlsp,jerbuild,jpkg}"
diff --git a/test/run.ss b/test/run.ss
index cf7424e..cc028de 100644
--- a/test/run.ss
+++ b/test/run.ss
@@ -2074,6 +2074,76 @@
              (builtin-skill "create-plan")
              (lambda (s) (str-contains? s "plans/")))
 
+;; ── Slash-command completion coverage ─────────────────────────────
+;; Every command the TUI dispatcher (handle-slash-command! in tui.ss)
+;; accepts must appear in the completion popup (*slash-commands* in
+;; tui-input.ss). Scans the SOURCE text so adding a new
+;; (equal? cmd "...") / (string-prefix? "... " cmd) clause without a
+;; completion entry fails the suite.
+
+(define (slurp-file path)
+  (call-with-input-file path (lambda (p) (get-string-all p))))
+
+(define (str-find src needle start)
+  (let ([nl (string-length needle)] [sl (string-length src)])
+    (let loop ([i start])
+      (cond
+        [(> (+ i nl) sl) #f]
+        [(string=? (substring src i (+ i nl)) needle) i]
+        [else (loop (+ i 1))]))))
+
+(define (collect-quoted src marker require-cmd-after?)
+  ;; For each MARKER occurrence, the text up to the next double-quote.
+  ;; With require-cmd-after?, keep only matches whose quote is followed
+  ;; by " cmd)" -- filters dispatcher clauses from unrelated
+  ;; string-prefix? calls.
+  (let ([mlen (string-length marker)])
+    (let loop ([i 0] [acc '()])
+      (let ([hit (str-find src marker i)])
+        (if (not hit)
+          (reverse acc)
+          (let* ([s (+ hit mlen)]
+                 [q (str-find src "\"" s)])
+            (if (not q)
+              (reverse acc)
+              (let ([keep? (or (not require-cmd-after?)
+                               (and (<= (+ q 6) (string-length src))
+                                    (string=? (substring src (+ q 1) (+ q 6))
+                                              " cmd)")))])
+                (loop (+ q 1)
+                      (if keep? (cons (substring src s q) acc) acc))))))))))
+
+(define (strip-trailing-space s)
+  (let ([n (string-length s)])
+    (if (and (> n 0) (char=? (string-ref s (- n 1)) #\space))
+      (substring s 0 (- n 1))
+      s)))
+
+(define (dedupe lst)
+  (let loop ([l lst] [acc '()])
+    (cond
+      [(null? l) (reverse acc)]
+      [(member (car l) acc) (loop (cdr l) acc)]
+      [else (loop (cdr l) (cons (car l) acc))])))
+
+(let* ([tui-src     (slurp-file "src/jcode/ui/tui.ss")]
+       [input-src   (slurp-file "src/jcode/ui/tui-input.ss")]
+       [dispatcher  (dedupe
+                      (map strip-trailing-space
+                        (append
+                          (collect-quoted tui-src "(equal? cmd \"" #f)
+                          (collect-quoted tui-src "(string-prefix? \"" #t))))]
+       [completions (collect-quoted input-src "(\"/" #f)])
+  (check-pred! "dispatcher commands found by source scan"
+               dispatcher
+               (lambda (d) (> (length d) 30)))
+  (for-each
+    (lambda (cmd)
+      (check-pred! (format "completion popup has /~a" cmd)
+                   completions
+                   (lambda (cs) (and (member cmd cs) #t))))
+    dispatcher))
+
 ;; ── Results ───────────────────────────────────────────────────────
 
 (printf "~n~a passed, ~a failed~n" pass-count fail-count)