Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 72 additions & 27 deletions .github/workflows/amber-issue-handler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,16 +100,14 @@ jobs:
first line of the PR body (read your session ID from the
AGENTIC_SESSION_NAME environment variable):
<!-- acp:session_id=$AGENTIC_SESSION_NAME source=#${{ steps.issue.outputs.number }} last_action=<ISO8601_NOW> retry_count=0 -->
At the bottom of the PR body, add a session link:
---
🤖 [Ambient Session]($PLATFORM_HOST/projects/$AGENTIC_SESSION_NAMESPACE/sessions/$AGENTIC_SESSION_NAME)
6. Add the `ambient-code:managed` label to the PR.
7. After creating the PR, send a Slack notification:
```bash
curl -X POST -H 'Content-type: application/json' \
--data '{"text":"PR created for #${{ steps.issue.outputs.number }}\n*PR*: <PR_URL>\n*Session*: '"$PLATFORM_HOST/projects/$AGENTIC_SESSION_NAMESPACE/sessions/$AGENTIC_SESSION_NAME"'"}' \
"$SLACK_WEBHOOK_URL"
```
Only send if SLACK_WEBHOOK_URL is set.
8. Ensure CI passes. If it fails, investigate and fix.
9. Do not merge. Leave the PR open for human review.
7. Ensure CI passes. If it fails, investigate and fix.
8. Do not merge. Leave the PR open for human review.
9. When you comment on the PR, include this footer at the end:
_🤖 [Session]($PLATFORM_HOST/projects/$AGENTIC_SESSION_NAMESPACE/sessions/$AGENTIC_SESSION_NAME)_
repos: >-
[{"url": "https://github.com/${{ github.repository }}", "branch": "main"}]
model: claude-opus-4-6
Expand Down Expand Up @@ -231,13 +229,17 @@ jobs:
4. Ensure the PR body contains this frontmatter as the first line
(read your session ID from the AGENTIC_SESSION_NAME environment variable):
<!-- acp:session_id=$AGENTIC_SESSION_NAME source=#${{ steps.context.outputs.number }} last_action=<ISO8601_NOW> retry_count=<N> -->
Increment retry_count from whatever it was before. If retry_count reaches 3 or more,
stop working, add `ambient-code:needs-human` label, remove `ambient-code:managed` label,
Only increment retry_count if you actually had to fix something (CI failure,
conflict, review comment). If the PR is already healthy, do NOT increment —
just update last_action. If retry_count reaches 3 or more, stop working,
add `ambient-code:needs-human` label, remove `ambient-code:managed` label,
comment "AI was unable to resolve after 3 attempts. Needs human attention.",
and send a Slack notification (see below).
5. Add the `ambient-code:managed` label.
6. Do not merge. Do not close. Do not force-push.
7. If fundamentally broken beyond repair, add a comment explaining and stop.
8. When you comment on the PR, include this footer at the end:
_🤖 [Session]($PLATFORM_HOST/projects/$AGENTIC_SESSION_NAMESPACE/sessions/$AGENTIC_SESSION_NAME)_

## Slack Notifications

Expand Down Expand Up @@ -293,16 +295,14 @@ jobs:
first line of the PR body (read your session ID from the
AGENTIC_SESSION_NAME environment variable):
<!-- acp:session_id=$AGENTIC_SESSION_NAME source=#${{ steps.context.outputs.number }} last_action=<ISO8601_NOW> retry_count=0 -->
At the bottom of the PR body, add a session link:
---
🤖 [Ambient Session]($PLATFORM_HOST/projects/$AGENTIC_SESSION_NAMESPACE/sessions/$AGENTIC_SESSION_NAME)
6. Add the `ambient-code:managed` label to the PR.
7. After creating the PR, send a Slack notification:
```bash
curl -X POST -H 'Content-type: application/json' \
--data '{"text":"PR created for #${{ steps.context.outputs.number }}\n*PR*: <PR_URL>\n*Session*: '"$PLATFORM_HOST/projects/$AGENTIC_SESSION_NAMESPACE/sessions/$AGENTIC_SESSION_NAME"'"}' \
"$SLACK_WEBHOOK_URL"
```
Only send if SLACK_WEBHOOK_URL is set.
8. Ensure CI passes. If it fails, investigate and fix.
9. Do not merge. Leave the PR open for human review.
7. Ensure CI passes. If it fails, investigate and fix.
8. Do not merge. Leave the PR open for human review.
9. When you comment on the PR, include this footer at the end:
_🤖 [Session]($PLATFORM_HOST/projects/$AGENTIC_SESSION_NAMESPACE/sessions/$AGENTIC_SESSION_NAME)_
repos: >-
[{"url": "https://github.com/${{ github.repository }}", "branch": "main"}]
model: claude-opus-4-6
Expand Down Expand Up @@ -552,10 +552,49 @@ jobs:
print(f" Failed to create session: {e}")
return None

