@openhands/extensions 0.23.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/.agents/skills/custom-codereview-guide.md +14 -12
  2. package/.claude-plugin/marketplace.json +20 -0
  3. package/.codex-plugin/marketplace.json +20 -0
  4. package/.github/workflows/check-extensions.yml +6 -6
  5. package/.github/workflows/deprecation-check.yml +1 -1
  6. package/.github/workflows/issue-readiness-check.yml +37 -106
  7. package/.github/workflows/npm-publish.yml +2 -2
  8. package/.github/workflows/pr-description-check.yml +1 -1
  9. package/.github/workflows/pypi-publish.yml +3 -3
  10. package/.github/workflows/tests.yml +2 -2
  11. package/.plugin/marketplace.json +20 -0
  12. package/.release-please-manifest.json +1 -1
  13. package/AGENTS.md +10 -1
  14. package/README.md +4 -2
  15. package/automations/bundle-index.js +11 -7
  16. package/automations/catalog/github-pr-reviewer/manifest.json +19 -8
  17. package/automations/catalog/github-stale-ci-pr-closer/manifest.json +71 -0
  18. package/automations/catalog-index.js +2 -0
  19. package/integrations/catalog/granola.json +38 -0
  20. package/integrations/catalog-index.js +16 -14
  21. package/marketplaces/openhands-extensions.json +20 -0
  22. package/package.json +1 -1
  23. package/plugins/openhands/skills/openhands-sdk/SKILL.md +1 -0
  24. package/plugins/pr-review/skills/github-pr-review/SKILL.md +12 -2
  25. package/plugins/qa-changes/skills/github-pr-review/SKILL.md +12 -2
  26. package/plugins/vulnerability-remediation/skills/github/scripts/github_client.py +116 -0
  27. package/pyproject.toml +2 -2
  28. package/python/openhands_extensions/_version.py +1 -1
  29. package/skills/add-skill/SKILL.md +5 -2
  30. package/skills/canvas-extension-api/.claude-plugin/plugin.json +19 -0
  31. package/skills/canvas-extension-api/.codex-plugin/plugin.json +19 -0
  32. package/skills/canvas-extension-api/.plugin/plugin.json +19 -0
  33. package/skills/canvas-extension-api/README.md +33 -0
  34. package/skills/canvas-extension-api/SKILL.md +254 -0
  35. package/skills/canvas-extension-api/references/acceptance-checklist.md +20 -0
  36. package/skills/canvas-extension-api/references/backend-safety.md +9 -0
  37. package/skills/canvas-extension-api/references/connections.md +126 -0
  38. package/skills/canvas-extension-api/references/packaging-recipes.md +43 -0
  39. package/skills/canvas-extension-api/references/sidecar-pattern.md +49 -0
  40. package/skills/canvas-extension-api/references/testing-and-installation.md +286 -0
  41. package/skills/canvas-extension-api/references/v1-contract.md +250 -0
  42. package/skills/canvas-extension-api/scripts/validate-extension.mjs +92 -0
  43. package/skills/canvas-extension-api/scripts/validate-extension.test.mjs +60 -0
  44. package/skills/github/scripts/github_client.py +116 -0
  45. package/skills/github-agents-md-maintainer/scripts/github_client.py +116 -0
  46. package/skills/github-delivery-watchdog/scripts/github_client.py +116 -0
  47. package/skills/github-issue-to-pr/scripts/github_client.py +116 -0
  48. package/skills/github-issue-triage/scripts/github_client.py +116 -0
  49. package/skills/github-pr-review/SKILL.md +12 -2
  50. package/skills/github-pr-reviewer/README.md +41 -1
  51. package/skills/github-pr-reviewer/SKILL.md +180 -5
  52. package/skills/github-pr-reviewer/references/state-schema.md +15 -0
  53. package/skills/github-pr-reviewer/scripts/github_client.py +116 -0
  54. package/skills/github-pr-reviewer/scripts/main.py +57 -12
  55. package/skills/github-pr-reviewer/scripts/worker.py +832 -61
  56. package/skills/github-pr-reviewer/tests/test_main.py +69 -0
  57. package/skills/github-repo-monitor/scripts/github_client.py +116 -0
  58. package/skills/github-repo-monitor/scripts/main.py +32 -1
  59. package/skills/github-stale-ci-pr-closer/.claude-plugin/plugin.json +9 -0
  60. package/skills/github-stale-ci-pr-closer/.codex-plugin/plugin.json +9 -0
  61. package/skills/github-stale-ci-pr-closer/.plugin/plugin.json +9 -0
  62. package/skills/github-stale-ci-pr-closer/README.md +9 -0
  63. package/skills/github-stale-ci-pr-closer/SKILL.md +24 -0
  64. package/skills/github-stale-ci-pr-closer/commands/github-stale-ci-pr-closer.md +8 -0
  65. package/skills/github-stale-ci-pr-closer/scripts/worker.py +335 -0
  66. package/skills/github-stale-ci-pr-closer/tests/test_worker.py +213 -0
  67. package/skills/index.js +22 -5
  68. package/skills/openhands-sdk/SKILL.md +1 -0
  69. package/tests/fixtures/automations/github-pr-reviewer.json +39 -24
  70. package/tests/test_github_automation_foundation.py +158 -0
  71. package/tests/test_github_repo_monitor.py +107 -0
  72. package/tests/test_github_reviewer_delivery.py +2236 -4
  73. package/.github/scripts/check_issue_readiness.py +0 -272
  74. package/.github/scripts/post-readiness-comment.mjs +0 -184
  75. package/tests/test_check_issue_readiness.py +0 -135
