PRs the pr-bot opens arm auto-merge; the merge invariant says a human's approval decides (#84)

* setup-repo.sh turns auto-merge on; check-repos.sh --check settings requires it

A pull request merges by itself once a human approves it and the required checks pass. The
ruleset is unchanged: one approval from someone other than the last pusher, and the checks.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* pr-open.sh opens a pull request as the bot and arms auto-merge; the merge invariant says why a human still decides

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Bruno Guilhermo de Barros Valério <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-24 21:58:15 +00:00
1 parent 84d7d8f9ba
commit 16e372e622
3 files changed
+83 -2

No files matched your search

+6 -2
View File
@@ -10,7 +10,10 @@ loosen these.
1. Humans steer. Stop and ask when work would leave an approved charter, spec or plan.
2. Write the failing test first. A test that never failed proves nothing.
3. Never merge a pull request, and never push to `main`. Humans merge.
3. Never merge a pull request, and never push to `main`. Humans merge: an agent may arm auto-merge on a pull
request it opens (`scripts/pr-open.sh`), because the ruleset still requires a human's approval, and that approval
is the merge decision. Never approve a pull request, never use an admin bypass, and never arm auto-merge on a
repo whose ruleset does not require an approval.
4. Never read, print or store secrets, tokens or keys.
5. Never weaken a guardrail: approval points, sandbox settings, policies, rulesets or security tests.
6. Write each document in the style the docs standard assigns to its path.
@@ -39,7 +42,8 @@ folder.
| Need | Script |
| --- | --- |
| Push, open a pull request or read GitHub as `ouroboric-pr-bot` | `scripts/as-bot.sh <repo> <cmd>`; `--read all` to read. The only way an agent reaches GitHub |
| Push, open a pull request or read GitHub as `ouroboric-pr-bot` | `scripts/as-bot.sh <repo> <cmd>`; `--read all` to read. The only way an agent reaches GitHub; to open a pull request, `scripts/pr-open.sh` is the default |
| Open a pull request as `ouroboric-pr-bot` and arm auto-merge on it: the default way an agent opens one. A human's approval still decides the merge; if arming fails the PR stays open and the script says why | `scripts/pr-open.sh <repo> [gh pr create args]`, e.g. `scripts/pr-open.sh gleipnir --base main --head my-branch --title "…" --body-file body.md`. Prints the PR's URL. Never merges directly, never `--admin` |
| Any real-engine run (Podman, devcontainer, the sandbox): the engine, registry config and checks | `scripts/ygg-engine.sh check\|env\|run --folder DIR`. Never set `CONTAINER_HOST` by hand: the root socket is the wrong one |
| The realms smoke runs (v0-H13, v0-H15) and their evidence | `scripts/ygg-smoke-realms.sh`, `scripts/ygg-smoke-loop.sh`, `scripts/ygg-evidence.sh` |
| The grader smoke on a real engine (v0-H16), with the public sample suite | `scripts/ygg-smoke-grade.sh [--build]`. Never a hidden suite by default; `--task ID --suite-dir DIR --reference DIR [--suite-engine DIR]` points the grader at a suite store as it is (never copied or read) and grades the task's reference solution; its output holds numbers only |
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Open a pull request as ouroboric-pr-bot and arm GitHub auto-merge on it. The default way an agent
# opens a pull request. Everything runs through as-bot.sh, so the token lasts one hour and reaches
# only the named repo.
# Arming auto-merge is not merging: the PR still waits for a human's approval and the required checks,
# which the ruleset demands. This script never merges directly and never uses an admin bypass.
# If arming fails, such as on a repo with auto-merge off (huldra), the PR stays open, the script says
# why and exits 1. It never closes or deletes the PR.
# Prints the PR's URL. Agent-run.
# pr-open.sh <repo> [gh pr create arguments...]
# pr-open.sh gleipnir --base main --head my-branch --title "..." --body-file body.md
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ORG="ouroboric-dev"
die() { echo "pr-open: $*" >&2; exit 2; }
[ $# -ge 1 ] || die "usage: pr-open.sh <repo> [gh pr create arguments...]"
repo="$1"; shift
[[ "$repo" =~ ^[A-Za-z0-9._-]+$ ]] || die "bad repo: $repo"
out="$("$HERE/as-bot.sh" "$repo" gh pr create --repo "$ORG/$repo" "$@")" || exit $?
url="$(printf '%s\n' "$out" | grep -Eo 'https://[^[:space:]]+/pull/[0-9]+' | tail -1 || true)"
[ -n "$url" ] || die "gh pr create printed no pull request URL: $out"
echo "$url"
if ! err="$("$HERE/as-bot.sh" "$repo" gh pr merge "$url" --auto --squash 2>&1)"; then
{
echo "pr-open: the PR stays open, but auto-merge is not armed: $url"
echo "pr-open: $err"
echo "pr-open: the repo may have auto-merge off, as huldra does; a human can merge it once approved"
} >&2
exit 1
fi
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# Tests for pr-open.sh against a fake as-bot.sh. scripts/tests/pr-open.test.sh
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
T="$(mktemp -d)"
trap 'rm -rf -- "$T"' EXIT
fail=0
check() { local d="$1"; shift; if "$@"; then echo "ok $d"; else echo "FAIL $d"; fail=1; fi; }
# The script runs from a copy next to a fake as-bot.sh, which logs each call and plays GitHub.
mkdir -p "$T/s"
cp "$HERE/../pr-open.sh" "$T/s/pr-open.sh"
cat > "$T/s/as-bot.sh" <<'FAKE'
#!/usr/bin/env bash
printf '%s\n' "$*" >> "$FAKE_LOG"
case "$3 $4" in
"pr create") echo "https://github.com/ouroboric-dev/gleipnir/pull/7" ;;
"pr merge") if [ -n "${FAKE_MERGE_FAIL:-}" ]; then echo "GraphQL: Auto merge is not allowed for this repository" >&2; exit 1; fi ;;
esac
FAKE
chmod +x "$T/s/pr-open.sh" "$T/s/as-bot.sh"
export FAKE_LOG="$T/log"
U=https://github.com/ouroboric-dev/gleipnir/pull/7
: > "$FAKE_LOG"
out="$("$T/s/pr-open.sh" gleipnir --base main --head my-branch --title "A title" --body-file body.md 2>"$T/err")"; rc=$?
check "success: exit 0" [ "$rc" = 0 ]
check "success: the URL is printed" [ "$out" = "$U" ]
check "success: create, then arm, in order, both as the bot for the repo" \
[ "$(cat "$FAKE_LOG")" = "gleipnir gh pr create --repo ouroboric-dev/gleipnir --base main --head my-branch --title A title --body-file body.md
gleipnir gh pr merge $U --auto --squash" ]
check "no admin bypass" bash -c "! grep -e '--admin' '$FAKE_LOG'"
check "the only merge call carries --auto" bash -c "[ \"\$(grep -c 'pr merge' '$FAKE_LOG')\" = 1 ] && grep 'pr merge' '$FAKE_LOG' | grep -q -e '--auto'"
: > "$FAKE_LOG"
out="$(FAKE_MERGE_FAIL=1 "$T/s/pr-open.sh" huldra --title t 2>"$T/err")"; rc=$?
check "arming fails: exit non-zero" [ "$rc" != 0 ]
check "arming fails: the message says the PR stays open and why" \
bash -c "grep -q 'stays open' '$T/err' && grep -q 'auto-merge' '$T/err' && grep -q 'Auto merge is not allowed' '$T/err'"
check "arming fails: the URL is still printed" [ "$out" = "$U" ]
check "arming fails: nothing closes or deletes the PR" bash -c "! grep -e 'close' -e 'delete' '$FAKE_LOG'"
: > "$FAKE_LOG"
check "no repo: usage, exit 2, no call" bash -c "'$T/s/pr-open.sh' >/dev/null 2>&1; [ \$? = 2 ] && [ ! -s '$FAKE_LOG' ]"
exit "$fail"