@openhands/extensions 0.19.0 → 0.21.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 (64) hide show
  1. package/.github/workflows/check-extensions.yml +4 -0
  2. package/.github/workflows/pr-artifacts.yml +294 -0
  3. package/.github/workflows/pr-description-check.yml +4 -2
  4. package/.release-please-manifest.json +1 -1
  5. package/AGENTS.md +28 -4
  6. package/README.md +5 -2
  7. package/automations/README.md +52 -36
  8. package/automations/bundle-index.js +13 -3
  9. package/automations/catalog/custom-automation/manifest.json +229 -0
  10. package/automations/catalog/github-agents-md-maintainer/manifest.json +2 -1
  11. package/automations/catalog/github-issue-to-pr/manifest.json +3 -2
  12. package/automations/catalog/github-issue-triage/manifest.json +72 -0
  13. package/automations/catalog/github-pr-reviewer/manifest.json +3 -2
  14. package/automations/catalog/gitlab-issue-to-mr/manifest.json +124 -0
  15. package/automations/catalog/qa-changes/manifest.json +1 -1
  16. package/automations/catalog-index.js +23 -17
  17. package/automations/catalog.schema.json +81 -7
  18. package/automations/index.d.ts +52 -2
  19. package/automations/interface.json +2 -1
  20. package/integrations/catalog/gitlab.json +27 -5
  21. package/integrations/catalog/reportportal.json +76 -0
  22. package/integrations/catalog-index.js +6 -4
  23. package/marketplaces/openhands-extensions.json +36 -0
  24. package/package.json +1 -1
  25. package/pyproject.toml +2 -2
  26. package/python/openhands_extensions/_version.py +1 -1
  27. package/skills/github/scripts/github_client.py +204 -0
  28. package/skills/github-agents-md-maintainer/SKILL.md +7 -1
  29. package/skills/github-agents-md-maintainer/scripts/main.py +4 -40
  30. package/skills/github-issue-to-pr/SKILL.md +8 -1
  31. package/skills/github-issue-to-pr/scripts/main.py +3 -40
  32. package/skills/github-issue-triage/.plugin/plugin.json +9 -0
  33. package/skills/github-issue-triage/README.md +9 -0
  34. package/skills/github-issue-triage/SKILL.md +37 -0
  35. package/skills/github-issue-triage/commands/github-issue-triage.md +8 -0
  36. package/skills/github-issue-triage/scripts/worker.py +123 -0
  37. package/skills/github-pr-reviewer/SKILL.md +8 -1
  38. package/skills/github-pr-reviewer/scripts/main.py +3 -41
  39. package/skills/github-repo-monitor/SKILL.md +7 -1
  40. package/skills/github-repo-monitor/scripts/main.py +4 -46
  41. package/skills/gitlab-issue-to-mr/.plugin/plugin.json +19 -0
  42. package/skills/gitlab-issue-to-mr/README.md +92 -0
  43. package/skills/gitlab-issue-to-mr/SKILL.md +408 -0
  44. package/skills/gitlab-issue-to-mr/commands/issue-to-mr-setup.md +8 -0
  45. package/skills/gitlab-issue-to-mr/references/state-schema.md +140 -0
  46. package/skills/gitlab-issue-to-mr/scripts/main.py +1448 -0
  47. package/skills/index.js +43 -6
  48. package/skills/openhands-automation/README.md +1 -1
  49. package/skills/openhands-automation/SKILL.md +18 -8
  50. package/skills/openhands-enterprise-troubleshooting/.plugin/plugin.json +20 -0
  51. package/skills/openhands-enterprise-troubleshooting/README.md +79 -0
  52. package/skills/openhands-enterprise-troubleshooting/SKILL.md +339 -0
  53. package/skills/openhands-enterprise-troubleshooting/references/diagnostics.md +698 -0
  54. package/skills/openhands-enterprise-troubleshooting/references/support-bundle-analysis.md +570 -0
  55. package/skills/openhands-enterprise-troubleshooting/scripts/bundle_triage.py +532 -0
  56. package/skills/openhands-sdk/SKILL.md +2 -0
  57. package/tests/fixtures/automations/custom-automation.json +398 -0
  58. package/tests/fixtures/automations/github-pr-reviewer.json +9 -9
  59. package/tests/github_automation_helpers.py +25 -0
  60. package/tests/test_automation_setup.py +340 -92
  61. package/tests/test_github_automation_foundation.py +73 -0
  62. package/tests/test_github_skill_installation.py +63 -0
  63. package/tests/test_github_triage_delivery.py +53 -0
  64. package/tests/test_gitlab_issue_to_mr.py +823 -0
@@ -58,6 +58,8 @@ jobs:
58
58
  run: python scripts/sync_extensions.py --check
59
59
 
60
60
  sync-sdk-skill:
61
+ permissions:
62
+ contents: read
61
63
  runs-on: ubuntu-latest
62
64
  steps:
63
65
  - name: Checkout repository
@@ -69,4 +71,6 @@ jobs:
69
71
  python-version: "3.12"
70
72
 
71
73
  - name: Check SDK skill is up to date
74
+ env:
75
+ GITHUB_TOKEN: ${{ github.token }}
72
76
  run: python scripts/sync_openhands_sdk_skill.py --check
