@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.
Files changed (67) 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/README.md +3 -2
  11. package/automations/bundle-index.js +5 -5
  12. package/automations/catalog/github-issue-to-pr/manifest.json +2 -2
  13. package/automations/catalog/github-issue-triage/manifest.json +2 -2
  14. package/automations/catalog/github-pr-reviewer/manifest.json +2 -2
  15. package/marketplaces/openhands-extensions.json +9 -0
  16. package/package.json +1 -1
  17. package/plugins/openhands/skills/openhands-automation/README.md +6 -4
  18. package/plugins/openhands/skills/openhands-automation/SKILL.md +103 -21
  19. package/plugins/openhands/skills/openhands-automation/references/custom-automation.md +3 -4
  20. package/plugins/openhands/skills/openhands-sdk/SKILL.md +2 -1
  21. package/plugins/qa-changes/README.md +4 -0
  22. package/plugins/qa-changes/skills/qa-changes/README.md +3 -1
  23. package/plugins/qa-changes/skills/qa-changes/SKILL.md +42 -0
  24. package/plugins/vulnerability-remediation/skills/github/README.md +5 -1
  25. package/plugins/vulnerability-remediation/skills/github/SKILL.md +27 -0
  26. package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +51 -4
  27. package/pyproject.toml +1 -1
  28. package/python/openhands_extensions/_version.py +1 -1
  29. package/scripts/sync_openhands_sdk_skill.py +16 -13
  30. package/skills/azure-devops/README.md +3 -1
  31. package/skills/azure-devops/SKILL.md +4 -0
  32. package/skills/bitbucket/README.md +3 -1
  33. package/skills/bitbucket/SKILL.md +4 -2
  34. package/skills/bitbucket-cloud/README.md +2 -0
  35. package/skills/bitbucket-cloud/SKILL.md +2 -0
  36. package/skills/bitbucket-data-center/README.md +2 -0
  37. package/skills/bitbucket-data-center/SKILL.md +2 -0
  38. package/skills/github/README.md +5 -1
  39. package/skills/github/SKILL.md +27 -0
  40. package/skills/github/scripts/agent_conversation.py +51 -4
  41. package/skills/github-issue-to-pr/scripts/agent_conversation.py +51 -4
  42. package/skills/github-issue-triage/scripts/agent_conversation.py +51 -4
  43. package/skills/github-pr-reviewer/README.md +5 -0
  44. package/skills/github-pr-reviewer/SKILL.md +12 -0
  45. package/skills/github-pr-reviewer/scripts/agent_conversation.py +51 -4
  46. package/skills/github-pr-reviewer/scripts/main.py +20 -6
  47. package/skills/github-pr-reviewer/scripts/worker.py +77 -15
  48. package/skills/github-pr-reviewer/tests/test_main.py +23 -0
  49. package/skills/gitlab/README.md +3 -1
  50. package/skills/gitlab/SKILL.md +4 -0
  51. package/skills/index.js +23 -11
  52. package/skills/linear/README.md +2 -0
  53. package/skills/linear/SKILL.md +6 -2
  54. package/skills/openhands-automation/README.md +6 -4
  55. package/skills/openhands-automation/SKILL.md +103 -21
  56. package/skills/openhands-automation/references/custom-automation.md +3 -4
  57. package/skills/openhands-sdk/SKILL.md +2 -1
  58. package/skills/qa-changes/README.md +3 -1
  59. package/skills/qa-changes/SKILL.md +42 -0
  60. package/skills/simplified-technical-english/.claude-plugin/plugin.json +15 -0
  61. package/skills/simplified-technical-english/.codex-plugin/plugin.json +15 -0
  62. package/skills/simplified-technical-english/.plugin/plugin.json +15 -0
  63. package/skills/simplified-technical-english/README.md +21 -0
  64. package/skills/simplified-technical-english/SKILL.md +65 -0
  65. package/tests/fixtures/automations/github-pr-reviewer.json +9 -9
  66. package/tests/test_agent_conversation_dispatch.py +130 -0
  67. package/tests/test_github_reviewer_delivery.py +279 -2
@@ -98,7 +98,7 @@
98
98
  }
99
99
  },