@@ -12,9 +12,49 @@ This skill is activated by:
12
12
  ## Features
13
13
 
14
14
  - Reviews PRs on demand from a GitHub reviewer request or label event
15
+ - Gates each scheduled review on the current head's GitHub-required checks
16
+ before starting an agent, read through the `isRequired` signal, so an optional
17
+ workflow that fails cannot block a mergeable head: a completed `failure`,
18
+ `cancelled`, or `timed_out` required check blocks the review, and a `queued`,
19
+ `in_progress`, or not-yet-reported required check exits with a clear
20
+ waiting-on-checks outcome instead of holding a slot. If the required-check
21
+ signal is unavailable the gate falls back to every current-head check run and
22
+ workflow run, including a workflow that fails before creating any check run
23
+ - Dispatches an explicit `all-hands-bot` review request even when required CI is
24
+ red or pending; the CI gate applies to scheduled discovery
25
+ - Ignores checks and workflow runs recorded for an obsolete head, so a stale
26
+ failure cannot block the push that fixed it
27
+ - Resumes an outstanding reviewer request on the next scheduled scan once the
28
+ requested head's checks are green, so a request that arrives during CI is not
29
+ lost; the explicit-request event path is kept for event-only deployments
30
+ - Reviews open, non-draft PRs that nobody requested on a scheduled scan once
31
+ their current head is green and carries no review yet, keyed by
32
+ repository/PR/head so a repeat scan never duplicates a conversation or review
33
+ and a changed head becomes eligible again
34
+ - Examines a bounded, rotating window of that unrequested backlog per scan
35
+ (10 PRs per repository), remembering its position in the Automation KV store so
36
+ the next scan resumes past it; explicit requests and trigger labels are always
37
+ examined, never skipped behind the window
38
+ - Posts no managed gate comment for an unrequested PR that is merely red or
39
+ pending - the comment answers an explicit request, so a scan over a large
40
+ backlog cannot storm the PRs with comments
41
+ - Names the retry the deployment actually has in the waiting comment: a
42
+ scheduled scan where a cron trigger exists, and removing and re-requesting the
43
+ bot where only the event trigger does
44
+ - Rewrites its own managed gate comment in place when the retry wording changes,
45
+ so switching a deployment from the event trigger to a cron scan updates the
46
+ outstanding-request explanation instead of leaving the old instruction
15
47
  - Watches several repositories from a single automation, each with its own state
48
+ - Bounds a scheduled scan to a small, configurable number of new review
49
+ conversations across all repositories (default 2), draining the oldest
50
+ outstanding reviewer requests first and reaching the rest on later scans
16
51
  - Processes each review request or label application idempotently
17
- - Supports re-review by requesting the bot again or re-applying the label
52
+ - Supports re-review by requesting the bot again or re-applying the label. Each
53
+ explicit request refreshes mutable GitHub state (head, PR body, reviews,
54
+ comments, review requests, linked issues, and current-head checks) instead of
55
+ trusting what an earlier turn observed, so a same-head re-review sees a linked
56
+ issue that has since gained or lost readiness, or a check that has since moved.
57
+ Repository analysis already done, such as reading `AGENTS.md`, is retained
18
58
  - Suppresses stale reviews when the PR head commit changes mid-review
19
59
  - Hands the agent the reviewed commit already checked out, and removes that
20
60
  checkout when the review ends, so nothing accumulates between runs
@@ -25,9 +25,178 @@ once per label event, and publishes the AI review to GitHub.
25
25
  Windows PowerShell equivalents for the setup, packaging, upload, and API-check shell snippets are in `references/windows.md`.
26
26
 
27
27
  The automation script is deterministic: PR discovery, label-event tracking,
