@openhands/extensions 0.17.0 → 0.18.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 (42) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/README.md +5 -2
  3. package/automations/README.md +3 -2
  4. package/automations/bundle-index.js +9 -0
  5. package/automations/catalog/github-agents-md-maintainer/manifest.json +96 -0
  6. package/automations/catalog/github-issue-to-pr/manifest.json +107 -0
  7. package/automations/catalog/github-pr-reviewer/manifest.json +1 -0
  8. package/automations/catalog/github-repo-monitor/manifest.json +1 -0
  9. package/automations/catalog/news-digest/manifest.json +76 -0
  10. package/automations/catalog-index.js +13 -7
  11. package/automations/catalog.schema.json +10 -2
  12. package/automations/index.d.ts +18 -1
  13. package/automations/interface.json +3 -1
  14. package/marketplaces/openhands-extensions.json +38 -0
  15. package/package.json +1 -1
  16. package/pyproject.toml +1 -1
  17. package/python/openhands_extensions/_version.py +1 -1
  18. package/skills/github-agents-md-maintainer/.plugin/plugin.json +19 -0
  19. package/skills/github-agents-md-maintainer/README.md +59 -0
  20. package/skills/github-agents-md-maintainer/SKILL.md +271 -0
  21. package/skills/github-agents-md-maintainer/commands/agents-md-setup.md +8 -0
  22. package/skills/github-agents-md-maintainer/references/state-schema.md +113 -0
  23. package/skills/github-agents-md-maintainer/scripts/main.py +1187 -0
  24. package/skills/github-issue-to-pr/.plugin/plugin.json +19 -0
  25. package/skills/github-issue-to-pr/README.md +84 -0
  26. package/skills/github-issue-to-pr/SKILL.md +373 -0
  27. package/skills/github-issue-to-pr/commands/issue-to-pr-setup.md +8 -0
  28. package/skills/github-issue-to-pr/references/state-schema.md +139 -0
  29. package/skills/github-issue-to-pr/scripts/main.py +1305 -0
  30. package/skills/index.js +27 -0
  31. package/skills/news-digest/.plugin/plugin.json +19 -0
  32. package/skills/news-digest/README.md +71 -0
  33. package/skills/news-digest/SKILL.md +284 -0
  34. package/skills/news-digest/commands/news-digest-setup.md +8 -0
  35. package/skills/news-digest/references/state-schema.md +178 -0
  36. package/skills/news-digest/scripts/main.py +1250 -0
  37. package/tests/fixtures/automations/github-repo-monitor.json +77 -0
  38. package/tests/test_automation_setup.py +34 -0
  39. package/tests/test_catalogs.py +3 -1
  40. package/tests/test_github_agents_md_maintainer.py +352 -0
  41. package/tests/test_github_issue_to_pr.py +539 -0
  42. package/tests/test_news_digest.py +646 -0
package/skills/index.js CHANGED
@@ -224,6 +224,24 @@ export const SKILLS_CATALOG = [
224
224
  "content": "# GitHub Actions Guide\n\n## Critical Rules\n\n**Custom Action Deployment:**\n- New custom actions MUST be merged to the main branch before they can be used\n- After the initial merge, they should be tested from feature branches\n\n**Debug Steps:**\nAdd debug steps that print non-secret parameters when:\n- Creating a new action, OR\n- Troubleshooting a particularly tricky issue\n\n(Not required for every workflow - use when needed)\n\n## Effectiveness Principles\n\nActions cost CI minutes. Be deliberate, not iterative:\n\n1. **Monitor, don't poll** - use `gh run watch` / `gh pr checks --watch` to follow runs live\n2. **Read logs, don't guess** - fetch the failed job's log before changing code\n3. **Print actual values** - debug steps reveal the real `inputs`/`github` context, not your assumptions\n4. **Test locally first** - `act` runs workflows on your machine and avoids burning CI minutes\n5. **Plan the smallest reproduction** - one job, minimal matrix, narrow trigger before scaling up\n\nSee [README.md](README.md) for the full debugging workflow, `gh` commands, and YAML debug-step examples.\n\n## Key Gotchas\n\n1. **Secrets unavailable in fork PRs** - `pull_request` has no secrets for forks; `pull_request_target` does but **never check out or execute fork PR code inside it** (RCE with write permissions)\n2. **Pin action versions** - Use `@v4` or SHA, not `@main` (prevents breaking changes)\n3. **Explicit permissions** - Set `permissions:` block for GITHUB_TOKEN operations\n4. **Artifacts for job-to-job data** - Files don't persist between jobs without `upload-artifact`/`download-artifact`",
225
225
  "category": "code-hosting"
226
226
  },