100
100
  "bundle": {
101
- "version": "1.2.1",
101
+ "version": "1.2.2",
102
102
  "entrypoint": "python3 worker.py",
103
103
  "timeout": 300,
104
104
  "files": {
@@ -117,5 +117,5 @@
117
117
  },
118
118
  "message": "Configure the repositories, labels, branch prefix, pull request mode, agent profile, and schedule."
119
119
  },
120
- "version": "1.2.1"
120
+ "version": "1.2.2"
121
121
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "github-issue-triage",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "name": "GitHub issue triage",
5
5
  "category": "Project management",
6
6
  "description": "Prioritize open issues and establish acceptance criteria before marking them ready for development.",
@@ -54,7 +54,7 @@
54
54
  }
55
55
  },
56
56
  "bundle": {
57
- "version": "1.3.0",
57
+ "version": "1.3.1",
58
58
  "entrypoint": "python3 worker.py",
59
59
  "timeout": 300,
60
60
  "files": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "github-pr-reviewer",
3
- "version": "1.8.1",
3
+ "version": "1.9.2",
4
4
  "name": "GitHub code review",
5
5
  "category": "Code review",
6
6
  "description": "Review requested pull-request heads, resume an outstanding reviewer request on a schedule once its checks are green, classify all open non-draft pull requests that nobody requested and review eligible green heads, drain the backlog oldest-first under a small per-scan new-conversation bound, record exact-head decisions, stop early on out-of-scope changes, and optionally request a code-aware human maintainer after approval or a maintainer-decision scope stop.",
@@ -135,7 +135,7 @@
135
135
  }
136
136
  },
137
137
  "bundle": {
138
- "version": "1.8.1",
138
+ "version": "1.9.2",
139
139
  "entrypoint": "python3 worker.py",
140
140
  "timeout": 300,
141
141
  "files": {
@@ -830,6 +830,15 @@
830
830
  "technical-writing"
831
831
  ]
832
832
  },