28
- state persistence, stale-result suppression, the repository checkout, and its
29
- removal are all handled in Python. The LLM is invoked only for the review
30
- itself.
28
+ head eligibility, state persistence, stale-result suppression, the repository
29
+ checkout, and its removal are all handled in Python. The LLM is invoked only for
30
+ the review itself.
31
+
32
+ Before any review conversation is created, a scheduled scan evaluates the
33
+ current head's GitHub-required checks, read through the GraphQL `isRequired`
34
+ signal for the pull request. That signal is the merge policy's own source of
35
+ truth and is pull-request-scoped, so it stays correct for a stacked PR whose
36
+ symbolic base branch carries no branch rules of its own:
37
+
38
+ - Only checks GitHub reports as required decide the scheduled gate. An optional
39
+ workflow that fails - including one that fails before creating any check run -
40
+ does not block a head whose required checks pass.
41
+ - A completed required run on the exact head whose conclusion is `failure`,
42
+ `cancelled`, or `timed_out` blocks the review. Any other unrecognized
43
+ conclusion fails closed as a block rather than silently approving.
44
+ - A completed required run whose conclusion is `success`, `neutral`, or
45
+ `skipped` does not block. A required `commit status` context is classified
46
+ alongside required check runs.
47
+ - A `queued` or `in_progress` required run on the exact head, and a required
48
+ context that has not reported a current-head check run at all, make the run
49
+ exit with a **waiting on checks** outcome. Unfulfilled is never approval. The
50
+ worker does not hold a slot polling; the next scheduled scan or explicit
51
+ review request retries.
52
+ - When the required-check signal cannot be read or is empty, the gate falls back
53
+ to every current-head check run and workflow run, so a red head still blocks.
54
+ In that fallback, workflow runs are read as well as check runs, because a
55
+ workflow can fail before creating any check run - a workflow-level error, or a
56
+ `pull_request` run whose jobs never start. Such a run leaves a failed check
57
+ suite with no check runs under it, so the commit's check-run rollup and
58
+ `gh pr checks` both report success and only the workflow run reveals the red
59
+ CI. A workflow run whose check suite already reported check runs is left to
60
+ those runs, so a workflow is never counted twice.
61
+ - A scheduled scan considers the trigger label **and** every open, non-draft PR
62
+ that still holds an outstanding `all-hands-bot` review request. That is what
63
+ resumes a request made while CI was running: the request is keyed by its own
64
+ `review_requested` event, so repeated scans reuse one conversation and one
65
+ review instead of creating duplicates, and a PR whose request was answered or
66
+ withdrawn simply drops out of `requested_reviewers`.
67
+ - The scheduled scan also reviews open, non-draft PRs that **nobody requested**
68
+ and that carry no trigger label, once their current head has no completed
69
+ review by this account. That is what keeps reviewing PRs after the outstanding
70
+ requests are exhausted. The delivery key is the stable
71
+ `scan:{repository}:{number}:{head}`, so repeated scans reuse one conversation
72
+ and one native review, and a changed head becomes eligible again under its new
73
+ SHA.
74
+ - The unrequested part of a scan is a **bounded, rotating window**, not the whole
75
+ backlog. Classifying one unrequested head costs a review read plus the
76
+ exact-head check and workflow reads, so examining every open PR in one scan
77
+ spent the run's API budget in the largest repository and announced every red or
78
+ pending head. A scan examines at most `SCAN_WINDOW` (10) unrequested PRs per
79
+ repository and stores where the window ended in the automation service's own
80
+ KV store, under a per-repository `review-scan:{owner}__{repo}` key, so the next
81
+ scan resumes past that point and the backlog is covered fairly over several
82
+ scans. The window wraps at the end of the backlog. When the KV store is not
83
+ available (a local run) the position is kept in memory, so the scan still
84
+ rotates within the run.
85
+ - An explicit `all-hands-bot` review request or a trigger label is **never**
86
+ subject to the window: every explicit candidate is examined on every scan,
87
+ whatever the stored position is, so a request is not delayed behind the
88
+ rotation.
89
+ - A gate stop on an **unrequested** head posts no managed comment. A PR nobody
90
+ asked about that is merely red or pending is simply skipped without starting an
91
+ agent, which is what keeps a scan over a large backlog from posting a gate
92
+ comment on every PR. The managed comment is the answer to an explicit request,
93
+ so a requested or labeled head that is red or pending still gets its
94
+ explanation.
95
+ - Workflow runs are read as well as check runs, because a workflow can fail
96
+ before creating any check run - a workflow-level error, or a `pull_request`
97
+ run whose jobs never start. Such a run leaves a failed check suite with no
98
+ check runs under it, so the commit's check-run rollup and `gh pr checks` both
99
+ report success and only the workflow run reveals the red CI. A workflow run
100
+ whose check suite already reported check runs is left to those runs, so a
101
+ workflow is never counted twice.
102
+ - Runs attributed to any other (obsolete) head SHA are ignored, so a stale
103
+ failure cannot block the push that fixed it. This applies to workflow runs
104
+ too.
105
+ - Only the latest run of each logical check or workflow counts. A logical check
106
+ is its name plus the reporting app identity, and a logical workflow is its
107
+ name plus workflow ID. The latest is chosen by the run ID (the reliable
108
+ creation sequence) with the start time as a tie-break, so a re-run that fixed
109
+ a check supersedes its earlier failure on the same SHA while a newer queued or
110
+ in-progress re-run supersedes an earlier success and makes the head wait.
111
+ Ordering by the run ID keeps a run whose start time is still absent in its
112
+ true creation position in both directions.
113
+
114
+ An explicit `all-hands-bot` review request is the intake-policy exception: the
115
+ caller asked for that head by name, so the request dispatches even when required
116
+ CI is red or pending, and the gate leaves no explanatory comment. Draft, scope,
117
+ exact-head, and delivery-deduplication safeguards still apply. A blocked or
118
+ waiting scheduled stop consumes no trigger: once the head's required checks are
119
+ non-blocking, the scheduled scan, or a new `all-hands-bot` review request,
120
+ starts the normal review.
121
+
122
+ The gate needs no configured list of check names. When it stops a review it
123
+ leaves one concise explanation on the PR, identified by a hidden marker carrying
124
+ the head SHA and gate category, so a later run for a different head updates that
125
+ comment instead of posting another. Only a marker this reviewer account authored
126
+ counts as its own comment; every other PR comment is untrusted and can neither
127
+ suppress the explanation nor be edited. If the repository's own workflow already
128
+ posted a deterministic remediation comment that names the same current-head
129
+ checks, the gate adds nothing; a disclosure about some other check does not
130
+ suppress it. The gate applies to scheduled label scans; an explicit
131
+ `all-hands-bot` review request bypasses it by design.
132
+
133
+ Each explicit review request - a re-applied trigger label, or a new reviewer
134
+ request in event mode - is a fresh review. The conversation for a PR is reused,
135
+ but its earlier turns must not be trusted as current: before deciding a verdict
136
+ the reviewer re-fetches the mutable GitHub state (the exact head, the PR body,
137
+ review comments and threads, review requests, the linked issues' bodies and
138
+ labels, and the current-head Actions results) and ignores any earlier finding,
139
+ verdict, or label/priority claim that state no longer supports. Repository
140
+ analysis the conversation already did, such as reading `AGENTS.md`, stays
141
+ useful and is not repeated.
142
+
143
+ The waiting and blocked explanations name the retry the deployment actually has.
144
+ A scheduled run proves a scan is configured, so it says the next scan will
145
+ retry. An event-only run does not, so it tells the reader to remove the
146
+ outstanding `all-hands-bot` request and request `all-hands-bot` again instead of
147
+ promising a scan that does not exist: GitHub will not accept a second request
148
+ while the first is still outstanding.
149
+
150
+ The gate leaves only one explanation per head. When the same head keeps the same
151
+ hidden marker but the retry wording changes -- an event-only comment on a head
152
+ whose automation is later switched to a cron scan -- the scheduled run rewrites
153
+ that managed comment in place, so the comment always names the retry that is
154
+ actually deployed, and an unchanged body is left untouched.
155
+
156
+ ## Bounded intake per scheduled scan
157
+
158
+ A scheduled scan drains outstanding reviewer requests fairly, but starting an
159
+ agent for every eligible pull request at once would overload the deployment, so
160
+ the scan starts at most `MAX_NEW_PER_RUN` new review conversations, counted
161
+ **across every configured repository** rather than per repository. The default
162
+ is `2`, and the rendered `config.json` overrides it through the
163
+ `max_new_per_run` key that `github-issue-to-pr` and `gitlab-issue-to-mr` already
164
+ use.
165
+
166
+ - Eligible candidates are ordered deterministically: an explicit `all-hands-bot`
167
+ request (or a trigger label) is ordered before an unrequested PR, then by the
168
+ oldest `all-hands-bot` `review_requested` event for a requested PR or the PR's
169
+ own creation time (oldest first) for an unrequested one, then by repository,
170
+ then by pull-request number, so the oldest work drains first and a later scan
171
+ reaches the remainder.
172
+ - The bound counts the conversations a scan **starts**. A delivery that only
173
+ deduplicates or reports an already-running conversation reuses a runtime and
174
+ consumes no slot, so repeated scans make progress on the backlog instead of
175
+ re-spending the bound on work already in flight.
176
+ - Reaching the bound never cuts the scan short. The scan still evaluates the
177
+ exact-head checks of the remaining candidates, reconciles completed reviews,
178
+ and runs the maintainer handoff. A candidate whose checks are pending or
179
+ failing starts no conversation and consumes no slot, so it cannot block a later
180
+ green candidate. Only an explicit request or a trigger label gets a waiting or
181
+ blocked gate comment; an unrequested head that is merely red or pending is
182
+ skipped silently.
183
+ - A dispatch that raises is reported and does not consume a slot or abort the
184
+ scan, so the candidates behind it are still considered.
185
+ - The explicit `review_requested` event path and the trigger-label scan are
186
+ unchanged: an explicit request still starts its conversation immediately, and
187
+ only the scheduled scan's new conversations are bounded.
188
+
189
+ The review prompt starts with a scope gate: using the repository's own guidance
190
+ (its scope categories and ownership boundaries, not a list of individual PR
191
+ numbers), the reviewer decides whether the change belongs in this repository
192
+ and has the product/architecture direction it needs. When it does not, the review
193
+ stops with a single `event: COMMENT` review that says whether the change should
194
+ move repositories, close, or receive a maintainer decision, and ends with the
195
+ `🛑 MAINTAINER DECISION REQUIRED` verdict. That outcome is **neither an approval
196
+ nor a change request**: it does not approve or merge the PR. The completion
197
+ handler recognizes the verdict and requests one configured maintainer through the
198
+ same handoff used after an approval. An in-scope change continues the existing
199
+ review unchanged.
31
200
 
