Update release artifacts for 0.2.0

ober

9d338f0e81164b15990afe967fde26eac9e2a1b2

diff --git a/.builds/release-freebsd-amd64.yml b/.builds/release-freebsd-amd64.yml
index 2e4396c..532be99 100644
--- a/.builds/release-freebsd-amd64.yml
+++ b/.builds/release-freebsd-amd64.yml
@@ -22,7 +22,7 @@ tasks:
     cd jerboa
     case "${GIT_REF:-}" in
       refs/tags/*) version=${GIT_REF#refs/tags/} ;;
-      *) version=$(git describe --tags --always) ;;
+      *) version=v$(cat VERSION) ;;
     esac
     gmake jerboa
     gmake release-artifact RELEASE_VERSION="$version" RELEASE_TARGET="$release_target"
diff --git a/.builds/release-linux-amd64.yml b/.builds/release-linux-amd64.yml
index 6d9d625..642798b 100644
--- a/.builds/release-linux-amd64.yml
+++ b/.builds/release-linux-amd64.yml
@@ -24,7 +24,7 @@ tasks:
     cd jerboa
     case "${GIT_REF:-}" in
       refs/tags/*) version=${GIT_REF#refs/tags/} ;;
-      *) version=$(git describe --tags --always) ;;
+      *) version=v$(cat VERSION) ;;
     esac
     make jerboa
     make release-artifact RELEASE_VERSION="$version" RELEASE_TARGET="$release_target"
diff --git a/.builds/release-linux-arm64.yml b/.builds/release-linux-arm64.yml
index f86d805..cf7a1ef 100644
--- a/.builds/release-linux-arm64.yml
+++ b/.builds/release-linux-arm64.yml
@@ -24,7 +24,7 @@ tasks:
     cd jerboa
     case "${GIT_REF:-}" in
       refs/tags/*) version=${GIT_REF#refs/tags/} ;;
-      *) version=$(git describe --tags --always) ;;
+      *) version=v$(cat VERSION) ;;
     esac
     make jerboa
     make release-artifact RELEASE_VERSION="$version" RELEASE_TARGET="$release_target"
diff --git a/Makefile b/Makefile
index 45273a6..e4d1636 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,7 @@
 JERBOA_HOME := $(CURDIR)
+PROJECT_VERSION ?= $(shell v=$$(tr -d '[:space:]' < VERSION 2>/dev/null || true); printf '%s\n' "$${v:-0.2.0}")
+HOST_UNAME_S := $(shell uname -s)
+HOST_UNAME_M := $(shell uname -m)
 CHEZ_BUILD_DIR ?= $(JERBOA_HOME)/build/chez
 CHEZ_PREFIX ?= $(JERBOA_HOME)/.chez
 SCHEME ?= $(CHEZ_PREFIX)/bin/scheme
@@ -82,7 +85,7 @@ help:
 	@echo "  unification-check Run manifest/report/provenance/diff/audit checks"
 	@echo "  jerboa-portable  Build multicall release targets: macos-arm64, linux-amd64, linux-arm64, freebsd-amd64"
 	@echo "                   (cross targets need: make chez-cross CHEZ_TARGET_MACHINE=ta6le|tarm64le|ta6fb ...)"
-	@echo "  release-artifact Package one built target: RELEASE_VERSION=v0.1.0 RELEASE_TARGET=linux-amd64"
+	@echo "  release-artifact Package one built target: RELEASE_VERSION=v$(PROJECT_VERSION) RELEASE_TARGET=linux-amd64"
 	@echo "  release-artifacts Build/package all release targets"
 	@echo "  release-upload   Upload dist/release artifacts to a SourceHut git tag via hut"
 	@echo "  native-cross     Cross-build Rust native lib for a target"
@@ -454,8 +457,8 @@ jerboa-portable: jerboa-macos-arm64 jerboa-linux-amd64 jerboa-linux-arm64 jerboa
 # Native SourceHut jobs call `release-artifact` after `make jerboa`. Local
 # maintainers can call `release-artifacts` to cross-build and package all
 # supported bootstrap toolchains in one pass.
-RELEASE_VERSION ?= $(shell git describe --tags --exact-match 2>/dev/null || git describe --tags --always --dirty 2>/dev/null || echo dev)
-RELEASE_TARGET ?= $(shell case "$$(uname -s)-$$(uname -m)" in Darwin-arm64) echo macos-arm64 ;; Linux-x86_64) echo linux-amd64 ;; Linux-aarch64|Linux-arm64) echo linux-arm64 ;; FreeBSD-amd64|FreeBSD-x86_64) echo freebsd-amd64 ;; *) echo unknown ;; esac)
+RELEASE_VERSION ?= $(shell git describe --tags --exact-match 2>/dev/null || printf 'v%s\n' "$(PROJECT_VERSION)")
+RELEASE_TARGET ?= $(shell host="$(HOST_UNAME_S)-$(HOST_UNAME_M)"; if [ "$$host" = Darwin-arm64 ]; then echo macos-arm64; elif [ "$$host" = Linux-x86_64 ]; then echo linux-amd64; elif [ "$$host" = Linux-aarch64 ] || [ "$$host" = Linux-arm64 ]; then echo linux-arm64; elif [ "$$host" = FreeBSD-amd64 ] || [ "$$host" = FreeBSD-x86_64 ]; then echo freebsd-amd64; else echo unknown; fi)
 RELEASE_TARGETS ?= macos-arm64 linux-amd64 linux-arm64 freebsd-amd64
 RELEASE_DIR ?= dist/release
 RELEASE_REPO ?= ~lisp/jerboa
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..0ea3a94
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+0.2.0
diff --git a/bin/jerboa b/bin/jerboa
index 0b79343..815d907 100755
--- a/bin/jerboa
+++ b/bin/jerboa
@@ -11,7 +11,8 @@ JERBOA_HOME="${JERBOA_HOME:-$(cd "$SCRIPT_DIR/.." && pwd)}"
 
 SCHEME="${SCHEME:-$JERBOA_HOME/.chez/bin/scheme}"
 LIBDIRS="$JERBOA_HOME/lib"
-VERSION="0.1.0"
+VERSION="$(tr -d '[:space:]' < "$JERBOA_HOME/VERSION" 2>/dev/null || true)"
+VERSION="${VERSION:-0.2.0}"
 
 if [ ! -x "$SCHEME" ]; then
     echo "Error: Chez Scheme not found at $SCHEME" >&2
diff --git a/data/cookbooks.sexp b/data/cookbooks.sexp
index 58e04a6..15739ff 100644
--- a/data/cookbooks.sexp
+++ b/data/cookbooks.sexp
@@ -5254,4 +5254,53 @@
      "reader-extension" "module-import" "colon-path")
    ("title"
      .
-     "Use Parenthesized Module Paths in Plain Chez REPLs")))
+     "Use Parenthesized Module Paths in Plain Chez REPLs"))
+ (("code"
+    .
+    ";; WRONG: a plain foreign-procedure that can BLOCK (TTY write, socket\n;; read, poll) pins the GC rendezvous -- any other thread that triggers\n;; a collection waits for the C call to return. Under tmux/vterm\n;; backpressure a tb_present write() stall froze ALL threads, including\n;; the SSE-streaming worker.\n(def c-tb-present-bad\n  (foreign-procedure \"jcode_tb_present\" () void))\n\n;; RIGHT: __collect_safe releases the GC/scheduler lock for the call's\n;; duration. Required for ANY C call that can sleep or block on I/O.\n(def c-tb-present\n  (foreign-procedure __collect_safe \"jcode_tb_present\" () void))\n\n;; Constraint: a __collect_safe C function must not call back into\n;; Scheme or allocate Scheme objects.") ("id" . "ffi-collect-safe-blocking-calls") ("imports")
+   ("notes"
+     .
+     "Symptoms: whole app freezes intermittently while one thread does I/O; `sample <pid>` shows other threads stuck in S_mutex_acquire/__psynch_mutexwait while one thread sits inside the foreign call. Audit every foreign-procedure wrapping read/write/poll/select/recv/accept/sleep. Found three times in jcode now: socket c-read (macOS freeze), tb_peek_event, tb_present.")
+   ("tags" "ffi" "collect_safe" "foreign-procedure" "blocking"
+     "GC" "threads" "freeze")
+   ("title"
+     .
+     "Declare blocking C calls __collect_safe or every Chez thread freezes"))
+ (("code"
+    .
+    "(define (slurp-file path)\n  (call-with-input-file path (lambda (p) (get-string-all p))))\n\n(define (str-find src needle start)\n  (let ([nl (string-length needle)] [sl (string-length src)])\n    (let loop ([i start])\n      (cond\n        [(> (+ i nl) sl) #f]\n        [(string=? (substring src i (+ i nl)) needle) i]\n        [else (loop (+ i 1))]))))\n\n;; For each MARKER occurrence, collect text up to the next double-quote.\n(define (collect-quoted src marker)\n  (let ([mlen (string-length marker)])\n    (let loop ([i 0] [acc '()])\n      (let ([hit (str-find src marker i)])\n        (if (not hit)\n          (reverse acc)\n          (let* ([s (+ hit mlen)] [q (str-find src \"\\\"\" s)])\n            (if (not q) (reverse acc)\n              (loop (+ q 1) (cons (substring src s q) acc)))))))))\n\n;; Example invariant: every dispatcher command appears in the\n;; completion list. Adding a clause without a popup entry fails CI.\n(let* ([dispatch (collect-quoted (slurp-file \"src/ui/tui.ss\") \"(equal? cmd \\\"\")]\n       [complete (collect-quoted (slurp-file \"src/ui/tui-input.ss\") \"(\\\"/\")])\n  (for-each (lambda (cmd) (assert (member cmd complete))) dispatch))") ("id" . "source-scan-sync-test") ("imports" "(chezscheme)")
+   ("notes"
+     .
+     "Useful when a runtime check is impossible (the dispatcher is a closed cond, the list lives in an FFI-importing module tests can't load). O(n*m) naive search is fine for source-sized files. To avoid false positives from similar text elsewhere, also check the chars FOLLOWING the closing quote (e.g. require \" cmd)\" after the match). Used in jcode to keep *slash-commands* completion in sync with handle-slash-command!.")
+   ("tags" "testing" "source-scan" "string-search" "sync"
+     "invariant" "completion")
+   ("title"
+     .
+     "Test that two source files stay in sync by scanning their text (no regex needed)"))
+ (("code"
+    .
+    "(import (jerboa prelude))\n(import (std net tcp))\n(import (std misc thread))\n\n(def (read-line/crlf in)\n  (let ((out (open-output-string)))\n    (let loop ()\n      (let ((c (read-char in)))\n        (cond\n          ((eof-object? c) (get-output-string out))\n          ((char=? c #\\newline)\n           (let ((s (get-output-string out)))\n             (if (and (> (string-length s) 0)\n                      (char=? (string-ref s (- (string-length s) 1)) #\\return))\n               (substring s 0 (- (string-length s) 1))\n               s)))\n          (else (write-char c out) (loop)))))))\n\n(def (post-lines-with-idle-timeout host port request on-line timeout-secs)\n  (let-values (((in out) (tcp-connect host port)))\n    (let ((last-activity (vector (time-second (current-time))))\n          (timed-out? (vector #f))\n          (done? (vector #f))\n          (closed? (vector #f)))\n      (def (touch!)\n        (vector-set! last-activity 0 (time-second (current-time))))\n      (def (close-once!)\n        (unless (vector-ref closed? 0)\n          (vector-set! closed? 0 #t)\n          (guard (e [(i/o-error? e) (void)]) (close-port in))\n          (guard (e [(i/o-error? e) (void)]) (close-port out))))\n      (fork-thread\n        (lambda ()\n          (let loop ()\n            (thread-sleep! 5)\n            (cond\n              ((vector-ref done? 0) (void))\n              ((>= (- (time-second (current-time))\n                      (vector-ref last-activity 0))\n                   timeout-secs)\n               (vector-set! timed-out? 0 #t)\n               (close-once!))\n              (else (loop))))))\n      (dynamic-wind\n        void\n        (lambda ()\n          (put-string out request)\n          (flush-output-port out)\n          (touch!)\n          (let loop ()\n            (let ((c (peek-char in)))\n              (touch!)\n              (unless (eof-object? c)\n                (let ((line (read-line/crlf in)))\n                  (touch!)\n                  (on-line line)\n                  (loop)))))\n          (when (vector-ref timed-out? 0)\n            (error 'post-lines-with-idle-timeout\n              \"stream read timed out\")))\n        (lambda ()\n          (vector-set! done? 0 #t)\n          (close-once!))))))") ("id" . "tcp-stream-idle-watchdog")
+   ("imports"
+     "(jerboa prelude)"
+     "(std net tcp)"
+     "(std misc thread)")
+   ("notes"
+     .
+     "Use thread-sleep! from (std misc thread), not sleep/make-time. Keep cleanup in dynamic-wind so ports close on normal return, errors, or timeout. Catch only i/o-error? around close-port so unexpected conditions are not silently converted into success.")
+   ("tags" "std net tcp" "std misc thread" "streaming"
+     "timeout" "watchdog" "close-port")
+   ("title"
+     .
+     "Add an idle-timeout watchdog around a blocking TCP stream"))
+ (("code"
+    .
+    "(import (jerboa prelude))\n(import (std misc thread))\n\n(def worker #f)\n(def done-seen-at #f)\n(def tick 0)\n\n(def (busy?)\n  (and worker (not (thread-done? worker))))\n\n(def (start-worker!)\n  (set! done-seen-at #f)\n  (set! worker\n    (spawn\n      (lambda ()\n        ;; Do blocking work here, then send a final event/mailbox message.\n        (thread-sleep! 1)))))\n\n(def (cleanup-stale-worker!)\n  ;; Call from the owner/main event loop after draining worker messages.\n  ;; The grace tick avoids racing a final message that was sent just before\n  ;; the worker exited but has not been drained yet.\n  (set! tick (+ tick 1))\n  (when (and worker (thread-done? worker))\n    (cond\n      (done-seen-at\n       (when (>= (- tick done-seen-at) 3)\n         (set! worker #f)))\n      (else\n       (set! done-seen-at tick)))))\n\n(start-worker!)\n(busy?)") ("id" . "spawn-worker-liveness")
+   ("imports" "(jerboa prelude)" "(std misc thread)")
+   ("notes"
+     .
+     "spawn returns a thread object in (std misc thread). Use thread-done? to distinguish a real live worker from stale UI busy state. Drain any worker mailbox/final events before cleanup, then require a short grace interval before clearing busy so a just-sent terminal event is not raced.")
+   ("tags" "std-misc-thread" "spawn" "thread-done" "worker"
+     "liveness" "busy-state")
+   ("title"
+     .
+     "Track spawned worker liveness with thread-done?")))
diff --git a/data/features.sexp b/data/features.sexp
index afa6506..4c276f6 100644
--- a/data/features.sexp
+++ b/data/features.sexp
@@ -1314,26 +1314,23 @@
    ("votes" . 0))
  (("description"
     .
-    "jerboa_verify and jerboa_compile_check crashed internally with `Exception in string-ref: <idx> is not a valid index for \"<file contents>\"` while checking edited large files including test/run.ss, src/jcode/ui/tui.ss, and src/jcode/ui/tui-input.ss. The files compiled and tests passed via make, so the verifier should return a normal diagnostic or success result instead of crashing and dumping large file contents.")
+    "jerboa_verify and jerboa_compile_check crashed internally with `Exception in string-ref: 100111 is not a valid index` while checking src/jcode/provider/provider.ss. The normal jerbuild build compiled the file successfully, so the MCP verifier appears to have a source-indexing or pre-scan bug on large files. The tool should return a compact diagnostic with the failing pass/name and a usable location, or fall back to compile-only verification instead of dumping the entire file string.")
    ("estimated_token_reduction"
      .
-     "500-1500 tokens per failure by avoiding fallback commands and huge file-content exception output.")
+     "~2000-5000 tokens per failure by avoiding huge file dumps and fallback investigation.")
    ("example_scenario"
      .
-     "During the jcode stream-error and /copy command work, MCP verification repeatedly crashed on edited files, requiring fallback to direct make test and make binary despite the code being valid.")
+     "After adding a TCP stream watchdog to src/jcode/provider/provider.ss, both jerboa_verify and jerboa_compile_check failed before compiling with string-ref index 100111, while jerboa_make build passed.")
    ("id" . "verify-large-file-string-ref-crash")
    ("impact" . "medium")
-   ("tags"
-     "verify"
-     "compile-check"
-     "large-files"
-     "diagnostics")
+   ("tags" "verify" "compile_check" "large-file" "diagnostics"
+     "source-index")
    ("title"
      .
-     "Fix jerboa_verify string-ref crash on large files")
+     "Report source location instead of crashing on large-file verify index errors")
    ("use_case"
      .
-     "Validating edited Jerboa files before running the full build/test cycle.")
+     "When validating a large Jerboa source file after an edit, agents need to distinguish user code errors from verifier/tooling crashes without falling back to noisy shell builds.")
    ("votes" . 0))
  (("description"
     .
@@ -1471,4 +1468,43 @@
    ("use_case"
      .
      "Avoid duplicate cookbook entries and quickly confirm whether a known pattern has already been saved.")
+   ("votes" . 0))
+ (("description"
+    .
+    "JSON boolean false (and null) in the jerboa dispatcher's args object arrives truthy (or as default) at the tool. Observed with balanced_replace: dry_run:false, dry_run omitted, and dry_run:null ALL run as dry-run, so the apply path is unreachable through MCP. Agents must fall back to plain text edits, losing the balance-validated apply. Likely the JSON->Scheme bridge maps false to a non-#f value or hash-ref treats #f as missing.")
+   ("estimated_token_reduction"
+     .
+     "Eliminates 2-3 fallback tool calls per edit; restores the mandated balanced-edit workflow (~600 tokens per .ss edit)")
+   ("example_scenario"
+     .
+     "Editing jcode .ss files per project policy: balanced_replace(dry_run:false) returned 'Balanced replace OK (dry-run)' three times; the edit was never applied. Had to apply via the harness Edit tool and re-verify with check_balance.")
+   ("id" . "dispatcher-boolean-false-args") ("impact" . "high")
+   ("tags" "jmcp" "dispatcher" "boolean" "json"
+     "balanced_replace")
+   ("title"
+     .
+     "jmcp dispatcher swallows boolean false in args (dry_run:false unreachable)")
+   ("use_case"
+     .
+     "Any dispatcher tool with a boolean arg that must be set to false (dry_run, recursive, case_sensitive).")
+   ("votes" . 0))
+ (("description"
+    .
+    "jerboa_compile_check fails on every file tried (pure-ASCII included) with 'Exception in string-ref: N is not a valid index' where N equals the file's character count -- an EOF off-by-one in the top-level form scanner (string-ref past the last char instead of checking bounds). This breaks the documented verification step for .ss edits; only check_balance still works, and real verification requires a full `make binary` (minutes instead of seconds).")
+   ("estimated_token_reduction"
+     .
+     "Restores a 1-call verification (~50 tokens) vs a multi-minute build with log truncation (~2k tokens)")
+   ("example_scenario"
+     .
+     "After editing jcode's checkpoints.ss and lsp.ss, compile_check crashed on both with string-ref 8294/11275 == file length. Fell back to make binary (2+ min) to validate.")
+   ("id" . "compile-check-eof-stringref-crash")
+   ("impact" . "high")
+   ("tags" "compile_check" "crash" "eof" "string-ref"
+     "verification")
+   ("title"
+     .
+     "jerboa_compile_check crashes: string-ref at index == file length")
+   ("use_case"
+     .
+     "Verifying any .ss edit before building, as mandated by jerboa-* project CLAUDE.md files.")
    ("votes" . 0)))
diff --git a/data/security-rules.sexp b/data/security-rules.sexp
index 7eed538..3d62a51 100644
--- a/data/security-rules.sexp
+++ b/data/security-rules.sexp
@@ -936,4 +936,13 @@
      .
      "extern\\s+void\\s+(open|close|flock|mmap|munmap|ftruncate|usleep)\\s*\\(")
    ("scope" . "c-shim")
-   ("severity" . "medium")))
+   ("severity" . "medium"))
+ (("id" . "ffi-blocking-call-not-collect-safe")
+   ("message"
+     .
+     "A foreign-procedure wrapping a potentially-blocking C call (I/O, poll, sleep) without __collect_safe pins the Chez GC rendezvous: while the C call blocks, ANY thread that triggers a collection stalls every Scheme thread in the process. Symptoms are whole-app freezes during I/O backpressure (slow TTY, stalled socket). Caused three jcode incidents: socket c-read (macOS TUI freeze), tb_peek_event, tb_present under tmux/vterm.")
+   ("pattern"
+     .
+     "foreign-procedure\\s+\"[^\"]*(read|write|recv|send|poll|select|accept|connect|sleep|wait|flush|peek|present)")
+   ("scope" . "ffi-boundary")
+   ("severity" . "high")))
diff --git a/docs/release-artifacts.md b/docs/release-artifacts.md
index 1c5cb8d..0f0eb51 100644
--- a/docs/release-artifacts.md
+++ b/docs/release-artifacts.md
@@ -25,9 +25,12 @@ jerboa-vX.Y.Z-<target>/bin/jlsp     -> jerboa
 ## Build Locally
 
 ```sh
-make release-artifacts RELEASE_VERSION=v0.1.0
+make release-artifacts
 ```
 
+By default, release packaging uses the exact git tag when building from a tag,
+or `v$(cat VERSION)` for local builds.
+
 This cross-builds and packages:
 
 ```text
@@ -73,7 +76,7 @@ git tag artifacts are the stable download channel.
 Package repos can vendor `support/ensure-jerboa.sh` and use:
 
 ```make
-JERBOA_VERSION ?= v0.1.0
+JERBOA_VERSION ?= v0.2.0
 JERBOA_BINDIR := .jerboa/bin
 
 .PHONY: install
diff --git a/jerboa-native-rs/Cargo.lock b/jerboa-native-rs/Cargo.lock
index 4b1e09d..11a7783 100644
--- a/jerboa-native-rs/Cargo.lock
+++ b/jerboa-native-rs/Cargo.lock
@@ -1333,7 +1333,7 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
 
 [[package]]
 name = "jerboa-native"
-version = "0.1.0"
+version = "0.2.0"
 dependencies = [
  "argon2",
  "duckdb",
diff --git a/jerboa-native-rs/Cargo.toml b/jerboa-native-rs/Cargo.toml
index dea55be..96d6877 100644
--- a/jerboa-native-rs/Cargo.toml
+++ b/jerboa-native-rs/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "jerboa-native"
-version = "0.1.0"
+version = "0.2.0"
 edition = "2021"
 
 [lib]
diff --git a/jpkg.sexp b/jpkg.sexp
index 9282ce9..8c5d619 100644
--- a/jpkg.sexp
+++ b/jpkg.sexp
@@ -1,9 +1,9 @@
 (package
   (name "@local/jerboa")
-  (version "0.1.0")
+  (version "0.2.0")
   (description "")
   (license "UNLICENSED")
-  (jerboa ">=0.1.0")
+  (jerboa ">=0.2.0")
   (modules ((root "src")))
   (dependencies ())
   (dev-dependencies ())
diff --git a/lib/std/lsp/server.ss b/lib/std/lsp/server.ss
index 9cfc0bc..4a54bc4 100644
--- a/lib/std/lsp/server.ss
+++ b/lib/std/lsp/server.ss
@@ -104,7 +104,7 @@
       "textDocumentSync" (make-json-obj "openClose" #t "change" 1)
       "completionProvider" (make-json-obj "triggerCharacters" '("(" " " "-"))
       "hoverProvider" #t)
-     "serverInfo" (make-json-obj "name" "jerboa-lsp" "version" "0.1.0")))
+     "serverInfo" (make-json-obj "name" "jerboa-lsp" "version" "0.2.0")))
 
   (def (handle-did-open state params)
     (let* ([td (jref params "textDocument")]
diff --git a/lib/std/pkg/cli.ss b/lib/std/pkg/cli.ss
index adc43eb..01b5063 100644
--- a/lib/std/pkg/cli.ss
+++ b/lib/std/pkg/cli.ss
@@ -31,7 +31,7 @@
                 cmd-build cmd-clean cmd-policy
                 cmd-audit cmd-search cmd-dir))
 
-  (def jpkg-version "0.1.0")
+  (def jpkg-version "0.2.0")
 
   ;; ── command table ──────────────────────────────────────────────────────
   ;; Each entry: (name synopsis one-line-description handler)
diff --git a/lsp/handlers/lifecycle.ss b/lsp/handlers/lifecycle.ss
index 4aa0bcf..703bc4d 100644
--- a/lsp/handlers/lifecycle.ss
+++ b/lsp/handlers/lifecycle.ss
@@ -28,7 +28,7 @@
       ;; Return capabilities
       (json-obj
         "capabilities" (server-capabilities)
-        "serverInfo" (json-obj "name" "jerboa-lsp" "version" "0.1.0"))))
+        "serverInfo" (json-obj "name" "jerboa-lsp" "version" "0.2.0"))))
 
   (define (handle-initialized state params)
     ;; Index workspace in background after initialization
diff --git a/lsp/main-binary.ss b/lsp/main-binary.ss
index ae4a6d7..ae5d4dd 100644
--- a/lsp/main-binary.ss
+++ b/lsp/main-binary.ss
@@ -23,7 +23,7 @@
                (loop (cddr args) (cons (cons 'log-level (cadr args)) opts)))
              (loop (cdr args) opts))]
           [(string=? arg "--version")
-           (display "jerboa-lsp 0.1.0\n")
+           (display "jerboa-lsp 0.2.0\n")
            (exit 0)]
           [(string=? arg "--help")
            (display "Usage: jerboa-lsp [--stdio] [--log-level debug|info|warn|error] [--version]\n")
diff --git a/lsp/main.ss b/lsp/main.ss
index 4a59255..e683987 100644
--- a/lsp/main.ss
+++ b/lsp/main.ss
@@ -40,7 +40,7 @@
                (loop (cddr args) (cons (cons 'log-level (cadr args)) opts)))
              (loop (cdr args) opts))]
           [(string=? arg "--version")
-           (display "jerboa-lsp 0.1.0\n")
+           (display "jerboa-lsp 0.2.0\n")
            (exit 0)]
           [(string=? arg "--help")
            (display "Usage: jerboa-lsp [--stdio] [--log-level debug|info|warn|error] [--version]\n")
diff --git a/mcp/server.ss b/mcp/server.ss
index e3cd519..edc4f4c 100644
--- a/mcp/server.ss
+++ b/mcp/server.ss
@@ -8,7 +8,7 @@
         (std misc string))
 
 (def server-name "jerboa-mcp")
-(def server-version "0.1.0")
+(def server-version "0.2.0")
 (def result-marker "JERBOA-MCP-RESULT:")
 (def error-marker "JERBOA-MCP-ERROR:")
 (def stdout-marker "JERBOA-MCP-STDOUT:")
diff --git a/support/binary-entry.ss b/support/binary-entry.ss
index 71a16bd..cd3378d 100644
--- a/support/binary-entry.ss
+++ b/support/binary-entry.ss
@@ -31,7 +31,7 @@
             (loop)])))]
     [(or (string=? (car args) "--version")
          (string=? (car args) "-v"))
-     (displayln "jerboa-bin 0.1.0")
+     (displayln "jerboa-bin 0.2.0")
      (displayln (string-append "Bundled " (scheme-version)
                                " (Apache 2.0, (c) Cisco Systems, Inc.)"))
      (displayln "See LICENSE-CHEZ for Chez Scheme's NOTICE and license.")]
diff --git a/support/build-jerboa-multicall.ss b/support/build-jerboa-multicall.ss
index 7ab6fb1..e4e844b 100644
--- a/support/build-jerboa-multicall.ss
+++ b/support/build-jerboa-multicall.ss
@@ -62,6 +62,18 @@
         (let ([c (get-string-n in 4096)])
           (if (eof-object? c) (apply string-append (reverse acc)) (lp (cons c acc))))))))
 
+(define (read-project-version repo)
+  (let ([v (getenv "JERBOA_PROJECT_VERSION")])
+    (if (and v (> (string-length v) 0))
+        v
+        (let ([path (format "~a/VERSION" repo)])
+          (if (file-exists? path)
+              (let ([file-version (trim-ws (read-text-file path))])
+                (if (> (string-length file-version) 0)
+                    file-version
+                    "0.2.0"))
+              "0.2.0")))))
+
 (define (find-csv-dir lib-dir machine)
   (let lp ([entries (directory-list* lib-dir)])
     (cond
@@ -210,7 +222,7 @@
                       (only (std pkg cli) jpkg-main))
              out)
       (newline out)
-      (write '(let ([mode (or (getenv "JERBOA_MULTICALL_NAME") "jerboa")]
+      (write `(let ([mode (or (getenv "JERBOA_MULTICALL_NAME") "jerboa")]
                     [args (command-line-arguments)])
                 (cond
                   [(string=? mode "jmcp") (mcp-main)]
@@ -231,7 +243,7 @@
                               (unless (eq? result (void)) (write result) (newline))))
                           (loop)])))]
                   [(or (string=? (car args) "--version") (string=? (car args) "-v"))
-                   (displayln "jerboa 0.1.0 (multicall: jerboa/jmcp/jlsp/jerbuild/jpkg)")
+                   (displayln ,(string-append "jerboa " project-version " (multicall: jerboa/jmcp/jlsp/jerbuild/jpkg)"))
                    (displayln (string-append "Bundled " (scheme-version)
                                              " (Apache 2.0, (c) Cisco Systems, Inc.)"))
                    (displayln "See LICENSE-CHEZ for Chez Scheme's NOTICE and license.")]
@@ -258,6 +270,7 @@
                                   script
                                   (format "~a/~a" (current-directory) script))])
                     (path-parent (path-parent abs)))))
+(define project-version (read-project-version repo))
 ;; ── cross-compilation params (mirror support/build-jerbuild.sh) ───────────────
 ;; TARGET_MACHINE set => cross build. JERBOA_CROSS_PREFIX is the install prefix
 ;; of the cross-built Chez (`.chez-cross-<mt>`, holding lib/csv*/<mt>/...) and
diff --git a/support/ensure-jerboa.sh b/support/ensure-jerboa.sh
index fcb7006..b402840 100755
--- a/support/ensure-jerboa.sh
+++ b/support/ensure-jerboa.sh
@@ -2,10 +2,10 @@
 # Bootstrap a project-local Jerboa toolchain from SourceHut release artifacts.
 #
 # Usage:
-#   support/ensure-jerboa.sh v0.1.0 .jerboa/bin
+#   support/ensure-jerboa.sh v0.2.0 .jerboa/bin
 #
 # Override the artifact location with:
-#   JERBOA_RELEASE_BASE=https://example.org/releases/v0.1.0
+#   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:
diff --git a/support/jerboa-embed.c b/support/jerboa-embed.c
index 9cfc680..68686b0 100644
--- a/support/jerboa-embed.c
+++ b/support/jerboa-embed.c
@@ -274,5 +274,5 @@ void jerboa_error_free(jerboa_error_t *err) {
 }
 
 const char *jerboa_version(void) {
-    return "jerboa 0.1.0";
+    return "jerboa 0.2.0";
 }