@openhands/extensions 0.24.0 → 0.26.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 (133) hide show
  1. package/.claude-plugin/marketplace.json +30 -0
  2. package/.codex-plugin/marketplace.json +30 -0
  3. package/.github/pull_request_template.md +17 -2
  4. package/.github/scripts/check_pr_description.py +64 -5
  5. package/.github/scripts/markdown_sections.py +95 -0
  6. package/.github/workflows/build-vscode-app.yml +30 -0
  7. package/.github/workflows/check-extensions.yml +3 -0
  8. package/.github/workflows/tests.yml +26 -0
  9. package/.plugin/marketplace.json +30 -0
  10. package/.release-please-manifest.json +1 -1
  11. package/AGENTS.md +3 -2
  12. package/README.md +10 -3
  13. package/apps/README.md +20 -0
  14. package/apps/catalog.json +10 -0
  15. package/apps/index.d.ts +11 -0
  16. package/apps/index.js +5 -0
  17. package/apps/vscode/README.md +13 -0
  18. package/apps/vscode/canvas-extension.json +19 -0
  19. package/apps/vscode/extension.js +114 -0
  20. package/apps/vscode/scripts/package.mjs +84 -0
  21. package/automations/bundle-index.js +18 -14
  22. package/automations/catalog/github-agents-md-maintainer/manifest.json +1 -1
  23. package/automations/catalog/github-pr-reviewer/manifest.json +19 -8
  24. package/automations/catalog/github-stale-ci-pr-closer/manifest.json +71 -0
  25. package/automations/catalog/gitlab-issue-to-mr/manifest.json +1 -1
  26. package/automations/catalog/news-digest/manifest.json +2 -2
  27. package/automations/catalog-index.js +2 -0
  28. package/index.d.ts +2 -0
  29. package/index.js +1 -0
  30. package/marketplaces/openhands-extensions.json +30 -0
  31. package/package.json +9 -1
  32. package/plugins/openhands/skills/openhands-api/README.md +1 -1
  33. package/plugins/openhands/skills/openhands-api/SKILL.md +3 -3
  34. package/plugins/openhands/skills/openhands-api/scripts/openhands_api.py +13 -0
  35. package/plugins/openhands/skills/openhands-automation/SKILL.md +66 -4
  36. package/plugins/openhands/skills/openhands-automation/references/custom-automation.md +21 -7
  37. package/plugins/openhands/skills/openhands-sdk/SKILL.md +1 -0
  38. package/plugins/pr-review/skills/github-pr-review/SKILL.md +12 -2
  39. package/plugins/qa-changes/skills/github-pr-review/SKILL.md +12 -2
  40. package/plugins/runtime-api-configs/.claude-plugin/plugin.json +20 -0
  41. package/plugins/runtime-api-configs/.codex-plugin/plugin.json +20 -0
  42. package/plugins/runtime-api-configs/.plugin/plugin.json +20 -0
  43. package/plugins/runtime-api-configs/README.md +59 -0
  44. package/plugins/runtime-api-configs/SKILL.md +240 -0
  45. package/plugins/runtime-api-configs/scripts/warm_runtime_configs.py +331 -0
  46. package/plugins/runtime-api-configs/tests/test_warm_runtime_configs.py +338 -0
  47. package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +223 -43
  48. package/plugins/vulnerability-remediation/skills/github/scripts/github_client.py +183 -15
  49. package/pyproject.toml +1 -1
  50. package/python/openhands_extensions/_version.py +1 -1
  51. package/scripts/check_plugin_resources.py +116 -0
  52. package/scripts/sync_extensions.py +32 -5
  53. package/skills/canvas-extension-api/SKILL.md +34 -3
  54. package/skills/canvas-extension-api/references/backend-safety.md +14 -0
  55. package/skills/canvas-extension-api/references/connections.md +24 -0
  56. package/skills/canvas-extension-api/references/sidecar-pattern.md +49 -10
  57. package/skills/canvas-extension-api/references/v1-contract.md +53 -3
  58. package/skills/canvas-extension-api/scripts/validate-extension.mjs +52 -0
  59. package/skills/canvas-extension-api/scripts/validate-extension.test.mjs +192 -1
  60. package/skills/github/scripts/agent_conversation.py +223 -43
  61. package/skills/github/scripts/github_client.py +183 -15
  62. package/skills/github-agents-md-maintainer/README.md +7 -0
  63. package/skills/github-agents-md-maintainer/SKILL.md +13 -6
  64. package/skills/github-agents-md-maintainer/scripts/github_client.py +183 -15
  65. package/skills/github-agents-md-maintainer/scripts/main.py +199 -29
  66. package/skills/github-delivery-watchdog/scripts/github_client.py +183 -15
  67. package/skills/github-issue-to-pr/scripts/agent_conversation.py +223 -43
  68. package/skills/github-issue-to-pr/scripts/github_client.py +183 -15
  69. package/skills/github-issue-to-pr/scripts/worker.py +10 -1
  70. package/skills/github-issue-triage/scripts/agent_conversation.py +223 -43
  71. package/skills/github-issue-triage/scripts/github_client.py +183 -15
  72. package/skills/github-pr-review/SKILL.md +12 -2
  73. package/skills/github-pr-reviewer/README.md +36 -0
  74. package/skills/github-pr-reviewer/SKILL.md +147 -4
  75. package/skills/github-pr-reviewer/scripts/agent_conversation.py +223 -43
  76. package/skills/github-pr-reviewer/scripts/github_client.py +183 -15
  77. package/skills/github-pr-reviewer/scripts/main.py +17 -1
  78. package/skills/github-pr-reviewer/scripts/worker.py +842 -57
  79. package/skills/github-repo-monitor/scripts/github_client.py +183 -15
  80. package/skills/github-repo-monitor/scripts/main.py +32 -1
  81. package/skills/github-stale-ci-pr-closer/.claude-plugin/plugin.json +9 -0
  82. package/skills/github-stale-ci-pr-closer/.codex-plugin/plugin.json +9 -0
  83. package/skills/github-stale-ci-pr-closer/.plugin/plugin.json +9 -0
  84. package/skills/github-stale-ci-pr-closer/README.md +9 -0
  85. package/skills/github-stale-ci-pr-closer/SKILL.md +24 -0
  86. package/skills/github-stale-ci-pr-closer/commands/github-stale-ci-pr-closer.md +8 -0
  87. package/skills/github-stale-ci-pr-closer/scripts/worker.py +508 -0
  88. package/skills/github-stale-ci-pr-closer/tests/test_worker.py +353 -0
  89. package/skills/gitlab-issue-to-mr/README.md +12 -0
  90. package/skills/gitlab-issue-to-mr/SKILL.md +13 -6
  91. package/skills/gitlab-issue-to-mr/scripts/main.py +217 -42
  92. package/skills/incident-retrospective/SKILL.md +5 -5
  93. package/skills/index.js +34 -16
  94. package/skills/jira-issue-to-pr/README.md +12 -4
  95. package/skills/jira-issue-to-pr/SKILL.md +129 -18
  96. package/skills/jira-issue-to-pr/references/setup.md +8 -0
  97. package/skills/jira-issue-to-pr/scripts/main.py +181 -54
  98. package/skills/jira-issue-to-pr/scripts/setup_webhook.py +290 -0
  99. package/skills/linear-triage/SKILL.md +5 -5
  100. package/skills/news-digest/README.md +6 -0
  101. package/skills/news-digest/SKILL.md +19 -9
  102. package/skills/news-digest/commands/news-digest-setup.md +1 -1
  103. package/skills/news-digest/scripts/main.py +133 -15
  104. package/skills/openhands-api/README.md +1 -1
  105. package/skills/openhands-api/SKILL.md +3 -3
  106. package/skills/openhands-api/scripts/openhands_api.py +13 -0
  107. package/skills/openhands-automation/SKILL.md +66 -4
  108. package/skills/openhands-automation/references/custom-automation.md +21 -7
  109. package/skills/openhands-sdk/SKILL.md +1 -0
  110. package/skills/research-brief/SKILL.md +5 -5
  111. package/skills/slack-standup-digest/SKILL.md +5 -5
  112. package/skills/ticket-to-code-change/SKILL.md +6 -3
  113. package/skills/use-jev/.claude-plugin/plugin.json +8 -0
  114. package/skills/use-jev/.codex-plugin/plugin.json +8 -0
  115. package/skills/use-jev/.plugin/plugin.json +8 -0
  116. package/skills/use-jev/README.md +66 -0
  117. package/skills/use-jev/SKILL.md +66 -0
  118. package/tests/fixtures/automations/github-pr-reviewer.json +39 -24
  119. package/tests/test_agent_conversation_dispatch.py +212 -0
  120. package/tests/test_apps_catalog.py +26 -0
  121. package/tests/test_automation_setup.py +44 -0
  122. package/tests/test_check_pr_description.py +89 -6
  123. package/tests/test_github_automation_foundation.py +158 -0
  124. package/tests/test_github_repo_monitor.py +107 -0
  125. package/tests/test_github_reviewer_delivery.py +2247 -6
  126. package/tests/test_github_stale_ci_pr_closer.py +56 -0
  127. package/tests/test_jira_issue_to_pr.py +312 -0
  128. package/tests/test_jira_setup_webhook.py +309 -0
  129. package/tests/test_openhands_api_python.py +37 -0
  130. package/tests/test_skill_plugin_loading.py +25 -15
  131. package/tests/test_sync_extensions.py +102 -0
  132. package/tests/test_vscode_app.py +33 -0
  133. package/tests/vscode-app.test.mjs +43 -0
