Set up Forgejo CI/CD policy

ober

816f50c105ad92b5c120640a79bd2926e472b217

diff --git a/.build.yml b/.build.yml
deleted file mode 100644
index d1cce83..0000000
--- a/.build.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-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
-      sudo fallocate -l 4G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile
-      export CARGO_BUILD_JOBS=1
-      # Build the multicall Jerboa tool artifact used by jerboa-shell.
-      curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
-        sh -s -- -y --profile minimal --default-toolchain 1.94.1
-      source "$HOME/.cargo/env"
-      make jerboa CHEZ_CONFIGURE_EXTRA=--disable-x11 JERBOA_NATIVE_FEATURES=tls
-  - 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/.forgejo/ci-required.sh b/.forgejo/ci-required.sh
new file mode 100755
index 0000000..8496b8d
--- /dev/null
+++ b/.forgejo/ci-required.sh
@@ -0,0 +1,68 @@
+#!/bin/sh
+set -eu
+
+has_target() {
+  target=$1
+  [ -f Makefile ] && grep -Eq "^${target}[[:space:]]*:" Makefile
+}
+
+if has_target verify; then
+  make verify
+else
+  ran=0
+  for target in security test check build; do
+    if has_target "$target"; then
+      make "$target"
+      ran=1
+    fi
+  done
+  [ "$ran" = 1 ] || {
+    echo "ERROR: no verify, test, check, or build target is available" >&2
+    exit 1
+  }
+fi
+
+if ! has_target binary; then
+  echo "No standalone binary target; full repository verification passed."
+  exit 0
+fi
+
+make binary
+
+if has_target binary-smoke; then
+  make binary-smoke
+  exit 0
+fi
+if has_target smoke; then
+  make smoke
+  exit 0
+fi
+
+binary_list=$(mktemp)
+trap 'rm -f "$binary_list"' EXIT HUP INT TERM
+find . -maxdepth 2 -type f -perm -111 \
+  ! -path './.git/*' ! -path './.jerboa/*' ! -path './vendor/*' \
+  ! -path './test/*' ! -path './tests/*' \
+  -exec file {} \; |
+  awk -F: '/(ELF .*executable|Mach-O .*executable)/ { print $1 }' > "$binary_list"
+
+[ -s "$binary_list" ] || {
+  echo "ERROR: make binary succeeded but produced no runnable ELF executable" >&2
+  exit 1
+}
+
+while IFS= read -r binary; do
+  echo "Smoke-checking $binary"
+  if timeout 30 env QT_QPA_PLATFORM=offscreen \
+      QTWEBENGINE_CHROMIUM_FLAGS=--disable-gpu "$binary" --version >/dev/null 2>&1; then
+    continue
+  fi
+  if timeout 30 env QT_QPA_PLATFORM=offscreen \
+      QTWEBENGINE_CHROMIUM_FLAGS=--disable-gpu "$binary" --help >/dev/null 2>&1; then
+    continue
+  fi
+  echo "ERROR: $binary failed both --version and --help runtime smoke checks" >&2
+  exit 1
+done < "$binary_list"
+
+echo "Full verification, binary build, and runtime smoke checks passed."
diff --git a/.forgejo/require-version-bump.sh b/.forgejo/require-version-bump.sh
new file mode 100755
index 0000000..ec38872
--- /dev/null
+++ b/.forgejo/require-version-bump.sh
@@ -0,0 +1,50 @@
+#!/bin/sh
+set -eu
+
+version_file=${VERSION_FILE:-VERSION}
+test -f "$version_file" || {
+  echo "ERROR: $version_file is required" >&2
+  exit 1
+}
+
+new_version=$(tr -d '[:space:]' < "$version_file")
+printf '%s\n' "$new_version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' || {
+  echo "ERROR: VERSION must be semantic MAJOR.MINOR.PATCH, got: $new_version" >&2
+  exit 1
+}
+
+if [ -f jpkg.sexp ]; then
+  manifest_version=$(awk -F'"' '/\(version "/ { print $2; exit }' jpkg.sexp)
+  [ "$manifest_version" = "$new_version" ] || {
+    echo "ERROR: jpkg.sexp version $manifest_version must match VERSION $new_version" >&2
+    exit 1
+  }
+fi
+
+if [ "${FORGEJO_EVENT_NAME:-}" != pull_request ]; then
+  echo "VERSION $new_version is valid"
+  exit 0
+fi
+
+base_ref=${FORGEJO_BASE_REF:?FORGEJO_BASE_REF is required for pull requests}
+old_version=$(git show "origin/$base_ref:$version_file" 2>/dev/null | tr -d '[:space:]' || true)
+old_version=${old_version:-0.0.0}
+printf '%s\n' "$old_version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' || {
+  echo "ERROR: target branch VERSION is malformed: $old_version" >&2
+  exit 1
+}
+
+if ! awk -F. -v old="$old_version" -v new="$new_version" 'BEGIN {
+  split(old, o, ".")
+  split(new, n, ".")
+  for (i = 1; i <= 3; i++) {
+    if ((n[i] + 0) > (o[i] + 0)) exit 0
+    if ((n[i] + 0) < (o[i] + 0)) exit 1
+  }
+  exit 1
+}'; then
+  echo "ERROR: VERSION must advance beyond $old_version; got $new_version" >&2
+  exit 1
+fi
+
+echo "VERSION advances: $old_version -> $new_version"
diff --git a/.forgejo/workflows/ci.yaml b/.forgejo/workflows/ci.yaml
new file mode 100644
index 0000000..c25a301
--- /dev/null
+++ b/.forgejo/workflows/ci.yaml
@@ -0,0 +1,60 @@
+name: build-test
+
+on:
+  pull_request:
+    branches: [main]
+  push:
+    branches: [main]
+    tags: ['v*']
+  workflow_dispatch:
+
+permissions:
+  contents: read
+
+jobs:
+  required:
+    runs-on: docker
+    container:
+      image: debian:stable
+    steps:
+      - name: Install system dependencies
+        run: |
+          apt-get update
+          DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
+            build-essential libncurses-dev uuid-dev zlib1g-dev liblz4-dev \
+            libpcre2-dev libssl-dev libx11-dev pkg-config git curl file \
+            python3 unzip zip ca-certificates
+      - name: Check out jerboa-shell-extras
+        uses: https://code.forgejo.org/actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
+        with:
+          persist-credentials: false
+      - name: Install Rust
+        run: |
+          curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs |
+            sh -s -- -y --profile minimal --default-toolchain 1.94.1
+          echo "$HOME/.cargo/bin" >> "$FORGEJO_PATH"
+      - name: Fetch and build Jerboa
+        run: |
+          mkdir ../jerboa
+          git -C ../jerboa init
+          git -C ../jerboa remote add origin https://git.jerboa.sh/ober/jerboa.git
+          JERBOA_COMMIT="6a5230800b3599147f04"
+          JERBOA_COMMIT="${JERBOA_COMMIT}bd5b1493bc996047506a"
+          git -C ../jerboa fetch --depth 1 origin "$JERBOA_COMMIT"
+          git -C ../jerboa checkout --detach FETCH_HEAD
+          JERBOA_TREE="6eb4710558222c4e2ef9"
+          JERBOA_TREE="${JERBOA_TREE}b076491f7db5af5e3748"
+          test "$(git -C ../jerboa rev-parse 'HEAD^{tree}')" = "$JERBOA_TREE"
+          . "$HOME/.cargo/env"
+          CARGO_BUILD_JOBS=1 make -C ../jerboa jerboa \
+            CHEZ_CONFIGURE_EXTRA=--disable-x11 JERBOA_NATIVE_FEATURES=tls
+      - name: Build and test
+        run: |
+          JB="$PWD/../jerboa/dist/jerbuild"
+          sh support/check-source-release.sh
+          JERBUILD="$JB" make security
+          JERBUILD="$JB" make test
+          JERBUILD="$JB" make binary
+          JERBUILD="$JB" make test-binary
+      - name: Smoke-check binary
+        run: ./jsh-linux-amd64 -c 'exit 0'
diff --git a/.forgejo/workflows/version-policy.yaml b/.forgejo/workflows/version-policy.yaml
new file mode 100644
index 0000000..2ccc656
--- /dev/null
+++ b/.forgejo/workflows/version-policy.yaml
@@ -0,0 +1,23 @@
+name: version-policy
+
+on:
+  pull_request:
+    branches: [main]
+
+jobs:
+  required:
+    runs-on: docker
+    container:
+      image: debian:stable
+    steps:
+      - name: Install Git
+        run: |
+          apt-get update
+          DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends git ca-certificates
+      - name: Check out full history
+        uses: https://code.forgejo.org/actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
+        with:
+          fetch-depth: 0
+          persist-credentials: false
+      - name: Require semantic version advancement
+        run: sh .forgejo/require-version-bump.sh
diff --git a/.gitsafeignore b/.gitsafeignore
new file mode 100644
index 0000000..6ebfdd3
--- /dev/null
+++ b/.gitsafeignore
@@ -0,0 +1,4 @@
+.forgejo/workflows/ci.yaml:high-entropy-hex:28
+.forgejo/workflows/ci.yaml:high-entropy-hex:41
+.forgejo/workflows/ci.yaml:high-entropy-hex:43
+.forgejo/workflows/version-policy.yaml:high-entropy-hex:18
diff --git a/AGENTS.md b/AGENTS.md
index 9b29031..8c65ec7 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,3 +1,21 @@
+## STOP: Forgejo Pull Requests Are Mandatory
+
+Every change to this repository must use the Forgejo pull-request workflow.
+
+1. Start from the current remote default branch and create a dedicated feature, fix, or chore branch **before editing**.
+2. Make only the scoped changes on that branch.
+3. Run every repository-required test and build. If the repository produces binaries, build them and run a meaningful smoke check (such as the documented startup, `--help`, or `--version`). Do not commit while any required check fails.
+4. Commit the verified changes on the feature branch and push that branch to `origin`.
+5. Open a pull request on `git.jerboa.sh` targeting the default branch. A human must review, approve, and merge it.
+
+Absolute bans: never commit or push directly to `main` or `master`; never self-approve or self-merge; never bypass branch protection; and never leave completed changes only in a local branch. Release work and urgent fixes follow the same branch-and-PR process.
+
+### Every PR Must Advance the Version
+
+`VERSION` is the authoritative repository version. Every pull request must change it to a strictly greater semantic version (`MAJOR.MINOR.PATCH`). Use a patch increment for fixes and maintenance, a minor increment for backward-compatible features, and a major increment for breaking changes. Keep package manifests, generated version constants, release artifact names, and user-visible version output synchronized with `VERSION`.
+
+Forgejo CI compares the proposed `VERSION` with the target branch and rejects an unchanged, malformed, or lower version.
+
 ## STOP: Editing `.ss`/`.sls` Files — Mandatory Rules
 
 These rules exist because local-model sessions have lost **hours** fighting