833
+ {
834
+ "name": "simplified-technical-english",
835
+ "source": "./skills/simplified-technical-english",
836
+ "description": "Write explanations and chat in the style of ASD-STE100 Simplified Technical English: short active sentences, one idea each, plain consistent words. Applies to prose only, never to code.",
837
+ "category": "writing",
838
+ "keywords": [
839
+ "simplified-technical-english"
840
+ ]
841
+ },
833
842
  {
834
843
  "name": "plain-english-content",
835
844
  "source": "./skills/plain-english-content",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openhands/extensions",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "Public OpenHands extension catalogs for skills, plugins, integrations, and automation templates.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -16,13 +16,13 @@ This skill is activated by keywords:
16
16
 
17
17
  ## Features
18
18
 
19
- - **Prompt-based creation**: Create automations from a natural language prompt (recommended)
19
+ - **Prompt-based creation**: Create automations from a natural language prompt, for tasks that benefit from agent reasoning
20
+ - **Custom scripts**: Run your own code, with or without an LLM — the cheaper, more reliable fit for deterministic tasks (fixed data, scheduled HTTP calls, templated messages), and for custom dependencies or full control (see [references/custom-automation.md](references/custom-automation.md))
20
21
  - **Event-triggered automations**: Trigger on GitHub events (PR opened, issue commented, push, etc.)
21
22
  - **Custom webhooks**: Register webhooks for any service (Stripe, Slack, Linear, etc.)
22
23
  - **JMESPath filters**: Match events based on payload content (labels, mentions, repos)
23
24
  - **Automation management**: List, update, enable/disable, and delete automations
24
25
  - **Manual dispatch**: Trigger automation runs on-demand
25
- - **Custom automations**: For advanced users who need full control (see [references/custom-automation.md](references/custom-automation.md))
26
26
 
27
27
  ## API Base URL
28
28
 
@@ -30,6 +30,8 @@ Use an explicitly provided host first. When a local Agent Canvas automation serv
30
30
 
31
31
  ## Quick Start
32
32
 
33
+ The examples below use the prompt preset, which suits tasks that need reasoning or judgment. For a deterministic task that needs no LLM, use a custom script instead — see the *Custom Script Example (No LLM)* in [SKILL.md](SKILL.md).
34
+
33
35
  ### Cron-Triggered Automation
34
36
 
35
37
  ```bash
@@ -81,9 +83,9 @@ curl -X POST "https://app.all-hands.dev/api/automation/v1/preset/prompt" \
81
83
  }'
82
84
  ```
83
85
 
84
- The service handles SDK code generation, tarball packaging, upload, and automation creation automatically.
86
+ For preset automations, the service handles SDK code generation, tarball packaging, upload, and automation creation automatically.
85
87
 
86
88
  ## See Also
87
89
 
88
90
  - [SKILL.md](SKILL.md) — Full API reference, agent behavior rules, event keys, filters, and examples
89
- - [references/custom-automation.md](references/custom-automation.md) — Reference for custom automations with user-provided SDK scripts
91
+ - [references/custom-automation.md](references/custom-automation.md) — Reference for custom automations with user-provided scripts, SDK-based or deterministic (no LLM)
@@ -64,14 +64,14 @@ The agent server typically runs inside a **sandbox** (a Docker or Kubernetes con
64
64
  > 1. **For LLM-appropriate work, default to preset endpoints.** They handle all SDK boilerplate, tarball packaging, and upload automatically:
65
65
  > - **Prompt preset** (`POST /v1/preset/prompt`) — for tasks expressed as a natural language prompt that benefit from agent reasoning
66
66
  > - **Plugin preset** (`POST /v1/preset/plugin`) — when plugins with skills, MCP configs, or commands are needed
67
- > 2. **Do not silently create custom scripts.** Do not generate Python code, `setup.sh` files, or tarball uploads without user consent. But *do* proactively recommend the custom path (per rule 0) when the task is deterministic or high-frequency — surface the option and let the user choose.
68
- > 3. **If neither preset is the right fit**, do NOT silently fall back to custom automation. Instead, explain the available options to the user:
69
- > - **Prompt preset** — natural language prompt execution (LLM-driven)
70
- > - **Plugin preset** — load plugins with extended capabilities (skills, MCP, hooks, commands)
71
- > - **Custom script** — full control over code, with or without LLM; point them to `references/custom-automation.md`
72
- > - Let the user choose which approach to use.
73
- > 4. **Only create custom scripts after the user agrees to that path.** Refer to `references/custom-automation.md` for the full reference.
74
- > 5. **Before suggesting event-triggered (webhook) automations, check whether the deployment is publicly reachable.** Check `RUNTIME_URL`. Webhooks require an internet-accessible URL so that external services (GitHub, Slack, Linear, etc.) can deliver events to the automation service. If `RUNTIME_URL` is unset, empty, or resolves to a local or private address (`localhost`, `127.0.0.1`, `0.0.0.0`, or any RFC 1918 range: `10.x.x.x`, `192.168.x.x`, `172.16–31.x.x`), the service cannot receive inbound webhook traffic from the public internet. In that case:
67
+ > 2. **Present the options, then build the one the user picks.** Whenever rule 0 applies, or neither preset is a clean fit (custom Python dependencies, a non-Python entrypoint, a multi-file project, direct SDK lifecycle control), lay the choices out side by side with a one-line note on cost and reliability for each, and let the user choose:
68
+ > - **Prompt preset** - natural language prompt execution (LLM-driven)
69
+ > - **Plugin preset** - load plugins with extended capabilities (skills, MCP, hooks, commands)
70
+ > - **Custom script** - full control over code, with or without an LLM; see `references/custom-automation.md`
71
+ >
72
+ > Do not silently fall back to either side. The "ready to deploy?" confirmation in the creation process applies to every path equally; a custom script needs no extra permission beyond it, and a preset gets no pass on it.
73
+ > 3. **Building a custom script:** follow `references/custom-automation.md` for tarball packaging, validation, and upload. The *Custom Script Example (No LLM)* below shows the whole flow end to end.
74
+ > 4. **Before suggesting event-triggered (webhook) automations, check whether the deployment is publicly reachable.** Check `RUNTIME_URL`. Webhooks require an internet-accessible URL so that external services (GitHub, Slack, Linear, etc.) can deliver events to the automation service. If `RUNTIME_URL` is unset, empty, or resolves to a local or private address (`localhost`, `127.0.0.1`, `0.0.0.0`, or any RFC 1918 range: `10.x.x.x`, `192.168.x.x`, `172.16–31.x.x`), the service cannot receive inbound webhook traffic from the public internet. In that case:
75
75
  > - **Recommend a cron-based polling automation instead.** Have the automation run on a schedule and call the external service's API (e.g., the GitHub REST API) to check for new events since the last run.
76
76
  > - Explain the limitation clearly to the user: "Because this is a local deployment, external services can't reach the webhook endpoint. I'll set up a polling automation using a cron schedule instead."
77
77
 
@@ -255,8 +255,10 @@ curl -X POST "${OPENHANDS_HOST}/api/automation/v1/preset/prompt" \
255
255
  Write the prompt as an instruction to an AI agent. The prompt executes inside a sandbox with full tool access (bash, file editing, etc.), the user's configured LLM, stored secrets, and MCP server integrations. Examples:
256
256
 
257
257
  - `"Generate a weekly status report summarizing the team's GitHub activity and post it to Slack"`
258
- - `"Check the production API health endpoint every hour and alert if it returns non-200"`
259
- - `"Pull the latest data from our analytics API and update the dashboard spreadsheet"`
258
+ - `"Read yesterday's new error reports in Sentry, group them by likely root cause, and open one GitHub issue per new failure pattern with a proposed fix"`
259
+ - `"Go through support tickets tagged 'bug' since the last run, try to reproduce each against main, and comment on the ticket with what you found"`
260
+
261
+ Each of these needs judgment: deciding what counts as a pattern, whether a reproduction succeeded, what is worth reporting. A task whose output could be written as a fixed template right now belongs in a custom script instead (see *Custom Script Example (No LLM)* below).
260
262
 
261
263
  #### Cron Schedule
262
264
 
@@ -296,24 +298,105 @@ curl -X POST "${OPENHANDS_HOST}/api/automation/v1/preset/prompt" \
296
298
  }'
297
299
  ```
298
300
 
299
- **Weekly cleanup:**
301
+ **Weekly dependency review:**
300
302
  ```bash
301
303
  curl -X POST "${OPENHANDS_HOST}/api/automation/v1/preset/prompt" \
302
304
  -H "Authorization: Bearer ${OPENHANDS_API_KEY}" \
303
305
  -H "Content-Type: application/json" \
304
306
  -d '{
305
- "name": "Weekly Cleanup",
306
- "prompt": "Clean up temporary files older than 7 days and send a summary of what was removed",
307
+ "name": "Weekly Dependency Review",
308
+ "prompt": "Check the repository dependencies for new releases. Read each changelog, open one PR with the upgrades that look safe, and list any release with breaking changes in the PR description so a human can decide on it",
307
309
  "trigger": {"type": "cron", "schedule": "0 2 * * 0", "timezone": "UTC"},
308
- "timeout": 300
310
+ "timeout": 1800
309
311
  }'
