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
4 changes: 2 additions & 2 deletions automations/bundle-index.js

Large diffs are not rendered by default.

21 changes: 16 additions & 5 deletions automations/catalog/github-pr-reviewer/manifest.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"id": "github-pr-reviewer",
"version": "1.6.0",
"version": "1.7.0",
"name": "GitHub code review",
"category": "Code review",
"description": "Review requested pull-request heads, resuming an outstanding reviewer request on a schedule once its checks are green, record exact-head decisions, stop early on out-of-scope changes, and optionally request a code-aware human maintainer after approval or a maintainer-decision scope stop.",
"description": "Review requested pull-request heads, resuming an outstanding reviewer request on a schedule once its checks are green, draining the backlog oldest-request first under a small per-scan new-conversation bound, record exact-head decisions, stop early on out-of-scope changes, and optionally request a code-aware human maintainer after approval or a maintainer-decision scope stop.",
"requires": {
"integrations": {},
"features": [
Expand All @@ -13,7 +13,7 @@
},
"popularityRank": 100,
"estimatedSetupMinutes": 4,
"exampleImplementation": "A scheduled scan of the trigger label and of open non-draft PRs holding an outstanding reviewer request, or a GitHub reviewer-request event, selects a PR head and submits an idempotent subject turn. The profile-backed agent clones that exact head in its runtime, applies an early repository-ownership and product/architecture scope gate, then reuses the established review workflow, runs appropriate tests, and publishes a readable native review. A native approval, or a maintainer-decision scope stop, can then be handed to a code-aware human maintainer.",
"exampleImplementation": "A scheduled scan of the trigger label and of open non-draft PRs holding an outstanding reviewer request, or a GitHub reviewer-request event, selects a PR head and submits an idempotent subject turn. The scheduled scan orders eligible heads by the oldest outstanding request and starts at most a small configurable number of new conversations per scan across every repository. The profile-backed agent clones that exact head in its runtime, applies an early repository-ownership and product/architecture scope gate, then reuses the established review workflow, runs appropriate tests, and publishes a readable native review. A native approval, or a maintainer-decision scope stop, can then be handed to a code-aware human maintainer.",
"impact": {
"basis": "completed-runs",
"one": "1 PR review sweep completed",
Expand Down Expand Up @@ -87,6 +87,16 @@
"maxLength": 39
}
},
"maxNewPerRun": {
"type": "number",
"label": "New reviews per scan",
"help": "Most new review conversations one scheduled scan may start, counted across every repository. Keeps a first scan over a large backlog from starting an agent for every eligible pull request at once. Reviews that are already running are resumed without counting against it.",
"default": 2,
"required": true,
"constraints": {
"min": 1
}
},
"reviewTone": {
"type": "select",
"label": "Review tone",
Expand Down Expand Up @@ -125,7 +135,7 @@
}
},
"bundle": {
"version": "1.6.0",
"version": "1.7.0",
"entrypoint": "python3 worker.py",
"timeout": 300,
"files": {
Expand All @@ -139,12 +149,13 @@
"repos": "{{form.repositories}}",
"trigger_label": "{{form.triggerLabel}}",
"trigger_reviewer": "{{form.triggerReviewer}}",
"max_new_per_run": "{{form.maxNewPerRun}}",
"review_tone": "{{form.reviewTone}}",
"maintainers": "{{form.maintainers}}",
"github_token_secret": "{{form.githubTokenSecret}}"
}
},
"filter": "contains({{form.repositories}}, repository.full_name) && contains(keys(@), 'pull_request') && ((action == 'review_requested' && requested_reviewer.login == '{{form.triggerReviewer}}') || (action == 'submitted' && contains(keys(@), 'review') && review.user.login == '{{form.triggerReviewer}}'))",
"message": "Configure the repositories, trigger, review tone, optional maintainer roster, credential name, and agent profile."
"message": "Configure the repositories, trigger, review tone, optional maintainer roster, credential name, per-scan new-review bound, and agent profile."
}
}
3 changes: 3 additions & 0 deletions skills/github-pr-reviewer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ This skill is activated by:
so switching a deployment from the event trigger to a cron scan updates the
outstanding-request explanation instead of leaving the old instruction
- Watches several repositories from a single automation, each with its own state
- Bounds a scheduled scan to a small, configurable number of new review
conversations across all repositories (default 2), draining the oldest
outstanding reviewer requests first and reaching the rest on later scans
- Processes each review request or label application idempotently
- Supports re-review by requesting the bot again or re-applying the label. Each
explicit request refreshes mutable GitHub state (head, PR body, reviews,
Expand Down
32 changes: 31 additions & 1 deletion skills/github-pr-reviewer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,34 @@ whose automation is later switched to a cron scan -- the scheduled run rewrites
that managed comment in place, so the comment always names the retry that is
actually deployed, and an unchanged body is left untouched.

## Bounded intake per scheduled scan

A scheduled scan drains outstanding reviewer requests fairly, but starting an
agent for every eligible pull request at once would overload the deployment, so
the scan starts at most `MAX_NEW_PER_RUN` new review conversations, counted
**across every configured repository** rather than per repository. The default
is `2`, and the rendered `config.json` overrides it through the
`max_new_per_run` key that `github-issue-to-pr` and `gitlab-issue-to-mr` already
use.

- Eligible candidates are ordered by the oldest outstanding `all-hands-bot`
`review_requested` event, then by repository, then by pull-request number, so
the oldest requests drain first and a later scan reaches the remainder.
- The bound counts the conversations a scan **starts**. A delivery that only
deduplicates or reports an already-running conversation reuses a runtime and
consumes no slot, so repeated scans make progress on the backlog instead of
re-spending the bound on work already in flight.
- Reaching the bound never cuts the scan short. The scan still evaluates the
exact-head checks of the remaining candidates, posts its waiting or blocked
gate comments, reconciles completed reviews, and runs the maintainer handoff.
A candidate whose checks are pending or failing starts no conversation and
consumes no slot, so it cannot block a later green candidate.
- A dispatch that raises is reported and does not consume a slot or abort the
scan, so the candidates behind it are still considered.
- The explicit `review_requested` event path and the trigger-label scan are
unchanged: an explicit request still starts its conversation immediately, and
only the scheduled scan's new conversations are bounded.

The review prompt starts with a scope gate: using the repository's own guidance
(its scope categories and ownership boundaries, not a list of individual PR
numbers), the reviewer decides whether the change belongs in this repository
Expand Down Expand Up @@ -255,7 +283,7 @@ Record as `CRON_SCHEDULE`.

### Step 6 - Generate the automation script

Read `scripts/main.py` from this skill's directory. Apply exactly six constant
Read `scripts/main.py` from this skill's directory. Apply exactly seven constant
substitutions near the top of the file:

> The script also reads a `config.json` shipped beside it, if there is one, over
Expand All @@ -271,6 +299,7 @@ substitutions near the top of the file:
| `REVIEW_TONE = "thorough"` | `REVIEW_TONE = "{review_tone}"` |
| `REVIEW_STYLE_INSTRUCTIONS = ""` | `REVIEW_STYLE_INSTRUCTIONS = "{style_instructions}"` |
| `REPO_REVIEW_GUIDE_PATH = ".agents/skills/custom-codereview-guide.md"` | leave unchanged to auto-load a repo review guide at this path, or set to `""` to disable |
| `MAX_NEW_PER_RUN = 2` | leave unchanged to bound a scheduled scan to two new review conversations across all repositories, or raise it if the deployment can hold more agents at once |
| `DEFAULT_OPENHANDS_URL = "http://localhost:8000"` | leave unchanged unless the user has a preference |

Use a safe string writer such as `json.dumps(value)` when inserting user-provided
Expand Down Expand Up @@ -430,6 +459,7 @@ The completion callback fires once for the whole run.
| Review paused with a failing-check comment | A current-head required check reported `failure`, `cancelled`, or `timed_out` | Fix the named checks and push; the review starts on the new head, or request `all-hands-bot` to review immediately |
| Review reported waiting on checks | A current-head required check is `queued` or `in_progress`, or has not reported yet | No action; a later scan or a new review request retries |
| Optional workflow failed but no review was paused | The failed workflow is not required, so the required-only scheduled gate ignored it | No action; only GitHub-required checks gate scheduled discovery |
| Only a few reviews start on a large backlog | The per-scan `max_new_per_run` bound (default 2) reached | No action; later scans drain the remaining oldest requests, or raise `max_new_per_run` if the deployment can hold more agents |
| Review result never posts | Conversation still running or stuck | Open the conversation link from the acknowledgement comment |
| Stale review suppressed | PR head SHA changed while the agent was reviewing | Re-apply the trigger label after the latest commit |
| Review arrives as a plain comment, not a review | Publishing failed, so the script posted the text as a fallback | Check that the token has Pull requests: Read and Write |
Expand Down
18 changes: 17 additions & 1 deletion skills/github-pr-reviewer/scripts/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@
# than relying on the spawned agent's skill activation. Set to "" to disable.
REPO_REVIEW_GUIDE_PATH = ".agents/skills/custom-codereview-guide.md"
DEFAULT_OPENHANDS_URL = "http://localhost:8000"
# The most new review conversations one scheduled scan may start, counted across
# every configured repository rather than per repository. The scheduled scan
# drains outstanding reviewer requests in oldest-request order, so a small bound
# keeps a first scan over a large backlog from starting an agent for every
# eligible pull request at once.
MAX_NEW_PER_RUN = 2

# A review that ends with this marker is a scope stop: the reviewer found the
# change out of scope, or needing a product/architecture decision, before the
Expand All @@ -69,6 +75,7 @@
"review_tone": str,
"review_style_instructions": str,
"repo_review_guide_path": str,
"max_new_per_run": int,
"openhands_url": str,
}

