From b1d15fe914944bf22b03d759f6de1e64cf342b91 Mon Sep 17 00:00:00 2001 From: Roberto Cano <3525807+robercano@users.noreply.github.com> Date: Fri, 3 Jul 2026 09:11:27 +0200 Subject: [PATCH] fix(harness): auto-assign new bot PRs to the repo owner (closes #33) Detects -a as well as --assignee/--assignee=* so callers passing a short-form assignee aren't overridden. Guards the exec against an unbound-variable error on bash < 4.4 when no args remain. Resolves the owner from the --repo/-R target (when given) ahead of the local origin remote, so cross-repo `pr create --repo other/acct` assigns the other repo's owner instead of the local repo's. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01BLTWAXsWFRtN6c1u8aKnP2 --- .claude/scripts/bot-gh.sh | 59 +++++++++++++++++++++++++++++++++++---- docs/USAGE.md | 5 ++++ 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/.claude/scripts/bot-gh.sh b/.claude/scripts/bot-gh.sh index 8f80080..3547f55 100755 --- a/.claude/scripts/bot-gh.sh +++ b/.claude/scripts/bot-gh.sh @@ -34,14 +34,16 @@ fi # Preflight: the bot needs collaborator access to EACH (private) repo it acts on # (setup step 2). Without it, gh fails with an opaque # "Could not resolve to a Repository with the name '/'" that reads like -# a typo, not a missing grant. If a --repo target is given and the bot can't see it, -# print the exact one-time grant + invite-accept commands instead. +# a typo, not a missing grant. If a --repo/-R target is given and the bot can't see +# it, print the exact one-time grant + invite-accept commands instead. This same +# $target_repo is also reused below to resolve the correct owner for cross-repo +# `pr create` calls. target_repo="" prev="" for a in "$@"; do - if [ "$prev" = "--repo" ]; then target_repo="$a"; break; fi + if [ -n "$prev" ]; then target_repo="$a"; break; fi case "$a" in - --repo) prev="--repo"; continue;; + --repo|-R) prev="1"; continue;; --repo=*) target_repo="${a#--repo=}"; break;; esac done @@ -57,4 +59,51 @@ EOF exit 1 fi -GH_TOKEN="$GH_BOT_TOKEN" exec gh "$@" +args=("$@") + +# Auto-assign new bot PRs to the repo owner, so the owner gets a notification +# that a PR is waiting for review (a bot-authored PR otherwise has no assignee). +# Only applies to `pr create`, and only if the caller didn't already pass +# --assignee (or its short form -a) themselves. +if [ "${1:-}" = "pr" ] && [ "${2:-}" = "create" ]; then + has_assignee=0 + for a in "$@"; do + case "$a" in + -a|--assignee|--assignee=*) has_assignee=1; break;; + esac + done + if [ "$has_assignee" -eq 0 ]; then + # Resolve the repo owner dynamically — never hardcode it. Precedence: + # OWNER_LOGIN env override, then the --repo/-R target (so cross-repo + # `pr create --repo other/acct` assigns the OTHER repo's owner, not the + # local origin's), then the local `origin` remote (works offline, unlike + # `gh repo view`), then `gh repo view` as a last resort. + owner="${OWNER_LOGIN:-}" + if [ -z "$owner" ] && [ -n "$target_repo" ]; then + owner="${target_repo%%/*}" + fi + if [ -z "$owner" ]; then + origin_url="$(git -C "$root" remote get-url origin 2>/dev/null || true)" + case "$origin_url" in + git@github.com:*) + owner="${origin_url#git@github.com:}" + owner="${owner%%/*}" + ;; + https://github.com/*) + owner="${origin_url#https://github.com/}" + owner="${owner%%/*}" + ;; + esac + fi + if [ -z "$owner" ]; then + owner="$(GH_TOKEN="$GH_BOT_TOKEN" gh repo view --json owner --jq .owner.login 2>/dev/null || true)" + fi + # Fail soft: if the owner can't be resolved, create the PR unassigned + # rather than erroring out. + if [ -n "$owner" ]; then + args+=(--assignee "$owner") + fi + fi +fi + +GH_TOKEN="$GH_BOT_TOKEN" exec gh ${args[@]+"${args[@]}"} diff --git a/docs/USAGE.md b/docs/USAGE.md index 9b32ccf..c4234bf 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -86,6 +86,11 @@ With `pr-per-agent`, the standing loop per ticket looks like: missing grant). `bot-gh.sh` preflights this and prints the fix; the one-time setup is, as the **owner**: `gh api -X PUT repos///collaborators/ -f permission=push`, then **accept as the bot**: `bot-gh.sh api -X PATCH user/repository_invitations/` (private-repo invites require acceptance). + **Owner notification:** `bot-gh.sh pr create` auto-assigns the new PR to the repo owner (unless the + caller already passed `--assignee`/`-a`) so the owner gets a GitHub notification that review is awaited. + The owner login comes from `$OWNER_LOGIN` if set, else — for cross-repo calls — from the `--repo`/`-R` + target's owner, else it's parsed from the local `origin` git remote. If it can't be resolved, the PR is + still created — just unassigned. 4. **Review** — the owner reviews on GitHub. To address comments, feed them back through the orchestrator (*"address the comments on PR #N"*): same implementer loop, same branch, push updates the PR in place. 5. **Merge** — owner approves, merge per `gates.json.merge`, clean the worktree (below).