310
312
  ```
311
313
 
314
+ ### Custom Script Example (No LLM)
315
+
316
+ The same shape as the prompt preset, for a task that needs no reasoning: check a URL on a schedule and post to Slack only when it fails. Every run takes a moment and costs no tokens. This is the complete flow; `get_secret` and `fire_callback` are the helpers from *No-LLM Script Helpers* above.
317
+
318
+ ```bash
319
+ mkdir healthcheck && cd healthcheck
320
+
321
+ cat > main.py << 'PYEOF'
322
+ """Hourly health check: alert Slack on a non-200 response. No LLM, no SDK."""
323
+ import json, os, sys, urllib.request
324
+
325
+ URL = "https://api.example.com/health"
326
+ CHANNEL = "C12345678"
327
+
328
+ def get_secret(name):
329
+ url = os.environ.get("AGENT_SERVER_URL", "").rstrip("/")
330
+ key = os.environ.get("SESSION_API_KEY") or os.environ.get("OH_SESSION_API_KEYS_0", "")
331
+ if url: # local Agent Canvas
332
+ url = f"{url}/api/settings/secrets/{name}"
333
+ else: # OpenHands Cloud / Enterprise
334
+ api = os.environ["OPENHANDS_CLOUD_API_URL"].rstrip("/")
335
+ url = f"{api}/api/v1/sandboxes/{os.environ['SANDBOX_ID']}/settings/secrets/{name}"
336
+ req = urllib.request.Request(url,
337
+ headers={"X-Session-API-Key": key})
338
+ with urllib.request.urlopen(req) as r:
339
+ return r.read().decode().strip()
340
+
341
+ def fire_callback(status="COMPLETED", error=None):
342
+ url = os.environ.get("AUTOMATION_CALLBACK_URL", "")
343
+ if not url: return
344
+ body = {"status": status, "run_id": os.environ.get("AUTOMATION_RUN_ID", "")}
345
+ if error: body["error"] = error
346
+ req = urllib.request.Request(url, data=json.dumps(body).encode(), headers={
347
+ "Content-Type": "application/json",
348
+ "Authorization": f"Bearer {os.environ.get('AUTOMATION_CALLBACK_API_KEY', '')}",
349
+ })
350
+ try: urllib.request.urlopen(req)
351
+ except Exception as e: print(f"Callback error: {e}")
352
+
353
+ def slack(text):
354
+ token = get_secret("SLACK_BOT_TOKEN")
355
+ req = urllib.request.Request("https://slack.com/api/chat.postMessage",
356
+ data=json.dumps({"channel": CHANNEL, "text": text}).encode(),
357
+ headers={"Content-Type": "application/json", "Authorization": f"Bearer {token}"})
358
+ urllib.request.urlopen(req)
359
+
360
+ try:
361
+ try:
362
+ status = urllib.request.urlopen(URL, timeout=10).status
363
+ except Exception as e:
364
+ status = f"error: {e}"
365
+ if status != 200:
366
+ slack(f":rotating_light: {URL} returned {status}")
367
+ print(f"{URL} -> {status}")
368
+ fire_callback("COMPLETED")
369
+ except Exception as e:
370
+ print(f"ERROR: {e}", file=sys.stderr)
371
+ fire_callback("FAILED", str(e))
372
+ sys.exit(1)
373
+ PYEOF
374
+
375
+ tar -czf ../healthcheck.tar.gz .
376
+
377
+ TARBALL_PATH=$(curl -s -X POST "${OPENHANDS_HOST}/api/automation/v1/uploads?name=healthcheck" \
378
+ -H "Authorization: Bearer ${OPENHANDS_API_KEY}" \
379
+ -H "Content-Type: application/gzip" \
380
+ --data-binary @../healthcheck.tar.gz | jq -r '.tarball_path')
381
+
382
+ curl -X POST "${OPENHANDS_HOST}/api/automation/v1" \
383
+ -H "Authorization: Bearer ${OPENHANDS_API_KEY}" \
384
+ -H "Content-Type: application/json" \
385
+ -d "{
386
+ \"name\": \"Hourly Health Check\",
387
+ \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 * * * *\", \"timezone\": \"UTC\"},
388
+ \"tarball_path\": \"$TARBALL_PATH\",
389
+ \"entrypoint\": \"python3 main.py\"
390
+ }"
391
+ ```
392
+
393
+ For state between runs (last alert time, last seen event), use the KV store helpers in `references/custom-automation.md#state-persistence-kv-store`.
394
+
312
395
  ---