@@ -0,0 +1,294 @@
1
+ ---
2
+ name: PR Artifacts
3
+
4
+ on:
5
+ workflow_dispatch: # Manual trigger for testing
6
+ pull_request_target:
7
+ types: [opened, synchronize, reopened, closed]
8
+ branches: [main]
9
+ pull_request_review:
10
+ types: [submitted]
11
+
12
+ jobs:
13
+ # Auto-remove .pr/ directory from same-repository PRs when approved.
14
+ cleanup-on-approval:
15
+ concurrency:
16
+ group: cleanup-pr-artifacts-${{ github.event.pull_request.number }}
17
+ cancel-in-progress: false
18
+ if: >-
19
+ github.event_name == 'pull_request_review' &&
20
+ github.event.review.state == 'approved' &&
21
+ github.event.pull_request.head.repo.full_name == github.repository
22
+ runs-on: ubuntu-latest
23
+ permissions:
24
+ contents: write
25
+ pull-requests: write
26
+ steps:
27
+ # Use PAT so the push triggers CI workflows that will complete and
28
+ # satisfy branch protection. We can't use [skip ci] because GitHub Apps
29
+ # can create stuck checks that block merging.
30
+ - uses: actions/checkout@v7
31
+ with:
32
+ ref: ${{ github.event.pull_request.head.ref }}
33
+ token: ${{ secrets.OPENHANDS_BOT_GITHUB_PAT_PUBLIC }}
34
+
35
+ - name: Remove .pr/ directory
36
+ id: remove
37
+ run: |
38
+ if [ -d ".pr" ]; then
39
+ git config user.name "allhands-bot"
40
+ git config user.email "allhands-bot@users.noreply.github.com"
41
+ git rm -rf .pr/
42
+ git commit -m "chore: Remove PR-only artifacts [automated]"
43
+ git push || {
44
+ echo "::error::Failed to push cleanup commit. Check branch protection rules."
45
+ exit 1
46
+ }
47
+ echo "removed=true" >> "$GITHUB_OUTPUT"
48
+ echo "::notice::Removed .pr/ directory"
49
+ else
50
+ echo "removed=false" >> "$GITHUB_OUTPUT"
51
+ echo "::notice::No .pr/ directory to remove"
52
+ fi
53
+
54
+ - name: Update PR comment after cleanup
55
+ if: steps.remove.outputs.removed == 'true'
56
+ uses: actions/github-script@v9
57
+ with:
58
+ script: |
59
+ const marker = '<!-- pr-artifacts-notice -->';
60
+ const body = `${marker}
61
+ ✅ **PR Artifacts Cleaned Up**
62
+
63
+ The \`.pr/\` directory has been automatically removed.
64
+ `;
65
+
66
+ const comments = await github.paginate(
67
+ github.rest.issues.listComments,
68
+ {
69
+ owner: context.repo.owner,
70
+ repo: context.repo.repo,
71
+ issue_number: context.issue.number,
72
+ per_page: 100,
73
+ },
74
+ );
75
+
76
+ const existing = comments.find(c => c.body.includes(marker));
77
+ if (existing) {
78
+ await github.rest.issues.updateComment({
79
+ owner: context.repo.owner,
80
+ repo: context.repo.repo,
81
+ comment_id: existing.id,
82
+ body: body,
83
+ });
84
+ }
85
+
86
+ # Inspect the fork through the API; never check out untrusted code with a write token.
87
+ check-pr-artifacts:
88
+ if: >-
89
+ github.event_name == 'pull_request_target' &&
90
+ github.event.action != 'closed'
91
+ runs-on: ubuntu-latest
92
+ permissions:
93
+ contents: read
94
+ pull-requests: write
95
+ steps:
96
+ - name: Post or update PR comment
97
+ uses: actions/github-script@v9
98
+ with:
99
+ script: |
100
+ const marker = '<!-- pr-artifacts-notice -->';
101
+ const pullRequest = context.payload.pull_request;
102
+ const headRepository = pullRequest.head.repo;
103
+ let exists = true;
104
+
105
+ try {
106
+ await github.rest.repos.getContent({
107
+ owner: headRepository.owner.login,
108
+ repo: headRepository.name,
109
+ path: '.pr',
110
+ ref: pullRequest.head.sha,
111
+ });
112
+ } catch (error) {
113
+ if (error.status === 404) {
114
+ exists = false;
115
+ } else {
116
+ throw error;
117
+ }
118
+ }
119
+
120
+ const comments = await github.paginate(
121
+ github.rest.issues.listComments,
122
+ {
123
+ owner: context.repo.owner,
124
+ repo: context.repo.repo,
125
+ issue_number: context.issue.number,
126
+ per_page: 100,
127
+ },
128
+ );
129
+ const existing = comments.find(c => c.body.includes(marker));
130
+
131
+ if (!exists) {
132
+ if (existing) {
133
+ await github.rest.issues.updateComment({
134
+ owner: context.repo.owner,
135
+ repo: context.repo.repo,
136
+ comment_id: existing.id,
137
+ body: `${marker}
138
+ ✅ **PR Artifacts Cleaned Up**
139
+
140
+ The \`.pr/\` directory is no longer present.
141
+ `,
142
+ });
143
+ }
144
+ return;
145
+ }
146
+
147
+ const isFork = headRepository.full_name !== context.payload.repository.full_name;
148
+ const cleanup = isFork
149
+ ? 'Because this is a fork PR, the workflow will **open or update a cleanup PR against `main` after merge**.'
150
+ : 'The directory will be **automatically removed when the PR is approved**.';
151
+ const body = `${marker}
152
+ 📁 **PR Artifacts Notice**
153
+
154
+ This PR contains a \`.pr/\` directory with temporary PR-specific documents. ${cleanup}
155
+ `;
156
+
157
+ core.warning('.pr/ directory contains temporary PR-only artifacts');
158
+ if (existing) {
159
+ await github.rest.issues.updateComment({
160
+ owner: context.repo.owner,
161
+ repo: context.repo.repo,
162
+ comment_id: existing.id,
163
+ body: body,
164
+ });
165
+ } else {
166
+ await github.rest.issues.createComment({
167
+ owner: context.repo.owner,
168
+ repo: context.repo.repo,
169
+ issue_number: context.issue.number,
170
+ body: body,
171
+ });
172
+ }
173
+
174
+ # Fork branches cannot be modified reliably. If artifacts reach the trusted base,
175
+ # remove them through a pull request so branch protections remain enforced.
176
+ cleanup-after-merge:
177
+ concurrency:
178
+ group: cleanup-pr-artifacts-${{ github.event.pull_request.base.ref }}
179
+ cancel-in-progress: false
180
+ if: >-
181
+ github.event_name == 'pull_request_target' &&
182
+ github.event.action == 'closed' &&
183
+ github.event.pull_request.merged == true
184
+ runs-on: ubuntu-latest
185
+ permissions:
186
+ contents: write
187
+ pull-requests: write
188
+ steps:
189
+ - uses: actions/checkout@v7
190
+ with:
191
+ ref: ${{ github.event.pull_request.base.ref }}
192
+ token: ${{ secrets.OPENHANDS_BOT_GITHUB_PAT_PUBLIC }}
193
+
194
+ - name: Create or update cleanup PR
195
+ id: cleanup
196
+ env:
197
+ BASE_REF: ${{ github.event.pull_request.base.ref }}
198
+ CLEANUP_BRANCH: automation/remove-pr-artifacts
199
+ GH_TOKEN: ${{ secrets.OPENHANDS_BOT_GITHUB_PAT_PUBLIC }}
200
+ run: |
201
+ if [ ! -d ".pr" ]; then
202
+ echo "created=false" >> "$GITHUB_OUTPUT"
203
+ echo "::notice::No .pr/ directory to remove from $BASE_REF"
204
+ exit 0
205
+ fi
206
+
207
+ git config user.name "allhands-bot"
208
+ git config user.email "allhands-bot@users.noreply.github.com"
209
+ git fetch origin \
210
+ "+refs/heads/$CLEANUP_BRANCH:refs/remotes/origin/$CLEANUP_BRANCH" || true
211
+ git checkout -B "$CLEANUP_BRANCH"
212
+ git rm -rf .pr/
213
+ git commit \
214
+ -m "chore: remove merged PR artifacts" \
215
+ -m "Co-authored-by: openhands <openhands@all-hands.dev>"
216
+ git push --force-with-lease origin "HEAD:$CLEANUP_BRANCH"
217
+
218
+ cleanup_pr_url=$(gh pr list \
219
+ --base "$BASE_REF" \
220
+ --head "$CLEANUP_BRANCH" \
221
+ --state open \
222
+ --json url \
223
+ --jq '.[0].url')
224
+
225
+ if [ -z "$cleanup_pr_url" ]; then
226
+ cleanup_pr_url=$(gh pr create \
227
+ --base "$BASE_REF" \
228
+ --head "$CLEANUP_BRANCH" \
229
+ --title "chore: remove merged PR artifacts" \
230
+ --body-file - <<EOF
231
+ ## Why
232
+
233
+ Temporary \`.pr/\` artifacts reached \`$BASE_REF\` through one or more merged PRs.
234
+
235
+ ## Summary
236
+
237
+ - Remove the temporary \`.pr/\` directory from \`$BASE_REF\`.
238
+
239
+ ## How to Test
240
+
241
+ - Confirm this PR only deletes files under \`.pr/\`.
242
+ - Let the required checks validate the cleanup commit.
243
+
244
+ ---
245
+
246
+ _This PR was automatically created by the PR Artifacts workflow._
247
+ EOF
248
+ )
249
+ fi
250
+
251
+ echo "created=true" >> "$GITHUB_OUTPUT"
252
+ echo "pr_url=$cleanup_pr_url" >> "$GITHUB_OUTPUT"
253
+ echo "::notice::Cleanup PR ready: $cleanup_pr_url"
254
+
255
+ - name: Update source PR comment
256
+ if: steps.cleanup.outputs.created == 'true'
257
+ uses: actions/github-script@v9
258
+ env:
259
+ CLEANUP_PR_URL: ${{ steps.cleanup.outputs.pr_url }}
260
+ with:
261
+ script: |
262
+ const marker = '<!-- pr-artifacts-notice -->';
263
+ const body = `${marker}
264
+ 🧹 **PR Artifact Cleanup Queued**
265
+
266
+ The \`.pr/\` directory reached \`${context.payload.pull_request.base.ref}\` after merge. A cleanup PR has been opened or updated: ${process.env.CLEANUP_PR_URL}
267
+ `;
268
+
269
+ const comments = await github.paginate(
270
+ github.rest.issues.listComments,
271
+ {
272
+ owner: context.repo.owner,
273
+ repo: context.repo.repo,
274
+ issue_number: context.issue.number,
275
+ per_page: 100,
276
+ },
277
+ );
278
+ const existing = comments.find(c => c.body.includes(marker));
279
+
280
+ if (existing) {
281
+ await github.rest.issues.updateComment({
282
+ owner: context.repo.owner,
283
+ repo: context.repo.repo,
284
+ comment_id: existing.id,
285
+ body: body,
286
+ });
287
+ } else {
288
+ await github.rest.issues.createComment({
289
+ owner: context.repo.owner,
290
+ repo: context.repo.repo,
291
+ issue_number: context.issue.number,
292
+ body: body,
293
+ });
294
+ }
@@ -17,13 +17,15 @@ jobs:
17
17
  validate-pr-description:
18
18
  name: Validate PR description
19
19
  # Draft PRs may still have incomplete descriptions; validate when review starts.
20
- # Release PRs are generated by the trusted prepare-release workflow without
20
+ # Release and artifact-cleanup PRs are generated by trusted workflows without
21
21
  # the standard human-authored PR template.
22
22
  if: >-
23
23
  github.event.pull_request.draft == false &&
24
24
  github.event.pull_request.user.login != 'dependabot[bot]' &&
25
25
  !(github.event.pull_request.head.repo.full_name == github.repository &&
26
- startsWith(github.event.pull_request.head.ref, 'release-please-'))
26
+ (startsWith(github.event.pull_request.head.ref, 'release-please-') ||
27
+ (github.event.pull_request.base.ref == 'main' &&
28
+ github.event.pull_request.head.ref == 'automation/remove-pr-artifacts')))
27
29
  runs-on: ubuntu-latest
