@openhands/extensions 0.21.0 → 0.22.1

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 (267) hide show
  1. package/.claude-plugin/marketplace.json +881 -0
  2. package/.codex-plugin/marketplace.json +881 -0
  3. package/.github/workflows/npm-publish.yml +9 -3
  4. package/.plugin/marketplace.json +881 -0
  5. package/.release-please-manifest.json +1 -1
  6. package/README.md +3 -2
  7. package/automations/bundle-index.js +16 -7
  8. package/automations/catalog/github-delivery-watchdog/manifest.json +79 -0
  9. package/automations/catalog/github-issue-to-pr/manifest.json +23 -15
  10. package/automations/catalog/github-issue-triage/manifest.json +4 -3
  11. package/automations/catalog/github-pr-reviewer/manifest.json +22 -15
  12. package/automations/catalog-index.js +6 -4
  13. package/marketplaces/openhands-extensions.json +6 -0
  14. package/package.json +1 -1
  15. package/plugins/city-weather/.claude-plugin/plugin.json +13 -0
  16. package/plugins/city-weather/.codex-plugin/plugin.json +13 -0
  17. package/plugins/cobol-modernization/.claude-plugin/plugin.json +19 -0
  18. package/plugins/cobol-modernization/.codex-plugin/plugin.json +19 -0
  19. package/plugins/issue-duplicate-checker/.claude-plugin/plugin.json +13 -0
  20. package/plugins/issue-duplicate-checker/.codex-plugin/plugin.json +13 -0
  21. package/plugins/magic-test/.claude-plugin/plugin.json +13 -0
  22. package/plugins/magic-test/.codex-plugin/plugin.json +13 -0
  23. package/plugins/migration-scoring/.claude-plugin/plugin.json +19 -0
  24. package/plugins/migration-scoring/.codex-plugin/plugin.json +19 -0
  25. package/plugins/onboarding/.claude-plugin/plugin.json +20 -0
  26. package/plugins/onboarding/.codex-plugin/plugin.json +20 -0
  27. package/plugins/onboarding/skills/agent-readiness-report/references/criteria.md +144 -0
  28. package/plugins/onboarding/skills/improve-agent-readiness/references/criteria.md +144 -0
  29. package/plugins/openhands/.claude-plugin/plugin.json +13 -0
  30. package/plugins/openhands/.codex-plugin/plugin.json +13 -0
  31. package/plugins/openhands/skills/openhands-api/.claude-plugin/plugin.json +22 -0
  32. package/plugins/openhands/skills/openhands-api/.codex-plugin/plugin.json +22 -0
  33. package/plugins/openhands/skills/openhands-api/.plugin/plugin.json +22 -0
  34. package/plugins/openhands/skills/openhands-api/README.md +52 -0
  35. package/plugins/openhands/skills/openhands-api/SKILL.md +469 -0
  36. package/plugins/openhands/skills/openhands-api/references/README.md +33 -0
  37. package/plugins/openhands/skills/openhands-api/references/TROUBLESHOOTING.md +81 -0
  38. package/plugins/openhands/skills/openhands-api/references/example_prompt.md +12 -0
  39. package/plugins/openhands/skills/openhands-api/references/windows.md +44 -0
  40. package/plugins/openhands/skills/openhands-api/scripts/openhands_api.py +606 -0
  41. package/plugins/openhands/skills/openhands-api/scripts/openhands_api.ts +252 -0
  42. package/plugins/openhands/skills/openhands-automation/.claude-plugin/plugin.json +19 -0
  43. package/plugins/openhands/skills/openhands-automation/.codex-plugin/plugin.json +19 -0
  44. package/plugins/openhands/skills/openhands-automation/.plugin/plugin.json +19 -0
  45. package/plugins/openhands/skills/openhands-automation/README.md +89 -0
  46. package/plugins/openhands/skills/openhands-automation/SKILL.md +899 -0
  47. package/plugins/openhands/skills/openhands-automation/commands/automation-create.md +8 -0
  48. package/plugins/openhands/skills/openhands-automation/references/ab-testing.md +185 -0
  49. package/plugins/openhands/skills/openhands-automation/references/custom-automation.md +832 -0
  50. package/plugins/openhands/skills/openhands-automation/references/security.md +84 -0
  51. package/plugins/openhands/skills/openhands-automation/references/windows.md +57 -0
  52. package/plugins/openhands/skills/openhands-sdk/.claude-plugin/plugin.json +20 -0
  53. package/plugins/openhands/skills/openhands-sdk/.codex-plugin/plugin.json +20 -0
  54. package/plugins/openhands/skills/openhands-sdk/.plugin/plugin.json +20 -0
  55. package/plugins/openhands/skills/openhands-sdk/README.md +22 -0
  56. package/plugins/openhands/skills/openhands-sdk/SKILL.md +245 -0
  57. package/plugins/openhands/skills/openhands-sdk/commands/sdk.md +8 -0
  58. package/plugins/pr-review/.claude-plugin/plugin.json +13 -0
  59. package/plugins/pr-review/.codex-plugin/plugin.json +13 -0
  60. package/plugins/pr-review/skills/code-review/.claude-plugin/plugin.json +19 -0
  61. package/plugins/pr-review/skills/code-review/.codex-plugin/plugin.json +19 -0
  62. package/plugins/pr-review/skills/code-review/.plugin/plugin.json +19 -0
  63. package/plugins/pr-review/skills/code-review/README.md +18 -0
  64. package/plugins/pr-review/skills/code-review/SKILL.md +209 -0
  65. package/plugins/pr-review/skills/code-review/commands/codereview-roasted.md +8 -0
  66. package/plugins/pr-review/skills/code-review/commands/codereview.md +8 -0
  67. package/plugins/pr-review/skills/code-review/references/risk-evaluation.md +41 -0
  68. package/plugins/pr-review/skills/code-review/references/supply-chain-security.md +31 -0
  69. package/plugins/pr-review/skills/github-pr-review/.claude-plugin/plugin.json +18 -0
  70. package/plugins/pr-review/skills/github-pr-review/.codex-plugin/plugin.json +18 -0
  71. package/plugins/pr-review/skills/github-pr-review/.plugin/plugin.json +18 -0
  72. package/plugins/pr-review/skills/github-pr-review/README.md +145 -0
  73. package/plugins/pr-review/skills/github-pr-review/SKILL.md +187 -0
  74. package/plugins/pr-review/skills/github-pr-review/commands/github-pr-review.md +8 -0
  75. package/plugins/pr-review/skills/github-pr-review/references/windows.md +47 -0
  76. package/plugins/qa-changes/.claude-plugin/plugin.json +11 -0
  77. package/plugins/qa-changes/.codex-plugin/plugin.json +11 -0
  78. package/plugins/qa-changes/skills/github-pr-review/.claude-plugin/plugin.json +18 -0
  79. package/plugins/qa-changes/skills/github-pr-review/.codex-plugin/plugin.json +18 -0
  80. package/plugins/qa-changes/skills/github-pr-review/.plugin/plugin.json +18 -0
  81. package/plugins/qa-changes/skills/github-pr-review/README.md +145 -0
  82. package/plugins/qa-changes/skills/github-pr-review/SKILL.md +187 -0
  83. package/plugins/qa-changes/skills/github-pr-review/commands/github-pr-review.md +8 -0
  84. package/plugins/qa-changes/skills/github-pr-review/references/windows.md +47 -0
  85. package/plugins/qa-changes/skills/qa-changes/README.md +18 -0
  86. package/plugins/qa-changes/skills/qa-changes/SKILL.md +229 -0
  87. package/plugins/qa-changes/skills/qa-changes/commands/qa-changes.md +8 -0
  88. package/plugins/release-notes/.claude-plugin/plugin.json +19 -0
  89. package/plugins/release-notes/.codex-plugin/plugin.json +19 -0
  90. package/plugins/release-notes/skills/release-notes/README.md +24 -0
  91. package/plugins/release-notes/skills/release-notes/SKILL.md +19 -0
  92. package/plugins/release-notes/skills/release-notes/commands/release-notes.md +8 -0
  93. package/plugins/vulnerability-remediation/.claude-plugin/plugin.json +19 -0
  94. package/plugins/vulnerability-remediation/.codex-plugin/plugin.json +19 -0
  95. package/plugins/vulnerability-remediation/skills/github/.claude-plugin/plugin.json +19 -0
  96. package/plugins/vulnerability-remediation/skills/github/.codex-plugin/plugin.json +19 -0
  97. package/plugins/vulnerability-remediation/skills/github/.plugin/plugin.json +19 -0
  98. package/plugins/vulnerability-remediation/skills/github/README.md +42 -0
  99. package/plugins/vulnerability-remediation/skills/github/SKILL.md +107 -0
  100. package/plugins/vulnerability-remediation/skills/github/references/windows.md +48 -0
  101. package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +150 -0
  102. package/plugins/vulnerability-remediation/skills/github/scripts/github_client.py +230 -0
  103. package/plugins/vulnerability-remediation/skills/security/.claude-plugin/plugin.json +18 -0
  104. package/plugins/vulnerability-remediation/skills/security/.codex-plugin/plugin.json +18 -0
  105. package/plugins/vulnerability-remediation/skills/security/.plugin/plugin.json +18 -0
  106. package/plugins/vulnerability-remediation/skills/security/README.md +38 -0
  107. package/plugins/vulnerability-remediation/skills/security/SKILL.md +33 -0
  108. package/pyproject.toml +1 -1
  109. package/python/openhands_extensions/_version.py +1 -1
  110. package/skills/add-javadoc/.claude-plugin/plugin.json +18 -0
  111. package/skills/add-javadoc/.codex-plugin/plugin.json +18 -0
  112. package/skills/add-skill/.claude-plugin/plugin.json +18 -0
  113. package/skills/add-skill/.codex-plugin/plugin.json +18 -0
  114. package/skills/agent-canvas-environment/.claude-plugin/plugin.json +19 -0
  115. package/skills/agent-canvas-environment/.codex-plugin/plugin.json +19 -0
  116. package/skills/agent-creator/.claude-plugin/plugin.json +20 -0
  117. package/skills/agent-creator/.codex-plugin/plugin.json +20 -0
  118. package/skills/agent-memory/.claude-plugin/plugin.json +18 -0
  119. package/skills/agent-memory/.codex-plugin/plugin.json +18 -0
  120. package/skills/agent-sdk-builder/.claude-plugin/plugin.json +18 -0
  121. package/skills/agent-sdk-builder/.codex-plugin/plugin.json +18 -0
  122. package/skills/azure-devops/.claude-plugin/plugin.json +18 -0
  123. package/skills/azure-devops/.codex-plugin/plugin.json +18 -0
  124. package/skills/bitbucket/.claude-plugin/plugin.json +17 -0
  125. package/skills/bitbucket/.codex-plugin/plugin.json +17 -0
  126. package/skills/bitbucket-cloud/.claude-plugin/plugin.json +18 -0
  127. package/skills/bitbucket-cloud/.codex-plugin/plugin.json +18 -0
  128. package/skills/bitbucket-data-center/.claude-plugin/plugin.json +19 -0
  129. package/skills/bitbucket-data-center/.codex-plugin/plugin.json +19 -0
  130. package/skills/code-review/.claude-plugin/plugin.json +19 -0
  131. package/skills/code-review/.codex-plugin/plugin.json +19 -0
  132. package/skills/code-simplifier/.claude-plugin/plugin.json +21 -0
  133. package/skills/code-simplifier/.codex-plugin/plugin.json +21 -0
  134. package/skills/datadog/.claude-plugin/plugin.json +19 -0
  135. package/skills/datadog/.codex-plugin/plugin.json +19 -0
  136. package/skills/deno/.claude-plugin/plugin.json +18 -0
  137. package/skills/deno/.codex-plugin/plugin.json +18 -0
  138. package/skills/discord/.claude-plugin/plugin.json +18 -0
  139. package/skills/discord/.codex-plugin/plugin.json +18 -0
  140. package/skills/docker/.claude-plugin/plugin.json +17 -0
  141. package/skills/docker/.codex-plugin/plugin.json +17 -0
  142. package/skills/evidence-based-citations/.claude-plugin/plugin.json +20 -0
  143. package/skills/evidence-based-citations/.codex-plugin/plugin.json +20 -0
  144. package/skills/flarglebargle/.claude-plugin/plugin.json +16 -0
  145. package/skills/flarglebargle/.codex-plugin/plugin.json +16 -0
  146. package/skills/frontend-design/.claude-plugin/plugin.json +21 -0
  147. package/skills/frontend-design/.codex-plugin/plugin.json +21 -0
  148. package/skills/github/.claude-plugin/plugin.json +19 -0
  149. package/skills/github/.codex-plugin/plugin.json +19 -0
  150. package/skills/github/scripts/agent_conversation.py +150 -0
  151. package/skills/github/scripts/github_client.py +31 -5
  152. package/skills/github-actions/.claude-plugin/plugin.json +19 -0
  153. package/skills/github-actions/.codex-plugin/plugin.json +19 -0
  154. package/skills/github-agents-md-maintainer/.claude-plugin/plugin.json +19 -0
  155. package/skills/github-agents-md-maintainer/.codex-plugin/plugin.json +19 -0
  156. package/skills/github-agents-md-maintainer/scripts/github_client.py +230 -0
  157. package/skills/github-delivery-watchdog/.claude-plugin/plugin.json +9 -0
  158. package/skills/github-delivery-watchdog/.codex-plugin/plugin.json +9 -0
  159. package/skills/github-delivery-watchdog/.plugin/plugin.json +9 -0
  160. package/skills/github-delivery-watchdog/README.md +10 -0
  161. package/skills/github-delivery-watchdog/SKILL.md +35 -0
  162. package/skills/github-delivery-watchdog/commands/github-delivery-watchdog.md +8 -0
  163. package/skills/github-delivery-watchdog/scripts/github_client.py +230 -0
  164. package/skills/github-delivery-watchdog/scripts/worker.py +96 -0
  165. package/skills/github-issue-to-pr/.claude-plugin/plugin.json +19 -0
  166. package/skills/github-issue-to-pr/.codex-plugin/plugin.json +19 -0
  167. package/skills/github-issue-to-pr/SKILL.md +7 -0
  168. package/skills/github-issue-to-pr/scripts/agent_conversation.py +150 -0
  169. package/skills/github-issue-to-pr/scripts/github_client.py +230 -0
  170. package/skills/github-issue-to-pr/scripts/main.py +16 -10
  171. package/skills/github-issue-to-pr/scripts/worker.py +172 -0
  172. package/skills/github-issue-triage/.claude-plugin/plugin.json +9 -0
  173. package/skills/github-issue-triage/.codex-plugin/plugin.json +9 -0
  174. package/skills/github-issue-triage/SKILL.md +15 -18
  175. package/skills/github-issue-triage/scripts/agent_conversation.py +150 -0
  176. package/skills/github-issue-triage/scripts/github_client.py +230 -0
  177. package/skills/github-issue-triage/scripts/worker.py +81 -98
  178. package/skills/github-pr-review/.claude-plugin/plugin.json +18 -0
  179. package/skills/github-pr-review/.codex-plugin/plugin.json +18 -0
  180. package/skills/github-pr-reviewer/.claude-plugin/plugin.json +20 -0
  181. package/skills/github-pr-reviewer/.codex-plugin/plugin.json +20 -0
  182. package/skills/github-pr-reviewer/SKILL.md +7 -0
  183. package/skills/github-pr-reviewer/scripts/agent_conversation.py +150 -0
  184. package/skills/github-pr-reviewer/scripts/github_client.py +230 -0
  185. package/skills/github-pr-reviewer/scripts/main.py +20 -8
  186. package/skills/github-pr-reviewer/scripts/worker.py +94 -0
  187. package/skills/github-repo-monitor/.claude-plugin/plugin.json +22 -0
  188. package/skills/github-repo-monitor/.codex-plugin/plugin.json +22 -0
  189. package/skills/github-repo-monitor/scripts/github_client.py +230 -0
  190. package/skills/gitlab/.claude-plugin/plugin.json +17 -0
  191. package/skills/gitlab/.codex-plugin/plugin.json +17 -0
  192. package/skills/gitlab-issue-to-mr/.claude-plugin/plugin.json +19 -0
  193. package/skills/gitlab-issue-to-mr/.codex-plugin/plugin.json +19 -0
  194. package/skills/incident-retrospective/.claude-plugin/plugin.json +21 -0
  195. package/skills/incident-retrospective/.codex-plugin/plugin.json +21 -0
  196. package/skills/index.js +12 -3
  197. package/skills/iterate/.claude-plugin/plugin.json +13 -0
  198. package/skills/iterate/.codex-plugin/plugin.json +13 -0
  199. package/skills/jira-issue-to-pr/.claude-plugin/plugin.json +19 -0
  200. package/skills/jira-issue-to-pr/.codex-plugin/plugin.json +19 -0
  201. package/skills/jupyter/.claude-plugin/plugin.json +18 -0
  202. package/skills/jupyter/.codex-plugin/plugin.json +18 -0
  203. package/skills/kubernetes/.claude-plugin/plugin.json +18 -0
  204. package/skills/kubernetes/.codex-plugin/plugin.json +18 -0
  205. package/skills/learn-from-code-review/.claude-plugin/plugin.json +19 -0
  206. package/skills/learn-from-code-review/.codex-plugin/plugin.json +19 -0
  207. package/skills/linear/.claude-plugin/plugin.json +19 -0
  208. package/skills/linear/.codex-plugin/plugin.json +19 -0
  209. package/skills/linear-triage/.claude-plugin/plugin.json +21 -0
  210. package/skills/linear-triage/.codex-plugin/plugin.json +21 -0
  211. package/skills/news-digest/.claude-plugin/plugin.json +19 -0
  212. package/skills/news-digest/.codex-plugin/plugin.json +19 -0
  213. package/skills/notion/.claude-plugin/plugin.json +17 -0
  214. package/skills/notion/.codex-plugin/plugin.json +17 -0
  215. package/skills/npm/.claude-plugin/plugin.json +17 -0
  216. package/skills/npm/.codex-plugin/plugin.json +17 -0
  217. package/skills/openhands-api/.claude-plugin/plugin.json +22 -0
  218. package/skills/openhands-api/.codex-plugin/plugin.json +22 -0
  219. package/skills/openhands-automation/.claude-plugin/plugin.json +19 -0
  220. package/skills/openhands-automation/.codex-plugin/plugin.json +19 -0
  221. package/skills/openhands-enterprise-troubleshooting/.claude-plugin/plugin.json +20 -0
  222. package/skills/openhands-enterprise-troubleshooting/.codex-plugin/plugin.json +20 -0
  223. package/skills/openhands-sdk/.claude-plugin/plugin.json +20 -0
  224. package/skills/openhands-sdk/.codex-plugin/plugin.json +20 -0
  225. package/skills/pdflatex/.claude-plugin/plugin.json +18 -0
  226. package/skills/pdflatex/.codex-plugin/plugin.json +18 -0
  227. package/skills/plain-english-content/.claude-plugin/plugin.json +15 -0
  228. package/skills/plain-english-content/.codex-plugin/plugin.json +15 -0
  229. package/skills/prd/.claude-plugin/plugin.json +19 -0
  230. package/skills/prd/.codex-plugin/plugin.json +19 -0
  231. package/skills/research-brief/.claude-plugin/plugin.json +20 -0
  232. package/skills/research-brief/.codex-plugin/plugin.json +20 -0
  233. package/skills/security/.claude-plugin/plugin.json +18 -0
  234. package/skills/security/.codex-plugin/plugin.json +18 -0
  235. package/skills/skill-creator/.claude-plugin/plugin.json +17 -0
  236. package/skills/skill-creator/.codex-plugin/plugin.json +17 -0
  237. package/skills/slack-channel-monitor/.claude-plugin/plugin.json +21 -0
  238. package/skills/slack-channel-monitor/.codex-plugin/plugin.json +21 -0
  239. package/skills/slack-standup-digest/.claude-plugin/plugin.json +21 -0
  240. package/skills/slack-standup-digest/.codex-plugin/plugin.json +21 -0
  241. package/skills/spark-version-upgrade/.claude-plugin/plugin.json +20 -0
  242. package/skills/spark-version-upgrade/.codex-plugin/plugin.json +20 -0
  243. package/skills/ssh/.claude-plugin/plugin.json +18 -0
  244. package/skills/ssh/.codex-plugin/plugin.json +18 -0
  245. package/skills/swift-linux/.claude-plugin/plugin.json +17 -0
  246. package/skills/swift-linux/.codex-plugin/plugin.json +17 -0
  247. package/skills/technical-writing/.claude-plugin/plugin.json +15 -0
  248. package/skills/technical-writing/.codex-plugin/plugin.json +15 -0
  249. package/skills/theme-factory/.claude-plugin/plugin.json +19 -0
  250. package/skills/theme-factory/.codex-plugin/plugin.json +19 -0
  251. package/skills/ticket-to-code-change/.claude-plugin/plugin.json +21 -0
  252. package/skills/ticket-to-code-change/.codex-plugin/plugin.json +21 -0
  253. package/skills/upstream-fork-sync/.claude-plugin/plugin.json +21 -0
  254. package/skills/upstream-fork-sync/.codex-plugin/plugin.json +21 -0
  255. package/skills/uv/.claude-plugin/plugin.json +18 -0
  256. package/skills/uv/.codex-plugin/plugin.json +18 -0
  257. package/skills/vercel/.claude-plugin/plugin.json +18 -0
  258. package/skills/vercel/.codex-plugin/plugin.json +18 -0
  259. package/tests/fixtures/automations/capabilities.json +6 -3
  260. package/tests/fixtures/automations/github-pr-reviewer.json +64 -43
  261. package/tests/github_automation_helpers.py +7 -1
  262. package/tests/test_agent_conversation_dispatch.py +196 -0
  263. package/tests/test_automation_setup.py +3 -3
  264. package/tests/test_github_developer_delivery.py +150 -0
  265. package/tests/test_github_reviewer_delivery.py +110 -0
  266. package/tests/test_github_triage_delivery.py +78 -41
  267. package/tests/test_github_watchdog_delivery.py +233 -0
