Initial modern minimal jsh build

ober

95d5a47af2020ba2dbd68b4921377deb8469b713

diff --git a/.build.yml b/.build.yml
new file mode 100644
index 0000000..51312d1
--- /dev/null
+++ b/.build.yml
@@ -0,0 +1,29 @@
+image: debian/stable
+packages:
+  - build-essential
+  - libncurses-dev
+  - uuid-dev
+  - libz-dev
+  - liblz4-dev
+  - libpcre2-dev
+  - libssl-dev
+  - libx11-dev
+  - pkg-config
+  - git
+  - curl
+  - unzip
+  - zip
+sources:
+  - https://git.sr.ht/~lisp/jerboa
+  - https://git.sr.ht/~lisp/jerboa-shell
+tasks:
+  - build-jerboa-tools: |
+      cd jerboa
+      # Build the multicall Jerboa tool artifact used by jerboa-shell.
+      make jerboa
+  - build-shell: |
+      cd jerboa-shell
+      JERBUILD=$HOME/jerboa/dist/jerbuild make jsh-compile
+  - test-shell: |
+      cd jerboa-shell
+      JERBUILD=$HOME/jerboa/dist/jerbuild make test
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..3fcabe4
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,40 @@
+# Build artifacts — rebuilt inside container
+**/*.so
+**/*.wpo
+**/*.o
+**/*.a
+*.boot
+jsh
+jsh-musl
+jsh-musl.sha256
+jsh-macos
+jsh-macos.sha256
+jsh-linux-amd64
+jsh-linux-amd64.sha256
+jsh-freebsd
+jsh-freebsd.sha256
+jsh-android
+jsh-android.sha256
+jsh_program.h
+jsh_petite_boot.h
+jsh_scheme_boot.h
+jsh_jsh_boot.h
+
+# Staging dirs — local build output, rebuilt fresh inside the container.
+# _jerbuild-stage holds macOS ar-merge output incl. a mode-000 __.SYMDEF that
+# breaks Podman's context tar if not excluded.
+_jerbuild-stage/
+coreutils-stage/
+ssl-stage/
+aws-stage/
+android-stage/
+
+# Vendored deps — cloned fresh inside container from GitHub
+vendor/
+
+# VCS
+.git
+
+# Docker
+Dockerfile
+.dockerignore
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..4d491ac
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+*.sha256 binary
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..211917c
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,388 @@
+name: CI
+
+on:
+  push:
+    branches: [master]
+  pull_request:
+
+permissions:
+  contents: write
+
+jobs:
+  test:
+    runs-on: ubuntu-latest
+    container:
+      image: jerboa21/jerboa:latest
+    steps:
+      - uses: actions/checkout@v4
+      - name: Run unit tests
+        run: make test JERBOA=/build/mine/jerboa/lib SCHEME=scheme
+
+  rust-musl:
+    runs-on: ubuntu-latest
+    container:
+      image: jerboa21/jerboa:latest
+    steps:
+      - uses: actions/checkout@v4
+      - name: Cache Rust registry
+        uses: actions/cache@v4
+        with:
+          path: ~/.cargo/registry
+          key: rust-musl-${{ hashFiles('patches/regex_native.rs', 'rust-coreutils/Cargo.lock') }}
+          restore-keys: rust-musl-
+      - name: Build Rust libs (x86_64-musl)
+        run: |
+          cd /build/mine/jerboa/jerboa-native-rs
+          cp $GITHUB_WORKSPACE/patches/regex_native.rs src/regex_native.rs
+          grep -q '#[cfg(feature = "duckdb")]' src/lib.rs || \
+            sed -i 's/^mod duckdb_native;/#[cfg(feature = "duckdb")]\nmod duckdb_native;/' src/lib.rs
+          RUSTFLAGS="--remap-path-prefix $HOME/.cargo/registry/src=crate" \
+          cargo build --release --target x86_64-unknown-linux-musl --no-default-features --features "tls crypto sqlite"
+          strip -S target/x86_64-unknown-linux-musl/release/libjerboa_native.a
+          cp target/x86_64-unknown-linux-musl/release/libjerboa_native.a $GITHUB_WORKSPACE/
+
+          cd $GITHUB_WORKSPACE/rust-coreutils
+          RUSTFLAGS="--remap-path-prefix $HOME/.cargo/registry/src=crate" \
+          cargo build --release --target x86_64-unknown-linux-musl
+          strip -S target/x86_64-unknown-linux-musl/release/libjsh_coreutils.a
+          cp target/x86_64-unknown-linux-musl/release/libjsh_coreutils.a $GITHUB_WORKSPACE/
+      - uses: actions/upload-artifact@v4
+        with:
+          name: rust-libs-musl
+          path: |
+            libjerboa_native.a
+            libjsh_coreutils.a
+          retention-days: 7
+
+  rust-freebsd:
+    runs-on: ubuntu-latest
+    container:
+      image: jerboa21/jerboa:latest
+    steps:
+      - uses: actions/checkout@v4
+      - name: Cache Rust registry
+        uses: actions/cache@v4
+        with:
+          path: ~/.cargo/registry
+          key: rust-freebsd-${{ hashFiles('patches/regex_native.rs', 'rust-coreutils/Cargo.lock') }}
+          restore-keys: rust-freebsd-
+      - name: Install FreeBSD cross target
+        run: rustup target add x86_64-unknown-freebsd
+      - name: Cache FreeBSD sysroot
+        id: cache-freebsd-sysroot
+        uses: actions/cache@v4
+        with:
+          path: /opt/freebsd-sysroot
+          key: freebsd-sysroot-14.4-v1
+      - name: Install FreeBSD sysroot link libs
+        if: steps.cache-freebsd-sysroot.outputs.cache-hit != 'true'
+        run: |
+          # Some transitive deps (e.g. crc-fast via uucore) declare cdylib and
+          # therefore need the FreeBSD link libs (libexecinfo, libkvm, ...).
+          # Extract just the libs from the FreeBSD 14.4 base release tarball.
+          # 14.0 was removed from download.freebsd.org; 14.4 is the latest 14.x.
+          command -v curl >/dev/null || (apt-get update -qq && apt-get install -y -qq curl xz-utils)
+          mkdir -p /opt/freebsd-sysroot
+          curl -fsSL --retry 3 -o /tmp/freebsd-base.txz \
+            https://download.freebsd.org/releases/amd64/14.4-RELEASE/base.txz
+          tar -xJf /tmp/freebsd-base.txz -C /opt/freebsd-sysroot ./lib ./usr/lib
+          rm -f /tmp/freebsd-base.txz
+      - name: Build Rust libs (x86_64-freebsd)
+        run: |
+          # libc.so on FreeBSD is a linker script that references /lib/libc.so.7
+          # and /usr/lib/libc_nonshared.a as ABSOLUTE paths. Plain `-L` flags
+          # don't redirect absolute paths — only `--sysroot` does. Without it,
+          # ld looks at the host's /lib/libc.so.7 (which doesn't exist on the
+          # Linux runner) and the link fails.
+          SYSROOT_FLAGS="-C link-arg=--sysroot=/opt/freebsd-sysroot"
+          LIB_FLAGS="-L /opt/freebsd-sysroot/usr/lib -L /opt/freebsd-sysroot/lib"
+
+          cd /build/mine/jerboa/jerboa-native-rs
+          cp $GITHUB_WORKSPACE/patches/regex_native.rs src/regex_native.rs
+          grep -q '#[cfg(feature = "duckdb")]' src/lib.rs || \
+            sed -i 's/^mod duckdb_native;/#[cfg(feature = "duckdb")]\nmod duckdb_native;/' src/lib.rs
+          # Drop cdylib for jerboa-native: only the static .a is needed by jsh-freebsd.
+          sed -i 's/^crate-type = \["cdylib", "staticlib"\]/crate-type = ["staticlib"]/' Cargo.toml
+          RUSTFLAGS="--remap-path-prefix $HOME/.cargo/registry/src=crate $SYSROOT_FLAGS $LIB_FLAGS" \
+          cargo build --release --target x86_64-unknown-freebsd --no-default-features
+          strip -S target/x86_64-unknown-freebsd/release/libjerboa_native.a
+          cp target/x86_64-unknown-freebsd/release/libjerboa_native.a $GITHUB_WORKSPACE/
+
+          cd $GITHUB_WORKSPACE/rust-coreutils
+          RUSTFLAGS="--remap-path-prefix $HOME/.cargo/registry/src=crate $SYSROOT_FLAGS $LIB_FLAGS" \
+          cargo build --release --target x86_64-unknown-freebsd
+          strip -S target/x86_64-unknown-freebsd/release/libjsh_coreutils.a
+          cp target/x86_64-unknown-freebsd/release/libjsh_coreutils.a $GITHUB_WORKSPACE/
+      - uses: actions/upload-artifact@v4
+        with:
+          name: rust-libs-freebsd
+          path: |
+            libjerboa_native.a
+            libjsh_coreutils.a
+          retention-days: 7
+
+  rust-android:
+    runs-on: ubuntu-latest
+    container:
+      image: jerboa21/jerboa:latest
+    steps:
+      - uses: actions/checkout@v4
+      - name: Cache Rust registry
+        uses: actions/cache@v4
+        with:
+          path: ~/.cargo/registry
+          key: rust-android-${{ hashFiles('patches/regex_native.rs', 'rust-coreutils/Cargo.lock') }}
+          restore-keys: rust-android-
+      - name: Install Android cross target
+        run: rustup target add aarch64-unknown-linux-musl
+      - name: Cache zig + cargo-zigbuild
+        id: cache-zig
+        uses: actions/cache@v4
+        with:
+          path: |
+            /opt/zig
+            /usr/local/bin/cargo-zigbuild
+          key: zig-0.13.0-zigbuild-0.22.3
+      - name: Install zig + cargo-zigbuild
+        if: steps.cache-zig.outputs.cache-hit != 'true'
+        shell: bash
+        run: |
+          # Use zig as the cross-linker for aarch64-linux-musl. Zig bundles its
+          # own musl libc/headers and downloads from ziglang.org / GitHub release
+          # mirrors which are far more reliable than musl.cc (which routinely
+          # times out from GitHub runners). cargo-zigbuild wraps cargo to use
+          # zig as the linker, transparently handling musl cross-link.
+          command -v curl >/dev/null || (apt-get update -qq && apt-get install -y -qq curl xz-utils)
+          mkdir -p /opt/zig
+          ZIG_VERSION=0.13.0
+          curl -fsSL --retry 5 --retry-delay 5 --max-time 300 \
+            "https://ziglang.org/download/${ZIG_VERSION}/zig-linux-x86_64-${ZIG_VERSION}.tar.xz" \
+            -o /tmp/zig.tar.xz
+          tar -xJf /tmp/zig.tar.xz -C /opt/zig --strip-components=1
+          rm -f /tmp/zig.tar.xz
+          # Pre-built cargo-zigbuild binary (avoid 5-min cargo install build)
+          ZIGBUILD_VERSION=0.22.3
+          curl -fsSL --retry 5 --retry-delay 5 --max-time 300 \
+            "https://github.com/rust-cross/cargo-zigbuild/releases/download/v${ZIGBUILD_VERSION}/cargo-zigbuild-x86_64-unknown-linux-musl.tar.xz" \
+            -o /tmp/cargo-zigbuild.tar.xz
+          tar -xJf /tmp/cargo-zigbuild.tar.xz -C /usr/local/bin/
+          rm -f /tmp/cargo-zigbuild.tar.xz
+      - name: Build Rust libs (aarch64-musl)
+        run: |
+          export PATH=/opt/zig:/usr/local/bin:$PATH
+          cd /build/mine/jerboa/jerboa-native-rs
+          cp $GITHUB_WORKSPACE/patches/regex_native.rs src/regex_native.rs
+          grep -q '#[cfg(feature = "duckdb")]' src/lib.rs || \
+            sed -i 's/^mod duckdb_native;/#[cfg(feature = "duckdb")]\nmod duckdb_native;/' src/lib.rs
+          sed -i '/SYS_LANDLOCK_RESTRICT_SELF.*446;$/a\\n#[cfg(target_arch = "aarch64")]\nconst SYS_LANDLOCK_CREATE_RULESET: libc::c_long = 444;\n#[cfg(target_arch = "aarch64")]\nconst SYS_LANDLOCK_ADD_RULE: libc::c_long = 445;\n#[cfg(target_arch = "aarch64")]\nconst SYS_LANDLOCK_RESTRICT_SELF: libc::c_long = 446;' src/landlock.rs
+          # Drop cdylib for cross-compile: only the static .a is needed by jsh-android.
+          sed -i 's/^crate-type = \["cdylib", "staticlib"\]/crate-type = ["staticlib"]/' Cargo.toml
+          RUSTFLAGS="--remap-path-prefix $HOME/.cargo/registry/src=crate" \
+          cargo zigbuild --release --target aarch64-unknown-linux-musl --no-default-features
+          strip -S target/aarch64-unknown-linux-musl/release/libjerboa_native.a
+          cp target/aarch64-unknown-linux-musl/release/libjerboa_native.a $GITHUB_WORKSPACE/
+
+          cd $GITHUB_WORKSPACE/rust-coreutils
+          RUSTFLAGS="--remap-path-prefix $HOME/.cargo/registry/src=crate" \
+          cargo zigbuild --release --target aarch64-unknown-linux-musl
+          strip -S target/aarch64-unknown-linux-musl/release/libjsh_coreutils.a
+          cp target/aarch64-unknown-linux-musl/release/libjsh_coreutils.a $GITHUB_WORKSPACE/
+      - uses: actions/upload-artifact@v4
+        with:
+          name: rust-libs-android
+          path: |
+            libjerboa_native.a
+            libjsh_coreutils.a
+          retention-days: 7
+
+  build:
+    needs: rust-musl
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/download-artifact@v4
+        with:
+          name: rust-libs-musl
+          path: prebuilt/
+      - name: Install Podman
+        run: |
+          sudo apt-get update
+          sudo apt-get install -y podman
+          podman --version
+      - name: Build jsh-musl (Podman, pre-built Rust libs injected)
+        run: |
+          make podman \
+            JSH_PREBUILT_NATIVE=prebuilt/libjerboa_native.a \
+            JSH_PREBUILT_COREUTILS=prebuilt/libjsh_coreutils.a
+      - name: Verify binary
+        run: |
+          ls -lh jsh-musl
+          file jsh-musl
+          cat jsh-musl.sha256
+      - uses: actions/upload-artifact@v4
+        with:
+          name: jsh-musl
+          path: |
+            jsh-musl
+            jsh-musl.sha256
+          if-no-files-found: error
+          retention-days: 30
+
+  build-freebsd:
+    needs: rust-freebsd
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/download-artifact@v4
+        with:
+          name: rust-libs-freebsd
+          path: prebuilt/
+      - name: Install Podman
+        run: |
+          sudo apt-get update
+          sudo apt-get install -y podman
+          podman --version
+      - name: Build jsh-freebsd (Podman, pre-built Rust libs injected)
+        run: |
+          make freebsd-podman \
+            JSH_PREBUILT_NATIVE=prebuilt/libjerboa_native.a \
+            JSH_PREBUILT_COREUTILS=prebuilt/libjsh_coreutils.a
+      - name: Verify binary
+        run: |
+          ls -lh jsh-freebsd
+          file jsh-freebsd
+          cat jsh-freebsd.sha256
+      - uses: actions/upload-artifact@v4
+        with:
+          name: jsh-freebsd
+          path: |
+            jsh-freebsd
+            jsh-freebsd.sha256
+          if-no-files-found: error
+          retention-days: 30
+
+  build-android:
+    needs: rust-android
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - name: Install Podman and QEMU
+        run: |
+          sudo apt-get update
+          sudo apt-get install -y podman qemu-user-static binfmt-support
+          sudo update-binfmts --enable qemu-aarch64 || true
+          podman --version
+      - uses: actions/download-artifact@v4
+        with:
+          name: rust-libs-android
+          path: prebuilt/
+      - name: Build jsh-android (Podman, pre-built Rust libs injected)
+        run: |
+          make android-podman \
+            JSH_PREBUILT_NATIVE=prebuilt/libjerboa_native.a \
+            JSH_PREBUILT_COREUTILS=prebuilt/libjsh_coreutils.a
+      - name: Verify binary
+        run: |
+          ls -lh jsh-android
+          file jsh-android
+          cat jsh-android.sha256
+      - uses: actions/upload-artifact@v4
+        with:
+          name: jsh-android
+          path: |
+            jsh-android
+            jsh-android.sha256
+          if-no-files-found: error
+          retention-days: 30
+
+  build-macos:
+    runs-on: macos-latest
+    steps:
+      - uses: actions/checkout@v4
+      - name: Install dependencies
+        run: |
+          brew install openssl@3 lz4
+          echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV
+      - name: Resolve Chez Scheme HEAD SHA
+        id: chez-sha
+        run: |
+          SHA=$(git ls-remote https://github.com/ober/ChezScheme.git refs/heads/main | cut -f1)
+          echo "sha=$SHA" >> "$GITHUB_OUTPUT"
+      - name: Cache our Chez Scheme install
+        id: cache-chez
+        uses: actions/cache@v4
+        with:
+          path: |
+            /opt/homebrew/lib/csv10.4.0-pre-release.4
+            /opt/homebrew/bin/scheme
+            /opt/homebrew/bin/petite
+            /opt/homebrew/bin/scheme-script
+          key: chez-macos-arm64-${{ steps.chez-sha.outputs.sha }}
+      - name: Build our Chez Scheme fork
+        if: steps.cache-chez.outputs.cache-hit != 'true'
+        run: |
+          # jerboa-shell needs ober/ChezScheme (10.4.0-pre-release.4+) which adds
+          # base64-encode/decode, sha256-bytevector, etc. Stock Cisco Chez 10.3
+          # and Homebrew chezscheme are too old.
+          git clone --depth 1 --branch main https://github.com/ober/ChezScheme.git /tmp/ChezScheme
+          cd /tmp/ChezScheme
+          ./configure --installprefix=/opt/homebrew
+          make -j$(sysctl -n hw.ncpu)
+          sudo make install
+      - name: Verify Chez Scheme
+        run: |
+          which scheme
+          scheme --version
+          echo '(display (top-level-bound? (quote base64-encode))) (newline)' | scheme --script /dev/stdin
+      - name: Install Rust toolchain
+        uses: dtolnay/rust-toolchain@stable
+      - name: Cache Rust registry
+        uses: actions/cache@v4
+        with:
+          path: ~/.cargo/registry
+          key: rust-macos-${{ hashFiles('patches/regex_native.rs', 'rust-coreutils/Cargo.lock') }}
+          restore-keys: rust-macos-
+      - name: Build jsh-macos
+        run: make jsh-macos
+      - name: Smoke-test binary
+        run: |
+          ls -lh jsh-macos
+          ./jsh-macos -c 'echo ok'
+      - uses: actions/upload-artifact@v4
+        with:
+          name: jsh-macos
+          path: |
+            jsh-macos
+            jsh-macos.sha256
+          if-no-files-found: error
+          retention-days: 30
+
+  release:
+    needs: [test, build, build-freebsd, build-android, build-macos]
+    if: github.ref == 'refs/heads/master' && github.event_name == 'push'
+    runs-on: ubuntu-latest
+    permissions:
+      contents: write
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/download-artifact@v4
+        with:
+          path: artifacts
+          merge-multiple: true
+      - name: Update rolling 'latest' release
+        env:
+          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: |
+          TAG=latest
+          gh release view "$TAG" --repo "${{ github.repository }}" >/dev/null 2>&1 || \
+            gh release create "$TAG" \
+              --repo "${{ github.repository }}" \
+              --title "Latest static jsh binaries" \
+              --notes "Auto-built by CI from latest master commit." \
+              --target master
+          gh release upload "$TAG" \
+            artifacts/jsh-musl artifacts/jsh-musl.sha256 \
+            artifacts/jsh-freebsd artifacts/jsh-freebsd.sha256 \
+            artifacts/jsh-android artifacts/jsh-android.sha256 \
+            artifacts/jsh-macos artifacts/jsh-macos.sha256 \
+            --repo "${{ github.repository }}" --clobber
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8550442
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,107 @@
+# Build artifacts
+*.sls
+*.so
+*.wpo
+*.o
+*.boot
+.jerbuild-hashes
+*.tarm64osx
+/jsh
+/jsh-musl
+/jsh-musl.sha256
+/jsh-android
+/jsh-android.sha256
+/jsh-macos
+/jsh-macos.sha256
+/jsh-macos-minimal
+/jsh-macos-minimal.sha256
+/jsh-macos-full
+/jsh-macos-full.sha256
+/jsh-freebsd
+/jsh-freebsd.sha256
+/jsh-kernel
+/jsh-linux-amd64
+/jsh-linux-amd64-main.c
+/jsh-linux-arm64
+/jsh-linux-arm64-main.c
+/jsh-freebsd-amd64
+/jsh-freebsd-amd64-main.c
+/petite_boot.h
+/scheme_boot.h
+/jsh_libs_boot.h
+/jsh_program.h
+/qemu_*.core
+gsh_program.h
+jsh-all.so
+libgsh-ffi.so
+
+# Auto-generated .sls files (built by build-jerboa.ss from .ss sources)
+# Handwritten files are explicitly tracked via git add -f
+src/jsh/arithmetic.sls
+src/jsh/ast.sls
+src/jsh/builtins.sls
+src/jsh/completion.sls
+src/jsh/control.sls
+src/jsh/environment.sls
+src/jsh/executor.sls
+src/jsh/expander.sls
+src/jsh/functions.sls
+src/jsh/fuzzy.sls
+src/jsh/fzf.sls
+src/jsh/glob.sls
+src/jsh/history.sls
+src/jsh/jobs.sls
+src/jsh/lexer.sls
+src/jsh/lineedit.sls
+src/jsh/macros.sls
+src/jsh/main.sls
+src/jsh/parser.sls
+src/jsh/pipeline.sls
+src/jsh/pregexp-compat.sls
+src/jsh/prompt.sls
+src/jsh/procwatch.sls
+src/jsh/redirect.sls
+src/jsh/registry.sls
+src/jsh/script.sls
+src/jsh/signals.sls
+src/jsh/startup.sls
+src/jsh/util.sls
+src/jsh/player.sls
+src/jsh/recorder.sls
+src/jsh/embed-data.sls
+
+# Embedded files (may contain secrets)
+embed/
+
+# Vendor
+_vendor/
+vendor/
+/jerboa-shell/
+
+# Rust build artifacts
+rust-coreutils/target/
+rust-coreutils/ripgrep-core/target/
+
+# Android build staging
+/android-stage/
+/aws-stage/
+
+# macOS build vault staging (re-populated each build from upstream chez-fuse)
+/vault-stage-macos/
+
+# Legacy SSH build staging
+/ssh-stage/
+
+# Cache
+.gerbil-lsp-cache/
+.jerboa/bin/
+jsh-freebsd
+# jerbuild staging directory
+/jsh-src/
+/_jerbuild-stage/
+# generated at build time from jsh-generate.ss
+/jsh-generated.ss
+# Claude Code session files (user-specific)
+/.claude/
+*.dylib
+.jcode/
diff --git a/.gitsafe.json b/.gitsafe.json
new file mode 100644
index 0000000..1e97561
--- /dev/null
+++ b/.gitsafe.json
@@ -0,0 +1,3 @@
+{
+  "severity": "high"
+}
diff --git a/.jerboa/security.json b/.jerboa/security.json
new file mode 100644
index 0000000..545ca1e
--- /dev/null
+++ b/.jerboa/security.json
@@ -0,0 +1,22 @@
+{
+  "version": 1,
+  "repo": "jerboa-shell",
+  "extends": ["jerboa:cli", "jerboa:ffi", "jerboa:parser", "jerboa:generated-heavy"],
+  "paths": {
+    "production": ["*.ss", "*.sls", "lib/**/*.ss", "lib/**/*.sls", "src/**/*.{ss,sls,c,h,rs}", "support/**", "ffi-shim.c", "Makefile"],
+    "tests": ["test/**", "tests/**", "**/*-test.ss", "fixtures/**"],
+    "generated": ["build/**", "dist/**", "target/**", "_tmp/**", "jsh-generated.ss", "jsh-macos", "*.sha256", "*.so", "*.dylib", "*.wpo"],
+    "vendor": ["vendor/**", "third_party/**", "oils/**"],
+    "docs": ["README.md", "docs/**", "*.md", "AGENTS.md"]
+  },
+  "policy": {
+    "failOn": ["critical", "high"],
+    "imports": { "directChezscheme": "allow-in-ffi-boundaries" },
+    "ffi": { "allowed": true, "requireCloseOnExec": true, "requireDynamicWindCleanup": true },
+    "process": { "shellInterpolation": "deny", "restrictedModeExpected": true },
+    "network": { "requireTimeouts": true },
+    "eval": { "stringEval": "deny", "bareRead": "deny", "allowReadEval": false },
+    "shell": { "treatScriptsAsCodeExecution": true, "requireFdHygiene": true }
+  },
+  "suppressions": []
+}
diff --git a/.jerbuild b/.jerbuild
new file mode 100644
index 0000000..94b1d2b
--- /dev/null
+++ b/.jerbuild
@@ -0,0 +1,57 @@
+;; jerbuild build config for jerboa-shell (jsh) — macOS.
+;;
+;; All paths are relative to this file. The pre-build hook regenerates
+;; jsh-generated.ss + stages stdlib overrides into _jerbuild-stage.
+;;
+;; Out: jsh-macos (matches what `make jsh-macos` produces). Run with
+;;   jerbuild build
+;; from this directory.
+
+(entry  "jsh-generated.ss")
+(output "jsh-macos")
+;; Requires: jerbuild, cargo/rustc, cc, and libssl/libcrypto.
+;; The pre-build hook uses jerbuild's bundled Chez/std library, sparse-cloned
+;; vendor/jerboa-native-rs, and cargo-built Rust archives. No Jerboa source
+;; checkout is required.
+
+(libdirs "_jerbuild-stage"   ; stdlib patches — must shadow bundle copies
+         "src"
+         "vendor/jerboa-coreutils/lib"
+         "vendor/jerboa-awk/lib"
+         "vendor/jerboa-sed/lib"
+         "vendor/jerboa-aws/lib"
+         "vendor/jerboa-wormhole"
+         "vendor/jerboa-yubikey/lib"
+         "vendor/jerboa-ssh/lib"
+         "vendor/jerboa-fuse/lib")
+
+;; FFI shim — implements all ffi_* foreign-procedure targets used by
+;; (jsh ffi), (jsh limits), (std os landlock), etc.
+(extra-sources "ffi-shim.c")
+
+;; Rust archive staged by support/stage-for-jerbuild.sh. It builds
+;; libjerboa_native.a + libjsh_coreutils.a with Cargo, then merges them into
+;; a single archive with duplicate Rust runtime symbols localized.
+(extra-archives
+  "_jerbuild-stage/librust_combined.a"
+  ;; jerboa-ssh agent + transport C shims, staged by stage-for-jerbuild.sh.
+  "_jerbuild-stage/libjerboa_ssh.a"
+  ;; jerboa-fuse mount C shim (mount_helper.c), staged by stage-for-jerbuild.sh.
+  "_jerbuild-stage/libjerboa_fuse.a")
+
+;; jerboa-ssh + jerboa-crypto need libssl/libcrypto from Homebrew. libutil is
+;; needed by openpty/forkpty used in (jsh pty).
+(extra-ldflags "-L/opt/homebrew/lib" "-L/usr/local/lib"
+               "-lssl" "-lcrypto" "-lutil")
+
+;; Custom main.c — jsh bypasses Chez's arg parsing (JSH_ARG* env vars)
+;; and uses Sscheme_script instead of Sscheme_program to avoid the
+;; fork-thread / GC futex deadlock.
+(main-c "support/jsh-jerbuild-main.c")
+
+;; FFI symbols registered via Sforeign_symbol after Sbuild_heap — static
+;; macOS binaries can't resolve via dlsym(RTLD_DEFAULT).
+(ffi-symbols "support/jsh-jerbuild-symbols.list")
+
+;; Pre-build: wipe stale .so/.wpo + patch stdlib + regen jsh-generated.ss.
+(pre-build "support/stage-for-jerbuild.sh")
diff --git a/.jerbuild.freebsd-amd64 b/.jerbuild.freebsd-amd64
new file mode 100644
index 0000000..a8353e7
--- /dev/null
+++ b/.jerbuild.freebsd-amd64
@@ -0,0 +1,46 @@
+;; jerbuild build config for jerboa-shell (jsh) — native FreeBSD amd64.
+;;
+;; Build on a FreeBSD amd64 host with:
+;;   jerbuild build --config .jerbuild.freebsd-amd64
+;;
+;; This uses only the installed jerbuild bundle, cc, and cargo. It deliberately
+;; has no source-checkout, cross-compiler, or absolute runtime-path dependency.
+
+(entry  "jsh-generated.ss")
+(output "jsh-freebsd")
+
+(cc "cc")
+
+;; FreeBSD libc has dlopen, no separate -ldl. -lutil is for openpty.
+;; Chez's expeditor needs termcap, and the packaged Chez kernel references
+;; GNU libiconv from /usr/local/lib.
+(os-libs "-lm -lpthread -lutil -ltermcap -L/usr/local/lib -liconv")
+
+(libdirs "_jerbuild-stage"   ; stdlib patches — must shadow bundle copies
+         "src"
+         "vendor/jerboa-coreutils/lib"
+         "vendor/jerboa-awk/lib"
+         "vendor/jerboa-sed/lib"
+         "vendor/jerboa-aws/lib"
+         "../jerboa-wormhole"
+         "vendor/jerboa-yubikey/lib"
+         "vendor/jerboa-ssh/lib"
+         "vendor/jerboa-fuse/lib")
+
+;; FFI shim — same shim used by the macOS + Linux builds. Embed crypto symbols
+;; come from the staged jerboa-native-rs archive.
+(extra-sources "ffi-shim.c")
+
+;; Rust archive staged by support/stage-for-jerbuild-freebsd.sh. No coreutils
+;; archive is linked on FreeBSD yet; freebsd-main.c provides weak stubs.
+(extra-archives
+  "_jerbuild-stage/libjerboa_native.a"
+  "_jerbuild-stage/libjerboa_ssh.a"
+  "_jerbuild-stage/libjerboa_fuse.a")
+
+(extra-ldflags "-Wl,--export-dynamic")
+
+;; FreeBSD-specific main.c generated by the pre-build hook.
+(main-c "_jerbuild-stage/freebsd-main.c")
+
+(pre-build "support/stage-for-jerbuild-freebsd.sh")
diff --git a/.jerbuild.linux-amd64 b/.jerbuild.linux-amd64
new file mode 100644
index 0000000..33b9335
--- /dev/null
+++ b/.jerbuild.linux-amd64
@@ -0,0 +1,47 @@
+;; jerbuild build config for jerboa-shell (jsh) — native Linux amd64.
+;;
+;; Build on a Linux amd64 host with:
+;;   jerbuild build --config .jerbuild.linux-amd64
+;;
+;; This intentionally uses only the installed jerbuild bundle, cc, and cargo.
+;; There is no Jerboa source checkout, cross compiler, or absolute runtime path.
+
+(entry  "jsh-generated.ss")
+(output "jsh-linux-amd64")
+
+(cc "cc")
+
+(os-libs "-lm -ldl -lpthread -ltinfo")
+
+(libdirs "_jerbuild-stage"   ; stdlib patches — must shadow bundle copies
+         "src"
+         "vendor/jerboa-coreutils/lib"
+         "vendor/jerboa-awk/lib"
+         "vendor/jerboa-sed/lib"
+         "vendor/jerboa-aws/lib"
+         "../jerboa-wormhole"
+         "vendor/jerboa-yubikey/lib"
+         "vendor/jerboa-ssh/lib"
+         "vendor/jerboa-fuse/lib")
+
+;; FFI shim — same shim used by the macOS build. Embed crypto symbols come
+;; from the staged jerboa-native-rs archive.
+(extra-sources "ffi-shim.c")
+
+;; Rust archives staged by support/stage-for-jerbuild-linux.sh.
+(extra-archives
+  "_jerbuild-stage/libjerboa_native.a"
+  "_jerbuild-stage/libjsh_coreutils.a"
+  "_jerbuild-stage/libjerboa_ssh.a"
+  "_jerbuild-stage/libjerboa_fuse.a")
+
+;; --export-dynamic + Sforeign_symbol registrations in main.c are how Chez
+;; foreign-procedure finds symbols. --allow-multiple-definition tolerates
+;; duplicate Rust runtime symbols across the two static archives.
+(extra-ldflags "-Wl,--export-dynamic"
+               "-Wl,--allow-multiple-definition")
+
+;; Linux-specific main.c is generated by the pre-build hook.
+(main-c "_jerbuild-stage/linux-main.c")
+
+(pre-build "support/stage-for-jerbuild-linux.sh")
diff --git a/.jerbuild.linux-amd64-native b/.jerbuild.linux-amd64-native
new file mode 100644
index 0000000..34b83fc
--- /dev/null
+++ b/.jerbuild.linux-amd64-native
@@ -0,0 +1,35 @@
+;; Compatibility alias for older local commands. The canonical native Linux
+;; config is .jerbuild.linux-amd64.
+
+(entry  "jsh-generated.ss")
+(output "jsh-linux-amd64")
+
+(cc "cc")
+
+(os-libs "-lm -ldl -lpthread -ltinfo")
+
+(libdirs "_jerbuild-stage"
+         "src"
+         "vendor/jerboa-coreutils/lib"
+         "vendor/jerboa-awk/lib"
+         "vendor/jerboa-sed/lib"
+         "vendor/jerboa-aws/lib"
+         "../jerboa-wormhole"
+         "vendor/jerboa-yubikey/lib"
+         "vendor/jerboa-ssh/lib"
+         "vendor/jerboa-fuse/lib")
+
+(extra-sources "ffi-shim.c")
+
+(extra-archives
+  "_jerbuild-stage/libjerboa_native.a"
+  "_jerbuild-stage/libjsh_coreutils.a"
+  "_jerbuild-stage/libjerboa_ssh.a"
+  "_jerbuild-stage/libjerboa_fuse.a")
+
+(extra-ldflags "-Wl,--export-dynamic"
+               "-Wl,--allow-multiple-definition")
+
+(main-c "_jerbuild-stage/linux-main.c")
+
+(pre-build "support/stage-for-jerbuild-linux.sh")
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..6a5523f
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,466 @@
+## The Jerboa Language — Quick Reference
+
+Jerboa is a Scheme dialect built on Chez Scheme. It has legacy shell ancestry but is its own language. **All user-facing code is `.ss` files. Never write `.sls` files for the user** — those are internal implementation files.
+
+### File Structure
+
+Every Jerboa file looks like this:
+
+```scheme
+(import (jerboa prelude))    ;; ONE import gives you the ENTIRE language
+;; Optional extra imports for modules NOT in the prelude:
+;; (import (std net request))
+
+(def (my-function x y)
+  (+ x y))
+
+(displayln (my-function 1 2))
+```
+
+Run with: `scheme --libdirs lib --script file.ss`
+
+**NEVER** write `(library ...)` forms — that's `.sls` internal syntax.
+
+### Reader Syntax Extensions
+
+```
+[...]                → plain parentheses — same as Chez Scheme
+{method obj args}    → (~ obj 'method args)  — method dispatch
+name:                → keyword #:name
+:std/sort            → (std sort)        — colon-style module path
+#<<END ... END       → heredoc string
+```
+
+Square brackets `[...]` are interchangeable with `(...)`, exactly like stock Chez Scheme. You can freely use them in bindings, match clauses, and anywhere you'd use parentheses:
+```scheme
+;; All of these are correct:
+(let ([x 1] [y 2]) (+ x y))
+(for/collect ([x (in-range 5)]) (* x x))
+(match val ([list a b] (+ a b)))
+(cond [(> x 0) "positive"] [else "non-positive"])
+```
+
+### CRITICAL: Things That DO NOT EXIST in Jerboa/Chez
+
+Claude frequently hallucinates these from other Scheme implementations, Gambit, Racket, or R7RS training data.
+**NONE of them are real in Jerboa/Chez. STOP and use the correct form.**
+
+#### AI Compatibility Aliases (these now work in the prelude)
+The following names from other Scheme dialects are aliased in `(jerboa prelude)`:
+- `hash-has-key?` → `hash-key?` (Racket)
+- `hash-table-set!` → `hash-put!` (Racket)
+- `directory-exists?` → `file-directory?` (Gambit)
+- `eql?` → `eqv?` (Common Lisp)
+- `random-integer` → `random` (Gambit)
+- `read-line` → `get-line` wrapper (Gambit) — works with or without port arg
+- `force-output` → `flush-output-port` wrapper (Gambit) — works with or without port arg
+- `string-map` → char-level map (Racket/R7RS) — `(string-map f str)`
+
+#### Hallucinated Functions (still do NOT exist)
+- `symbol<?` — use `(lambda (a b) (string<? (symbol->string a) (symbol->string b)))`
+- `string-contains?` — use `(string-contains str sub)` (returns index or #f, NOT boolean)
+- `define-struct` — use `(defstruct name (fields ...))`
+- `raise` with a string — use `(error 'who "message" irritants ...)`
+- `environment-bound?` — non-Jerboa. No direct Chez equivalent
+
+#### Non-Jerboa Scheme-isms (from training data — wrong in Jerboa)
+- `time->seconds` — use `(time-second (current-time))` for epoch seconds
+- `thread-sleep!` — Gambit. Use `(sleep (make-time 'time-duration 0 seconds))`
+- `thread-yield` — no Chez equivalent. Use `(sleep (make-time 'time-duration 0 0))` as workaround
+- `path-expand` with 2 args — Jerboa takes 1 arg. Use `(path-join base rel)` for 2-arg version
+- `process-status` — non-Jerboa. Use `(std misc process)` API in Jerboa
+- `user-info-home` — non-Jerboa. Use `(getenv "HOME")`
+- `the-environment` — non-Jerboa. Use `(interaction-environment)` in Chez
+- `condition/report-string` — non-Jerboa. Use `(with-output-to-string (lambda () (display-condition c)))`
+- `make-class-type` — non-Jerboa. Use `(defstruct ...)` or `(defclass ...)` in Jerboa
+- `string-subst` — non-Jerboa. Not in prelude. Use `(string-replace str old new)` or implement manually
+- `open-fd-pair` — Gambit. Does not exist in Chez; requires different API
+
+#### R6RS/Racket-isms (wrong variant)
+- `make-equal-hashtable` — R6RS. Use `(make-hash-table)` from Jerboa prelude
+- `arithmetic-shift` — Racket. Use `(bitwise-arithmetic-shift n k)` or `(ash n k)` in Chez
+- `pregexp-match` — Racket. Use `(std text regex)` or `(std pregexp)` API in Jerboa
+
+### CRITICAL: Common Arity Mistakes
+
+- `(list-of? pred)` → returns a PREDICATE. It takes 1 arg. Use: `((list-of? number?) lst)`
+- `(maybe pred)` → returns a PREDICATE. It takes 1 arg. Use: `((maybe string?) val)`
+- `(in-range end)` or `(in-range start end)` or `(in-range start end step)` — NOT `(in-range start step end)`
+- `(hash-ref ht key)` or `(hash-ref ht key default)` — NOT `(hash-ref key ht)`
+- `(string-split str delimiter)` where delimiter is a CHAR: `(string-split "a,b" #\,)`
+- `(make-rwlock)` — takes **0 args**, NOT `(make-rwlock 'name)`
+- `(path-expand path)` — takes **1 arg**, NOT `(path-expand rel base)`; use `path-join` for 2-arg
+- `(sort predicate list)` — Chez arg order. NOT `(sort list predicate)`
+
+### Core Forms (all from `(import (jerboa prelude))`)
+
+#### Definitions
+```scheme
+(def x 42)                              ;; variable
+(def (f x y) (+ x y))                  ;; function
+(def (f x (y 10)) body)                ;; optional param with default
+(def (f x . rest) body)                ;; rest args
+(def* f ((x) ...) ((x y) ...))         ;; multi-arity
+(defrule (name pat) template)           ;; macro
+```
+
+#### Data Structures
+```scheme
+(defstruct point (x y))                ;; → make-point, point?, point-x, point-y, point-x-set!
+(defstruct (circle shape) (radius))    ;; inheritance (single only)
+(defmethod (area (self circle)) body)  ;; method on type
+(~ obj 'method arg ...)                ;; dispatch (or {method obj arg ...})
+(defrecord person (name age))          ;; struct + pretty-print + ->alist
+(define-enum color (red green blue))   ;; → color-red, color?, color->name
+```
+
+#### Pattern Matching
+```scheme
+(match value
+  (42 "exact")                          ;; literal
+  ((list a b c) (+ a b c))             ;; list destructure
+  ((cons h t) h)                        ;; pair
+  ((? number?) "num")                   ;; predicate
+  ((? string? s) (string-upcase s))    ;; predicate + bind
+  ((and (? number?) (? positive?)) "positive number")
+  ((or "yes" "y") #t)
+  ((=> string->number n) n)            ;; view pattern
+  (n (where (> n 0)) "positive")       ;; guard
+  (_ "default"))                        ;; wildcard
+```
+
+#### Error Handling
+```scheme
+(try expr (catch (e) handler) (finally cleanup))
+(try expr (catch (error? e) handler))
+(unwind-protect body cleanup)
+(with-resource (var init cleanup) body)
+```
+
+#### Result Type (Rust-inspired ok/err)
+```scheme
+(ok 42)  (err "bad")  (ok? r)  (err? r)
+(unwrap (ok 42))         ;; → 42 (raises on err)
+(unwrap-or (err "x") 0)  ;; → 0
+(map-ok f result)  (map-err f result)
+(and-then result f)       ;; monadic bind
+(try-result expr)         ;; exceptions → (err condition)
+(try-result* expr)        ;; exceptions → (err "message string")
+(sequence-results list-of-results)  ;; → (ok list) or first (err)
+(->? (ok 10) (+ 5) (* 2))  ;; → (ok 30), short-circuits on err
+```
+
+#### Iterators
+```scheme
+(for ((x (in-range 5))) (displayln x))
+(for/collect ((x (in-range 5))) (* x x))           ;; → (0 1 4 9 16)
+(for/fold ((sum 0)) ((x (in-range 10))) (+ sum x)) ;; → 45
+(for/or ((x lst)) (and (pred? x) x))               ;; first truthy
+(for/and ((x lst)) (pred? x))                       ;; all truthy
+
+;; Iterators: in-list, in-vector, in-string, in-range, in-hash-keys,
+;; in-hash-values, in-hash-pairs, in-naturals, in-indexed,
+;; in-port, in-lines, in-chars, in-bytes, in-producer
+```
+
+#### Threading Macros
+```scheme
+(-> x (f a) (g b))       ;; thread first: (g (f x a) b)
+(->> x (f a) (g b))      ;; thread last:  (g b (f a x))
+(as-> x v (f v) (g v))   ;; named
+(some-> x (f) (g))       ;; short-circuit on #f
+(cond-> x test (f) t2 (g))  ;; conditional steps
+(->? (ok x) (f) (g))     ;; result-aware thread first
+```
+
+#### Ergo Typing
+```scheme
+(: expr pred?)                    ;; checked cast
+(using (p (make-point 1 2) : point?)
+  (+ p.x p.y))                   ;; dot-access → (point-x p) etc.
+((list-of? number?) '(1 2 3))    ;; predicate factory → #t
+((maybe string?) #f)              ;; accepts #f or string → #t
+```
+
+#### Hash Tables
+```scheme
+(def ht (make-hash-table))
+(hash-put! ht "key" "val")
+(hash-ref ht "key")              ;; error if missing
+(hash-ref ht "key" "default")    ;; with default
+(hash-get ht "key")              ;; → val or #f
+(hash-key? ht "key")             ;; → #t/#f
+(hash-remove! ht "key")
+(hash->list ht)  (hash-keys ht)  (hash-values ht)
+(hash-for-each (lambda (k v) ...) ht)
+(list->hash-table '(("a" . 1) ("b" . 2)))
+```
+
+#### Strings
+```scheme