From 89a5b1e39797182f63a695100f24b835ba8c28dc Mon Sep 17 00:00:00 2001 From: Roberto Cano <3525807+robercano@users.noreply.github.com> Date: Sat, 20 Jun 2026 12:56:51 +0200 Subject: [PATCH 1/2] feat: autonomous PR feedback-iteration loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add pr-feedback.sh, which lists open bot-authored PRs whose latest CHANGES_REQUESTED review is newer than the bot's last marker comment (so handled feedback is not re-dispatched despite GitHub keeping reviewDecision=CHANGES_REQUESTED), skipping any PR already labeled claude-addressing. The notification cron runs it each firing and dispatches a background implementer per PR to address comments, run gates, push, and re-request review — merging stays gated on the human's approval. Pre-approve `bash .claude/scripts/pr-feedback.sh` so cron firings don't block on a permission prompt. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/scripts/pr-feedback.sh | 42 ++++++++++++++++++++++++++++++++++ .claude/settings.json | 2 ++ 2 files changed, 44 insertions(+) create mode 100644 .claude/scripts/pr-feedback.sh diff --git a/.claude/scripts/pr-feedback.sh b/.claude/scripts/pr-feedback.sh new file mode 100644 index 0000000..47042c4 --- /dev/null +++ b/.claude/scripts/pr-feedback.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# pr-feedback.sh — print open, bot-authored PRs that have UNADDRESSED "changes +# requested" feedback, so the notification cron can dispatch an implementer per PR +# to address it. Prints one TSV line per PR needing action: +# \t\t\t +# +# A PR is listed when its latest CHANGES_REQUESTED review is NEWER than the bot's +# last "" marker comment (so already-handled feedback is +# not re-dispatched even though GitHub keeps reviewDecision=CHANGES_REQUESTED until +# you re-review), AND it is not currently labeled `claude-addressing` (a guard so +# overlapping firings don't double-dispatch). The implementer posts the marker +# comment after pushing its fix, which advances the cursor past the request. +# +# Repo derived from the git remote; override with $1. Bot login via $BOT_LOGIN. +# Invoke as `bash .claude/scripts/pr-feedback.sh` (pre-approve that exact command). +set -euo pipefail + +repo="${1:-$(gh repo view --json nameWithOwner -q .nameWithOwner)}" +bot="${BOT_LOGIN:-robercano-ghbot}" +marker="" + +gh pr list -R "$repo" --state open \ + --json number,headRefName,author,labels \ + --jq '.[] | select(.author.login=="'"$bot"'") | [.number, .headRefName, ([.labels[].name]|join(","))] | @tsv' \ +| while IFS=$'\t' read -r num branch labels; do + case ",$labels," in *,claude-addressing,*) continue;; esac + + cr=$(gh api "repos/$repo/pulls/$num/reviews" \ + --jq '[.[]|select(.state=="CHANGES_REQUESTED")]|sort_by(.submitted_at)|last|"\(.submitted_at)\t\(.user.login)"' \ + 2>/dev/null || true) + if [ -z "$cr" ]; then continue; fi + tcr="${cr%%$'\t'*}" + reviewer="${cr#*$'\t'}" + + ta=$(gh api "repos/$repo/issues/$num/comments" \ + --jq '[.[]|select(.user.login=="'"$bot"'" and (.body|contains("'"$marker"'")))]|sort_by(.created_at)|last|.created_at // empty' \ + 2>/dev/null || true) + + if [ -z "$ta" ] || [[ "$tcr" > "$ta" ]]; then + printf '%s\t%s\t%s\t%s\n' "$num" "$branch" "$reviewer" "$tcr" + fi + done diff --git a/.claude/settings.json b/.claude/settings.json index dbf5483..b72447a 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -30,6 +30,8 @@ "Read(//**)", "Bash(bash .claude/scripts/gate.sh:*)", + "Bash(bash .claude/scripts/notify-poll.sh:*)", + "Bash(bash .claude/scripts/pr-feedback.sh:*)", "Bash(git status:*)", "Bash(git diff:*)", From 06ccddd0afc7520a71bc95cde6643542f435ad6b Mon Sep 17 00:00:00 2001 From: Roberto Cano <3525807+robercano@users.noreply.github.com> Date: Sat, 20 Jun 2026 13:34:02 +0200 Subject: [PATCH 2/2] fix: don't flag PRs with zero change-requests in pr-feedback.sh gojq renders `null|"\(.submitted_at)\t\(.user.login)"` as the literal "null\tnull" when there are no CHANGES_REQUESTED reviews, so the empty guard never tripped and every open bot PR was flagged. Add `select(.!=null)` after `last` so the extract is truly empty when no changes were requested. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/scripts/pr-feedback.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/scripts/pr-feedback.sh b/.claude/scripts/pr-feedback.sh index 47042c4..36ead53 100644 --- a/.claude/scripts/pr-feedback.sh +++ b/.claude/scripts/pr-feedback.sh @@ -26,7 +26,7 @@ gh pr list -R "$repo" --state open \ case ",$labels," in *,claude-addressing,*) continue;; esac cr=$(gh api "repos/$repo/pulls/$num/reviews" \ - --jq '[.[]|select(.state=="CHANGES_REQUESTED")]|sort_by(.submitted_at)|last|"\(.submitted_at)\t\(.user.login)"' \ + --jq '[.[]|select(.state=="CHANGES_REQUESTED")]|sort_by(.submitted_at)|last|select(.!=null)|"\(.submitted_at)\t\(.user.login)"' \ 2>/dev/null || true) if [ -z "$cr" ]; then continue; fi tcr="${cr%%$'\t'*}"