32
201
  The script prepares each review's workspace before the agent starts: the pull
33
202
  request's head commit is downloaded as a tarball and extracted to a directory of
@@ -154,7 +323,7 @@ Record as `CRON_SCHEDULE`.
154
323
 
155
324
  ### Step 6 - Generate the automation script
156
325
 
157
- Read `scripts/main.py` from this skill's directory. Apply exactly six constant
326
+ Read `scripts/main.py` from this skill's directory. Apply exactly seven constant
158
327
  substitutions near the top of the file:
159
328
 
160
329
  > The script also reads a `config.json` shipped beside it, if there is one, over
@@ -170,6 +339,7 @@ substitutions near the top of the file:
170
339
  | `REVIEW_TONE = "thorough"` | `REVIEW_TONE = "{review_tone}"` |
171
340
  | `REVIEW_STYLE_INSTRUCTIONS = ""` | `REVIEW_STYLE_INSTRUCTIONS = "{style_instructions}"` |
172
341
  | `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 |
342
+ | `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 |
173
343
  | `DEFAULT_OPENHANDS_URL = "http://localhost:8000"` | leave unchanged unless the user has a preference |
174
344
 
175
345
  Use a safe string writer such as `json.dumps(value)` when inserting user-provided
@@ -278,7 +448,8 @@ For each repository:
278
448
  paths, and symlinks skipped rather than materialised.