@@ -0,0 +1,150 @@
1
+ """Idempotently deliver automation work to profile-backed conversations."""
2
+
3
+ import json
4
+ import os
5
+ from urllib.error import HTTPError
6
+ from urllib.request import Request, urlopen
7
+ from uuid import NAMESPACE_URL, UUID, uuid5
8
+
9
+ import httpx
10
+ from openhands.sdk import RemoteConversation
11
+ from openhands.sdk.conversation.request import (
12
+ SendMessageRequest,
13
+ StartConversationRequest,
14
+ )
15
+ from openhands.sdk.conversation.state import ConversationExecutionStatus
16
+ from openhands.sdk.llm.message import TextContent
17
+ from openhands.sdk.workspace import LocalWorkspace, RemoteWorkspace
18
+
19
+ _CONVERSATION_KEY_PREFIX = "agent-conversation-"
20
+
21
+
22
+ def _register_tools() -> None:
23
+ """Register tool models needed to deserialize an attached agent."""
24
+ from openhands.tools import register_default_tools
25
+
26
+ register_default_tools()
27
+
28
+
29
+ def _kv_request(key: str, method: str, value: dict | None = None) -> dict | None:
30
+ base_url = os.environ.get("AUTOMATION_API_URL", "").rstrip("/")
31
+ token = os.environ.get("AUTOMATION_KV_TOKEN", "")
32
+ if not base_url or not token:
33
+ raise RuntimeError("Automation KV is required for agent conversation dispatch")
34
+ request = Request(
35
+ f"{base_url}/v1/kv/{key}",
36
+ data=json.dumps(value).encode() if value is not None else None,
37
+ headers={
38
+ "Authorization": f"Bearer {token}",
39
+ "Content-Type": "application/json",
40
+ },
41
+ method=method,
42
+ )
43
+ try:
44
+ with urlopen(request, timeout=90) as response:
45
+ body = json.load(response)
46
+ except HTTPError as exc:
47
+ if method == "GET" and exc.code == 404:
48
+ return None
49
+ raise
50
+ return body.get("value") if method == "GET" else body
51
+
52
+
53
+ class AgentConversationDispatcher:
54
+ """Deliver one revision at a time to a stable conversation for each subject."""
55
+
56
+ def __init__(self) -> None:
57
+ self.agent_url = os.environ["AGENT_SERVER_URL"]
58
+ self.api_key = os.environ["SESSION_API_KEY"]
59
+ self.profile_id = UUID(os.environ["AUTOMATION_AGENT_PROFILE_ID"])
60
+ payload = json.loads(os.environ["AUTOMATION_EVENT_PAYLOAD"])
61
+ self.automation_id = str(payload["automation_id"])
62
+ self._workspace: RemoteWorkspace | None = None
63
+ self._secrets = {}
64
+
65
+ def __enter__(self):
66
+ _register_tools()
67
+ self._workspace = RemoteWorkspace(
68
+ host=self.agent_url,
69
+ api_key=self.api_key,
70
+ working_dir=os.environ.get("WORKSPACE_BASE", "/workspace"),
71
+ )
72
+ self._workspace.__enter__()
73
+ self._secrets = self._workspace.get_secrets(
74
+ agent_profile_id=str(self.profile_id)
75
+ )
76
+ return self
77
+
78
+ def __exit__(self, *args):
79
+ assert self._workspace is not None
80
+ return self._workspace.__exit__(*args)
81
+
82
+ def deliver(self, subject: str, delivery: str, prompt: str) -> dict[str, str]:
83
+ conversation_id = uuid5(NAMESPACE_URL, f"{self.automation_id}:{subject}")
84
+ state_key = f"{_CONVERSATION_KEY_PREFIX}{conversation_id}"
85
+ record = _kv_request(state_key, "GET") or {}
86
+ if self._workspace is None:
87
+ raise RuntimeError("AgentConversationDispatcher must be used as a context")
88
+
89
+ same_delivery = record.get("delivery") == delivery
90
+
91
+ try:
92
+ conversation = RemoteConversation.attach(
93
+ self._workspace, conversation_id, visualizer=None
94
+ )
95
+ disposition = "resumed"
96
+ except httpx.HTTPStatusError as exc:
97
+ if exc.response.status_code != 404:
98
+ raise
99
+ conversation = RemoteConversation.create(
100
+ self._workspace,
101
+ StartConversationRequest(
102
+ workspace=LocalWorkspace(working_dir="/workspace"),
103
+ conversation_id=conversation_id,
104
+ agent_profile_id=self.profile_id,
105
+ secrets=self._secrets,
106
+ initial_message=SendMessageRequest(
107
+ content=[TextContent(text=prompt)], run=True
108
+ ),
109
+ ),
110
+ visualizer=None,
111
+ )
112
+ disposition = "created"
113
+ try:
114
+ if disposition == "resumed":
115
+ if same_delivery:
116
+ if conversation.state.execution_status in (
117
+ ConversationExecutionStatus.IDLE,
118
+ ConversationExecutionStatus.RUNNING,
119
+ ConversationExecutionStatus.PAUSED,
120
+ ):
121
+ conversation.update_secrets(self._secrets)
122
+ conversation.run(blocking=False)
123
+ else:
124
+ disposition = "deduplicated"
125
+ else:
126
+ conversation.update_secrets(self._secrets)
127
+ conversation.send_message(prompt)
128
+ conversation.run(blocking=False)
129
+ finally:
130
+ conversation.close()
131
+
132
+ if disposition == "deduplicated":
133
+ return {
134
+ "disposition": disposition,
135
+ "conversation_id": str(conversation_id),
136
+ }
137
+
138
+ _kv_request(
139
+ state_key,
140
+ "PUT",
141
+ {
142
+ "subject": subject,
143
+ "conversation_id": str(conversation_id),
144
+ "delivery": delivery,
145
+ },
146
+ )
147
+ return {
148
+ "disposition": disposition,
149
+ "conversation_id": str(conversation_id),
150
+ }
@@ -0,0 +1,230 @@
1
+ """Shared GitHub transport and repository operations for GitHub automations."""
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import re
7
+ import subprocess
8
+ from functools import cached_property
9
+ from pathlib import Path
10
+ from urllib.error import HTTPError
11
+ from urllib.parse import parse_qsl, urlencode, urlsplit
12
+ from urllib.request import Request, urlopen
13
+
14
+
15
+ def _load_secret(name: str) -> str:
16
+ """Read one named secret from the environment or configured Agent Server."""
17
+ value = os.environ.get(name)
18
+ if value:
19
+ return value
20
+
21
+ from openhands.sdk.workspace import RemoteWorkspace
22
+
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()
33
+ if not value:
34
+ raise ValueError(f"The GitHub credential {name} is unavailable")
35
+ return value
36
+
37
+
38
+ def github_request(
39
+ token: str,
40
+ method: str,
41
+ path: str,
42
+ params: dict | None = None,
43
+ body: dict | None = None,
44
+ accept: str = "application/vnd.github+json",
45
+ ) -> tuple:
46
+ url = f"https://api.github.com{path}"
47
+ if params:
48
+ url = f"{url}?{urlencode(params)}"
49
+ headers = {
50
+ "Authorization": f"Bearer {token}",
51
+ "Accept": accept,
52
+ "X-GitHub-Api-Version": "2022-11-28",
53
+ "Content-Type": "application/json",
54
+ }
55
+ data = json.dumps(body).encode() if body is not None else None
56
+ req = Request(url, data=data, headers=headers, method=method)
57
+ with urlopen(req, timeout=90) as r:
58
+ raw = r.read()
59
+ return (json.loads(raw) if raw.strip() else {}), dict(r.headers)
60
+
61
+
62
+ def github_paginate(token: str, path: str, params: dict | None = None) -> list:
63
+ results = []
64
+ base_params = dict(params or {})
65
+ base_params.setdefault("per_page", 100)
66
+ for page in range(1, 101):
67
+ base_params["page"] = page
68
+ data, _ = github_request(token, "GET", path, params=base_params)
69
+ if not isinstance(data, list):
70
+ raise TypeError("Expected a paginated GitHub list")
71
+ results.extend(data)
72
+ if len(data) < int(base_params["per_page"]):
73
+ return results
74
+ raise RuntimeError("GitHub pagination exceeded limit")
75
+
76
+
77
+ class GitHubRepository:
78
+ name = "GitHub automation"
79
+
80
+ def __init__(
81
+ self,
82
+ config_path=Path("config.json"),
83
+ *,
84
+ github_token_secret,
85
+ repository=None,
86
+ conversation=None,
87
+ dispatcher=None,
88
+ ):
89
+ self.config = json.loads(Path(config_path).read_text())
90
+ self.repository = repository or self.config["repository"]
91
+ if not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", self.repository):
92
+ raise ValueError("repository must be owner/repo")
93
+ if not re.fullmatch(r"[A-Z_][A-Z0-9_]*", github_token_secret):
94
+ raise ValueError(
95
+ "Expected the environment variable containing the GitHub token"
96
+ )
97
+ self.token_name = github_token_secret
98
+ self.token = _load_secret(github_token_secret)
99
+ self.conversation = conversation
100
+ self.conversation_id = str(conversation.id) if conversation else None
101
+ self.dispatcher = dispatcher
102
+ self.workspace = Path(os.environ["WORKSPACE_BASE"])
103
+ self.project = self.workspace
104
+ self.evidence = self.workspace / "evidence"
105
+ self.evidence.mkdir(exist_ok=True)
106
+ self._completed_dependencies = {}
107
+
108
+ @cached_property
109
+ def base_branch(self):
110
+ return self.config.get("base_branch") or self.gh("GET", "")["default_branch"]
111
+
112
+ @property
113
+ def github_instructions(self):
114
+ return (
115
+ f"Use `GH_TOKEN=${self.token_name} gh api` for GitHub requests. "
116
+ "Never print the credential value. "
117
+ f"Only {self.repository} is in scope. Work in {self.project}. "
118
+ "Do not modify the automation bundle or its configuration."
119
+ )
120
+
121
+ def gh(self, method, path, body=None):
122
+ return github_request(
123
+ self.token, method, f"/repos/{self.repository}" + path, body=body
124
+ )[0]
125
+
126
+ def shell(self, args, cwd=None, timeout=300):
127
+ result = subprocess.run(
128
+ args,
129
+ cwd=cwd or self.project,
130
+ text=True,
131
+ stdout=subprocess.PIPE,
132
+ stderr=subprocess.STDOUT,
133
+ timeout=timeout,
134
+ check=False,
135
+ )
136
+ if result.returncode:
137
+ raise RuntimeError(
138
+ f"{args[0]} failed: {result.stdout[-4000:].replace(self.token, '[REDACTED]')}"
139
+ )
140
+ return result.stdout.strip()
141
+
142
+ def comment(self, number, text):
143
+ return self.gh(
144
+ "POST",
145
+ f"/issues/{number}/comments",
146
+ {
147
+ "body": text
148
+ + f"\n\nFactory role: `{self.name}`; conversation: `{self.conversation_id}`."
149
+ + "\n\n_This comment was posted by an AI agent (OpenHands)._"
150
+ },
151
+ )
152
+
153
+ def open_issues(self):
154
+ return [
155
+ i for i in self.gh_pages("/issues?state=open") if "pull_request" not in i
156
+ ]
157
+
158
+ def statuses(self, sha):
159
+ result = {}
160
+ for item in self.gh_pages(f"/commits/{sha}/statuses"):
161
+ result.setdefault(item["context"], item["state"])
162
+ return result
163
+
164
+ def completed_dependency(self, number):
165
+ if number in self._completed_dependencies:
166
+ return self._completed_dependencies[number]
167
+ try:
168
+ dependency = self.gh("GET", f"/issues/{number}")
169
+ except HTTPError as exc:
170
+ if exc.code == 404:
171
+ return False
172
+ raise
173
+ completed = (
174
+ dependency["state"] == "closed"
175
+ and dependency.get("state_reason") == "completed"
176
+ )
177
+ self._completed_dependencies[number] = completed
178
+ return completed
179
+
180
+ def dependencies_complete(self, issue):
181
+ """Honor explicit Depends on lines; unknown/incomplete issues remain blocked."""
182
+ for line in re.findall(
183
+ "^Depends on:\\s*(.+)$",
184
+ issue.get("body") or "",
185
+ re.MULTILINE | re.IGNORECASE,
186
+ ):
187
+ for number in re.findall("#(\\d+)", line):
188
+ if not self.completed_dependency(number):
189
+ return False
190
+ return True
191
+
192
+ def gh_pages(self, endpoint):
193
+ split = urlsplit(endpoint)
194
+ return github_paginate(
195
+ self.token,
196
+ f"/repos/{self.repository}" + split.path,
197
+ params=dict(parse_qsl(split.query)),
198
+ )
199
+
200
+
201
+ def run_repositories(automation_type, conversation=None, dispatcher=None):
202
+ parser = argparse.ArgumentParser(description=automation_type.__doc__)
203
+ parser.add_argument("--github-token-secret")
204
+ args = parser.parse_args()
205
+ config = json.loads(Path("config.json").read_text())
206
+ token_name = args.github_token_secret or config.get(
207
+ "github_token_secret", "GITHUB_PERSONAL_ACCESS_TOKEN"
208
+ )
209
+ repositories = config.get("repos") or [config["repository"]]
210
+ failures = []
211
+ for repository in repositories:
212
+ options = dict(
213
+ github_token_secret=token_name,
214
+ repository=repository,
215
+ conversation=conversation,
216
+ )
217
+ if dispatcher is not None:
218
+ options["dispatcher"] = dispatcher
219
+ automation = automation_type(**options)
220
+ try:
221
+ automation.run()
222
+ except Exception as exc: # noqa: BLE001 - one repository must not block others
223
+ failures.append(repository)
224
+ print(
225
+ json.dumps({"repository": repository, "error": type(exc).__name__}),
226
+ flush=True,
227
+ )
228
+ if failures:
229
+ raise RuntimeError("Automation failed for: " + ", ".join(failures))
230
+ return str(conversation.id) if conversation else None
@@ -8,10 +8,10 @@ GitHub `labeled` event has not already been processed by this automation.
8
8
  Each repository is polled independently and keeps its own state document, so