227
+ {
228
+ "name": "github-agents-md-maintainer",
229
+ "description": "Create an automation that keeps AGENTS.md current in one or more GitHub repositories. On a schedule - weekly by default - it clones the default branch, starts an OpenHands conversation that reads the repository and creates or updates AGENTS.md, and opens a pull request with the result.",
230
+ "triggers": [
231
+ "/agents-md:setup"
232
+ ],
233
+ "content": "# AGENTS.md Maintainer Automation\n\nCreate a cron automation that keeps each configured repository's `AGENTS.md` -\nthe file an agent reads first when it starts work there - matching what the\nrepository actually is. It is created when missing, updated when the repository\nhas moved on, and left alone when it is still accurate.\n\nThe automation script is deterministic: scheduling, the once-per-week claim, the\nclone, the branch, the commit, the push, the pull request, and the clone's\nremoval are all handled in Python. The LLM is invoked only to read the\nrepository and write the file.\n\nA week is one unit of work per repository, so a cron that fires more often than\nintended, a retried run, or a restarted service cannot open the same pull\nrequest twice. **A repository whose previous pull request from this automation\nis still open is skipped**, because a second one would edit the same file and\nreviewing it would tell you nothing the first did not.\n\n---\n\n## Prerequisites\n\n### Required secret\n\nVerify that the following secret is set in **OpenHands Settings -> Secrets**:\n\n| Secret name | Token type | Minimum permissions |\n|---|---|---|\n| `GITHUB_PERSONAL_ACCESS_TOKEN` | Classic PAT | `repo` |\n| `GITHUB_PERSONAL_ACCESS_TOKEN` | Fine-grained PAT | Contents: **Read and write**, Metadata: Read, Pull requests: **Read and write** |\n\nContents write is required because the branch is pushed, and pull request write\nbecause the pull request is opened. No issue permission is needed: this\nautomation never comments on an issue.\n\nCheck with:\n```bash\ncurl -s https://api.github.com/user \\\n -H \"Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN\" \\\n | python3 -c \"import json,sys; d=json.load(sys.stdin); print(d.get('login') or d.get('message'))\"\n```\n\nIf the token is missing or invalid, inform the user and stop.\n\n---\n\n## Setup Workflow\n\nFollow these steps in order.\n\n### Step 1 - Verify `GITHUB_PERSONAL_ACCESS_TOKEN`\n\nRun the `curl` check above.\n\n- If absent: *\"GITHUB_PERSONAL_ACCESS_TOKEN is not set. Please add it in\n OpenHands Settings -> Secrets.\"* Stop.\n- If the API returns `{\"message\": \"Bad credentials\"}`: tell the user the token is\n invalid and ask them to update it. Stop.\n\n### Step 2 - Collect repositories\n\nAsk: *\"Which GitHub repositories should have their AGENTS.md maintained?\n(Format: `owner/repo`, e.g. `myorg/backend`. List several separated by commas.)\"*\n\nValidate access to **each** repository, and confirm the token can push:\n```bash\ncurl -s \"https://api.github.com/repos/{owner}/{repo}\" \\\n -H \"Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN\" \\\n | python3 -c \"\nimport json, sys\nd = json.load(sys.stdin)\nif 'message' in d:\n print('ERROR:', d['message'])\nelse:\n print(f\\\"Accessible. Default branch: {d.get('default_branch')}. Push: {d.get('permissions',{}).get('push')}\\\")\n\"\n```\n\nRecord every accepted repository into `REPOS = [\"{owner}/{repo}\", ...]`. Each\nrepository keeps its own state and its own weekly claim, so one falling behind\nnever blocks another.\n\n### Step 3 - Collect the schedule\n\nAsk: *\"How often should AGENTS.md be checked?\n(Press Enter for the default: every Monday at 09:00 UTC, `0 9 * * 1`.)\"*\n\nDefault: `0 9 * * 1`. Record as `CRON_SCHEDULE`, and the timezone as\n`CRON_TIMEZONE` (default `UTC`).\n\nA schedule more frequent than weekly is allowed but rarely useful: the work is\nkeyed by ISO week, so extra runs inside the same week do nothing but poll.\n\n### Step 4 - Collect the pull request mode\n\nAsk: *\"Should the pull requests be opened as drafts?\n 1. Draft (default) - opened as a draft, ready for a human to mark ready\n 2. Ready for review - opened as a normal pull request\n(Press Enter for Draft)\"*\n\nMap the choice to `DRAFT_PULL_REQUEST` (`True` or `False`).\n\n### Step 5 - Collect the branch prefix\n\nAsk: *\"What branch prefix should the automation use?\n(Press Enter for the default: `openhands/agents-md`, which produces\n`openhands/agents-md-2026-W34`.)\"*\n\nRecord as `BRANCH_PREFIX`. The prefix is also how the automation recognises its\nown open pull requests, so changing it later makes it stop seeing the older ones.\n\n### Step 6 - Confirm the secret scope\n\nThe agent is handed `GITHUB_PERSONAL_ACCESS_TOKEN`, because it pushes its branch\nand opens the pull request itself. Ask: *\"Beyond the GitHub token, does reading\nthis repository need a secret of its own? (Press Enter for none.)\"*\n\nRecord the answers appended to the default, as\n`AGENT_SECRET_NAMES = [\"GITHUB_PERSONAL_ACCESS_TOKEN\", \"NAME\", ...]`. Keep it an\nallow-list: the conversation reads a whole repository, so the rest of the\ndeployment's secrets should stay out of its reach.\n\n### Step 7 - Generate the automation script\n\nRead `scripts/main.py` from this skill's directory. Apply exactly four constant\nsubstitutions near the top of the file:\n\n> The script also reads a `config.json` shipped beside it, if there is one, over\n> these constants. That is how the catalog entry\n> (`automations/catalog/github-agents-md-maintainer/`) configures an unmodified\n> copy, since a declarative host cannot rewrite Python. This setup path\n> substitutes the constants and ships no `config.json`, so the two never collide.\n\n| Placeholder | Replace with |\n|---|---|\n| `REPOS = [\"owner/repo\"]` | `REPOS = [\"{owner_repo}\", ...]` - one entry per repository from Step 2 |\n| `BRANCH_PREFIX = \"openhands/agents-md\"` | `BRANCH_PREFIX = \"{branch_prefix}\"` |\n| `DRAFT_PULL_REQUEST = True` | `DRAFT_PULL_REQUEST = {True or False}` |\n| `AGENT_SECRET_NAMES: list[str] = [\"GITHUB_PERSONAL_ACCESS_TOKEN\"]` | the list from Step 6 |\n\nUse a safe string writer such as `json.dumps(value)` when inserting user-provided\nrepository names or prefixes into Python string literals.\n\nWrite the customized script to a temporary build directory and validate it:\n```bash\nmkdir -p /tmp/agents-md-build\n# write the customized main.py to /tmp/agents-md-build/main.py\npython3 -m py_compile /tmp/agents-md-build/main.py && echo \"Syntax OK\"\n```\n\n### Step 8 - Package and upload\n\nDetermine the Automation backend URL and auth from the `<RUNTIME_SERVICES>`\nblock in your system context:\n- **OPENHANDS_HOST**: the Automation backend `url_from_agent`\n- **Auth**: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`\n\n```bash\ntar -czf /tmp/agents-md.tar.gz -C /tmp/agents-md-build .\n\nTARBALL_PATH=$(curl -s -X POST \\\n \"${OPENHANDS_HOST}/api/automation/v1/uploads?name=github-agents-md-maintainer\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/gzip\" \\\n --data-binary @/tmp/agents-md.tar.gz \\\n | python3 -c \"import json,sys; print(json.load(sys.stdin)['tarball_path'])\")\n\necho \"Uploaded: $TARBALL_PATH\"\n```\n\n### Step 9 - Register the automation\n\n```bash\ncurl -s -X POST \"${OPENHANDS_HOST}/api/automation/v1\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\n \\\"name\\\": \\\"AGENTS.md Maintainer: {repo_summary}\\\",\n \\\"trigger\\\": {\\\"type\\\": \\\"cron\\\", \\\"schedule\\\": \\\"{cron_schedule}\\\", \\\"timezone\\\": \\\"{cron_timezone}\\\"},\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 900\n }\" | python3 -m json.tool\n```\n\nRecord the returned `id`.\n\n### Step 10 - Confirm\n\nTell the user:\n\n> ✅ **AGENTS.md Maintainer** is running!\n>\n> - Automation ID: `{id}`\n> - Repositories: `{owner}/{repo}`, ... (one line each)\n> - Schedule: `{cron_schedule}` ({cron_timezone})\n> - Branch prefix: `{branch_prefix}`\n> - Pull requests: `{draft or ready for review}`\n> - State file per repository:\n> `~/.openhands/workspaces/automation-state/github_agents_md_{id}_{owner}__{repo}.json`\n>\n> Each week it reads the repository and proposes an AGENTS.md change, or reports\n> that none is needed. While one of its pull requests is still open, it stays\n> quiet - merge or close it to get the next one.\n\n---\n\n## Runtime Behaviour (per run)\n\nEach cron run executes `main.py`, which loads `config.json` if the catalog\nshipped one, checks that `git` is available, resolves and validates\n`GITHUB_PERSONAL_ACCESS_TOKEN` once, then processes every repository in `REPOS`\nindependently. One repository failing does not stop the others; the run fails\nonly if every repository fails.\n\nFor each repository:\n\n1. Loads that repository's state and reads its default branch.\n2. Computes the current ISO week, e.g. `2026-W34`, and stops here if that week\n is already recorded - which is what makes extra runs inside a week harmless.\n3. Lists open pull requests whose head branch starts with the branch prefix. If\n any exist, records the skip and moves on.\n4. Asks GitHub whether `AGENTS.md` exists on the default branch, which decides\n whether the run is a create or an update, and the pull request's title.\n5. Claims the week in state **before** the slow work, so an overlapping run\n cannot start it twice.\n6. Picks the first free branch name (`{prefix}-{period}`, else a numbered\n variant), clones the default branch shallow and single-branch into\n `{WORKSPACE_BASE}/agents-md/{owner}__{repo}/{period}`, and creates the branch.\n7. Starts an OpenHands conversation with that clone as its working directory,\n and only the secrets named in `AGENT_SECRET_NAMES` attached.\n8. When the conversation reaches `idle`, `finished`, `error`, or `stuck`:\n - Adopts the pull request the agent opened, if GitHub says one exists.\n - Records the failure and opens nothing if the conversation errored.\n - Records `no-changes` and opens nothing if there are no commits - the\n expected outcome when `AGENTS.md` is already accurate.\n - Otherwise commits what was left, pushes, and opens the pull request itself.\n - Retries a failed push or pull request on the next two runs before giving up.\n9. Removes the clone once the conversation is confirmed stopped.\n\n---\n\n## Additional Resources\n\n- **`references/state-schema.md`** - State JSON schema and the task lifecycle.\n- **`scripts/main.py`** - The complete automation script.\n\n---\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---|---|---|\n| Nothing happens for a repository | A pull request from this automation is still open | Merge or close it; the next run proposes the following one |\n| Nothing happens after a manual dispatch | The current ISO week is already recorded in state | Wait for the next week, or clear that week's entry from the state document |\n| \"The token cannot push to ...\" | Token lacks Contents: write | Issue a token with write access, or drop the repository from `REPOS` |\n| `git is not available in the automation runtime` | The runtime image has no git | Use a runtime image that ships git |\n| Pull request says nothing changed | The agent judged AGENTS.md accurate but still committed | Read its summary in the pull request body; tighten the prompt if it keeps making cosmetic edits |\n| Every run reports `no-changes` | AGENTS.md is accurate, or the agent cannot read the repository | Open the conversation from the run log and check what it saw |\n| Clones remain under `agents-md/` | Their conversations had not stopped yet | They are removed by a later run once the conversation is terminal |",
234
+ "category": "automations"
235
+ },
236
+ {
237
+ "name": "github-issue-to-pr",
238
+ "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.",
239
+ "triggers": [
240
+ "/issue-to-pr:setup"
241
+ ],
242
+ "content": "# GitHub Issue to PR Automation\n\nCreate a cron automation that watches one or more GitHub repositories for issues\nwith a trigger label, starts an OpenHands conversation once per label event with\nthe repository's default branch already checked out, and opens a pull request\nwith whatever the agent produced.\n\nThe automation script is deterministic: issue discovery, label-event tracking,\nstate persistence, the clone, the branch, the commit, the push, the pull request,\nthe issue comments, and the clone's removal are all handled in Python. The LLM is\ninvoked only to write the code.\n\nThe agent is told **which** issue to implement, not what it says. It fetches the\ndescription, the discussion, and whatever they link to itself, so nothing in the\nprompt goes stale between dispatch and the moment the agent reads it.\n\nThat needs read access, so the conversation is handed exactly one secret,\n`GITHUB_PERSONAL_ACCESS_TOKEN`, and no MCP servers. `AGENT_SECRET_NAMES` stays an\nallow-list: the rest of the deployment's secret store is not reachable from a\nconversation whose instructions came from an issue.\n\nThe agent also finishes the job: it commits, pushes its branch, and opens the\npull request, so the pull request appears when the agent stops rather than on the\nnext poll. The script does not trust that it happened - when the conversation\nends it asks GitHub whether the pull request exists, and opens it itself when it\ndoes not. `origin` still carries no credential, so every GitHub command the agent\nruns has to name `GITHUB_PERSONAL_ACCESS_TOKEN`; the SDK only puts a secret in the\nenvironment of a command that mentions it, and masks it in the output.\n\n---\n\n## Prerequisites\n\n### Required secret\n\nVerify that the following secret is set in **OpenHands Settings -> Secrets**:\n\n| Secret name | Token type | Minimum permissions |\n|---|---|---|\n| `GITHUB_PERSONAL_ACCESS_TOKEN` | Classic PAT | `repo`, plus `workflow` |\n| `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** |\n\nThe workflow scope is not optional in practice. An issue asking for a CI change\nis a normal issue, and GitHub rejects the whole push when a token without it\ntouches `.github/workflows/`: *\"refusing to allow a Personal Access Token to\ncreate or update workflow ... without `workflow` scope\"*. The branch is rejected\nin full, so the pull request never opens.\n\nContents write access is required because the script pushes the branch, and pull\nrequest write because it opens the pull request. A read-only token will poll\nhappily and then fail at the point of pushing.\n\nWhen several repositories are monitored, the token must cover all of them.\n\nCheck with:\n```bash\ncurl -s https://api.github.com/user \\\n -H \"Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN\" \\\n | python3 -c \"import json,sys; d=json.load(sys.stdin); print(d.get('login') or d.get('message'))\"\n```\n\nIf the token is missing or invalid, inform the user and stop.\n\n---\n\n## Setup Workflow\n\nFollow these steps in order.\n\n### Step 1 - Verify `GITHUB_PERSONAL_ACCESS_TOKEN`\n\nRun the `curl` check above.\n\n- If absent: *\"GITHUB_PERSONAL_ACCESS_TOKEN is not set. Please add it in\n OpenHands Settings -> Secrets.\"* Stop.\n- If the API returns `{\"message\": \"Bad credentials\"}`: tell the user the token is\n invalid and ask them to update it. Stop.\n\n### Step 2 - Collect repositories\n\nAsk: *\"Which GitHub repositories should be watched?\n(Format: `owner/repo`, e.g. `myorg/backend`. List several separated by commas to\nserve them all from one automation.)\"*\n\nValidate access to **each** repository, and confirm the token can push:\n```bash\ncurl -s \"https://api.github.com/repos/{owner}/{repo}\" \\\n -H \"Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN\" \\\n | python3 -c \"\nimport json, sys\nd = json.load(sys.stdin)\nif 'message' in d:\n print('ERROR:', d['message'])\nelse:\n perms = d.get('permissions', {})\n print(f\\\"Accessible. Default branch: {d.get('default_branch')}. Push: {perms.get('push')}\\\")\n\"\n```\n\nRecord every accepted repository into `REPOS = [\"{owner}/{repo}\", ...]`. If one\nrepository fails the check, say which and ask whether to continue without it. If\n`Push: False`, say that the automation cannot open pull requests there and ask\nfor a token with write access.\n\nEach repository is polled independently and keeps its own state, so issue numbers\nnever collide between them. The trigger label, branch prefix, and schedule are\nshared; a repository needing different settings wants its own automation.\n\n### Step 3 - Collect trigger label\n\nAsk: *\"Which issue label should trigger an implementation?\n(Press Enter for the default: `openhands`.)\"*\n\nRecord the answer as `TRIGGER_LABEL`. If the label does not exist yet, tell the\nuser that GitHub will still record the event once the label is created and\napplied to an issue.\n\nThe automation works an issue when it sees the latest matching `labeled` event\nfor that label. To ask for another attempt later, remove and re-apply the label -\nthat opens a second branch and a second pull request rather than overwriting the\nfirst.\n\n### Step 4 - Collect the pull request mode\n\nAsk: *\"Should the pull requests be opened as drafts?\n 1. Draft (default) - opened as a draft, ready for a human to mark ready\n 2. Ready for review - opened as a normal pull request\n(Press Enter for Draft)\"*\n\nMap the choice to `DRAFT_PULL_REQUEST` (`True` or `False`).\n\n### Step 5 - Collect the branch prefix\n\nAsk: *\"What branch prefix should the automation use?\n(Press Enter for the default: `openhands/issue`, which produces\n`openhands/issue-42`.)\"*\n\nRecord as `BRANCH_PREFIX`. Keep it free of spaces and of characters git rejects\nin a ref name.\n\n### Step 6 - Collect cron schedule\n\nAsk: *\"How often should the automation poll for labelled issues?\n(Press Enter for the default: every 5 minutes.\nUse a cron expression for a different interval, e.g. `0 * * * *` = hourly)\"*\n\nDefault: `*/5 * * * *`.\n\nRecord as `CRON_SCHEDULE`.\n\n### Step 7 - Confirm the secret scope\n\nThe agent is handed `GITHUB_PERSONAL_ACCESS_TOKEN`, because it reads the issue and\nits discussion itself. Ask: *\"Beyond the GitHub token, does the repository's build\nneed a secret of its own - a package registry token, for example? (Press Enter for\nnone.)\"*\n\nRecord the answers appended to the default, as\n`AGENT_SECRET_NAMES = [\"GITHUB_PERSONAL_ACCESS_TOKEN\", \"NAME\", ...]`.\n\nKeep it an allow-list. Forwarding the whole secret store would put every\ncredential in the deployment behind a prompt written by whoever opened the issue.\nIf the repositories are public and you would rather the conversation held no\ncredential at all, set the list to `[]` - the agent can still read a public issue\nunauthenticated, and private repositories then stop working.\n\n### Step 8 - Generate the automation script\n\nRead `scripts/main.py` from this skill's directory. Apply exactly five constant\nsubstitutions near the top of the file:\n\n> The script also reads a `config.json` shipped beside it, if there is one, over\n> these constants. That is how the catalog entry\n> (`automations/catalog/github-issue-to-pr/`) configures an unmodified copy,\n> since a declarative host cannot rewrite Python. This setup path substitutes the\n> constants and ships no `config.json`, so the two never collide.\n\n| Placeholder | Replace with |\n|---|---|\n| `REPOS = [\"owner/repo\"]` | `REPOS = [\"{owner_repo}\", ...]` - one entry per repository collected in Step 2 |\n| `TRIGGER_LABEL = \"openhands\"` | `TRIGGER_LABEL = \"{trigger_label}\"` |\n| `BRANCH_PREFIX = \"openhands/issue\"` | `BRANCH_PREFIX = \"{branch_prefix}\"` |\n| `DRAFT_PULL_REQUEST = True` | `DRAFT_PULL_REQUEST = {True or False}` |\n| `AGENT_SECRET_NAMES: list[str] = []` | `AGENT_SECRET_NAMES: list[str] = [\"{name}\", ...]` |\n\nLeave `MAX_NEW_PER_RUN` and `DEFAULT_OPENHANDS_URL` alone unless the user asks\nfor a different cap or a non-default OpenHands URL.\n\nA repository may be given as `owner/repo`, as a clone URL, or as an SSH remote;\nthe script normalizes each one at startup and names the value it could not read\nrather than blaming the token.\n\nUse a safe string writer such as `json.dumps(value)` when inserting user-provided\nrepository names, labels, or prefixes into Python string literals.\n`json.dumps(list_of_repos)` produces the whole `REPOS` list safely in one step.\n\nWrite the customized script to a temporary build directory:\n```bash\nmkdir -p /tmp/issue-to-pr-build\n# write the customized main.py to /tmp/issue-to-pr-build/main.py\n```\n\nValidate syntax before packaging:\n```bash\npython3 -m py_compile /tmp/issue-to-pr-build/main.py && echo \"Syntax OK\"\n```\n\nFix any syntax errors before proceeding.\n\n### Step 9 - Package and upload\n\nDetermine the Automation backend URL and auth from the `<RUNTIME_SERVICES>`\nblock in your system context:\n- **OPENHANDS_HOST**: the Automation backend `url_from_agent`\n- **Auth**: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`\n\n```bash\ntar -czf /tmp/issue-to-pr.tar.gz -C /tmp/issue-to-pr-build .\n\nTARBALL_PATH=$(curl -s -X POST \\\n \"${OPENHANDS_HOST}/api/automation/v1/uploads?name=github-issue-to-pr\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/gzip\" \\\n --data-binary @/tmp/issue-to-pr.tar.gz \\\n | python3 -c \"import json,sys; print(json.load(sys.stdin)['tarball_path'])\")\n\necho \"Uploaded: $TARBALL_PATH\"\n```\n\n### Step 10 - Register the automation\n\n```bash\ncurl -s -X POST \"${OPENHANDS_HOST}/api/automation/v1\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\n \\\"name\\\": \\\"GitHub Issue to PR: {repo_summary} label {trigger_label}\\\",\n \\\"trigger\\\": {\\\"type\\\": \\\"cron\\\", \\\"schedule\\\": \\\"{cron_schedule}\\\"},\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 900\n }\" | python3 -m json.tool\n```\n\nUse the single repository as `{repo_summary}` when there is one, and something\nlike `3 repos` when there are several. A poll clones a repository per queued\nissue and pushes finished branches, so the timeout allows for that; a run never\nwaits for an agent to finish, only for it to be started.\n\nRecord the returned `id`.\n\n### Step 11 - Confirm\n\nTell the user:\n\n> ✅ **GitHub Issue to PR** is running!\n>\n> - Automation ID: `{id}`\n> - Repositories: `{owner}/{repo}`, ... (one line each)\n> - Trigger label: `{trigger_label}`\n> - Branch prefix: `{branch_prefix}`\n> - Pull requests: `{draft or ready for review}`\n> - Polling schedule: `{cron_schedule}`\n> - State file per repository:\n> `~/.openhands/workspaces/automation-state/github_issue_to_pr_{id}_{owner}__{repo}.json`\n>\n> Apply the `{trigger_label}` label to an issue to queue an implementation. Each\n> label event is processed once. To ask for another attempt, remove and re-apply\n> the label - that opens a second branch and pull request.\n>\n> The agent runs without GitHub credentials; the automation pushes the branch and\n> opens the pull request once the agent has stopped.\n\n---\n\n## Runtime Behaviour (per poll)\n\nEach cron run executes `main.py`, which loads `config.json` if the catalog\nshipped one, checks that `git` is available, resolves and validates\n`GITHUB_PERSONAL_ACCESS_TOKEN` once, then processes every repository in `REPOS`\nindependently. One repository failing does not stop the\nothers; the run fails only if every repository fails.\n\nFor each repository:\n\n1. Loads that repository's state (see `references/state-schema.md`) and reads its\n default branch.\n2. Lists open issues carrying `TRIGGER_LABEL`, newest-updated first. Pull\n requests are dropped, so labelling a PR never queues an implementation.\n3. For each labelled issue, up to `MAX_NEW_PER_RUN` new ones per run:\n - Refetches the issue so a label removed since the listing does not start work.\n - Finds the latest matching GitHub `labeled` event, and skips it if that event\n has already been tracked.\n - Picks the first free branch name, `{BRANCH_PREFIX}-{number}` or a numbered\n variant of it.\n - Clones the default branch, shallow and single-branch, into\n `{WORKSPACE_BASE}/issue-to-pr/{owner}__{repo}/issue-{number}-{event_id}`,\n sets the commit identity, and creates the branch. `origin` keeps its plain\n HTTPS URL, so the workspace holds no credential.\n - Starts an OpenHands conversation **whose working directory is that clone**,\n with the issue title, body, labels, and discussion in the prompt, and only\n the secrets named in `AGENT_SECRET_NAMES` attached.\n - Comments on the issue with the branch, the label event, and the conversation\n link.\n - Records the task with `status: \"active\"`.\n - If the clone or the conversation cannot be created, the clone is removed and\n nothing is recorded, so the next poll retries the label event.\n4. For each active task:\n - Abandons a conversation that has not reached a terminal status within two\n hours, comments on the issue, and reclaims its clone.\n - When the conversation reaches `idle`, `finished`, `error`, or `stuck`:\n - Adopts the pull request the agent opened, if GitHub says one exists for\n the branch, and comments its link on the issue. Everything below is the\n path taken when it does not.\n - Skips the pull request if the issue was closed meanwhile.\n - Reports the problem on the issue if the conversation ended in `error` or\n `stuck`.\n - Commits whatever the agent left uncommitted, on top of any commits it made\n itself.\n - Posts the agent's answer on the issue, and opens no pull request, when\n there are no commits at all - that is how an agent reports an issue too\n ambiguous to implement.\n - Otherwise pushes the branch, opens the pull request (draft by default,\n titled `[#42] <issue title>`, with the agent's summary and `Closes #42` in\n the body), and comments the link on the issue.\n - A push or pull request that fails is retried on the next two polls before\n the task is reported as failed, so a transient GitHub error does not throw\n the work away.\n5. Removes the clone of every finished task, but only after confirming the\n conversation has stopped - deleting it under a running agent would remove its\n working directory. When that cannot be confirmed the directory is left alone\n and the next poll tries again.\n6. Saves that repository's state atomically.\n\nThe completion callback fires once for the whole run.\n\n---\n\n## Additional Resources\n\n- **`references/state-schema.md`** - State JSON schema, field definitions, and the\n task lifecycle.\n- **`scripts/main.py`** - The complete automation script. Customize the five\n constants at the top before packaging.\n\n---\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---|---|---|\n| 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 |\n| \"Bad credentials\" in run logs | Token expired | Rotate and update `GITHUB_PERSONAL_ACCESS_TOKEN` |\n| \"The token cannot push to ...\" | Token lacks Contents: write on that repository | Issue a token with write access, or drop the repository from `REPOS` |\n| 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 |\n| 404 on repo access | Repo name wrong or no access | Re-check the entry in `REPOS` and the token's permissions |\n| `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 |\n| 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 |\n| Same issue not picked up again after new comments | Its label event was already processed | Remove and re-apply the trigger label |\n| 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 |\n| 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 |\n| Clones remain under `issue-to-pr/` | Their conversations had not stopped yet | They are removed by a later poll once the conversation is terminal |",
243
+ "category": "automations"
244
+ },
227
245
  {
228
246
  "name": "github-pr-review",
229
247
  "description": "Post PR review comments using the GitHub API with inline comments, suggestions, and priority labels.",
@@ -339,6 +357,15 @@ export const SKILLS_CATALOG = [
339
357
  "content": "# Linear Issue Triage Automation\n\nSet up an automation that triages new Linear issues — classifying, labeling,\nand suggesting owners automatically.\n\n---\n\n## Prerequisites\n\n### Required integration\n\n- **Linear MCP** must be installed in Settings → MCP.\n\n### Information to collect\n\nAsk the user for:\n\n1. **Teams/projects** — which Linear teams or projects should be triaged (e.g. `Engineering`, `Support`)\n2. **Label taxonomy** — what labels are used for classification? (e.g. `bug`, `feature`, `support`, `chore`)\n3. **Priority conventions** — how does the team use priority levels? Any mapping rules?\n4. **Auto-apply or suggest** — should the automation apply labels/priority/assignee directly, or post a triage comment with suggestions for human approval?\n5. **Duplicate detection** — should it search for and flag potential duplicate issues?\n\n---\n\n## Setup Workflow\n\n### Step 1 — Verify Linear MCP access\n\nConfirm the Linear MCP integration is working:\n```\nUse the Linear MCP to list recent issues for one of the target teams.\n```\n\nIf it fails, tell the user to install the Linear MCP integration first.\n\n### Step 2 — Determine trigger type\n\n**Event-based (recommended if publicly reachable):**\nCheck `<RUNTIME_SERVICES>` for deployment reachability. If public, recommend an event trigger on Linear `Issue` create events.\n\n**Cron-based (local/private deployments):**\nPoll for recently created issues on a schedule (e.g. every 5 minutes).\n\n### Step 3 — Build the triage prompt\n\nConstruct a prompt that includes:\n- Target teams/projects\n- Label taxonomy and classification rules\n- Priority mapping conventions\n- Whether to auto-apply or suggest\n- Duplicate detection preference\n\n### Step 4 — Create the automation\n\nRead the Automation backend URL and auth from `<RUNTIME_SERVICES>`:\n- Use the **Automation backend** `url_from_agent` as `OPENHANDS_HOST`\n- Auth: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`\n\nUse the **prompt preset** endpoint:\n```bash\ncurl -s -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Linear Issue Triage\",\n \"prompt\": \"<constructed triage prompt>\",\n \"trigger\": <trigger config from step 2>\n }'\n```\n\nPowerShell note: use `curl.exe` for this exact flag syntax, and replace `${OPENHANDS_HOST}` / `$OPENHANDS_AUTOMATION_API_KEY` with `$env:OPENHANDS_HOST` / `$env:OPENHANDS_AUTOMATION_API_KEY` if running it natively.\n\n### Step 5 — Confirm\n\nTell the user:\n> ✅ **Linear Issue Triage** is running!\n>\n> - Automation ID: `{id}`\n> - Teams: `{team list}`\n> - Mode: `{auto-apply or suggest}`\n> - Trigger: `{trigger description}`",
340
358
  "category": "automations"
341
359
  },
360
+ {
361
+ "name": "news-digest",
362
+ "description": "Create an automation that reads a list of public RSS and Atom feeds on a schedule - daily by default - keeps what is new and matches the configured topics, and has an agent write a short digest of it. It needs no credentials: the feeds are public URLs and the conversation is started with no secrets and no MCP servers.",
363
+ "triggers": [
364
+ "/news-digest:setup"
365
+ ],
366
+ "content": "# Daily News Digest Automation\n\nCreate a cron automation that turns a list of feeds into something worth\nreading: a few hundred words a day on what happened in the topics you care\nabout, with a link under each item.\n\n**It connects to nothing.** There is no token to issue, no account to link, no\nOAuth screen. That makes it the automation to run first - it exercises the\nschedule, the conversation, the model and the run log end to end while nothing\nof yours is at stake, and it is useful in its own right afterwards.\n\nThe script is deterministic: the schedule, the once-a-day claim, fetching,\nparsing, the freshness window, and remembering what has already been covered\nare all Python. The LLM is invoked only for the part that\nis judgement - reading the shortlist and writing something that is not just the\nheadlines pasted back. **When nothing new matches, no conversation is started\nat all**, so a quiet day costs no tokens.\n\n---\n\n## Prerequisites\n\nNone. That is the point.\n\nThe runtime needs outbound HTTPS to the feed hosts, which it already has if it\ncan reach the model. Nothing is read from **Settings -> Secrets**, and nothing\nneeds to be added there.\n\n---\n\n## Setup Workflow\n\nFollow these steps in order.\n\n### Step 1 - Collect the feeds\n\nAsk: *\"Which feeds should the digest read? (One RSS or Atom URL per line. Press\nEnter for a general technology set.)\"*\n\nDefault:\n\n```\nhttps://news.ycombinator.com/rss\nhttps://feeds.arstechnica.com/arstechnica/index\nhttps://www.theverge.com/rss/index.xml\n```\n\nCheck each one before accepting it, because a URL that returns a web page\nrather than a feed is the most common setup mistake:\n\n```bash\ncurl -sSL --max-time 20 -A \"OpenHands-News-Digest/1.0\" \"{feed_url}\" \\\n | python3 -c \"\nimport sys\nfrom xml.etree import ElementTree\ntry:\n root = ElementTree.fromstring(sys.stdin.buffer.read())\nexcept ElementTree.ParseError as exc:\n print('ERROR: not valid XML:', exc); raise SystemExit\nname = root.tag.rsplit('}', 1)[-1]\nitems = [e for e in root.iter() if e.tag.rsplit('}', 1)[-1] in ('item', 'entry')]\nprint(f'OK: <{name}> with {len(items)} entries' if name.lower() in ('rss', 'feed', 'rdf')\n else f'ERROR: root element is <{name}>, which is not a feed')\n\"\n```\n\nRecord every accepted URL into `FEEDS = [\"...\", ...]`. A feed that fails at\nruntime is reported and skipped, so one bad URL does not cost you the digest -\nbut it is better to find out now.\n\n### Step 2 - Collect the topics\n\nAsk: *\"What should the digest be about? (One topic per line, or comma\nseparated. Press Enter for `artificial intelligence, open source, developer\ntools`. Leave it blank to summarise everything the feeds carry.)\"*\n\nRecord as `TOPICS`. Two things are worth telling the user:\n\n- The agent decides which stories are about them, reading each headline and\n excerpt. So write topics the way you would explain your interests to a\n colleague - `artificial intelligence` works even though almost every headline\n says `AI`, and a story about a company releasing its model weights counts as\n `open source` without using the phrase.\n- An empty list means \"cover whatever is most significant\". That is right for a\n handful of narrow feeds and vaguer for a firehose.\n\n### Step 3 - Collect the schedule\n\nAsk: *\"When should the digest be written? (Press Enter for the default: every\nday at 08:00 UTC, `0 8 * * *`.)\"*\n\nDefault: `0 8 * * *`. Record as `CRON_SCHEDULE`, and the timezone as\n`CRON_TIMEZONE` (default `UTC`).\n\nA schedule more frequent than daily is allowed and is not wasteful: work is\nkeyed by UTC date, so extra runs stop at a state read once the day is done, and\nbefore that they cost one HTTP request per feed and no tokens. It is a\nreasonable way to say \"write the digest as soon as there is anything to write\".\n\n### Step 4 - Confirm the secret scope\n\nDo **not** ask which secrets to forward. `AGENT_SECRET_NAMES` is empty and\nshould stay empty: the conversation summarises text fetched from the open web,\nwhich is written by strangers, and a credential handed to it would make every\nfeed on the list an instruction channel into the deployment's secret store.\n\nIf the user asks for a digest posted to Slack, Notion or a repository, that is a\ndifferent automation - it needs that integration connected, and it should be\nbuilt from the skill for it rather than by widening this one.\n\n### Step 5 - Generate the automation script\n\nRead `scripts/main.py` from this skill's directory. Apply exactly two constant\nsubstitutions near the top of the file:\n\n> The script also reads a `config.json` shipped beside it, if there is one, over\n> these constants. That is how the catalog entry\n> (`automations/catalog/news-digest/`) configures an unmodified copy, since a\n> declarative host cannot rewrite Python. This setup path substitutes the\n> constants and ships no `config.json`, so the two never collide.\n\n| Placeholder | Replace with |\n|---|---|\n| `FEEDS = [...]` | the list from Step 1 |\n| `TOPICS = [...]` | the list from Step 2, or `[]` for no filter |\n\n`LOOKBACK_HOURS` (48) and `MAX_ITEMS` (50) are left alone unless the user asks.\nThe lookback is deliberately wider than the schedule so a failed or missed run\nis recovered by the next one; the seen-list is what stops the overlap from\nrepeating anything.\n\nUse a safe string writer such as `json.dumps(value)` when inserting\nuser-provided URLs or topics into Python string literals.\n\nWrite the customized script to a temporary build directory and validate it:\n\n```bash\nmkdir -p /tmp/news-digest-build\n# write the customized main.py to /tmp/news-digest-build/main.py\npython3 -m py_compile /tmp/news-digest-build/main.py && echo \"Syntax OK\"\n```\n\n### Step 6 - Package and upload\n\nDetermine the Automation backend URL and auth from the `<RUNTIME_SERVICES>`\nblock in your system context:\n- **OPENHANDS_HOST**: the Automation backend `url_from_agent`\n- **Auth**: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`\n\n```bash\ntar -czf /tmp/news-digest.tar.gz -C /tmp/news-digest-build .\n\nTARBALL_PATH=$(curl -s -X POST \\\n \"${OPENHANDS_HOST}/api/automation/v1/uploads?name=news-digest\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/gzip\" \\\n --data-binary @/tmp/news-digest.tar.gz \\\n | python3 -c \"import json,sys; print(json.load(sys.stdin)['tarball_path'])\")\n\necho \"Uploaded: $TARBALL_PATH\"\n```\n\n### Step 7 - Register the automation\n\n```bash\ncurl -s -X POST \"${OPENHANDS_HOST}/api/automation/v1\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\n \\\"name\\\": \\\"Daily news digest\\\",\n \\\"trigger\\\": {\\\"type\\\": \\\"cron\\\", \\\"schedule\\\": \\\"{cron_schedule}\\\", \\\"timezone\\\": \\\"{cron_timezone}\\\"},\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 900\n }\" | python3 -m json.tool\n```\n\nRecord the returned `id`.\n\n### Step 8 - Confirm\n\nTell the user:\n\n> ✅ **Daily news digest** is running!\n>\n> - Automation ID: `{id}`\n> - Feeds: `{url}`, ... (one line each)\n> - Topics: `{topics}` (or: no filter - everything the feeds carry)\n> - Schedule: `{cron_schedule}` ({cron_timezone})\n> - Credentials used: **none**\n> - State file: `~/.openhands/workspaces/automation-state/news_digest_{id}.json`\n>\n> Each day it reads the feeds and writes the digest into the run's conversation\n> and the run log. A day with nothing new produces nothing and costs nothing -\n> the day stays open, so a later run picks up news published after this one.\n\nThen offer to dispatch it once so they can read today's digest immediately:\n\n```bash\ncurl -s -X POST \"${OPENHANDS_HOST}/api/automation/v1/{id}/dispatch\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\"\n```\n\n---\n\n## Runtime Behaviour (per run)\n\nEach cron run executes `main.py`, which loads `config.json` if the catalog\nshipped one and then:\n\n1. Loads state (the automation service's KV store, or a local JSON file when\n the store is unavailable).\n2. Computes today's UTC date and **stops immediately if it is already\n recorded** - no feed is fetched, so an extra run inside a finished day costs\n one state read.\n3. Otherwise fetches every feed with a 20-second timeout and a 4 MB cap, and\n parses RSS 2.0, RSS 1.0/RDF and Atom by local element name. A feed that\n fails, is not XML, or is XML that is not a feed is recorded and skipped. The\n run fails only if *every* feed fails.\n4. Selects the stories: not already covered, and published within\n `LOOKBACK_HOURS` (undated stories are treated as current rather than\n dropped). The newest `MAX_ITEMS` survive. Subject is deliberately not a\n filter here.\n5. If none survive, records the check, says which stage emptied it, and\n **leaves the day unclaimed** so a later run can try again. No conversation,\n no tokens.\n6. Otherwise claims the day in state *before* the slow work, so an overlapping\n run cannot write the digest twice, then starts an OpenHands conversation\n with the stories **and the topics** in its prompt, an empty secrets payload\n and no MCP servers, working in `{WORKSPACE_BASE}/news-digest/{date}`. The\n agent decides which stories are relevant before it writes anything.\n7. When the conversation reaches `idle`, `finished`, `error` or `stuck`:\n - reads `digest.md` from the working directory, falling back to the agent's\n final message;\n - prints the digest into the run log and keeps its opening in state;\n - records the stories as covered **only now**, so a run that failed leaves\n them for the next one;\n - removes the working directory once the conversation is confirmed stopped.\n8. Prunes the task history to the last 14 days and the seen-list to 1000\n fingerprints, both so the state document stays inside the KV store's 64 KB\n value limit.\n\n### How a story is recognised again\n\nTwo fingerprints per story: one over the feed's own identifier (`guid`/`id`),\none over its link with the host lowercased, the fragment removed and campaign\nparameters stripped. A story counts as already covered if **either** matches.\nBoth are needed - a feed whose links carry a per-fetch campaign tag is only\nrecognisable by its identifier, and two publishers syndicating the same article\nagree on nothing *but* the link.\n\n---\n\n## Additional Resources\n\n- **`references/state-schema.md`** - State JSON schema and the task lifecycle.\n- **`scripts/main.py`** - The complete automation script.\n\n---\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---|---|---|\n| Every run says \"Nothing new to digest\" | Nothing was published, or it was all covered already | The run log now names which of the two it was; the digest covers what the topics miss only if the feeds carry it at all |\n| The digest ignores a topic you care about | The feeds do not carry stories about it | Add a feed that does; the agent can only choose from what was fetched |\n| A feed reports \"not a feed\" | The URL serves a web page, not the feed | Find the real feed URL - it is usually linked from the page as `application/rss+xml` |\n| A feed reports an HTTP 403 | The host blocks unknown readers | Use a different feed for that source; this automation sends no credentials by design |\n| The digest is thin and full of \"Headlines\" | Those feeds carry titles only | Expected for Hacker News and similar; add a feed that publishes summaries, such as Ars Technica |\n| Nothing happens after a manual dispatch | Today is already recorded in state | Read the previous run's log for the digest, or clear today's entry from the state document |\n| A day was missed entirely | The run failed, or the service was down | The next run covers it: the lookback window is 48 hours and failed runs deliberately remember nothing |\n| Digest repeats a story | Two feeds identify the same article differently, and neither the guid nor the canonical link matched | Expected occasionally; the prompt asks the agent to merge duplicate coverage it can see |",
367
+ "category": "automations"
368
+ },
342
369
  {
343
370
  "name": "notion",
344
371
  "description": "Create, search, and update Notion pages/databases using the Notion API. Use for documenting work, generating runbooks, and automating knowledge base updates.",
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "news-digest",
3
+ "version": "1.0.0",
4
+ "description": "Create an automation that reads public RSS and Atom feeds on a schedule, keeps what is new and matches the configured topics, and has an agent write a short digest of it. Needs no credentials.",
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
+ "news",
14
+ "rss",
15
+ "atom",
16
+ "digest",
17
+ "automation"
18
+ ]
19
+ }
@@ -0,0 +1,71 @@
1
+ # Daily News Digest
2
+
3
+ Create an automation that reads a list of public RSS and Atom feeds on a
4
+ schedule, hands an agent everything new, and has it pick out what matters for
5
+ your topics and write a short digest.
6
+
7
+ **It needs no credentials.** No token, no OAuth, no connected account - which
8
+ makes it the automation to run first, before deciding what access you are
9
+ willing to hand over.
10
+
11
+ ## Trigger
12
+
13
+ This skill is activated by:
14
+
15
+ - `/news-digest:setup`
16
+
17
+ ## Features
18
+
19
+ - Reads RSS 2.0, RSS 1.0/RDF and Atom, matched by element name rather than by
20
+ dialect, so a mixed feed list works
21
+ - Filters to what is new since the last digest and recent; what a story is
22
+ *about* is left to the agent, because that is the half with no right answer
23
+ - Starts no conversation at all when nothing matches, so a quiet day costs no
24
+ tokens and leaves the day open for a later run
25
+ - Recognises the same story arriving from two feeds, by the feed's identifier
26
+ and by the article's canonical link
27
+ - Runs daily by default, keyed by UTC date, so a retried or duplicated run
28
+ cannot write the same digest twice
29
+ - Survives a feed that is down, moved, or no longer a feed; fails only when
30
+ every feed fails
31
+ - Remembers what it reported **only once a digest exists**, so a failed run is
32
+ recovered by the next one instead of being silently skipped
33
+ - Keeps its own state small enough for the KV store's 64 KB limit, indefinitely
34
+
35
+ ## What the agent is asked to do
36
+
37
+ Read every new story the script fetched, decide which ones are actually about
38
+ the configured topics - a judgement call rather than a word search - and write
39
+ four to six hundred words about those: a lead on what actually matters, the rest grouped by topic,
40
+ one or two sentences and a link per story, and duplicate coverage of one event
41
+ merged into a single item. It is told that every claim must be supported by an
42
+ excerpt or a page it actually read, that a story it cannot substantiate belongs
43
+ in a headlines list rather than being guessed at, and that feed text is data to
44
+ be summarised - never instructions to follow.
45
+
46
+ ## Prerequisites
47
+
48
+ None. The runtime needs outbound HTTPS to the feed hosts, which it already has
49
+ if it can reach the model.
50
+
51
+ ## Quick Start
52
+
53
+ Ask OpenHands:
54
+
55
+ > "Set up a daily news digest at 8am for AI and open source news."
56
+
57
+ ## Where the digest goes
58
+
59
+ Nowhere that needs a credential, which is the trade: it stays in the run's
60
+ conversation, it is printed into the run log, and its opening is kept in state
61
+ so the next run's log can show what the last one said. Posting it to Slack,
62
+ Notion or a repository is a different automation, built from the skill for that
63
+ integration.
64
+
65
+ ## See Also
66
+
67
+ - [SKILL.md](SKILL.md) - Full setup workflow reference
68
+ - [references/state-schema.md](references/state-schema.md) - State document and
69
+ task lifecycle
70
+ - [../research-brief/SKILL.md](../research-brief/SKILL.md) - The credentialed
71
+ version of this idea: web search, then publish the brief
@@ -0,0 +1,284 @@
1
+ ---
2
+ name: news-digest
3
+ description: >
4
+ Create an automation that reads a list of public RSS and Atom feeds on a
5
+ schedule - daily by default - keeps what is new and matches the configured
6
+ topics, and has an agent write a short digest of it. It needs no credentials:
7
+ the feeds are public URLs and the conversation is started with no secrets and
8
+ no MCP servers.
9
+ triggers:
10
+ - /news-digest:setup
11
+ ---
12
+
13
+ # Daily News Digest Automation
14
+
15
+ Create a cron automation that turns a list of feeds into something worth
16
+ reading: a few hundred words a day on what happened in the topics you care
17
+ about, with a link under each item.
18
+
19
+ **It connects to nothing.** There is no token to issue, no account to link, no
20
+ OAuth screen. That makes it the automation to run first - it exercises the
21
+ schedule, the conversation, the model and the run log end to end while nothing
22
+ of yours is at stake, and it is useful in its own right afterwards.
23
+
24
+ The script is deterministic: the schedule, the once-a-day claim, fetching,
25
+ parsing, the freshness window, and remembering what has already been covered
26
+ are all Python. The LLM is invoked only for the part that
27
+ is judgement - reading the shortlist and writing something that is not just the
28
+ headlines pasted back. **When nothing new matches, no conversation is started
29
+ at all**, so a quiet day costs no tokens.
30
+
31
+ ---
32
+
33
+ ## Prerequisites
34
+
35
+ None. That is the point.
36
+
37
+ The runtime needs outbound HTTPS to the feed hosts, which it already has if it
38
+ can reach the model. Nothing is read from **Settings -> Secrets**, and nothing
39
+ needs to be added there.
40
+
41
+ ---
42
+
43
+ ## Setup Workflow
44
+
45
+ Follow these steps in order.
46
+
47
+ ### Step 1 - Collect the feeds
48
+
49
+ Ask: *"Which feeds should the digest read? (One RSS or Atom URL per line. Press
50
+ Enter for a general technology set.)"*
51
+
52
+ Default:
53
+
54
+ ```
55
+ https://news.ycombinator.com/rss
56
+ https://feeds.arstechnica.com/arstechnica/index
57
+ https://www.theverge.com/rss/index.xml
58
+ ```
59
+
60
+ Check each one before accepting it, because a URL that returns a web page
61
+ rather than a feed is the most common setup mistake:
62
+
63
+ ```bash
64
+ curl -sSL --max-time 20 -A "OpenHands-News-Digest/1.0" "{feed_url}" \
65
+ | python3 -c "
66
+ import sys
67
+ from xml.etree import ElementTree
68
+ try:
69
+ root = ElementTree.fromstring(sys.stdin.buffer.read())
70
+ except ElementTree.ParseError as exc:
71
+ print('ERROR: not valid XML:', exc); raise SystemExit
72
+ name = root.tag.rsplit('}', 1)[-1]
73
+ items = [e for e in root.iter() if e.tag.rsplit('}', 1)[-1] in ('item', 'entry')]
74
+ print(f'OK: <{name}> with {len(items)} entries' if name.lower() in ('rss', 'feed', 'rdf')
75
+ else f'ERROR: root element is <{name}>, which is not a feed')
76
+ "
77
+ ```
78
+
79
+ Record every accepted URL into `FEEDS = ["...", ...]`. A feed that fails at
80
+ runtime is reported and skipped, so one bad URL does not cost you the digest -
81
+ but it is better to find out now.
82
+
83
+ ### Step 2 - Collect the topics
84
+
85
+ Ask: *"What should the digest be about? (One topic per line, or comma
86
+ separated. Press Enter for `artificial intelligence, open source, developer
87
+ tools`. Leave it blank to summarise everything the feeds carry.)"*
88
+
89
+ Record as `TOPICS`. Two things are worth telling the user:
90
+
91
+ - The agent decides which stories are about them, reading each headline and
92
+ excerpt. So write topics the way you would explain your interests to a
93
+ colleague - `artificial intelligence` works even though almost every headline
94
+ says `AI`, and a story about a company releasing its model weights counts as
95
+ `open source` without using the phrase.
96
+ - An empty list means "cover whatever is most significant". That is right for a
97
+ handful of narrow feeds and vaguer for a firehose.
98
+
99
+ ### Step 3 - Collect the schedule
100
+
101
+ Ask: *"When should the digest be written? (Press Enter for the default: every
102
+ day at 08:00 UTC, `0 8 * * *`.)"*
103
+
104
+ Default: `0 8 * * *`. Record as `CRON_SCHEDULE`, and the timezone as
105
+ `CRON_TIMEZONE` (default `UTC`).
106
+
107
+ A schedule more frequent than daily is allowed and is not wasteful: work is
108
+ keyed by UTC date, so extra runs stop at a state read once the day is done, and
109
+ before that they cost one HTTP request per feed and no tokens. It is a
110
+ reasonable way to say "write the digest as soon as there is anything to write".
111
+
112
+ ### Step 4 - Confirm the secret scope
113
+
114
+ Do **not** ask which secrets to forward. `AGENT_SECRET_NAMES` is empty and
115
+ should stay empty: the conversation summarises text fetched from the open web,
116
+ which is written by strangers, and a credential handed to it would make every
117
+ feed on the list an instruction channel into the deployment's secret store.
118
+
119
+ If the user asks for a digest posted to Slack, Notion or a repository, that is a
120
+ different automation - it needs that integration connected, and it should be
121
+ built from the skill for it rather than by widening this one.
122
+
123
+ ### Step 5 - Generate the automation script
124
+
125
+ Read `scripts/main.py` from this skill's directory. Apply exactly two constant
126
+ substitutions near the top of the file:
127
+
128
+ > The script also reads a `config.json` shipped beside it, if there is one, over
129
+ > these constants. That is how the catalog entry
130
+ > (`automations/catalog/news-digest/`) configures an unmodified copy, since a
131
+ > declarative host cannot rewrite Python. This setup path substitutes the
132
+ > constants and ships no `config.json`, so the two never collide.
133
+
134
+ | Placeholder | Replace with |
135
+ |---|---|
136
+ | `FEEDS = [...]` | the list from Step 1 |
137
+ | `TOPICS = [...]` | the list from Step 2, or `[]` for no filter |
138
+
139
+ `LOOKBACK_HOURS` (48) and `MAX_ITEMS` (50) are left alone unless the user asks.
140
+ The lookback is deliberately wider than the schedule so a failed or missed run
141
+ is recovered by the next one; the seen-list is what stops the overlap from
142
+ repeating anything.
143
+
144
+ Use a safe string writer such as `json.dumps(value)` when inserting
145
+ user-provided URLs or topics into Python string literals.
146
+
147
+ Write the customized script to a temporary build directory and validate it:
148
+
149
+ ```bash
150
+ mkdir -p /tmp/news-digest-build
151
+ # write the customized main.py to /tmp/news-digest-build/main.py
152
+ python3 -m py_compile /tmp/news-digest-build/main.py && echo "Syntax OK"
153
+ ```
154
+
155
+ ### Step 6 - Package and upload
156
+
157
+ Determine the Automation backend URL and auth from the `<RUNTIME_SERVICES>`
158
+ block in your system context:
159
+ - **OPENHANDS_HOST**: the Automation backend `url_from_agent`
160
+ - **Auth**: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`
161
+
162
+ ```bash
163
+ tar -czf /tmp/news-digest.tar.gz -C /tmp/news-digest-build .
164
+
165
+ TARBALL_PATH=$(curl -s -X POST \
166
+ "${OPENHANDS_HOST}/api/automation/v1/uploads?name=news-digest" \
167
+ -H "X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY" \
168
+ -H "Content-Type: application/gzip" \
169
+ --data-binary @/tmp/news-digest.tar.gz \
170
+ | python3 -c "import json,sys; print(json.load(sys.stdin)['tarball_path'])")
171
+
172
+ echo "Uploaded: $TARBALL_PATH"
173
+ ```
174
+
175
+ ### Step 7 - Register the automation
176
+
177
+ ```bash
178
+ curl -s -X POST "${OPENHANDS_HOST}/api/automation/v1" \
179
+ -H "X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY" \
180
+ -H "Content-Type: application/json" \
181
+ -d "{
182
+ \"name\": \"Daily news digest\",
183
+ \"trigger\": {\"type\": \"cron\", \"schedule\": \"{cron_schedule}\", \"timezone\": \"{cron_timezone}\"},
184
+ \"tarball_path\": \"$TARBALL_PATH\",
185
+ \"entrypoint\": \"python3 main.py\",
186
+ \"timeout\": 900
187
+ }" | python3 -m json.tool
188
+ ```
189
+
190
+ Record the returned `id`.
191
+
192
+ ### Step 8 - Confirm
193
+
194
+ Tell the user:
195
+
196
+ > ✅ **Daily news digest** is running!
197
+ >
198
+ > - Automation ID: `{id}`
199
+ > - Feeds: `{url}`, ... (one line each)
200
+ > - Topics: `{topics}` (or: no filter - everything the feeds carry)
201
+ > - Schedule: `{cron_schedule}` ({cron_timezone})
202
+ > - Credentials used: **none**
203
+ > - State file: `~/.openhands/workspaces/automation-state/news_digest_{id}.json`
204
+ >
205
+ > Each day it reads the feeds and writes the digest into the run's conversation
206
+ > and the run log. A day with nothing new produces nothing and costs nothing -
207
+ > the day stays open, so a later run picks up news published after this one.
208
+
209
+ Then offer to dispatch it once so they can read today's digest immediately:
210
+
211
+ ```bash
212
+ curl -s -X POST "${OPENHANDS_HOST}/api/automation/v1/{id}/dispatch" \
213
+ -H "X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY"
214
+ ```
215
+
216
+ ---
217
+
218
+ ## Runtime Behaviour (per run)
219
+
220
+ Each cron run executes `main.py`, which loads `config.json` if the catalog
221
+ shipped one and then:
222
+
223
+ 1. Loads state (the automation service's KV store, or a local JSON file when
224
+ the store is unavailable).
225
+ 2. Computes today's UTC date and **stops immediately if it is already
226
+ recorded** - no feed is fetched, so an extra run inside a finished day costs
227
+ one state read.
228
+ 3. Otherwise fetches every feed with a 20-second timeout and a 4 MB cap, and
229
+ parses RSS 2.0, RSS 1.0/RDF and Atom by local element name. A feed that
230
+ fails, is not XML, or is XML that is not a feed is recorded and skipped. The
231
+ run fails only if *every* feed fails.
232
+ 4. Selects the stories: not already covered, and published within
233
+ `LOOKBACK_HOURS` (undated stories are treated as current rather than
234
+ dropped). The newest `MAX_ITEMS` survive. Subject is deliberately not a
235
+ filter here.
236
+ 5. If none survive, records the check, says which stage emptied it, and
237
+ **leaves the day unclaimed** so a later run can try again. No conversation,
238
+ no tokens.
239
+ 6. Otherwise claims the day in state *before* the slow work, so an overlapping
240
+ run cannot write the digest twice, then starts an OpenHands conversation
241
+ with the stories **and the topics** in its prompt, an empty secrets payload
242
+ and no MCP servers, working in `{WORKSPACE_BASE}/news-digest/{date}`. The
243
+ agent decides which stories are relevant before it writes anything.
244
+ 7. When the conversation reaches `idle`, `finished`, `error` or `stuck`:
245
+ - reads `digest.md` from the working directory, falling back to the agent's
246
+ final message;
247
+ - prints the digest into the run log and keeps its opening in state;
248
+ - records the stories as covered **only now**, so a run that failed leaves
249
+ them for the next one;
250
+ - removes the working directory once the conversation is confirmed stopped.
251
+ 8. Prunes the task history to the last 14 days and the seen-list to 1000
252
+ fingerprints, both so the state document stays inside the KV store's 64 KB
253
+ value limit.
254
+
255
+ ### How a story is recognised again
256
+
257
+ Two fingerprints per story: one over the feed's own identifier (`guid`/`id`),
258
+ one over its link with the host lowercased, the fragment removed and campaign
259
+ parameters stripped. A story counts as already covered if **either** matches.
260
+ Both are needed - a feed whose links carry a per-fetch campaign tag is only
261
+ recognisable by its identifier, and two publishers syndicating the same article
262
+ agree on nothing *but* the link.
263
+
264
+ ---
265
+
266
+ ## Additional Resources
267
+
268
+ - **`references/state-schema.md`** - State JSON schema and the task lifecycle.
269
+ - **`scripts/main.py`** - The complete automation script.
270
+
271
+ ---
272
+
273
+ ## Troubleshooting
274
+
275
+ | Symptom | Likely cause | Fix |
276
+ |---|---|---|
277
+ | Every run says "Nothing new to digest" | Nothing was published, or it was all covered already | The run log now names which of the two it was; the digest covers what the topics miss only if the feeds carry it at all |
278
+ | The digest ignores a topic you care about | The feeds do not carry stories about it | Add a feed that does; the agent can only choose from what was fetched |
279
+ | A feed reports "not a feed" | The URL serves a web page, not the feed | Find the real feed URL - it is usually linked from the page as `application/rss+xml` |
280
+ | A feed reports an HTTP 403 | The host blocks unknown readers | Use a different feed for that source; this automation sends no credentials by design |
281
+ | The digest is thin and full of "Headlines" | Those feeds carry titles only | Expected for Hacker News and similar; add a feed that publishes summaries, such as Ars Technica |
282
+ | Nothing happens after a manual dispatch | Today is already recorded in state | Read the previous run's log for the digest, or clear today's entry from the state document |
283
+ | A day was missed entirely | The run failed, or the service was down | The next run covers it: the lookback window is 48 hours and failed runs deliberately remember nothing |
284
+ | Digest repeats a story | Two feeds identify the same article differently, and neither the guid nor the canonical link matched | Expected occasionally; the prompt asks the agent to merge duplicate coverage it can see |
@@ -0,0 +1,8 @@
1
+ ---
2
+ # auto-generated by sync_extensions.py
3
+ description: Create an automation that reads a list of public RSS and Atom feeds on a schedule - daily by default - keeps what is new and matches the configured topics, and has an agent write a short digest of it. It needs no credentials: the feeds are public URLs and the conversation is started with no secrets and no MCP servers.
4
+ ---
5
+
6
+ Read and follow the complete instructions in the SKILL.md file located in this skill's directory.
7
+
8
+ $ARGUMENTS