279
449
  - Starts an OpenHands conversation **whose working directory is that
280
450
  checkout**, with a review prompt carrying PR metadata, the exact head SHA,
281
- and label event details.
451
+ label event details, and the requirement to re-fetch the current mutable
452
+ GitHub state before deciding a verdict.
282
453
  - Posts an acknowledgement comment with the label event, head SHA, and
283
454
  conversation link.
284
455
  - Records the review in state with `status: "active"` and the checkout path.
@@ -325,6 +496,10 @@ The completion callback fires once for the whole run.
325
496
  | 404 on repo access | Repo name wrong or no access | Re-check the entry in `REPOS` and the token's permissions |
326
497
  | One repository is skipped, others work | That repository failed its access check | Read the `=== owner/repo ===` block in the run log |
327
498
  | Same PR not reviewed after new commits | Label event was already processed | Remove and re-apply the trigger label |
499
+ | 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 |
500
+ | 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 |
501
+ | 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 |
502
+ | 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 and the bounded rotating window of unrequested PRs, or raise `max_new_per_run` if the deployment can hold more agents |
328
503
  | Review result never posts | Conversation still running or stuck | Open the conversation link from the acknowledgement comment |
329
504
  | Stale review suppressed | PR head SHA changed while the agent was reviewing | Re-apply the trigger label after the latest commit |
330
505
  | 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 |
@@ -18,6 +18,21 @@ store under the key `state:{owner}__{repo}` — for example
18
18
  `AUTOMATION_KV_TOKEN` is injected into the run environment. Each automation has
19
19
  its own isolated namespace.
20
20
 
21
+ The scheduled worker (`worker.py`) uses the same store for one more document, the
22
+ **unrequested-scan cursor**, under the key `review-scan:{owner}__{repo}`:
23
+
24
+ ```json
25
+ { "cursor": 20 }
26
+ ```
27
+
28
+ `cursor` is the position in the repository's unrequested-PR backlog at which the
29
+ next scheduled scan's bounded window starts. Each scan examines at most
30
+ `SCAN_WINDOW` (10) unrequested PRs from that position, then writes the position
31
+ after the window, so successive scans rotate through the whole backlog instead of
32
+ reading one pull request per open PR. A cursor past the end wraps to the start.
33
+ This document is separate from `main.py`'s per-repository review state; a missing
34
+ or unreadable cursor simply starts the window at the beginning.
35
+
21
36
  **Fallback (local/dev):** When the KV store is not available, the state is
22
37
  written to a local JSON file at:
23
38
 
@@ -165,6 +165,122 @@ class GitHubRepository:
165
165
  result.setdefault(item["context"], item["state"])
166
166
  return result
167
167
 
