Merge main and reconcile repository policies

ober

c9e0097d8e72fd2c78fba03787e92db08c240286

diff --git a/.forgejo/ci-required.sh b/.forgejo/ci-required.sh
new file mode 100755
index 0000000..7be8575
--- /dev/null
+++ b/.forgejo/ci-required.sh
@@ -0,0 +1,76 @@
+#!/bin/sh
+set -eu
+
+if [ -n "${MAKE:-}" ]; then
+  MAKE_CMD=$MAKE
+elif command -v gmake >/dev/null 2>&1; then
+  MAKE_CMD=gmake
+else
+  MAKE_CMD=make
+fi
+
+has_target() {
+  target=$1
+  [ -f Makefile ] && grep -Eq "^${target}[[:space:]]*:" Makefile
+}
+
+if has_target verify; then
+  "$MAKE_CMD" verify
+else
+  ran=0
+  for target in security test check build; do
+    if has_target "$target"; then
+      "$MAKE_CMD" "$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_CMD" binary
+
+if has_target binary-smoke; then
+  "$MAKE_CMD" binary-smoke
+  exit 0
+fi
+if has_target smoke; then
+  "$MAKE_CMD" smoke
+  exit 0
+fi
+
+binary_list=$(mktemp)
+trap 'rm -f "$binary_list"' EXIT HUP INT TERM
+find . -maxdepth 3 -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 executable" >&2
+  exit 1
+}
+
+while IFS= read -r binary; do
+  echo "Smoke-checking $binary"
+  if command -v timeout >/dev/null 2>&1; then
+    run_with_timeout() { timeout 30 "$@"; }
+  else
+    run_with_timeout() { "$@"; }
+  fi
+  if run_with_timeout env QT_QPA_PLATFORM=offscreen       QTWEBENGINE_CHROMIUM_FLAGS=--disable-gpu "$binary" --version >/dev/null 2>&1; then
+    continue
+  fi
+  if run_with_timeout 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..85ebd63
--- /dev/null
+++ b/.forgejo/require-version-bump.sh
@@ -0,0 +1,38 @@
+#!/bin/sh
+set -eu
+base_ref=${1:-origin/${GITHUB_BASE_REF:-${FORGEJO_BASE_REF:-master}}}
+head_ref=${2:-HEAD}
+
+current_version() {
+  if [ -f VERSION ]; then
+    tr -d '[:space:]' < VERSION
+  elif [ -f Cargo.toml ]; then
+    sed -n '0,/^version[[:space:]]*=/s/^version[[:space:]]*=[[:space:]]*"\([^"]*\)".*//p' Cargo.toml | head -1
+  elif [ -f package.json ]; then
+    sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*//p' package.json | head -1
+  else
+    echo "0.0.0"
+  fi
+}
+
+old=$(git show "$base_ref:VERSION" 2>/dev/null | tr -d '[:space:]' || echo 0.0.0)
+new=$(current_version)
+case "$new" in
+  [0-9]*.[0-9]*.[0-9]*) ;;
+  *) echo "ERROR: VERSION must be semantic version MAJOR.MINOR.PATCH, got '$new'" >&2; exit 1 ;;
+esac
+if [ "$old" = "$new" ]; then
+  echo "ERROR: version did not advance: $old" >&2
+  exit 1
+fi
+python3 - "$old" "$new" <<'PY2'
+import sys
+old=sys.argv[1]; new=sys.argv[2]
+def parse(v):
+    try: return tuple(int(x) for x in v.split('.')[:3])
+    except Exception: return (0,0,0)
+if parse(new) <= parse(old):
+    print(f"ERROR: version must increase: {old} -> {new}", file=sys.stderr)
+    sys.exit(1)
+print(f"VERSION {new} is valid")
+PY2
diff --git a/.forgejo/workflows/ci.yaml b/.forgejo/workflows/ci.yaml
new file mode 100644
index 0000000..0697489
--- /dev/null
+++ b/.forgejo/workflows/ci.yaml
@@ -0,0 +1,25 @@
+name: required-ci
+
+on:
+  pull_request:
+    branches: [main]
+  push:
+    branches: [main]
+    tags: ['v*']
+  workflow_dispatch:
+
+jobs:
+  required:
+    runs-on: freebsd-amd64
+    steps:
+      - name: Check runner tools
+        run: |
+          for tool in sh git gmake file; do
+            command -v "$tool"
+          done
+      - name: Check out repository
+        uses: https://code.forgejo.org/actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
+        with:
+          persist-credentials: false
+      - name: Build, test, and smoke-check
+        run: sh .forgejo/ci-required.sh
diff --git a/.forgejo/workflows/version-policy.yaml b/.forgejo/workflows/version-policy.yaml
new file mode 100644
index 0000000..33de887
--- /dev/null
+++ b/.forgejo/workflows/version-policy.yaml
@@ -0,0 +1,19 @@
+name: version-policy
+
+on:
+  pull_request:
+    branches: [main]
+
+jobs:
+  required:
+    runs-on: freebsd-amd64
+    steps:
+      - name: Check runner tools
+        run: command -v git
+      - 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 origin/main HEAD
diff --git a/.gitsafeignore b/.gitsafeignore
new file mode 100644
index 0000000..3d4de38
--- /dev/null
+++ b/.gitsafeignore
@@ -0,0 +1,5 @@
+.forgejo/workflows/ci.yaml:high-entropy-hex:24
+.forgejo/workflows/ci.yaml:high-entropy-hex:37
+.forgejo/workflows/ci.yaml:high-entropy-hex:39
+.forgejo/workflows/ci.yaml:high-entropy-hex:63
+.forgejo/workflows/version-policy.yaml:high-entropy-hex:18
diff --git a/AGENTS.md b/AGENTS.md
index 643529d..db42032 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -4,6 +4,24 @@ Always commit development changes only in the matching `/Users/user/mine/jerboa-
 
 Before starting work that compares against or uses a vendor checkout, update that vendor checkout to the latest revision and verify it matches exactly with the corresponding `/Users/user/mine/jerboa-*` repository state. If they differ, stop and sync from `/Users/user/mine/jerboa-*` before making or committing changes. The `/Users/user/mine/jerboa-*` worktree must always represent the latest authoritative version.
 
+## 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
@@ -376,30 +394,26 @@ 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.
 
-### Release Distribution: git.jerboa.sh Only
+### Git Hosting & Release Distribution: git.jerboa.sh Only
 
-**Never use SourceHut or GitHub Releases, release URLs, mirrors, or fallbacks
-for Jerboa artifacts.** The canonical human-facing repository URL is
-`https://git.jerboa.sh/ober/jerboa`. Publish release files on the matching
-versioned release at `git.jerboa.sh`; consumers download them from
+**Never use SourceHut or GitHub** for remotes, dependency fetches, release
+artifacts, mirrors, or fallbacks. All Jerboa repositories and versioned release
+assets live on `git.jerboa.sh`. Publish through Forgejo releases and download
+artifacts from
 `https://git.jerboa.sh/ober/jerboa/releases/download/<tag>/<artifact>`.
 
-If delivery needs work, repair or configure the `git.jerboa.sh` release path.
-Do not reintroduce SourceHut or add a GitHub mirror.
-
 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
@@ -409,7 +423,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/VERSION b/VERSION
new file mode 100644
index 0000000..b1e80bb
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+0.1.3
diff --git a/jpkg.sexp b/jpkg.sexp
index b9a8f64..09f96a4 100644
--- a/jpkg.sexp
+++ b/jpkg.sexp
@@ -1,6 +1,6 @@
 (package
   (name "@ober/gitsite")
-  (version "0.1.0")
+  (version "0.1.2")
   (description "Minimal self-hosted git forge in Jerboa")
   (license "MIT")
   (source "https://git.jerboa.sh/ober/jerboa-gitsite")