@openhands/extensions 0.17.0 → 0.19.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 (76) hide show
  1. package/.agents/skills/custom-codereview-guide.md +7 -0
  2. package/.github/dependabot.yml +24 -0
  3. package/.github/scripts/check_issue_readiness.py +272 -0
  4. package/.github/scripts/check_pr_description.py +226 -0
  5. package/.github/scripts/post-readiness-comment.mjs +184 -0
  6. package/.github/scripts/refresh_linked_pr_checks.py +171 -0
  7. package/.github/workflows/issue-readiness-check.yml +142 -0
  8. package/.github/workflows/pr-description-check.yml +37 -0
  9. package/.release-please-manifest.json +1 -1
  10. package/AGENTS.md +16 -0
  11. package/README.md +13 -2
  12. package/automations/README.md +3 -2
  13. package/automations/bundle-index.js +10 -1
  14. package/automations/catalog/github-agents-md-maintainer/manifest.json +101 -0
  15. package/automations/catalog/github-issue-to-pr/manifest.json +112 -0
  16. package/automations/catalog/github-pr-reviewer/manifest.json +6 -0
  17. package/automations/catalog/github-repo-monitor/manifest.json +6 -0
  18. package/automations/catalog/jira-issue-to-bitbucket-pr/manifest.json +23 -0
  19. package/automations/catalog/jira-issue-to-gitlab-mr/manifest.json +23 -0
  20. package/automations/catalog/linear-issue-to-bitbucket-pr/manifest.json +23 -0
  21. package/automations/catalog/linear-issue-to-github-pr/manifest.json +23 -0
  22. package/automations/catalog/linear-issue-to-gitlab-mr/manifest.json +23 -0
  23. package/automations/catalog/news-digest/manifest.json +81 -0
  24. package/automations/catalog/qa-changes/manifest.json +72 -0
  25. package/automations/catalog/slack-github-linear-daily-organization.json +27 -0
  26. package/automations/catalog-index.js +25 -7
  27. package/automations/catalog.schema.json +35 -2
  28. package/automations/index.d.ts +34 -1
  29. package/automations/interface.json +3 -1
  30. package/integrations/catalog/datadog.json +2 -2
  31. package/integrations/catalog/gitlab.json +41 -0
  32. package/integrations/catalog/notion.json +1 -1
  33. package/integrations/catalog-index.js +71 -69
  34. package/integrations/index.d.ts +1 -1
  35. package/marketplaces/openhands-extensions.json +59 -6
  36. package/package.json +1 -1
  37. package/plugins/pr-review/scripts/prompt.py +2 -0
  38. package/pyproject.toml +1 -1
  39. package/python/openhands_extensions/_version.py +1 -1
  40. package/skills/github-agents-md-maintainer/.plugin/plugin.json +19 -0
  41. package/skills/github-agents-md-maintainer/README.md +59 -0
  42. package/skills/github-agents-md-maintainer/SKILL.md +271 -0
  43. package/skills/github-agents-md-maintainer/commands/agents-md-setup.md +8 -0
  44. package/skills/github-agents-md-maintainer/references/state-schema.md +113 -0
  45. package/skills/github-agents-md-maintainer/scripts/main.py +1187 -0
  46. package/skills/github-issue-to-pr/.plugin/plugin.json +19 -0
  47. package/skills/github-issue-to-pr/README.md +84 -0
  48. package/skills/github-issue-to-pr/SKILL.md +373 -0
  49. package/skills/github-issue-to-pr/commands/issue-to-pr-setup.md +8 -0
  50. package/skills/github-issue-to-pr/references/state-schema.md +139 -0
  51. package/skills/github-issue-to-pr/scripts/main.py +1305 -0
  52. package/skills/github-pr-reviewer/SKILL.md +2 -1
  53. package/skills/github-pr-reviewer/scripts/main.py +45 -4
  54. package/skills/github-pr-reviewer/tests/test_main.py +74 -0
  55. package/skills/index.js +38 -2
  56. package/skills/news-digest/.plugin/plugin.json +19 -0
  57. package/skills/news-digest/README.md +71 -0
  58. package/skills/news-digest/SKILL.md +284 -0
  59. package/skills/news-digest/commands/news-digest-setup.md +8 -0
  60. package/skills/news-digest/references/state-schema.md +178 -0
  61. package/skills/news-digest/scripts/main.py +1250 -0
  62. package/skills/openhands-sdk/SKILL.md +1 -0
  63. package/skills/ticket-to-code-change/.plugin/plugin.json +21 -0
  64. package/skills/ticket-to-code-change/README.md +5 -0
  65. package/skills/ticket-to-code-change/SKILL.md +50 -0
  66. package/skills/ticket-to-code-change/commands/ticket-to-code-change-setup.md +8 -0
  67. package/tests/fixtures/automations/github-repo-monitor.json +77 -0
  68. package/tests/test_automation_setup.py +56 -0
  69. package/tests/test_catalogs.py +5 -3
  70. package/tests/test_check_issue_readiness.py +135 -0
  71. package/tests/test_check_pr_description.py +228 -0
  72. package/tests/test_github_agents_md_maintainer.py +352 -0
  73. package/tests/test_github_issue_to_pr.py +539 -0
  74. package/tests/test_news_digest.py +646 -0
  75. package/tests/test_pr_review_prompt.py +10 -0
  76. package/tests/test_refresh_linked_pr_checks.py +130 -0
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "github-issue-to-pr",
3
+ "version": "1.0.0",
4
+ "description": "Create an automation that implements GitHub issues when a configurable trigger label is applied. It clones the default branch, starts one OpenHands conversation per label event, then commits, pushes, and opens the pull request itself, without ever handing the agent a GitHub credential.",
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
+ "issue",
15
+ "pull-request",
16
+ "automation",
17
+ "integration"
18
+ ]
19
+ }
@@ -0,0 +1,84 @@
1
+ # GitHub Issue to PR
2
+
3
+ Create an automation that implements GitHub issues when a configurable trigger
4
+ label is applied, and opens the pull request for you.
5
+
6
+ ## Trigger
7
+
8
+ This skill is activated by:
9
+
10
+ - `/issue-to-pr:setup`
11
+
12
+ ## Features
13
+
14
+ - Implements an issue on demand by watching for a GitHub label event
15
+ - Watches several repositories from a single automation, each with its own state
16
+ - Processes each label application exactly once, with persistent state
17
+ - Re-runs on demand by removing and re-applying the label, on a fresh branch
18
+ - Clones the default branch for the agent, and removes the clone when the task
19
+ ends, so nothing accumulates between runs
20
+ - Lets the agent open the pull request, and opens it in Python when the agent did not
21
+ - Opens draft pull requests by default, titled `[#42] <issue title>`, with the
22
+ agent's summary and `Closes #42` in the body
23
+ - Comments on the issue when work starts, when it finishes, and when it does not
24
+ - Posts the agent's answer instead of a pull request when it made no changes
25
+ - Caps how many conversations one poll starts, so a labelled backlog does not
26
+ start dozens at once
27
+
28
+ ## What the agent is told, and what it can reach
29
+
30
+ The prompt names the repository, the issue number and title, and its URL. The
31
+ agent fetches the description, the discussion, and anything they link to itself -
32
+ a copy pasted at dispatch would already be stale, and it would stop where the
33
+ issue's own text stops.
34
+
35
+ Reading that needs credentials, so the conversation is handed exactly one secret,
36
+ `GITHUB_PERSONAL_ACCESS_TOKEN`, and none of the deployment's MCP servers.
37
+ `AGENT_SECRET_NAMES` is an allow-list, so the rest of the secret store stays out
38
+ of reach of a conversation whose instructions came from an issue. Set it to `[]`
39
+ for public repositories if you would rather it held nothing.
40
+
41
+ The agent commits, pushes its branch, and opens the pull request itself, so it
42
+ appears as soon as the agent stops instead of waiting for the next poll. The
43
+ script verifies that on GitHub rather than trusting the agent's word, and opens
44
+ the pull request itself when the agent did not - a failed push or a dead
45
+ conversation never loses the work. `origin` carries no credential, so each GitHub
46
+ command has to name `GITHUB_PERSONAL_ACCESS_TOKEN`, which the SDK injects only
47
+ into a command that mentions it and masks in the output.
48
+
49
+ ## Two setup paths
50
+
51
+ The `/issue-to-pr:setup` conversation substitutes the constants at the top of
52
+ `scripts/main.py` and uploads the result. The catalog entry
53
+ (`automations/catalog/github-issue-to-pr/`) ships the same script unmodified as
54
+ a **bundle** and renders a `config.json` beside it from the setup form, which the
55
+ script loads over those constants. Both paths produce the same automation: a
56
+ tarball the automation service runs on a cron, not a prompt handed to an agent.
57
+
58
+ ## Prerequisites
59
+
60
+ Set `GITHUB_PERSONAL_ACCESS_TOKEN` in OpenHands Settings -> Secrets. The token
61
+ must be able to read the repositories, read and write issues (for the progress
62
+ comments), **write contents** (to push the branch), **write pull requests**, and
63
+ carry the **`workflow` scope** - an issue asking for a CI change makes the agent
64
+ touch `.github/workflows/`, and GitHub rejects the entire push from a token
65
+ without it.
66
+
67
+ The automation runtime must have `git` available; the script clones, commits, and
68
+ pushes with it.
69
+
70
+ ## Quick Start
71
+
72
+ Ask OpenHands:
73
+
74
+ > "Set up an issue-to-PR automation for my `myorg/backend` repo using the
75
+ > `openhands` label."
76
+
77
+ After setup, apply the configured label to an issue to queue an implementation.
78
+ To ask for another attempt later, remove and re-apply the label.
79
+
80
+ ## See Also
81
+
82
+ - [SKILL.md](SKILL.md) - Full setup workflow reference
83
+ - [references/state-schema.md](references/state-schema.md) - State document and
84
+ task lifecycle
@@ -0,0 +1,373 @@
1
+ ---
2
+ name: github-issue-to-pr
3
+ description: >
4
+ Create an automation that implements GitHub issues when a configurable
5
+ trigger label is applied. Polls one or more repositories deterministically,
6
+ clones the default branch, starts one OpenHands conversation per label event,
7
+ then commits, pushes, and opens the pull request itself.
8
+ triggers:
9
+ - /issue-to-pr:setup
10
+ ---
11
+
12
+ # GitHub Issue to PR Automation
13
+
14
+ Create a cron automation that watches one or more GitHub repositories for issues
15
+ with a trigger label, starts an OpenHands conversation once per label event with
16
+ the repository's default branch already checked out, and opens a pull request
17
+ with whatever the agent produced.
18
+
19
+ The automation script is deterministic: issue discovery, label-event tracking,
20
+ state persistence, the clone, the branch, the commit, the push, the pull request,
21
+ the issue comments, and the clone's removal are all handled in Python. The LLM is
22
+ invoked only to write the code.
23
+
24
+ The agent is told **which** issue to implement, not what it says. It fetches the
25
+ description, the discussion, and whatever they link to itself, so nothing in the
26
+ prompt goes stale between dispatch and the moment the agent reads it.
27
+
28
+ That needs read access, so the conversation is handed exactly one secret,
29
+ `GITHUB_PERSONAL_ACCESS_TOKEN`, and no MCP servers. `AGENT_SECRET_NAMES` stays an
30
+ allow-list: the rest of the deployment's secret store is not reachable from a
31
+ conversation whose instructions came from an issue.
32
+
33
+ The agent also finishes the job: it commits, pushes its branch, and opens the
34
+ pull request, so the pull request appears when the agent stops rather than on the
35
+ next poll. The script does not trust that it happened - when the conversation
36
+ ends it asks GitHub whether the pull request exists, and opens it itself when it
37
+ does not. `origin` still carries no credential, so every GitHub command the agent
38
+ runs has to name `GITHUB_PERSONAL_ACCESS_TOKEN`; the SDK only puts a secret in the
39
+ environment of a command that mentions it, and masks it in the output.
40
+
41
+ ---
42
+
43
+ ## Prerequisites
44
+
45
+ ### Required secret
46
+
47
+ Verify that the following secret is set in **OpenHands Settings -> Secrets**:
48
+
49
+ | Secret name | Token type | Minimum permissions |
50
+ |---|---|---|
51
+ | `GITHUB_PERSONAL_ACCESS_TOKEN` | Classic PAT | `repo`, plus `workflow` |
52
+ | `GITHUB_PERSONAL_ACCESS_TOKEN` | Fine-grained PAT | Contents: **Read and write**, Metadata: Read, Issues: **Read and write**, Pull requests: **Read and write**, Workflows: **Read and write** |
53
+
54
+ The workflow scope is not optional in practice. An issue asking for a CI change
55
+ is a normal issue, and GitHub rejects the whole push when a token without it
56
+ touches `.github/workflows/`: *"refusing to allow a Personal Access Token to
57
+ create or update workflow ... without `workflow` scope"*. The branch is rejected
58
+ in full, so the pull request never opens.
59
+
60
+ Contents write access is required because the script pushes the branch, and pull
61
+ request write because it opens the pull request. A read-only token will poll
62
+ happily and then fail at the point of pushing.
63
+
64
+ When several repositories are monitored, the token must cover all of them.
65
+
66
+ Check with:
67
+ ```bash
68
+ curl -s https://api.github.com/user \
69
+ -H "Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN" \
70
+ | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('login') or d.get('message'))"
71
+ ```
72
+
73
+ If the token is missing or invalid, inform the user and stop.
74
+
75
+ ---
76
+
77
+ ## Setup Workflow
78
+
79
+ Follow these steps in order.
80
+
81
+ ### Step 1 - Verify `GITHUB_PERSONAL_ACCESS_TOKEN`
82
+
83
+ Run the `curl` check above.
84
+
85
+ - If absent: *"GITHUB_PERSONAL_ACCESS_TOKEN is not set. Please add it in
86
+ OpenHands Settings -> Secrets."* Stop.
87
+ - If the API returns `{"message": "Bad credentials"}`: tell the user the token is
88
+ invalid and ask them to update it. Stop.
89
+
90
+ ### Step 2 - Collect repositories
91
+
92
+ Ask: *"Which GitHub repositories should be watched?
93
+ (Format: `owner/repo`, e.g. `myorg/backend`. List several separated by commas to
94
+ serve them all from one automation.)"*
95
+
96
+ Validate access to **each** repository, and confirm the token can push:
97
+ ```bash
98
+ curl -s "https://api.github.com/repos/{owner}/{repo}" \
99
+ -H "Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN" \
100
+ | python3 -c "
101
+ import json, sys
102
+ d = json.load(sys.stdin)
103
+ if 'message' in d:
104
+ print('ERROR:', d['message'])
105
+ else:
106
+ perms = d.get('permissions', {})
107
+ print(f\"Accessible. Default branch: {d.get('default_branch')}. Push: {perms.get('push')}\")
108
+ "
109
+ ```
110
+
111
+ Record every accepted repository into `REPOS = ["{owner}/{repo}", ...]`. If one
112
+ repository fails the check, say which and ask whether to continue without it. If
113
+ `Push: False`, say that the automation cannot open pull requests there and ask
114
+ for a token with write access.
115
+
116
+ Each repository is polled independently and keeps its own state, so issue numbers
117
+ never collide between them. The trigger label, branch prefix, and schedule are
118
+ shared; a repository needing different settings wants its own automation.
119
+
120
+ ### Step 3 - Collect trigger label
121
+
122
+ Ask: *"Which issue label should trigger an implementation?
123
+ (Press Enter for the default: `openhands`.)"*
124
+
125
+ Record the answer as `TRIGGER_LABEL`. If the label does not exist yet, tell the
126
+ user that GitHub will still record the event once the label is created and
127
+ applied to an issue.
128
+
129
+ The automation works an issue when it sees the latest matching `labeled` event
130
+ for that label. To ask for another attempt later, remove and re-apply the label -
131
+ that opens a second branch and a second pull request rather than overwriting the
132
+ first.
133
+
134
+ ### Step 4 - Collect the pull request mode
135
+
136
+ Ask: *"Should the pull requests be opened as drafts?
137
+ 1. Draft (default) - opened as a draft, ready for a human to mark ready
138
+ 2. Ready for review - opened as a normal pull request
139
+ (Press Enter for Draft)"*
140
+
141
+ Map the choice to `DRAFT_PULL_REQUEST` (`True` or `False`).
142
+
143
+ ### Step 5 - Collect the branch prefix
144
+
145
+ Ask: *"What branch prefix should the automation use?
146
+ (Press Enter for the default: `openhands/issue`, which produces
147
+ `openhands/issue-42`.)"*
148
+
149
+ Record as `BRANCH_PREFIX`. Keep it free of spaces and of characters git rejects
150
+ in a ref name.
151
+
152
+ ### Step 6 - Collect cron schedule
153
+
154
+ Ask: *"How often should the automation poll for labelled issues?
155
+ (Press Enter for the default: every 5 minutes.
156
+ Use a cron expression for a different interval, e.g. `0 * * * *` = hourly)"*
157
+
158
+ Default: `*/5 * * * *`.
159
+
160
+ Record as `CRON_SCHEDULE`.
161
+
162
+ ### Step 7 - Confirm the secret scope
163
+
164
+ The agent is handed `GITHUB_PERSONAL_ACCESS_TOKEN`, because it reads the issue and
165
+ its discussion itself. Ask: *"Beyond the GitHub token, does the repository's build
166
+ need a secret of its own - a package registry token, for example? (Press Enter for
167
+ none.)"*
168
+
169
+ Record the answers appended to the default, as
170
+ `AGENT_SECRET_NAMES = ["GITHUB_PERSONAL_ACCESS_TOKEN", "NAME", ...]`.
171
+
172
+ Keep it an allow-list. Forwarding the whole secret store would put every
173
+ credential in the deployment behind a prompt written by whoever opened the issue.
174
+ If the repositories are public and you would rather the conversation held no
175
+ credential at all, set the list to `[]` - the agent can still read a public issue
176
+ unauthenticated, and private repositories then stop working.
177
+
178
+ ### Step 8 - Generate the automation script
179
+
180
+ Read `scripts/main.py` from this skill's directory. Apply exactly five constant
181
+ substitutions near the top of the file:
182
+
183
+ > The script also reads a `config.json` shipped beside it, if there is one, over
184
+ > these constants. That is how the catalog entry
185
+ > (`automations/catalog/github-issue-to-pr/`) configures an unmodified copy,
186
+ > since a declarative host cannot rewrite Python. This setup path substitutes the
187
+ > constants and ships no `config.json`, so the two never collide.
188
+
189
+ | Placeholder | Replace with |
190
+ |---|---|
191
+ | `REPOS = ["owner/repo"]` | `REPOS = ["{owner_repo}", ...]` - one entry per repository collected in Step 2 |
192
+ | `TRIGGER_LABEL = "openhands"` | `TRIGGER_LABEL = "{trigger_label}"` |
193
+ | `BRANCH_PREFIX = "openhands/issue"` | `BRANCH_PREFIX = "{branch_prefix}"` |
194
+ | `DRAFT_PULL_REQUEST = True` | `DRAFT_PULL_REQUEST = {True or False}` |
195
+ | `AGENT_SECRET_NAMES: list[str] = []` | `AGENT_SECRET_NAMES: list[str] = ["{name}", ...]` |
196
+
197
+ Leave `MAX_NEW_PER_RUN` and `DEFAULT_OPENHANDS_URL` alone unless the user asks
198
+ for a different cap or a non-default OpenHands URL.
199
+
200
+ A repository may be given as `owner/repo`, as a clone URL, or as an SSH remote;
201
+ the script normalizes each one at startup and names the value it could not read
202
+ rather than blaming the token.
203
+
204
+ Use a safe string writer such as `json.dumps(value)` when inserting user-provided
205
+ repository names, labels, or prefixes into Python string literals.
206
+ `json.dumps(list_of_repos)` produces the whole `REPOS` list safely in one step.
207
+
208
+ Write the customized script to a temporary build directory:
209
+ ```bash
210
+ mkdir -p /tmp/issue-to-pr-build
211
+ # write the customized main.py to /tmp/issue-to-pr-build/main.py
212
+ ```
213
+
214
+ Validate syntax before packaging:
215
+ ```bash
216
+ python3 -m py_compile /tmp/issue-to-pr-build/main.py && echo "Syntax OK"
217
+ ```
218
+
219
+ Fix any syntax errors before proceeding.
220
+
221
+ ### Step 9 - Package and upload
222
+
223
+ Determine the Automation backend URL and auth from the `<RUNTIME_SERVICES>`
224
+ block in your system context:
225
+ - **OPENHANDS_HOST**: the Automation backend `url_from_agent`
226
+ - **Auth**: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`
227
+
228
+ ```bash
229
+ tar -czf /tmp/issue-to-pr.tar.gz -C /tmp/issue-to-pr-build .
230
+
231
+ TARBALL_PATH=$(curl -s -X POST \
232
+ "${OPENHANDS_HOST}/api/automation/v1/uploads?name=github-issue-to-pr" \
233
+ -H "X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY" \
234
+ -H "Content-Type: application/gzip" \
235
+ --data-binary @/tmp/issue-to-pr.tar.gz \
236
+ | python3 -c "import json,sys; print(json.load(sys.stdin)['tarball_path'])")
237
+
238
+ echo "Uploaded: $TARBALL_PATH"
239
+ ```
240
+
241
+ ### Step 10 - Register the automation
242
+
243
+ ```bash
244
+ curl -s -X POST "${OPENHANDS_HOST}/api/automation/v1" \
245
+ -H "X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY" \
246
+ -H "Content-Type: application/json" \
247
+ -d "{
248
+ \"name\": \"GitHub Issue to PR: {repo_summary} label {trigger_label}\",
249
+ \"trigger\": {\"type\": \"cron\", \"schedule\": \"{cron_schedule}\"},
250
+ \"tarball_path\": \"$TARBALL_PATH\",
251
+ \"entrypoint\": \"python3 main.py\",
252
+ \"timeout\": 900
253
+ }" | python3 -m json.tool
254
+ ```
255
+
256
+ Use the single repository as `{repo_summary}` when there is one, and something
257
+ like `3 repos` when there are several. A poll clones a repository per queued
258
+ issue and pushes finished branches, so the timeout allows for that; a run never
259
+ waits for an agent to finish, only for it to be started.
260
+
261
+ Record the returned `id`.
262
+
263
+ ### Step 11 - Confirm
264
+
265
+ Tell the user:
266
+
267
+ > ✅ **GitHub Issue to PR** is running!
268
+ >
269
+ > - Automation ID: `{id}`
270
+ > - Repositories: `{owner}/{repo}`, ... (one line each)
271
+ > - Trigger label: `{trigger_label}`
272
+ > - Branch prefix: `{branch_prefix}`
273
+ > - Pull requests: `{draft or ready for review}`
274
+ > - Polling schedule: `{cron_schedule}`
275
+ > - State file per repository:
276
+ > `~/.openhands/workspaces/automation-state/github_issue_to_pr_{id}_{owner}__{repo}.json`
277
+ >
278
+ > Apply the `{trigger_label}` label to an issue to queue an implementation. Each
279
+ > label event is processed once. To ask for another attempt, remove and re-apply
280
+ > the label - that opens a second branch and pull request.
281
+ >
282
+ > The agent runs without GitHub credentials; the automation pushes the branch and
283
+ > opens the pull request once the agent has stopped.
284
+
285
+ ---
286
+
287
+ ## Runtime Behaviour (per poll)
288
+
289
+ Each cron run executes `main.py`, which loads `config.json` if the catalog
290
+ shipped one, checks that `git` is available, resolves and validates
291
+ `GITHUB_PERSONAL_ACCESS_TOKEN` once, then processes every repository in `REPOS`
292
+ independently. One repository failing does not stop the
293
+ others; the run fails only if every repository fails.
294
+
295
+ For each repository:
296
+
297
+ 1. Loads that repository's state (see `references/state-schema.md`) and reads its
298
+ default branch.
299
+ 2. Lists open issues carrying `TRIGGER_LABEL`, newest-updated first. Pull
300
+ requests are dropped, so labelling a PR never queues an implementation.
301
+ 3. For each labelled issue, up to `MAX_NEW_PER_RUN` new ones per run:
302
+ - Refetches the issue so a label removed since the listing does not start work.
303
+ - Finds the latest matching GitHub `labeled` event, and skips it if that event
304
+ has already been tracked.
305
+ - Picks the first free branch name, `{BRANCH_PREFIX}-{number}` or a numbered
306
+ variant of it.
307
+ - Clones the default branch, shallow and single-branch, into
308
+ `{WORKSPACE_BASE}/issue-to-pr/{owner}__{repo}/issue-{number}-{event_id}`,
309
+ sets the commit identity, and creates the branch. `origin` keeps its plain
310
+ HTTPS URL, so the workspace holds no credential.
311
+ - Starts an OpenHands conversation **whose working directory is that clone**,
312
+ with the issue title, body, labels, and discussion in the prompt, and only
313
+ the secrets named in `AGENT_SECRET_NAMES` attached.
314
+ - Comments on the issue with the branch, the label event, and the conversation
315
+ link.
316
+ - Records the task with `status: "active"`.
317
+ - If the clone or the conversation cannot be created, the clone is removed and
318
+ nothing is recorded, so the next poll retries the label event.
319
+ 4. For each active task:
320
+ - Abandons a conversation that has not reached a terminal status within two
321
+ hours, comments on the issue, and reclaims its clone.
322
+ - When the conversation reaches `idle`, `finished`, `error`, or `stuck`:
323
+ - Adopts the pull request the agent opened, if GitHub says one exists for
324
+ the branch, and comments its link on the issue. Everything below is the
325
+ path taken when it does not.
326
+ - Skips the pull request if the issue was closed meanwhile.
327
+ - Reports the problem on the issue if the conversation ended in `error` or
328
+ `stuck`.
329
+ - Commits whatever the agent left uncommitted, on top of any commits it made
330
+ itself.
331
+ - Posts the agent's answer on the issue, and opens no pull request, when
332
+ there are no commits at all - that is how an agent reports an issue too
333
+ ambiguous to implement.
334
+ - Otherwise pushes the branch, opens the pull request (draft by default,
335
+ titled `[#42] <issue title>`, with the agent's summary and `Closes #42` in
336
+ the body), and comments the link on the issue.
337
+ - A push or pull request that fails is retried on the next two polls before
338
+ the task is reported as failed, so a transient GitHub error does not throw
339
+ the work away.
340
+ 5. Removes the clone of every finished task, but only after confirming the
341
+ conversation has stopped - deleting it under a running agent would remove its
342
+ working directory. When that cannot be confirmed the directory is left alone
343
+ and the next poll tries again.
344
+ 6. Saves that repository's state atomically.
345
+
346
+ The completion callback fires once for the whole run.
347
+
348
+ ---
349
+
350
+ ## Additional Resources
351
+
352
+ - **`references/state-schema.md`** - State JSON schema, field definitions, and the
353
+ task lifecycle.
354
+ - **`scripts/main.py`** - The complete automation script. Customize the five
355
+ constants at the top before packaging.
356
+
357
+ ---
358
+
359
+ ## Troubleshooting
360
+
361
+ | Symptom | Likely cause | Fix |
362
+ |---|---|---|
363
+ | Nothing is ever queued | Trigger label not present, or applied to a pull request rather than an issue | Apply the configured label to an issue |
364
+ | "Bad credentials" in run logs | Token expired | Rotate and update `GITHUB_PERSONAL_ACCESS_TOKEN` |
365
+ | "The token cannot push to ..." | Token lacks Contents: write on that repository | Issue a token with write access, or drop the repository from `REPOS` |
366
+ | Push rejected: "refusing to allow a Personal Access Token to create or update workflow" | The change touches `.github/workflows/` and the token has no `workflow` scope | Add the scope to the token; the next poll retries the same branch and opens the pull request |
367
+ | 404 on repo access | Repo name wrong or no access | Re-check the entry in `REPOS` and the token's permissions |
368
+ | `git is not available in the automation runtime` | The runtime image has no git | Use a runtime image that ships git; the script clones, commits, and pushes with it |
369
+ | Issue commented "did not change any code" | The agent judged the issue too ambiguous, or made no edits | Read its answer in the comment, add the missing detail to the issue, then re-apply the label |
370
+ | Same issue not picked up again after new comments | Its label event was already processed | Remove and re-apply the trigger label |
371
+ | Agent reports it cannot push or open a PR | By design - it has no credentials | No action; the automation pushes and opens the pull request after the agent stops |
372
+ | A backlog of labelled issues starts slowly | `MAX_NEW_PER_RUN` caps how many conversations one poll starts | Wait for the next polls, or raise the cap in the script |
373
+ | Clones remain under `issue-to-pr/` | Their conversations had not stopped yet | They are removed by a later poll once the conversation is terminal |
@@ -0,0 +1,8 @@
1
+ ---
2
+ # auto-generated by sync_extensions.py
3
+ description: Create an automation that implements GitHub issues when a configurable trigger label is applied. Polls one or more repositories deterministically, clones the default branch, starts one OpenHands conversation per label event, then commits, pushes, and opens the pull request itself.
4
+ ---
5
+
6
+ Read and follow the complete instructions in the SKILL.md file located in this skill's directory.
7
+
8
+ $ARGUMENTS
@@ -0,0 +1,139 @@
1
+ # State Schema
2
+
3
+ The automation maintains a JSON state document **per repository**, persisted
4
+ across polling runs. It is the source of truth for which trigger-label events
5
+ have already queued work, which conversations are still active, and which clones
6
+ are still on disk.
7
+
8
+ Each repository in `REPOS` gets its own document, so issue numbers from different
9
+ repositories never share a bucket.
10
+
11
+ The document holds only what a poll needs in order to decide what to do next.
12
+ Issue metadata that can be read back from GitHub is not mirrored here, so there
13
+ is nothing to drift.
14
+
15
+ ---
16
+
17
+ ## Storage
18
+
19
+ **Primary (cloud):** The state is stored in the automation service's built-in KV
20
+ store under the key `state:{owner}__{repo}` - for example
21
+ `state:OpenHands__extensions`. The KV store is available when
22
+ `AUTOMATION_KV_TOKEN` is injected into the run environment. Each automation has
23
+ its own isolated namespace.
24
+
25
+ **Fallback (local/dev):** When the KV store is not available, the state is
26
+ written to a local JSON file at:
27
+
28
+ ```
29
+ {WORKSPACE_BASE_ROOT}/automation-state/github_issue_to_pr_{automation_id}_{owner}__{repo}.json
30
+ ```
31
+
32
+ `WORKSPACE_BASE_ROOT` is derived by going two levels up from the `WORKSPACE_BASE`
33
+ environment variable, stripping `automation-runs/{run_id}`.
34
+
35
+ Example on a local install:
36
+
37
+ ```
38
+ ~/.openhands/workspaces/automation-state/github_issue_to_pr_abc12345-..._myorg__backend.json
39
+ ```
40
+
41
+ The `automation_id` is read from the `AUTOMATION_EVENT_PAYLOAD` environment
42
+ variable, field `automation_id`.
43
+
44
+ ---
45
+
46
+ ## Top-Level Schema
47
+
48
+ ```jsonc
49
+ {
50
+ "version": 1,
51
+ "repo": "owner/repo",
52
+ "trigger_label": "openhands",
53
+ "updated_at": 1717200000.0,
54
+ "tasks": {}
55
+ }
56
+ ```
57
+
58
+ ---
59
+
60
+ ## `tasks` Map
61
+
62
+ Key: `"{issue_number}:label:{label_event_id}"`. This makes the latest GitHub
63
+ `labeled` event the idempotency key. Re-applying the trigger label creates a new
64
+ GitHub event and therefore a new task, on a new branch.
65
+
66
+ Value: **TaskRecord**
67
+
68
+ ```jsonc
69
+ {
70
+ "issue_number": 42,
71
+ "issue_title": "Retry uploads on a 502",
72
+ "trigger_label_event_id": 123456789,
73
+ "trigger_label_event_created_at": "2026-06-12T00:00:00Z",
74
+ "html_url": "https://github.com/owner/repo/issues/42",
75
+ "base_branch": "main",
76
+ "base_sha": "0123456789abcdef...",
77
+ "branch": "openhands/issue-42",
78
+ "status": "active",
79
+ "conversation_id": "conv_abc123",
80
+ "workspace_dir": "/workspace/issue-to-pr/owner__repo/issue-42-123456789",
81
+ "last_activity": 1717200000.0,
82
+ "finalize_attempts": 1,
83
+ "pull_request_url": "https://github.com/owner/repo/pull/57",
84
+ "pull_request_number": 57,
85
+ "completed_at": 1717203600.0,
86
+ "expired_after": 7200.5,
87
+ "error": "git push origin ... failed (128): ..."
88
+ }
89
+ ```
90
+
91
+ | Field | Written when | Meaning |
92
+ |---|---|---|
93
+ | `issue_number` | claim | The issue being implemented |
94
+ | `issue_title` | claim | Used for the commit message and the pull request title |
95
+ | `trigger_label_event_id` | claim | The GitHub `labeled` event this task belongs to |
96
+ | `trigger_label_event_created_at` | claim | When that label was applied |
97
+ | `html_url` | claim | The issue URL |
98
+ | `base_branch` | claim | The repository's default branch at claim time |
99
+ | `base_sha` | start | The commit the clone starts from; commits are counted against it |
100
+ | `branch` | start | The branch the pull request is opened from |
101
+ | `status` | throughout | See the lifecycle below |
102
+ | `conversation_id` | start | The OpenHands conversation doing the work |
103
+ | `workspace_dir` | start | The clone. Removed, and the field dropped, once the task ends |
104
+ | `last_activity` | throughout | Drives the debounce, the two-hour abandonment, and the stalled-claim release |
105
+ | `finalize_attempts` | finalize | How many times pushing and opening the pull request has been tried |
106
+ | `pull_request_url` / `pull_request_number` | success | The pull request that was opened |
107
+ | `completed_at` | end | When the task reached a terminal status |
108
+ | `expired_after` | expiry | Seconds the conversation ran before being abandoned |
109
+ | `error` | failure | The last finalization error, after the retries ran out |
110
+
111
+ ---
112
+
113
+ ## Task Lifecycle
114
+
115
+ ```
116
+ label event seen
117
+ │
118
+ ▼
119
+ "starting" ── claim persisted before the slow work, so an
120
+ │ overlapping poll cannot start it twice.
121
+ │ Released after 15 minutes if the poll died.
122
+ clone + conversation
123
+ │
124
+ ▼
125
+ "active"
126
+ │
127
+ ┌─────────────┬───────┴────────┬──────────────┬─────────────────┐
128
+ ▼ ▼ ▼ ▼ ▼
129
+ "issue-closed" "failed" "no-changes" "closed" "expired"
130
+ issue closed conversation agent produced pull request no terminal
131
+ meanwhile errored, or no commits; opened and status within
132
+ push/PR gave its answer is linked on the two hours
133
+ up after 3 posted on the issue
134
+ attempts issue
135
+ ```
136
+
137
+ Every terminal status releases the clone, but only once the conversation is
138
+ confirmed stopped. When that cannot be confirmed, `workspace_dir` stays in the
139
+ record and a later poll retries the removal.