@@ -12,24 +12,62 @@ from urllib.parse import parse_qsl, urlencode, urlsplit
12
12
  from urllib.request import Request, urlopen
13
13
 
14
14
 
15
+ # The secret name of the GitHub token an OpenHands Cloud or Enterprise user's
16
+ # connected GitHub integration provides: how this run's sandbox serves it, and
17
+ # how a conversation sees it in its environment.
18
+ CLOUD_GITHUB_TOKEN_SECRET = "github_token"
19
+ CLOUD_GITHUB_TOKEN_ENV = "GITHUB_TOKEN"
20
+
21
+
22
+ def is_cloud_run() -> bool:
23
+ """Whether this run is on OpenHands Cloud or Enterprise.
24
+
25
+ The automation service hands a run the Agent Server URL only on a local
26
+ Agent Canvas; elsewhere the run talks to the OpenHands API instead.
27
+ """
28
+ return bool(os.environ.get("OPENHANDS_CLOUD_API_URL")) and not os.environ.get(
29
+ "AGENT_SERVER_URL"
30
+ )
31
+
32
+
33
+ def _load_cloud_secret(name: str) -> str | None:
34
+ """Read one named secret of this run's sandbox from the OpenHands API."""
35
+ api = os.environ["OPENHANDS_CLOUD_API_URL"].rstrip("/")
36
+ request = Request(
37
+ f"{api}/api/v1/sandboxes/{os.environ['SANDBOX_ID']}/settings/secrets/{name}",
38
+ headers={"X-Session-API-Key": os.environ["SESSION_API_KEY"]},
39
+ )
40
+ try:
41
+ with urlopen(request, timeout=90) as response:
42
+ return response.read().decode().strip()
43
+ except HTTPError as exc:
44
+ if exc.code == 404:
45
+ return None
46
+ raise
47
+
48
+
15
49
  def _load_secret(name: str) -> str:
16
- """Read one named secret from the environment or configured Agent Server."""
50
+ """Read one named secret from the environment, or from the configured Agent
51
+ Server on a local run and the OpenHands API on a cloud one."""
17
52
  value = os.environ.get(name)
18
53
  if value:
19
54
  return value
20
55
 
21
- from openhands.sdk.workspace import RemoteWorkspace
56
+ if is_cloud_run():
57
+ value = _load_cloud_secret(name)
58
+ else:
59
+ from openhands.sdk.workspace import RemoteWorkspace
22
60
 
23
- workspace = RemoteWorkspace(
24
- host=os.environ["AGENT_SERVER_URL"],
25
- api_key=os.environ["SESSION_API_KEY"],
26
- working_dir=os.environ.get("WORKSPACE_BASE", "/workspace"),
27
- )
28
- try:
29
- secret = workspace.get_secrets([name]).get(name)
30
- value = secret.get_value() if secret else None
31
- finally:
32
- workspace.reset_client()
61
+ workspace = RemoteWorkspace(
62
+ host=os.environ["AGENT_SERVER_URL"],
63
+ api_key=os.environ["SESSION_API_KEY"],
64
+ working_dir=os.environ.get("WORKSPACE_BASE", "/workspace"),
65
+ )
66
+ try:
67
+ secret = workspace.get_secrets([name]).get(name)
68
+ value = secret.get_value() if secret else None
69
+ finally:
70
+ workspace.reset_client()
33
71
  if not value:
34
72
  raise ValueError(f"The GitHub credential {name} is unavailable")
35
73
  return value
@@ -95,11 +133,22 @@ class GitHubRepository:
95
133
  "Expected the environment variable containing the GitHub token"
96
134
  )
97
135
  self.token_name = github_token_secret
98
- self.token = _load_secret(github_token_secret)
136
+ try:
137
+ self.token = _load_secret(github_token_secret)
138
+ except ValueError:
139
+ if not is_cloud_run():
140
+ raise
141
+ # No secret is saved under that name, so use the user's connected
142
+ # GitHub integration, under the name a conversation sees it by.
143
+ self.token = _load_cloud_secret(CLOUD_GITHUB_TOKEN_SECRET)
144
+ if not self.token:
145
+ raise
146
+ self.token_name = CLOUD_GITHUB_TOKEN_ENV
99
147
  self.conversation = conversation
100
148
  self.conversation_id = str(conversation.id) if conversation else None
101
149
  self.dispatcher = dispatcher
102
- self.workspace = Path(os.environ["WORKSPACE_BASE"])
150
+ # A cloud run is not given a workspace base; its sandbox has /workspace.
151
+ self.workspace = Path(os.environ.get("WORKSPACE_BASE", "/workspace"))
103
152
  self.project = self.workspace
104
153
  self.evidence = self.workspace / "evidence"
105
154
  self.evidence.mkdir(exist_ok=True)
@@ -111,10 +160,13 @@ class GitHubRepository:
111
160
 
112
161
  @property
113
162
  def github_instructions(self):
163
+ # A cloud conversation runs in a sandbox of its own, so this run's
164
+ # workspace path means nothing to it.
165
+ where = "your working directory" if is_cloud_run() else self.project
114
166
  return (
115
167
  f"Use `GH_TOKEN=${self.token_name} gh api` for GitHub requests. "
116
168
  "Never print the credential value. "
117
- f"Only {self.repository} is in scope. Work in {self.project}. "
169
+ f"Only {self.repository} is in scope. Work in {where}. "
118
170
  "Do not modify the automation bundle or its configuration."
119
171
  )
120
172
 
@@ -165,6 +217,122 @@ class GitHubRepository:
165
217
  result.setdefault(item["context"], item["state"])
166
218
  return result
167
219
 