Expand All @@ -95,7 +102,11 @@ def load_config(directory: Path | None = None) -> dict:
if key not in raw:
continue
value = raw[key]
if not isinstance(value, expected):
# bool is an int in Python, so an unguarded int check would accept
# `"max_new_per_run": true` and then start `True` conversations.
if not isinstance(value, expected) or (
expected is int and isinstance(value, bool)
):
raise SystemExit(
f"{CONFIG_FILENAME}: {key} must be {expected.__name__}, "
f"got {type(value).__name__}"
Expand All @@ -106,6 +117,10 @@ def load_config(directory: Path | None = None) -> dict:
raise SystemExit(
f'{CONFIG_FILENAME}: repos must be a non-empty list of "owner/repo" strings'
)
if key == "max_new_per_run" and value < 1:
raise SystemExit(
f"{CONFIG_FILENAME}: max_new_per_run must be at least 1"
)
config[key] = value
return config

Expand Down Expand Up @@ -150,6 +165,7 @@ def normalize_repo(value: str) -> str:
REVIEW_TONE = _CONFIG.get("review_tone", REVIEW_TONE)
REVIEW_STYLE_INSTRUCTIONS = _CONFIG.get("review_style_instructions", REVIEW_STYLE_INSTRUCTIONS)
REPO_REVIEW_GUIDE_PATH = _CONFIG.get("repo_review_guide_path", REPO_REVIEW_GUIDE_PATH)
MAX_NEW_PER_RUN = _CONFIG.get("max_new_per_run", MAX_NEW_PER_RUN)
DEFAULT_OPENHANDS_URL = _CONFIG.get("openhands_url", DEFAULT_OPENHANDS_URL)

DONE_DEBOUNCE = 15
Expand Down
Loading
Loading