@openhands/extensions 0.27.0 → 0.29.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/AGENTS.md +7 -2
- 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 +79 -12
- 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/canvas-extension-api/SKILL.md +4 -1
- package/skills/canvas-extension-api/references/native-design.md +250 -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 +25 -13
- 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 +79 -12
- 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
|
@@ -27,6 +27,11 @@ from maintainer_handoff import (
|
|
|
27
27
|
# every current-head check and workflow run, so a red head still blocks.
|
|
28
28
|
CHECK_GATE_MARKER = "<!-- openhands-review-gate:"
|
|
29
29
|
NON_BLOCKING_CHECK_CONCLUSIONS = frozenset({"success", "neutral", "skipped"})
|
|
30
|
+
# GitHub parks a fork head's workflow runs at this conclusion until a maintainer
|
|
31
|
+
# approves them. Such a run is waiting on a person, not failing, so the gate
|
|
32
|
+
# lists it as waiting with this note and its explanation asks for the approval.
|
|
33
|
+
AWAITING_APPROVAL_CONCLUSION = "action_required"
|
|
34
|
+
AWAITING_APPROVAL_NOTE = " (awaiting maintainer approval)"
|
|
30
35
|
# The gate is deterministic; this disclosure is what tells a reader no model ran.
|
|
31
36
|
WORKFLOW_DISCLOSURE = "no AI was used to generate this comment"
|
|
32
37
|
|
|
@@ -98,7 +103,8 @@ class ReviewIntake:
|
|
|
98
103
|
flush=True,
|
|
99
104
|
)
|
|
100
105
|
continue
|
|
101
|
-
|
|
106
|
+
# A retried conversation runs again, so it uses a slot like a new one.
|
|
107
|
+
if result["disposition"] in ("created", "retried"):
|
|
102
108
|
self._started += 1
|
|
103
109
|
if failures:
|
|
104
110
|
raise RuntimeError(
|
|
@@ -283,7 +289,7 @@ class PullRequestReviewer(GitHubRepository):
|
|
|
283
289
|
f"second result.{self_review_note}"
|
|
284
290
|
)
|
|
285
291
|
|
|
286
|
-
def _finish_completed_review(self, pr, trigger, label=None):
|
|
292
|
+
def _finish_completed_review(self, pr, trigger, label=None, submitted_review=None):
|
|
287
293
|
"""Complete an exact-head review, including an optional human handoff.
|
|
288
294
|
|
|
289
295
|
`trigger` is the event the current work is keyed on. It is None for an
|
|
@@ -291,7 +297,11 @@ class PullRequestReviewer(GitHubRepository):
|
|
|
291
297
|
is the whole key, so any submitted review by this account on the current
|
|
292
298
|
head is the completion of that work. That is what lets an unrequested
|
|
293
299
|
review the scan started be reconciled and handed off on a later scan
|
|
294
|
-
instead of being restarted.
|
|
300
|
+
instead of being restarted. `submitted_review` is the review a
|
|
301
|
+
`submitted` delivery reports. It can predate the latest
|
|
302
|
+
`review_requested` event, so it is found by id rather than by the
|
|
303
|
+
trigger window; this account's later reviews of the head still count, so
|
|
304
|
+
a newer verdict supersedes it. A dismissed review completes nothing.
|
|
295
305
|
"""
|
|
296
306
|
head_sha = pr["head"]["sha"]
|
|
297
307
|
if trigger is not None:
|
|
@@ -299,14 +309,35 @@ class PullRequestReviewer(GitHubRepository):
|
|
|
299
309
|
else:
|
|
300
310
|
triggered_at = ""
|
|
301
311
|
reviews = self.gh_pages(f"/pulls/{pr['number']}/reviews")
|
|
302
|
-
|
|
312
|
+
mine = [
|
|
303
313
|
review
|
|
304
314
|
for review in reviews
|
|
305
315
|
if review.get("commit_id") == head_sha
|
|
306
316
|
and ((review.get("user") or {}).get("login") or "").lower()
|
|
307
317
|
== self.github_login.lower()
|
|
308
|
-
and (review.get("
|
|
318
|
+
and (review.get("state") or "").upper() != "DISMISSED"
|
|
309
319
|
]
|
|
320
|
+
if submitted_review and submitted_review.get("id") is not None:
|
|
321
|
+
reported = next(
|
|
322
|
+
(review for review in mine if review.get("id") == submitted_review["id"]),
|
|
323
|
+
None,
|
|
324
|
+
)
|
|
325
|
+
reported_at = (reported or {}).get("submitted_at") or ""
|
|
326
|
+
completed = (
|
|
327
|
+
[]
|
|
328
|
+
if reported is None
|
|
329
|
+
else [
|
|
330
|
+
review
|
|
331
|
+
for review in mine
|
|
332
|
+
if (review.get("submitted_at") or "") >= reported_at
|
|
333
|
+
]
|
|
334
|
+
)
|
|
335
|
+
else:
|
|
336
|
+
completed = [
|
|
337
|
+
review
|
|
338
|
+
for review in mine
|
|
339
|
+
if (review.get("submitted_at") or "") > triggered_at
|
|
340
|
+
]
|
|
310
341
|
if not completed:
|
|
311
342
|
return False
|
|
312
343
|
approved = None
|
|
@@ -482,16 +513,19 @@ class PullRequestReviewer(GitHubRepository):
|
|
|
482
513
|
|
|
483
514
|
A completed run whose conclusion is neither blocking nor explicitly
|
|
484
515
|
non-blocking fails closed, so an unknown conclusion cannot silently
|
|
485
|
-
approve a PR. A run that has not completed
|
|
486
|
-
approval.
|
|
516
|
+
approve a PR. A run that has not completed, or one awaiting a
|
|
517
|
+
maintainer's approval to run, means waiting, never approval.
|
|
487
518
|
"""
|
|
488
519
|
blocking, pending = [], []
|
|
489
520
|
for run in reporters:
|
|
490
521
|
name = run.get("name") or "unnamed check"
|
|
491
522
|
status = (run.get("status") or "").lower()
|
|
523
|
+
conclusion = (run.get("conclusion") or "").lower()
|
|
492
524
|
if status != "completed":
|
|
493
525
|
pending.append(name)
|
|
494
|
-
elif
|
|
526
|
+
elif conclusion == AWAITING_APPROVAL_CONCLUSION:
|
|
527
|
+
pending.append(f"{name}{AWAITING_APPROVAL_NOTE}")
|
|
528
|
+
elif conclusion in NON_BLOCKING_CHECK_CONCLUSIONS:
|
|
495
529
|
continue
|
|
496
530
|
else:
|
|
497
531
|
blocking.append(name)
|
|
@@ -650,10 +684,10 @@ class PullRequestReviewer(GitHubRepository):
|
|
|
650
684
|
short = sha[:12]
|
|
651
685
|
listed = "\n".join(f"- `{name}`" for name in names)
|
|
652
686
|
if state == "blocked":
|
|
653
|
-
heading = "### ⚠️ Review paused: current-head checks
|
|
687
|
+
heading = "### ⚠️ Review paused: current-head checks did not pass"
|
|
654
688
|
lead = (
|
|
655
|
-
f"The current head `{short}` has
|
|
656
|
-
"conversation was started:"
|
|
689
|
+
f"The current head `{short}` has checks that did not pass, so no "
|
|
690
|
+
"review conversation was started:"
|
|
657
691
|
)
|
|
658
692
|
action = (
|
|
659
693
|
"Fix the checks above and push. The scheduled scan then starts "
|
|
@@ -665,6 +699,25 @@ class PullRequestReviewer(GitHubRepository):
|
|
|
665
699
|
"second request while the first is still outstanding. The review "
|
|
666
700
|
"starts on the updated head."
|
|
667
701
|
)
|
|
702
|
+
elif any(name.endswith(AWAITING_APPROVAL_NOTE) for name in names):
|
|
703
|
+
heading = "### ⏳ Review waiting on workflow approval"
|
|
704
|
+
lead = (
|
|
705
|
+
f"The current head `{short}` has workflow runs that GitHub holds "
|
|
706
|
+
"until a maintainer approves them, or checks that have not "
|
|
707
|
+
"finished, so no review conversation was started:"
|
|
708
|
+
)
|
|
709
|
+
action = (
|
|
710
|
+
"A maintainer needs to approve the workflow runs marked above. "
|
|
711
|
+
"The scheduled scan retries once every check on the head reports "
|
|
712
|
+
"a conclusion."
|
|
713
|
+
if scheduled
|
|
714
|
+
else "A maintainer needs to approve the workflow runs marked "
|
|
715
|
+
"above. Once every check on the head reports a conclusion, "
|
|
716
|
+
f"remove the outstanding `{self.trigger_reviewer}` request and "
|
|
717
|
+
f"request `{self.trigger_reviewer}` again: GitHub will not "
|
|
718
|
+
"accept a second request while the first is still outstanding. "
|
|
719
|
+
"That starts the review."
|
|
720
|
+
)
|
|
668
721
|
else:
|
|
669
722
|
heading = "### ⏳ Review waiting on checks"
|
|
670
723
|
lead = (
|
|
@@ -858,6 +911,11 @@ class PullRequestReviewer(GitHubRepository):
|
|
|
858
911
|
for candidate in prs:
|
|
859
912
|
try:
|
|
860
913
|
pr = self.gh("GET", f"/pulls/{candidate['number']}")
|
|
914
|
+
submitted_review = (
|
|
915
|
+
payload.get("review")
|
|
916
|
+
if event_mode and payload.get("action") == "submitted"
|
|
917
|
+
else None
|
|
918
|
+
)
|
|
861
919
|
has_label = label in {
|
|
862
920
|
item["name"] for item in pr.get("labels", [])
|
|
863
921
|
}
|
|
@@ -894,7 +952,11 @@ class PullRequestReviewer(GitHubRepository):
|
|
|
894
952
|
delivery_key = self._unrequested_head(pr)
|
|
895
953
|
unrequested_candidate = True
|
|
896
954
|
if trigger is None and delivery_key is None:
|
|
897
|
-
|
|
955
|
+
# A submitted review is its own completion signal: its
|
|
956
|
+
# request can predate the issue-event window, and that
|
|
957
|
+
# review must still be reconciled.
|
|
958
|
+
if submitted_review is None:
|
|
959
|
+
continue
|
|
898
960
|
# A head this account already reviewed, with no clarifying
|
|
899
961
|
# comment since, is done: another conversation would publish a
|
|
900
962
|
# second review of identical code. This must come before the
|
|
@@ -918,7 +980,7 @@ class PullRequestReviewer(GitHubRepository):
|
|
|
918
980
|
),
|
|
919
981
|
flush=True,
|
|
920
982
|
)
|
|
921
|
-
if trigger is not None:
|
|
983
|
+
if trigger is not None or submitted_review is not None:
|
|
922
984
|
# Reconcile the completed review (clearing the label and
|
|
923
985
|
# running the maintainer handoff) where the review is
|
|
924
986
|
# this trigger's result. When it predates the trigger -
|
|
@@ -927,7 +989,7 @@ class PullRequestReviewer(GitHubRepository):
|
|
|
927
989
|
# otherwise the label stays and every later scan re-reads
|
|
928
990
|
# this head and re-logs with no effect.
|
|
929
991
|
if not self._finish_completed_review(
|
|
930
|
-
pr, trigger, trigger_label
|
|
992
|
+
pr, trigger, trigger_label, submitted_review
|
|
931
993
|
) and trigger_label:
|
|
932
994
|
self.gh(
|
|
933
995
|
"DELETE",
|
|
@@ -936,7 +998,7 @@ class PullRequestReviewer(GitHubRepository):
|
|
|
936
998
|
)
|
|
937
999
|
continue
|
|
938
1000
|
if delivery_key is None and self._finish_completed_review(
|
|
939
|
-
pr, trigger, trigger_label
|
|
1001
|
+
pr, trigger, trigger_label, submitted_review
|
|
940
1002
|
):
|
|
941
1003
|
continue
|
|
942
1004
|
if event_mode and payload.get("action") == "submitted":
|
|
@@ -538,6 +538,29 @@ class TestRepoReviewGuide(unittest.TestCase):
|
|
|
538
538
|
self.assertIn("Do not add speculative or out-of-scope notes", prompt)
|
|
539
539
|
self.assertIn("forbids the configured bot from approving its own PR", prompt)
|
|
540
540
|
|
|
541
|
+
def test_prompt_requires_live_ui_evidence_before_approval(self):
|
|
542
|
+
prompt = main._build_review_prompt(
|
|
543
|
+
"owner/repo",
|
|
544
|
+
self._pr(),
|
|
545
|
+
"0123456789abcdef",
|
|
546
|
+
{"id": "1", "created_at": "t"},
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
# The gate sits after the finding-quality rules and before the review is
|
|
550
|
+
# published, so a missing capture is a blocking finding, not a note.
|
|
551
|
+
gate = prompt.index("LIVE EVIDENCE GATE")
|
|
552
|
+
self.assertLess(prompt.index("Ground every finding"), gate)
|
|
553
|
+
self.assertLess(gate, prompt.index("Publish one review"))
|
|
554
|
+
self.assertIn("user-visible UI behavior", prompt)
|
|
555
|
+
self.assertIn("real running application", prompt)
|
|
556
|
+
self.assertIn("production-facing path", prompt)
|
|
557
|
+
self.assertIn("CSS-token", prompt)
|
|
558
|
+
self.assertIn("generated mockups", prompt)
|
|
559
|
+
self.assertIn("reconstructed", prompt)
|
|
560
|
+
self.assertIn("never substitute for the required live evidence", prompt)
|
|
561
|
+
self.assertIn("names exactly which live evidence is missing", prompt)
|
|
562
|
+
self.assertIn("`🔄 CHANGES REQUESTED`", prompt)
|
|
563
|
+
|
|
541
564
|
def test_prompt_requires_refreshing_mutable_github_state(self):
|
|
542
565
|
"""A resumed conversation must not trust an earlier turn's observations."""
|
|
543
566
|
prompt = main._build_review_prompt(
|
package/skills/gitlab/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 GitLab 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 `GITLAB_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, `GITLAB_TOKEN`, which allows you to interact with
|
|
15
17
|
the GitLab API.
|
|
16
18
|
|
|
@@ -34,4 +36,4 @@ Here are some instructions for pushing, but ONLY do this if the user asks you to
|
|
|
34
36
|
```bash
|
|
35
37
|
git remote -v && git branch # to find the current org, repo and branch
|
|
36
38
|
git checkout -b create-widget && git add . && git commit -m "Create widget" && git push -u origin create-widget
|
|
37
|
-
```
|
|
39
|
+
```
|
package/skills/gitlab/SKILL.md
CHANGED
|
@@ -5,6 +5,10 @@ triggers:
|
|
|
5
5
|
- gitlab
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
## Connection selection
|
|
9
|
+
|
|
10
|
+
Use authenticated GitLab 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 GitLab MCP tools are unavailable or when raw API or `curl` access is explicitly needed.
|
|
11
|
+
|
|
8
12
|
You have access to an environment variable, `GITLAB_TOKEN`, which allows you to interact with
|
|
9
13
|
the GitLab API.
|
|
10
14
|
|