220
+ def check_runs(self, sha):
221
+ """Return every check run GitHub reported for one commit SHA.
222
+
223
+ Reading check runs needs no branch-protection or ruleset access, so this
224
+ is the head-eligibility signal the automation's own token can always
225
+ see. The endpoint answers with an object rather than a list, so it
226
+ paginates through ``gh`` instead of ``gh_pages``.
227
+ """
228
+ runs = []
229
+ for page in range(1, 101):
230
+ data = self.gh(
231
+ "GET", f"/commits/{sha}/check-runs?per_page=100&page={page}"
232
+ )
233
+ batch = data.get("check_runs") or []
234
+ runs.extend(batch)
235
+ if len(runs) >= int(data.get("total_count") or 0) or not batch:
236
+ return runs
237
+ raise RuntimeError("GitHub check-run pagination exceeded limit")
238
+
239
+ def required_check_contexts(self, number):
240
+ """Return the status contexts GitHub marks required on one pull request.
241
+
242
+ `isRequired` is the merge-policy source of truth and is PR-scoped, so it
243
+ is correct for a stacked PR whose symbolic base branch carries no branch
244
+ rules of its own. It is a field on each context in the head's status
245
+ check rollup and takes the pull request number, so a single GraphQL query
246
+ answers with the required check runs and any required commit statuses,
247
+ each already attributed to the exact head. Only contexts GitHub itself
248
+ reports as required are returned, so an optional workflow that fails
249
+ before creating any check run is not present.
250
+ """
251
+ owner, name = self.repository.split("/", 1)
252
+ query = (
253
+ "query($owner:String!,$name:String!,$number:Int!){"
254
+ "repository(owner:$owner,name:$name){"
255
+ "pullRequest(number:$number){"
256
+ "commits(last:1){"
257
+ "nodes{"
258
+ "commit{"
259
+ "statusCheckRollup{"
260
+ "contexts(first:100){"
261
+ "pageInfo{hasNextPage}"
262
+ "nodes{"
263
+ "__typename"
264
+ " ... on CheckRun{name isRequired(pullRequestNumber:$number)}"
265
+ " ... on StatusContext{context isRequired(pullRequestNumber:$number)}"
266
+ "}"
267
+ "}"
268
+ "}"
269
+ "}"
270
+ "}"
271
+ "}"
272
+ "}"
273
+ "}"
274
+ "}"
275
+ )
276
+ data = self.api(
277
+ "POST",
278
+ "/graphql",
279
+ body={
280
+ "query": query,
281
+ "variables": {"owner": owner, "name": name, "number": number},
282
+ },
283
+ )
284
+ if data.get("errors"):
285
+ raise RuntimeError(
286
+ f"GitHub required-check query failed: {data['errors'][0].get('message')}"
287
+ )
288
+ nodes = (
289
+ (((data.get("data") or {}).get("repository") or {}).get("pullRequest") or {})
290
+ .get("commits", {})
291
+ .get("nodes")
292
+ ) or []
293
+ contexts = (
294
+ ((nodes[0].get("commit") or {}).get("statusCheckRollup") or {}).get(
295
+ "contexts", {}
296
+ )
297
+ if nodes
298
+ else {}
299
+ )
300
+ if (contexts.get("pageInfo") or {}).get("hasNextPage"):
301
+ raise RuntimeError(
302
+ "GitHub required-check rollup exceeds one page; the required "
303
+ "set may be incomplete"
304
+ )
305
+ return [
306
+ {
307
+ "name": node.get("name") or node.get("context"),
308
+ "kind": node.get("__typename"),
309
+ }
310
+ for node in (contexts.get("nodes") or [])
311
+ if node.get("isRequired") and (node.get("name") or node.get("context"))
312
+ ]
313
+
314
+ def workflow_runs(self, sha):
315
+ """Return every Actions workflow run GitHub reported for one commit SHA.
316
+
317
+ A workflow that fails before any job starts - a workflow-level error, or
318
+ a `pull_request` run whose jobs never materialize - still records a
319
+ failed check suite, but it contributes no check runs, so the commit's
320
+ check-run rollup and `gh pr checks` both report success. Reading the
321
+ workflow runs directly is the only way the gate can see that failure.
322
+ Like the check-run endpoint this answers with an object, so it paginates
323
+ manually.
324
+ """
325
+ runs = []
326
+ for page in range(1, 101):
327
+ data = self.gh(
328
+ "GET", f"/actions/runs?head_sha={sha}&per_page=100&page={page}"
329
+ )
330
+ batch = data.get("workflow_runs") or []
331
+ runs.extend(batch)
332
+ if len(runs) >= int(data.get("total_count") or 0) or not batch:
333
+ return runs
334
+ raise RuntimeError("GitHub workflow-run pagination exceeded limit")
335
+
168
336
  def completed_dependency(self, number):
169
337
  if number in self._completed_dependencies:
170
338
  return self._completed_dependencies[number]
@@ -57,12 +57,22 @@ gh api -X POST repos/{owner}/{repo}/pulls/{pr_number}/reviews --input /tmp/revie
57
57
  | Parameter | Description |
58
58
  |-----------|-------------|