@@ -371,18 +389,17 @@ improved versions of the above.
 When working in a Jerboa project, **ONLY modify files in the current repo** unless the user explicitly names another path.
 
 Common sibling repos that exist but must NOT be touched without explicit instruction:
-- `~/mine/jerboa-mcp` — Legacy node MCP, superseded. The active MCP server now lives in THIS repo at `mcp/` + `data/`. Don't modify the legacy repo unless told.
-- `~/mine/jerboa-shell` — Only modify when user explicitly says to work there.
-- `~/mine/gerbil-mcp` — **NEVER touch**. Deprecated.
-- `~/mine/gerbil-orig` — Read-only reference for upstream Gerbil. Never modify.
+- the legacy Jerboa MCP sibling checkout — Legacy node MCP, superseded. The active MCP server now lives in THIS repo at `mcp/` + `data/`. Don't modify the legacy repo unless told.
+- the Jerboa shell sibling checkout — Only modify when user explicitly says to work there.
+- the deprecated Gerbil MCP sibling checkout — **NEVER touch**. Deprecated.
+- the upstream Gerbil reference checkout — Read-only reference for upstream Gerbil. Never modify.
 
 If a user instruction mentions a file path, use EXACTLY that path. Do not substitute a similar-looking path from another repo.
 
 ### Never Reference Sibling Checkouts in Build Files
 
 Build files (Makefile, shell scripts, CI config) must **never** resolve a
