feat: extract reforge engine into a standalone consumable flake

The forgejo-sandbox / reforge harness, lifted out of the machine config
into a host-agnostic, generic engine anyone can consume with Nix.

Two layers:
- engine (this repo) — nixosModules.reforge stands up the sandbox forge,
  provisions role accounts + tokens, enforces branch protection, and puts
  the reforge-* CLI + forgejo-mcp on PATH. Carries no project specifics.
- run config — per-project manifest/charter/agenda/issues an adopter fills
  in; scaffold one with the `reforge` flake template.

Portability fixes vs the in-config version:
- forgejo-mcp resolved from $REFORGE_MCP_BIN or PATH, never a named host
  (kills the nixosConfigurations.omni hardcode).
- all instance data + paths parameterized via REFORGE_* env, baked into the
  reforge-scripts wrappers from module options (configDir, agentsDir,
  refsDir, org, port, tokenOwner, ...).
- option namespace neutral (reforge.* not omni.packs.*); settings policies
  carry no absolute /etc/nixos paths.
- role briefs + orchestrator playbook genericized: all project specifics
  point at the charter; refs corpus optional.

Validated: nix flake check (eval) + builds of forgejo-mcp, reforge-scripts,
and a module-eval check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-07-18 22:50:49 +02:00
commit df0fd9a9ba
32 changed files with 2698 additions and 0 deletions

151
scripts/reforge-seed.sh Normal file
View file