59
59
  | `commit_id` | Commit SHA to comment on (use `git rev-parse HEAD`) |
60
- | `event` | `COMMENT`, `APPROVE`, or `REQUEST_CHANGES` |
60
+ | `event` | `APPROVE`, `COMMENT`, or `REQUEST_CHANGES` (see below) |
61
61
  | `path` | File path as shown in the diff |
62
62
  | `line` | Line number in the NEW version (right side of diff) |
63
63
  | `side` | `RIGHT` for new/added lines, `LEFT` for deleted lines |
64
64
  | `body` | Comment text with priority label |
65
65
 
66
+ ### Choosing the `event` Value
67
+
68
+ | Event | When to use |
69
+ |-------|-------------|
70
+ | `APPROVE` | No issues found, or only 🟡 suggestions that are non-blocking |
71
+ | `COMMENT` | You have 🟠 Important feedback but it's not a hard blocker |
72
+ | `REQUEST_CHANGES` | There are 🔴 Critical issues that **must** be fixed before merge |
73
+
74
+ **Default to `APPROVE`** when the code is correct and merge-ready. Use `COMMENT` or `REQUEST_CHANGES` only when there are actionable issues that warrant it.
75
+
66
76
  ### Multi-Line Comments
67
77
 
68
78
  For comments spanning multiple lines, add `start_line` to specify the range:
@@ -184,4 +194,4 @@ curl -X POST \
184
194
  5. Do NOT post comments for code that is acceptable — only comment when action is needed
185
195
  6. Use suggestion syntax for concrete code changes, but only after verifying the resulting code matches your description (see "How Suggestions Actually Work")
186
196
  7. Keep the review body brief (details go in inline comments)
187
- 8. If no issues: post a short approval message with no inline comments
197
+ 8. If no issues: use `"event": "APPROVE"` with a short approval message and no inline comments
@@ -12,7 +12,43 @@ 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
+ - Explicit reviewer requests include draft PRs in both event mode and scheduled
28
+ scans; drafts without a request or trigger label remain excluded
29
+ - Resumes an outstanding reviewer request on the next scheduled scan once the
30
+ requested head's checks are green, so a request that arrives during CI is not
31
+ lost; the explicit-request event path is kept for event-only deployments
32
+ - Reviews open, non-draft PRs that nobody requested on a scheduled scan once
33
+ their current head is green and carries no review yet, keyed by
34
+ repository/PR/head so a repeat scan never duplicates a conversation or review
35
+ and a changed head becomes eligible again
36
+ - Examines the whole unrequested backlog on each scheduled scan, while the
37
+ global `max_new_per_run` quota limits only newly created review conversations;
38
+ blocked or pending heads consume no launch slot
39
+ - Posts no managed gate comment for an unrequested PR that is merely red or
40
+ pending - the comment answers an explicit request, so a scan over a large
41
+ backlog cannot storm the PRs with comments
42
+ - Names the retry the deployment actually has in the waiting comment: a
43
+ scheduled scan where a cron trigger exists, and removing and re-requesting the
44
+ bot where only the event trigger does
45
+ - Rewrites its own managed gate comment in place when the retry wording changes,
46
+ so switching a deployment from the event trigger to a cron scan updates the
47
+ outstanding-request explanation instead of leaving the old instruction
15
48
  - Watches several repositories from a single automation, each with its own state
49
+ - Bounds a scheduled scan to a small, configurable number of new review
50
+ conversations across all repositories (default 2), draining the oldest
51
+ outstanding reviewer requests first and reaching the rest on later scans
16
52
  - Processes each review request or label application idempotently
17
53
  - Supports re-review by requesting the bot again or re-applying the label. Each