# Get all open ambient-code:managed PRs (include updatedAt to avoid per-PR API calls)
BOT_LOGINS = ["github-actions[bot]", "ambient-code[bot]", "ambient-bot"]

def needs_attention(pr_number):
"""Check if a PR has actionable issues that need the fixer's attention.
Returns (needs_work, reason) tuple."""
# Check CI status
checks_json = gh("pr", "checks", str(pr_number), "--repo", REPO,
"--json", "name,state",
"--jq", "[.[] | .state] | unique")
try:
states = json.loads(checks_json) if checks_json else []
except json.JSONDecodeError:
states = []

if not states:
pass # No CI checks — nothing to fix
elif "FAILURE" in states:
return True, "CI failing"

# Check for merge conflicts
mergeable = gh("pr", "view", str(pr_number), "--repo", REPO,
"--json", "mergeable", "--jq", ".mergeable")
if mergeable == "CONFLICTING":
return True, "merge conflicts"

# Check for changes_requested from non-bot users
bot_filter = " and ".join([f'.user.login != "{b}"' for b in BOT_LOGINS])
try:
reviews_raw = gh("api", f"repos/{REPO}/pulls/{pr_number}/reviews",
"--jq", f'[.[] | select(.state == "CHANGES_REQUESTED" and {bot_filter})] | length')
changes_requested = int(reviews_raw) if reviews_raw else 0
except (ValueError, TypeError):
changes_requested = 0

if changes_requested > 0:
return True, "changes requested"
Comment on lines +583 to +590
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="${GITHUB_REPOSITORY:-$(gh repo view --json nameWithOwner --jq '.nameWithOwner')}"

gh pr list --repo "$repo" --state open --label "ambient-code:managed" --json number --jq '.[].number' |
while read -r pr; do
  reviews="$(gh api --paginate "repos/$repo/pulls/$pr/reviews" | jq -cs 'add')"
  python - "$pr" <<'PY' <<<"$reviews"
import json, sys

pr = sys.argv[1]
bots = {"github-actions[bot]", "ambient-code[bot]", "ambient-bot"}
reviews = json.load(sys.stdin)

historical = any(
    r.get("state") == "CHANGES_REQUESTED"
    and r.get("user", {}).get("login") not in bots
    for r in reviews
)

latest = {}
for r in sorted(reviews, key=lambda r: r.get("submitted_at") or ""):
    login = r.get("user", {}).get("login")
    if login and login not in bots:
        latest[login] = r.get("state")

active = any(state == "CHANGES_REQUESTED" for state in latest.values())
if historical and not active:
    print(f"PR #{pr} has stale CHANGES_REQUESTED history but no active latest non-bot changes request")
PY
done

Repository: ambient-code/platform

Length of output: 1796


🏁 Script executed:

fd -t f amber-issue-handler.yml

Repository: ambient-code/platform

Length of output: 47


🏁 Script executed:

ls -la .github/workflows/ | head -30

Repository: ambient-code/platform

Length of output: 1922


🏁 Script executed:

sed -n '570,600p' .github/workflows/amber-issue-handler.yml

Repository: ambient-code/platform

Length of output: 1482


🏁 Script executed:

sed -n '1,100p' .github/workflows/amber-issue-handler.yml | grep -A 30 "def gh"

Repository: ambient-code/platform

Length of output: 47


🏁 Script executed:

grep -n "def gh" .github/workflows/amber-issue-handler.yml

Repository: ambient-code/platform

Length of output: 93


🏁 Script executed:

sed -n '434,460p' .github/workflows/amber-issue-handler.yml

Repository: ambient-code/platform

Length of output: 1437


🌐 Web query:

GitHub API pulls reviews endpoint historical all reviews paginated

💡 Result:

Use the REST endpoint List reviews for a pull request:

  • Endpoint: GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews (returns reviews in chronological order) [1]
  • Pagination params: per_page (default 30, max 100) and page (default 1) [1]
  • How to fetch all (historical) reviews: request pages until GitHub stops returning a Link header with rel="next" (GitHub’s standard REST pagination mechanism) [2]