168
+ def check_runs(self, sha):
169
+ """Return every check run GitHub reported for one commit SHA.
170
+
171
+ Reading check runs needs no branch-protection or ruleset access, so this
172
+ is the head-eligibility signal the automation's own token can always
173
+ see. The endpoint answers with an object rather than a list, so it
174
+ paginates through ``gh`` instead of ``gh_pages``.
175
+ """
176
+ runs = []
177
+ for page in range(1, 101):
178
+ data = self.gh(
179
+ "GET", f"/commits/{sha}/check-runs?per_page=100&page={page}"
180
+ )
181
+ batch = data.get("check_runs") or []
182
+ runs.extend(batch)
183
+ if len(runs) >= int(data.get("total_count") or 0) or not batch:
184
+ return runs
185
+ raise RuntimeError("GitHub check-run pagination exceeded limit")
186
+
187
+ def required_check_contexts(self, number):
188
+ """Return the status contexts GitHub marks required on one pull request.
189
+
190
+ `isRequired` is the merge-policy source of truth and is PR-scoped, so it
191
+ is correct for a stacked PR whose symbolic base branch carries no branch
192
+ rules of its own. It is a field on each context in the head's status
193
+ check rollup and takes the pull request number, so a single GraphQL query
194
+ answers with the required check runs and any required commit statuses,
195
+ each already attributed to the exact head. Only contexts GitHub itself
196
+ reports as required are returned, so an optional workflow that fails
197
+ before creating any check run is not present.
198
+ """
199
+ owner, name = self.repository.split("/", 1)
200
+ query = (
201
+ "query($owner:String!,$name:String!,$number:Int!){"
202
+ "repository(owner:$owner,name:$name){"
203
+ "pullRequest(number:$number){"
204
+ "commits(last:1){"
205
+ "nodes{"
206
+ "commit{"
207
+ "statusCheckRollup{"
208
+ "contexts(first:100){"
209
+ "pageInfo{hasNextPage}"
210
+ "nodes{"
211
+ "__typename"
212
+ " ... on CheckRun{name isRequired(pullRequestNumber:$number)}"
213
+ " ... on StatusContext{context isRequired(pullRequestNumber:$number)}"
214
+ "}"
215
+ "}"
216
+ "}"
217
+ "}"
218
+ "}"
219
+ "}"
220
+ "}"
221
+ "}"
222
+ "}"
223
+ )
224
+ data = self.api(
225
+ "POST",
226
+ "/graphql",
227
+ body={
228
+ "query": query,
229
+ "variables": {"owner": owner, "name": name, "number": number},
230
+ },
231
+ )
232
+ if data.get("errors"):
233
+ raise RuntimeError(
234
+ f"GitHub required-check query failed: {data['errors'][0].get('message')}"
235
+ )
236
+ nodes = (
237
+ (((data.get("data") or {}).get("repository") or {}).get("pullRequest") or {})
238
+ .get("commits", {})
239
+ .get("nodes")
240
+ ) or []
241
+ contexts = (
242
+ ((nodes[0].get("commit") or {}).get("statusCheckRollup") or {}).get(
243
+ "contexts", {}
244
+ )
245
+ if nodes
246
+ else {}
247
+ )
248
+ if (contexts.get("pageInfo") or {}).get("hasNextPage"):
249
+ raise RuntimeError(
250
+ "GitHub required-check rollup exceeds one page; the required "
251
+ "set may be incomplete"
252
+ )
253
+ return [
254
+ {
255
+ "name": node.get("name") or node.get("context"),
256
+ "kind": node.get("__typename"),
257
+ }
258
+ for node in (contexts.get("nodes") or [])
259
+ if node.get("isRequired") and (node.get("name") or node.get("context"))
260
+ ]
261
+
262
+ def workflow_runs(self, sha):
263
+ """Return every Actions workflow run GitHub reported for one commit SHA.
264
+
265
+ A workflow that fails before any job starts - a workflow-level error, or
266
+ a `pull_request` run whose jobs never materialize - still records a
267
+ failed check suite, but it contributes no check runs, so the commit's
268
+ check-run rollup and `gh pr checks` both report success. Reading the
269
+ workflow runs directly is the only way the gate can see that failure.
270
+ Like the check-run endpoint this answers with an object, so it paginates
271
+ manually.
272
+ """
273
+ runs = []
274
+ for page in range(1, 101):
275
+ data = self.gh(
276
+ "GET", f"/actions/runs?head_sha={sha}&per_page=100&page={page}"
277
+ )
278
+ batch = data.get("workflow_runs") or []
279
+ runs.extend(batch)
280
+ if len(runs) >= int(data.get("total_count") or 0) or not batch:
281
+ return runs
282
+ raise RuntimeError("GitHub workflow-run pagination exceeded limit")
283
+
168
284
  def completed_dependency(self, number):
169
285
  if number in self._completed_dependencies:
170
286
  return self._completed_dependencies[number]