18
54
  explicit request refreshes mutable GitHub state (head, PR body, reviews,
@@ -25,9 +25,101 @@ 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 PR, including drafts,
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
+ - Each scheduled scan classifies the **whole open backlog**, including
75
+ unrequested PRs. Blocked, pending, draft, or already-reviewed heads therefore
76
+ cannot hide an eligible head behind an inspection window. The separate
77
+ `max_new_per_run` quota limits only newly created review conversations, not the
78
+ number of PRs inspected; explicit requests and trigger labels retain priority
79
+ when eligible candidates are drained.
80
+ - A gate stop on an **unrequested** head posts no managed comment. A PR nobody
81
+ asked about that is merely red or pending is simply skipped without starting an
82
+ agent, which is what keeps a scan over a large backlog from posting a gate
83
+ comment on every PR. The managed comment is the answer to an explicit request,
84
+ so a requested or labeled head that is red or pending still gets its
85
+ explanation.
86
+ - Workflow runs are read as well as check runs, because a workflow can fail
87
+ before creating any check run - a workflow-level error, or a `pull_request`
88
+ run whose jobs never start. Such a run leaves a failed check suite with no
89
+ check runs under it, so the commit's check-run rollup and `gh pr checks` both
90
+ report success and only the workflow run reveals the red CI. A workflow run
91
+ whose check suite already reported check runs is left to those runs, so a
92
+ workflow is never counted twice.
93
+ - Runs attributed to any other (obsolete) head SHA are ignored, so a stale
94
+ failure cannot block the push that fixed it. This applies to workflow runs
95
+ too.
96
+ - Only the latest run of each logical check or workflow counts. A logical check
97
+ is its name plus the reporting app identity, and a logical workflow is its
98
+ name plus workflow ID. The latest is chosen by the run ID (the reliable
99
+ creation sequence) with the start time as a tie-break, so a re-run that fixed
100
+ a check supersedes its earlier failure on the same SHA while a newer queued or
101
+ in-progress re-run supersedes an earlier success and makes the head wait.
102
+ Ordering by the run ID keeps a run whose start time is still absent in its
103
+ true creation position in both directions.
104
+
105
+ An explicit `all-hands-bot` review request is the intake-policy exception: the
106
+ caller asked for that head by name, so the request dispatches even when required
107
+ CI is red or pending, and the gate leaves no explanatory comment. Draft, scope,
108
+ exact-head, and delivery-deduplication safeguards still apply. A blocked or
109
+ waiting scheduled stop consumes no trigger: once the head's required checks are
110
+ non-blocking, the scheduled scan, or a new `all-hands-bot` review request,
111
+ starts the normal review.
112
+
113
+ The gate needs no configured list of check names. When it stops a review it
114
+ leaves one concise explanation on the PR, identified by a hidden marker carrying
115
+ the head SHA and gate category, so a later run for a different head updates that
116
+ comment instead of posting another. Only a marker this reviewer account authored
117
+ counts as its own comment; every other PR comment is untrusted and can neither
118
+ suppress the explanation nor be edited. If the repository's own workflow already
119
+ posted a deterministic remediation comment that names the same current-head
120
+ checks, the gate adds nothing; a disclosure about some other check does not
121
+ suppress it. The gate applies to scheduled label scans; an explicit
122
+ `all-hands-bot` review request bypasses it by design.
31
123
 
32
124
  Each explicit review request - a re-applied trigger label, or a new reviewer
33
125
  request in event mode - is a fresh review. The conversation for a PR is reused,
@@ -39,6 +131,52 @@ verdict, or label/priority claim that state no longer supports. Repository
39
131
  analysis the conversation already did, such as reading `AGENTS.md`, stays
40
132
  useful and is not repeated.
41
133
 
134
+ The waiting and blocked explanations name the retry the deployment actually has.
135
+ A scheduled run proves a scan is configured, so it says the next scan will
136
+ retry. An event-only run does not, so it tells the reader to remove the
137
+ outstanding `all-hands-bot` request and request `all-hands-bot` again instead of
138
+ promising a scan that does not exist: GitHub will not accept a second request
139
+ while the first is still outstanding.
140
+
141
+ The gate leaves only one explanation per head. When the same head keeps the same
142
+ hidden marker but the retry wording changes -- an event-only comment on a head
143
+ whose automation is later switched to a cron scan -- the scheduled run rewrites
144
+ that managed comment in place, so the comment always names the retry that is
145
+ actually deployed, and an unchanged body is left untouched.
146
+
147
+ ## Bounded intake per scheduled scan
148
+
149
+ A scheduled scan drains outstanding reviewer requests fairly, but starting an
150
+ agent for every eligible pull request at once would overload the deployment, so
151
+ the scan starts at most `MAX_NEW_PER_RUN` new review conversations, counted
152
+ **across every configured repository** rather than per repository. The default
153
+ is `2`, and the rendered `config.json` overrides it through the
154
+ `max_new_per_run` key that `github-issue-to-pr` and `gitlab-issue-to-mr` already
155
+ use.
156
+
157
+ - Eligible candidates are ordered deterministically: an explicit `all-hands-bot`
158
+ request (or a trigger label) is ordered before an unrequested PR, then by the
159
+ oldest `all-hands-bot` `review_requested` event for a requested PR or the PR's
160
+ own creation time (oldest first) for an unrequested one, then by repository,
161
+ then by pull-request number, so the oldest work drains first and a later scan
162
+ reaches the remainder.
163
+ - The bound counts the conversations a scan **starts**. A delivery that only
164
+ deduplicates or reports an already-running conversation reuses a runtime and
165
+ consumes no slot, so repeated scans make progress on the backlog instead of
166
+ re-spending the bound on work already in flight.
167
+ - Reaching the bound never cuts the scan short. The scan still evaluates the
168
+ exact-head checks of the remaining candidates, reconciles completed reviews,
169
+ and runs the maintainer handoff. A candidate whose checks are pending or
170
+ failing starts no conversation and consumes no slot, so it cannot block a later
171
+ green candidate. Only an explicit request or a trigger label gets a waiting or
172
+ blocked gate comment; an unrequested head that is merely red or pending is
173
+ skipped silently.
174
+ - A dispatch that raises is reported and does not consume a slot or abort the
175
+ scan, so the candidates behind it are still considered.
176
+ - The explicit `review_requested` event path and the trigger-label scan are
177
+ unchanged: an explicit request still starts its conversation immediately, and
178
+ only the scheduled scan's new conversations are bounded.
179
+
42
180
  The review prompt starts with a scope gate: using the repository's own guidance
43
181
  (its scope categories and ownership boundaries, not a list of individual PR
44
182
  numbers), the reviewer decides whether the change belongs in this repository
@@ -176,7 +314,7 @@ Record as `CRON_SCHEDULE`.
176
314
 
177
315
  ### Step 6 - Generate the automation script
178
316
 
179
- Read `scripts/main.py` from this skill's directory. Apply exactly six constant
317
+ Read `scripts/main.py` from this skill's directory. Apply exactly seven constant
180
318
  substitutions near the top of the file:
181
319
 
182
320
  > The script also reads a `config.json` shipped beside it, if there is one, over
@@ -192,6 +330,7 @@ substitutions near the top of the file:
192
330
  | `REVIEW_TONE = "thorough"` | `REVIEW_TONE = "{review_tone}"` |
193
331
  | `REVIEW_STYLE_INSTRUCTIONS = ""` | `REVIEW_STYLE_INSTRUCTIONS = "{style_instructions}"` |
194
332
  | `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 |
333
+ | `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 |
195
334
  | `DEFAULT_OPENHANDS_URL = "http://localhost:8000"` | leave unchanged unless the user has a preference |
196
335
 
197
336
  Use a safe string writer such as `json.dumps(value)` when inserting user-provided
@@ -348,6 +487,10 @@ The completion callback fires once for the whole run.
348
487
  | 404 on repo access | Repo name wrong or no access | Re-check the entry in `REPOS` and the token's permissions |
349
488
  | One repository is skipped, others work | That repository failed its access check | Read the `=== owner/repo ===` block in the run log |
350
489
  | Same PR not reviewed after new commits | Label event was already processed | Remove and re-apply the trigger label |
490
+ | 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 |
491
+ | 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 |
492
+ | 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 |
493
+ | 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 eligible PRs, or raise `max_new_per_run` if the deployment can hold more agents |
351
494
  | Review result never posts | Conversation still running or stuck | Open the conversation link from the acknowledgement comment |
352
495
  | Stale review suppressed | PR head SHA changed while the agent was reviewing | Re-apply the trigger label after the latest commit |
353
496
  | 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 |