@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.
Files changed (70) hide show
  1. package/.agents/skills/custom-codereview-guide.md +31 -0
  2. package/.claude-plugin/marketplace.json +9 -0
  3. package/.codex-plugin/marketplace.json +9 -0
  4. package/.github/workflows/build-vscode-app.yml +3 -3
  5. package/.github/workflows/deprecation-check.yml +1 -1
  6. package/.github/workflows/pypi-publish.yml +2 -2
  7. package/.github/workflows/tests.yml +1 -1
  8. package/.plugin/marketplace.json +9 -0
  9. package/.release-please-manifest.json +1 -1
  10. package/AGENTS.md +7 -2
  11. package/README.md +3 -2
  12. package/automations/bundle-index.js +5 -5
  13. package/automations/catalog/github-issue-to-pr/manifest.json +2 -2
  14. package/automations/catalog/github-issue-triage/manifest.json +2 -2
  15. package/automations/catalog/github-pr-reviewer/manifest.json +2 -2
  16. package/marketplaces/openhands-extensions.json +9 -0
  17. package/package.json +1 -1
  18. package/plugins/openhands/skills/openhands-automation/README.md +6 -4
  19. package/plugins/openhands/skills/openhands-automation/SKILL.md +103 -21
  20. package/plugins/openhands/skills/openhands-automation/references/custom-automation.md +79 -12
  21. package/plugins/openhands/skills/openhands-sdk/SKILL.md +2 -1
  22. package/plugins/qa-changes/README.md +4 -0
  23. package/plugins/qa-changes/skills/qa-changes/README.md +3 -1
  24. package/plugins/qa-changes/skills/qa-changes/SKILL.md +42 -0
  25. package/plugins/vulnerability-remediation/skills/github/README.md +5 -1
  26. package/plugins/vulnerability-remediation/skills/github/SKILL.md +27 -0
  27. package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +51 -4
  28. package/pyproject.toml +1 -1
  29. package/python/openhands_extensions/_version.py +1 -1
  30. package/scripts/sync_openhands_sdk_skill.py +16 -13
  31. package/skills/azure-devops/README.md +3 -1
  32. package/skills/azure-devops/SKILL.md +4 -0
  33. package/skills/bitbucket/README.md +3 -1
  34. package/skills/bitbucket/SKILL.md +4 -2
  35. package/skills/bitbucket-cloud/README.md +2 -0
  36. package/skills/bitbucket-cloud/SKILL.md +2 -0
  37. package/skills/bitbucket-data-center/README.md +2 -0
  38. package/skills/bitbucket-data-center/SKILL.md +2 -0
  39. package/skills/canvas-extension-api/SKILL.md +4 -1
  40. package/skills/canvas-extension-api/references/native-design.md +250 -0
  41. package/skills/github/README.md +5 -1
  42. package/skills/github/SKILL.md +27 -0
  43. package/skills/github/scripts/agent_conversation.py +51 -4
  44. package/skills/github-issue-to-pr/scripts/agent_conversation.py +51 -4
  45. package/skills/github-issue-triage/scripts/agent_conversation.py +51 -4
  46. package/skills/github-pr-reviewer/README.md +5 -0
  47. package/skills/github-pr-reviewer/SKILL.md +12 -0
  48. package/skills/github-pr-reviewer/scripts/agent_conversation.py +51 -4
  49. package/skills/github-pr-reviewer/scripts/main.py +20 -6
  50. package/skills/github-pr-reviewer/scripts/worker.py +77 -15
  51. package/skills/github-pr-reviewer/tests/test_main.py +23 -0
  52. package/skills/gitlab/README.md +3 -1
  53. package/skills/gitlab/SKILL.md +4 -0
  54. package/skills/index.js +25 -13
  55. package/skills/linear/README.md +2 -0
  56. package/skills/linear/SKILL.md +6 -2
  57. package/skills/openhands-automation/README.md +6 -4
  58. package/skills/openhands-automation/SKILL.md +103 -21
  59. package/skills/openhands-automation/references/custom-automation.md +79 -12
  60. package/skills/openhands-sdk/SKILL.md +2 -1
  61. package/skills/qa-changes/README.md +3 -1
  62. package/skills/qa-changes/SKILL.md +42 -0
  63. package/skills/simplified-technical-english/.claude-plugin/plugin.json +15 -0
  64. package/skills/simplified-technical-english/.codex-plugin/plugin.json +15 -0
  65. package/skills/simplified-technical-english/.plugin/plugin.json +15 -0
  66. package/skills/simplified-technical-english/README.md +21 -0
  67. package/skills/simplified-technical-english/SKILL.md +65 -0
  68. package/tests/fixtures/automations/github-pr-reviewer.json +9 -9
  69. package/tests/test_agent_conversation_dispatch.py +130 -0
  70. 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
- if result["disposition"] == "created":
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
- completed = [
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("submitted_at") or "") > triggered_at
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 means waiting, never
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 (run.get("conclusion") or "").lower() in NON_BLOCKING_CHECK_CONCLUSIONS:
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 failed"
687
+ heading = "### ⚠️ Review paused: current-head checks did not pass"
654
688
  lead = (
655
- f"The current head `{short}` has failing checks, so no review "
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
- continue
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(
@@ -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
+ ```
@@ -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