Fix Shift-Tab plan/build toggle and harden build pipeline

ober

557e329c95b24743c4e32bed21f7d8ec8620ef42

diff --git a/Makefile b/Makefile
index 0fecfaa..6abb30b 100644
--- a/Makefile
+++ b/Makefile
@@ -10,11 +10,43 @@ TUI_SHIM_DIR   := $(CURDIR)/vendor/termbox2
 NATIVE_LIB_DIR := $(JERBOA_HOME)/lib
 LDPATH         := $(SHIM_DIR):$(TUI_SHIM_DIR):$(SQLITE_LIB_DIR):$(NATIVE_LIB_DIR)
 
-.PHONY: all build gen run test clean repl binary install tui-shim run-tui jcode-musl linux linux-check linux-local docker
+.PHONY: all build gen run test clean repl binary install tui-shim run-tui jcode-musl linux linux-check linux-local docker purge-stale sqlite-shim
 
 all: build
 
-gen:
+# ── 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
+# to the class of bug where a stale .so silently linked against an old
+# shim causes a runtime "no entry for X" foreign-procedure error.
+#
+# Always also clear the prebuilt vendor/chez-sqlite artifacts — they pin
+# absolute paths to the shim and are cheap to recompile.
+purge-stale:
+	@find lib -name "*.so" -o -name "*.wpo" 2>/dev/null | while read f; do \
+	  src=$$(echo $$f | sed -e 's|^lib/|src/|' -e 's|\.so$$|.ss|' -e 's|\.wpo$$|.ss|'); \
+	  sls=$$(echo $$f | sed 's|\.\(so\|wpo\)$$|.sls|'); \
+	  if [ ! -f "$$src" ] && [ ! -f "$$sls" ]; then rm -f "$$f"; \
+	  elif [ -f "$$src" ] && [ "$$src" -nt "$$f" ]; then rm -f "$$f"; \
+	  elif [ -f "$$sls" ] && [ "$$sls" -nt "$$f" ]; then rm -f "$$f"; \
+	  fi; \
+	done
+	@rm -f vendor/chez-sqlite/src/chez-sqlite.so vendor/chez-sqlite/src/chez-sqlite.wpo
+
+# ── chez-sqlite shim build ──────────────────────────────────────────────────
+# Rebuild the FFI shim if its C source is newer than the .so. This catches
+# the case where vendor/chez-sqlite is updated but the .so isn't refreshed.
+sqlite-shim:
+	@if [ ! -f vendor/chez-sqlite/chez_sqlite_shim.so ] || \
+	    [ vendor/chez-sqlite/chez_sqlite_shim.c -nt vendor/chez-sqlite/chez_sqlite_shim.so ]; then \
+	  echo "=== Rebuilding chez_sqlite_shim.so ==="; \
+	  cc -shared -fPIC -O2 \
+	    -I$(SQLITE_LIB_DIR)/../include -L$(SQLITE_LIB_DIR) \
+	    -o vendor/chez-sqlite/chez_sqlite_shim.so \
+	    vendor/chez-sqlite/chez_sqlite_shim.c -lsqlite3; \
+	fi
+
+gen: purge-stale sqlite-shim
 	$(JERBUILD) src lib
 
 build: gen
diff --git a/lib/jcode/provider/provider.sls b/lib/jcode/provider/provider.sls
index f789f2b..a92f3cd 100644
--- a/lib/jcode/provider/provider.sls
+++ b/lib/jcode/provider/provider.sls
@@ -10,10 +10,10 @@
     (except (chezscheme) make-hash-table hash-table? iota \x31;+ \x31;-
       getenv path-extension path-absolute? thread? make-mutex
       mutex? mutex-name)