Example:

# page 1
curl -i -H "Accept: application/vnd.github+json" \
  -H "Authorization: Bearer $TOKEN" \
  "https://api.github.com/repos/OWNER/REPO/pulls/123/reviews?per_page=100&page=1"

# page 2, 3, ... until there's no rel="next" in the Link header

If you also need the inline comments for each review, fetch them per review id:

  • GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/comments [1]

Sources: GitHub REST “Pull request reviews” docs [1], GitHub REST pagination guide (Link headers / per_page / page iteration) [2].


🏁 Script executed:

sed -n '575,595p' .github/workflows/amber-issue-handler.yml

Repository: ambient-code/platform

Length of output: 1081


🌐 Web query:

GitHub CLI gh api command automatic pagination default behavior

💡 Result:

gh api does not automatically paginate by default. Without any flags, it makes a single request and returns only the first page that the GitHub API returns for that endpoint. To fetch all pages, you must opt in with --paginate. [1]

Notes:

  • --paginate tells gh to “make additional HTTP requests to fetch all pages of results.” [1]
  • If you want a single combined JSON array/object across pages (instead of page-by-page output), use --slurp together with --paginate. [1]
  • Page size is still controlled by the API parameters (e.g., -F per_page=100 for many REST endpoints); --paginate just follows the pagination links/cursors to retrieve subsequent pages. [1]

Sources
[1] GitHub CLI manual: gh api (flags --paginate, --slurp) (cli.github.com)

Citations:


🌐 Web query:

GitHub API pull request review decision latest state each reviewer

💡 Result:

To get the latest review state per reviewer and the PR’s overall review decision, you have two practical options:

1) GraphQL API (best: server computes “latest per user”)

Query the PR’s overall decision via PullRequest.reviewDecision (values include APPROVED, CHANGES_REQUESTED, REVIEW_REQUIRED) and the latest review per user via latestOpinionatedReviews. [1][2][3]

Example:

query($owner:String!, $repo:String!, $number:Int!) {
  repository(owner:$owner, name:$repo) {
    pullRequest(number:$number) {
      reviewDecision
      latestOpinionatedReviews(first: 100) {
        nodes {
          author { login }
          state
          submittedAt
          url
        }
      }
    }
  }
}

state is a PullRequestReviewState (e.g. APPROVED, CHANGES_REQUESTED, COMMENTED, DISMISSED, PENDING). [3]

2) REST API (you compute “latest per user” yourself)

Call “List reviews for a pull request”, then group by user.login and pick the most recent relevant review (typically by submitted_at, optionally ignoring state == "PENDING"). The REST review objects include user and state (e.g. APPROVED, CHANGES_REQUESTED, DISMISSED). [4]

Endpoint:

  • GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews [4]

Sources

[1] GitHub Community discussion referencing PullRequest.reviewDecision field [7]
[2] GitHub GraphQL objects docs (GHES) snippet: latestOpinionatedReviews = “latest reviews per user”, and reviewDecision field [2]
[3] GitHub GraphQL enum docs: PullRequestReviewDecision + PullRequestReviewState values [3]
[4] GitHub REST docs: Pull request reviews endpoint and review object fields (includes user, state, submitted_at) [1]


Count only each reviewer's latest non-bot review state, not all historical entries. Add --paginate and group by reviewer.

The current code counts every CHANGES_REQUESTED entry in the reviews history. If reviewer A requests changes, then approves, the old entry still gets counted, marking the PR unhealthy. Additionally, without --paginate, the command only fetches the first ~30 reviews; if the PR has more reviewers, recent reviews on later pages are skipped.

Use one of:

  1. GraphQL (simpler): Query reviewDecision + latestOpinionatedReviews to get GitHub's computed latest state per reviewer.
  2. REST + grouping: Add --paginate, then post-process to group reviews by reviewer and take the most recent non-bot entry per user before checking for CHANGES_REQUESTED.
Current problematic snippet
reviews_raw = gh("api", f"repos/{REPO}/pulls/{pr_number}/reviews",
                "--jq", f'[.[] | select(.state == "CHANGES_REQUESTED" and {bot_filter})] | length')
