From 251a1f6572ef8afd5ebfd82dd0618dfe3dd24d6e Mon Sep 17 00:00:00 2001 From: robercano-ghbot Date: Tue, 30 Jun 2026 13:34:13 +0300 Subject: [PATCH 1/2] =?UTF-8?q?feat(scripts):=20merge-ready.sh=20=E2=80=94?= =?UTF-8?q?=20auto-merge=20owner-approved,=20CI-green=20PRs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/scripts/merge-ready.sh | 87 ++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 .claude/scripts/merge-ready.sh diff --git a/.claude/scripts/merge-ready.sh b/.claude/scripts/merge-ready.sh new file mode 100644 index 0000000..c98ad96 --- /dev/null +++ b/.claude/scripts/merge-ready.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# merge-ready.sh — merge every open PR that the repo OWNER has approved and that +# is safe to merge, then delete the branch. The human Approve on GitHub is the +# ONLY gate; this script never approves anything — it just acts on approvals. +# +# A PR is merged iff ALL hold: +# - base is the configured baseBranch (from gates.json, default main), not a draft +# - latest review by the OWNER (the approver) is APPROVED +# - that approval was submitted at/after the PR's last commit (so it covers the +# current head — guards against new commits pushed after an approval, since a +# free private repo has no branch protection to auto-dismiss stale approvals) +# - mergeable (no conflicts) +# - every CI check is green (no failing, none still pending) +# Anything else is SKIPPED with a reason. Output is JSON lines the cron summarizes. +# +# Repo is derived from the git remote (override with $1 = owner/repo). The approver +# defaults to the repo's owner login (override with $APPROVER — e.g. when the repo is +# org-owned and the human reviewer is not the org). Runs as the bot (GH_BOT_TOKEN, a +# write collaborator) so it works headless in the notification cron. Merging is not +# approving, so the bot may merge bot-authored PRs. +# Pre-approved in .claude/settings.json as `bash .claude/scripts/merge-ready.sh`. +set -euo pipefail +export PATH="$HOME/.local/bin:$PATH" + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +if [ -f "$root/.env" ]; then set -a; . "$root/.env"; set +a; fi +: "${GH_BOT_TOKEN:?GH_BOT_TOKEN not set — add it to .env (see .env.example)}" +export GH_TOKEN="$GH_BOT_TOKEN" + +repo="${1:-$(gh repo view --json nameWithOwner -q .nameWithOwner)}" +owner="${APPROVER:-$(gh repo view -R "$repo" --json owner -q .owner.login)}" # approver whose APPROVED review authorizes a merge +gates="$root/.claude/gates.json" +base="$(node -e "try{const g=require('$gates');process.stdout.write((g.merge&&g.merge.baseBranch)||'main')}catch(e){process.stdout.write('main')}")" + +# Decide MERGE / SKIP: for one PR's JSON (read on stdin). +decide() { + node -e ' + const base = process.argv[1], owner = process.argv[2]; + function verdict(p) { + if (p.isDraft) return "SKIP:draft"; + if (p.baseRefName !== base) return "SKIP:base-is-"+p.baseRefName; + if (p.mergeable !== "MERGEABLE") return "SKIP:mergeable="+p.mergeable; + + // CI: every check green; none failing or pending. + for (const c of (p.statusCheckRollup||[])) { + if (c.conclusion !== undefined && c.conclusion !== null && c.conclusion !== "") { // CheckRun + if (["FAILURE","CANCELLED","TIMED_OUT","ACTION_REQUIRED","STARTUP_FAILURE","STALE"].includes(c.conclusion)) + return "SKIP:check-failed:"+(c.name||""); + if (c.status && c.status !== "COMPLETED") return "SKIP:check-pending:"+(c.name||""); + } else if (c.state) { // legacy StatusContext + if (["FAILURE","ERROR"].includes(c.state)) return "SKIP:status-failed:"+(c.context||""); + if (c.state === "PENDING") return "SKIP:status-pending:"+(c.context||""); + } + } + + // Latest review by the owner must be APPROVED and cover the current head. + const mine = (p.reviews||[]).filter(r => r.author && r.author.login === owner && r.submittedAt) + .sort((a,b) => a.submittedAt.localeCompare(b.submittedAt)); + const last = mine[mine.length-1]; + if (!last) return "SKIP:no-owner-review"; + if (last.state !== "APPROVED") return "SKIP:owner-review="+last.state; + const commits = p.commits||[]; + const head = commits.length ? commits[commits.length-1].committedDate : null; + if (head && last.submittedAt < head) return "SKIP:approval-stale (re-approve current head)"; + return "MERGE"; + } + let p; try { p = JSON.parse(require("fs").readFileSync(0,"utf8")); } catch(e){ console.log("SKIP:bad-json"); process.exit(0); } + console.log(verdict(p)); + ' "$base" "$owner" +} + +merged=0; skipped=0 +for n in $(gh pr list -R "$repo" --base "$base" --state open --json number -q '.[].number'); do + data="$(gh pr view "$n" -R "$repo" --json number,title,isDraft,baseRefName,mergeable,reviews,statusCheckRollup,commits)" + verdict="$(printf '%s' "$data" | decide)" + title="$(printf '%s' "$data" | node -e 'process.stdout.write((JSON.parse(require("fs").readFileSync(0,"utf8")).title)||"")')" + if [ "$verdict" = "MERGE" ]; then + if gh pr merge "$n" -R "$repo" --merge --delete-branch >/dev/null 2>&1; then + echo "{\"pr\":$n,\"action\":\"merged\",\"title\":\"$title\"}"; merged=$((merged+1)) + else + echo "{\"pr\":$n,\"action\":\"merge-failed\",\"title\":\"$title\"}" + fi + else + echo "{\"pr\":$n,\"action\":\"skip\",\"reason\":\"${verdict#SKIP:}\",\"title\":\"$title\"}"; skipped=$((skipped+1)) + fi +done +echo "=== merge-ready: merged=$merged skipped=$skipped ===" From d461dc7d771a1fe04a43df16285b752da2b3610c Mon Sep 17 00:00:00 2001 From: robercano-ghbot Date: Tue, 30 Jun 2026 13:34:14 +0300 Subject: [PATCH 2/2] feat(commands): /pr-loop slash command to arm the PR loop in one step --- .claude/commands/pr-loop.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .claude/commands/pr-loop.md diff --git a/.claude/commands/pr-loop.md b/.claude/commands/pr-loop.md new file mode 100644 index 0000000..e82e945 --- /dev/null +++ b/.claude/commands/pr-loop.md @@ -0,0 +1,29 @@ +--- +description: Arm (or re-arm) the autonomous PR-loop cron and run one tick now +--- + +You are (re)arming this project's autonomous PR loop. The loop is session-scoped (cron jobs die when Claude Code exits and may not persist across restarts even when durable), so it is lost at the start of each new session. This command restores the whole loop in one step. Do BOTH parts. + +The repo is derived from the git remote (`gh repo view --json nameWithOwner -q .nameWithOwner`); the bot login defaults to `$BOT_LOGIN`. Nothing here is project-specific — it reads `.claude/gates.json`, `.claude/scripts/*`, and `docs/USAGE.md`. + +## 1. (Re)arm the cron — idempotent +- Call `CronList`. If a job already exists whose prompt mentions "autonomous PR loop", leave it (do not duplicate) and report its id + schedule. +- Otherwise `CronCreate` with `durable: true`, schedule `6,21,36,51 * * * *`, and the EXACT prompt below (STEP 0 will self-adjust the cadence on the first tick). + +Prompt to use (the tick logic, with adaptive STEP 0): + +> Run one tick of the autonomous PR loop. Resolve the repo with `gh repo view --json nameWithOwner -q .nameWithOwner`. Follow docs/USAGE.md and .claude/agents/*; reviewer lenses + consensus per .claude/gates.json; PRs are created/updated via .claude/scripts/bot-gh.sh (bot author), commits stay as the owner. +> +> STEP 0 — adaptive cadence: count open PRs (base = gates.json merge.baseBranch, default main) and open issues labelled module:*. Desired cadence = FAST "* * * * *" if there is ≥1 open PR OR ≥1 open module:* issue; else IDLE "17 * * * *". If this job's current schedule != desired, CronDelete this job and CronCreate a durable replacement with this SAME prompt at the desired schedule. +> +> Then, in order: +> 1. POLL: run `bash .claude/scripts/notify-poll.sh`; summarize new issues / PR comments / reviews and the open-PR status section. +> 2. MERGE: run `bash .claude/scripts/merge-ready.sh`; report each PR merged or why skipped. (It only merges PRs the owner APPROVED that are CI-green & mergeable; never approves.) +> 3. ADDRESS FEEDBACK: run `bash .claude/scripts/pr-feedback.sh`; for each PR it lists (bot-authored, with unaddressed CHANGES_REQUESTED), run orchestrator→worktree implementer→reviewer-lenses on the SAME branch, push to update the PR in place, and post the `` marker comment. Do NOT merge here. +> 4. ADVANCE: ONLY when there are ZERO open PRs — pick the lowest-numbered open module:* issue with no feat/issue--* branch; drive it through the orchestrator (scope → worktree implementer → gate.sh gates → reviewer lenses → bot PR). One issue in flight at a time. +> 5. If nothing actionable, reply exactly one line: "No actionable activity." + +## 2. Run one tick now +Execute steps 1–5 above immediately so the loop doesn't wait for the next cron fire. Report what happened (polled items, merges, feedback addressed, issue advanced — or "no actionable activity"). + +Notes: requires the bot machine account set up per docs/USAGE.md (`GH_BOT_TOKEN` in `.env`, bot is a write collaborator) so PRs are bot-authored and the owner can formally Approve them. For a tighter in-session cadence you can also run `/loop 5m /pr-loop`.