-    (std text json) (std net request) (std net tls-rustls)
-    (std net tcp) (std misc string) (std misc retry)
-    (jcode core log) (jcode core message) (jerboa core)
-    (jerboa runtime))
+    (std text json) (except (std net request) http-post-stream)
+    (std net tls-rustls) (std net tcp) (std misc string)
+    (std misc retry) (jcode core log) (jcode core message)
+    (jerboa core) (jerboa runtime))
   (def logger (make-logger "provider"))
   (def *api-retry-policy* (make-retry-policy 3 1.0 30.0 #t))
   (def (retryable-error? e)
diff --git a/lib/jcode/ui/tui-ffi.sls b/lib/jcode/ui/tui-ffi.sls
index 77329e3..12b3116 100644
--- a/lib/jcode/ui/tui-ffi.sls
+++ b/lib/jcode/ui/tui-ffi.sls
@@ -17,23 +17,24 @@
    TB_KEY_HOME TB_KEY_END TB_KEY_PGUP TB_KEY_PGDN
    TB_KEY_ARROW_UP TB_KEY_ARROW_DOWN TB_KEY_ARROW_LEFT
    TB_KEY_ARROW_RIGHT TB_KEY_BACKSPACE TB_KEY_BACKSPACE2
-   TB_KEY_TAB TB_KEY_ENTER TB_KEY_ESC TB_KEY_SPACE
-   TB_KEY_MOUSE_LEFT TB_KEY_MOUSE_RIGHT TB_KEY_MOUSE_MIDDLE
-   TB_KEY_MOUSE_RELEASE TB_KEY_MOUSE_WHEEL_UP
-   TB_KEY_MOUSE_WHEEL_DOWN TB_KEY_CTRL_A TB_KEY_CTRL_B
-   TB_KEY_CTRL_C TB_KEY_CTRL_D TB_KEY_CTRL_E TB_KEY_CTRL_F
-   TB_KEY_CTRL_G TB_KEY_CTRL_H TB_KEY_CTRL_I TB_KEY_CTRL_J
-   TB_KEY_CTRL_K TB_KEY_CTRL_L TB_KEY_CTRL_M TB_KEY_CTRL_N
-   TB_KEY_CTRL_O TB_KEY_CTRL_P TB_KEY_CTRL_Q TB_KEY_CTRL_R
-   TB_KEY_CTRL_S TB_KEY_CTRL_T TB_KEY_CTRL_U TB_KEY_CTRL_V
-   TB_KEY_CTRL_W TB_KEY_CTRL_X TB_KEY_CTRL_Y TB_KEY_CTRL_Z
-   TB_MOD_ALT TB_MOD_SHIFT TB_MOD_CTRL TB_MOD_MOTION
-   TB_INPUT_CURRENT TB_INPUT_ESC TB_INPUT_ALT TB_INPUT_MOUSE
-   TB_OUTPUT_CURRENT TB_OUTPUT_NORMAL TB_OUTPUT_256
-   TB_OUTPUT_216 TB_OUTPUT_GRAYSCALE TB_OUTPUT_TRUECOLOR
-   TB_DEFAULT TB_BLACK TB_RED TB_GREEN TB_YELLOW TB_BLUE
-   TB_MAGENTA TB_CYAN TB_WHITE TB_BOLD TB_UNDERLINE TB_REVERSE
-   TB_ITALIC TB_BLINK TB_DIM TB_HIDE_CURSOR)
+   TB_KEY_TAB TB_KEY_BACK_TAB TB_KEY_ENTER TB_KEY_ESC
+   TB_KEY_SPACE TB_KEY_MOUSE_LEFT TB_KEY_MOUSE_RIGHT
+   TB_KEY_MOUSE_MIDDLE TB_KEY_MOUSE_RELEASE
+   TB_KEY_MOUSE_WHEEL_UP TB_KEY_MOUSE_WHEEL_DOWN TB_KEY_CTRL_A
+   TB_KEY_CTRL_B TB_KEY_CTRL_C TB_KEY_CTRL_D TB_KEY_CTRL_E
+   TB_KEY_CTRL_F TB_KEY_CTRL_G TB_KEY_CTRL_H TB_KEY_CTRL_I
+   TB_KEY_CTRL_J TB_KEY_CTRL_K TB_KEY_CTRL_L TB_KEY_CTRL_M
+   TB_KEY_CTRL_N TB_KEY_CTRL_O TB_KEY_CTRL_P TB_KEY_CTRL_Q
+   TB_KEY_CTRL_R TB_KEY_CTRL_S TB_KEY_CTRL_T TB_KEY_CTRL_U
+   TB_KEY_CTRL_V TB_KEY_CTRL_W TB_KEY_CTRL_X TB_KEY_CTRL_Y
+   TB_KEY_CTRL_Z TB_MOD_ALT TB_MOD_SHIFT TB_MOD_CTRL
+   TB_MOD_MOTION TB_INPUT_CURRENT TB_INPUT_ESC TB_INPUT_ALT
+   TB_INPUT_MOUSE TB_OUTPUT_CURRENT TB_OUTPUT_NORMAL
+   TB_OUTPUT_256 TB_OUTPUT_216 TB_OUTPUT_GRAYSCALE
+   TB_OUTPUT_TRUECOLOR TB_DEFAULT TB_BLACK TB_RED TB_GREEN
+   TB_YELLOW TB_BLUE TB_MAGENTA TB_CYAN TB_WHITE TB_BOLD
+   TB_UNDERLINE TB_REVERSE TB_ITALIC TB_BLINK TB_DIM
+   TB_HIDE_CURSOR)
   (import
     (except (chezscheme) make-hash-table hash-table? iota \x31;+ \x31;-
       getenv path-extension path-absolute? thread? make-mutex
@@ -88,18 +89,19 @@
   (def TB_KEY_ARROW_DOWN (- 65535 19))
   (def TB_KEY_ARROW_LEFT (- 65535 20))
   (def TB_KEY_ARROW_RIGHT (- 65535 21))
+  (def TB_KEY_BACK_TAB (- 65535 22))
   (def TB_KEY_BACKSPACE 8)
   (def TB_KEY_BACKSPACE2 127)
   (def TB_KEY_TAB 9)
   (def TB_KEY_ENTER 13)
   (def TB_KEY_ESC 27)
   (def TB_KEY_SPACE 32)
-  (def TB_KEY_MOUSE_LEFT (- 65535 22))
-  (def TB_KEY_MOUSE_RIGHT (- 65535 23))
-  (def TB_KEY_MOUSE_MIDDLE (- 65535 24))
-  (def TB_KEY_MOUSE_RELEASE (- 65535 25))
-  (def TB_KEY_MOUSE_WHEEL_UP (- 65535 26))
-  (def TB_KEY_MOUSE_WHEEL_DOWN (- 65535 27))
+  (def TB_KEY_MOUSE_LEFT (- 65535 23))
+  (def TB_KEY_MOUSE_RIGHT (- 65535 24))
+  (def TB_KEY_MOUSE_MIDDLE (- 65535 25))
+  (def TB_KEY_MOUSE_RELEASE (- 65535 26))
+  (def TB_KEY_MOUSE_WHEEL_UP (- 65535 27))
+  (def TB_KEY_MOUSE_WHEEL_DOWN (- 65535 28))
   (def TB_KEY_CTRL_A 1)
   (def TB_KEY_CTRL_B 2)
   (def TB_KEY_CTRL_C 3)
diff --git a/lib/jcode/ui/tui-keys.sls b/lib/jcode/ui/tui-keys.sls
index e008d7a..dfcbe74 100644
--- a/lib/jcode/ui/tui-keys.sls
+++ b/lib/jcode/ui/tui-keys.sls
@@ -49,6 +49,7 @@
                 "alt-enter"
                 "enter")]
            [(= key TB_KEY_TAB) "tab"]
+           [(= key TB_KEY_BACK_TAB) "shift-tab"]
            [(= key TB_KEY_ESC) "escape"]
            [(= key TB_KEY_SPACE)
             (if (not (zero? (bitwise-and mod TB_MOD_CTRL)))
diff --git a/lib/jcode/ui/tui.sls b/lib/jcode/ui/tui.sls
index bd673e1..4ab8b60 100644
--- a/lib/jcode/ui/tui.sls
+++ b/lib/jcode/ui/tui.sls
@@ -230,9 +230,8 @@
             (tui-log "  -> cycle-theme")
             (cycle-theme!)
             (app-state-dirty?-set! state #t)]
-           [(and (= key TB_KEY_TAB)
-                 (not (zero? (bitwise-and mod TB_MOD_SHIFT))))
-            (tui-log "  -> toggle-mode (shift-tab)")
+           [(= key TB_KEY_BACK_TAB)
+            (tui-log "  -> toggle-mode (back-tab)")
             (toggle-mode! state)
             (app-state-dirty?-set! state #t)]
            [(= key TB_KEY_PGUP)
diff --git a/src/jcode/provider/provider.ss b/src/jcode/provider/provider.ss
index 0e3a122..bc70721 100644
--- a/src/jcode/provider/provider.ss
+++ b/src/jcode/provider/provider.ss
@@ -9,7 +9,7 @@
         provider-model)
 
 (import :std/text/json
-        :std/net/request
+        (except (std net request) http-post-stream)
         :std/net/tls-rustls
         :std/net/tcp
         :std/misc/string
diff --git a/src/jcode/ui/jcode_tui_shim.c b/src/jcode/ui/jcode_tui_shim.c
index e6f819e..dc36a76 100644
--- a/src/jcode/ui/jcode_tui_shim.c
+++ b/src/jcode/ui/jcode_tui_shim.c
@@ -11,12 +11,58 @@
 #define TB_OPT_ATTR_W 32
 #define TB_IMPL
 #include "termbox2.h"
+#include <string.h>
 
 /* Shared event struct for peek/poll results */
 static struct tb_event last_event;
 
+/* Pre-extract hook: explicitly recognize escape sequences that terminfo
+ * may not have loaded reliably. Currently:
+ *   \x1b[Z      → Shift-Tab (TB_KEY_BACK_TAB)
+ *   \x1b[1;2Z   → Shift-Tab in modifyOtherKeys mode
+ *
+ * Termbox2's `global` is file-scope static; this shim is compiled in the
+ * same translation unit (TB_IMPL), so we can access it directly.
+ *
+ * Returning TB_OK tells termbox to consume `*consumed` bytes and use the
+ * populated event. Returning TB_ERR falls through to its normal parsing. */
+static int jcode_tb_extract_esc_pre(struct tb_event *event, size_t *consumed) {
+    if (global.in.len >= 3 &&
+        (unsigned char)global.in.buf[0] == 0x1b &&
+        global.in.buf[1] == '[' &&
+        global.in.buf[2] == 'Z') {
+        event->type = TB_EVENT_KEY;
+        event->key  = TB_KEY_BACK_TAB;
+        event->ch   = 0;
+        event->mod  = 0;
+        *consumed   = 3;
+        return TB_OK;
+    }
+    if (global.in.len >= 6 &&
+        (unsigned char)global.in.buf[0] == 0x1b &&
+        global.in.buf[1] == '[' &&
+        global.in.buf[2] == '1' &&
+        global.in.buf[3] == ';' &&
+        global.in.buf[4] == '2' &&
+        global.in.buf[5] == 'Z') {
+        event->type = TB_EVENT_KEY;
+        event->key  = TB_KEY_BACK_TAB;
+        event->ch   = 0;
+        event->mod  = 0;
+        *consumed   = 6;
+        return TB_OK;
+    }
+    return TB_ERR;
+}
+
 /* ---- Lifecycle ---- */
-int  jcode_tb_init(void)       { return tb_init(); }
+int jcode_tb_init(void) {
+    int rv = tb_init();
+    if (rv == TB_OK) {
+        tb_set_func(TB_FUNC_EXTRACT_PRE, jcode_tb_extract_esc_pre);
+    }
+    return rv;
+}
 void jcode_tb_shutdown(void)   { tb_shutdown(); }
 
 /* ---- Dimensions ---- */
diff --git a/src/jcode/ui/tui-ffi.ss b/src/jcode/ui/tui-ffi.ss
index 738a890..26f968c 100644
--- a/src/jcode/ui/tui-ffi.ss
+++ b/src/jcode/ui/tui-ffi.ss
@@ -26,7 +26,7 @@
   TB_KEY_INSERT TB_KEY_DELETE TB_KEY_HOME TB_KEY_END
   TB_KEY_PGUP TB_KEY_PGDN
   TB_KEY_ARROW_UP TB_KEY_ARROW_DOWN TB_KEY_ARROW_LEFT TB_KEY_ARROW_RIGHT
-  TB_KEY_BACKSPACE TB_KEY_BACKSPACE2 TB_KEY_TAB TB_KEY_ENTER
+  TB_KEY_BACKSPACE TB_KEY_BACKSPACE2 TB_KEY_TAB TB_KEY_BACK_TAB TB_KEY_ENTER
   TB_KEY_ESC TB_KEY_SPACE
   ;; Constants — mouse
   TB_KEY_MOUSE_LEFT TB_KEY_MOUSE_RIGHT TB_KEY_MOUSE_MIDDLE
@@ -140,6 +140,7 @@
 (def TB_KEY_ARROW_DOWN  (- #xFFFF 19))
 (def TB_KEY_ARROW_LEFT  (- #xFFFF 20))
 (def TB_KEY_ARROW_RIGHT (- #xFFFF 21))
+(def TB_KEY_BACK_TAB    (- #xFFFF 22))
 (def TB_KEY_BACKSPACE   #x08)
 (def TB_KEY_BACKSPACE2  #x7F)
 (def TB_KEY_TAB         #x09)
@@ -148,12 +149,12 @@
 (def TB_KEY_SPACE       #x20)
 
 ;; Mouse keys
-(def TB_KEY_MOUSE_LEFT       (- #xFFFF 22))
-(def TB_KEY_MOUSE_RIGHT      (- #xFFFF 23))
-(def TB_KEY_MOUSE_MIDDLE     (- #xFFFF 24))
-(def TB_KEY_MOUSE_RELEASE    (- #xFFFF 25))
-(def TB_KEY_MOUSE_WHEEL_UP   (- #xFFFF 26))
-(def TB_KEY_MOUSE_WHEEL_DOWN (- #xFFFF 27))
+(def TB_KEY_MOUSE_LEFT       (- #xFFFF 23))
+(def TB_KEY_MOUSE_RIGHT      (- #xFFFF 24))
+(def TB_KEY_MOUSE_MIDDLE     (- #xFFFF 25))
+(def TB_KEY_MOUSE_RELEASE    (- #xFFFF 26))
+(def TB_KEY_MOUSE_WHEEL_UP   (- #xFFFF 27))
+(def TB_KEY_MOUSE_WHEEL_DOWN (- #xFFFF 28))
 
 ;; Ctrl keys
 (def TB_KEY_CTRL_A #x01) (def TB_KEY_CTRL_B #x02)
diff --git a/src/jcode/ui/tui-keys.ss b/src/jcode/ui/tui-keys.ss
index 47f28e3..9de5d45 100644
--- a/src/jcode/ui/tui-keys.ss
+++ b/src/jcode/ui/tui-keys.ss
@@ -50,6 +50,7 @@
       ((= key TB_KEY_ENTER)      (if (not (zero? (bitwise-and mod TB_MOD_ALT)))
                                    "alt-enter" "enter"))
       ((= key TB_KEY_TAB)        "tab")
+      ((= key TB_KEY_BACK_TAB)   "shift-tab")
       ((= key TB_KEY_ESC)        "escape")
       ((= key TB_KEY_SPACE)      (if (not (zero? (bitwise-and mod TB_MOD_CTRL)))
                                    "ctrl-space" "space"))
diff --git a/src/jcode/ui/tui.ss b/src/jcode/ui/tui.ss
index c9d4784..208c66b 100644
--- a/src/jcode/ui/tui.ss
+++ b/src/jcode/ui/tui.ss
@@ -327,9 +327,9 @@
        (cycle-theme!)
        (app-state-dirty?-set! state #t))
 
-      ;; Global: Shift-Tab toggle PLAN/BUILD mode (opencode-style)
-      ((and (= key TB_KEY_TAB) (not (zero? (bitwise-and mod TB_MOD_SHIFT))))
-       (tui-log "  -> toggle-mode (shift-tab)")
+      ;; Global: Shift-Tab (BACK_TAB) toggle PLAN/BUILD mode (opencode-style)
+      ((= key TB_KEY_BACK_TAB)
+       (tui-log "  -> toggle-mode (back-tab)")
        (toggle-mode! state)
        (app-state-dirty?-set! state #t))