diff --git a/AGENTS.md b/AGENTS.md index 6f568d6..c6d76bd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 `; `--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 `; `--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 [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 | diff --git a/scripts/pr-open.sh b/scripts/pr-open.sh new file mode 100755 index 0000000..4c7461e --- /dev/null +++ b/scripts/pr-open.sh @@ -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 [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 [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 diff --git a/scripts/tests/pr-open.test.sh b/scripts/tests/pr-open.test.sh new file mode 100755 index 0000000..c655a1d --- /dev/null +++ b/scripts/tests/pr-open.test.sh @@ -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"