changes_requested = int(reviews_raw) if reviews_raw else 0
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/amber-issue-handler.yml around lines 581 - 588, The
reviews counting currently treats every historical review as equal; update the
gh call and post-processing so only each reviewer's latest non-bot review is
considered: add "--paginate" to the gh("api",
f"repos/{REPO}/pulls/{pr_number}/reviews", ...) invocation, then change the jq
expression used with bot_filter to group by reviewer (user.login), sort by
submitted_at descending, pick the first non-bot entry per user, and count those
with state == "CHANGES_REQUESTED"; finally set changes_requested from that count
(instead of counting all CHANGES_REQUESTED entries). Alternatively, replace the
REST call with a GraphQL query that returns reviewDecision or
latestOpinionatedReviews and derive changes_requested from that result.


return False, "healthy"
Comment on lines +557 to +592
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if any ambient-code:managed PRs currently have no CI checks
gh pr list --repo "$GITHUB_REPOSITORY" --state open --label "ambient-code:managed" --json number --jq '.[].number' | while read -r pr; do
  checks=$(gh pr checks "$pr" --repo "$GITHUB_REPOSITORY" --json name 2>/dev/null | jq 'length')
  if [ "$checks" = "0" ] || [ -z "$checks" ]; then
    echo "PR #$pr has no CI checks"
  fi
done

Repository: ambient-code/platform

Length of output: 246


🏁 Script executed:

# Find and inspect the is_ready_to_review function to compare CI handling
fd -e yml -e yaml | head -20

Repository: ambient-code/platform

Length of output: 1233


🏁 Script executed:

# Look at the actual amber-issue-handler.yml file around the flagged lines
wc -l .github/workflows/amber-issue-handler.yml

Repository: ambient-code/platform

Length of output: 110


🏁 Script executed:

# Search for is_ready_to_review to see how it differs from needs_attention
rg "is_ready_to_review" .github/workflows/ -A 20

Repository: ambient-code/platform

Length of output: 3498


Add handling for PRs with no CI checks.

When states is empty (no CI runs), needs_attention() returns (False, "healthy"), causing the batch-fixer to skip. Meanwhile, is_ready_to_review() in pr-merge-review.yml explicitly returns (False, "CI unknown"), keeping the PR out of the Review Queue.

Result: PRs without CI are neither fixed nor queued for review. Consider either:

  • Adding an explicit check in needs_attention() to handle missing CI (e.g., if not states: return False, "CI unknown"), or
  • Documenting why this edge case is acceptable for the project's workflow
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/amber-issue-handler.yml around lines 555 - 588, The
needs_attention function currently treats an empty CI `states` list as "healthy"
and skips PRs with no CI; add an explicit check after parsing `states` (the
variable produced by json.loads) to handle the no-CI case by returning False,
"CI unknown" (matching is_ready_to_review behavior). Update the code in
needs_attention (function name: needs_attention) to: after the try/except that
sets `states`, add `if not states: return False, "CI unknown"`, ensuring PRs
with no CI are classified consistently.


# Get all open ambient-code:managed PRs
prs_json = gh("pr", "list", "--repo", REPO, "--state", "open",
"--label", "ambient-code:managed", "--limit", "200",
"--json", "number,body,title,updatedAt")
"--json", "number,body,title")
prs = json.loads(prs_json) if prs_json else []
print(f"Found {len(prs)} ambient-code:managed PRs")

Expand All @@ -576,12 +615,13 @@ jobs:
session_id = fm["session_id"]
source = fm["source"]

# Check for changes using updatedAt from gh pr list (no extra API call)
updated_at = pr.get("updatedAt", "")
if updated_at and updated_at <= fm["last_action"]:
print(f"PR #{number}: no changes since {fm['last_action']} (updatedAt={updated_at}), skipping")
# Only trigger if the PR actually needs work
needs_work, reason = needs_attention(number)
if not needs_work:
print(f"PR #{number}: {reason}, skipping")
skipped += 1
continue
print(f"PR #{number}: {reason}")