-dependency via a relative sibling path (`../jerboa-foo`) or an absolute
-`~/mine/jerboa-foo` path. That layout is specific to this one machine —
+dependency via a relative sibling path (`..`-relative) or an absolute machine-specific sibling path. That layout is specific to this one machine —
 other users and CI do not have it. Always vendor instead: fetch/clone the
 dependency into `vendor/` (or this repo's equivalent) at build time, or use
 a pinned-release fetch script, so the build is reproducible without
@@ -392,7 +409,7 @@ A sibling-path fallback is not just a portability bug: it can silently
 substitute a full alternate source tree (build config, embedded data,
 secrets) for the vendored one, with no equivalent safety default, changing
 what actually gets built without any indication. If you find one
-(`grep -rn '\.\./jerboa\|~/mine/jerboa'` over Makefiles/scripts), remove it
+(for example, any sibling-checkout reference in Makefiles or scripts), remove it
 and vendor properly instead.
 
 ---
diff --git a/Makefile b/Makefile
index 8b79c25..6492125 100644
--- a/Makefile
+++ b/Makefile
@@ -9,7 +9,7 @@ export JSH_VERSION
 export JSH_VERSION_SHORT
 JERBOA_VERSION ?= v0.2.0
 JERBOA_TOOL_DIR ?= $(CURDIR)/.jerboa/bin
-JERBOA_SHELL_URL ?= https://git.sr.ht/~lisp/jerboa-shell
+JERBOA_SHELL_URL ?= https://git.jerboa.sh/ober/jerboa-shell
 JERBOA_SHELL_DIR ?= $(CURDIR)/jerboa-shell
 LOCKED_SOURCE_FETCH ?= $(CURDIR)/support/fetch-locked-source.sh
 JERBUILD ?= $(shell if [ -x ./jerbuild ]; then echo ./jerbuild; \
@@ -48,7 +48,7 @@ JERBOA_NATIVE_HOST_FEATURES ?= $(JSH_NATIVE_NO_SQLITE_FEATURES)
 JSH_JERBUILD_DETERMINISTIC_IDS ?= 0
 JERBOA_SSH_REPO ?= $(VENDOR)/jerboa-ssh
 JSQLITE_REPO ?= $(VENDOR)/jsqlite
-JSQLITE_URL ?= https://git.sr.ht/~lisp/jerboa-sqlite
+JSQLITE_URL ?= https://git.jerboa.sh/ober/jerboa-sqlite
 JSQLITE_DIR ?= $(JSQLITE_REPO)/src
 JSQLITE_STAGE ?= _jerbuild-stage/jsqlite
 JERBOA_CRYPTO_REPO ?= $(VENDOR)/jerboa-crypto
@@ -725,12 +725,11 @@ freebsd:            freebsd-amd64
 jsh-freebsd-amd64:  freebsd-amd64
 
 # ─── macOS Pre-built Rust Libraries ─────────────────────────────────────
-# Upload locally-built .a files as sourcehut git artifacts (maintainer use).
-# Requires the `hut` CLI configured for git.sr.ht, and the tag $(MACOS_LIBS_TAG)
-# pushed to ~lisp/jerboa-shell first (sr.ht artifacts attach to an existing ref).
-# Note: sr.ht artifacts are immutable — to replace, delete via `hut git artifact delete` and re-upload.
+# Upload locally-built .a files to an existing Forgejo release.
 MACOS_LIBS_TAG ?= macos-libs-v1
 MACOS_ARCH := $(shell uname -m | sed 's/arm64/arm64/;s/x86_64/x86_64/')
+FORGEJO_API_URL ?= https://git.jerboa.sh/api/v1
+FORGEJO_RELEASE_REPOSITORY ?= ober/jerboa-shell
 
 upload-macos-libs:
 	@echo "=== Uploading pre-built Rust libs for macOS $(MACOS_ARCH) ==="
@@ -742,10 +741,20 @@ upload-macos-libs:
 		/tmp/libjerboa_native-macos-$(MACOS_ARCH).a
 	cp rust-coreutils/target/release/libjsh_coreutils.a \
 		/tmp/libjsh_coreutils-macos-$(MACOS_ARCH).a
-	hut git artifact upload $(MACOS_LIBS_TAG) \
-		/tmp/libjerboa_native-macos-$(MACOS_ARCH).a \
-		/tmp/libjsh_coreutils-macos-$(MACOS_ARCH).a
-	@echo "Done. Assets uploaded to ref $(MACOS_LIBS_TAG)"
+	@test -n "$$FORGEJO_TOKEN" || { echo "ERROR: FORGEJO_TOKEN is required"; exit 1; }
+	@command -v jq >/dev/null 2>&1 || { echo "ERROR: jq is required"; exit 1; }
+	@set -eu; \
+	  api="$(FORGEJO_API_URL)/repos/$(FORGEJO_RELEASE_REPOSITORY)"; \
+	  release_id=$$(curl -fsS -H "Authorization: token $$FORGEJO_TOKEN" \
+	    "$$api/releases/tags/$(MACOS_LIBS_TAG)" | jq -r .id); \
+	  for asset in \
+	    /tmp/libjerboa_native-macos-$(MACOS_ARCH).a \
+	    /tmp/libjsh_coreutils-macos-$(MACOS_ARCH).a; do \
+	    curl -fsS -H "Authorization: token $$FORGEJO_TOKEN" \
+	      -H 'Content-Type: application/octet-stream' --data-binary "@$$asset" \
+	      "$$api/releases/$$release_id/assets?name=$$(basename "$$asset")"; \
+	  done
+	@echo "Done. Assets uploaded to Forgejo release $(MACOS_LIBS_TAG)"
 
 # ─── Android/Termux Binary ───────────────────────────────────────────────
 # Build natively on Android/Termux, or cross-build an arm64 Android ELF from
@@ -847,7 +856,7 @@ vendor-deps: ensure-jsqlite vendor-native-rs-pruned
 		fi; \
 		if [ "$$needs_clone" = "1" ]; then \
 			echo "  Fetching locked $$repo..."; \
-			"$(LOCKED_SOURCE_FETCH)" "$$repo" "https://git.sr.ht/~lisp/$$repo" "vendor/$$repo"; \
+			"$(LOCKED_SOURCE_FETCH)" "$$repo" "https://git.jerboa.sh/ober/$$repo" "vendor/$$repo"; \
 		else \
 			echo "  (exists) $$repo"; \
 		fi; \
@@ -894,7 +903,7 @@ $(JERBOA_NATIVE_RS_DIR):
 	@echo "=== Sparse-cloning jerboa-native-rs from the jerboa monorepo ==="
 	@tmp="vendor/.jerboa-native-rs-monorepo"; \
 	  rm -rf "$$tmp"; \
-	  "$(LOCKED_SOURCE_FETCH)" jerboa https://git.sr.ht/~lisp/jerboa "$(JERBOA_NATIVE_RS_DIR)" jerboa-native-rs
+	  "$(LOCKED_SOURCE_FETCH)" jerboa https://git.jerboa.sh/ober/jerboa "$(JERBOA_NATIVE_RS_DIR)" jerboa-native-rs
 
 # ─── Hardening Verification ───────────────────────────────────────────────
 
diff --git a/README.md b/README.md
index 2d60c95..707e2b7 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # jerboa-shell-extras
 
-Optional feature bundles for [jerboa-shell](https://git.sr.ht/~lisp/jerboa-shell).
+Optional feature bundles for [jerboa-shell](https://git.jerboa.sh/ober/jerboa-shell).
 The base shell stays a compact Bash replacement: ordinary `make`, `make binary`,
 and `make jsh` builds in jerboa-shell do not fetch or enable this repository.
 
diff --git a/VERSION b/VERSION
index 0ea3a94..0c62199 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.2.0
+0.2.1
diff --git a/jpkg.sexp b/jpkg.sexp
index b22e6f6..7fabd3f 100644
--- a/jpkg.sexp
+++ b/jpkg.sexp
@@ -1,6 +1,6 @@
 (package
   (name "@ober/jerboa-shell-extras")
-  (version "0.1.0")
+  (version "0.2.1")
   (description "Optional feature bundles for the Jerboa shell")
   (license "MIT")
   (source "https://git.jerboa.sh/ober/jerboa-shell-extras")
diff --git a/support/ensure-jerboa.sh b/support/ensure-jerboa.sh
index 5a58d15..3398931 100755
--- a/support/ensure-jerboa.sh
+++ b/support/ensure-jerboa.sh
@@ -10,8 +10,8 @@
 #
 # Override the signed artifact location with:
 #   JERBOA_RELEASE_BASE=https://example.org/releases/v0.2.0
-# or the SourceHut repo with:
-#   JERBOA_RELEASE_REPO=~lisp/jerboa
+# or the Forgejo repo with:
+#   JERBOA_RELEASE_REPO=ober/jerboa
 # For testing or unusual hosts, override target detection with:
 #   JERBOA_RELEASE_TARGET=macos-arm64
 
@@ -26,8 +26,8 @@ usage() {
 
 version=$1
 bindir=${2:-.jerboa/bin}
-repo=${JERBOA_RELEASE_REPO:-~lisp/jerboa}
-origin=${JERBOA_RELEASE_ORIGIN:-https://git.sr.ht}
+repo=${JERBOA_RELEASE_REPO:-ober/jerboa}
+origin=${JERBOA_RELEASE_ORIGIN:-https://git.jerboa.sh}
 
 printf '%s\n' "$version" | grep -Eq '^v[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*$' || {
     echo "ERROR: version must be an exact tag such as v0.2.4" >&2
@@ -71,7 +71,7 @@ case "$target" in
 esac
 
 file="jerboa-${version}-${target}.tar.gz"
-base=${JERBOA_RELEASE_BASE:-${origin}/${repo}/refs/download/${version}}
+base=${JERBOA_RELEASE_BASE:-${origin}/${repo}/releases/download/${version}}
 url="${base%/}/${file}"
 manifest_url="${base%/}/release-manifest.sha256"
 signature_url="${manifest_url}.sig"
diff --git a/support/security-policy-check.sh b/support/security-policy-check.sh
index 41932d4..66b813e 100755
--- a/support/security-policy-check.sh
+++ b/support/security-policy-check.sh
@@ -59,7 +59,7 @@ end
 RUBY
 }
 
-for workflow in .github/workflows/*.yml .github/workflows/*.yaml; do
+for workflow in .forgejo/workflows/*.yml .forgejo/workflows/*.yaml; do
   [ -f "$workflow" ] || continue
   check_action_refs "$workflow" || fail "mutable action reference in $workflow"
 done
@@ -93,11 +93,11 @@ printf '%s\n' 'steps:' '  - uses: >-' \
 check_action_refs "$pinned_control" >/dev/null 2>&1 || \
   fail "workflow scanner rejected a pinned folded-scalar control"
 
-grep -q '^permissions:$' .github/workflows/ci.yml || fail "workflow permissions block missing"
-sed -n '/^permissions:$/,/^[^ ]/p' .github/workflows/ci.yml | grep -q '^  contents: read$' || \
+grep -q '^permissions:$' .forgejo/workflows/ci.yaml || fail "workflow permissions block missing"
+sed -n '/^permissions:$/,/^[^ ]/p' .forgejo/workflows/ci.yaml | grep -q '^  contents: read$' || \
   fail "workflow does not default to read-only contents permission"
 ! grep -Eq 'jerboa21/jerboa:latest|uses:[[:space:]]*[^#[:space:]]+@(v[0-9]+|stable|main|master)([[:space:]#]|$)' \
-  .github/workflows/ci.yml || fail "mutable CI execution input"
+  .forgejo/workflows/ci.yaml || fail "mutable CI execution input"
 
 grep -q 'ssh-keygen -Y verify' support/ensure-jerboa.sh || fail "release manifest signature verification missing"
 ! grep -Eq 'sum_url|\$\{?url\}?\.sha256' support/ensure-jerboa.sh || fail "co-hosted checksum bootstrap restored"
@@ -142,7 +142,7 @@ awk -F '\t' '
 ' support/source-lock.tsv || fail "source lock is malformed, incomplete, or duplicated"
 grep -q '^status=ready$' support/source-release.status || \
   fail "reviewed satellite source release status is not ready"
-grep -q 'sh support/check-source-release.sh' .github/workflows/ci.yml || \
+grep -q 'sh support/check-source-release.sh' .forgejo/workflows/ci.yaml || \
   fail "release workflow bypasses the satellite source gate"
 sh support/check-source-release.sh >/dev/null 2>&1 || \
   fail "reviewed satellite source gate did not pass"
@@ -151,13 +151,8 @@ sh support/check-source-release.sh >/dev/null 2>&1 || \
   fail "mutable Git dependency path restored"
 ! grep -Eq 'download_prebuilt|macos-libs-v1' build-jsh-macos.sh || \
   fail "unsigned prebuilt native fallback restored"
-if grep -E 'cargo (build|zigbuild)' build-jsh-macos.sh .github/workflows/ci.yml | grep -qv -- '--locked'; then
+if grep -E 'cargo (build|zigbuild)' build-jsh-macos.sh .forgejo/workflows/ci.yaml | grep -qv -- '--locked'; then
   fail "unlocked Cargo build restored"
 fi
 
-for variable in FREEBSD_BASE_SHA256 ZIG_SHA256 CARGO_ZIGBUILD_SHA256; do
-  grep -Eq "^  ${variable}: [0-9a-f]{64}$" .github/workflows/ci.yml || fail "$variable pin missing"
-  grep -q "\$${variable}  /tmp/" .github/workflows/ci.yml || fail "$variable is not checked before extraction"
-done
-
 echo "security policy checks: pass"
diff --git a/support/source-lock.tsv b/support/source-lock.tsv
index 49510aa..0f08889 100644
--- a/support/source-lock.tsv
+++ b/support/source-lock.tsv
@@ -1,13 +1,13 @@
 # name	url	commit	tree
-jerboa-shell	https://git.sr.ht/~lisp/jerboa-shell	52695eeffb1e08cf8998117436d28753288231da	9eafcdc347767badabeef9a45b34888eed289449
-jsqlite	https://git.sr.ht/~lisp/jerboa-sqlite	a25d077ff4b03bc6138c4d668aac31469b031f06	329d20dcce60a59b1135db017669c282dabb19b4
-jerboa	https://git.sr.ht/~lisp/jerboa	9f4ac78dc8c687d08841b14f26a747fd9ff74202	eeaa57cdb99666176adbd5eae21c6a0d23ed4dd0
-jerboa-coreutils	https://git.sr.ht/~lisp/jerboa-coreutils	549af48042752dae317a9573bad6e783fb0fca19	b04a9f05edf5d35e7c6d0464fc25cc3015248910
-jerboa-awk	https://git.sr.ht/~lisp/jerboa-awk	2a73bb948c8e513b97afe2f6e233f9ad6f314f03	4b5118d462aa9e12803de064573d00d133afed48
-jerboa-sed	https://git.sr.ht/~lisp/jerboa-sed	44df22636f89d138545bd16c1acf2a26b20b9a64	aaba1b863dd247dcfe47b665b148d97b203973f2
-jerboa-aws	https://git.sr.ht/~lisp/jerboa-aws	492b2b2d26222bc183702a504528286dcf75583a	ca10fa4de0e554097aad0658273b7c0e58067f08
-jerboa-yubikey	https://git.sr.ht/~lisp/jerboa-yubikey	788ff893c2667c720829e34b2e0a7cfa7098547f	33e2cee5d4a3aac04ef835ad123142eeb02cb162
-jerboa-ssh	https://git.sr.ht/~lisp/jerboa-ssh	7b5c4ac7c93901ea5c8b4005a67124a81b319d62	ec8d82461731f68e40072b1a25fe8fd22b479bbc
-jerboa-crypto	https://git.sr.ht/~lisp/jerboa-crypto	dc9fb27d8cad7900d1dc96c8ec8f0f57daef4ca0	462d8eafd371dcca4bf4e6eecda619bcc807678b
-jerboa-fuse	https://git.sr.ht/~lisp/jerboa-fuse	06cc92d9be2e20eed306d253732b36224a54ca46	6b679f07eff916be333ae60460afad922fc8b342
-jerboa-wormhole	https://git.sr.ht/~lisp/jerboa-wormhole	52c49e3b8b60cfa4ec2a4c3eecc8c97b6846e6dd	defbf7ff6b72bd2f057fb8dbef9fc24ba47e4dc6
+jerboa-shell	https://git.jerboa.sh/ober/jerboa-shell	52695eeffb1e08cf8998117436d28753288231da	9eafcdc347767badabeef9a45b34888eed289449
+jsqlite	https://git.jerboa.sh/ober/jerboa-sqlite	a25d077ff4b03bc6138c4d668aac31469b031f06	329d20dcce60a59b1135db017669c282dabb19b4
+jerboa	https://git.jerboa.sh/ober/jerboa	9f4ac78dc8c687d08841b14f26a747fd9ff74202	eeaa57cdb99666176adbd5eae21c6a0d23ed4dd0
+jerboa-coreutils	https://git.jerboa.sh/ober/jerboa-coreutils	549af48042752dae317a9573bad6e783fb0fca19	b04a9f05edf5d35e7c6d0464fc25cc3015248910
+jerboa-awk	https://git.jerboa.sh/ober/jerboa-awk	2a73bb948c8e513b97afe2f6e233f9ad6f314f03	4b5118d462aa9e12803de064573d00d133afed48
+jerboa-sed	https://git.jerboa.sh/ober/jerboa-sed	44df22636f89d138545bd16c1acf2a26b20b9a64	aaba1b863dd247dcfe47b665b148d97b203973f2
+jerboa-aws	https://git.jerboa.sh/ober/jerboa-aws	492b2b2d26222bc183702a504528286dcf75583a	ca10fa4de0e554097aad0658273b7c0e58067f08
+jerboa-yubikey	https://git.jerboa.sh/ober/jerboa-yubikey	788ff893c2667c720829e34b2e0a7cfa7098547f	33e2cee5d4a3aac04ef835ad123142eeb02cb162
+jerboa-ssh	https://git.jerboa.sh/ober/jerboa-ssh	7b5c4ac7c93901ea5c8b4005a67124a81b319d62	ec8d82461731f68e40072b1a25fe8fd22b479bbc
+jerboa-crypto	https://git.jerboa.sh/ober/jerboa-crypto	dc9fb27d8cad7900d1dc96c8ec8f0f57daef4ca0	462d8eafd371dcca4bf4e6eecda619bcc807678b
+jerboa-fuse	https://git.jerboa.sh/ober/jerboa-fuse	06cc92d9be2e20eed306d253732b36224a54ca46	6b679f07eff916be333ae60460afad922fc8b342
+jerboa-wormhole	https://git.jerboa.sh/ober/jerboa-wormhole	52c49e3b8b60cfa4ec2a4c3eecc8c97b6846e6dd	defbf7ff6b72bd2f057fb8dbef9fc24ba47e4dc6