@@ -51,6 +51,18 @@ REVIEW_STYLE_INSTRUCTIONS = ""
51
51
  # than relying on the spawned agent's skill activation. Set to "" to disable.
52
52
  REPO_REVIEW_GUIDE_PATH = ".agents/skills/custom-codereview-guide.md"
53
53
  DEFAULT_OPENHANDS_URL = "http://localhost:8000"
54
+ # The most new review conversations one scheduled scan may start, counted across
55
+ # every configured repository rather than per repository. The scheduled scan
56
+ # drains outstanding reviewer requests in oldest-request order, so a small bound
57
+ # keeps a first scan over a large backlog from starting an agent for every
58
+ # eligible pull request at once.
59
+ MAX_NEW_PER_RUN = 2
60
+
61
+ # A review that ends with this marker is a scope stop: the reviewer found the
62
+ # change out of scope, or needing a product/architecture decision, before the
63
+ # technical review. The completion handler hands it to a maintainer without
64
+ # approving or merging the PR.
65
+ MAINTAINER_DECISION_VERDICT = "🛑 MAINTAINER DECISION REQUIRED"
54
66
 
55
67
  CONFIG_FILENAME = "config.json"
56
68
 
@@ -63,6 +75,7 @@ _CONFIG_TYPES: dict[str, type] = {
63
75
  "review_tone": str,
64
76
  "review_style_instructions": str,
65
77
  "repo_review_guide_path": str,
78
+ "max_new_per_run": int,
66
79
  "openhands_url": str,
67
80
  }
68
81
 
@@ -89,7 +102,11 @@ def load_config(directory: Path | None = None) -> dict:
89
102
  if key not in raw:
90
103
  continue
91
104
  value = raw[key]