28
30
  steps:
29
31
  - name: Checkout trusted workflow scripts
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.19.0"
2
+ ".": "0.21.0"
3
3
  }
package/AGENTS.md CHANGED
@@ -10,12 +10,11 @@ This repository owns the public registry of reusable OpenHands skills, plugins,
10
10
 
11
11
  Related repositories have distinct responsibilities:
12
12
 
13
- - [`OpenHands/software-agent-sdk`](https://github.com/OpenHands/software-agent-sdk) owns the Python SDK, Agent Server, agent/tool behavior, conversations, workspaces, events, and canonical API.
14
- - [`OpenHands/typescript-client`](https://github.com/OpenHands/typescript-client) owns the browser-compatible typed Agent Server client.
13
+ - [`OpenHands/software-agent-sdk`](https://github.com/OpenHands/software-agent-sdk) owns the Python SDK, Agent Server, agent/tool behavior, conversations, workspaces, events, canonical API, and browser-compatible TypeScript Agent Server client under `clients/typescript/`.
15
14
  - [`OpenHands/OpenHands`](https://github.com/OpenHands/OpenHands) owns Agent Canvas UI and local-stack orchestration.
16
15
  - [`OpenHands/automation`](https://github.com/OpenHands/automation) owns scheduling, webhooks, run history, dispatch, and sandbox lifecycle orchestration.
17
16
 
18
- Put reusable skills, plugins, automations, and integrations here; put backend execution behavior in the SDK, typed API access in `typescript-client`, application UI in Agent Canvas, and scheduling/dispatch lifecycle code in `automation`. If a PR is opened in the wrong repository, explicitly recommend closing and moving it to the owning repository. PRs must follow this repository's applicable contribution and code-review guidance.
17
+ Put reusable skills, plugins, automations, and integrations here; put backend execution behavior and typed API access in the SDK, application UI in Agent Canvas, and scheduling/dispatch lifecycle code in `automation`. If a PR is opened in the wrong repository, explicitly recommend closing and moving it to the owning repository. PRs must follow this repository's applicable contribution and code-review guidance.
19
18
 
20
19
  ## What this repo contains
21
20
 
@@ -106,7 +105,7 @@ When editing or adding skills in this repo, follow these rules (and add new skil
106
105
  - If you change a skill’s behavior or scope, update its `README.md` (if present) accordingly.
107
106
  - If you change top-level documentation, ensure links still resolve.
108
107
  - `integrations/catalog/*.json` is the single hand-authored source of truth consumed by `@openhands/extensions`; adding or editing an integration should require changing exactly one JSON file in that directory. Do not reintroduce `integrations/integration-catalog.json`, separate provider files, per-language catalog duplicates, or provider-specific runtime code. Run `npm run build:integrations` after catalog edits to regenerate `integrations/catalog-index.js`, which statically imports each individual JSON file for the JS package. The Python package includes the same individual JSON files via wheel data and reads them directly. Agent-canvas and integrations-hub import this package directly, so integration marketplace fixes belong here rather than in app-local constants. When upstream MCP projects move repos, verify both `docsUrl` and the connection option (`transport`, `command`/`args`, or URL), not just links. JS exposes `listIntegrationCatalog({ mcp, oauth })` / `getIntegrationCatalogEntry`; Python mirrors with `list_integration_catalog_models(mcp=, oauth=)` / `get_integration_catalog_entry_model`, which return validated Pydantic models (the raw-dict accessors were deprecated in 0.10.0 and removed in 0.12.0). The legacy provider-catalog compatibility layer and `managedConnectorMigration` / `legacyScopeBundles` / `canonicalServerUrl` / `errorHints` mechanisms were removed intentionally; providers declare only standard OAuth config as integration data.
109
- - `automations/catalog/<id>/` follows the same rule, as a directory per automation so an automation can ship the scripts it uploads to the automations service alongside its metadata. `manifest.json` is the single hand-authored file, carrying both the card metadata and an optional nested `setup` block (the extension-owned configuration experience). Every entry is validated against `automations/catalog.schema.json`. Run `npm run build:automations` after catalog edits to regenerate `automations/catalog-index.js`. JS exposes `listAutomationCatalog()` / `getAutomationCatalogEntry(id)` alongside `AUTOMATION_CATALOG`. The governing rule is that a manifest states only what varies between automations, and states it once; anything derivable is absent and the host generates it. Derived, so never written in a manifest: the command that launches the card (read from the `triggers:` frontmatter of the skill named by `skill`, which defaults to `id` and is stated only where the two differ - editing an automation must never require editing a skill), the setup route (`/automations/new/<id>`), the trigger kinds a deployment must support (the keys of `setup.form.triggers`), schedule limits and timezone lists (the `cron` and `timezone` field types), the preflight call, the create request's `name`/`repos`/`trigger` (rebuilt from the entry name, the repo-picker field and the keys and fields under `setup.form.triggers`, where a field is named after the property it fills), the mapping from a rejected payload path back to the input at fault, the review screen, the create endpoint, the post-success navigation, and the analytics stages. `requires.integrations` is keyed by integration id and lives on the entry, not in `setup`, because a card lists its integrations whether or not it ships a setup flow; there is no key anywhere for a credential, because the credential comes with the connection. `setup.form` splits inputs into `triggers` (keyed by trigger kind - what decides when the automation runs) and `args` (everything else), both keyed by field name. `setup` declares a `prompt` (plus a `filter` for an event trigger) when `mode` is `direct`, and a `message` when it is `assisted`. Contract fixtures are test vectors, not catalog data: they live in `tests/fixtures/automations/`, are optional per automation, and are reachable only through the `@openhands/extensions/testing/automations/*.json` subpath - never from the runtime entry point.
108
+ - `automations/catalog/<id>/` follows the same rule, as a directory per automation so an automation can ship the scripts it uploads to the automations service alongside its metadata. `manifest.json` is the single hand-authored file, carrying both the card metadata and an optional nested `setup` block (the extension-owned configuration experience). Every entry is validated against `automations/catalog.schema.json`. Run `npm run build:automations` after catalog edits to regenerate `automations/catalog-index.js`. JS exposes `listAutomationCatalog()` / `getAutomationCatalogEntry(id)` alongside `AUTOMATION_CATALOG`. The governing rule is that a manifest states only what varies between automations, and states it once; anything derivable is absent and the host generates it. Derived, so never written in a manifest: the command that launches the card (read from the `triggers:` frontmatter of the skill named by `skill`, which defaults to `id` and is stated only where the two differ - editing an automation must never require editing a skill), the setup route (`/automations/new/<id>`), the trigger variants a deployment can offer (the keys of `setup.form.triggers`; multiple keys mean any supported selected variant can run), schedule limits, timezone lists, event choices, and model profiles (the `cron`, `timezone`, `event-source`, `event-type`, and `llm-profile` field types), the preflight call, the create request's `name`/`repos`/`model`/`timeout`/`trigger` (rebuilt from same-named form fields when present, the repo-picker field, and the selected key and fields under `setup.form.triggers`, where a field is named after the property it fills), the mapping from a rejected payload path back to the input at fault, the review screen, the create endpoint, the post-success navigation, and the analytics stages. `requires.integrations` is keyed by integration id and lives on the entry, not in `setup`, because a card lists its integrations whether or not it ships a setup flow; there is no key anywhere for a credential, because the credential comes with the connection. `setup.form` splits inputs into `triggers` (keyed by trigger kind - what decides when the automation runs) and `args` (everything else), both keyed by field name. `setup` declares a `prompt` (plus a `filter` for an event trigger) when `mode` is `direct`, and a `message` when it is `assisted`. Contract fixtures are test vectors, not catalog data: they live in `tests/fixtures/automations/`, are optional per automation, and are reachable only through the `@openhands/extensions/testing/automations/*.json` subpath - never from the runtime entry point.
110
109
  - For Python test runs, prefer `uv sync --group test` followed by `uv run pytest -q`; the full suite depends on `openhands-sdk`, which is not available in the base environment.
111
110
  - Agent-driven plugins (for example `plugins/pr-review` and `plugins/release-notes`) use `uv run --with openhands-sdk --with openhands-tools ...` and require an `LLM_API_KEY` in addition to `GITHUB_TOKEN`.
112
111
  - For OpenHands Cloud API guidance, automations, and CLI integration, use `plugins/openhands`. It is the canonical unified OpenHands plugin covering the V1 Cloud API, Automations API, and CLI. The individual skills (`skills/openhands-api`, `skills/openhands-automation`) are also available standalone.
@@ -118,6 +117,31 @@ When editing or adding skills in this repo, follow these rules (and add new skil
118
117
  - `plugins/release-notes` now has a standalone validator at `plugins/release-notes/scripts/validate_release_notes.py`; it rebuilds the deterministic tag-range context, fails if a change bullet omits explicit PR/commit refs or matching author handles, and enforces full PR/author coverage by appending a compact `### 🔎 Small Fixes/Internal Changes` appendix grouped by author when the agent omits lower-signal items. New contributor detection in `generate_release_notes.py` should use merged PR history for human authors (excluding bots) rather than commit-author lookup.
119
118
 
120
119
 
120
+ ## PR-specific documents (`.pr/`)
121
+
122
+ When working on a PR that requires design documents, live-test logs, development-only scripts, or other temporary artifacts that should **not** be merged to `main`, store them in a `.pr/` directory at the repository root.
123
+
124
+ ```bash
125
+ mkdir -p .pr
126
+
127
+ .pr/
128
+ ├── design.md # Design decisions and architecture notes
129
+ ├── analysis.md # Investigation or debugging notes
130
+ └── notes.md # Any other PR-specific content
131
+ ```
132
+
133
+ The `PR Artifacts` workflow (`.github/workflows/pr-artifacts.yml`) owns the lifecycle of this directory:
134
+
135
+ 1. **Notification**: When a PR contains `.pr/`, a single comment is posted to the PR conversation alerting reviewers.
136
+ 2. **Auto-cleanup on approval**: For same-repository PRs, the directory is automatically removed by a follow-up commit when the PR is approved.
137
+ 3. **Post-merge cleanup**: If artifacts reach `main`, including through a fork PR, the workflow opens or updates a cleanup PR against `main`.
138
+
139
+ Important notes:
140
+
141
+ - Do not put anything in `.pr/` that needs to be preserved.
142
+ - The `.pr/` check is informational during development; it posts a notice rather than blocking the PR.
143
+ - Cleanup PRs follow the normal review and required-check protections for `main`.
144
+
121
145
  ## CI / validation gotchas
122
146
 
123
147
  - The test suite expects **every directory under `skills/`** to be listed in a marketplace. If you add a new skill (or rebase onto a main branch that added skills), update the appropriate marketplace file or CI will fail with `Skills missing from marketplace: [...]`.
package/README.md CHANGED
@@ -96,7 +96,7 @@ The JS and Python versions are kept in lock-step by `release-please` and guarded
96
96
  ## Extensions Catalog
97
97
 
98
98
  <!-- BEGIN AUTO-GENERATED CATALOG -->
99
- This repository contains **2 marketplace(s)** with **68 extensions** (58 skills, 10 plugins).
99
+ This repository contains **2 marketplace(s)** with **71 extensions** (61 skills, 10 plugins).
100
100
 
101
101
  ### large-codebase
102
102
 
@@ -115,7 +115,7 @@ OpenHands skills for interacting, improving, and refactoring large codebases
115
115
 
116
116
  Official skills and plugins for OpenHands — the open-source AI software engineer.
117
117
 
118
- **64 extensions** (56 skills, 8 plugins)
118
+ **67 extensions** (59 skills, 8 plugins)
119
119
 
120
120
  | Name | Type | Description | Commands |
121
121
  |------|------|-------------|----------|
@@ -142,10 +142,12 @@ Official skills and plugins for OpenHands — the open-source AI software engine
142
142
  | github-actions | skill | Create, debug, and test GitHub Actions workflows and custom actions. Use when building CI/CD pipelines, automating wo... | — |
143
143
  | github-agents-md-maintainer | skill | Create an automation that keeps AGENTS.md current in one or more GitHub repositories. On a schedule an agent reads th... | `/agents-md:setup` |
144
144
  | github-issue-to-pr | skill | Create an automation that implements GitHub issues when a configurable trigger label is applied. Clones the default b... | `/issue-to-pr:setup` |
145
+ | github-issue-triage | skill | Prioritize open issues and establish acceptance criteria before marking them ready for development. | `/github-issue-triage` |
145
146
  | github-pr-review | skill | Post structured PR reviews to GitHub with inline comments/suggestions in a single API call. | `/github-pr-review` |
146
147
  | github-pr-reviewer | skill | Create an automation that reviews GitHub pull requests when they are opened or updated. Inspects the diff, changed fi... | `/pr-reviewer:setup` |
147
148
  | github-repo-monitor | skill | Create a cron automation that polls a GitHub repository for issue and PR comments containing a configurable trigger p... | `/github-monitor:poll` |
148
149
  | gitlab | skill | Interact with GitLab repositories, merge requests, and APIs using the GITLAB_TOKEN environment variable. Use when wor... | — |
150
+ | gitlab-issue-to-mr | skill | Create an automation that implements GitLab issues when a configurable trigger label is applied. Clones the default b... | `/issue-to-mr:setup` |
149
151
  | incident-retrospective | skill | Create an automation that drafts incident retrospectives by gathering incident-channel messages from Slack, collectin... | `/incident-retro:setup` |
150
152
  | iterate | skill | Iterate on a GitHub pull request — drive it through CI, code review, and QA until merge-ready. Monitors state, fixes ... | `/iterate`, `/verify`, `/babysit` |
151
153
  | jira-issue-to-pr | skill | Deploy a cron-based OpenHands automation that watches a Jira Cloud project for issues labeled with a configurable lab... | — |
@@ -162,6 +164,7 @@ Official skills and plugins for OpenHands — the open-source AI software engine
162
164
  | openhands | plugin | Unified OpenHands plugin — bundles Cloud CLI, REST API (openhands-api), and Automations (openhands-automation) into a... | `/openhands-cloud` |
163
165
  | openhands-api | skill | Use the OpenHands Cloud REST API (V1) and agent-server APIs to create and manage Cloud or local backend conversations... | — |
164
166
  | openhands-automation | skill | Create and manage OpenHands automations - scheduled tasks that run in sandboxes. Use the prompt preset to create auto... | `/automation:create` |
167
+ | openhands-enterprise-troubleshooting | skill | Diagnose and resolve common issues on OpenHands Enterprise self-hosted installations. Covers sandbox startup failures... | — |
165
168
  | openhands-sdk | skill | Reference skill for the OpenHands Software Agent SDK - build AI agents with custom tools, LLM configuration, conversa... | `/sdk` |
166
169
  | pdflatex | skill | Install and use pdflatex to compile LaTeX documents into PDFs on Linux. Use when generating academic papers, research... | — |
167
170
  | plain-english-content | skill | Write and edit clear, accessible prose in a plain English content style: active voice, front-loaded content, sentence... | — |
@@ -42,16 +42,17 @@ two records of the same fact from drifting apart, and it is why the file is as s
42
42
  | The setup route | `/automations/new/<id>` |
43
43
  | The integrations the card lists | `requires.integrations`, which every entry carries |
44
44
  | The capabilities endpoint | The host queries it for every automation |
45
- | The trigger kinds a deployment must support | The keys of `setup.form.triggers` |
46
- | Schedule limits and the timezone list | The `cron` and `timezone` field types |
47
- | Local validation rules | The `required` flag and `constraints` on each field |
45
+ | The trigger variants a deployment can offer | The keys of `setup.form.triggers`; an entry with multiple keys is usable when at least one variant is supported |
46
+ | Schedule limits, timezone list, event choices, and model profiles | The `cron`, `timezone`, `event-source`, `event-type`, and `llm-profile` field types |
47
+ | Local validation rules | The `required` flag and `constraints` on each field, applied to the selected trigger variant |
48
48
  | The preflight call | `POST /v1/validate` with the entry id, the create endpoint, and the rendered payload |
49
- | The created automation's name | The entry's `name`, plus the repository that was picked |
50
- | `repos` in the create request | The repo-picker field, its declared `provider`, and a field named `ref` if there is one |
51
- | `trigger` in the create request | The key under `form.triggers`, and the fields under it named after trigger properties |
49
+ | The created automation's name | A form field named `name`, or the entry's `name` plus the repository that was picked |
50
+ | `repos` in the create request | The repo-picker field, its declared `provider`, and a non-empty field named `ref` if there is one |
51
+ | `model` and `timeout` in the create request | Same-named form fields when they have values |
52
+ | `trigger` in the create request | The selected key under `form.triggers`, and the fields under it named after trigger properties |
52
53
  | Which input a rejected payload path belongs to | Rebuilding that body with each field standing in for its own value |
53
54
  | The review screen | The fields and their labels |
54
- | The create endpoint | `POST /v1/preset/prompt`, or `POST /v1` for an entry that ships a bundle |
55
+ | The create endpoint | `POST /v1/preset/prompt`, `POST /v1/preset/plugin`, or `POST /v1` for an uploaded or bundled tarball |
55
56
  | The files a bundle packs | `setup.bundle.files`, read from this repository at build time |
56
57
  | Where a success navigates | The created automation, or the started conversation |
57
58
  | The analytics stages | The same stages for every automation |
@@ -64,7 +65,7 @@ checks them against the recorded fixtures, so these deletions stay honest rather
64
65
  ```jsonc
65
66
  {
66
67
  "id": "github-pr-reviewer",
67
- "name": "GitHub Code Review Agent",
68
+ "name": "GitHub code review",
68
69
  "category": "Code review",
69
70
  "description": "...",
70
71
  "requires": {
@@ -97,32 +98,40 @@ three entries whose names differ from their skill state it. The command that lau
97
98
  repeated here: it lives once, in the skill's own `triggers:` frontmatter, and the skills catalog exposes it.
98
99
  That way a skill can rename its trigger without leaving a stale copy behind in this catalog.
99
100
 
100
- `setup` is optional. Three of eight entries carry one today. It never repeats `id`, `name`, `category`, or
101
- `description`. `setup.version` selects how the block is interpreted; a future format ships a new constant.
101
+ `setup` is optional. Entries that carry one never repeat `id`, `name`, `category`, or `description`.
102
+ `setup.version` selects how the block is interpreted; a future format ships a new constant.
102
103
 
103
104
  ### Triggers and args
104
105
 
105
106
  `form` separates the two things a user is configuring, and both are keyed by field name:
106
107
 
107
108
  - **`form.triggers`** decides *when* the automation runs, keyed by trigger kind (`cron` or `event`).
108
- `github-pr-reviewer` and `github-repo-monitor` each ask for a schedule and a timezone.
109
- - **`form.args`** is everything else: the arguments to the automation itself, such as the repository to
110
- clone and the tone of the review.
109
+ If an entry declares more than one key, the host renders those keys as selectable variants and validates
110
+ only the selected variant's fields before building the payload.
111
+ - **`form.args`** is everything else that is common to every creation path, such as the automation name,
112
+ model profile, or timeout.
113
+ - **`setup.actions`** is optional and declares user-selectable creation paths for one card. The host renders
114
+ the common form plus the selected action's `args`, checks that action's `features`, and creates through
115
+ the endpoint implied by the action key: `prompt` -> `/v1/preset/prompt`, `plugin` -> `/v1/preset/plugin`,
116
+ and `upload` -> `/v1/uploads` followed by `/v1`.
111
117
 
112
118
  An assisted entry declares no triggers, because the trigger is settled during the conversation.
113
119
 
114
120
  ### What the form produces
115
121
 
116
- - **`mode: "direct"`** declares a `prompt`: what the automation is told to do. The rest of the create
117
- request restates the form, so it is not written out. An event trigger also declares a `filter`, because
118
- composing form values into a JMESPath expression is the one part of an event trigger that cannot be read
119
- off the form.
122
+ - **`mode: "direct"`** declares exactly one creation archetype: a `prompt`, a repository-shipped `bundle`,
123
+ or `actions`. A prompt creates through `/v1/preset/prompt`. A bundle is packed and uploaded by the host,
124
+ then creates through `/v1`. An actions block lets the user choose between prompt, plugin, and uploaded
125
+ tarball paths in one card. The rest of the create request restates the form, so it is not written out. A
126
+ form field named `name`, `model`, or `timeout` fills the matching top-level create property. An event
127
+ trigger may also declare a `filter`, because composing form values into a JMESPath expression is the one
128
+ part of an event trigger that cannot always be read off the form.
120
129
  - **`mode: "assisted"`** declares a `message`: setup context handed to an agent conversation that finishes
121
130
  the job. The command that opens that conversation comes from the skill, so it is not repeated here.
122
131
 
123
132
  A direct entry may also declare a `message`. It is the seed for the fallback conversation the host offers
124
- when the deployment cannot run the direct path - a deployment whose capabilities lack the entry's trigger
125
- kind or required features. The same 2000-character cap applies, and because the fallback fires before the
133
+ when the deployment cannot run the direct path - a deployment whose capabilities lack every declared trigger
134
+ variant or required features. The same 2000-character cap applies, and because the fallback fires before the
126
135
  form is trustworthy, a direct `message` should not reference `{{form.*}}`.
127
136
 
128
137
  A `repo-picker` may declare `multiple: true`, and then it collects several
@@ -135,17 +144,17 @@ repository when there is one and after the count when there are several, since a
135
144
  list of names does not fit a name.
136
145
 
137
146
  A form field is named after the property it fills. `schedule` and `timezone` under `triggers.cron` become
138
- `trigger.schedule` and `trigger.timezone`; `on` under `triggers.event` becomes `trigger.on`; a field named
139
- `ref` becomes `repos[].ref`. Any other field under a trigger kind, such as a phrase to match, is an input
140
- to `filter` rather than a trigger property.
147
+ `trigger.schedule` and `trigger.timezone`; `source` and `on` under `triggers.event` become `trigger.source`
148
+ and `trigger.on`; a field named `ref` becomes `repos[].ref` when it has a value. Any other field under a
149
+ trigger kind, such as a phrase to match, is an input to `filter` rather than a trigger property.
141
150
 
142
151
  ### Entries that ship a script
143
152
 
144
- `mode: "direct"` produces either a `prompt` or a `bundle`, never both. A prompt
145
- is the right shape when the automation *is* the judgement: the agent reads the
146
- prompt and does the work. A bundle is the right shape when most of what the
147
- automation does is deterministic machinery - polling, dedupe, state, fixed API
148
- calls - and the agent is needed only for the part that genuinely needs judgement.
153
+ `mode: "direct"` produces exactly one of `prompt`, `bundle`, or `actions`. A
154
+ prompt is the right shape when the automation *is* the judgement: the agent
155
+ reads the prompt and does the work. A bundle is the right shape when most of
156
+ what the automation does is deterministic machinery - polling, dedupe, state,
157
+ fixed API calls - and the agent is needed only for the part that genuinely needs judgement.
149
158
  `github-pr-reviewer` is the first: its script owns discovery, label-event
150
159
  dedupe, per-repo state and the review checkout, and starts a conversation only
151
160
  once a pull request actually needs reviewing.
@@ -188,7 +197,10 @@ Two things follow from the archetype rather than being stated:
188
197
  A bundle declares `requires.features: ["customTarball"]`: a deployment that
189
198
  cannot run a client-supplied tarball cannot run the entry, whatever trigger kinds
190
199
  it offers. It never declares `repos` - the raw create endpoint has no such field,
191
- and a bundle fetches what it needs itself.
200
+ and a bundle fetches what it needs itself. In an `actions` entry, creation-path
201
+ features move down to each action (`presetPrompt`, `presetPlugin`, or
202
+ `customTarball`) so a deployment can offer the supported choices without needing
203
+ every creation path.
192
204
 
193
205
  ### Format constraints
194
206
 
@@ -208,17 +220,19 @@ trust boundary. It enforces:
208
220
  Placeholders are namespaced and the schema rejects any other namespace: `{{form.*}}` for what the user
209
221
  entered and `{{automation.*}}` for the entry itself. There is deliberately no secrets namespace.
210
222
 
211
- ### The three archetypes
223
+ ### The archetypes
212
224
 
213
225
  | Entry | Archetype | Trigger | Produces |
214
226
  | --- | --- | --- | --- |
215
227
  | `github-pr-reviewer` | Direct scheduled, script bundle | `cron` | an upload, then a create payload |
216
228
  | `github-repo-monitor` | Direct scheduled | `cron` | a create payload |
229
+ | `qa-changes` | Direct event | `event` | a create payload |
230
+ | `custom-automation` | Direct custom action with prompt, plugin, or upload variants | `cron` or `event` | a create payload, or an upload then create payload |
217
231
  | `incident-retrospective-drafter` | Assisted conversation | decided during the conversation | a seed message |
218
232
 
219
- An event archetype is still expressible - `github-repo-monitor` used one until its deployment could no
220
- longer receive webhooks, and was converted to the polled `cron` form. The schema keeps supporting `event`
221
- triggers for deployments that can. See the `event` key under `setup.form.triggers` in `catalog.schema.json`.
233
+ An entry can declare both `cron` and `event` under `setup.form.triggers`. The host treats those keys as
234
+ selectable variants and creates the payload from the selected one; a deployment that only supports one of
235
+ the variants can still offer that supported path.
222
236
 
223
237
  The assisted archetype has no payload and no preflight, because at the end of its flow no automation exists
224
238
  yet. The agent creates it during the conversation, and the service validates it there. That is the defining
@@ -300,9 +314,11 @@ import scenarios from "@openhands/extensions/testing/automations/github-pr-revie
300
314
  must result. That pairing is the contract: form shape and API shape genuinely differ, the create endpoint
301
315
  is declared `extra="forbid"`, and a mapping mistake is a 422 discovered only at creation time.
302
316
 
303
- Each scenario carries whichever blocks apply: `formValues`, `integrationState`, `localValidation`,
304
- `preflight`, `create`, `conversation`, `expectedFieldErrors`, `expectedPrerequisiteOutcome`.
305
- `capabilities.json` holds three deployment shapes so the unsupported paths have coverage too.
317
+ Each scenario carries whichever blocks apply: `formValues`, `selectedTrigger`, `integrationState`,
318
+ `localValidation`, `preflight`, `create`, `conversation`, `expectedFieldErrors`,
319
+ `expectedPrerequisiteOutcome`. `selectedTrigger` is required when an entry declares multiple trigger
320
+ variants and the scenario records a create or preflight payload. `capabilities.json` holds deployment
321
+ shapes so the unsupported paths have coverage too.
306
322
 
307
323
  Beyond the derivation checks, every request body here has been verified against the live Pydantic models in
308
324
  `OpenHands/automation`:
@@ -359,7 +375,7 @@ questions to this work. What changed and why:
359
375
  | `analytics` | Removed. The same stages fire for every automation, so they belong in shared host code. |
360
376
  | `workflow.steps` | Removed. It restated which keys were present, creating a second source of truth that could contradict the file. |
361
377
  | `form.intent: "seed"` | Removed. Derivable from `setup.mode: "assisted"`. |
362
- | `triggerKindsAnyOf` | Removed. The keys of `form.triggers` are the trigger kinds. |
378
+ | `triggerKindsAnyOf` | Removed. The keys of `form.triggers` are the available trigger variants, and multiple keys already mean the host can offer any supported one. |
363
379
  | `{{form.filledCount}}` | Removed. It overloaded the `form` namespace with a computed value that names no field. |
364
380
  | `submit.message` | Kept as `setup.message`, capped at 2000 characters, so seed messages cannot grow back into the giant runtime prompts that recommended automation cards were already fixed to stop sending. |
365
381