313
396
 
314
397
  ## Polling as a Webhook Alternative
315
398
 
316
- When the deployment cannot receive inbound webhook traffic (see rule 5), use a cron-triggered automation that calls the external service’s API on a schedule to check for new events.
399
+ When the deployment cannot receive inbound webhook traffic (see rule 4), use a cron-triggered automation that calls the external service’s API on a schedule to check for new events.
317
400
 
318
401
  ### Polling vs. Webhooks at a Glance
319
402
 
@@ -442,15 +525,15 @@ curl -X POST "${OPENHANDS_HOST}/api/automation/v1/preset/prompt" \
442
525
  }'
443
526
  ```
444
527
 
445
- #### GitHub: Run tests on push to main
528
+ #### GitHub: Flag risky changes pushed to main
446
529
 
447
530
  ```bash
448
531
  curl -X POST "${OPENHANDS_HOST}/api/automation/v1/preset/prompt" \
449
532
  -H "Authorization: Bearer ${OPENHANDS_API_KEY}" \
450
533
  -H "Content-Type: application/json" \
451
534
  -d '{
452
- "name": "Run Tests on Main",
453
- "prompt": "Clone the repository and run the test suite. Report any failures.",
535
+ "name": "Risky Change Watch",
536
+ "prompt": "Review the diff of this push. If it touches authentication, billing, data migrations, or anything that changes behavior for existing users, open an issue that explains the risk in plain terms and mentions the code owners. Otherwise do nothing.",
454
537
  "trigger": {
455
538
  "type": "event",
456
539
  "source": "github",
@@ -942,7 +1025,7 @@ The **prompt preset** is the right default for genuinely agent-shaped work — a
942
1025
 
943
1026
  **Watch for deterministic, high-frequency patterns.** Requests like "send a daily standup reminder", "ping a healthcheck URL every minute", "post a random quote every 5 minutes", or "rotate a fact-of-the-day message" do not need an LLM. Surface this to the user explicitly with a rough cost framing (e.g. "this schedule will invoke your LLM ~288 times/day") before defaulting to a preset. As a rule of thumb, any cron tighter than hourly deserves a deliberate "should this really be agent-driven?" check.
944
1027
 
945
- **When neither preset is the right fit** (deterministic task, custom Python dependencies, non-Python entrypoint, multi-file project structure, direct SDK lifecycle control), explain the options to the user and let them decide. Do not attempt custom automation without explicit user agreement. If they choose the custom route, refer to `references/custom-automation.md`.
1028
+ **When neither preset is the right fit** (deterministic task, custom Python dependencies, non-Python entrypoint, multi-file project structure, direct SDK lifecycle control), explain the options and let the user decide, exactly as for any other automation. If they choose the custom route, follow `references/custom-automation.md` and the *Custom Script Example (No LLM)* above.
946
1029
 
947
1030
  ## Security Considerations
948
1031
 
@@ -955,7 +1038,6 @@ See `references/security.md` — also covers narrowing triggers and sender-level
955
1038
 
956
1039
  ## Reference Files
957
1040
 
958
- - **`references/custom-automation.md`** — Detailed guide for custom automations: tarball uploads, code structure (SDK and no-LLM), state persistence via the KV store, environment variables, validation rules, and complete examples. Consult this whenever you need to evaluate or recommend the custom path (including for deterministic / cost-sensitive tasks per rule 0). Only *implement* a custom automation after the user agrees to that path.
1041
+ - **`references/custom-automation.md`** — Detailed guide for custom automations: tarball uploads, code structure (SDK and no-LLM), state persistence via the KV store, environment variables, validation rules, and complete examples. Consult this whenever you need to evaluate, recommend, or build the custom path (including for deterministic / cost-sensitive tasks per rule 0).
959
1042
  - **`references/ab-testing.md`** — A/B testing for plugin automations: defining variants with weights, experiment configuration, variant selection logic, observability via conversation tags, and complete examples. Consult this when a user wants to compare plugin versions or configurations.
960
- - **`references/security.md`** — Trust boundaries: untrusted content vs. verified sender, least-privilege secrets, trigger scoping, sender authorization, pre-deploy verification. Consult whenever an automation handles external input or forwards secrets to a spawned conversation.
961
1043
  - **`references/security.md`** — Trust boundaries for automations: untrusted event content vs. verified sender, least-privilege secret scoping for spawned conversations, narrowing triggers, sender-level authorization, and verifying a script actually runs before deploying it. Consult this whenever an automation handles external/untrusted input (GitHub issues/PRs, Slack messages, any public-facing webhook) or forwards secrets to a spawned conversation.
@@ -1,13 +1,12 @@
1
1
  # Custom Automation Reference
2
2
 
3
- > **⚠️ Do NOT use this reference unless the user has explicitly requested a custom automation.** Always use the preset/prompt endpoint from the main SKILL.md first. If the preset approach cannot satisfy the requirement, explain the options to the user and let them decide.
4
-
5
3
  This file contains detailed documentation for creating custom automations with user-provided code, uploads, and entrypoints.
6
4
 
7
- **When to use custom automation (only if the user explicitly chooses this):**
5
+ **When to use a custom automation:**
6
+ - The task is deterministic and needs no LLM (fixed data, scheduled HTTP calls, templated messages), especially on a frequent schedule
8
7
  - Full control over the automation code structure is needed
9
8
  - Custom dependencies or a specific runtime are required
10
- - The user has confirmed that the prompt preset does not meet their requirements
9
+ - The user picked this path after seeing the options side by side (see the agent behavior rules in `SKILL.md`)
11
10
 
12
11
  ## Table of Contents
13
12
 
@@ -131,10 +131,11 @@ print("All done!")
131
131
  - [Secret Registry](https://docs.openhands.dev/sdk/guides/secrets.md): Provide environment variables and secrets to agent workspace securely.
132
132
  - [Security & Action Confirmation](https://docs.openhands.dev/sdk/guides/security.md): Control agent action execution through confirmation policy and security analyzer.
133
133
  - [Send Message While Running](https://docs.openhands.dev/sdk/guides/convo-send-message-while-running.md): Interrupt running agents to provide additional context or corrections.
134
- - [Software Agent SDK](https://docs.openhands.dev/sdk.md): Build AI agents that write software. A clean, modular SDK with production-ready tools.
134
+ - [Software Agent SDK](https://docs.openhands.dev/sdk/index.md): Build AI agents that write software. A clean, modular SDK with production-ready tools.
135
135
  - [Structured Output](https://docs.openhands.dev/sdk/guides/structured-output.md): Attach a schema to any tool so the LLM returns typed, validated fields alongside the tool's own arguments.
136
136
  - [Stuck Detector](https://docs.openhands.dev/sdk/guides/agent-stuck-detector.md): Detect and handle stuck agents automatically with timeout mechanisms.
137
137
  - [Task Tool Set](https://docs.openhands.dev/sdk/guides/task-tool-set.md): Delegate complex work to specialized sub-agents that run synchronously and return results to the parent agent.
138
+ - [Task-Based Model Routing](https://docs.openhands.dev/sdk/guides/llm-task-routing.md): Let the agent pick the best saved LLM profile for the task at hand with the built-in route_task_to_model tool.
138
139
  - [Theory of Mind (TOM) Agent](https://docs.openhands.dev/sdk/guides/agent-tom-agent.md): Enable your agent to understand user intent and preferences through Theory of Mind capabilities, providing personalized guidance based on user modeling.
139
140
  - [TODO Management](https://docs.openhands.dev/sdk/guides/github-workflows/todo-management.md): Implement TODOs using OpenHands Agent
140
141
 
@@ -121,6 +121,10 @@ The agent posts a PR comment with this structure:
121
121
  ### Functional Verification
122
122
  [Commands run, outputs observed, screenshots, behavior verified]
123
123
 
124
+ ### Visual Evidence (frontend PRs only)
125
+ [Screenshots of each affected state and a GIF of the key interaction, embedded
126
+ inline or saved to the run's `openhands-qa-changes-logs` artifact]
127
+
124
128
  ### Unable to Verify (if applicable)
125
129
  [What could not be verified, what was attempted, suggested AGENTS.md guidance]
126
130
 
@@ -14,5 +14,7 @@ The skill guides the agent through four phases:
14
14
 
15
15
  1. **Understand** — Classify changes and identify entry points
16
16
  2. **Setup** — Install dependencies, build, check CI status
17
- 3. **Exercise** — Run the actual code as a user would (browser, CLI, API requests)
17
+ 3. **Exercise** — Run the actual code as a user would (browser, CLI, API requests).
18
+ For frontend changes, capture screenshots of each affected state and a GIF of
19
+ the key interaction (before/after where behavior changes)
18
20
  4. **Report** — Post a structured QA report with evidence and a verdict
@@ -75,6 +75,37 @@ This is the most important phase. **Actually use the software** the way a real u
75
75
  - Verify the visual change renders correctly. Take screenshots as evidence.
76
76
  - Test user interactions (clicks, form submissions, navigation).
77
77
  - Try at least one edge case (empty state, long text, missing data).
78
+ - Then capture visual evidence and attach it to the PR (see next step).
79
+
80
+ **Capture visual evidence (frontend PRs only):**
81
+
82
+ If the PR has frontend work, run the affected screens from the branch's final
83
+ state and record what a reviewer would otherwise have to imagine from the diff.
84
+ The goal: the change can be reviewed **by observation, not by reading code.**
85
+
86
+ 1. **Screenshot each relevant state** of every affected screen - whichever
87
+ apply: *empty*, *loading*, *error*, and *populated*. Skip states the screen
88
+ genuinely cannot reach.
89
+ 2. **Record the key interaction end to end** - the main flow the PR changes,
90
+ driven the way a user would. Prefer a GIF: GitHub renders a GIF inline from
91
+ an image link, but only links a video file.
92
+ 3. **Where behavior changes, capture before/after** - the same state or
93
+ interaction on the base branch and on the PR branch, so the delta is
94
+ visible.
95
+ 4. **Attach it to the report.** Embed the images and GIF in the QA report so
96
+ they render inline, and label each one (screen and state, or before/after).
97
+ Do not push commits to the PR branch to host media: that changes the head
98
+ you are verifying, and the shipped QA workflow has read-only access to
99
+ repository contents. If you cannot upload media where the report is posted,
100
+ save the files where the run keeps its artifacts, say where to find them,
101
+ and describe in text what each capture shows. In the `qa-changes` GitHub
102
+ Action that is `$GITHUB_WORKSPACE/output/`, the parent of the PR checkout
103
+ you work in, which the action uploads as the `openhands-qa-changes-logs`
104
+ artifact; a relative `output/` inside the checkout is not uploaded.
105
+
106
+ Keep it proportional: capture the screens the PR actually touches, not the whole
107
+ app. If you cannot render a screen (missing data, an unreachable state, no
108
+ browser), say so in the report rather than faking it.
78
109
 
79
110
  **For CLI changes:**
80
111
  - Run the CLI command with realistic arguments. Capture stdout and stderr.
@@ -181,6 +212,16 @@ prior state rather than reproducing a failure.}
181
212
 
182
213
  </details>
183
214
 
215
+ <details><summary>Visual Evidence</summary>
216
+
217
+ {Frontend PRs only. Embed the screenshots (per screen and state: empty,
218
+ loading, error, populated) and the GIF of the key interaction so they
219
+ render inline, or say where the captures are stored if they could not be
220
+ uploaded. Where behavior changed, show before/after. Label each clearly.
221
+ Omit this section entirely for non-frontend PRs.}
222
+
223
+ </details>
224
+
184
225
  <details><summary>Unable to Verify</summary>
185
226
 
186
227
  {What could not be verified, what was attempted, and suggested
@@ -221,6 +262,7 @@ was verified.}
221
262
  - **Run the code, not the tests.** Execute the actual software — start servers, run CLI commands, make HTTP requests, open browsers. Do not run `pytest`, `npm test`, or equivalent test suites. That is CI's job.
222
263
  - **Do not analyze code.** Reading files and commenting on style, structure, or logic is code review's job. Your job is to exercise behavior, not read source files.
223
264
  - **Set a high bar.** If the change affects a UI, open it in a real browser. If it affects a CLI, run the actual CLI with real inputs. If it affects an API, make real HTTP requests.
265
+ - **Make frontend changes reviewable by observation.** For UI PRs, attach screenshots of each relevant state and a GIF of the key interaction (before/after where behavior changes) so a reviewer can see the change, not infer it from the diff.
224
266
  - **Test what the PR claims.** The PR description is the specification. Verify the claim, not hypothetical scenarios.
225
267
  - **Leave CI to CI.** Do not re-run tests, linters, formatters, or type checkers. Note CI status, then focus entirely on functional verification that CI cannot do.
226
268
  - **Report evidence, not opinions.** Include exact commands, outputs, and error messages — inside collapsible blocks.
@@ -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
@@ -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, conversation_id, subject, prompt, same_delivery, same_head
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, record, conversation_id, subject, prompt, same_delivery, same_head
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(self, conversation_id, prompt, same_delivery, same_head) -> str:
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:
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openhands-extensions"
3
- version = "0.27.0"
3
+ version = "0.28.0"
4
4
  description = "OpenHands extensions, plugins, and skills (Python bindings for the integration catalog)"
5
5
  requires-python = ">=3.12"
6
6
  dependencies = [
@@ -18,7 +18,7 @@ from importlib.metadata import PackageNotFoundError, version
18
18
  #: Kept in lock-step with pyproject.toml/package.json by the version test.
19
19
  #: release-please only rewrites a semver found on the annotated line itself, so
20
20
  #: the annotation must stay on the assignment; on its own line it is a no-op.
21
- _FALLBACK_VERSION = "0.27.0" # x-release-please-version
21
+ _FALLBACK_VERSION = "0.28.0" # x-release-please-version
22
22
 
23
23
  try:
24
24
  __version__: str = version("openhands-extensions")