92
- if not isinstance(value, expected):
105
+ # bool is an int in Python, so an unguarded int check would accept
106
+ # `"max_new_per_run": true` and then start `True` conversations.
107
+ if not isinstance(value, expected) or (
108
+ expected is int and isinstance(value, bool)
109
+ ):
93
110
  raise SystemExit(
94
111
  f"{CONFIG_FILENAME}: {key} must be {expected.__name__}, "
95
112
  f"got {type(value).__name__}"
@@ -100,6 +117,10 @@ def load_config(directory: Path | None = None) -> dict:
100
117
  raise SystemExit(
101
118
  f'{CONFIG_FILENAME}: repos must be a non-empty list of "owner/repo" strings'
102
119
  )
120
+ if key == "max_new_per_run" and value < 1:
121
+ raise SystemExit(
122
+ f"{CONFIG_FILENAME}: max_new_per_run must be at least 1"
123
+ )
103
124
  config[key] = value
104
125
  return config
105
126
 
@@ -144,6 +165,7 @@ TRIGGER_LABEL = _CONFIG.get("trigger_label", TRIGGER_LABEL)
144
165
  REVIEW_TONE = _CONFIG.get("review_tone", REVIEW_TONE)
145
166
  REVIEW_STYLE_INSTRUCTIONS = _CONFIG.get("review_style_instructions", REVIEW_STYLE_INSTRUCTIONS)
146
167
  REPO_REVIEW_GUIDE_PATH = _CONFIG.get("repo_review_guide_path", REPO_REVIEW_GUIDE_PATH)
168
+ MAX_NEW_PER_RUN = _CONFIG.get("max_new_per_run", MAX_NEW_PER_RUN)
147
169
  DEFAULT_OPENHANDS_URL = _CONFIG.get("openhands_url", DEFAULT_OPENHANDS_URL)
148
170
 
149
171
  DONE_DEBOUNCE = 15
@@ -787,22 +809,45 @@ def _build_review_prompt(
787
809
  f"\nPR Description:\n---\n{body}\n---\n\n"
788
810
  "Required workflow:\n"
789
811
  f"1. {workspace}\n"
790
- "2. Before reviewing, you MUST read the repository's own guidance to understand the repo first.\n"
812
+ "2. CURRENT STATE - treat this request as a fresh review, never a continuation of "
813
+ "earlier observations. Before the scope gate and before deciding any verdict, re-fetch "
814
+ "the current mutable GitHub state for this pull request and act only on what you read "
815
+ "now: whether the exact head still matches the Head SHA above, the current PR title and "
816
+ "body, the current review comments and threads, the current review requests, the current "
817
+ "GitHub Actions check results for that head, and the current body and labels of every "
818
+ "linked issue the PR references - a linked issue may have gained or lost a readiness "
819
+ "label (for example `ready-for-dev`) or changed priority since an earlier turn. If an "
820
+ "earlier turn in this conversation reviewed this PR or its linked issues, that analysis "
821
+ "and the repository guidance you read remain useful background, but every mutable fact "
822
+ "above must be re-established now; never repeat an earlier finding, verdict, or "
823
+ "label/priority claim that the state you just read does not support.\n"
824
+ f" Use `gh` or GitHub REST API calls with `{github_token_secret}`; never print secret values.\n"
825
+ "3. Before reviewing, you MUST read the repository's own guidance to understand the repo first.\n"
791
826
  " Read `AGENTS.md` at the repository root (and any nested `AGENTS.md` covering the "
792
827
  "changed files), plus other relevant docs when present - e.g. `CONTRIBUTING.md`, "
793
828
  "`CLAUDE.md`, `.cursorrules`, and any review or coding-guideline docs. Apply that "
794
829
  "guidance to your review.\n"
795
- " Then inspect the PR discussion, existing review comments, changed files, and the diff, "
796
- "together with the surrounding code in the workspace.\n"
797
- f" Use `gh` or GitHub REST API calls with `{github_token_secret}`; never print secret values.\n"
798
- "3. Ground every finding in the workspace code. Before using an inline location, verify that "
830
+ "4. SCOPE GATE - before inspecting changed files, reading the diff, or running any test, "
831
+ "use the repository guidance above (its scope categories and ownership boundaries) to decide "
832
+ "whether this change belongs in this repository and has the product/architecture direction "
833
+ "it needs. If it does, continue the technical review unchanged. If it does not, or needs a "
834
+ "product/architecture decision, stop here and publish exactly one review with "
835
+ f"`POST /repos/{repo}/pulls/{number}/reviews`, using `commit_id` equal to the Head SHA above "
836
+ "and `event: COMMENT`. Briefly say whether the change should move repositories, close, or "
837
+ "receive a maintainer decision; when it belongs elsewhere, name the likely owning repository "
838
+ "only if the evidence supports it. Do not run tests or report implementation findings. This "
839
+ "outcome is not an approval, and its body ends with the verdict on its own line: "
840
+ f"`{MAINTAINER_DECISION_VERDICT}`.\n"
841
+ "5. Otherwise continue the technical review. Inspect the PR discussion, existing review "
842
+ "comments, changed files, and the diff, together with the surrounding code in the workspace.\n"
843
+ "6. Ground every finding in the workspace code. Before using an inline location, verify that "
799
844
  "the path and line are part of this pull request's diff. Compare every changed branch with "
800
845
  "the base behavior, including side effects outside the reported bug; a revision, event, or "
801
846
  "delivery identifier proves only the inputs it actually includes, not that unrelated profile, "
802
847
  "credential, configuration, or external state stayed unchanged. Do not add speculative or "
803
848
  "out-of-scope notes: every blocking or non-blocking observation must identify demonstrated "
804
849
  "behavior on the current head and explain why it matters to the merge decision.\n"
805
- f"4. Publish one review with `POST /repos/{repo}/pulls/{number}/reviews`, using "
850
+ f"7. Publish one review with `POST /repos/{repo}/pulls/{number}/reviews`, using "
806
851
  "`commit_id` equal to the Head SHA above. Use `event: APPROVE` when there are "
807
852
  "no material findings; otherwise use `event: COMMENT`. Never use "
808
853
  "`REQUEST_CHANGES`.\n"
@@ -813,19 +858,19 @@ def _build_review_prompt(
813
858
  " Put the overall assessment in `body`, and each line-specific finding in the `comments` "
814
859
  "array with `path`, `line`, `side: RIGHT`, and `body`.\n"
815
860
  " Only create inline comments for actionable findings; do not open praise or nitpick threads.\n"
816
- "5. If a finding cannot be attached to a changed line, put it in the review body instead. "
861
+ "8. If a finding cannot be attached to a changed line, put it in the review body instead. "
817
862
  "If the API rejects the inline positions, retry with every finding in the body and no `comments` array. "
818
863
  "If GitHub forbids the configured bot from approving its own PR, retry the clean review with "
819
864
  "`event: COMMENT` and keep the approved verdict.\n"
820
- "6. Begin the review body with this disclosure: "
865
+ "9. Begin the review body with this disclosure: "
821
866
  "`_This review was posted by an AI agent (OpenHands)._`\n"
822
- "7. End the review body with a verdict on its own line: either `✅ APPROVED` "
867
+ "10. End the review body with a verdict on its own line: either `✅ APPROVED` "
823
868
  "or `🔄 CHANGES REQUESTED`.\n"
824
- "8. If there are no material issues, still publish a review saying so, with the "
869
+ "11. If there are no material issues, still publish a review saying so, with the "
825
870
  "disclosure and the verdict.\n"
826
871
  f"\nReview instructions:\n{tone}{extra}{guide_section}\n\n"
827
872
  "After GitHub accepts the review, output exactly `GITHUB_REVIEW_POSTED`. "
828
- "If publishing still fails after the fallback in step 5, output the complete review text "
873
+ "If publishing still fails after the fallback in step 8, output the complete review text "
829
874
  "so it can be posted as a comment instead."
830
875
  )
831
876