@ -0,0 +1,151 @@
#!/usr/bin/env bash
# Seed the sandbox forge for a reforge run (docs/reforge.md). The repo set,
# pinned bases and declared targets live in the run manifest,
# $REFORGE_CONFIG_DIR/manifest.txt:
#
# - kind=fork -> clone of the upstream at base_ref (branch, tag, or
# commit SHA; full history), pushed as `main`, then
# protected (no direct push, security-lead approval
# required).
# - kind=original -> empty placeholder repo, unprotected until a first
# scaffold exists.
# - charter -> in-forge copy of $REFORGE_CONFIG_DIR/charter.md
# (README.md) + agenda.md (AGENDA.md).
#
# Targets are never pushed into the sandbox — the acceptance diff against
# them is reforge-compare.
#
# Idempotent: every step is check-before-create / skip-if-pushed, so
# re-running converges. NOTE: changing base_ref does NOT re-seed an
# existing repo — reset first (reforge-reset reset).
#
# reforge-seed
set -euo pipefail
FORGE_URL=${REFORGE_FORGE_URL:-http://localhost:3030}
ORG=${REFORGE_ORG:-sandbox-team}
ADMIN_USER=${REFORGE_ADMIN_USER:-sandbox-admin}
TOKENS_DIR=${REFORGE_TOKENS_DIR:-/var/lib/forgejo-sandbox/tokens}
CONFIG_DIR=${REFORGE_CONFIG_DIR:?set REFORGE_CONFIG_DIR to your run config dir (manifest.txt, charter.md, agenda.md)}
TOKEN_FILE=${REFORGE_ADMIN_TOKEN_FILE:-$TOKENS_DIR/${ADMIN_USER}.token}
API="$FORGE_URL/api/v1"
TOKEN=$(cat "$TOKEN_FILE")
MANIFEST="$CONFIG_DIR/manifest.txt"
[ -r "$MANIFEST" ] || { echo "seed: no readable manifest at $MANIFEST" >&2; exit 1; }
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
# Loud abort: a mid-run failure (bad pin, unreachable upstream) must not
# scroll by unnoticed — the run is incomplete until "converged" prints.
trap 'echo; echo "seed: ABORTED — fix the error above and re-run (completed repos are skipped)." >&2' ERR
RESP="$WORK/resp"
api() { # method path [json-body] -> echoes HTTP code, body in $RESP
local method=$1 path=$2 data=${3:-}
local args=(
-sS -o "$RESP" -w '%{http_code}' -X "$method"
-H "Authorization: token $TOKEN"
-H 'Content-Type: application/json'
)
if [ -n "$data" ]; then args+=(--data "$data"); fi
curl "${args[@]}" "$API$path"
}
ok() { case $1 in 200 | 201 | 204) return 0 ;; *) return 1 ;; esac }
must() { # code context
if ! ok "$1"; then
echo "seed: $2 failed (HTTP $1):" >&2
cat "$RESP" >&2
exit 1
fi
}
auth_url() { # name
echo "http://$ADMIN_USER:$TOKEN@${FORGE_URL#http://}/$ORG/$1.git"
}
ensure_repo() { # name description
if [ "$(api GET "/repos/$ORG/$1")" = 404 ]; then
must "$(api POST "/orgs/$ORG/repos" \
"{\"name\":\"$1\",\"private\":true,\"auto_init\":false,\"default_branch\":\"main\",\"description\":\"$2\"}")" \
"create repo $ORG/$1"
echo " created repo $ORG/$1"
fi
}
protect_main() { # name (same rule the module puts on the working repo)
if [ "$(api GET "/repos/$ORG/$1/branch_protections/main")" = 404 ]; then
must "$(api POST "/repos/$ORG/$1/branch_protections" \
'{"branch_name":"main","rule_name":"main","enable_push":false,"required_approvals":1,"enable_approvals_whitelist":true,"approvals_whitelist_username":["security-lead"],"block_on_rejected_reviews":true,"dismiss_stale_approvals":true}')" \
"protect $ORG/$1 main"
echo " protected main"
fi
}
seed_fork() { # name upstream ref
local name=$1 url=$2 ref=$3 sha
echo "-- $name <- $url @ $ref"
ensure_repo "$name" "clean base of $url (no downstream work)"
if [ -n "$(git ls-remote "$(auth_url "$name")" refs/heads/main)" ]; then
echo " main already seeded, skipping (reset to re-seed at a new base_ref)"
else
if [ "$ref" = auto ]; then
ref=$(git ls-remote --symref "$url" HEAD |
awk '/^ref:/ {sub("refs/heads/", "", $2); print $2}')
fi
echo " cloning at $ref"
if git clone --quiet --single-branch --branch "$ref" "$url" "$WORK/$name" 2>/dev/null; then
: # branch or tag — single-branch history
else
# bare commit SHA — needs a full clone, then detached checkout
git clone --quiet "$url" "$WORK/$name"
git -C "$WORK/$name" checkout --quiet "$ref"
fi
sha=$(git -C "$WORK/$name" rev-parse HEAD)
git -C "$WORK/$name" push --quiet "$(auth_url "$name")" "HEAD:refs/heads/main"
rm -rf "${WORK:?}/$name"
echo " seeded main = $sha ($ref)"
fi
protect_main "$name"
}
echo "== charter: in-forge copy of the standard + run agenda =="
ensure_repo charter "project charter — the standard changes are judged against, plus the run agenda"
if [ -z "$(git ls-remote "$(auth_url charter)" refs/heads/main)" ]; then
git init --quiet -b main "$WORK/charter"
cp "$CONFIG_DIR/charter.md" "$WORK/charter/README.md"
cp "$CONFIG_DIR/agenda.md" "$WORK/charter/AGENDA.md"
git -C "$WORK/charter" add README.md AGENDA.md
git -C "$WORK/charter" \
-c user.name="$ADMIN_USER" -c user.email="$ADMIN_USER@sandbox.invalid" \
commit --quiet -m "charter: the standard + run agenda"
git -C "$WORK/charter" push --quiet "$(auth_url charter)" main
rm -rf "${WORK:?}/charter"
echo " seeded charter (README.md + AGENDA.md <- $CONFIG_DIR)"
fi
protect_main charter
echo
echo "== stack repos (from the manifest) =="
mapfile -t ROWS < <(grep -Ev '^[[:space:]]*(#|$)' "$MANIFEST")
for row in "${ROWS[@]}"; do
IFS='|' read -r name kind upstream base_ref _target_url _target_ref <<<"$row"
case $kind in
fork)
seed_fork "$name" "$upstream" "$base_ref"
;;
original)
echo "-- $name (placeholder)"
ensure_repo "$name" "placeholder — original software, to be rebuilt"
;;
*)
echo "seed: unknown kind '$kind' for $name in $MANIFEST" >&2
exit 1
;;
esac
done
echo
echo "seed: converged. Repos: $FORGE_URL/$ORG"