9
9
  pull-request numbers never collide across repositories.
10
10
 
11
- The script owns the repository checkout: it downloads the pull request's head
12
- commit as a tarball, hands the agent that directory as its workspace, and
13
- removes it once the review has finished. The agent never clones, checks out, or
14
- deletes anything.
11
+ This standalone script owns the repository checkout: it downloads the pull
12
+ request's head commit as a tarball, hands the agent that directory as its
13
+ workspace, and removes it once the review has finished. Catalog workers may
14
+ instead reuse its prompt builder with their own workspace instructions.
15
15
  """
16
16
 
17
17
  import io
@@ -732,7 +732,16 @@ def _load_repo_review_guide(workspace_dir: Path) -> str | None:
732
732
  return None
733
733
 
734
734
 
735
- def _build_review_prompt(repo: str, pr: dict, head_sha: str, label_event: dict, repo_review_guide: str | None = None) -> str:
735
+ def _build_review_prompt(
736
+ repo: str,
737
+ pr: dict,
738
+ head_sha: str,
739
+ label_event: dict,
740
+ repo_review_guide: str | None = None,
741
+ *,
742
+ workspace_instructions: str | None = None,
743
+ github_token_secret: str = "GITHUB_PERSONAL_ACCESS_TOKEN",
744
+ ) -> str:
736
745
  number = pr.get("number", "?")
737
746
  title = pr.get("title", "(no title)")
738
747
  body = (pr.get("body") or "").strip() or "(no description)"
@@ -752,6 +761,10 @@ def _build_review_prompt(repo: str, pr: dict, head_sha: str, label_event: dict,
752
761
  f"\n\nRepo-specific review guide (from {REPO_REVIEW_GUIDE_PATH}):\n---\n{repo_review_guide}\n---\n"
753
762
  if repo_review_guide else ""
754
763
  )
764
+ workspace = workspace_instructions or (
765
+ "The workspace is already the repository root at the exact Head SHA above. "
766
+ "Do not clone, fetch, check out, or delete the repository."
767
+ )
755
768
 
756
769
  return (
757
770
  "You are an AI code reviewer. Review the GitHub pull request below and publish "
@@ -768,8 +781,7 @@ def _build_review_prompt(repo: str, pr: dict, head_sha: str, label_event: dict,
768
781
  f"URL : {html_url}\n"
769
782
  f"\nPR Description:\n---\n{body}\n---\n\n"
770
783
  "Required workflow:\n"
771
- "1. The workspace is already the repository root at the exact Head SHA above. "
772
- "Do not clone, fetch, check out, or delete the repository.\n"
784
+ f"1. {workspace}\n"
773
785
  "2. Before reviewing, you MUST read the repository's own guidance to understand the repo first.\n"
774
786
  " Read `AGENTS.md` at the repository root (and any nested `AGENTS.md` covering the "
775
787
  "changed files), plus other relevant docs when present - e.g. `CONTRIBUTING.md`, "
@@ -777,7 +789,7 @@ def _build_review_prompt(repo: str, pr: dict, head_sha: str, label_event: dict,
777
789
  "guidance to your review.\n"
778
790
  " Then inspect the PR discussion, existing review comments, changed files, and the diff, "
779
791
  "together with the surrounding code in the workspace.\n"
780
- " Use `gh` or GitHub REST API calls with `GITHUB_PERSONAL_ACCESS_TOKEN`; never print secret values.\n"
792
+ f" Use `gh` or GitHub REST API calls with `{github_token_secret}`; never print secret values.\n"
781
793
  "3. Ground every finding in the workspace code. Before using an inline location, verify that "
782
794
  "the path and line are part of this pull request's diff.\n"
783
795
  f"4. Publish one review with `POST /repos/{repo}/pulls/{number}/reviews`, using "
@@ -0,0 +1,94 @@
1
+ """Select labeled PR heads and delegate each review to a sandboxed agent."""
2
+
3
+ import json
4
+
5
+ import main as workflow
6
+ from agent_conversation import AgentConversationDispatcher
7
+ from github_client import GitHubRepository, run_repositories
8
+
9
+
10
+ class PullRequestReviewer(GitHubRepository):
11
+ name = "github-pr-reviewer"
12
+
13
+ def _prompt(self, pr, label_event):
14
+ number = pr["number"]
15
+ sha = pr["head"]["sha"]
16
+ label = self.config.get("trigger_label", workflow.TRIGGER_LABEL)
17
+ token = self.token_name
18
+ workspace = (
19
+ "The workspace contains an empty Git repository. Before fetching, run "
20
+ f"`GH_TOKEN=${{{token}}} gh auth setup-git` so Git uses the "
21
+ "profile-scoped credential without storing it in the remote URL. Set "
22
+ f"the plain HTTPS origin to `https://github.com/{self.repository}.git`, "
23
+ f"fetch PR #{number}, and check out exact head `{sha}` in detached-HEAD "
24
+ "mode. Set `GIT_TERMINAL_PROMPT=0` on Git network commands so a missing "
25
+ "permission fails immediately instead of waiting for input."
26
+ )
27
+ prompt = workflow._build_review_prompt(
28
+ self.repository,
29
+ pr,
30
+ sha,
31
+ label_event,
32
+ workspace_instructions=workspace,
33
+ github_token_secret=token,
34
+ )
35
+ return (
36
+ prompt + "\n\nAcceptance reporting:\n"
37
+ "- Inspect the current GitHub Actions results for the exact head and run "
38
+ "the repository's appropriate focused tests in the workspace. Do not "
39
+ "modify tracked files.\n"
40
+ f"- Re-read {self.repository} PR #{number} immediately before reporting. "
41
+ f"If its head is no longer `{sha}`, publish no status and leave `{label}` "
42
+ "in place so the new head is reviewed.\n"
43
+ "- After the readable native review is accepted by GitHub, set commit "
44
+ "status `software-factory/review` to success only for an `✅ APPROVED` "
45
+ "verdict, otherwise failure. Set `software-factory/tests` to success only "
46
+ "when the relevant checks and tests pass, otherwise failure. Use the exact "
47
+ f"head `{sha}` for both statuses.\n"
48
+ f"- After both statuses are visible on that head, remove the `{label}` "
49
+ "label. Never paste JSON artifacts or full command logs into comments."
50
+ )
51
+
52
+ def run(self):
53
+ repository_id = self.gh("GET", "")["id"]
54
+ label = self.config.get("trigger_label", workflow.TRIGGER_LABEL)
55
+ prs = self.gh_pages("/pulls?state=open&sort=updated&direction=asc")
56
+ for candidate in prs:
57
+ if label not in {item["name"] for item in candidate.get("labels", [])}:
58
+ continue
59
+ try:
60
+ pr = self.gh("GET", f"/pulls/{candidate['number']}")
61
+ event = workflow._latest_trigger_label_event(
62
+ self.token, self.repository, pr["number"]
63
+ )
64
+ if event is None:
65
+ continue
66
+ sha = pr["head"]["sha"]
67
+ result = self.dispatcher.deliver(
68
+ subject=f"{repository_id}:pr:{pr['number']}",
69
+ delivery=f"{event['id']}:{sha}",
70
+ prompt=self._prompt(pr, event),
71
+ )
72
+ print(
73
+ json.dumps(
74
+ {
75
+ "repository": self.repository,
76
+ "pr": pr["number"],
77
+ "head_sha": sha,
78
+ "disposition": result["disposition"],
79
+ "conversation_id": result["conversation_id"],
80
+ }
81
+ ),
82
+ flush=True,
83
+ )
84
+ except Exception as exc: # noqa: BLE001 - one PR must not block the scan
85
+ print(
86
+ f"Failed to submit {self.repository} PR "
87
+ f"#{candidate.get('number', '?')}: {exc}",
88
+ flush=True,
89
+ )
90
+
91
+
92
+ if __name__ == "__main__":
93
+ with AgentConversationDispatcher() as dispatcher:
94
+ run_repositories(PullRequestReviewer, dispatcher=dispatcher)
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "github-repo-monitor",
3
+ "version": "1.0.0",
4
+ "description": "Create a cron automation that polls a GitHub repository for issue and PR comments containing a configurable trigger phrase (@OpenHands by default). Starts an OpenHands conversation with full issue/PR context, posts acknowledgement comments with a conversation link, and summarises results back to the issue/PR when the agent finishes.",
5
+ "author": {
6
+ "name": "OpenHands",
7
+ "email": "contact@all-hands.dev"
8
+ },
9
+ "homepage": "https://github.com/OpenHands/extensions",
10
+ "repository": "https://github.com/OpenHands/extensions",
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "github",
14
+ "monitor",
15
+ "issues",
16
+ "pull-requests",
17
+ "trigger",
18
+ "cron",
19
+ "automation",
20
+ "integration"
21
+ ]
22
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "github-repo-monitor",
3
+ "version": "1.0.0",
4
+ "description": "Create a cron automation that polls a GitHub repository for issue and PR comments containing a configurable trigger phrase (@OpenHands by default). Starts an OpenHands conversation with full issue/PR context, posts acknowledgement comments with a conversation link, and summarises results back to the issue/PR when the agent finishes.",
5
+ "author": {
6
+ "name": "OpenHands",
7
+ "email": "contact@all-hands.dev"
8
+ },
9
+ "homepage": "https://github.com/OpenHands/extensions",
10
+ "repository": "https://github.com/OpenHands/extensions",
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "github",
14
+ "monitor",
15
+ "issues",
16
+ "pull-requests",
17
+ "trigger",
18
+ "cron",
19
+ "automation",
20
+ "integration"
21
+ ]
22
+ }