# Trigger fix — reuse session if exists, create new if not
print(f"PR #{number}: triggering fix (session_id={session_id or 'new'})")
Expand All @@ -602,12 +642,17 @@ jobs:
4. Ensure the PR body contains this frontmatter as the first line
(read your session ID from the AGENTIC_SESSION_NAME environment variable):
<!-- acp:session_id=$AGENTIC_SESSION_NAME source={source} last_action=<ISO8601_NOW> retry_count=<N> -->
The current retry_count is {current_retry}. Increment it by 1.
The current retry_count is {current_retry}. Only increment retry_count if
you actually had to fix something (CI failure, conflict, review comment).
If the PR is already healthy (CI green, no conflicts, no open reviews),
do NOT increment — just update last_action.
If retry_count reaches 3 or more, stop working, add `ambient-code:needs-human` label,
remove `ambient-code:managed` label, comment on the PR, and send a Slack notification.
5. Add the `ambient-code:managed` label.
6. Do not merge. Do not close. Do not force-push.
7. If fundamentally broken beyond repair, add a comment explaining and stop.
8. When you comment on the PR, include this footer at the end:
_🤖 [Session]($PLATFORM_HOST/projects/$AGENTIC_SESSION_NAMESPACE/sessions/$AGENTIC_SESSION_NAME)_

## Slack Notifications

Expand Down
206 changes: 170 additions & 36 deletions .github/workflows/pr-merge-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,51 +3,185 @@ name: Review Queue
on:
workflow_dispatch:
schedule:
- cron: '0 13 * * 1-5' # weekdays 1pm UTC
- cron: '0 * * * 1-5' # hourly on weekdays

permissions:
contents: read
issues: write
pull-requests: write

jobs:
create-session:
review-queue:
runs-on: ubuntu-latest
timeout-minutes: 15
concurrency:
group: review-queue
cancel-in-progress: false

steps:
- name: Create review queue session
id: session
uses: ambient-code/ambient-action@v0.0.2
with:
api-url: ${{ secrets.AMBIENT_API_URL }}
api-token: ${{ secrets.AMBIENT_BOT_TOKEN }}
project: ${{ secrets.AMBIENT_PROJECT }}
prompt: >-
Review all open PRs in https://github.com/${{ github.repository }}
and generate a prioritized review queue. Evaluate each PR via
sub-agents, classify by type (bug-fix, feature, refactor, etc.),
rank by urgency, test merge order, manage the "Review Queue"
milestone, and update the milestone description with the report.
repos: >-
[{"url": "https://github.com/${{ github.repository }}", "branch": "main"}]
workflow: >-
{"gitUrl": "https://github.com/ambient-code/workflows", "branch": "main", "path": "internal-workflows/pr-overview"}
model: claude-sonnet-4-5
wait: 'true'
timeout: '10'

