@openhands/extensions 0.27.0 → 0.28.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.
- package/.agents/skills/custom-codereview-guide.md +31 -0
- package/.claude-plugin/marketplace.json +9 -0
- package/.codex-plugin/marketplace.json +9 -0
- package/.github/workflows/build-vscode-app.yml +3 -3
- package/.github/workflows/deprecation-check.yml +1 -1
- package/.github/workflows/pypi-publish.yml +2 -2
- package/.github/workflows/tests.yml +1 -1
- package/.plugin/marketplace.json +9 -0
- package/.release-please-manifest.json +1 -1
- package/README.md +3 -2
- package/automations/bundle-index.js +5 -5
- package/automations/catalog/github-issue-to-pr/manifest.json +2 -2
- package/automations/catalog/github-issue-triage/manifest.json +2 -2
- package/automations/catalog/github-pr-reviewer/manifest.json +2 -2
- package/marketplaces/openhands-extensions.json +9 -0
- package/package.json +1 -1
- package/plugins/openhands/skills/openhands-automation/README.md +6 -4
- package/plugins/openhands/skills/openhands-automation/SKILL.md +103 -21
- package/plugins/openhands/skills/openhands-automation/references/custom-automation.md +3 -4
- package/plugins/openhands/skills/openhands-sdk/SKILL.md +2 -1
- package/plugins/qa-changes/README.md +4 -0
- package/plugins/qa-changes/skills/qa-changes/README.md +3 -1
- package/plugins/qa-changes/skills/qa-changes/SKILL.md +42 -0
- package/plugins/vulnerability-remediation/skills/github/README.md +5 -1
- package/plugins/vulnerability-remediation/skills/github/SKILL.md +27 -0
- package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +51 -4
- package/pyproject.toml +1 -1
- package/python/openhands_extensions/_version.py +1 -1
- package/scripts/sync_openhands_sdk_skill.py +16 -13
- package/skills/azure-devops/README.md +3 -1
- package/skills/azure-devops/SKILL.md +4 -0
- package/skills/bitbucket/README.md +3 -1
- package/skills/bitbucket/SKILL.md +4 -2
- package/skills/bitbucket-cloud/README.md +2 -0
- package/skills/bitbucket-cloud/SKILL.md +2 -0
- package/skills/bitbucket-data-center/README.md +2 -0
- package/skills/bitbucket-data-center/SKILL.md +2 -0
- package/skills/github/README.md +5 -1
- package/skills/github/SKILL.md +27 -0
- package/skills/github/scripts/agent_conversation.py +51 -4
- package/skills/github-issue-to-pr/scripts/agent_conversation.py +51 -4
- package/skills/github-issue-triage/scripts/agent_conversation.py +51 -4
- package/skills/github-pr-reviewer/README.md +5 -0
- package/skills/github-pr-reviewer/SKILL.md +12 -0
- package/skills/github-pr-reviewer/scripts/agent_conversation.py +51 -4
- package/skills/github-pr-reviewer/scripts/main.py +20 -6
- package/skills/github-pr-reviewer/scripts/worker.py +77 -15
- package/skills/github-pr-reviewer/tests/test_main.py +23 -0
- package/skills/gitlab/README.md +3 -1
- package/skills/gitlab/SKILL.md +4 -0
- package/skills/index.js +23 -11
- package/skills/linear/README.md +2 -0
- package/skills/linear/SKILL.md +6 -2
- package/skills/openhands-automation/README.md +6 -4
- package/skills/openhands-automation/SKILL.md +103 -21
- package/skills/openhands-automation/references/custom-automation.md +3 -4
- package/skills/openhands-sdk/SKILL.md +2 -1
- package/skills/qa-changes/README.md +3 -1
- package/skills/qa-changes/SKILL.md +42 -0
- package/skills/simplified-technical-english/.claude-plugin/plugin.json +15 -0
- package/skills/simplified-technical-english/.codex-plugin/plugin.json +15 -0
- package/skills/simplified-technical-english/.plugin/plugin.json +15 -0
- package/skills/simplified-technical-english/README.md +21 -0
- package/skills/simplified-technical-english/SKILL.md +65 -0
- package/tests/fixtures/automations/github-pr-reviewer.json +9 -9
- package/tests/test_agent_conversation_dispatch.py +130 -0
- package/tests/test_github_reviewer_delivery.py +279 -2
|
@@ -25,6 +25,7 @@ from pathlib import Path
|
|
|
25
25
|
|
|
26
26
|
SKILL_PATH = Path(__file__).resolve().parent.parent / "skills" / "openhands-sdk" / "SKILL.md"
|
|
27
27
|
LLMS_TXT_URL = "https://docs.openhands.dev/llms.txt"
|
|
28
|
+
SDK_URL_PREFIX = "https://docs.openhands.dev/sdk/"
|
|
28
29
|
GH_API = "https://api.github.com/repos/OpenHands/software-agent-sdk/contents/examples"
|
|
29
30
|
GH_RAW = "https://raw.githubusercontent.com/OpenHands/software-agent-sdk/main"
|
|
30
31
|
GH_BROWSE = "https://github.com/OpenHands/software-agent-sdk/blob/main/examples"
|
|
@@ -90,18 +91,20 @@ def _fetch_or(url: str, fallback, **kw):
|
|
|
90
91
|
# -- llms.txt parsing --
|
|
91
92
|
|
|
92
93
|
def parse_sdk_entries(llms_txt: str) -> list[dict]:
|
|
93
|
-
"""Extract entries
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
94
|
+
"""Extract entries for the OpenHands Software Agent SDK.
|
|
95
|
+
|
|
96
|
+
The docs index (llms.txt) no longer groups entries under an
|
|
97
|
+
"OpenHands Software Agent SDK" heading, so select entries by their
|
|
98
|
+
canonical URL prefix instead. Sort by title (case-insensitively) to
|
|
99
|
+
keep the generated skill stable across runs.
|
|
100
|
+
"""
|
|
101
|
+
entries = [
|
|
102
|
+
m.groupdict()
|
|
103
|
+
for line in llms_txt.splitlines()
|
|
104
|
+
if (m := _ENTRY_RE.match(line.strip()))
|
|
105
|
+
and m.group("url").startswith(SDK_URL_PREFIX)
|
|
106
|
+
]
|
|
107
|
+
return sorted(entries, key=lambda e: e["title"].lower())
|
|
105
108
|
|
|
106
109
|
|
|
107
110
|
# -- content builders --
|
|
@@ -123,7 +126,7 @@ def build_classes_table(entries: list[dict]) -> str:
|
|
|
123
126
|
def build_api_refs(entries: list[dict]) -> str:
|
|
124
127
|
return ", ".join(
|
|
125
128
|
f"[`{e['title']}`]({e['url']})"
|
|
126
|
-
for e in entries if "/api-reference/" in e["url"]
|
|
129
|
+
for e in entries if "/sdk/api-reference/" in e["url"]
|
|
127
130
|
)
|
|
128
131
|
|
|
129
132
|
|
|
@@ -11,6 +11,8 @@ This skill is activated by the following keywords:
|
|
|
11
11
|
|
|
12
12
|
## Details
|
|
13
13
|
|
|
14
|
+
Use authenticated Azure DevOps MCP tools first when they are available in the agent's environment. Detect the connection through tool availability rather than a particular server name. Fall back to the documented `AZURE_DEVOPS_TOKEN` and direct API flow when those tools are unavailable or raw API or `curl` access is explicitly needed.
|
|
15
|
+
|
|
14
16
|
You have access to an environment variable, `AZURE_DEVOPS_TOKEN`, which allows you to interact with
|
|
15
17
|
the Azure DevOps API.
|
|
16
18
|
|
|
@@ -52,4 +54,4 @@ Common API endpoints:
|
|
|
52
54
|
- List repositories: `https://dev.azure.com/{organization}/{project}/_apis/git/repositories?api-version=7.1`
|
|
53
55
|
- Get repository details: `https://dev.azure.com/{organization}/{project}/_apis/git/repositories/{repositoryId}?api-version=7.1`
|
|
54
56
|
- List pull requests: `https://dev.azure.com/{organization}/{project}/_apis/git/pullrequests?api-version=7.1`
|
|
55
|
-
- Create pull request: `https://dev.azure.com/{organization}/{project}/_apis/git/repositories/{repositoryId}/pullrequests?api-version=7.1` (POST)
|
|
57
|
+
- Create pull request: `https://dev.azure.com/{organization}/{project}/_apis/git/repositories/{repositoryId}/pullrequests?api-version=7.1` (POST)
|
|
@@ -6,6 +6,10 @@ triggers:
|
|
|
6
6
|
- azure
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## Connection selection
|
|
10
|
+
|
|
11
|
+
Use authenticated Azure DevOps MCP tools first when they are available in the agent's environment. Detect this by tool availability, without depending on a particular MCP server name. Use the token and direct API instructions below only when Azure DevOps MCP tools are unavailable or when raw API or `curl` access is explicitly needed.
|
|
12
|
+
|
|
9
13
|
You have access to an environment variable, `AZURE_DEVOPS_TOKEN`, which allows you to interact with
|
|
10
14
|
the Azure DevOps API.
|
|
11
15
|
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
Bitbucket integration **hub** skill. It detects whether the repository is on Bitbucket
|
|
4
4
|
Cloud or Bitbucket Data Center and directs the agent to the matching detailed skill.
|
|
5
5
|
|
|
6
|
+
Authenticated Bitbucket MCP tools are preferred when available, detected generically by tool availability rather than a particular server name. The hub still routes to Cloud or Data Center, and the documented token and direct API paths remain the fallback when MCP tools are unavailable or raw API or `curl` access is explicitly needed.
|
|
7
|
+
|
|
6
8
|
## Triggers
|
|
7
9
|
|
|
8
10
|
This skill is activated by the following keywords:
|
|
@@ -30,4 +32,4 @@ when the words "data center" never appear. It then:
|
|
|
30
32
|
The hub also carries a small quick-reference table as a fallback in case the detailed skill
|
|
31
33
|
cannot be loaded.
|
|
32
34
|
|
|
33
|
-
See [`SKILL.md`](SKILL.md) for the full content.
|
|
35
|
+
See [`SKILL.md`](SKILL.md) for the full content.
|
|
@@ -5,6 +5,8 @@ triggers:
|
|
|
5
5
|
- bitbucket
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
Use authenticated Bitbucket MCP tools first when they are available in the agent's environment. Detect this by tool availability, without depending on a particular MCP server name. Still identify Cloud versus Data Center and load the matching detailed skill. Use the token and direct API instructions only when Bitbucket MCP tools are unavailable or when raw API or `curl` access is explicitly needed.
|
|
9
|
+
|
|
8
10
|
You are working with **Bitbucket**, which ships as two distinct products that behave
|
|
9
11
|
differently:
|
|
10
12
|
|
|
@@ -19,7 +21,7 @@ detailed skill for full instructions.
|
|
|
19
21
|
|
|
20
22
|
## Step 1 — Detect which Bitbucket you are on
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
First use the repository host or available Bitbucket MCP tool context to identify the product. For the token fallback, check which token environment variable is present. Environment variable names are
|
|
23
25
|
case-sensitive, so look for it case-insensitively:
|
|
24
26
|
|
|
25
27
|
```bash
|
|
@@ -53,4 +55,4 @@ Bitbucket API (not a web browser) and always use the listed PR tool to open a pu
|
|
|
53
55
|
| Host | `bitbucket.org` | self-hosted domain |
|
|
54
56
|
| REST API base | `https://api.bitbucket.org/2.0` | `https://<host>/rest/api/1.0` |
|
|
55
57
|
| Repository identifier | `workspace/repo_slug` | `PROJECT/repo_slug` (project key) |
|
|
56
|
-
| Pull request tool | `create_bitbucket_pr` | `create_bitbucket_data_center_pr` |
|
|
58
|
+
| Pull request tool | `create_bitbucket_pr` | `create_bitbucket_data_center_pr` |
|
|
@@ -11,5 +11,7 @@ it detects a Bitbucket Cloud environment (i.e. `BITBUCKET_TOKEN` is set).
|
|
|
11
11
|
|
|
12
12
|
## Details
|
|
13
13
|
|
|
14
|
+
Use authenticated Bitbucket Cloud MCP tools first when they are available, detected by tool availability rather than a particular server name. Fall back to the documented `BITBUCKET_TOKEN` and direct REST API flow when those tools are unavailable or raw API or `curl` access is explicitly needed.
|
|
15
|
+
|
|
14
16
|
See [`SKILL.md`](SKILL.md) for the full content, including authenticated git remote
|
|
15
17
|
construction and pull request instructions.
|
|
@@ -3,6 +3,8 @@ name: bitbucket-cloud
|
|
|
3
3
|
description: Bitbucket Cloud (bitbucket.org) specifics — authenticate with BITBUCKET_TOKEN, use the REST API v2, workspace/repo_slug repositories, and the create_bitbucket_pr tool. Loaded on demand by the bitbucket skill once a Cloud environment is detected.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
Use authenticated Bitbucket Cloud MCP tools first when they are available in the agent's environment. Detect this by tool availability, without depending on a particular MCP server name. Use the token and direct REST API instructions below only when those tools are unavailable or when raw API or `curl` access is explicitly needed.
|
|
7
|
+
|
|
6
8
|
You are working with **Bitbucket Cloud** (`bitbucket.org`). You have access to an
|
|
7
9
|
environment variable, `BITBUCKET_TOKEN`, which allows you to interact with the Bitbucket
|
|
8
10
|
Cloud API.
|
|
@@ -12,6 +12,8 @@ it detects a Bitbucket Data Center environment (i.e. `BITBUCKET_DATA_CENTER_TOKE
|
|
|
12
12
|
|
|
13
13
|
## Details
|
|
14
14
|
|
|
15
|
+
Use authenticated Bitbucket Data Center MCP tools first when they are available, detected by tool availability rather than a particular server name. Fall back to the documented `BITBUCKET_DATA_CENTER_TOKEN` and direct REST API flow when those tools are unavailable or raw API or `curl` access is explicitly needed.
|
|
16
|
+
|
|
15
17
|
See [`SKILL.md`](SKILL.md) for the full content, including authenticated `scm/` git remote
|
|
16
18
|
construction and pull request instructions. Note the key differences from Bitbucket Cloud:
|
|
17
19
|
the REST API is version `1.0` under `https://{domain}/rest/api/1.0`, repositories are
|
|
@@ -3,6 +3,8 @@ name: bitbucket-data-center
|
|
|
3
3
|
description: Bitbucket Data Center (self-hosted Bitbucket Server) specifics — authenticate with BITBUCKET_DATA_CENTER_TOKEN, use the REST API 1.0, PROJECT/repo_slug repositories, scm/ git remotes, and the create_bitbucket_data_center_pr tool. Loaded on demand by the bitbucket skill once a Data Center environment is detected.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
Use authenticated Bitbucket Data Center MCP tools first when they are available in the agent's environment. Detect this by tool availability, without depending on a particular MCP server name. Use the token and direct REST API instructions below only when those tools are unavailable or when raw API or `curl` access is explicitly needed.
|
|
7
|
+
|
|
6
8
|
You are working with **Bitbucket Data Center** (self-hosted Bitbucket Server). You have
|
|
7
9
|
access to an environment variable, `BITBUCKET_DATA_CENTER_TOKEN`, which contains a basic
|
|
8
10
|
auth token in the format `username:your-token` that allows you to interact with the git
|
package/skills/github/README.md
CHANGED
|
@@ -11,6 +11,8 @@ This skill is activated by the following keywords:
|
|
|
11
11
|
|
|
12
12
|
## Details
|
|
13
13
|
|
|
14
|
+
Use authenticated GitHub MCP tools first when they are available in the agent's environment. Detect the connection through tool availability rather than a particular server name. Fall back to the documented `GITHUB_TOKEN` and direct API flow when those tools are unavailable or raw API or `curl` access is explicitly needed.
|
|
15
|
+
|
|
14
16
|
You have access to an environment variable, `GITHUB_TOKEN`, which allows you to interact with
|
|
15
17
|
the GitHub API.
|
|
16
18
|
|
|
@@ -39,4 +41,6 @@ Here are some instructions for pushing, but ONLY do this if the user asks you to
|
|
|
39
41
|
```bash
|
|
40
42
|
git remote -v && git branch # to find the current org, repo and branch
|
|
41
43
|
git checkout -b create-widget && git add . && git commit -m "Create widget" && git push -u origin create-widget
|
|
42
|
-
```
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
When asked to merge a pull request, the skill uses GitHub's async merge API (`PUT /repos/{owner}/{repo}/pulls/{pull_number}/merge-async`, then poll the returned UUID). It supports stacked PRs and merge queues. Docs: https://docs.github.com/en/rest/pulls/pulls#merge-a-pull-request-asynchronously
|
package/skills/github/SKILL.md
CHANGED
|
@@ -5,6 +5,10 @@ triggers:
|
|
|
5
5
|
- github
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
## Connection selection
|
|
9
|
+
|
|
10
|
+
Use authenticated GitHub MCP tools first when they are available in the agent's environment. Detect this by tool availability, without depending on a particular MCP server name. Use the token and direct API instructions below only when GitHub MCP tools are unavailable or when raw API or `curl` access is explicitly needed.
|
|
11
|
+
|
|
8
12
|
You have access to an environment variable, `GITHUB_TOKEN`, which allows you to interact with
|
|
9
13
|
the GitHub API.
|
|
10
14
|
|
|
@@ -37,6 +41,29 @@ git remote -v && git branch # to find the current org, repo and branch
|
|
|
37
41
|
git checkout -b create-widget && git add . && git commit -m "Create widget" && git push -u origin create-widget
|
|
38
42
|
```
|
|
39
43
|
|
|
44
|
+
## Merging Pull Requests (async merge API)
|
|
45
|
+
|
|
46
|
+
IMPORTANT:
|
|
47
|
+
Only merge if the user asks you to, without ambiguity.
|
|
48
|
+
|
|
49
|
+
GitHub released an async merge API in October 2026; it is now the
|
|
50
|
+
recommended way to merge PRs programmatically, and the only merge API that supports stacked PRs. It can also add a PR to a merge queue.
|
|
51
|
+
Prefer it over `PUT .../pulls/{n}/merge` and the GraphQL `mergePullRequest` mutation.
|
|
52
|
+
A GitHub MCP merge tool may still use the older synchronous endpoint; for stacked PRs or merge queues, use the calls below.
|
|
53
|
+
Docs: https://docs.github.com/en/rest/pulls/pulls#merge-a-pull-request-asynchronously
|
|
54
|
+
|
|
55
|
+
1. Request the merge (`merge_action`: `default` uses the merge queue if the branch has one, else merges directly; or `direct_merge` / `merge_queue`). `merge_method`, `commit_title` and `commit_message` apply only to direct merges:
|
|
56
|
+
```bash
|
|
57
|
+
gh api -X PUT repos/<OWNER>/<REPO>/pulls/<PR_NUMBER>/merge-async -f merge_method=squash -f sha=<HEAD_SHA>
|
|
58
|
+
```
|
|
59
|
+
2. A `202` returns `details.uuid`; poll it every few seconds until `status` is no longer `pending`:
|
|
60
|
+
```bash
|
|
61
|
+
gh api repos/<OWNER>/<REPO>/pulls/<PR_NUMBER>/merge-async/<UUID>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Final `status` is `merged`, `enqueued` (added to the merge queue, NOT merged yet), or `failed` (see `details.message`).
|
|
65
|
+
A `200` on the PUT means the PR is already merged or queued; a `409` means a merge request is already pending (its UUID is returned).
|
|
66
|
+
|
|
40
67
|
## Handling Review Comments
|
|
41
68
|
|
|
42
69
|
- Critically evaluate each review comment before acting on it. Not all feedback is worth implementing:
|
|
@@ -32,6 +32,11 @@ _CLOUD_START_GRACE_SECONDS = 10 * 60
|
|
|
32
32
|
# How long to wait for a paused conversation's sandbox to come back up.
|
|
33
33
|
_CLOUD_RESUME_TIMEOUT_SECONDS = 120
|
|
34
34
|
_CLOUD_POLL_SECONDS = 3
|
|
35
|
+
# How many times a matched delivery whose conversation ended in ERROR is sent
|
|
36
|
+
# again. The delivery key never changes for a stable subject, so without a
|
|
37
|
+
# retry its work is stranded; without a bound, a conversation that fails the
|
|
38
|
+
# same way every time would be re-run on every scan.
|
|
39
|
+
_MAX_ERROR_RETRIES = 2
|
|
35
40
|
|
|
36
41
|
|
|
37
42
|
def _register_tools() -> None:
|
|
@@ -204,14 +209,23 @@ class AgentConversationDispatcher:
|
|
|
204
209
|
|
|
205
210
|
same_delivery = record.get("delivery") == delivery
|
|
206
211
|
same_head = bool(head) and record.get("head") == head
|
|
212
|
+
# Error retries count against one delivery; a new delivery starts over.
|
|
213
|
+
error_retries = int(record.get("error_retries") or 0) if same_delivery else 0
|
|
214
|
+
can_retry = error_retries < _MAX_ERROR_RETRIES
|
|
207
215
|
|
|
208
216
|
if self._cloud:
|
|
209
217
|
disposition, conversation_id = self._deliver_cloud(
|
|
210
|
-
record,
|
|
218
|
+
record,
|
|
219
|
+
conversation_id,
|
|
220
|
+
subject,
|
|
221
|
+
prompt,
|
|
222
|
+
same_delivery,
|
|
223
|
+
same_head,
|
|
224
|
+
can_retry,
|
|
211
225
|
)
|
|
212
226
|
else:
|
|
213
227
|
disposition = self._deliver_local(
|
|
214
|
-
conversation_id, prompt, same_delivery, same_head
|
|
228
|
+
conversation_id, prompt, same_delivery, same_head, can_retry
|
|
215
229
|
)
|
|
216
230
|
|
|
217
231
|
if disposition in ("deduplicated", "in_progress"):
|
|
@@ -228,6 +242,10 @@ class AgentConversationDispatcher:
|
|
|
228
242
|
}
|
|
229
243
|
if disposition == "created" and self._cloud:
|
|
230
244
|
new_record["started_at"] = time.time()
|
|
245
|
+
if disposition == "retried":
|
|
246
|
+
error_retries += 1
|
|
247
|
+
if error_retries:
|
|
248
|
+
new_record["error_retries"] = error_retries
|
|
231
249
|
_kv_request(state_key, "PUT", new_record)
|
|
232
250
|
return {
|
|
233
251
|
"disposition": disposition,
|
|
@@ -235,7 +253,14 @@ class AgentConversationDispatcher:
|
|
|
235
253
|
}
|
|
236
254
|
|
|
237
255
|
def _deliver_cloud(
|
|
238
|
-
self,
|
|
256
|
+
self,
|
|
257
|
+
record,
|
|
258
|
+
conversation_id,
|
|
259
|
+
subject,
|
|
260
|
+
prompt,
|
|
261
|
+
same_delivery,
|
|
262
|
+
same_head,
|
|
263
|
+
can_retry,
|
|
239
264
|
):
|
|
240
265
|
"""Deliver through the OpenHands API; return the disposition and the id.
|
|
241
266
|
|
|
@@ -272,11 +297,21 @@ class AgentConversationDispatcher:
|
|
|
272
297
|
if status in ("idle", "paused"):
|
|
273
298
|
self._cloud.run(conversation)
|
|
274
299
|
return "resumed", current_id
|
|
300
|
+
if status == "error" and can_retry:
|
|
301
|
+
# The delivery matched but its conversation died before the work
|
|
302
|
+
# finished, so send the revision again rather than strand it.
|
|
303
|
+
# The API reports an execution status only while the sandbox
|
|
304
|
+
# runs, so an errored conversation whose sandbox was already
|
|
305
|
+
# paused is not seen here and keeps reporting deduplicated.
|
|
306
|
+
self._cloud.send(conversation, prompt)
|
|
307
|
+
return "retried", current_id
|
|
275
308
|
return "deduplicated", current_id
|
|
276
309
|
self._cloud.send(conversation, prompt)
|
|
277
310
|
return "resumed", current_id
|
|
278
311
|
|
|
279
|
-
def _deliver_local(
|
|
312
|
+
def _deliver_local(
|
|
313
|
+
self, conversation_id, prompt, same_delivery, same_head, can_retry
|
|
314
|
+
) -> str:
|
|
280
315
|
if self._workspace is None:
|
|
281
316
|
raise RuntimeError("AgentConversationDispatcher must be used as a context")
|
|
282
317
|
|
|
@@ -321,6 +356,18 @@ class AgentConversationDispatcher:
|
|
|
321
356
|
):
|
|
322
357
|
conversation.update_secrets(self._secrets)
|
|
323
358
|
conversation.run(blocking=False)
|
|
359
|
+
elif (
|
|
360
|
+
conversation.state.execution_status
|
|
361
|
+
== ConversationExecutionStatus.ERROR
|
|
362
|
+
and can_retry
|
|
363
|
+
):
|
|
364
|
+
# The delivery matched but its conversation died before
|
|
365
|
+
# the work finished, so send the revision again rather
|
|
366
|
+
# than strand it.
|
|
367
|
+
conversation.update_secrets(self._secrets)
|
|
368
|
+
conversation.send_message(prompt)
|
|
369
|
+
conversation.run(blocking=False)
|
|
370
|
+
disposition = "retried"
|
|
324
371
|
else:
|
|
325
372
|
disposition = "deduplicated"
|
|
326
373
|
else:
|
|
@@ -32,6 +32,11 @@ _CLOUD_START_GRACE_SECONDS = 10 * 60
|
|
|
32
32
|
# How long to wait for a paused conversation's sandbox to come back up.
|
|
33
33
|
_CLOUD_RESUME_TIMEOUT_SECONDS = 120
|
|
34
34
|
_CLOUD_POLL_SECONDS = 3
|
|
35
|
+
# How many times a matched delivery whose conversation ended in ERROR is sent
|
|
36
|
+
# again. The delivery key never changes for a stable subject, so without a
|
|
37
|
+
# retry its work is stranded; without a bound, a conversation that fails the
|
|
38
|
+
# same way every time would be re-run on every scan.
|
|
39
|
+
_MAX_ERROR_RETRIES = 2
|
|
35
40
|
|
|
36
41
|
|
|
37
42
|
def _register_tools() -> None:
|
|
@@ -204,14 +209,23 @@ class AgentConversationDispatcher:
|
|
|
204
209
|
|
|
205
210
|
same_delivery = record.get("delivery") == delivery
|
|
206
211
|
same_head = bool(head) and record.get("head") == head
|
|
212
|
+
# Error retries count against one delivery; a new delivery starts over.
|
|
213
|
+
error_retries = int(record.get("error_retries") or 0) if same_delivery else 0
|
|
214
|
+
can_retry = error_retries < _MAX_ERROR_RETRIES
|
|
207
215
|
|
|
208
216
|
if self._cloud:
|
|
209
217
|
disposition, conversation_id = self._deliver_cloud(
|
|
210
|
-
record,
|
|
218
|
+
record,
|
|
219
|
+
conversation_id,
|
|
220
|
+
subject,
|
|
221
|
+
prompt,
|
|
222
|
+
same_delivery,
|
|
223
|
+
same_head,
|
|
224
|
+
can_retry,
|
|
211
225
|
)
|
|
212
226
|
else:
|
|
213
227
|
disposition = self._deliver_local(
|
|
214
|
-
conversation_id, prompt, same_delivery, same_head
|
|
228
|
+
conversation_id, prompt, same_delivery, same_head, can_retry
|
|
215
229
|
)
|
|
216
230
|
|
|
217
231
|
if disposition in ("deduplicated", "in_progress"):
|
|
@@ -228,6 +242,10 @@ class AgentConversationDispatcher:
|
|
|
228
242
|
}
|
|
229
243
|
if disposition == "created" and self._cloud:
|
|
230
244
|
new_record["started_at"] = time.time()
|
|
245
|
+
if disposition == "retried":
|
|
246
|
+
error_retries += 1
|
|
247
|
+
if error_retries:
|
|
248
|
+
new_record["error_retries"] = error_retries
|
|
231
249
|
_kv_request(state_key, "PUT", new_record)
|
|
232
250
|
return {
|
|
233
251
|
"disposition": disposition,
|
|
@@ -235,7 +253,14 @@ class AgentConversationDispatcher:
|
|
|
235
253
|
}
|
|
236
254
|
|
|
237
255
|
def _deliver_cloud(
|
|
238
|
-
self,
|
|
256
|
+
self,
|
|
257
|
+
record,
|
|
258
|
+
conversation_id,
|
|
259
|
+
subject,
|
|
260
|
+
prompt,
|
|
261
|
+
same_delivery,
|
|
262
|
+
same_head,
|
|
263
|
+
can_retry,
|
|
239
264
|
):
|
|
240
265
|
"""Deliver through the OpenHands API; return the disposition and the id.
|
|
241
266
|
|
|
@@ -272,11 +297,21 @@ class AgentConversationDispatcher:
|
|
|
272
297
|
if status in ("idle", "paused"):
|
|
273
298
|
self._cloud.run(conversation)
|
|
274
299
|
return "resumed", current_id
|
|
300
|
+
if status == "error" and can_retry:
|
|
301
|
+
# The delivery matched but its conversation died before the work
|
|
302
|
+
# finished, so send the revision again rather than strand it.
|
|
303
|
+
# The API reports an execution status only while the sandbox
|
|
304
|
+
# runs, so an errored conversation whose sandbox was already
|
|
305
|
+
# paused is not seen here and keeps reporting deduplicated.
|
|
306
|
+
self._cloud.send(conversation, prompt)
|
|
307
|
+
return "retried", current_id
|
|
275
308
|
return "deduplicated", current_id
|
|
276
309
|
self._cloud.send(conversation, prompt)
|
|
277
310
|
return "resumed", current_id
|
|
278
311
|
|
|
279
|
-
def _deliver_local(
|
|
312
|
+
def _deliver_local(
|
|
313
|
+
self, conversation_id, prompt, same_delivery, same_head, can_retry
|
|
314
|
+
) -> str:
|
|
280
315
|
if self._workspace is None:
|
|
281
316
|
raise RuntimeError("AgentConversationDispatcher must be used as a context")
|
|
282
317
|
|
|
@@ -321,6 +356,18 @@ class AgentConversationDispatcher:
|
|
|
321
356
|
):
|
|
322
357
|
conversation.update_secrets(self._secrets)
|
|
323
358
|
conversation.run(blocking=False)
|
|
359
|
+
elif (
|
|
360
|
+
conversation.state.execution_status
|
|
361
|
+
== ConversationExecutionStatus.ERROR
|
|
362
|
+
and can_retry
|
|
363
|
+
):
|
|
364
|
+
# The delivery matched but its conversation died before
|
|
365
|
+
# the work finished, so send the revision again rather
|
|
366
|
+
# than strand it.
|
|
367
|
+
conversation.update_secrets(self._secrets)
|
|
368
|
+
conversation.send_message(prompt)
|
|
369
|
+
conversation.run(blocking=False)
|
|
370
|
+
disposition = "retried"
|
|
324
371
|
else:
|
|
325
372
|
disposition = "deduplicated"
|
|
326
373
|
else:
|
|
@@ -32,6 +32,11 @@ _CLOUD_START_GRACE_SECONDS = 10 * 60
|
|
|
32
32
|
# How long to wait for a paused conversation's sandbox to come back up.
|
|
33
33
|
_CLOUD_RESUME_TIMEOUT_SECONDS = 120
|
|
34
34
|
_CLOUD_POLL_SECONDS = 3
|
|
35
|
+
# How many times a matched delivery whose conversation ended in ERROR is sent
|
|
36
|
+
# again. The delivery key never changes for a stable subject, so without a
|
|
37
|
+
# retry its work is stranded; without a bound, a conversation that fails the
|
|
38
|
+
# same way every time would be re-run on every scan.
|
|
39
|
+
_MAX_ERROR_RETRIES = 2
|
|
35
40
|
|
|
36
41
|
|
|
37
42
|
def _register_tools() -> None:
|
|
@@ -204,14 +209,23 @@ class AgentConversationDispatcher:
|
|
|
204
209
|
|
|
205
210
|
same_delivery = record.get("delivery") == delivery
|
|
206
211
|
same_head = bool(head) and record.get("head") == head
|
|
212
|
+
# Error retries count against one delivery; a new delivery starts over.
|
|
213
|
+
error_retries = int(record.get("error_retries") or 0) if same_delivery else 0
|
|
214
|
+
can_retry = error_retries < _MAX_ERROR_RETRIES
|
|
207
215
|
|
|
208
216
|
if self._cloud:
|
|
209
217
|
disposition, conversation_id = self._deliver_cloud(
|
|
210
|
-
record,
|
|
218
|
+
record,
|
|
219
|
+
conversation_id,
|
|
220
|
+
subject,
|
|
221
|
+
prompt,
|
|
222
|
+
same_delivery,
|
|
223
|
+
same_head,
|
|
224
|
+
can_retry,
|
|
211
225
|
)
|
|
212
226
|
else:
|
|
213
227
|
disposition = self._deliver_local(
|
|
214
|
-
conversation_id, prompt, same_delivery, same_head
|
|
228
|
+
conversation_id, prompt, same_delivery, same_head, can_retry
|
|
215
229
|
)
|
|
216
230
|
|
|
217
231
|
if disposition in ("deduplicated", "in_progress"):
|
|
@@ -228,6 +242,10 @@ class AgentConversationDispatcher:
|
|
|
228
242
|
}
|
|
229
243
|
if disposition == "created" and self._cloud:
|
|
230
244
|
new_record["started_at"] = time.time()
|
|
245
|
+
if disposition == "retried":
|
|
246
|
+
error_retries += 1
|
|
247
|
+
if error_retries:
|
|
248
|
+
new_record["error_retries"] = error_retries
|
|
231
249
|
_kv_request(state_key, "PUT", new_record)
|
|
232
250
|
return {
|
|
233
251
|
"disposition": disposition,
|
|
@@ -235,7 +253,14 @@ class AgentConversationDispatcher:
|
|
|
235
253
|
}
|
|
236
254
|
|
|
237
255
|
def _deliver_cloud(
|
|
238
|
-
self,
|
|
256
|
+
self,
|
|
257
|
+
record,
|
|
258
|
+
conversation_id,
|
|
259
|
+
subject,
|
|
260
|
+
prompt,
|
|
261
|
+
same_delivery,
|
|
262
|
+
same_head,
|
|
263
|
+
can_retry,
|
|
239
264
|
):
|
|
240
265
|
"""Deliver through the OpenHands API; return the disposition and the id.
|
|
241
266
|
|
|
@@ -272,11 +297,21 @@ class AgentConversationDispatcher:
|
|
|
272
297
|
if status in ("idle", "paused"):
|
|
273
298
|
self._cloud.run(conversation)
|
|
274
299
|
return "resumed", current_id
|
|
300
|
+
if status == "error" and can_retry:
|
|
301
|
+
# The delivery matched but its conversation died before the work
|
|
302
|
+
# finished, so send the revision again rather than strand it.
|
|
303
|
+
# The API reports an execution status only while the sandbox
|
|
304
|
+
# runs, so an errored conversation whose sandbox was already
|
|
305
|
+
# paused is not seen here and keeps reporting deduplicated.
|
|
306
|
+
self._cloud.send(conversation, prompt)
|
|
307
|
+
return "retried", current_id
|
|
275
308
|
return "deduplicated", current_id
|
|
276
309
|
self._cloud.send(conversation, prompt)
|
|
277
310
|
return "resumed", current_id
|
|
278
311
|
|
|
279
|
-
def _deliver_local(
|
|
312
|
+
def _deliver_local(
|
|
313
|
+
self, conversation_id, prompt, same_delivery, same_head, can_retry
|
|
314
|
+
) -> str:
|
|
280
315
|
if self._workspace is None:
|
|
281
316
|
raise RuntimeError("AgentConversationDispatcher must be used as a context")
|
|
282
317
|
|
|
@@ -321,6 +356,18 @@ class AgentConversationDispatcher:
|
|
|
321
356
|
):
|
|
322
357
|
conversation.update_secrets(self._secrets)
|
|
323
358
|
conversation.run(blocking=False)
|
|
359
|
+
elif (
|
|
360
|
+
conversation.state.execution_status
|
|
361
|
+
== ConversationExecutionStatus.ERROR
|
|
362
|
+
and can_retry
|
|
363
|
+
):
|
|
364
|
+
# The delivery matched but its conversation died before
|
|
365
|
+
# the work finished, so send the revision again rather
|
|
366
|
+
# than strand it.
|
|
367
|
+
conversation.update_secrets(self._secrets)
|
|
368
|
+
conversation.send_message(prompt)
|
|
369
|
+
conversation.run(blocking=False)
|
|
370
|
+
disposition = "retried"
|
|
324
371
|
else:
|
|
325
372
|
disposition = "deduplicated"
|
|
326
373
|
else:
|
|
@@ -61,6 +61,11 @@ This skill is activated by:
|
|
|
61
61
|
checkout when the review ends, so nothing accumulates between runs
|
|
62
62
|
- Publishes a real pull request review, with inline comments where a finding
|
|
63
63
|
maps to a changed line, and verifies on GitHub that it landed
|
|
64
|
+
- Requires live evidence from the real app before approving a user-visible UI
|
|
65
|
+
change when the repository's guidance demands it: unit tests, CSS-token
|
|
66
|
+
assertions, generated mockups, and reconstructed captures cannot substitute.
|
|
67
|
+
Missing evidence yields a COMMENT review that names the gap, so no approval
|
|
68
|
+
and no maintainer handoff
|
|
64
69
|
- Posts acknowledgement comments with AI disclosure
|
|
65
70
|
- Configurable review tone and polling schedule
|
|
66
71
|
- Optional human handoff after an exact-head approval. The scanner ranks the
|
|
@@ -189,6 +189,18 @@ handler recognizes the verdict and requests one configured maintainer through th
|
|
|
189
189
|
same handoff used after an approval. An in-scope change continues the existing
|
|
190
190
|
review unchanged.
|
|
191
191
|
|
|
192
|
+
Before approving a change to user-visible UI behavior, the prompt requires live
|
|
193
|
+
evidence from a real running application when the repository's guidance demands
|
|
194
|
+
it: a screenshot, screen recording, or equivalent capture of the running app
|
|
195
|
+
that exercises the production-facing path. Unit tests, CSS-token or contract
|
|
196
|
+
assertions, generated mockups, and reconstructed captures may support the review
|
|
197
|
+
but cannot substitute for that evidence. When the required evidence is missing,
|
|
198
|
+
the reviewer publishes one `event: COMMENT` review naming exactly what is
|
|
199
|
+
missing and ends with `🔄 CHANGES REQUESTED`, so approval is withheld and the
|
|
200
|
+
deterministic maintainer handoff does not fire. Non-UI changes keep the existing
|
|
201
|
+
requirement of the real command and its observed output, with tests insufficient
|
|
202
|
+
as sole proof.
|
|
203
|
+
|
|
192
204
|
The script prepares each review's workspace before the agent starts: the pull
|
|
193
205
|
request's head commit is downloaded as a tarball and extracted to a directory of
|
|
194
206
|
its own, which becomes the conversation's working directory. The agent is told
|