- name: Session summary
if: always()
- name: Evaluate PRs and update milestone
id: evaluate
env:
SESSION_NAME: ${{ steps.session.outputs.session-name }}
SESSION_UID: ${{ steps.session.outputs.session-uid }}
SESSION_PHASE: ${{ steps.session.outputs.session-phase }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
run: |
pip install --quiet 'requests>=2.31.0'

python3 - << 'PYEOF'
import json
import os
import subprocess

REPO = os.environ.get("GITHUB_REPOSITORY", "")
SLACK_URL = os.environ.get("SLACK_WEBHOOK_URL", "")
MILESTONE_NAME = "Review Queue"

def gh(*args, input_text=None):
result = subprocess.run(["gh"] + list(args), capture_output=True, text=True, input=input_text)
return result.stdout.strip()

def get_or_create_milestone():
"""Get the Review Queue milestone number, creating it if needed."""
milestones = gh("api", f"repos/{REPO}/milestones", "--jq",
f'[.[] | select(.title == "{MILESTONE_NAME}")] | .[0].number')
if milestones and milestones != "null":
return int(milestones)
# Create it
result = gh("api", f"repos/{REPO}/milestones", "-X", "POST",
"-f", f"title={MILESTONE_NAME}",
"-f", "description=PRs ready for human review and merge",
"--jq", ".number")
return int(result) if result else None

def get_milestone_prs(milestone_number):
"""Get PR numbers currently in the milestone."""
prs = gh("pr", "list", "--repo", REPO, "--state", "open",
"--json", "number,milestone",
"--jq", f'[.[] | select(.milestone.number == {milestone_number})] | [.[].number]')
return set(json.loads(prs)) if prs else set()

def is_ready_to_review(pr):
"""Check if a PR is ready for human review."""
number = pr["number"]

# Skip drafts
if pr.get("isDraft", False):
return False, "draft"

# Check CI status
status_checks = gh("pr", "checks", str(number), "--repo", REPO,
"--json", "name,state",
"--jq", '[.[] | .state] | unique')
try:
states = json.loads(status_checks) if status_checks else []
except json.JSONDecodeError:
states = []

if not states:
return False, "CI unknown"

if "FAILURE" in states:
return False, "CI failing"
if "PENDING" in states:
return False, "CI pending"

# Check for unresolved critical/major review comments
bot_logins = ["github-actions[bot]", "ambient-code[bot]", "ambient-bot"]
bot_filter = " and ".join([f'.user.login != "{b}"' for b in bot_logins])
reviews = gh("api", f"repos/{REPO}/pulls/{number}/reviews",
"--jq", f'[.[] | select(.state == "CHANGES_REQUESTED" and {bot_filter})] | length')
try:
changes_requested = int(reviews) if reviews else 0
except ValueError:
changes_requested = 0

if changes_requested > 0:
return False, "changes requested"

return True, "ready"

def slack_notify(text):
"""Send a Slack notification."""
if not SLACK_URL:
return
import requests
try:
requests.post(SLACK_URL, json={"text": text}, timeout=10)
except Exception as e:
print(f" Slack notification failed: {e}")

# Get milestone
milestone_number = get_or_create_milestone()
if not milestone_number:
print("Failed to get/create milestone")
exit(1)

current_milestone_prs = get_milestone_prs(milestone_number)
print(f"Current milestone PRs: {current_milestone_prs}")

# Get all open non-draft PRs (not just ambient-code:managed)
prs_json = gh("pr", "list", "--repo", REPO, "--state", "open",
"--limit", "200", "--json", "number,title,isDraft,url,headRefName")
prs = json.loads(prs_json) if prs_json else []
print(f"Found {len(prs)} open PRs")

ready_prs = []
not_ready = []
newly_added = []

for pr in prs:
number = pr["number"]
ready, reason = is_ready_to_review(pr)

if ready:
ready_prs.append(pr)
if number not in current_milestone_prs:
# Add to milestone
gh("api", f"repos/{REPO}/issues/{number}",
"-X", "PATCH", "-f", f"milestone={milestone_number}")
newly_added.append(pr)
print(f" PR #{number}: added to milestone (ready)")
else:
print(f" PR #{number}: already in milestone")
else:
# Remove from milestone if it was there
if number in current_milestone_prs:
gh("api", f"repos/{REPO}/issues/{number}",
"-X", "PATCH", "--input", "-",
input_text=json.dumps({"milestone": None}))
print(f" PR #{number}: removed from milestone ({reason})")
else:
print(f" PR #{number}: not ready ({reason})")
not_ready.append({"number": number, "reason": reason})

# Send Slack notification only for newly added PRs
if newly_added:
lines = [f"📋 *{len(newly_added)} PR{'s' if len(newly_added) > 1 else ''} ready for review*\n"]
for pr in newly_added:
lines.append(f"• <{pr['url']}|#{pr['number']}> — {pr['title']}")
lines.append(f"\n_Total in <https://github.com/{REPO}/milestone/{milestone_number}|Review Queue>: {len(ready_prs)}_")
slack_notify("\n".join(lines))
print(f"\nSlack: notified about {len(newly_added)} new PR(s)")
else:
print("\nNo new PRs added to milestone — no Slack notification")

# Update milestone description
ready_list = "\n".join([f"- #{pr['number']} — {pr['title']}" for pr in ready_prs])
not_ready_list = "\n".join([f"- #{item['number']} ({item['reason']})" for item in not_ready[:10]])
description = f"## Ready for Review ({len(ready_prs)})\n\n{ready_list or 'None'}"
if not_ready_list:
description += f"\n\n## Not Ready ({len(not_ready)})\n\n{not_ready_list}"
if len(not_ready) > 10:
description += f"\n- ... and {len(not_ready) - 10} more"

gh("api", f"repos/{REPO}/milestones/{milestone_number}",
"-X", "PATCH", "-f", f"description={description}")

print(f"\nSummary: {len(ready_prs)} ready, {len(not_ready)} not ready, {len(newly_added)} newly added")
PYEOF

- name: Summary
if: always()
run: |
echo "### Review Queue" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ -n "$SESSION_NAME" ]; then
echo "- **Session**: \`$SESSION_NAME\`" >> $GITHUB_STEP_SUMMARY
echo "- **UID**: \`$SESSION_UID\`" >> $GITHUB_STEP_SUMMARY
echo "- **Phase**: \`$SESSION_PHASE\`" >> $GITHUB_STEP_SUMMARY
else
echo "- **Status**: Failed to create session" >> $GITHUB_STEP_SUMMARY
fi
echo "See step logs for details" >> $GITHUB_STEP_SUMMARY
Loading