@openhands/agent-canvas 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +1 -1
  2. package/README.windows.md +2 -2
  3. package/build/assets/{active-backend-context-DRFevVIj.js → active-backend-context-u2tdYRgH.js} +1 -1
  4. package/build/assets/{add-backend-modal-DzuB9zlG.js → add-backend-modal-DIEyAkA4.js} +1 -1
  5. package/build/assets/{agent-profiles-settings-BfAwTshW.js → agent-profiles-settings-BovdV4N8.js} +1 -1
  6. package/build/assets/{agent-server-conversation-service.api-DfH2k6Sx.js → agent-server-conversation-service.api-BP73yONA.js} +1 -1
  7. package/build/assets/agent-settings-FGL3Yvkp.js +1 -0
  8. package/build/assets/{agent-settings-DjVlKgaP.js → agent-settings-Ofbh6UTD.js} +1 -1
  9. package/build/assets/{api-key-entry-screen-O8gS-fnF.js → api-key-entry-screen-CZIdRmUJ.js} +1 -1
  10. package/build/assets/{app-settings-SlZvaXlk.js → app-settings-BHJ9nKti.js} +1 -1
  11. package/build/assets/{automation-detail-CwsNJ9AM.js → automation-detail-DwYGzLkp.js} +1 -1
  12. package/build/assets/{automations-list-B9cPtugh.js → automations-list-CuAfJmMW.js} +1 -1
  13. package/build/assets/{backend-form-modal-D2EDGVAc.js → backend-form-modal-BiaOB2C4.js} +1 -1
  14. package/build/assets/{backend-synced-settings-badge-6OCwIuLY.js → backend-synced-settings-badge-Bs3FSXsS.js} +1 -1
  15. package/build/assets/{color-themes-Dd1sO5U6.js → color-themes-DPBzNtVg.js} +3 -3
  16. package/build/assets/{condenser-settings-BhxvlfoY.js → condenser-settings-D_5vY4m3.js} +1 -1
  17. package/build/assets/conversation-DnVC6vRV.js +1 -0
  18. package/build/assets/conversation-panel-ZEz-z3az.js +1 -0
  19. package/build/assets/{conversation-service.api-Ch9kR9aE.js → conversation-service.api-CqZcHr1c.js} +1 -1
  20. package/build/assets/{conversation-B8prFAZF.js → conversation-xkBS3AHC.js} +3 -3
  21. package/build/assets/{edit-automation-modal-BoibW8cd.js → edit-automation-modal-B6U96gVV.js} +1 -1
  22. package/build/assets/{entry.client-BgWj9jL_.js → entry.client-Dy--3GeA.js} +1 -1
  23. package/build/assets/{extensions-hub-MtC8lK3A.js → extensions-hub-CksKNpP9.js} +1 -1
  24. package/build/assets/{extensions-navigation-BtV14oQL.js → extensions-navigation-Di9rJ-b5.js} +1 -1
  25. package/build/assets/{files-tab-DHqtsKLy.js → files-tab-61Vaihc6.js} +1 -1
  26. package/build/assets/{git-provider-icon-CGU7_KiU.js → git-provider-icon-aQRcUVI-.js} +1 -1
  27. package/build/assets/{home-BgjWP_UN.js → home-5gOtUe-n.js} +1 -1
  28. package/build/assets/{install-server-modal-J_eQ6lhG.js → install-server-modal-BRab9inF.js} +1 -1
  29. package/build/assets/{launch-DamfAY5L.js → launch-1TJRKkkX.js} +1 -1
  30. package/build/assets/{lesson-plan-HD886V64.js → lesson-plan-BtrZUCuj.js} +1 -1
  31. package/build/assets/{llm-not-configured-banner-DTrLsVr_.js → llm-not-configured-banner-BrU5uHto.js} +1 -1
  32. package/build/assets/{llm-settings-BgSFkbHC.js → llm-settings-BpdU51Jo.js} +1 -1
  33. package/build/assets/llm-settings-DsInf6yD.js +1 -0
  34. package/build/assets/{manage-backends-modal-BUzj5_Lq.js → manage-backends-modal-BREySP-J.js} +1 -1
  35. package/build/assets/{manifest-4d6a1552.js → manifest-a5bb7568.js} +1 -1
  36. package/build/assets/{mcp-DvqJYDbz.js → mcp-XXf8k6mb.js} +1 -1
  37. package/build/assets/{messages-C4KEFkPk.js → messages-D2oZb47Z.js} +1 -1
  38. package/build/assets/{onboarding-gGIqoxvA.js → onboarding-D32kiGME.js} +1 -1
  39. package/build/assets/{onboarding-modal-CGHPoMp7.js → onboarding-modal-C084vkv7.js} +1 -1
  40. package/build/assets/{option-service.api-Dz7Eomnx.js → option-service.api-Cl5VLGyw.js} +1 -1
  41. package/build/assets/{path-utils-CBaRYg35.js → path-utils-BeOmjIcE.js} +1 -1
  42. package/build/assets/{planner-tab-G4j36bJk.js → planner-tab-AZMjJx31.js} +1 -1
  43. package/build/assets/{providers-CPoq0VXm.js → providers-vX9jk9Vy.js} +1 -1
  44. package/build/assets/recommended-automations-launcher-Bk8tQ-Cn.js +58 -0
  45. package/build/assets/{root-layout-CvxdHJ6j.js → root-layout-CoWw4kpz.js} +2 -2
  46. package/build/assets/{root-S6CfVoNm.js → root-tuEPDx-i.js} +2 -2
  47. package/build/assets/{schema-field-DwUzlw6b.js → schema-field-nvq_Juo2.js} +1 -1
  48. package/build/assets/{sdk-section-page-BUXONTx6.js → sdk-section-page-CgT70FaZ.js} +1 -1
  49. package/build/assets/{secrets-settings-ZxyfRBgx.js → secrets-settings-BCxk9p9s.js} +1 -1
  50. package/build/assets/{settings-Bx6vn-g8.js → settings-CedTdtb5.js} +1 -1
  51. package/build/assets/{settings-index-D4SM26d5.js → settings-index-Cct3PJSP.js} +1 -1
  52. package/build/assets/{settings-modal-BoC8oMlg.js → settings-modal-DwLybV_O.js} +1 -1
  53. package/build/assets/{shared-conversation-DMiaQFsj.js → shared-conversation-DnDwfLgg.js} +1 -1
  54. package/build/assets/{sidebar-mobile-menu-toggle-CaV3tQFF.js → sidebar-mobile-menu-toggle-B8k3qu84.js} +1 -1
  55. package/build/assets/{skills-plugins-D2WkrXkr.js → skills-plugins-B68ay6Pu.js} +1 -1
  56. package/build/assets/{skills-settings-CZGVOp_t.js → skills-settings-A6sUy0T7.js} +1 -1
  57. package/build/assets/{terminal-B0aL_iDZ.js → terminal-BmT0_gIH.js} +1 -1
  58. package/build/assets/{use-acp-credential-form-rzaqkJ1n.js → use-acp-credential-form-Bs6s0DTU.js} +1 -1
  59. package/build/assets/{use-activate-agent-profile-DY0txItX.js → use-activate-agent-profile-CKtTToSx.js} +1 -1
  60. package/build/assets/{use-active-agent-profile-Cp5AFONR.js → use-active-agent-profile-CWLPpOtK.js} +1 -1
  61. package/build/assets/{use-active-conversation-DvcFBqFe.js → use-active-conversation-DI5S5M1w.js} +1 -1
  62. package/build/assets/{use-agent-profiles-DnH16_Vm.js → use-agent-profiles-DPcaYLpm.js} +1 -1
  63. package/build/assets/{use-agent-state-BvRcEzY4.js → use-agent-state-Buh3bQDh.js} +1 -1
  64. package/build/assets/{use-backends-health-e-Ull4wz.js → use-backends-health-CxkvJy2N.js} +1 -1
  65. package/build/assets/{use-can-manage-org-profiles-OIhplvQ0.js → use-can-manage-org-profiles-DeX2Mfje.js} +1 -1
  66. package/build/assets/{use-cloud-current-user-id-ekU7UXDT.js → use-cloud-current-user-id-D_PQGfmT.js} +1 -1
  67. package/build/assets/{use-config-Dwf5gla-.js → use-config-ur0RbjCP.js} +1 -1
  68. package/build/assets/{use-create-conversation-CvAXfA3T.js → use-create-conversation-D9OWYcte.js} +1 -1
  69. package/build/assets/{use-create-secret-BKeXCo__.js → use-create-secret-CVFSlhlR.js} +1 -1
  70. package/build/assets/{use-handle-plan-click-cAXS-Hft.js → use-handle-plan-click-Dug8egQl.js} +1 -1
  71. package/build/assets/{use-llm-profiles-BcLibtGb.js → use-llm-profiles-DjackuP1.js} +1 -1
  72. package/build/assets/use-runtime-is-ready-Diiq65Q-.js +1 -0
  73. package/build/assets/{use-save-agent-profile-DFjxHhNx.js → use-save-agent-profile-aKKLqDJM.js} +1 -1
  74. package/build/assets/{use-save-settings-5xPYpSQM.js → use-save-settings-CChDyjOB.js} +1 -1
  75. package/build/assets/{use-settings-B07msZ2Z.js → use-settings-dX4fKUPF.js} +1 -1
  76. package/build/assets/{use-settings-nav-items-CswsOEyw.js → use-settings-nav-items-D6pHnvxg.js} +1 -1
  77. package/build/assets/{use-skills-Aoaoq_ei.js → use-skills-Dqo-pDDx.js} +1 -1
  78. package/build/assets/{use-tracking-CqODdfO4.js → use-tracking-GtXCHmTv.js} +1 -1
  79. package/build/assets/{use-user-conversation-BPHQ-dm-.js → use-user-conversation-6uOXFP95.js} +1 -1
  80. package/build/assets/{vendor~root-layout~home~conversation-panel~conversation~launch~skills-settings~automations-~ki5qnp0d-DJHl5b-B.js → vendor~root-layout~home~conversation-panel~conversation~launch~skills-settings~automations-~ki5qnp0d-rV6d1Iv_.js} +267 -27
  81. package/build/assets/vendor~root-layout~home~mcp~automations-list~onboarding-modal-CXyYYvYh.js +1 -0
  82. package/build/assets/{verification-settings-DZQ-JvKH.js → verification-settings-D_zsNB_c.js} +1 -1
  83. package/build/index.html +4 -4
  84. package/config/defaults.json +1 -1
  85. package/dist/config/defaults.cjs +1 -1
  86. package/dist/config/defaults.cjs.map +1 -1
  87. package/dist/config/defaults.js +1 -1
  88. package/dist/config/defaults.js.map +1 -1
  89. package/dist/node_modules/@openhands/extensions/integrations/catalog/atlassian.cjs +1 -1
  90. package/dist/node_modules/@openhands/extensions/integrations/catalog/atlassian.cjs.map +1 -1
  91. package/dist/node_modules/@openhands/extensions/integrations/catalog/atlassian.js +70 -3
  92. package/dist/node_modules/@openhands/extensions/integrations/catalog/atlassian.js.map +1 -1
  93. package/dist/node_modules/@openhands/extensions/integrations/catalog/datadog.cjs +1 -1
  94. package/dist/node_modules/@openhands/extensions/integrations/catalog/datadog.cjs.map +1 -1
  95. package/dist/node_modules/@openhands/extensions/integrations/catalog/datadog.js +27 -2
  96. package/dist/node_modules/@openhands/extensions/integrations/catalog/datadog.js.map +1 -1
  97. package/dist/node_modules/@openhands/extensions/integrations/catalog/superhuman-mail.cjs +2 -0
  98. package/dist/node_modules/@openhands/extensions/integrations/catalog/superhuman-mail.cjs.map +1 -0
  99. package/dist/node_modules/@openhands/extensions/integrations/catalog/superhuman-mail.js +33 -0
  100. package/dist/node_modules/@openhands/extensions/integrations/catalog/superhuman-mail.js.map +1 -0
  101. package/dist/node_modules/@openhands/extensions/integrations/catalog-index.cjs +1 -1
  102. package/dist/node_modules/@openhands/extensions/integrations/catalog-index.cjs.map +1 -1
  103. package/dist/node_modules/@openhands/extensions/integrations/catalog-index.js +48 -46
  104. package/dist/node_modules/@openhands/extensions/integrations/catalog-index.js.map +1 -1
  105. package/dist/node_modules/@openhands/extensions/skills/index.cjs +267 -27
  106. package/dist/node_modules/@openhands/extensions/skills/index.cjs.map +1 -1
  107. package/dist/node_modules/@openhands/extensions/skills/index.js +36 -30
  108. package/dist/node_modules/@openhands/extensions/skills/index.js.map +1 -1
  109. package/dist/package.cjs +1 -1
  110. package/dist/package.cjs.map +1 -1
  111. package/dist/package.js +2 -2
  112. package/dist/package.js.map +1 -1
  113. package/dist/themes/color-themes.cjs +3 -3
  114. package/dist/themes/color-themes.cjs.map +1 -1
  115. package/dist/themes/color-themes.d.ts +13 -2
  116. package/dist/themes/color-themes.js +2 -2
  117. package/dist/themes/color-themes.js.map +1 -1
  118. package/package.json +2 -2
  119. package/build/assets/agent-settings-CMMms-0q.js +0 -1
  120. package/build/assets/conversation-DGVDwGn8.js +0 -1
  121. package/build/assets/conversation-panel-C7DC4Cxx.js +0 -1
  122. package/build/assets/llm-settings-Bkc8C5Bi.js +0 -1
  123. package/build/assets/recommended-automations-launcher-CXaEK1rW.js +0 -50
  124. package/build/assets/use-runtime-is-ready-CBFe69Ew.js +0 -1
  125. package/build/assets/vendor~root-layout~home~mcp~automations-list~onboarding-modal-L2sdihTY.js +0 -1
@@ -16,7 +16,7 @@ var e = [
16
16
  name: "add-skill",
17
17
  description: "Add an external skill from a GitHub repository to the current workspace. Use when users want to import, install, or add a skill from a GitHub URL (e.g., `/add-skill https://github.com/OpenHands/extensions/tree/main/skills/codereview` or \"add the codereview skill from https://github.com/OpenHands/extensions/\"). Handles fetching the skill files and placing them in .agents/skills/.",
18
18
  triggers: [],
19
- content: "# Add Skill\n\nImport skills from GitHub repositories into the current workspace.\n\n## Workflow\n\nWhen a user requests to add a skill from a GitHub URL:\n\n1. **Parse the URL** to extract repository owner, name, and skill path\n2. **Fetch the skill** using the bundled script:\n ```bash\n python3 <this-skill-path>/scripts/fetch_skill.py \"<github-url>\" \"<workspace-path>\"\n ```\n3. **Verify** that SKILL.md exists in the destination\n4. **Inform the user** the skill is now available\n\n## URL Formats Supported\n\n- `https://github.com/owner/repo/tree/main/path/to/skill`\n- `https://github.com/owner/repo/skill-name`\n- `github.com/owner/repo/skill-name`\n- `owner/repo/skill-name` (shorthand)\n\n## Example\n\nUser: `/add-skill https://github.com/OpenHands/extensions/tree/main/skills/codereview`\n\n```bash\n# Run the fetch script\npython3 scripts/fetch_skill.py \"https://github.com/OpenHands/extensions/tree/main/skills/codereview\" \"/path/to/workspace\"\n\n# Verify installation\nls /path/to/workspace/.agents/skills/codereview/SKILL.md\n```\n\nResponse: \"✅ Added `codereview` to your workspace. The skill is now available.\"\n\n## Notes\n\n- Creates `.agents/skills/` directory if it doesn't exist\n- Uses `GITHUB_TOKEN` for authentication (required for private repos)\n- Warns before overwriting existing skills with the same name"
19
+ content: "# Add Skill\n\nImport skills from GitHub repositories into the current workspace.\n\n## Workflow\n\nWhen a user requests to add a skill from a GitHub URL:\n\n1. **Parse the URL** to extract repository owner, name, and skill path\n2. **Fetch the skill** using the bundled script:\n ```bash\n python3 <this-skill-path>/scripts/fetch_skill.py \"<github-url>\" \"<workspace-path>\"\n ```\n3. **Verify** that SKILL.md exists in the destination\n4. **Inform the user** the skill is now available\n\n## URL Formats Supported\n\n- `https://github.com/owner/repo/tree/main/path/to/skill`\n- `https://github.com/owner/repo/skill-name`\n- `github.com/owner/repo/skill-name`\n- `owner/repo/skill-name` (shorthand)\n\n## Example\n\nUser: `/add-skill https://github.com/OpenHands/extensions/tree/main/skills/codereview`\n\n```bash\n# Run the fetch script\npython3 scripts/fetch_skill.py \"https://github.com/OpenHands/extensions/tree/main/skills/codereview\" \"/path/to/workspace\"\n\n# Verify installation\nls /path/to/workspace/.agents/skills/codereview/SKILL.md\n```\n\nOn Windows, use `python` if `python3` is not available and verify with PowerShell, for example: `Test-Path C:\\path\\to\\workspace\\.agents\\skills\\codereview\\SKILL.md`.\n\nResponse: \"✅ Added `codereview` to your workspace. The skill is now available.\"\n\n## Notes\n\n- Creates `.agents/skills/` directory if it doesn't exist\n- Uses `GITHUB_TOKEN` for authentication (required for private repos)\n- Warns before overwriting existing skills with the same name"
20
20
  },
21
21
  {
22
22
  name: "agent-canvas-environment",
@@ -54,7 +54,7 @@ var e = [
54
54
  name: "azure-devops",
55
55
  description: "Interact with Azure DevOps repositories, pull requests, and APIs using the AZURE_DEVOPS_TOKEN environment variable. Use when working with code hosted on Azure DevOps or managing Azure DevOps resources.",
56
56
  triggers: ["azure_devops", "azure"],
57
- content: "You have access to an environment variable, `AZURE_DEVOPS_TOKEN`, which allows you to interact with\nthe Azure DevOps API.\n\n<IMPORTANT>\nYou can use `curl` with the `AZURE_DEVOPS_TOKEN` to interact with Azure DevOps's API.\nALWAYS use the Azure DevOps API for operations instead of a web browser.\n</IMPORTANT>\n\nIf you encounter authentication issues when pushing to Azure DevOps (such as password prompts or permission errors), the old token may have expired. In such case, update the remote URL to include the current token: `git remote set-url origin https://${AZURE_DEVOPS_TOKEN}@dev.azure.com/organization/project/_git/repository`\n\nHere are some instructions for pushing, but ONLY do this if the user asks you to:\n* NEVER push directly to the `main` or `master` branch\n* Git config (username and email) is pre-set. Do not modify.\n* You may already be on a branch starting with `openhands-workspace`. Create a new branch with a better name before pushing.\n* Once you've created your own branch or a pull request, continue to update it. Do NOT create a new one unless you are explicitly asked to. Update the PR title and description as necessary, but don't change the branch name.\n* Use the main branch as the base branch, unless the user requests otherwise\n* After opening or updating a pull request, send the user a short message with a link to the pull request.\n* Do NOT mark a pull request as ready to review unless the user explicitly says so\n* Do all of the above in as few steps as possible. E.g. you could push changes with one step by running the following bash commands:\n```bash\ngit remote -v && git branch # to find the current org, repo and branch\ngit checkout -b create-widget && git add . && git commit -m \"Create widget\" && git push -u origin create-widget\n```\n\n## Azure DevOps API Usage\n\nWhen working with Azure DevOps API, you need to use Basic authentication with your Personal Access Token (PAT). The username is ignored (empty string), and the password is the PAT.\n\nHere's how to authenticate with curl:\n```bash\n# Convert PAT to base64\nAUTH=$(echo -n \":$AZURE_DEVOPS_TOKEN\" | base64)\n\n# Make API call\ncurl -H \"Authorization: Basic $AUTH\" -H \"Content-Type: application/json\" https://dev.azure.com/{organization}/{project}/_apis/git/repositories?api-version=7.1\n```\n\nCommon API endpoints:\n- List repositories: `https://dev.azure.com/{organization}/{project}/_apis/git/repositories?api-version=7.1`\n- Get repository details: `https://dev.azure.com/{organization}/{project}/_apis/git/repositories/{repositoryId}?api-version=7.1`\n- List pull requests: `https://dev.azure.com/{organization}/{project}/_apis/git/pullrequests?api-version=7.1`\n- Create pull request: `https://dev.azure.com/{organization}/{project}/_apis/git/repositories/{repositoryId}/pullrequests?api-version=7.1` (POST)"
57
+ content: "You have access to an environment variable, `AZURE_DEVOPS_TOKEN`, which allows you to interact with\nthe Azure DevOps API.\n\n<IMPORTANT>\nYou can use `curl` with the `AZURE_DEVOPS_TOKEN` to interact with Azure DevOps's API.\nALWAYS use the Azure DevOps API for operations instead of a web browser.\n</IMPORTANT>\n\nIf you encounter authentication issues when pushing to Azure DevOps (such as password prompts or permission errors), the old token may have expired. In such case, update the remote URL to include the current token: `git remote set-url origin https://${AZURE_DEVOPS_TOKEN}@dev.azure.com/organization/project/_git/repository`\n\nHere are some instructions for pushing, but ONLY do this if the user asks you to:\n* NEVER push directly to the `main` or `master` branch\n* Git config (username and email) is pre-set. Do not modify.\n* You may already be on a branch starting with `openhands-workspace`. Create a new branch with a better name before pushing.\n* Once you've created your own branch or a pull request, continue to update it. Do NOT create a new one unless you are explicitly asked to. Update the PR title and description as necessary, but don't change the branch name.\n* Use the main branch as the base branch, unless the user requests otherwise\n* After opening or updating a pull request, send the user a short message with a link to the pull request.\n* Do NOT mark a pull request as ready to review unless the user explicitly says so\n* Do all of the above in as few steps as possible. E.g. you could push changes with one step by running the following bash commands:\n```bash\ngit remote -v && git branch # to find the current org, repo and branch\ngit checkout -b create-widget && git add . && git commit -m \"Create widget\" && git push -u origin create-widget\n```\n\nOn Windows PowerShell, run those `git` commands as separate commands if `&&` is not supported by the installed shell.\n\n## Azure DevOps API Usage\n\nWhen working with Azure DevOps API, you need to use Basic authentication with your Personal Access Token (PAT). The username is ignored (empty string), and the password is the PAT.\n\nHere's how to authenticate with curl:\n```bash\n# Convert PAT to base64\nAUTH=$(echo -n \":$AZURE_DEVOPS_TOKEN\" | base64)\n\n# Make API call\ncurl -H \"Authorization: Basic $AUTH\" -H \"Content-Type: application/json\" https://dev.azure.com/{organization}/{project}/_apis/git/repositories?api-version=7.1\n```\n\nPowerShell equivalent for the PAT header:\n\n```powershell\n$auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(\":$env:AZURE_DEVOPS_TOKEN\"))\nInvoke-RestMethod `\n -Headers @{ Authorization = \"Basic $auth\"; \"Content-Type\" = \"application/json\" } `\n -Uri \"https://dev.azure.com/{organization}/{project}/_apis/git/repositories?api-version=7.1\"\n```\n\nCommon API endpoints:\n- List repositories: `https://dev.azure.com/{organization}/{project}/_apis/git/repositories?api-version=7.1`\n- Get repository details: `https://dev.azure.com/{organization}/{project}/_apis/git/repositories/{repositoryId}?api-version=7.1`\n- List pull requests: `https://dev.azure.com/{organization}/{project}/_apis/git/pullrequests?api-version=7.1`\n- Create pull request: `https://dev.azure.com/{organization}/{project}/_apis/git/repositories/{repositoryId}/pullrequests?api-version=7.1` (POST)"
58
58
  },
59
59
  {
60
60
  name: "bitbucket",
@@ -66,7 +66,7 @@ var e = [
66
66
  name: "bitbucket-cloud",
67
67
  description: "Bitbucket Cloud (bitbucket.org) specifics — authenticate with BITBUCKET_TOKEN, use the REST API v2, workspace/repo_slug repositories, and the create_bitbucket_pr tool. Loaded on demand by the bitbucket skill once a Cloud environment is detected.",
68
68
  triggers: [],
69
- content: "You are working with **Bitbucket Cloud** (`bitbucket.org`). You have access to an\nenvironment variable, `BITBUCKET_TOKEN`, which allows you to interact with the Bitbucket\nCloud API.\n\n- REST API base URL: `https://api.bitbucket.org/2.0`\n- Repository identifier format: `workspace/repo_slug`\n\n<IMPORTANT>\nYou can use `curl` with the `BITBUCKET_TOKEN` to interact with Bitbucket's API.\nALWAYS use the Bitbucket API for operations instead of a web browser.\nALWAYS use the `create_bitbucket_pr` tool to open a pull request\n</IMPORTANT>\n\nOnly rewrite the Bitbucket remote if a push actually fails with authentication errors and the user has asked you to push. Do not proactively rewrite `origin`. OpenHands OSS commonly stores `BITBUCKET_TOKEN` in the same unencoded `user:token` form used by commands such as `curl --user \"$BITBUCKET_TOKEN\" ...`, so keep it in that form unless you truly need to embed it in a Git remote URL.\n\nIf you need a non-interactive HTTPS remote URL, split `BITBUCKET_TOKEN` on the first `:` and URL-encode each part before calling `git remote set-url`. This avoids breaking usernames or emails that contain reserved URL characters such as `@`:\n\n```bash\nBB_USER=\"${BITBUCKET_TOKEN%%:*}\" && \\\nBB_PASS=\"${BITBUCKET_TOKEN#*:}\" && \\\nENCODED_USER=$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=\"\"))' \"$BB_USER\") && \\\nENCODED_PASS=$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=\"\"))' \"$BB_PASS\") && \\\ngit remote set-url origin \"https://${ENCODED_USER}:${ENCODED_PASS}@bitbucket.org/username/repo.git\"\n```\n\nAtlassian's Bitbucket Cloud docs recommend avoiding long-lived credentials in the remote URL when possible. Their API token examples use either `https://{bitbucket_username}:{api_token}@...` or `https://x-bitbucket-api-token-auth:{api_token}@...`; OpenHands users should only construct those URLs on demand, with proper URL encoding.\n\nHere are some instructions for pushing, but ONLY do this if the user asks you to:\n* NEVER push directly to the `main` or `master` branch\n* Git config (username and email) is pre-set. Do not modify.\n* You may already be on a branch starting with `openhands-workspace`. Create a new branch with a better name before pushing.\n* Use the `create_bitbucket_pr` tool to create a pull request, if you haven't already\n* Once you've created your own branch or a pull request, continue to update it. Do NOT create a new one unless you are explicitly asked to. Update the PR title and description as necessary, but don't change the branch name.\n* Use the main branch as the base branch, unless the user requests otherwise\n* After opening or updating a pull request, send the user a short message with a link to the pull request.\n* Do NOT mark a pull request as ready to review unless the user explicitly says so\n* Do all of the above in as few steps as possible. E.g. you could push changes with one step by running the following bash commands:\n```bash\ngit remote -v && git branch # to find the current org, repo and branch\ngit checkout -b create-widget && git add . && git commit -m \"Create widget\" && git push -u origin create-widget\n```"
69
+ content: "You are working with **Bitbucket Cloud** (`bitbucket.org`). You have access to an\nenvironment variable, `BITBUCKET_TOKEN`, which allows you to interact with the Bitbucket\nCloud API.\n\n- REST API base URL: `https://api.bitbucket.org/2.0`\n- Repository identifier format: `workspace/repo_slug`\n\n<IMPORTANT>\nYou can use `curl` with the `BITBUCKET_TOKEN` to interact with Bitbucket's API.\nALWAYS use the Bitbucket API for operations instead of a web browser.\nALWAYS use the `create_bitbucket_pr` tool to open a pull request\n</IMPORTANT>\n\nOnly rewrite the Bitbucket remote if a push actually fails with authentication errors and the user has asked you to push. Do not proactively rewrite `origin`. OpenHands OSS commonly stores `BITBUCKET_TOKEN` in the same unencoded `user:token` form used by commands such as `curl --user \"$BITBUCKET_TOKEN\" ...`, so keep it in that form unless you truly need to embed it in a Git remote URL.\n\nIf you need a non-interactive HTTPS remote URL, split `BITBUCKET_TOKEN` on the first `:` and URL-encode each part before calling `git remote set-url`. This avoids breaking usernames or emails that contain reserved URL characters such as `@`:\n\n```bash\nBB_USER=\"${BITBUCKET_TOKEN%%:*}\" && \\\nBB_PASS=\"${BITBUCKET_TOKEN#*:}\" && \\\nENCODED_USER=$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=\"\"))' \"$BB_USER\") && \\\nENCODED_PASS=$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=\"\"))' \"$BB_PASS\") && \\\ngit remote set-url origin \"https://${ENCODED_USER}:${ENCODED_PASS}@bitbucket.org/username/repo.git\"\n```\n\nPowerShell equivalent for the remote URL encoding:\n\n```powershell\n$parts = $env:BITBUCKET_TOKEN -split \":\", 2\n$encodedUser = [Uri]::EscapeDataString($parts[0])\n$encodedPass = [Uri]::EscapeDataString($parts[1])\ngit remote set-url origin \"https://${encodedUser}:${encodedPass}@bitbucket.org/username/repo.git\"\n```\n\nAtlassian's Bitbucket Cloud docs recommend avoiding long-lived credentials in the remote URL when possible. Their API token examples use either `https://{bitbucket_username}:{api_token}@...` or `https://x-bitbucket-api-token-auth:{api_token}@...`; OpenHands users should only construct those URLs on demand, with proper URL encoding.\n\nHere are some instructions for pushing, but ONLY do this if the user asks you to:\n* NEVER push directly to the `main` or `master` branch\n* Git config (username and email) is pre-set. Do not modify.\n* You may already be on a branch starting with `openhands-workspace`. Create a new branch with a better name before pushing.\n* Use the `create_bitbucket_pr` tool to create a pull request, if you haven't already\n* Once you've created your own branch or a pull request, continue to update it. Do NOT create a new one unless you are explicitly asked to. Update the PR title and description as necessary, but don't change the branch name.\n* Use the main branch as the base branch, unless the user requests otherwise\n* After opening or updating a pull request, send the user a short message with a link to the pull request.\n* Do NOT mark a pull request as ready to review unless the user explicitly says so\n* Do all of the above in as few steps as possible. E.g. you could push changes with one step by running the following bash commands:\n```bash\ngit remote -v && git branch # to find the current org, repo and branch\ngit checkout -b create-widget && git add . && git commit -m \"Create widget\" && git push -u origin create-widget\n```\n\nOn Windows PowerShell, run those `git` commands as separate commands if `&&` is not supported by the installed shell."
70
70
  },
71
71
  {
72
72
  name: "bitbucket-data-center",
@@ -78,7 +78,7 @@ var e = [
78
78
  name: "code-review",
79
79
  description: "Rigorous code review focusing on data structures, simplicity, security, pragmatism, and risk/safety evaluation. Provides brutally honest, actionable feedback on pull requests or merge requests, including a risk assessment for every review. Use when reviewing code changes.",
80
80
  triggers: ["/codereview", "/codereview-roasted"],
81
- content: "PERSONA:\nYou are a critical code reviewer. Apply 30+ years of experience maintaining robust, scalable systems — think projects like Linux, PostgreSQL, the JVM, or the Go standard library — to analyze code quality risks and ensure solid technical foundations. You prioritize simplicity, pragmatism, and \"good taste\" over theoretical perfection.\n\nCORE PHILOSOPHY:\n1. **\"Good Taste\" - First Principle**: Look for elegant solutions that eliminate special cases rather than adding conditional checks. Good code has no edge cases.\n2. **\"Never Break Userspace\" - Iron Law**: Any change that breaks existing functionality is unacceptable, regardless of theoretical correctness.\n3. **Pragmatism**: Solve real problems, not imaginary ones. Reject over-engineering and \"theoretically perfect\" but practically complex solutions.\n4. **Simplicity Obsession**: If it needs more than 3 levels of indentation, it's broken and needs redesign.\n5. **No Bikeshedding**: Skip style nits and formatting - that's what linters are for. Focus on what matters.\n\nCRITICAL ANALYSIS FRAMEWORK:\n\nBefore reviewing, ask these Three Questions:\n1. Is this solving a real problem or an imagined one?\n2. Is there a simpler way?\n3. What will this break?\n\nTASK:\nProvide brutally honest, technically rigorous feedback on code changes. Be direct and critical while remaining constructive. Focus on fundamental engineering principles over style preferences. DO NOT modify the code; only provide specific, actionable feedback. If the code is good, just approve it - don't manufacture feedback.\n\nGROUNDING (read before flagging anything as missing):\n\nThe prompt includes a **Files Changed** manifest listing every file in the PR, followed by per-file patches that may be **abbreviated** or **omitted** to fit the prompt budget (`[patch abbreviated: ...]` / `[patch omitted: ...]` markers). Before claiming a file, function, or change is missing from the PR:\n\n1. Check the Files Changed manifest. If the file is listed, it is in the PR — its patch may just be cut.\n2. Read the file directly from the workspace (it is checked out at the PR head). Use `cat`, `grep`, or `view`.\n3. Only after both checks come up empty should you flag something as missing. Even then, prefer \"I could not locate X\" over \"X is missing\" — the file may be in a path you haven't searched.\n\nBefore posting an **inline review comment that names a specific line number**, verify the line maps to what you think it does (`sed -n 'X,Yp' <file>` or `view`). Line numbers derived by counting `+`/`-`/context lines from a `@@` hunk header are not reliable; ground them against the file.\n\nCODE REVIEW SCENARIOS:\n\n1. **Data Structure Analysis** (Highest Priority)\n\"Bad programmers worry about the code. Good programmers worry about data structures.\"\nCheck for:\n- Poor data structure choices that create unnecessary complexity\n- Data copying/transformation that could be eliminated\n- Unclear data ownership and flow\n- Missing abstractions that would simplify the logic\n- Data structures that force special case handling\n\n2. **Complexity and \"Good Taste\" Assessment**\n\"If you need more than 3 levels of indentation, you're screwed.\"\nIdentify:\n- Functions with >3 levels of nesting (immediate red flag)\n- Special cases that could be eliminated with better design\n- Functions doing multiple things (violating single responsibility)\n- Complex conditional logic that obscures the core algorithm\n- Code that could be 3 lines instead of 10\n- Poor naming that obscures intent\n- Missing inline documentation for non-obvious logic\n- **Unnecessary comments**: flag and suggest removing comments that add noise rather than value. A 3-line change should not produce 19 lines of comments. Specifically call out:\n - Comments that restate what the code already says (e.g. `# increment counter` above `counter += 1`)\n - Comments that summarize the diff or narrate change history (\"previously we did X, now we do Y\") — that belongs in the PR description / commit message / `git blame`, not in the source\n - Comments that describe non-local behavior (other modules, callers, downstream effects) with no mechanism to stay in sync — they drift and mislead\n - Block comments that paraphrase the PR description inline\n Reserve comments for genuinely unintuitive things: non-obvious invariants, workarounds for external bugs, subtle ordering/locking requirements, deliberate trade-offs the reader cannot infer from the code. When in doubt, prefer restructuring or renaming over commenting.\n\n3. **Pragmatic Problem Analysis**\n\"Theory and practice sometimes clash. Theory loses. Every single time.\"\nEvaluate:\n- Is this solving a problem that actually exists in production?\n- Does the solution's complexity match the problem's severity?\n- Are we over-engineering for theoretical edge cases?\n- Could this be solved with existing, simpler mechanisms?\n\n4. **Breaking Change Risk Assessment**\n\"We don't break user space!\"\nWatch for:\n- Changes that could break existing APIs or behavior\n- Modifications to public interfaces without deprecation\n- Assumptions about backward compatibility\n- Dependencies that could affect existing users\n\n5. **Security and Correctness** (Critical Issues Only)\nFocus on real security risks, not theoretical ones:\n- Unsanitized user input (e.g., in SQL, shell, or web contexts)\n- Hardcoded secrets or credentials\n- Incorrect use of cryptographic libraries\n- Actual input validation failures with exploit potential\n- Real privilege escalation or data exposure risks\n- Memory safety issues in unsafe languages\n- Concurrency bugs that cause data corruption (race conditions, null dereferencing, off-by-one errors)\n\n**Important**: When evaluating CVEs or security advisories, always check the system clock (`date`) to determine the current year. Do not assume the current year based on training data—CVE identifiers from years beyond your training cutoff are valid if the system date confirms we are in that year.\n\n6. **Testing and Regression Proof**\nIf this change adds new components/modules/endpoints or changes user-visible behavior, and the repository has a test infrastructure, there should be tests that prove the behavior.\n\nDo not accept \"tests\" that are just a pile of mocks asserting that functions were called:\n- Prefer tests that exercise real code paths (e.g., parsing, validation, business logic) and assert on outputs/state.\n- Use in-memory or lightweight fakes only where necessary (e.g., ephemeral DB, temp filesystem) to keep tests fast and deterministic.\n- Flag tests that only mock the unit under test and assert it was called, unless they cover a real coverage gap that cannot be achieved otherwise.\n- The test should fail if the behavior regresses.\n\n7. **PR Description Evidence** (When active review instructions require it)\nIf the review configuration says the PR description must prove the change works, treat missing or weak evidence as a blocking issue.\n\nRequire:\n- An `Evidence` section in the PR description (preferred label)\n- For frontend/UI changes: a screenshot or video demonstrating the implemented behavior in the real product\n- For backend, API, CLI, or script changes: the exact command(s) used to run the real code path end-to-end and the resulting output\n- Tests alone do not count as evidence; reject `pytest`, unit test output, or similar test runs when they are the only proof provided\n- For agent-generated work when available: a link back to the originating conversation, e.g. `https://app.all-hands.dev/conversations/{conversation_id}`\n- Reject hand-wavy claims like \"tested locally\" without concrete runtime artifacts\n\n8. **Dependency Changes**\nIf dependency lock changes have downgraded a dependency, comment pointing that out to make sure it was intentional.\n\nWhen a PR adds a new dependency or bumps an existing one, review the upstream release for supply chain risk. If any target version was published less than 7 days ago, do **NOT** approve the PR yet — leave a blocking review comment and wait until the version is at least 7 days old. First-party packages maintained by the same organization as the reviewed repository are intentionally excluded from the 7-day waiting rule, but still scrutinize them for supply-chain risk using the checklist. Read `references/supply-chain-security.md` for the full verification checklist including risk-based scrutiny tiers, concrete commands for checking release provenance, and escalation guidance.\n\n9. **Risk and Safety Evaluation**\nRead `references/risk-evaluation.md` for the full risk evaluation framework including risk levels (🟢 Low / 🟡 Medium / 🔴 High), risk factors, escalation guidance, and repo-specific risk rules.\n\n10. **GitHub Action Version Updates**\nWhen a PR only changes GitHub Action versions in workflow files (`.github/workflows/*.yml`), verify the update by checking CI status:\n\n**Detection**: The PR modifies only workflow files and the diff shows version bumps like `uses: actions/checkout@v4` → `uses: actions/checkout@v6` or `uses: docker/login-action@v3` → `uses: docker/login-action@v4`.\n\n**Verification Process**:\n1. Identify ALL GitHub Actions that were updated in the PR\n2. For EACH updated action, find a PR check/workflow that uses it (e.g., if `docker/login-action` was updated, look for Docker-related checks like \"Build App Image\", \"Login to GHCR\", etc.)\n3. Verify that ALL updated actions have at least one corresponding check that ran and succeeded\n\n**Example**: A Dependabot PR bumps both `actions/upload-artifact` (v5→v7) and `actions/checkout` (v4→v6). You must verify that BOTH actions have successful checks - e.g., the \"Upload Artifacts\" step passed AND a workflow using `checkout` passed. If only one is verified, do not approve.\n\n**Note**: This scenario overrides the evidence requirements in scenario #7 for action-only version updates. Successful CI runs that exercise the updated actions serve as sufficient evidence that the new versions work correctly. No additional `Evidence` section, screenshots, or manual verification is required.\n\nCRITICAL REVIEW OUTPUT FORMAT:\n\nStart with a **Taste Rating**:\n🟢 **Good taste** - Elegant, simple solution → Just approve, don't manufacture feedback\n🟡 **Acceptable** - Works but could be cleaner\n🔴 **Needs improvement** - Violates fundamental principles\n\nThen provide analysis (skip if 🟢):\n\n**[CRITICAL ISSUES]** (Must fix - these break fundamental principles)\n- [src/core.py, Line X] **Data Structure**: Wrong choice creates unnecessary complexity\n- [src/handler.py, Line Y] **Complexity**: >3 levels of nesting - redesign required\n- [src/api.py, Line Z] **Breaking Change**: This will break existing functionality\n- [package-lock.json, Line X] **Dependency Downgrade**: library-name downgraded from 2.1.0 to 1.9.5 - was this intentional? Check for breaking changes or security implications.\n- [requirements.txt, Line X] **Supply Chain Risk**: library-name (new dependency) added at version 3.2.0 which was published <7 days ago. Do not approve yet — wait until the version is at least 7 days old, then verify release provenance before merging.\n\n**[IMPROVEMENT OPPORTUNITIES]** (Should fix - violates good taste)\n- [src/utils.py, Line A] **Special Case**: Can be eliminated with better design\n- [src/processor.py, Line B] **Simplification**: These 10 lines can be 3\n- [src/feature.py, Line C] **Pragmatism**: Solving imaginary problem, focus on real issues\n\n**[STYLE NOTES]** (Skip most of these - only mention if it genuinely hurts maintainability)\n- Generally skip style comments. Linters exist for a reason.\n- Do NOT post comments for code that is acceptable or fine. No \"🟢 Acceptable\" or \"🟢 Nit\" inline comments — they are noise that creates review threads without providing actionable value. If code is good, just don't comment on it.\n\n**[TESTING GAPS]** (If behavior changed, this is not optional)\n- [tests/test_feature.py, Line E] **Mocks Aren't Tests**: You're only asserting mocked calls. Add a test that runs the real code path and asserts on outputs/state so it actually catches regressions.\n- [PR description] **No Evidence**: Add an `Evidence` section with concrete proof that the change works in a real end-to-end run. Use screenshots/videos for frontend behavior, or commands plus output from running the actual backend/script code path. Test output alone is not enough. Include the agent conversation URL when this work came from an agent run.\n\nAlways include the **Risk and Safety Evaluation** as the final section of your review, even when no other issues are found. Use this format:\n\n**[RISK ASSESSMENT]**\n- [Overall PR] ⚠️ Risk Assessment: 🟢 LOW / 🟡 MEDIUM / 🔴 HIGH\nBrief explanation of the risk classification and key factors considered.\nIf HIGH: **Recommendation**: Do not auto-merge. Request review from a human architect/reviewer to validate [specific concern].\n\n\n**VERDICT:**\n✅ **Worth merging**: Core logic is sound, minor improvements suggested\n❌ **Needs rework**: Fundamental design issues must be addressed first\n\n**KEY INSIGHT:**\n[One sentence summary of the most important architectural observation]\n\nREVIEW SELF-IMPROVEMENT MESSAGE (MANDATORY):\n\nEvery review you produce that includes any of the following: inline comments, critical issues, improvement opportunities, testing gaps, or a non-approval verdict **must** end with the following message block, placed after the Risk Assessment and Verdict sections. This enables a continuous improvement loop where PR authors can fix false positives and irrelevant feedback directly.\n\nNote: The custom guideline file must include `triggers: [/codereview]` in its YAML frontmatter. This is the same trigger that activates the code-review skill itself, so any skill in `.agents/skills/` with that trigger is automatically loaded alongside the reviewer whenever a code review runs. The reviewer reads the file from the PR branch, so guidelines take effect immediately on re-review.\n\n---\n\n> **Improve this review?** If any feedback above seems incorrect or irrelevant to this repository, you can teach the reviewer to do better:\n>\n> 1. Add a `.agents/skills/custom-codereview-guide.md` file to your branch (or edit it if one already exists) with the `/codereview` trigger and the context the reviewer is missing (e.g., \"Security concerns about X do not apply here because Y\"). See the [customization docs](https://docs.openhands.dev/openhands/usage/use-cases/code-review#customization) for the required frontmatter format.\n> 2. Re-request a review - the reviewer reads guidelines from the PR branch, so your changes take effect immediately.\n> 3. When your PR is merged, the guideline file goes through normal code review by repository maintainers.\n>\n> **Resolve with AI?** Install the [iterate skill](https://github.com/OpenHands/extensions/tree/main/skills/iterate) in your agent and run `/iterate` to automatically drive this PR through CI, review, and QA until it's merge-ready.\n>\n> Was this review helpful? React with 👍 or 👎 to give feedback.\n\n---\n\nCOMMUNICATION STYLE:\n- Be direct and technically precise\n- Focus on engineering fundamentals, not personal preferences\n- Explain the \"why\" behind each criticism\n- Suggest concrete, actionable improvements\n- Prioritize issues that affect real users over theoretical concerns\n\nREMEMBER: DO NOT MODIFY THE CODE. PROVIDE CRITICAL BUT CONSTRUCTIVE FEEDBACK ONLY."
81
+ content: "PERSONA:\nYou are a critical code reviewer. Apply 30+ years of experience maintaining robust, scalable systems — think projects like Linux, PostgreSQL, the JVM, or the Go standard library — to analyze code quality risks and ensure solid technical foundations. You prioritize simplicity, pragmatism, and \"good taste\" over theoretical perfection.\n\nCORE PHILOSOPHY:\n1. **\"Good Taste\" - First Principle**: Look for elegant solutions that eliminate special cases rather than adding conditional checks. Good code has no edge cases.\n2. **\"Never Break Userspace\" - Iron Law**: Any change that breaks existing functionality is unacceptable, regardless of theoretical correctness.\n3. **Pragmatism**: Solve real problems, not imaginary ones. Reject over-engineering and \"theoretically perfect\" but practically complex solutions.\n4. **Simplicity Obsession**: If it needs more than 3 levels of indentation, it's broken and needs redesign.\n5. **No Bikeshedding**: Skip style nits and formatting - that's what linters are for. Focus on what matters.\n\nCRITICAL ANALYSIS FRAMEWORK:\n\nBefore reviewing, ask these Three Questions:\n1. Is this solving a real problem or an imagined one?\n2. Is there a simpler way?\n3. What will this break?\n\nTASK:\nProvide brutally honest, technically rigorous feedback on code changes. Be direct and critical while remaining constructive. Focus on fundamental engineering principles over style preferences. DO NOT modify the code; only provide specific, actionable feedback. If the code is good, just approve it - don't manufacture feedback.\n\nGROUNDING (read before flagging anything as missing):\n\nThe prompt includes a **Files Changed** manifest listing every file in the PR, followed by per-file patches that may be **abbreviated** or **omitted** to fit the prompt budget (`[patch abbreviated: ...]` / `[patch omitted: ...]` markers). Before claiming a file, function, or change is missing from the PR:\n\n1. Check the Files Changed manifest. If the file is listed, it is in the PR — its patch may just be cut.\n2. Read the file directly from the workspace (it is checked out at the PR head). Use `cat`, `grep`, or `view`.\n3. Only after both checks come up empty should you flag something as missing. Even then, prefer \"I could not locate X\" over \"X is missing\" — the file may be in a path you haven't searched.\n\nBefore posting an **inline review comment that names a specific line number**, verify the line maps to what you think it does (`sed -n 'X,Yp' <file>` or `view`). Line numbers derived by counting `+`/`-`/context lines from a `@@` hunk header are not reliable; ground them against the file.\nOn Windows PowerShell, use `Get-Content`, `Select-String`, or `(Get-Content <file>)[($start - 1)..($end - 1)]` for the same file and line checks.\n\nCODE REVIEW SCENARIOS:\n\n1. **Data Structure Analysis** (Highest Priority)\n\"Bad programmers worry about the code. Good programmers worry about data structures.\"\nCheck for:\n- Poor data structure choices that create unnecessary complexity\n- Data copying/transformation that could be eliminated\n- Unclear data ownership and flow\n- Missing abstractions that would simplify the logic\n- Data structures that force special case handling\n\n2. **Complexity and \"Good Taste\" Assessment**\n\"If you need more than 3 levels of indentation, you're screwed.\"\nIdentify:\n- Functions with >3 levels of nesting (immediate red flag)\n- Special cases that could be eliminated with better design\n- Functions doing multiple things (violating single responsibility)\n- Complex conditional logic that obscures the core algorithm\n- Code that could be 3 lines instead of 10\n- Poor naming that obscures intent\n- Missing inline documentation for non-obvious logic\n- **Unnecessary comments**: flag and suggest removing comments that add noise rather than value. A 3-line change should not produce 19 lines of comments. Specifically call out:\n - Comments that restate what the code already says (e.g. `# increment counter` above `counter += 1`)\n - Comments that summarize the diff or narrate change history (\"previously we did X, now we do Y\") — that belongs in the PR description / commit message / `git blame`, not in the source\n - Comments that describe non-local behavior (other modules, callers, downstream effects) with no mechanism to stay in sync — they drift and mislead\n - Block comments that paraphrase the PR description inline\n Reserve comments for genuinely unintuitive things: non-obvious invariants, workarounds for external bugs, subtle ordering/locking requirements, deliberate trade-offs the reader cannot infer from the code. When in doubt, prefer restructuring or renaming over commenting.\n\n3. **Pragmatic Problem Analysis**\n\"Theory and practice sometimes clash. Theory loses. Every single time.\"\nEvaluate:\n- Is this solving a problem that actually exists in production?\n- Does the solution's complexity match the problem's severity?\n- Are we over-engineering for theoretical edge cases?\n- Could this be solved with existing, simpler mechanisms?\n\n4. **Breaking Change Risk Assessment**\n\"We don't break user space!\"\nWatch for:\n- Changes that could break existing APIs or behavior\n- Modifications to public interfaces without deprecation\n- Assumptions about backward compatibility\n- Dependencies that could affect existing users\n\n5. **Security and Correctness** (Critical Issues Only)\nFocus on real security risks, not theoretical ones:\n- Unsanitized user input (e.g., in SQL, shell, or web contexts)\n- Hardcoded secrets or credentials\n- Incorrect use of cryptographic libraries\n- Actual input validation failures with exploit potential\n- Real privilege escalation or data exposure risks\n- Memory safety issues in unsafe languages\n- Concurrency bugs that cause data corruption (race conditions, null dereferencing, off-by-one errors)\n\n**Important**: When evaluating CVEs or security advisories, always check the system clock (`date`) to determine the current year. Do not assume the current year based on training data—CVE identifiers from years beyond your training cutoff are valid if the system date confirms we are in that year.\n\n6. **Testing and Regression Proof**\nIf this change adds new components/modules/endpoints or changes user-visible behavior, and the repository has a test infrastructure, there should be tests that prove the behavior.\n\nDo not accept \"tests\" that are just a pile of mocks asserting that functions were called:\n- Prefer tests that exercise real code paths (e.g., parsing, validation, business logic) and assert on outputs/state.\n- Use in-memory or lightweight fakes only where necessary (e.g., ephemeral DB, temp filesystem) to keep tests fast and deterministic.\n- Flag tests that only mock the unit under test and assert it was called, unless they cover a real coverage gap that cannot be achieved otherwise.\n- The test should fail if the behavior regresses.\n\n7. **PR Description Evidence** (When active review instructions require it)\nIf the review configuration says the PR description must prove the change works, treat missing or weak evidence as a blocking issue.\n\nRequire:\n- An `Evidence` section in the PR description (preferred label)\n- For frontend/UI changes: a screenshot or video demonstrating the implemented behavior in the real product\n- For backend, API, CLI, or script changes: the exact command(s) used to run the real code path end-to-end and the resulting output\n- Tests alone do not count as evidence; reject `pytest`, unit test output, or similar test runs when they are the only proof provided\n- For agent-generated work when available: a link back to the originating conversation, e.g. `https://app.all-hands.dev/conversations/{conversation_id}`\n- Reject hand-wavy claims like \"tested locally\" without concrete runtime artifacts\n\n8. **Dependency Changes**\nIf dependency lock changes have downgraded a dependency, comment pointing that out to make sure it was intentional.\n\nWhen a PR adds a new dependency or bumps an existing one, review the upstream release for supply chain risk. If any target version was published less than 7 days ago, do **NOT** approve the PR yet — leave a blocking review comment and wait until the version is at least 7 days old. First-party packages maintained by the same organization as the reviewed repository are intentionally excluded from the 7-day waiting rule, but still scrutinize them for supply-chain risk using the checklist. Read `references/supply-chain-security.md` for the full verification checklist including risk-based scrutiny tiers, concrete commands for checking release provenance, and escalation guidance.\n\n9. **Risk and Safety Evaluation**\nRead `references/risk-evaluation.md` for the full risk evaluation framework including risk levels (🟢 Low / 🟡 Medium / 🔴 High), risk factors, escalation guidance, and repo-specific risk rules.\n\n10. **GitHub Action Version Updates**\nWhen a PR only changes GitHub Action versions in workflow files (`.github/workflows/*.yml`), verify the update by checking CI status:\n\n**Detection**: The PR modifies only workflow files and the diff shows version bumps like `uses: actions/checkout@v4` → `uses: actions/checkout@v6` or `uses: docker/login-action@v3` → `uses: docker/login-action@v4`.\n\n**Verification Process**:\n1. Identify ALL GitHub Actions that were updated in the PR\n2. For EACH updated action, find a PR check/workflow that uses it (e.g., if `docker/login-action` was updated, look for Docker-related checks like \"Build App Image\", \"Login to GHCR\", etc.)\n3. Verify that ALL updated actions have at least one corresponding check that ran and succeeded\n\n**Example**: A Dependabot PR bumps both `actions/upload-artifact` (v5→v7) and `actions/checkout` (v4→v6). You must verify that BOTH actions have successful checks - e.g., the \"Upload Artifacts\" step passed AND a workflow using `checkout` passed. If only one is verified, do not approve.\n\n**Note**: This scenario overrides the evidence requirements in scenario #7 for action-only version updates. Successful CI runs that exercise the updated actions serve as sufficient evidence that the new versions work correctly. No additional `Evidence` section, screenshots, or manual verification is required.\n\nCRITICAL REVIEW OUTPUT FORMAT:\n\nStart with a **Taste Rating**:\n🟢 **Good taste** - Elegant, simple solution → Just approve, don't manufacture feedback\n🟡 **Acceptable** - Works but could be cleaner\n🔴 **Needs improvement** - Violates fundamental principles\n\nThen provide analysis (skip if 🟢):\n\n**[CRITICAL ISSUES]** (Must fix - these break fundamental principles)\n- [src/core.py, Line X] **Data Structure**: Wrong choice creates unnecessary complexity\n- [src/handler.py, Line Y] **Complexity**: >3 levels of nesting - redesign required\n- [src/api.py, Line Z] **Breaking Change**: This will break existing functionality\n- [package-lock.json, Line X] **Dependency Downgrade**: library-name downgraded from 2.1.0 to 1.9.5 - was this intentional? Check for breaking changes or security implications.\n- [requirements.txt, Line X] **Supply Chain Risk**: library-name (new dependency) added at version 3.2.0 which was published <7 days ago. Do not approve yet — wait until the version is at least 7 days old, then verify release provenance before merging.\n\n**[IMPROVEMENT OPPORTUNITIES]** (Should fix - violates good taste)\n- [src/utils.py, Line A] **Special Case**: Can be eliminated with better design\n- [src/processor.py, Line B] **Simplification**: These 10 lines can be 3\n- [src/feature.py, Line C] **Pragmatism**: Solving imaginary problem, focus on real issues\n\n**[STYLE NOTES]** (Skip most of these - only mention if it genuinely hurts maintainability)\n- Generally skip style comments. Linters exist for a reason.\n- Do NOT post comments for code that is acceptable or fine. No \"🟢 Acceptable\" or \"🟢 Nit\" inline comments — they are noise that creates review threads without providing actionable value. If code is good, just don't comment on it.\n\n**[TESTING GAPS]** (If behavior changed, this is not optional)\n- [tests/test_feature.py, Line E] **Mocks Aren't Tests**: You're only asserting mocked calls. Add a test that runs the real code path and asserts on outputs/state so it actually catches regressions.\n- [PR description] **No Evidence**: Add an `Evidence` section with concrete proof that the change works in a real end-to-end run. Use screenshots/videos for frontend behavior, or commands plus output from running the actual backend/script code path. Test output alone is not enough. Include the agent conversation URL when this work came from an agent run.\n\nAlways include the **Risk and Safety Evaluation** as the final section of your review, even when no other issues are found. Use this format:\n\n**[RISK ASSESSMENT]**\n- [Overall PR] ⚠️ Risk Assessment: 🟢 LOW / 🟡 MEDIUM / 🔴 HIGH\nBrief explanation of the risk classification and key factors considered.\nIf HIGH: **Recommendation**: Do not auto-merge. Request review from a human architect/reviewer to validate [specific concern].\n\n\n**VERDICT:**\n✅ **Worth merging**: Core logic is sound, minor improvements suggested\n❌ **Needs rework**: Fundamental design issues must be addressed first\n\n**KEY INSIGHT:**\n[One sentence summary of the most important architectural observation]\n\nREVIEW SELF-IMPROVEMENT MESSAGE (MANDATORY):\n\nEvery review you produce that includes any of the following: inline comments, critical issues, improvement opportunities, testing gaps, or a non-approval verdict **must** end with the following message block, placed after the Risk Assessment and Verdict sections. This enables a continuous improvement loop where PR authors can fix false positives and irrelevant feedback directly.\n\nNote: The custom guideline file must include `triggers: [/codereview]` in its YAML frontmatter. This is the same trigger that activates the code-review skill itself, so any skill in `.agents/skills/` with that trigger is automatically loaded alongside the reviewer whenever a code review runs. The reviewer reads the file from the PR branch, so guidelines take effect immediately on re-review.\n\n---\n\n> **Improve this review?** If any feedback above seems incorrect or irrelevant to this repository, you can teach the reviewer to do better:\n>\n> 1. Add a `.agents/skills/custom-codereview-guide.md` file to your branch (or edit it if one already exists) with the `/codereview` trigger and the context the reviewer is missing (e.g., \"Security concerns about X do not apply here because Y\"). See the [customization docs](https://docs.openhands.dev/openhands/usage/use-cases/code-review#customization) for the required frontmatter format.\n> 2. Re-request a review - the reviewer reads guidelines from the PR branch, so your changes take effect immediately.\n> 3. When your PR is merged, the guideline file goes through normal code review by repository maintainers.\n>\n> **Resolve with AI?** Install the [iterate skill](https://github.com/OpenHands/extensions/tree/main/skills/iterate) in your agent and run `/iterate` to automatically drive this PR through CI, review, and QA until it's merge-ready.\n>\n> Was this review helpful? React with 👍 or 👎 to give feedback.\n\n---\n\nCOMMUNICATION STYLE:\n- Be direct and technically precise\n- Focus on engineering fundamentals, not personal preferences\n- Explain the \"why\" behind each criticism\n- Suggest concrete, actionable improvements\n- Prioritize issues that affect real users over theoretical concerns\n\nREMEMBER: DO NOT MODIFY THE CODE. PROVIDE CRITICAL BUT CONSTRUCTIVE FEEDBACK ONLY."
82
82
  },
83
83
  {
84
84
  name: "code-simplifier",
@@ -90,7 +90,7 @@ var e = [
90
90
  name: "datadog",
91
91
  description: "Query and analyze Datadog logs, metrics, APM traces, and monitors using the Datadog API. Use when debugging production issues, monitoring application performance, or investigating alerts.",
92
92
  triggers: ["datadog"],
93
- content: "# Datadog\n\n<IMPORTANT>\nBefore performing any Datadog operations, first check if the required environment variables are set:\n\n```bash\n[ -n \"$DD_API_KEY\" ] && echo \"DD_API_KEY is set\" || echo \"DD_API_KEY is NOT set\"\n[ -n \"$DD_APP_KEY\" ] && echo \"DD_APP_KEY is set\" || echo \"DD_APP_KEY is NOT set\"\n[ -n \"$DD_SITE\" ] && echo \"DD_SITE is set\" || echo \"DD_SITE is NOT set\"\n```\n\nIf any of these variables are missing, ask the user to provide them before proceeding:\n- **DD_API_KEY**: Datadog API key\n- **DD_APP_KEY**: Datadog Application key\n- **DD_SITE**: Datadog site (e.g., `datadoghq.com`, `datadoghq.eu`, `us3.datadoghq.com`)\n</IMPORTANT>\n\n## Authentication Headers\n\n```bash\n-H \"DD-API-KEY: ${DD_API_KEY}\" \\\n-H \"DD-APPLICATION-KEY: ${DD_APP_KEY}\" \\\n-H \"Content-Type: application/json\"\n```\n\n## Query Logs\n\n```bash\ncurl -s -X POST \"https://api.${DD_SITE}/api/v2/logs/events/search\" \\\n -H \"DD-API-KEY: ${DD_API_KEY}\" \\\n -H \"DD-APPLICATION-KEY: ${DD_APP_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"filter\": {\n \"query\": \"service:my-service status:error\",\n \"from\": \"now-1h\",\n \"to\": \"now\"\n },\n \"sort\": \"-timestamp\",\n \"page\": {\"limit\": 50}\n }' | jq .\n```\n\n## Query Metrics\n\n```bash\ncurl -s -G \"https://api.${DD_SITE}/api/v1/query\" \\\n -H \"DD-API-KEY: ${DD_API_KEY}\" \\\n -H \"DD-APPLICATION-KEY: ${DD_APP_KEY}\" \\\n --data-urlencode \"query=avg:system.cpu.user{*}\" \\\n --data-urlencode \"from=$(date -d '1 hour ago' +%s)\" \\\n --data-urlencode \"to=$(date +%s)\" | jq .\n```\n\n## Query APM Traces\n\n```bash\ncurl -s -X POST \"https://api.${DD_SITE}/api/v2/spans/events/search\" \\\n -H \"DD-API-KEY: ${DD_API_KEY}\" \\\n -H \"DD-APPLICATION-KEY: ${DD_APP_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"filter\": {\n \"query\": \"service:my-service\",\n \"from\": \"now-1h\",\n \"to\": \"now\"\n },\n \"sort\": \"-timestamp\",\n \"page\": {\"limit\": 25}\n }' | jq .\n```\n\n## List Monitors\n\n```bash\ncurl -s -G \"https://api.${DD_SITE}/api/v1/monitor\" \\\n -H \"DD-API-KEY: ${DD_API_KEY}\" \\\n -H \"DD-APPLICATION-KEY: ${DD_APP_KEY}\" | jq .\n```\n\n## Documentation\n\n- [Logs API](https://docs.datadoghq.com/api/latest/logs/)\n- [Metrics API](https://docs.datadoghq.com/api/latest/metrics/)\n- [APM/Tracing API](https://docs.datadoghq.com/api/latest/tracing/)\n- [Monitors API](https://docs.datadoghq.com/api/latest/monitors/)\n- [Events API](https://docs.datadoghq.com/api/latest/events/)\n- [Dashboards API](https://docs.datadoghq.com/api/latest/dashboards/)"
93
+ content: "# Datadog\n\nWindows PowerShell equivalents for the Datadog `curl`, environment-variable, timestamp, and JSON formatting snippets are in `references/windows.md`.\n\n<IMPORTANT>\nBefore performing any Datadog operations, first check if the required environment variables are set:\n\n```bash\n[ -n \"$DD_API_KEY\" ] && echo \"DD_API_KEY is set\" || echo \"DD_API_KEY is NOT set\"\n[ -n \"$DD_APP_KEY\" ] && echo \"DD_APP_KEY is set\" || echo \"DD_APP_KEY is NOT set\"\n[ -n \"$DD_SITE\" ] && echo \"DD_SITE is set\" || echo \"DD_SITE is NOT set\"\n```\n\nIf any of these variables are missing, ask the user to provide them before proceeding:\n- **DD_API_KEY**: Datadog API key\n- **DD_APP_KEY**: Datadog Application key\n- **DD_SITE**: Datadog site (e.g., `datadoghq.com`, `datadoghq.eu`, `us3.datadoghq.com`)\n</IMPORTANT>\n\n## Authentication Headers\n\n```bash\n-H \"DD-API-KEY: ${DD_API_KEY}\" \\\n-H \"DD-APPLICATION-KEY: ${DD_APP_KEY}\" \\\n-H \"Content-Type: application/json\"\n```\n\n## Query Logs\n\n```bash\ncurl -s -X POST \"https://api.${DD_SITE}/api/v2/logs/events/search\" \\\n -H \"DD-API-KEY: ${DD_API_KEY}\" \\\n -H \"DD-APPLICATION-KEY: ${DD_APP_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"filter\": {\n \"query\": \"service:my-service status:error\",\n \"from\": \"now-1h\",\n \"to\": \"now\"\n },\n \"sort\": \"-timestamp\",\n \"page\": {\"limit\": 50}\n }' | jq .\n```\n\n## Query Metrics\n\n```bash\ncurl -s -G \"https://api.${DD_SITE}/api/v1/query\" \\\n -H \"DD-API-KEY: ${DD_API_KEY}\" \\\n -H \"DD-APPLICATION-KEY: ${DD_APP_KEY}\" \\\n --data-urlencode \"query=avg:system.cpu.user{*}\" \\\n --data-urlencode \"from=$(date -d '1 hour ago' +%s)\" \\\n --data-urlencode \"to=$(date +%s)\" | jq .\n```\n\n## Query APM Traces\n\n```bash\ncurl -s -X POST \"https://api.${DD_SITE}/api/v2/spans/events/search\" \\\n -H \"DD-API-KEY: ${DD_API_KEY}\" \\\n -H \"DD-APPLICATION-KEY: ${DD_APP_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"filter\": {\n \"query\": \"service:my-service\",\n \"from\": \"now-1h\",\n \"to\": \"now\"\n },\n \"sort\": \"-timestamp\",\n \"page\": {\"limit\": 25}\n }' | jq .\n```\n\n## List Monitors\n\n```bash\ncurl -s -G \"https://api.${DD_SITE}/api/v1/monitor\" \\\n -H \"DD-API-KEY: ${DD_API_KEY}\" \\\n -H \"DD-APPLICATION-KEY: ${DD_APP_KEY}\" | jq .\n```\n\n## Documentation\n\n- [Logs API](https://docs.datadoghq.com/api/latest/logs/)\n- [Metrics API](https://docs.datadoghq.com/api/latest/metrics/)\n- [APM/Tracing API](https://docs.datadoghq.com/api/latest/tracing/)\n- [Monitors API](https://docs.datadoghq.com/api/latest/monitors/)\n- [Events API](https://docs.datadoghq.com/api/latest/events/)\n- [Dashboards API](https://docs.datadoghq.com/api/latest/dashboards/)"
94
94
  },
95
95
  {
96
96
  name: "deno",
@@ -114,13 +114,13 @@ var e = [
114
114
  "discord.js",
115
115
  "discord.py"
116
116
  ],
117
- content: "# Discord\n\nUse this skill when implementing or automating Discord integrations.\n\n## Pick the right approach\n\n1. **Incoming webhooks (best for one-way posting)**\n - Good for CI notifications, alerts, build status, etc.\n - No bot user needed.\n - See: https://discord.com/developers/docs/resources/webhook#execute-webhook\n\n2. **Bot token + REST API (two-way / richer automation)**\n - Use when you need to post as a bot, manage channels, read history, moderate, etc.\n - REST API base: `https://discord.com/api/v10`\n - Most REST calls use `Authorization: Bot <token>`.\n\n3. **Interactions / slash commands (user-invoked commands)**\n - Use application commands and interaction webhooks.\n - Typically requires running a web server to receive interactions and respond quickly.\n\n## Secrets & safety\n\n- **Never hard-code tokens**. Use environment variables:\n - `DISCORD_WEBHOOK_URL` for incoming webhooks\n - `DISCORD_BOT_TOKEN` for bot REST API calls\n- Treat webhook URLs as secrets (they include a token).\n- Do **not** automate normal user accounts (“self-bots”). Use official bot/OAuth flows.\n\n## Footguns / safety notes (read this)\n\n- **Webhook URLs are secrets** (the token is embedded in the URL). Don’t paste them into issues, logs, CI output, or chat.\n- **Mentions are dangerous by default**: always set `allowed_mentions` to something strict (these examples use `{\"parse\": []}`) to avoid accidentally pinging `@everyone` / roles.\n- **Watch for accidental secret logging**:\n - If you build your own scripts, avoid including full webhook URLs in exception messages.\n - The bundled scripts sanitize webhook URLs in error output, but you should still avoid printing the URL yourself.\n- **Rate limits**: handle HTTP 429 with `retry_after`/`Retry-After`, and don’t retry forever.\n\n## Quick recipes\n\n### Post a message via an incoming webhook (recommended)\n\nDiscord requires at least one of `content`, `embeds`, `components`, `file`, or `poll`.\n\n```bash\ncurl -sS -X POST \\\n -H 'Content-Type: application/json' \\\n -d '{\"content\":\"Hello from OpenHands\",\"allowed_mentions\":{\"parse\":[]}}' \\\n \"$DISCORD_WEBHOOK_URL\"\n```\n\n### Post a message to a channel with a bot token\n\nEndpoint: `POST /channels/{channel_id}/messages` (Create Message)\n\n```bash\nCHANNEL_ID=\"...\"\n\ncurl -sS -X POST \"https://discord.com/api/v10/channels/${CHANNEL_ID}/messages\" \\\n -H \"Authorization: Bot $DISCORD_BOT_TOKEN\" \\\n -H 'Content-Type: application/json' \\\n -d '{\"content\":\"Hello from my bot\",\"allowed_mentions\":{\"parse\":[]}}'\n```\n\nDocs: https://discord.com/developers/docs/resources/channel#create-message\n\n## Automation scripts (bundled)\n\nThese scripts are self-contained and only use the Python standard library.\n\n- Post to a webhook:\n ```bash\n python3 -m skills.discord.scripts.post_webhook --content \"Build finished\" --wait\n ```\n\n- Post to a channel using a bot token:\n ```bash\n python3 -m skills.discord.scripts.send_message --channel-id \"$CHANNEL_ID\" --content \"Hello\"\n ```\n\n## Rate limits\n\n- Don’t hard-code limits. Use Discord’s `Retry-After` / `retry_after` and rate-limit headers when present.\n- On HTTP **429**, wait for the provided delay (clamp to a sane maximum, add small jitter), then retry.\n\nDocs: https://discord.com/developers/docs/topics/rate-limits\n\n## Slash commands / application commands\n\n- Use **guild commands** for fast iteration (instant updates).\n- Use **global commands** when ready; propagation can take longer.\n\nDocs: https://discord.com/developers/docs/interactions/application-commands\n\n## Reference\n\nFor more details (OAuth2 flows, command registration endpoints, troubleshooting), see:\n- [references/REFERENCE.md](references/REFERENCE.md)"
117
+ content: "# Discord\n\nUse this skill when implementing or automating Discord integrations.\n\n## Pick the right approach\n\n1. **Incoming webhooks (best for one-way posting)**\n - Good for CI notifications, alerts, build status, etc.\n - No bot user needed.\n - See: https://discord.com/developers/docs/resources/webhook#execute-webhook\n\n2. **Bot token + REST API (two-way / richer automation)**\n - Use when you need to post as a bot, manage channels, read history, moderate, etc.\n - REST API base: `https://discord.com/api/v10`\n - Most REST calls use `Authorization: Bot <token>`.\n\n3. **Interactions / slash commands (user-invoked commands)**\n - Use application commands and interaction webhooks.\n - Typically requires running a web server to receive interactions and respond quickly.\n\n## Secrets & safety\n\n- **Never hard-code tokens**. Use environment variables:\n - `DISCORD_WEBHOOK_URL` for incoming webhooks\n - `DISCORD_BOT_TOKEN` for bot REST API calls\n- Treat webhook URLs as secrets (they include a token).\n- Do **not** automate normal user accounts (“self-bots”). Use official bot/OAuth flows.\n\n## Footguns / safety notes (read this)\n\n- **Webhook URLs are secrets** (the token is embedded in the URL). Don’t paste them into issues, logs, CI output, or chat.\n- **Mentions are dangerous by default**: always set `allowed_mentions` to something strict (these examples use `{\"parse\": []}`) to avoid accidentally pinging `@everyone` / roles.\n- **Watch for accidental secret logging**:\n - If you build your own scripts, avoid including full webhook URLs in exception messages.\n - The bundled scripts sanitize webhook URLs in error output, but you should still avoid printing the URL yourself.\n- **Rate limits**: handle HTTP 429 with `retry_after`/`Retry-After`, and don’t retry forever.\n\n## Quick recipes\n\nThe shell snippets below use POSIX-style environment variables and line continuations. On Windows PowerShell, use `curl.exe` for the shown flags and `$env:DISCORD_WEBHOOK_URL` / `$env:DISCORD_BOT_TOKEN` for environment variables, or translate the request to `Invoke-RestMethod`.\n\n### Post a message via an incoming webhook (recommended)\n\nDiscord requires at least one of `content`, `embeds`, `components`, `file`, or `poll`.\n\n```bash\ncurl -sS -X POST \\\n -H 'Content-Type: application/json' \\\n -d '{\"content\":\"Hello from OpenHands\",\"allowed_mentions\":{\"parse\":[]}}' \\\n \"$DISCORD_WEBHOOK_URL\"\n```\n\n### Post a message to a channel with a bot token\n\nEndpoint: `POST /channels/{channel_id}/messages` (Create Message)\n\n```bash\nCHANNEL_ID=\"...\"\n\ncurl -sS -X POST \"https://discord.com/api/v10/channels/${CHANNEL_ID}/messages\" \\\n -H \"Authorization: Bot $DISCORD_BOT_TOKEN\" \\\n -H 'Content-Type: application/json' \\\n -d '{\"content\":\"Hello from my bot\",\"allowed_mentions\":{\"parse\":[]}}'\n```\n\nDocs: https://discord.com/developers/docs/resources/channel#create-message\n\n## Automation scripts (bundled)\n\nThese scripts are self-contained and only use the Python standard library.\n\n- Post to a webhook:\n ```bash\n python3 -m skills.discord.scripts.post_webhook --content \"Build finished\" --wait\n ```\n\n- Post to a channel using a bot token:\n ```bash\n python3 -m skills.discord.scripts.send_message --channel-id \"$CHANNEL_ID\" --content \"Hello\"\n ```\n\n## Rate limits\n\n- Don’t hard-code limits. Use Discord’s `Retry-After` / `retry_after` and rate-limit headers when present.\n- On HTTP **429**, wait for the provided delay (clamp to a sane maximum, add small jitter), then retry.\n\nDocs: https://discord.com/developers/docs/topics/rate-limits\n\n## Slash commands / application commands\n\n- Use **guild commands** for fast iteration (instant updates).\n- Use **global commands** when ready; propagation can take longer.\n\nDocs: https://discord.com/developers/docs/interactions/application-commands\n\n## Reference\n\nFor more details (OAuth2 flows, command registration endpoints, troubleshooting), see:\n- [references/REFERENCE.md](references/REFERENCE.md)"
118
118
  },
119
119
  {
120
120
  name: "docker",
121
121
  description: "Run Docker commands within a container environment, including starting the Docker daemon and managing containers. Use when building, running, or managing Docker containers and images.",
122
122
  triggers: ["docker", "container"],
123
- content: "# Docker Usage Guide\n\n## Starting Docker in Container Environments\n\nPlease check if docker is already installed. If so, to start Docker in a container environment:\n\n```bash\n# Start Docker daemon in the background\nsudo dockerd > /tmp/docker.log 2>&1 &\n\n# Wait for Docker to initialize\nsleep 5\n```\n\n## Verifying Docker Installation\n\nTo verify Docker is working correctly, run the hello-world container:\n\n```bash\nsudo docker run hello-world\n```"
123
+ content: "# Docker Usage Guide\n\n## Starting Docker in Container Environments\n\nPlease check if docker is already installed. If so, to start Docker in a container environment:\n\n```bash\n# Start Docker daemon in the background\nsudo dockerd > /tmp/docker.log 2>&1 &\n\n# Wait for Docker to initialize\nsleep 5\n```\n\nOn Windows, start Docker Desktop or the Docker service instead of running `sudo dockerd`; then run Docker commands from PowerShell without `sudo`.\n\n## Verifying Docker Installation\n\nTo verify Docker is working correctly, run the hello-world container:\n\n```bash\nsudo docker run hello-world\n```\n\nPowerShell equivalent after Docker Desktop is running: `docker run hello-world`."
124
124
  },
125
125
  {
126
126
  name: "evidence-based-citations",
@@ -155,7 +155,7 @@ var e = [
155
155
  name: "github",
156
156
  description: "Interact with GitHub repositories, pull requests, issues, and workflows using the GITHUB_TOKEN environment variable and GitHub CLI. Use when working with code hosted on GitHub or managing GitHub resources.",
157
157
  triggers: ["github", "git"],
158
- content: "You have access to an environment variable, `GITHUB_TOKEN`, which allows you to interact with\nthe GitHub API.\n\n<IMPORTANT>\nYou can use `curl` with the `GITHUB_TOKEN` to interact with GitHub's API.\nALWAYS use the GitHub API for operations instead of a web browser.\nALWAYS use the `create_pr` tool to open a pull request\nIf the user asks you to check GitHub Actions status, first try to use `gh` to work with workflows, and only fallback to basic API calls if that fails.\nExamples:\n- `gh run watch` (https://cli.github.com/manual/gh_run_watch) to monitor workflow runs\n- `gh pr checks 200 --watch --interval 10` to check until completed.\n</IMPORTANT>\n\nIf you encounter authentication issues when pushing to GitHub (such as password prompts or permission errors), the old token may have expired. In such case, update the remote URL to include the current token: `git remote set-url origin https://${GITHUB_TOKEN}@github.com/username/repo.git`\n\nHere are some instructions for pushing, but ONLY do this if the user asks you to:\n* NEVER push directly to the `main` or `master` branch\n* Git config (username and email) is pre-set. Do not modify.\n* You may already be on a branch starting with `openhands-workspace`. Create a new branch with a better name before pushing.\n* Use the `create_pr` tool to create a pull request, if you haven't already\n* Once you've created your own branch or a pull request, continue to update it. Do NOT create a new one unless you are explicitly asked to. Update the PR title and description as necessary, but don't change the branch name.\n* Use the main branch as the base branch, unless the user requests otherwise\n* After opening or updating a pull request, send the user a short message with a link to the pull request.\n* Do NOT mark a pull request as ready to review unless the user explicitly says so\n* Do all of the above in as few steps as possible. E.g. you could push changes with one step by running the following bash commands:\n```bash\ngit remote -v && git branch # to find the current org, repo and branch\ngit checkout -b create-widget && git add . && git commit -m \"Create widget\" && git push -u origin create-widget\n```\n\n## Handling Review Comments\n\n- Critically evaluate each review comment before acting on it. Not all feedback is worth implementing:\n - Does it fix a real bug or improve clarity significantly?\n - Does it align with the project's engineering principles (simplicity, maintainability)?\n - Is the suggested change proportional to the benefit, or does it add unnecessary complexity?\n- It's acceptable to respectfully decline suggestions that add verbosity without clear benefit, over-engineer for hypothetical edge cases, or contradict the project's pragmatic approach.\n- After addressing (or deciding not to address) inline review comments, mark the corresponding review threads as resolved.\n- Before resolving a thread, leave a reply comment that either explains the reason for dismissing the feedback or references the specific commit (e.g., commit SHA) that addressed the issue.\n- Prefer resolving threads only once fixes are pushed or a clear decision is documented.\n- Use the GitHub GraphQL API to reply to and resolve review threads (see below).\n- After making changes to a PR, verify the title and description still match the content. Update them if the scope, features, or intent changed.\n\n## Resolving Review Threads via GraphQL\n\nTo resolve existing review threads programmatically:\n\n1. Get the thread IDs (replace `<OWNER>`, `<REPO>`, `<PR_NUMBER>`):\n```bash\ngh api graphql -f query='\n{\n repository(owner: \"<OWNER>\", name: \"<REPO>\") {\n pullRequest(number: <PR_NUMBER>) {\n reviewThreads(first: 20) {\n nodes {\n id\n isResolved\n comments(first: 1) {\n nodes { body }\n }\n }\n }\n }\n }\n}'\n```\n\n2. Reply to the thread explaining how the feedback was addressed:\n```bash\ngh api graphql -f query='\nmutation {\n addPullRequestReviewThreadReply(input: {\n pullRequestReviewThreadId: \"<THREAD_ID>\"\n body: \"Fixed in <COMMIT_SHA>\"\n }) {\n comment { id }\n }\n}'\n```\n\n3. Resolve the thread:\n```bash\ngh api graphql -f query='\nmutation {\n resolveReviewThread(input: {threadId: \"<THREAD_ID>\"}) {\n thread { isResolved }\n }\n}'\n```\n\n4. Get the failed workflow run ID and rerun it:\n```bash\n# Find the run ID from the failed check URL, or use:\ngh run list --repo <OWNER>/<REPO> --branch <BRANCH> --limit 5\n\n# Rerun failed jobs\ngh run rerun <RUN_ID> --repo <OWNER>/<REPO> --failed\n```"
158
+ content: "You have access to an environment variable, `GITHUB_TOKEN`, which allows you to interact with\nthe GitHub API.\n\n<IMPORTANT>\nYou can use `curl` with the `GITHUB_TOKEN` to interact with GitHub's API.\nALWAYS use the GitHub API for operations instead of a web browser.\nALWAYS use the `create_pr` tool to open a pull request\nIf the user asks you to check GitHub Actions status, first try to use `gh` to work with workflows, and only fallback to basic API calls if that fails.\nExamples:\n- `gh run watch` (https://cli.github.com/manual/gh_run_watch) to monitor workflow runs\n- `gh pr checks 200 --watch --interval 10` to check until completed.\n</IMPORTANT>\n\nWindows PowerShell equivalents for the multi-line shell snippets below are in `references/windows.md`.\n\nIf you encounter authentication issues when pushing to GitHub (such as password prompts or permission errors), the old token may have expired. In such case, update the remote URL to include the current token: `git remote set-url origin https://${GITHUB_TOKEN}@github.com/username/repo.git`\n\nHere are some instructions for pushing, but ONLY do this if the user asks you to:\n* NEVER push directly to the `main` or `master` branch\n* Git config (username and email) is pre-set. Do not modify.\n* You may already be on a branch starting with `openhands-workspace`. Create a new branch with a better name before pushing.\n* Use the `create_pr` tool to create a pull request, if you haven't already\n* Once you've created your own branch or a pull request, continue to update it. Do NOT create a new one unless you are explicitly asked to. Update the PR title and description as necessary, but don't change the branch name.\n* Use the main branch as the base branch, unless the user requests otherwise\n* After opening or updating a pull request, send the user a short message with a link to the pull request.\n* Do NOT mark a pull request as ready to review unless the user explicitly says so\n* Do all of the above in as few steps as possible. E.g. you could push changes with one step by running the following bash commands:\n```bash\ngit remote -v && git branch # to find the current org, repo and branch\ngit checkout -b create-widget && git add . && git commit -m \"Create widget\" && git push -u origin create-widget\n```\n\n## Handling Review Comments\n\n- Critically evaluate each review comment before acting on it. Not all feedback is worth implementing:\n - Does it fix a real bug or improve clarity significantly?\n - Does it align with the project's engineering principles (simplicity, maintainability)?\n - Is the suggested change proportional to the benefit, or does it add unnecessary complexity?\n- It's acceptable to respectfully decline suggestions that add verbosity without clear benefit, over-engineer for hypothetical edge cases, or contradict the project's pragmatic approach.\n- After addressing (or deciding not to address) inline review comments, mark the corresponding review threads as resolved.\n- Before resolving a thread, leave a reply comment that either explains the reason for dismissing the feedback or references the specific commit (e.g., commit SHA) that addressed the issue.\n- Prefer resolving threads only once fixes are pushed or a clear decision is documented.\n- Use the GitHub GraphQL API to reply to and resolve review threads (see below).\n- After making changes to a PR, verify the title and description still match the content. Update them if the scope, features, or intent changed.\n\n## Resolving Review Threads via GraphQL\n\nTo resolve existing review threads programmatically:\n\n1. Get the thread IDs (replace `<OWNER>`, `<REPO>`, `<PR_NUMBER>`):\n```bash\ngh api graphql -f query='\n{\n repository(owner: \"<OWNER>\", name: \"<REPO>\") {\n pullRequest(number: <PR_NUMBER>) {\n reviewThreads(first: 20) {\n nodes {\n id\n isResolved\n comments(first: 1) {\n nodes { body }\n }\n }\n }\n }\n }\n}'\n```\n\n2. Reply to the thread explaining how the feedback was addressed:\n```bash\ngh api graphql -f query='\nmutation {\n addPullRequestReviewThreadReply(input: {\n pullRequestReviewThreadId: \"<THREAD_ID>\"\n body: \"Fixed in <COMMIT_SHA>\"\n }) {\n comment { id }\n }\n}'\n```\n\n3. Resolve the thread:\n```bash\ngh api graphql -f query='\nmutation {\n resolveReviewThread(input: {threadId: \"<THREAD_ID>\"}) {\n thread { isResolved }\n }\n}'\n```\n\n4. Get the failed workflow run ID and rerun it:\n```bash\n# Find the run ID from the failed check URL, or use:\ngh run list --repo <OWNER>/<REPO> --branch <BRANCH> --limit 5\n\n# Rerun failed jobs\ngh run rerun <RUN_ID> --repo <OWNER>/<REPO> --failed\n```"
159
159
  },
160
160
  {
161
161
  name: "github-actions",
@@ -173,31 +173,31 @@ var e = [
173
173
  name: "github-pr-review",
174
174
  description: "Post PR review comments using the GitHub API with inline comments, suggestions, and priority labels.",
175
175
  triggers: ["/github-pr-review"],
176
- content: "# GitHub PR Review\n\nPost structured code review feedback using the GitHub API with inline comments on specific lines.\n\n## Key Rule: One API Call\n\nBundle ALL comments into a **single review API call**. Do not post comments individually.\n\n## Posting a Review\n\nUse the GitHub CLI (`gh`) with a JSON input file. The `GITHUB_TOKEN` is automatically available.\n\n**Important**: Always use `--input` with a JSON file instead of `-F` flags. This avoids shell quoting issues with special characters in comment bodies (quotes, backticks, newlines, etc.) and eliminates the need for complex heredoc scripts.\n\n### Step 1: Create a JSON file\n\n```bash\ncat > /tmp/review.json << 'EOF'\n{\n \"commit_id\": \"{commit_sha}\",\n \"event\": \"COMMENT\",\n \"body\": \"Brief 1-3 sentence summary.\",\n \"comments\": [\n {\n \"path\": \"path/to/file.py\",\n \"line\": 42,\n \"side\": \"RIGHT\",\n \"body\": \"🟠 Important: Your comment here.\"\n },\n {\n \"path\": \"another/file.js\",\n \"line\": 15,\n \"side\": \"RIGHT\",\n \"body\": \"🟡 Suggestion: Another comment.\"\n }\n ]\n}\nEOF\n```\n\n### Step 2: Post the review\n\n```bash\ngh api -X POST repos/{owner}/{repo}/pulls/{pr_number}/reviews --input /tmp/review.json\n```\n\n### Parameters\n\n| Parameter | Description |\n|-----------|-------------|\n| `commit_id` | Commit SHA to comment on (use `git rev-parse HEAD`) |\n| `event` | `COMMENT`, `APPROVE`, or `REQUEST_CHANGES` |\n| `path` | File path as shown in the diff |\n| `line` | Line number in the NEW version (right side of diff) |\n| `side` | `RIGHT` for new/added lines, `LEFT` for deleted lines |\n| `body` | Comment text with priority label |\n\n### Multi-Line Comments\n\nFor comments spanning multiple lines, add `start_line` to specify the range:\n\n```json\n{\n \"path\": \"path/to/file.py\",\n \"start_line\": 10,\n \"line\": 12,\n \"side\": \"RIGHT\",\n \"body\": \"🟡 Suggestion: Refactor this block:\\n\\n```suggestion\\nline_one = \\\"new\\\"\\nline_two = \\\"code\\\"\\nline_three = \\\"here\\\"\\n```\"\n}\n```\n\n**`start_line`/`line` define the range that will be REPLACED.** The suggestion block may have any number of lines — it does **not** have to match the range size. See the next section for the exact semantics; getting this wrong is how suggestions silently delete or duplicate code.\n\n## Priority Labels\n\nStart each comment with a priority label. **Minimize nits** - leave minor style issues to linters.\n\n| Label | When to Use |\n|-------|-------------|\n| 🔴 **Critical** | Must fix: security vulnerabilities, bugs, data loss risks |\n| 🟠 **Important** | Should fix: logic errors, performance issues, missing error handling |\n| 🟡 **Suggestion** | Worth considering: significant improvements to clarity or maintainability |\n\n**Do NOT post 🟢 Nit or 🟢 Acceptable comments.** If code is fine, simply don't comment on it. Inline comments that say \"this looks good\" or \"acceptable trade-off\" are noise — they create review threads that must be resolved without providing actionable value.\n\n**Example:**\n```\n🟠 Important: This function doesn't handle None, which could cause an AttributeError.\n\n```suggestion\nif user is None:\n raise ValueError(\"User cannot be None\")\n```\n```\n\n## GitHub Suggestions\n\nFor small code changes, use the suggestion syntax for one-click apply:\n\n~~~\n```suggestion\nimproved_code_here()\n```\n~~~\n\nUse suggestions for: renaming, typos, small refactors (1-5 lines), type hints, docstrings.\n\nAvoid for: large refactors, architectural changes, ambiguous improvements.\n\n### How Suggestions Actually Work (READ THIS BEFORE WRITING ONE)\n\nA suggestion block **replaces** the targeted range with its contents. The replaced range is:\n\n- `line` only → the single line `line` (replaces 1 line)\n- `start_line` + `line` → the inclusive range `start_line..line` (replaces `line - start_line + 1` lines)\n\nThe suggestion content can be **any number of lines** — 0 (deletion), 1, or many. It does not have to match the range size. Whatever is between the ` ```suggestion ` and closing ` ``` ` fences becomes the new content of those lines.\n\nWriting the wrong combination of `start_line`/`line` and suggestion body is what causes accepted suggestions to **duplicate** or **delete** code. Use the table below as your contract:\n\n| Intent | `start_line` | `line` | Suggestion body must contain |\n|--------|--------------|--------|-------------------------------|\n| Change line N | omit | N | the new content for line N |\n| Change lines N..M | N | M | the new content for the whole block |\n| **Add** a line **after** line N (keep line N) | omit | N | line N's exact current text, then the new line(s) |\n| **Add** a line **before** line N (keep line N) | omit | N | the new line(s), then line N's exact current text |\n| **Insert** lines inside range N..M (keep N..M) | N | M | every original line in N..M plus the new lines, in the final desired order |\n| **Delete** line N | omit | N | empty body (just an empty ` ```suggestion ``` ` block) |\n| **Delete** lines N..M | N | M | empty body |\n\n### Common Mistakes That Break Code\n\n1. **Duplicated lines.** You copy a neighboring line (N-1 or N+1) into the suggestion body as context — that line is still present in the file outside the replaced range, so accepting the suggestion inserts a second copy of it. Fix: only include lines that fall within the targeted range, plus any genuinely new content.\n2. **Disappearing lines.** You target `start_line=10, line=12` to comment on a 3-line block, but your suggestion body only contains 1 line because you \"only want to change line 11\". Accepting that suggestion deletes lines 10 and 12. Fix: either narrow the range to just line 11, or include lines 10 and 12 verbatim in the body.\n3. **Description does not match the suggestion.** The prose says \"rename this variable\" but the suggestion replaces an entire function. Or the prose says \"add a None check\" but the suggestion only contains the check (deleting the original code). Fix: after writing the suggestion, re-read the prose and confirm the resulting file would match it line-for-line.\n\n### Mandatory Verification Before Posting\n\nFor every comment that contains a ` ```suggestion ``` ` block, do this check before adding it to the review JSON:\n\n1. Read the actual file lines that will be replaced: `sed -n '<start_line>,<line>p' <path>` (or `sed -n '<line>p' <path>` for a single-line target).\n2. Mentally apply the suggestion: drop those lines, splice in the suggestion body, and look at the result in context.\n3. Confirm the resulting code matches **exactly** what your prose description promises — no extra duplicated line above/below, no original line accidentally dropped, no off-by-one.\n4. If the change cannot be expressed cleanly as a contiguous replacement (e.g., it touches non-adjacent lines, or it depends on edits elsewhere in the file), do **not** use a suggestion block — describe the change in prose instead.\n\nIf you are not 100% sure the suggestion will produce the exact code you described, drop the ` ```suggestion ``` ` block and leave a regular inline comment. A correct prose comment is always better than a one-click suggestion that silently corrupts the file.\n\n## Finding Line Numbers\n\n```bash\n# From diff header: @@ -old_start,old_count +new_start,new_count @@\n# Count from new_start for added/modified lines\n\ngrep -n \"pattern\" filename # Find line number\nhead -n 42 filename | tail -1 # Verify line content\n```\n\n## Fallback: curl\n\nIf `gh` is unavailable, use curl with the JSON file:\n\n```bash\ncurl -X POST \\\n -H \"Authorization: token $GITHUB_TOKEN\" \\\n -H \"Accept: application/vnd.github+json\" \\\n \"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}/reviews\" \\\n -d @/tmp/review.json\n```\n\n## Summary\n\n1. Analyze the code and identify important issues (minimize nits)\n2. Write review data to a JSON file (e.g., `/tmp/review.json`)\n3. Post **ONE** review using `gh api --input /tmp/review.json`\n4. Use priority labels (🔴🟠🟡) on every comment\n5. Do NOT post comments for code that is acceptable — only comment when action is needed\n6. Use suggestion syntax for concrete code changes, but only after verifying the resulting code matches your description (see \"How Suggestions Actually Work\")\n7. Keep the review body brief (details go in inline comments)\n8. If no issues: post a short approval message with no inline comments"
176
+ content: "# GitHub PR Review\n\nPost structured code review feedback using the GitHub API with inline comments on specific lines.\nWindows PowerShell equivalents for JSON file creation, temp paths, line lookup, and fallback `curl` are in `references/windows.md`.\n\n## Key Rule: One API Call\n\nBundle ALL comments into a **single review API call**. Do not post comments individually.\n\n## Posting a Review\n\nUse the GitHub CLI (`gh`) with a JSON input file. The `GITHUB_TOKEN` is automatically available.\n\n**Important**: Always use `--input` with a JSON file instead of `-F` flags. This avoids shell quoting issues with special characters in comment bodies (quotes, backticks, newlines, etc.) and eliminates the need for complex heredoc scripts.\n\n### Step 1: Create a JSON file\n\n```bash\ncat > /tmp/review.json << 'EOF'\n{\n \"commit_id\": \"{commit_sha}\",\n \"event\": \"COMMENT\",\n \"body\": \"Brief 1-3 sentence summary.\",\n \"comments\": [\n {\n \"path\": \"path/to/file.py\",\n \"line\": 42,\n \"side\": \"RIGHT\",\n \"body\": \"🟠 Important: Your comment here.\"\n },\n {\n \"path\": \"another/file.js\",\n \"line\": 15,\n \"side\": \"RIGHT\",\n \"body\": \"🟡 Suggestion: Another comment.\"\n }\n ]\n}\nEOF\n```\n\n### Step 2: Post the review\n\n```bash\ngh api -X POST repos/{owner}/{repo}/pulls/{pr_number}/reviews --input /tmp/review.json\n```\n\n### Parameters\n\n| Parameter | Description |\n|-----------|-------------|\n| `commit_id` | Commit SHA to comment on (use `git rev-parse HEAD`) |\n| `event` | `COMMENT`, `APPROVE`, or `REQUEST_CHANGES` |\n| `path` | File path as shown in the diff |\n| `line` | Line number in the NEW version (right side of diff) |\n| `side` | `RIGHT` for new/added lines, `LEFT` for deleted lines |\n| `body` | Comment text with priority label |\n\n### Multi-Line Comments\n\nFor comments spanning multiple lines, add `start_line` to specify the range:\n\n```json\n{\n \"path\": \"path/to/file.py\",\n \"start_line\": 10,\n \"line\": 12,\n \"side\": \"RIGHT\",\n \"body\": \"🟡 Suggestion: Refactor this block:\\n\\n```suggestion\\nline_one = \\\"new\\\"\\nline_two = \\\"code\\\"\\nline_three = \\\"here\\\"\\n```\"\n}\n```\n\n**`start_line`/`line` define the range that will be REPLACED.** The suggestion block may have any number of lines — it does **not** have to match the range size. See the next section for the exact semantics; getting this wrong is how suggestions silently delete or duplicate code.\n\n## Priority Labels\n\nStart each comment with a priority label. **Minimize nits** - leave minor style issues to linters.\n\n| Label | When to Use |\n|-------|-------------|\n| 🔴 **Critical** | Must fix: security vulnerabilities, bugs, data loss risks |\n| 🟠 **Important** | Should fix: logic errors, performance issues, missing error handling |\n| 🟡 **Suggestion** | Worth considering: significant improvements to clarity or maintainability |\n\n**Do NOT post 🟢 Nit or 🟢 Acceptable comments.** If code is fine, simply don't comment on it. Inline comments that say \"this looks good\" or \"acceptable trade-off\" are noise — they create review threads that must be resolved without providing actionable value.\n\n**Example:**\n```\n🟠 Important: This function doesn't handle None, which could cause an AttributeError.\n\n```suggestion\nif user is None:\n raise ValueError(\"User cannot be None\")\n```\n```\n\n## GitHub Suggestions\n\nFor small code changes, use the suggestion syntax for one-click apply:\n\n~~~\n```suggestion\nimproved_code_here()\n```\n~~~\n\nUse suggestions for: renaming, typos, small refactors (1-5 lines), type hints, docstrings.\n\nAvoid for: large refactors, architectural changes, ambiguous improvements.\n\n### How Suggestions Actually Work (READ THIS BEFORE WRITING ONE)\n\nA suggestion block **replaces** the targeted range with its contents. The replaced range is:\n\n- `line` only → the single line `line` (replaces 1 line)\n- `start_line` + `line` → the inclusive range `start_line..line` (replaces `line - start_line + 1` lines)\n\nThe suggestion content can be **any number of lines** — 0 (deletion), 1, or many. It does not have to match the range size. Whatever is between the ` ```suggestion ` and closing ` ``` ` fences becomes the new content of those lines.\n\nWriting the wrong combination of `start_line`/`line` and suggestion body is what causes accepted suggestions to **duplicate** or **delete** code. Use the table below as your contract:\n\n| Intent | `start_line` | `line` | Suggestion body must contain |\n|--------|--------------|--------|-------------------------------|\n| Change line N | omit | N | the new content for line N |\n| Change lines N..M | N | M | the new content for the whole block |\n| **Add** a line **after** line N (keep line N) | omit | N | line N's exact current text, then the new line(s) |\n| **Add** a line **before** line N (keep line N) | omit | N | the new line(s), then line N's exact current text |\n| **Insert** lines inside range N..M (keep N..M) | N | M | every original line in N..M plus the new lines, in the final desired order |\n| **Delete** line N | omit | N | empty body (just an empty ` ```suggestion ``` ` block) |\n| **Delete** lines N..M | N | M | empty body |\n\n### Common Mistakes That Break Code\n\n1. **Duplicated lines.** You copy a neighboring line (N-1 or N+1) into the suggestion body as context — that line is still present in the file outside the replaced range, so accepting the suggestion inserts a second copy of it. Fix: only include lines that fall within the targeted range, plus any genuinely new content.\n2. **Disappearing lines.** You target `start_line=10, line=12` to comment on a 3-line block, but your suggestion body only contains 1 line because you \"only want to change line 11\". Accepting that suggestion deletes lines 10 and 12. Fix: either narrow the range to just line 11, or include lines 10 and 12 verbatim in the body.\n3. **Description does not match the suggestion.** The prose says \"rename this variable\" but the suggestion replaces an entire function. Or the prose says \"add a None check\" but the suggestion only contains the check (deleting the original code). Fix: after writing the suggestion, re-read the prose and confirm the resulting file would match it line-for-line.\n\n### Mandatory Verification Before Posting\n\nFor every comment that contains a ` ```suggestion ``` ` block, do this check before adding it to the review JSON:\n\n1. Read the actual file lines that will be replaced: `sed -n '<start_line>,<line>p' <path>` (or `sed -n '<line>p' <path>` for a single-line target).\n2. Mentally apply the suggestion: drop those lines, splice in the suggestion body, and look at the result in context.\n3. Confirm the resulting code matches **exactly** what your prose description promises — no extra duplicated line above/below, no original line accidentally dropped, no off-by-one.\n4. If the change cannot be expressed cleanly as a contiguous replacement (e.g., it touches non-adjacent lines, or it depends on edits elsewhere in the file), do **not** use a suggestion block — describe the change in prose instead.\n\nIf you are not 100% sure the suggestion will produce the exact code you described, drop the ` ```suggestion ``` ` block and leave a regular inline comment. A correct prose comment is always better than a one-click suggestion that silently corrupts the file.\n\n## Finding Line Numbers\n\n```bash\n# From diff header: @@ -old_start,old_count +new_start,new_count @@\n# Count from new_start for added/modified lines\n\ngrep -n \"pattern\" filename # Find line number\nhead -n 42 filename | tail -1 # Verify line content\n```\n\n## Fallback: curl\n\nIf `gh` is unavailable, use curl with the JSON file:\n\n```bash\ncurl -X POST \\\n -H \"Authorization: token $GITHUB_TOKEN\" \\\n -H \"Accept: application/vnd.github+json\" \\\n \"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}/reviews\" \\\n -d @/tmp/review.json\n```\n\n## Summary\n\n1. Analyze the code and identify important issues (minimize nits)\n2. Write review data to a JSON file (e.g., `/tmp/review.json`)\n3. Post **ONE** review using `gh api --input /tmp/review.json`\n4. Use priority labels (🔴🟠🟡) on every comment\n5. Do NOT post comments for code that is acceptable — only comment when action is needed\n6. Use suggestion syntax for concrete code changes, but only after verifying the resulting code matches your description (see \"How Suggestions Actually Work\")\n7. Keep the review body brief (details go in inline comments)\n8. If no issues: post a short approval message with no inline comments"
177
177
  },
178
178
  {
179
179
  name: "github-pr-reviewer",
180
180
  description: "Create an automation that reviews GitHub pull requests when a configurable trigger label is applied. Polls GitHub deterministically, starts one OpenHands review conversation per label event, inspects full repository and PR context, and posts the final review comment back to GitHub.",
181
181
  triggers: ["/pr-reviewer:setup"],
182
- content: "# GitHub PR Reviewer Automation\n\nCreate a cron automation that watches a GitHub repository for pull requests\nwith a review trigger label, starts an OpenHands review conversation once per\nlabel event, and posts the AI review as a GitHub comment.\n\nThe automation script is deterministic: PR discovery, label-event tracking,\nstate persistence, stale-result suppression, and GitHub comment posting are\nhandled in Python. The LLM is invoked only for the review itself.\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` for private repos or `public_repo` for public repos |\n| `GITHUB_PERSONAL_ACCESS_TOKEN` | Fine-grained PAT | Contents: Read, Metadata: Read, Pull requests: Read, Issues: Read and Write |\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\n token is invalid and ask them to update it. Stop.\n\n### Step 2 - Collect repository\n\nAsk: *\"Which GitHub repository should be monitored?\n(Format: `owner/repo`, e.g. `myorg/backend`)\"*\n\nValidate access:\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. Private: {d.get('private')}. Permissions: {d.get('permissions')}\\\")\n\"\n```\n\nRecord `REPO = \"{owner}/{repo}\"`.\n\n### Step 3 - Collect trigger label\n\nAsk: *\"Which PR label should trigger a review?\n(Press Enter for the default: `openhands-review`.)\"*\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 a PR.\n\nThe automation reviews a PR when it sees the latest matching `labeled` event for\nthat label. To request another review later, remove and re-apply the label.\n\n### Step 4 - Collect review tone\n\nAsk: *\"What review tone should the reviewer use?\n 1. Thorough (default) - comprehensive coverage of correctness, security, tests, style\n 2. Concise - high-signal only, skips minor style feedback\n 3. Friendly - constructive and encouraging\n(Press Enter for Thorough, or type your choice or any custom style description)\"*\n\nMap the choice to `REVIEW_TONE`:\n\n| Answer | `REVIEW_TONE` | `REVIEW_STYLE_INSTRUCTIONS` |\n|---|---|---|\n| 1 / Enter | `\"thorough\"` | `\"\"` |\n| 2 | `\"concise\"` | `\"\"` |\n| 3 | `\"friendly\"` | `\"\"` |\n| Custom text, e.g. `strict but kind` | `\"thorough\"` | the custom text verbatim |\n\n### Step 5 - Collect cron schedule\n\nAsk: *\"How often should the automation poll for labeled PRs?\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 6 - 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| Placeholder | Replace with |\n|---|---|\n| `REPO = \"owner/repo\"` | `REPO = \"{owner_repo}\"` |\n| `TRIGGER_LABEL = \"openhands-review\"` | `TRIGGER_LABEL = \"{trigger_label}\"` |\n| `REVIEW_TONE = \"thorough\"` | `REVIEW_TONE = \"{review_tone}\"` |\n| `REVIEW_STYLE_INSTRUCTIONS = \"\"` | `REVIEW_STYLE_INSTRUCTIONS = \"{style_instructions}\"` |\n| `DEFAULT_OPENHANDS_URL = \"http://localhost:8000\"` | leave unchanged unless the user has a preference |\n\nUse a safe string writer such as `json.dumps(value)` when inserting user-provided\nrepository names, labels, or style instructions into Python string literals.\n\nWrite the customized script to a temporary build directory:\n```bash\nmkdir -p /tmp/pr-reviewer-build\n# write the customized main.py to /tmp/pr-reviewer-build/main.py\n```\n\nValidate syntax before packaging:\n```bash\npython3 -m py_compile /tmp/pr-reviewer-build/main.py && echo \"Syntax OK\"\n```\n\nFix any syntax errors before proceeding.\n\n### Step 7 - 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/pr-reviewer.tar.gz -C /tmp/pr-reviewer-build .\n\nTARBALL_PATH=$(curl -s -X POST \\\n \"${OPENHANDS_HOST}/api/automation/v1/uploads?name=github-pr-reviewer\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/gzip\" \\\n --data-binary @/tmp/pr-reviewer.tar.gz \\\n | python3 -c \"import json,sys; print(json.load(sys.stdin)['tarball_path'])\")\n\necho \"Uploaded: $TARBALL_PATH\"\n```\n\n### Step 8 - 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 PR Reviewer: {owner}/{repo} label {trigger_label}\\\",\n \\\"trigger\\\": {\\\"type\\\": \\\"cron\\\", \\\"schedule\\\": \\\"{cron_schedule}\\\"},\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 300\n }\" | python3 -m json.tool\n```\n\nRecord the returned `id`.\n\n### Step 9 - Confirm\n\nTell the user:\n\n> ✅ **GitHub PR Reviewer** is running!\n>\n> - Automation ID: `{id}`\n> - Repository: `{owner}/{repo}`\n> - Trigger label: `{trigger_label}`\n> - Review tone: `{tone}`\n> - Polling schedule: `{cron_schedule}`\n> - State file: `~/.openhands/workspaces/automation-state/github_pr_reviewer_label_event_{id}.json`\n>\n> Apply the `{trigger_label}` label to a pull request to queue a review. Each\n> label event is processed once. To request another review, remove and re-apply\n> the label.\n\n---\n\n## Runtime Behaviour (per poll)\n\nEach cron run executes `main.py`, which:\n\n1. Loads state from the JSON file (see `references/state-schema.md`).\n2. Resolves and validates `GITHUB_PERSONAL_ACCESS_TOKEN` and repository access.\n3. Lists open PRs, newest-updated first.\n4. For each open PR carrying `TRIGGER_LABEL`:\n - Refetches current PR metadata to avoid acting on stale list data.\n - Finds the latest matching GitHub `labeled` issue event.\n - Skips the event if it has already been tracked.\n - Starts an OpenHands conversation with a review prompt that includes PR\n metadata, the exact head SHA, label event details, and instructions to\n clone the repo, inspect PR discussion, review comments, changed files,\n diff, and surrounding code.\n - Posts an acknowledgement comment with the label event, head SHA, and\n conversation link.\n - Records the label-event review in state with `status: \"active\"`.\n5. For each active review conversation:\n - Marks it closed without posting if the PR has closed or merged.\n - Suppresses stale results if the PR head SHA changed after the review was\n queued.\n - When the conversation reaches `idle`, `finished`, `error`, or `stuck`,\n posts the agent's final response as a GitHub comment and marks the review\n closed.\n6. Saves state atomically and fires the completion callback.\n\n---\n\n## Additional Resources\n\n- **`references/state-schema.md`** - State JSON schema, field definitions, and\n review lifecycle diagram.\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| Bot never queues reviews | Trigger label not present or no matching `labeled` event | Apply the configured label to the PR |\n| \"Bad credentials\" in run logs | Token expired | Rotate and update `GITHUB_PERSONAL_ACCESS_TOKEN` |\n| 404 on repo access | Repo name wrong or no access | Re-check `owner/repo` and token permissions |\n| Same PR not reviewed after new commits | Label event was already processed | Remove and re-apply the trigger label |\n| Review result never posts | Conversation still running or stuck | Open the conversation link from the acknowledgement comment |\n| Stale review suppressed | PR head SHA changed while the agent was reviewing | Re-apply the trigger label after the latest commit |"
182
+ content: "# GitHub PR Reviewer Automation\n\nCreate a cron automation that watches a GitHub repository for pull requests\nwith a review trigger label, starts an OpenHands review conversation once per\nlabel event, and posts the AI review as a GitHub comment.\nWindows PowerShell equivalents for the setup, packaging, upload, and API-check shell snippets are in `references/windows.md`.\n\nThe automation script is deterministic: PR discovery, label-event tracking,\nstate persistence, stale-result suppression, and GitHub comment posting are\nhandled in Python. The LLM is invoked only for the review itself.\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` for private repos or `public_repo` for public repos |\n| `GITHUB_PERSONAL_ACCESS_TOKEN` | Fine-grained PAT | Contents: Read, Metadata: Read, Pull requests: Read, Issues: Read and Write |\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\n token is invalid and ask them to update it. Stop.\n\n### Step 2 - Collect repository\n\nAsk: *\"Which GitHub repository should be monitored?\n(Format: `owner/repo`, e.g. `myorg/backend`)\"*\n\nValidate access:\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. Private: {d.get('private')}. Permissions: {d.get('permissions')}\\\")\n\"\n```\n\nRecord `REPO = \"{owner}/{repo}\"`.\n\n### Step 3 - Collect trigger label\n\nAsk: *\"Which PR label should trigger a review?\n(Press Enter for the default: `openhands-review`.)\"*\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 a PR.\n\nThe automation reviews a PR when it sees the latest matching `labeled` event for\nthat label. To request another review later, remove and re-apply the label.\n\n### Step 4 - Collect review tone\n\nAsk: *\"What review tone should the reviewer use?\n 1. Thorough (default) - comprehensive coverage of correctness, security, tests, style\n 2. Concise - high-signal only, skips minor style feedback\n 3. Friendly - constructive and encouraging\n(Press Enter for Thorough, or type your choice or any custom style description)\"*\n\nMap the choice to `REVIEW_TONE`:\n\n| Answer | `REVIEW_TONE` | `REVIEW_STYLE_INSTRUCTIONS` |\n|---|---|---|\n| 1 / Enter | `\"thorough\"` | `\"\"` |\n| 2 | `\"concise\"` | `\"\"` |\n| 3 | `\"friendly\"` | `\"\"` |\n| Custom text, e.g. `strict but kind` | `\"thorough\"` | the custom text verbatim |\n\n### Step 5 - Collect cron schedule\n\nAsk: *\"How often should the automation poll for labeled PRs?\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 6 - 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| Placeholder | Replace with |\n|---|---|\n| `REPO = \"owner/repo\"` | `REPO = \"{owner_repo}\"` |\n| `TRIGGER_LABEL = \"openhands-review\"` | `TRIGGER_LABEL = \"{trigger_label}\"` |\n| `REVIEW_TONE = \"thorough\"` | `REVIEW_TONE = \"{review_tone}\"` |\n| `REVIEW_STYLE_INSTRUCTIONS = \"\"` | `REVIEW_STYLE_INSTRUCTIONS = \"{style_instructions}\"` |\n| `DEFAULT_OPENHANDS_URL = \"http://localhost:8000\"` | leave unchanged unless the user has a preference |\n\nUse a safe string writer such as `json.dumps(value)` when inserting user-provided\nrepository names, labels, or style instructions into Python string literals.\n\nWrite the customized script to a temporary build directory:\n```bash\nmkdir -p /tmp/pr-reviewer-build\n# write the customized main.py to /tmp/pr-reviewer-build/main.py\n```\n\nValidate syntax before packaging:\n```bash\npython3 -m py_compile /tmp/pr-reviewer-build/main.py && echo \"Syntax OK\"\n```\n\nFix any syntax errors before proceeding.\n\n### Step 7 - 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/pr-reviewer.tar.gz -C /tmp/pr-reviewer-build .\n\nTARBALL_PATH=$(curl -s -X POST \\\n \"${OPENHANDS_HOST}/api/automation/v1/uploads?name=github-pr-reviewer\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/gzip\" \\\n --data-binary @/tmp/pr-reviewer.tar.gz \\\n | python3 -c \"import json,sys; print(json.load(sys.stdin)['tarball_path'])\")\n\necho \"Uploaded: $TARBALL_PATH\"\n```\n\n### Step 8 - 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 PR Reviewer: {owner}/{repo} label {trigger_label}\\\",\n \\\"trigger\\\": {\\\"type\\\": \\\"cron\\\", \\\"schedule\\\": \\\"{cron_schedule}\\\"},\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 300\n }\" | python3 -m json.tool\n```\n\nRecord the returned `id`.\n\n### Step 9 - Confirm\n\nTell the user:\n\n> ✅ **GitHub PR Reviewer** is running!\n>\n> - Automation ID: `{id}`\n> - Repository: `{owner}/{repo}`\n> - Trigger label: `{trigger_label}`\n> - Review tone: `{tone}`\n> - Polling schedule: `{cron_schedule}`\n> - State file: `~/.openhands/workspaces/automation-state/github_pr_reviewer_label_event_{id}.json`\n>\n> Apply the `{trigger_label}` label to a pull request to queue a review. Each\n> label event is processed once. To request another review, remove and re-apply\n> the label.\n\n---\n\n## Runtime Behaviour (per poll)\n\nEach cron run executes `main.py`, which:\n\n1. Loads state from the JSON file (see `references/state-schema.md`).\n2. Resolves and validates `GITHUB_PERSONAL_ACCESS_TOKEN` and repository access.\n3. Lists open PRs, newest-updated first.\n4. For each open PR carrying `TRIGGER_LABEL`:\n - Refetches current PR metadata to avoid acting on stale list data.\n - Finds the latest matching GitHub `labeled` issue event.\n - Skips the event if it has already been tracked.\n - Starts an OpenHands conversation with a review prompt that includes PR\n metadata, the exact head SHA, label event details, and instructions to\n clone the repo, inspect PR discussion, review comments, changed files,\n diff, and surrounding code.\n - Posts an acknowledgement comment with the label event, head SHA, and\n conversation link.\n - Records the label-event review in state with `status: \"active\"`.\n5. For each active review conversation:\n - Marks it closed without posting if the PR has closed or merged.\n - Suppresses stale results if the PR head SHA changed after the review was\n queued.\n - When the conversation reaches `idle`, `finished`, `error`, or `stuck`,\n posts the agent's final response as a GitHub comment and marks the review\n closed.\n6. Saves state atomically and fires the completion callback.\n\n---\n\n## Additional Resources\n\n- **`references/state-schema.md`** - State JSON schema, field definitions, and\n review lifecycle diagram.\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| Bot never queues reviews | Trigger label not present or no matching `labeled` event | Apply the configured label to the PR |\n| \"Bad credentials\" in run logs | Token expired | Rotate and update `GITHUB_PERSONAL_ACCESS_TOKEN` |\n| 404 on repo access | Repo name wrong or no access | Re-check `owner/repo` and token permissions |\n| Same PR not reviewed after new commits | Label event was already processed | Remove and re-apply the trigger label |\n| Review result never posts | Conversation still running or stuck | Open the conversation link from the acknowledgement comment |\n| Stale review suppressed | PR head SHA changed while the agent was reviewing | Re-apply the trigger label after the latest commit |"
183
183
  },
184
184
  {
185
185
  name: "github-repo-monitor",
186
186
  description: "This skill should be used when the user asks to \"monitor a GitHub repository\", \"watch GitHub for issues or PRs\", \"respond to @OpenHands mentions on GitHub\", \"set up an OpenHands GitHub integration\", \"trigger OpenHands from a GitHub comment\", or \"poll a GitHub repo for a trigger phrase\". Guides the user through creating a cron automation that polls a single repository and starts an OpenHands conversation whenever a configurable trigger phrase is detected in an issue or PR comment.",
187
187
  triggers: ["/github-monitor:poll"],
188
- content: "# GitHub Repository Monitor\n\nCreate a cron automation that polls a single GitHub repository on a\nconfigurable schedule (default: every minute).\n\nWhen a comment on an issue or PR contains the **trigger phrase**\n(default: `@OpenHands`) it:\n\n1. Posts a GitHub comment acknowledging the request with a conversation link.\n2. Creates an OpenHands conversation pre-loaded with the issue/PR title, body,\n labels, and recent comment history for full context.\n3. Posts a summary GitHub comment when the conversation finishes.\n\nOn every subsequent run:\n- New trigger comments on an already-tracked issue/PR are forwarded to the\n running conversation (or re-open a previously closed one).\n- When a conversation goes idle/finished/error the agent's final response\n is posted back as a GitHub comment.\n\n> **Local mode only.** This automation targets the local OpenHands setup\n> (`dev:automation` stack). A cloud/webhook variant is out of scope here.\n\n---\n\n## Prerequisites\n\n### Required secret\n\nVerify that the following secret is set in **OpenHands Settings → Secrets**\nbefore proceeding:\n\n| Secret name | Token type | Minimum permissions |\n|---|---|---|\n| `GITHUB_PERSONAL_ACCESS_TOKEN` | Classic PAT | `repo` (private repos) or `public_repo` (public repos) |\n| `GITHUB_PERSONAL_ACCESS_TOKEN` | Fine-grained PAT | Issues: Read and Write |\n\nCheck with:\n```bash\ncurl -s https://api.github.com/user \\\n -H \"Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN\" \\\n -H \"Accept: application/vnd.github+json\" \\\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, inform the user and stop — the automation cannot\nfunction without GitHub credentials.\n\n### Optional secret\n\n| Secret name | Default | Purpose |\n|---|---|---|\n| `OPENHANDS_URL` | `http://localhost:8000` | Base URL used to build conversation links in GitHub comments |\n\n---\n\n## Setup Workflow\n\nFollow these steps in order.\n\n### Step 1 - Verify GITHUB_PERSONAL_ACCESS_TOKEN\n\nFetch the secret and run the `curl` check above.\n\n- If the secret is absent: tell the user\n *\"GITHUB_PERSONAL_ACCESS_TOKEN is not set. Please add it in OpenHands Settings → Secrets\n (classic PAT with `repo` or `public_repo` scope, or a fine-grained PAT\n with Issues: Read and Write).\"* Then stop.\n\n- If the API returns a non-200 or `{\"message\": \"Bad credentials\"}`:\n tell the user the token is invalid and ask them to update it.\n\n### Step 2 - Collect repository\n\nAsk the user: *\"Which GitHub repository should be monitored?\n(Format: `owner/repo`, e.g. `microsoft/vscode`)\"*\n\nValidate access and write permissions:\n\n```bash\ncurl -s \"https://api.github.com/repos/{owner}/{repo}\" \\\n -H \"Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN\" \\\n -H \"Accept: application/vnd.github+json\" \\\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. Private: {d.get('private')}. Permissions: {perms}\\\")\n\"\n```\n\n- If `message: Not Found` or `message: Bad credentials` →\n inform the user and ask them to check the repo name and token.\n- If the repo is private and `permissions.push` is `false` →\n inform the user the token does not have write access and comments will fail.\n- If the check passes, record `REPO = \"{owner}/{repo}\"`.\n\n### Step 3 - Collect trigger phrase\n\nAsk the user: *\"What trigger phrase should OpenHands respond to?\n(Press Enter to use the default: `@OpenHands`)\"*\n\nAccepted values: any non-empty string unlikely to appear by accident.\n\nRecord as `TRIGGER_PHRASE`. Default: `\"@openhands\"`.\n\n### Step 4 - Collect allowed GitHub logins\n\nAsk the user: *\"Which GitHub users may trigger this automation?\nPress Enter to allow only the authenticated `GITHUB_PERSONAL_ACCESS_TOKEN` owner.\nYou may also provide comma-separated GitHub logins, or `*` to allow any\nnon-bot commenter on the monitored repository.\"*\n\nMap the answer to `ALLOWED_GITHUB_LOGINS`:\n\n| User answer | `ALLOWED_GITHUB_LOGINS` value |\n|---|---|\n| Empty/default | `[\"<TOKEN_OWNER>\"]` |\n| `enyst,tofarr` | `[\"enyst\", \"tofarr\"]` |\n| `*` | `[\"*\"]` |\n\nDefault to token-owner-only unless the user explicitly chooses a broader\nallowlist. Record as `ALLOWED_GITHUB_LOGINS`.\n\n### Step 5 - Collect event types\n\nAsk the user: *\"Which event types should be monitored?\nChoose one or more:*\n *1. Issue and PR comments (default)*\n *2. PR inline review comments*\n *3. Both*\n*(Press Enter to accept the default: issue and PR comments.)\"*\n\nMap the choice to the `EVENT_TYPES` list:\n\n| Choice | `EVENT_TYPES` value |\n|---|---|\n| 1 (default) | `[\"issue_comment\"]` |\n| 2 | `[\"pr_review_comment\"]` |\n| 3 | `[\"issue_comment\", \"pr_review_comment\"]` |\n\n### Step 6 - Collect cron schedule\n\nAsk the user: *\"How often should the automation poll GitHub?\n(Press Enter for the default: every minute.\nUse a cron expression for a different interval, e.g.:\n`*/5 * * * *` = every 5 minutes,\n`0 * * * *` = every hour)\"*\n\nDefault: `* * * * *` (every minute).\n\nRecord as `CRON_SCHEDULE`.\n\n### Step 7 - Generate the automation script\n\nRead `scripts/main.py` from this skill's directory. Apply exactly five\nconstant substitutions near the top of the file:\n\n| Placeholder | Replace with |\n|---|---|\n| `REPO = \"owner/repo\"` | `REPO = \"{owner_repo}\"` |\n| `TRIGGER_PHRASE = \"@openhands\"` | `TRIGGER_PHRASE = \"{trigger_phrase_lower}\"` |\n| `EVENT_TYPES = [\"issue_comment\"]` | `EVENT_TYPES = {event_types_list}` |\n| `ALLOWED_GITHUB_LOGINS = [\"<TOKEN_OWNER>\"]` | `ALLOWED_GITHUB_LOGINS = {allowed_logins_list}` |\n| `DEFAULT_OPENHANDS_URL = \"http://localhost:8000\"` | `DEFAULT_OPENHANDS_URL = \"{url}\"` (keep default if the user has no preference) |\n\nWrite the customised script to a temporary build directory:\n```bash\nmkdir -p /tmp/github-monitor-build\n# (write the customised main.py to /tmp/github-monitor-build/main.py)\n```\n\nValidate syntax before packaging:\n```bash\npython3 -m py_compile /tmp/github-monitor-build/main.py && echo \"Syntax OK\"\n```\n\nFix any syntax errors before proceeding.\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- Use the **Automation backend** `url_from_agent` as `OPENHANDS_HOST`\n- Auth: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`\n\nIf no Automation backend is listed in `<RUNTIME_SERVICES>`, stop and tell\nthe user to start the full automation stack.\n\n```bash\ntar -czf /tmp/github-monitor.tar.gz -C /tmp/github-monitor-build .\n\n# OPENHANDS_HOST: read from <RUNTIME_SERVICES> Automation backend url_from_agent\nOPENHANDS_HOST=\"<automation-url-from-runtime-services>\"\n\nTARBALL_PATH=$(curl -s -X POST \\\n \"${OPENHANDS_HOST}/api/automation/v1/uploads?name=github-repo-monitor\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/gzip\" \\\n --data-binary @/tmp/github-monitor.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 - Create 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 Monitor: {owner}/{repo}\\\",\n \\\"trigger\\\": {\\\"type\\\": \\\"cron\\\", \\\"schedule\\\": \\\"{cron_schedule}\\\"},\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 55\n }\" | python3 -m json.tool\n```\n\nRecord the returned `id`.\n\n### Step 10 - Confirm\n\nTell the user:\n\n> ✅ **GitHub Repository Monitor** is running!\n>\n> - Automation ID: `{id}`\n> - Repository: `{owner}/{repo}`\n> - Trigger phrase: `{phrase}`\n> - Event types: `{event_types}`\n> - Allowed GitHub logins: `{allowed_logins}`\n> - Polling schedule: `{cron_schedule}`\n> - State file: `~/.openhands/workspaces/automation-state/github_poller_{id}.json`\n>\n> From an allowed GitHub login, post a comment containing `{phrase}` on any\n> issue or PR in `{owner}/{repo}` to test it. OpenHands will acknowledge with\n> a comment and a link to the new conversation.\n\n---\n\n## Runtime Behaviour (per poll)\n\nEach cron run executes `main.py`, which:\n\n1. **Loads state** from the JSON file (see `references/state-schema.md`).\n2. **Resolves and validates GITHUB_PERSONAL_ACCESS_TOKEN** — aborts immediately if absent or invalid.\n3. **Polls for new events** since the previous `last_poll` timestamp:\n - `GET /repos/{owner}/{repo}/issues/comments?since=…` for `issue_comment`\n - `GET /repos/{owner}/{repo}/pulls/comments?since=…` for `pr_review_comment`\n4. **Processes matching comments** in chronological order:\n - Skips bot accounts (login ending in `[bot]`) to avoid feedback loops.\n - Skips already-processed comment IDs.\n - Skips comments from logins outside `ALLOWED_GITHUB_LOGINS`.\n - Checks body for the trigger phrase (case-insensitive).\n - Extracts the issue/PR number from the comment URL.\n5. **For each trigger comment**, per issue/PR:\n - **Active conversation** → forwards the new comment directly.\n - **Closed conversation** → tries to re-open it; falls back to creating\n a new conversation if the old one is unreachable.\n - **No conversation** → fetches full context (title, body, labels, last\n 10 comments) and creates a new conversation with a detailed prompt.\n - Posts a GitHub comment: *\"🤖 OpenHands is on it! View progress: {url}\"*\n6. **Checks active conversations** for completion:\n - If `status ∈ {idle, finished, error, stuck}` and enough time has passed\n since creation (debounce), fetches the agent's final response and posts\n it as a GitHub comment. Marks the conversation `closed`.\n7. **Saves state** and fires the completion callback.\n\n---\n\n## Additional Resources\n\n### Reference Files\n\n- **`references/state-schema.md`** - State JSON schema, field definitions,\n and conversation lifecycle diagram.\n- **`references/github-api.md`** - GitHub API endpoint reference, token\n scopes, rate limits, and common error codes.\n\n### Script Template\n\n- **`scripts/main.py`** - The complete automation script. Customise the four\n constants at the top (`REPO`, `TRIGGER_PHRASE`, `EVENT_TYPES`,\n `DEFAULT_OPENHANDS_URL`) before packaging.\n\n---\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---|---|---|\n| Bot doesn't respond to comments | `GITHUB_PERSONAL_ACCESS_TOKEN` missing or wrong scopes | Verify token with `curl /user`; check scopes in Step 1 |\n| \"Bad credentials\" in run logs | Token expired | Rotate token and update the secret in Settings |\n| 404 on repo access | Repo name wrong or token has no access | Re-check `owner/repo` spelling; add token as collaborator |\n| Comments posted but no conversation created | Agent server URL wrong | Check `OPENHANDS_URL` secret and `AGENT_SERVER_URL` env var |\n| Same comment processed twice | `processed_comment_ids` cleared | State file was deleted; harmless but duplicate comment may appear |\n| Summary never posted | Conversation stuck in `running` | Open the conversation in the OpenHands UI; agent may need input |\n| No events detected after first run | `last_poll` in the future | Delete the state file to reset; it will be recreated on next run |"
188
+ content: "# GitHub Repository Monitor\n\nCreate a cron automation that polls a single GitHub repository on a\nconfigurable schedule (default: every minute).\nWindows PowerShell equivalents for the setup, packaging, upload, and API-check shell snippets are in `references/windows.md`.\n\nWhen a comment on an issue or PR contains the **trigger phrase**\n(default: `@OpenHands`) it:\n\n1. Posts a GitHub comment acknowledging the request with a conversation link.\n2. Creates an OpenHands conversation pre-loaded with the issue/PR title, body,\n labels, and recent comment history for full context.\n3. Posts a summary GitHub comment when the conversation finishes.\n\nOn every subsequent run:\n- New trigger comments on an already-tracked issue/PR are forwarded to the\n running conversation (or re-open a previously closed one).\n- When a conversation goes idle/finished/error the agent's final response\n is posted back as a GitHub comment.\n\n> **Local mode only.** This automation targets the local OpenHands setup\n> (`dev:automation` stack). A cloud/webhook variant is out of scope here.\n\n---\n\n## Prerequisites\n\n### Required secret\n\nVerify that the following secret is set in **OpenHands Settings → Secrets**\nbefore proceeding:\n\n| Secret name | Token type | Minimum permissions |\n|---|---|---|\n| `GITHUB_PERSONAL_ACCESS_TOKEN` | Classic PAT | `repo` (private repos) or `public_repo` (public repos) |\n| `GITHUB_PERSONAL_ACCESS_TOKEN` | Fine-grained PAT | Issues: Read and Write |\n\nCheck with:\n```bash\ncurl -s https://api.github.com/user \\\n -H \"Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN\" \\\n -H \"Accept: application/vnd.github+json\" \\\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, inform the user and stop — the automation cannot\nfunction without GitHub credentials.\n\n### Optional secret\n\n| Secret name | Default | Purpose |\n|---|---|---|\n| `OPENHANDS_URL` | `http://localhost:8000` | Base URL used to build conversation links in GitHub comments |\n\n---\n\n## Setup Workflow\n\nFollow these steps in order.\n\n### Step 1 - Verify GITHUB_PERSONAL_ACCESS_TOKEN\n\nFetch the secret and run the `curl` check above.\n\n- If the secret is absent: tell the user\n *\"GITHUB_PERSONAL_ACCESS_TOKEN is not set. Please add it in OpenHands Settings → Secrets\n (classic PAT with `repo` or `public_repo` scope, or a fine-grained PAT\n with Issues: Read and Write).\"* Then stop.\n\n- If the API returns a non-200 or `{\"message\": \"Bad credentials\"}`:\n tell the user the token is invalid and ask them to update it.\n\n### Step 2 - Collect repository\n\nAsk the user: *\"Which GitHub repository should be monitored?\n(Format: `owner/repo`, e.g. `microsoft/vscode`)\"*\n\nValidate access and write permissions:\n\n```bash\ncurl -s \"https://api.github.com/repos/{owner}/{repo}\" \\\n -H \"Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN\" \\\n -H \"Accept: application/vnd.github+json\" \\\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. Private: {d.get('private')}. Permissions: {perms}\\\")\n\"\n```\n\n- If `message: Not Found` or `message: Bad credentials` →\n inform the user and ask them to check the repo name and token.\n- If the repo is private and `permissions.push` is `false` →\n inform the user the token does not have write access and comments will fail.\n- If the check passes, record `REPO = \"{owner}/{repo}\"`.\n\n### Step 3 - Collect trigger phrase\n\nAsk the user: *\"What trigger phrase should OpenHands respond to?\n(Press Enter to use the default: `@OpenHands`)\"*\n\nAccepted values: any non-empty string unlikely to appear by accident.\n\nRecord as `TRIGGER_PHRASE`. Default: `\"@openhands\"`.\n\n### Step 4 - Collect allowed GitHub logins\n\nAsk the user: *\"Which GitHub users may trigger this automation?\nPress Enter to allow only the authenticated `GITHUB_PERSONAL_ACCESS_TOKEN` owner.\nYou may also provide comma-separated GitHub logins, or `*` to allow any\nnon-bot commenter on the monitored repository.\"*\n\nMap the answer to `ALLOWED_GITHUB_LOGINS`:\n\n| User answer | `ALLOWED_GITHUB_LOGINS` value |\n|---|---|\n| Empty/default | `[\"<TOKEN_OWNER>\"]` |\n| `enyst,tofarr` | `[\"enyst\", \"tofarr\"]` |\n| `*` | `[\"*\"]` |\n\nDefault to token-owner-only unless the user explicitly chooses a broader\nallowlist. Record as `ALLOWED_GITHUB_LOGINS`.\n\n### Step 5 - Collect event types\n\nAsk the user: *\"Which event types should be monitored?\nChoose one or more:*\n *1. Issue and PR comments (default)*\n *2. PR inline review comments*\n *3. Both*\n*(Press Enter to accept the default: issue and PR comments.)\"*\n\nMap the choice to the `EVENT_TYPES` list:\n\n| Choice | `EVENT_TYPES` value |\n|---|---|\n| 1 (default) | `[\"issue_comment\"]` |\n| 2 | `[\"pr_review_comment\"]` |\n| 3 | `[\"issue_comment\", \"pr_review_comment\"]` |\n\n### Step 6 - Collect cron schedule\n\nAsk the user: *\"How often should the automation poll GitHub?\n(Press Enter for the default: every minute.\nUse a cron expression for a different interval, e.g.:\n`*/5 * * * *` = every 5 minutes,\n`0 * * * *` = every hour)\"*\n\nDefault: `* * * * *` (every minute).\n\nRecord as `CRON_SCHEDULE`.\n\n### Step 7 - Generate the automation script\n\nRead `scripts/main.py` from this skill's directory. Apply exactly five\nconstant substitutions near the top of the file:\n\n| Placeholder | Replace with |\n|---|---|\n| `REPO = \"owner/repo\"` | `REPO = \"{owner_repo}\"` |\n| `TRIGGER_PHRASE = \"@openhands\"` | `TRIGGER_PHRASE = \"{trigger_phrase_lower}\"` |\n| `EVENT_TYPES = [\"issue_comment\"]` | `EVENT_TYPES = {event_types_list}` |\n| `ALLOWED_GITHUB_LOGINS = [\"<TOKEN_OWNER>\"]` | `ALLOWED_GITHUB_LOGINS = {allowed_logins_list}` |\n| `DEFAULT_OPENHANDS_URL = \"http://localhost:8000\"` | `DEFAULT_OPENHANDS_URL = \"{url}\"` (keep default if the user has no preference) |\n\nWrite the customised script to a temporary build directory:\n```bash\nmkdir -p /tmp/github-monitor-build\n# (write the customised main.py to /tmp/github-monitor-build/main.py)\n```\n\nValidate syntax before packaging:\n```bash\npython3 -m py_compile /tmp/github-monitor-build/main.py && echo \"Syntax OK\"\n```\n\nFix any syntax errors before proceeding.\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- Use the **Automation backend** `url_from_agent` as `OPENHANDS_HOST`\n- Auth: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`\n\nIf no Automation backend is listed in `<RUNTIME_SERVICES>`, stop and tell\nthe user to start the full automation stack.\n\n```bash\ntar -czf /tmp/github-monitor.tar.gz -C /tmp/github-monitor-build .\n\n# OPENHANDS_HOST: read from <RUNTIME_SERVICES> Automation backend url_from_agent\nOPENHANDS_HOST=\"<automation-url-from-runtime-services>\"\n\nTARBALL_PATH=$(curl -s -X POST \\\n \"${OPENHANDS_HOST}/api/automation/v1/uploads?name=github-repo-monitor\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/gzip\" \\\n --data-binary @/tmp/github-monitor.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 - Create 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 Monitor: {owner}/{repo}\\\",\n \\\"trigger\\\": {\\\"type\\\": \\\"cron\\\", \\\"schedule\\\": \\\"{cron_schedule}\\\"},\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 55\n }\" | python3 -m json.tool\n```\n\nRecord the returned `id`.\n\n### Step 10 - Confirm\n\nTell the user:\n\n> ✅ **GitHub Repository Monitor** is running!\n>\n> - Automation ID: `{id}`\n> - Repository: `{owner}/{repo}`\n> - Trigger phrase: `{phrase}`\n> - Event types: `{event_types}`\n> - Allowed GitHub logins: `{allowed_logins}`\n> - Polling schedule: `{cron_schedule}`\n> - State file: `~/.openhands/workspaces/automation-state/github_poller_{id}.json`\n>\n> From an allowed GitHub login, post a comment containing `{phrase}` on any\n> issue or PR in `{owner}/{repo}` to test it. OpenHands will acknowledge with\n> a comment and a link to the new conversation.\n\n---\n\n## Runtime Behaviour (per poll)\n\nEach cron run executes `main.py`, which:\n\n1. **Loads state** from the JSON file (see `references/state-schema.md`).\n2. **Resolves and validates GITHUB_PERSONAL_ACCESS_TOKEN** — aborts immediately if absent or invalid.\n3. **Polls for new events** since the previous `last_poll` timestamp:\n - `GET /repos/{owner}/{repo}/issues/comments?since=…` for `issue_comment`\n - `GET /repos/{owner}/{repo}/pulls/comments?since=…` for `pr_review_comment`\n4. **Processes matching comments** in chronological order:\n - Skips bot accounts (login ending in `[bot]`) to avoid feedback loops.\n - Skips already-processed comment IDs.\n - Skips comments from logins outside `ALLOWED_GITHUB_LOGINS`.\n - Checks body for the trigger phrase (case-insensitive).\n - Extracts the issue/PR number from the comment URL.\n5. **For each trigger comment**, per issue/PR:\n - **Active conversation** → forwards the new comment directly.\n - **Closed conversation** → tries to re-open it; falls back to creating\n a new conversation if the old one is unreachable.\n - **No conversation** → fetches full context (title, body, labels, last\n 10 comments) and creates a new conversation with a detailed prompt.\n - Posts a GitHub comment: *\"🤖 OpenHands is on it! View progress: {url}\"*\n6. **Checks active conversations** for completion:\n - If `status ∈ {idle, finished, error, stuck}` and enough time has passed\n since creation (debounce), fetches the agent's final response and posts\n it as a GitHub comment. Marks the conversation `closed`.\n7. **Saves state** and fires the completion callback.\n\n---\n\n## Additional Resources\n\n### Reference Files\n\n- **`references/state-schema.md`** - State JSON schema, field definitions,\n and conversation lifecycle diagram.\n- **`references/github-api.md`** - GitHub API endpoint reference, token\n scopes, rate limits, and common error codes.\n\n### Script Template\n\n- **`scripts/main.py`** - The complete automation script. Customise the four\n constants at the top (`REPO`, `TRIGGER_PHRASE`, `EVENT_TYPES`,\n `DEFAULT_OPENHANDS_URL`) before packaging.\n\n---\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---|---|---|\n| Bot doesn't respond to comments | `GITHUB_PERSONAL_ACCESS_TOKEN` missing or wrong scopes | Verify token with `curl /user`; check scopes in Step 1 |\n| \"Bad credentials\" in run logs | Token expired | Rotate token and update the secret in Settings |\n| 404 on repo access | Repo name wrong or token has no access | Re-check `owner/repo` spelling; add token as collaborator |\n| Comments posted but no conversation created | Agent server URL wrong | Check `OPENHANDS_URL` secret and `AGENT_SERVER_URL` env var |\n| Same comment processed twice | `processed_comment_ids` cleared | State file was deleted; harmless but duplicate comment may appear |\n| Summary never posted | Conversation stuck in `running` | Open the conversation in the OpenHands UI; agent may need input |\n| No events detected after first run | `last_poll` in the future | Delete the state file to reset; it will be recreated on next run |"
189
189
  },
190
190
  {
191
191
  name: "gitlab",
192
192
  description: "Interact with GitLab repositories, merge requests, and APIs using the GITLAB_TOKEN environment variable. Use when working with code hosted on GitLab or managing GitLab resources.",
193
193
  triggers: ["gitlab", "git"],
194
- content: "You have access to an environment variable, `GITLAB_TOKEN`, which allows you to interact with\nthe GitLab API.\n\n<IMPORTANT>\nYou can use `curl` with the `GITLAB_TOKEN` to interact with GitLab's API.\nALWAYS use the GitLab API for operations instead of a web browser.\nALWAYS use the `create_mr` tool to open a merge request\n</IMPORTANT>\n\nIf you encounter authentication issues when pushing to GitLab (such as password prompts or permission errors), the old token may have expired. In such case, update the remote URL to include the current token: `git remote set-url origin https://oauth2:${GITLAB_TOKEN}@gitlab.com/username/repo.git`\n\nHere are some instructions for pushing, but ONLY do this if the user asks you to:\n* NEVER push directly to the `main` or `master` branch\n* Git config (username and email) is pre-set. Do not modify.\n* You may already be on a branch starting with `openhands-workspace`. Create a new branch with a better name before pushing.\n* Use the `create_mr` tool to create a merge request, if you haven't already\n* Once you've created your own branch or a merge request, continue to update it. Do NOT create a new one unless you are explicitly asked to. Update the PR title and description as necessary, but don't change the branch name.\n* Use the main branch as the base branch, unless the user requests otherwise\n* After opening or updating a merge request, send the user a short message with a link to the merge request.\n* Do all of the above in as few steps as possible. E.g. you could push changes with one step by running the following bash commands:\n```bash\ngit remote -v && git branch # to find the current org, repo and branch\ngit checkout -b create-widget && git add . && git commit -m \"Create widget\" && git push -u origin create-widget\n```"
194
+ content: "You have access to an environment variable, `GITLAB_TOKEN`, which allows you to interact with\nthe GitLab API.\n\n<IMPORTANT>\nYou can use `curl` with the `GITLAB_TOKEN` to interact with GitLab's API.\nALWAYS use the GitLab API for operations instead of a web browser.\nALWAYS use the `create_mr` tool to open a merge request\n</IMPORTANT>\n\nIf you encounter authentication issues when pushing to GitLab (such as password prompts or permission errors), the old token may have expired. In such case, update the remote URL to include the current token: `git remote set-url origin https://oauth2:${GITLAB_TOKEN}@gitlab.com/username/repo.git`\n\nHere are some instructions for pushing, but ONLY do this if the user asks you to:\n* NEVER push directly to the `main` or `master` branch\n* Git config (username and email) is pre-set. Do not modify.\n* You may already be on a branch starting with `openhands-workspace`. Create a new branch with a better name before pushing.\n* Use the `create_mr` tool to create a merge request, if you haven't already\n* Once you've created your own branch or a merge request, continue to update it. Do NOT create a new one unless you are explicitly asked to. Update the PR title and description as necessary, but don't change the branch name.\n* Use the main branch as the base branch, unless the user requests otherwise\n* After opening or updating a merge request, send the user a short message with a link to the merge request.\n* Do all of the above in as few steps as possible. E.g. you could push changes with one step by running the following bash commands:\n```bash\ngit remote -v && git branch # to find the current org, repo and branch\ngit checkout -b create-widget && git add . && git commit -m \"Create widget\" && git push -u origin create-widget\n```\n\nOn Windows PowerShell, use `$env:GITLAB_TOKEN` in remote URLs and run the `git` commands as separate commands if `&&` is not supported by the installed shell."
195
195
  },
196
196
  {
197
197
  name: "incident-retrospective",
198
198
  description: "Create an automation that drafts incident retrospectives. Gathers incident-channel messages from Slack, collects linked tickets and follow-ups from Linear, and publishes a retrospective draft to Notion with a timeline, impact summary, root-cause hypotheses, and action items.",
199
199
  triggers: ["/incident-retro:setup"],
200
- content: "# Incident Retrospective Drafter Automation\n\nSet up an automation that drafts incident retrospectives by pulling data from\nSlack, Linear, and Notion.\n\n---\n\n## Prerequisites\n\n### Required integrations\n\nAll three MCP integrations must be installed in Settings → MCP:\n\n- **Slack MCP** — to gather incident-channel messages\n- **Linear MCP** — to collect linked tickets and follow-ups\n- **Notion MCP** — to publish the retrospective draft\n\n### Information to collect\n\nAsk the user for:\n\n1. **Incident identification** — how are incidents identified? (e.g. Slack channel naming convention like `#inc-*`, a Linear label, or manual trigger)\n2. **Slack channels** — which channels contain incident chatter (e.g. `#incidents`, `#inc-*` pattern)\n3. **Linear teams** — which Linear teams/projects to inspect for follow-up tickets\n4. **Retrospective template** — what sections should the retro include? Default: Timeline, Impact, Root Cause, Action Items, Lessons Learned\n5. **Notion destination** — which Notion database or page should receive the draft\n6. **Trigger type** — manual dispatch, cron schedule, or triggered by an incident label being added\n\n---\n\n## Setup Workflow\n\n### Step 1 — Verify MCP access\n\nTest each integration:\n```\nUse the Slack MCP to list recent messages in an incident channel.\nUse the Linear MCP to list recent issues for the target team.\nUse the Notion MCP to search for the destination database.\n```\n\nIf any fail, tell the user which integration needs to be installed first.\n\n### Step 2 — Determine trigger type\n\nAsk the user how retros should be triggered:\n- **Manual** — dispatch from the automations page when an incident wraps up\n- **Cron** — run daily/weekly to check for recent incidents\n- **Event** — triggered by a Linear label change or Slack message\n\n### Step 3 — Build the retro prompt\n\nConstruct a prompt that includes:\n- How to identify the incident (channel pattern, label, etc.)\n- Which Slack channels and Linear teams to query\n- The retrospective template/sections\n- Where to publish in Notion\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\": \"Incident Retrospective Drafter\",\n \"prompt\": \"<constructed retro prompt>\",\n \"trigger\": <trigger config from step 2>\n }'\n```\n\n### Step 5 — Confirm\n\nTell the user:\n> ✅ **Incident Retrospective Drafter** is running!\n>\n> - Automation ID: `{id}`\n> - Incident source: `{identification method}`\n> - Slack channels: `{channels}`\n> - Linear teams: `{teams}`\n> - Notion destination: `{destination}`\n> - Trigger: `{trigger description}`"
200
+ content: "# Incident Retrospective Drafter Automation\n\nSet up an automation that drafts incident retrospectives by pulling data from\nSlack, Linear, and Notion.\n\n---\n\n## Prerequisites\n\n### Required integrations\n\nAll three MCP integrations must be installed in Settings → MCP:\n\n- **Slack MCP** — to gather incident-channel messages\n- **Linear MCP** — to collect linked tickets and follow-ups\n- **Notion MCP** — to publish the retrospective draft\n\n### Information to collect\n\nAsk the user for:\n\n1. **Incident identification** — how are incidents identified? (e.g. Slack channel naming convention like `#inc-*`, a Linear label, or manual trigger)\n2. **Slack channels** — which channels contain incident chatter (e.g. `#incidents`, `#inc-*` pattern)\n3. **Linear teams** — which Linear teams/projects to inspect for follow-up tickets\n4. **Retrospective template** — what sections should the retro include? Default: Timeline, Impact, Root Cause, Action Items, Lessons Learned\n5. **Notion destination** — which Notion database or page should receive the draft\n6. **Trigger type** — manual dispatch, cron schedule, or triggered by an incident label being added\n\n---\n\n## Setup Workflow\n\n### Step 1 — Verify MCP access\n\nTest each integration:\n```\nUse the Slack MCP to list recent messages in an incident channel.\nUse the Linear MCP to list recent issues for the target team.\nUse the Notion MCP to search for the destination database.\n```\n\nIf any fail, tell the user which integration needs to be installed first.\n\n### Step 2 — Determine trigger type\n\nAsk the user how retros should be triggered:\n- **Manual** — dispatch from the automations page when an incident wraps up\n- **Cron** — run daily/weekly to check for recent incidents\n- **Event** — triggered by a Linear label change or Slack message\n\n### Step 3 — Build the retro prompt\n\nConstruct a prompt that includes:\n- How to identify the incident (channel pattern, label, etc.)\n- Which Slack channels and Linear teams to query\n- The retrospective template/sections\n- Where to publish in Notion\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\": \"Incident Retrospective Drafter\",\n \"prompt\": \"<constructed retro 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> ✅ **Incident Retrospective Drafter** is running!\n>\n> - Automation ID: `{id}`\n> - Incident source: `{identification method}`\n> - Slack channels: `{channels}`\n> - Linear teams: `{teams}`\n> - Notion destination: `{destination}`\n> - Trigger: `{trigger description}`"
201
201
  },
202
202
  {
203
203
  name: "iterate",
@@ -207,13 +207,19 @@ var e = [
207
207
  "/verify",
208
208
  "/babysit"
209
209
  ],
210
- content: "# /iterate — Drive a PR to Merge-Ready\n\nIterate on a pull request until it passes all verification layers.\nYou push, poll, fix, and push again — the loop only ends when the PR is green\nor a blocker requires human help.\n\nNo scripts — you are the orchestration loop. Uses only standard `gh` CLI\ncommands that work on any GitHub repo.\n\nRequires: `gh` CLI authenticated with repo access, a PR branch.\n\n## Discover what the repo has\n\nNot every repo has all three verification layers. Before entering the loop,\ncheck which ones exist. Only poll layers that are actually set up.\n\n```bash\ngh workflow list --json name --jq '.[].name'\n```\n\n- **CI checks** — almost every repo has these. If `gh pr checks` returns results, CI is present.\n- **PR review bot** — look for a workflow named like \"PR Review\" or \"pr-review\" in the output above, or check for `.github/workflows/pr-review*.yml` in the repo. If it's not there, the repo doesn't have automated PR review. Skip step 3 entirely.\n- **QA bot** — look for a workflow named like \"QA\" or \"qa-changes\". If it's not there, the repo doesn't have automated QA. Skip step 4 entirely.\n\nA repo might have only CI. Or CI + review. Or all three. Your \"all passed\"\ncondition is: every *present* layer is green. Don't block waiting for layers\nthat don't exist.\n\n## The loop\n\n1. Push and ensure a draft PR exists.\n2. Poll each present verification layer.\n3. Decide: all passed? fix needed? wait?\n4. If fix needed — fix, commit, push, re-request review from bots, go to 2.\n5. If waiting — sleep per polling cadence, go to 2.\n6. If all present layers passed on the *current* SHA — mark PR ready, done.\n\nIMPORTANT: pushing a fix is NOT the end. After every fix+push you MUST\nre-request review from the review bot (if present) and go back to step 2.\nThe loop only ends when the verifiers pass on your latest SHA. Addressing\nfeedback and pushing a commit is just one iteration — the bot needs to\nreview the new code too.\n\nDo not stop to ask the user whether to continue polling; continue\nautonomously until a strict stop condition is met or the user interrupts.\n\n## Step 1 — Push and ensure PR exists (as draft)\n\nCreate the PR as a draft. This prevents repo automations (merge workflows,\nartifact cleanup, auto-merge) from triggering while you're still iterating.\nYou mark it ready only after all verification layers pass.\n\n```bash\ngit push origin HEAD\ngh pr create --fill --draft 2>/dev/null || true\ngh pr view --json number,url,headRefOid,isDraft --jq '\"\\(.number) \\(.url) \\(.headRefOid) draft=\\(.isDraft)\"'\n```\n\nIf the PR already exists and is not a draft, convert it:\n\n```bash\ngh pr ready --undo\n```\n\n## Step 2 — Poll CI checks\n\n```bash\ngh pr checks --json name,state,bucket --jq '\n { passed: [.[] | select(.bucket==\"pass\")] | length,\n failed: [.[] | select(.bucket==\"fail\")] | length,\n pending: [.[] | select(.bucket==\"pending\")] | length }'\n```\n\n- Zero failed, zero pending → CI green.\n- Any pending → wait and re-poll.\n- Any failed → diagnose (see \"CI failure classification\" below).\n\nTo inspect a failure:\n\n```bash\nSHA=$(gh pr view --json headRefOid --jq .headRefOid)\ngh run list --commit \"$SHA\" --status failure --json databaseId,name,conclusion \\\n --jq '.[] | \"\\(.databaseId)\\t\\(.name)\\t\\(.conclusion)\"'\ngh run view <run-id> --log-failed\n```\n\n## Step 3 — Poll PR review (if present)\n\nSkip this step if the repo has no review bot.\n\n```bash\ngh pr view --json reviews --jq '\n [.reviews[] | select(\n .authorAssociation == \"OWNER\" or\n .authorAssociation == \"MEMBER\" or\n .authorAssociation == \"COLLABORATOR\" or\n (.author.login | test(\"openhands|all-hands-bot\"; \"i\"))\n )] | last | { state: .state, reviewer: .author.login, body: .body[0:300] }'\n```\n\n- `APPROVED` → review passed.\n- `CHANGES_REQUESTED` → read the body and inline comments, fix code.\n- `COMMENTED` → may have actionable suggestions; read and decide.\n- No matching review yet → bot may still be running; wait and re-poll.\n\nInline review comments (when changes requested):\n\n```bash\ngh api \"repos/{owner}/{repo}/pulls/{number}/comments\" \\\n --jq '.[] | select(.user.login | test(\"openhands|all-hands-bot\"; \"i\"))\n | { path: .path, line: .line, body: .body[0:200] }'\n```\n\nOn a fresh iteration, existing pending review feedback should be checked\nimmediately — not only comments that arrive after monitoring starts.\nAlready-open review comments must not be missed.\n\n## Step 4 — Poll QA report (if present)\n\nSkip this step if the repo has no QA bot.\n\nQA reports are PR issue comments with a status line like `Status: PASS`.\n\n```bash\ngh api \"repos/{owner}/{repo}/issues/{number}/comments\" --paginate \\\n --jq '[.[] | select(\n (.user.login | test(\"openhands|all-hands-bot\"; \"i\")) and\n (.body | test(\"Status:\\\\s*(PASS|FAIL|PARTIAL)\"; \"i\"))\n )] | last | { author: .user.login, body: .body[0:500], url: .html_url }'\n```\n\n- `PASS` → QA passed.\n- `FAIL` → read details, fix code.\n- `PARTIAL` → some passed, some failed; read details.\n- No QA comment yet → bot may still be running; wait and re-poll.\n\n## Step 5 — Decide and act\n\nFor each present layer, check its status. If a layer is not present in the\nrepo, treat it as passing.\n\n- All present layers green on current SHA → done.\n- CI failed → fix code, or rerun if flaky (see below).\n- Review requested changes → read comments, fix, push.\n- QA failed/partial → read report, fix, push.\n- Anything still pending → sleep per polling cadence, re-poll.\n- PR closed/merged → stop.\n\n**Priority rule:** when both review feedback and flaky CI failures are present,\nprioritize review feedback first. A new commit will retrigger CI, so avoid\nrerunning flaky checks on the old SHA when you're about to push a review fix.\n\nAfter fixing, commit, push, AND re-request review:\n\n```bash\ngit add -A\ngit commit -m \"fix: address <CI failure | review feedback | QA failure>\"\ngit push origin HEAD\n\n# Re-request review from the bot so it reviews the new SHA:\ngh pr comment --body \"Addressed feedback in $(git rev-parse --short HEAD). Ready for another look.\"\ngh api -X POST \"repos/{owner}/{repo}/pulls/{number}/requested_reviewers\" \\\n -f 'reviewers[]=all-hands-bot'\n```\n\nThen go back to step 2. You are not done until the bot reviews the new\nSHA and all present layers pass.\n\n## CI failure classification\n\nUse `gh` commands to inspect failed runs before deciding to rerun:\n\n```bash\ngh run view <run-id> --json jobs,name,workflowName,conclusion,status,url,headSha\ngh run view <run-id> --log-failed\n```\n\n**Branch-related** (fix the code):\n- Compile/lint/typecheck failures in files you touched\n- Deterministic test failures in changed areas\n- Snapshot or static-analysis violations from your changes\n- Build config changes causing deterministic failures\n\n**Flaky / unrelated** (rerun the jobs):\n- Network/DNS/registry timeouts\n- Runner provisioning or startup failures\n- GitHub Actions infrastructure errors\n- Non-deterministic failures in code you didn't touch\n- Cloud/service rate limits or transient API outages\n\nIf classification is ambiguous, perform one manual diagnosis attempt (inspect\nlogs) before choosing rerun.\n\nRerun: `gh run rerun <run-id> --failed`\n\nRetry budget: at most 3 reruns per SHA. After that, treat as real.\n\nRead `references/heuristics.md` for a concise decision tree.\n\n## Review comment handling\n\nThe review polling in Step 3 surfaces feedback from trusted sources: human\nreviewers (OWNER/MEMBER/COLLABORATOR) and approved review bots (openhands,\nall-hands-bot, etc.). Ignore unrelated bot noise.\n\nReview items come from:\n- PR issue comments\n- Inline review comments\n- Review submissions (COMMENT / APPROVED / CHANGES_REQUESTED)\n\nWhen a comment is actionable and correct:\n1. Fix the code.\n2. Commit with `chore: address PR review feedback (#<n>)`.\n3. Push and continue the loop.\n4. Reply to the review thread referencing the commit SHA.\n5. Resolve the thread.\n\nWhen a comment is non-actionable, already addressed, or you disagree:\nreply briefly explaining why, then resolve the thread. Do not leave\nthreads dangling without a response.\n\nIf a review thread is already resolved in GitHub, ignore it unless new\nunresolved follow-up appears.\n\n### Replying to and resolving review threads\n\nEvery inline review comment creates a thread. After addressing a comment\n(or deciding it's non-actionable), you must:\n\n1. **Reply** to the thread so the reviewer can see how you addressed it:\n\n ```bash\n gh api \"repos/{owner}/{repo}/pulls/{number}/comments\" \\\n -F \"body=Fixed — <describe what you changed>\" \\\n -F \"in_reply_to=<comment_database_id>\"\n ```\n\n Use `-F` (not `-f`) for `in_reply_to` so it is sent as a number.\n\n2. **Resolve** the thread via GraphQL:\n\n ```bash\n gh api graphql \\\n -f query='mutation($id: ID!) {\n resolveReviewThread(input: { threadId: $id }) {\n thread { isResolved }\n }\n }' \\\n -f id=\"<thread_node_id>\"\n ```\n\nTo discover unresolved threads and their IDs:\n\n```bash\ngh api graphql -f query='\nquery($owner: String!, $repo: String!, $pr: Int!) {\n repository(owner: $owner, name: $repo) {\n pullRequest(number: $pr) {\n reviewThreads(last: 100) {\n nodes {\n id\n isResolved\n path\n line\n comments(first: 1) {\n nodes { databaseId author { login } body }\n }\n }\n }\n }\n }\n}' -f owner=\"{owner}\" -f repo=\"{repo}\" -F pr=\"{number}\" \\\n --jq '.data.repository.pullRequest.reviewThreads.nodes[]\n | select(.isResolved == false)'\n```\n\n**Rules:**\n- Reply to every thread, even nits. A brief \"Done\" or \"Kept as-is because…\" is fine.\n- Resolve threads you have addressed. Do not leave resolved-in-code threads\n showing as unresolved in the GitHub UI.\n- Before marking the PR ready, verify zero unresolved threads remain.\n\n### Requesting re-review\n\nIf the PR is green but blocked on review approval and you've addressed all\nfeedback, you can request another look — but only when the user explicitly\nasks, or after confirming with them (avoid spamming humans):\n\n1. Leave a brief PR comment summarizing what changed:\n ```bash\n gh pr comment <pr> --body \"Addressed the requested changes in <sha>. Could you take another look?\"\n ```\n Do NOT tag humans.\n\n2. Re-request reviewers via the GitHub API:\n ```bash\n gh api -X POST repos/{owner}/{repo}/pulls/{number}/requested_reviewers \\\n -f reviewers[]=<reviewer>\n ```\n\nPrefer requesting review only once per new head SHA. If the API returns an\nerror indicating reviewers are already requested, treat it as non-fatal.\n\n## Polling cadence\n\n- CI pending or failing: every 30–60 seconds.\n- CI green, waiting for review/QA: start at 60s, back off exponentially\n (60s → 2m → 4m → 8m → 16m → 32m), cap at 1 hour.\n- Reset to 60s whenever anything changes (new SHA, check status, review\n comment, mergeability change).\n- If CI stops being green (new commit, rerun, regression): return to 30–60s.\n- After pushing a fix: re-poll immediately.\n- If any poll shows the PR is merged or closed: stop immediately.\n\n## Stop conditions\n\nStop **only** when:\n- All present verification layers passed on current SHA and PR is mergeable.\n- PR merged or closed (stop as soon as a poll confirms this).\n- Flaky retry budget exhausted (3 reruns per SHA).\n- Blocked on something requiring human input (infra outage, permissions,\n ambiguity that cannot be resolved safely).\n\n**Not** a stop condition:\n- You pushed a fix. That's one iteration — keep going.\n- You addressed review comments. The bot still needs to review new code.\n- CI is green but review bot hasn't re-reviewed yet. Wait.\n- CI is still running/queued.\n- CI is green but mergeability is unknown/pending.\n- CI is green and mergeable, but waiting for possible new review comments\n per the green-state cadence.\n- PR is green but blocked on review approval (`REVIEW_REQUIRED`); continue\n polling and surface new review comments without asking for confirmation.\n\n## When done — mark PR ready\n\nOnce all present verification layers pass on the current SHA:\n\n1. Verify all review threads are resolved (zero unresolved remaining).\n2. Convert the draft PR to ready for review:\n\n```bash\ngh pr ready\n```\n\nOnly do this at the very end, after the loop exits successfully.\n\n## Git safety\n\n- Work only on the PR head branch.\n- No destructive git commands.\n- Do not switch branches unless necessary to recover context.\n- Check for unrelated uncommitted changes before editing. If present, ask user.\n- After every fix, commit and push, then re-poll.\n- A push is not a terminal outcome; continue the monitoring loop.\n\nCommit message defaults:\n- `fix: CI failure on PR #<n>`\n- `chore: address PR review feedback (#<n>)`\n\n## Output\n\nProvide concise progress updates during monitoring:\n\n- During long unchanged periods, avoid emitting a full update on every poll;\n summarize only status changes plus occasional heartbeat updates.\n- Treat push confirmations, intermediate CI snapshots, and review-action\n updates as progress updates only; do not emit the final summary unless a\n strict stop condition is met.\n- When CI first transitions to all green for the current SHA, emit a one-time\n celebratory update. Preferred style:\n `🚀 CI is all green! 33/33 passed. Still watching for review.`\n\nFinal summary should include:\n- Final PR SHA\n- CI status summary\n- Mergeability / conflict status\n- Fixes pushed\n- Flaky retry cycles used\n- Review threads resolved (count)\n- Remaining unresolved failures or review comments\n\n## References\n\n- Verification stack (layers, signals, retriggering): `references/verification.md`\n- CI/review heuristics and decision tree: `references/heuristics.md`"
210
+ content: "# /iterate — Drive a PR to Merge-Ready\n\nIterate on a pull request until it passes all verification layers.\nYou push, poll, fix, and push again — the loop only ends when the PR is green\nor a blocker requires human help.\n\nNo scripts — you are the orchestration loop. Uses only standard `gh` CLI\ncommands that work on any GitHub repo.\n\nRequires: `gh` CLI authenticated with repo access, a PR branch.\nWindows PowerShell equivalents for Bash-only assignment, redirection, and quoting patterns in this skill are in `references/windows.md`.\n\n## Discover what the repo has\n\nNot every repo has all three verification layers. Before entering the loop,\ncheck which ones exist. Only poll layers that are actually set up.\n\n```bash\ngh workflow list --json name --jq '.[].name'\n```\n\n- **CI checks** — almost every repo has these. If `gh pr checks` returns results, CI is present.\n- **PR review bot** — look for a workflow named like \"PR Review\" or \"pr-review\" in the output above, or check for `.github/workflows/pr-review*.yml` in the repo. If it's not there, the repo doesn't have automated PR review. Skip step 3 entirely.\n- **QA bot** — look for a workflow named like \"QA\" or \"qa-changes\". If it's not there, the repo doesn't have automated QA. Skip step 4 entirely.\n\nA repo might have only CI. Or CI + review. Or all three. Your \"all passed\"\ncondition is: every *present* layer is green. Don't block waiting for layers\nthat don't exist.\n\n## The loop\n\n1. Push and ensure a draft PR exists.\n2. Poll each present verification layer.\n3. Decide: all passed? fix needed? wait?\n4. If fix needed — fix, commit, push, re-request review from bots, go to 2.\n5. If waiting — sleep per polling cadence, go to 2.\n6. If all present layers passed on the *current* SHA — mark PR ready, done.\n\nIMPORTANT: pushing a fix is NOT the end. After every fix+push you MUST\nre-request review from the review bot (if present) and go back to step 2.\nThe loop only ends when the verifiers pass on your latest SHA. Addressing\nfeedback and pushing a commit is just one iteration — the bot needs to\nreview the new code too.\n\nDo not stop to ask the user whether to continue polling; continue\nautonomously until a strict stop condition is met or the user interrupts.\n\n## Step 1 — Push and ensure PR exists (as draft)\n\nCreate the PR as a draft. This prevents repo automations (merge workflows,\nartifact cleanup, auto-merge) from triggering while you're still iterating.\nYou mark it ready only after all verification layers pass.\n\n```bash\ngit push origin HEAD\ngh pr create --fill --draft 2>/dev/null || true\ngh pr view --json number,url,headRefOid,isDraft --jq '\"\\(.number) \\(.url) \\(.headRefOid) draft=\\(.isDraft)\"'\n```\n\nIf the PR already exists and is not a draft, convert it:\n\n```bash\ngh pr ready --undo\n```\n\n## Step 2 — Poll CI checks\n\n```bash\ngh pr checks --json name,state,bucket --jq '\n { passed: [.[] | select(.bucket==\"pass\")] | length,\n failed: [.[] | select(.bucket==\"fail\")] | length,\n pending: [.[] | select(.bucket==\"pending\")] | length }'\n```\n\n- Zero failed, zero pending → CI green.\n- Any pending → wait and re-poll.\n- Any failed → diagnose (see \"CI failure classification\" below).\n\nTo inspect a failure:\n\n```bash\nSHA=$(gh pr view --json headRefOid --jq .headRefOid)\ngh run list --commit \"$SHA\" --status failure --json databaseId,name,conclusion \\\n --jq '.[] | \"\\(.databaseId)\\t\\(.name)\\t\\(.conclusion)\"'\ngh run view <run-id> --log-failed\n```\n\n## Step 3 — Poll PR review (if present)\n\nSkip this step if the repo has no review bot.\n\n```bash\ngh pr view --json reviews --jq '\n [.reviews[] | select(\n .authorAssociation == \"OWNER\" or\n .authorAssociation == \"MEMBER\" or\n .authorAssociation == \"COLLABORATOR\" or\n (.author.login | test(\"openhands|all-hands-bot\"; \"i\"))\n )] | last | { state: .state, reviewer: .author.login, body: .body[0:300] }'\n```\n\n- `APPROVED` → review passed.\n- `CHANGES_REQUESTED` → read the body and inline comments, fix code.\n- `COMMENTED` → may have actionable suggestions; read and decide.\n- No matching review yet → bot may still be running; wait and re-poll.\n\nInline review comments (when changes requested):\n\n```bash\ngh api \"repos/{owner}/{repo}/pulls/{number}/comments\" \\\n --jq '.[] | select(.user.login | test(\"openhands|all-hands-bot\"; \"i\"))\n | { path: .path, line: .line, body: .body[0:200] }'\n```\n\nOn a fresh iteration, existing pending review feedback should be checked\nimmediately — not only comments that arrive after monitoring starts.\nAlready-open review comments must not be missed.\n\n## Step 4 — Poll QA report (if present)\n\nSkip this step if the repo has no QA bot.\n\nQA reports are PR issue comments with a status line like `Status: PASS`.\n\n```bash\ngh api \"repos/{owner}/{repo}/issues/{number}/comments\" --paginate \\\n --jq '[.[] | select(\n (.user.login | test(\"openhands|all-hands-bot\"; \"i\")) and\n (.body | test(\"Status:\\\\s*(PASS|FAIL|PARTIAL)\"; \"i\"))\n )] | last | { author: .user.login, body: .body[0:500], url: .html_url }'\n```\n\n- `PASS` → QA passed.\n- `FAIL` → read details, fix code.\n- `PARTIAL` → some passed, some failed; read details.\n- No QA comment yet → bot may still be running; wait and re-poll.\n\n## Step 5 — Decide and act\n\nFor each present layer, check its status. If a layer is not present in the\nrepo, treat it as passing.\n\n- All present layers green on current SHA → done.\n- CI failed → fix code, or rerun if flaky (see below).\n- Review requested changes → read comments, fix, push.\n- QA failed/partial → read report, fix, push.\n- Anything still pending → sleep per polling cadence, re-poll.\n- PR closed/merged → stop.\n\n**Priority rule:** when both review feedback and flaky CI failures are present,\nprioritize review feedback first. A new commit will retrigger CI, so avoid\nrerunning flaky checks on the old SHA when you're about to push a review fix.\n\nAfter fixing, commit, push, AND re-request review:\n\n```bash\ngit add -A\ngit commit -m \"fix: address <CI failure | review feedback | QA failure>\"\ngit push origin HEAD\n\n# Re-request review from the bot so it reviews the new SHA:\ngh pr comment --body \"Addressed feedback in $(git rev-parse --short HEAD). Ready for another look.\"\ngh api -X POST \"repos/{owner}/{repo}/pulls/{number}/requested_reviewers\" \\\n -f 'reviewers[]=all-hands-bot'\n```\n\nThen go back to step 2. You are not done until the bot reviews the new\nSHA and all present layers pass.\n\n## CI failure classification\n\nUse `gh` commands to inspect failed runs before deciding to rerun:\n\n```bash\ngh run view <run-id> --json jobs,name,workflowName,conclusion,status,url,headSha\ngh run view <run-id> --log-failed\n```\n\n**Branch-related** (fix the code):\n- Compile/lint/typecheck failures in files you touched\n- Deterministic test failures in changed areas\n- Snapshot or static-analysis violations from your changes\n- Build config changes causing deterministic failures\n\n**Flaky / unrelated** (rerun the jobs):\n- Network/DNS/registry timeouts\n- Runner provisioning or startup failures\n- GitHub Actions infrastructure errors\n- Non-deterministic failures in code you didn't touch\n- Cloud/service rate limits or transient API outages\n\nIf classification is ambiguous, perform one manual diagnosis attempt (inspect\nlogs) before choosing rerun.\n\nRerun: `gh run rerun <run-id> --failed`\n\nRetry budget: at most 3 reruns per SHA. After that, treat as real.\n\nRead `references/heuristics.md` for a concise decision tree.\n\n## Review comment handling\n\nThe review polling in Step 3 surfaces feedback from trusted sources: human\nreviewers (OWNER/MEMBER/COLLABORATOR) and approved review bots (openhands,\nall-hands-bot, etc.). Ignore unrelated bot noise.\n\nReview items come from:\n- PR issue comments\n- Inline review comments\n- Review submissions (COMMENT / APPROVED / CHANGES_REQUESTED)\n\nWhen a comment is actionable and correct:\n1. Fix the code.\n2. Commit with `chore: address PR review feedback (#<n>)`.\n3. Push and continue the loop.\n4. Reply to the review thread referencing the commit SHA.\n5. Resolve the thread.\n\nWhen a comment is non-actionable, already addressed, or you disagree:\nreply briefly explaining why, then resolve the thread. Do not leave\nthreads dangling without a response.\n\nIf a review thread is already resolved in GitHub, ignore it unless new\nunresolved follow-up appears.\n\n### Replying to and resolving review threads\n\nEvery inline review comment creates a thread. After addressing a comment\n(or deciding it's non-actionable), you must:\n\n1. **Reply** to the thread so the reviewer can see how you addressed it:\n\n ```bash\n gh api \"repos/{owner}/{repo}/pulls/{number}/comments\" \\\n -F \"body=Fixed — <describe what you changed>\" \\\n -F \"in_reply_to=<comment_database_id>\"\n ```\n\n Use `-F` (not `-f`) for `in_reply_to` so it is sent as a number.\n\n2. **Resolve** the thread via GraphQL:\n\n ```bash\n gh api graphql \\\n -f query='mutation($id: ID!) {\n resolveReviewThread(input: { threadId: $id }) {\n thread { isResolved }\n }\n }' \\\n -f id=\"<thread_node_id>\"\n ```\n\nTo discover unresolved threads and their IDs:\n\n```bash\ngh api graphql -f query='\nquery($owner: String!, $repo: String!, $pr: Int!) {\n repository(owner: $owner, name: $repo) {\n pullRequest(number: $pr) {\n reviewThreads(last: 100) {\n nodes {\n id\n isResolved\n path\n line\n comments(first: 1) {\n nodes { databaseId author { login } body }\n }\n }\n }\n }\n }\n}' -f owner=\"{owner}\" -f repo=\"{repo}\" -F pr=\"{number}\" \\\n --jq '.data.repository.pullRequest.reviewThreads.nodes[]\n | select(.isResolved == false)'\n```\n\n**Rules:**\n- Reply to every thread, even nits. A brief \"Done\" or \"Kept as-is because…\" is fine.\n- Resolve threads you have addressed. Do not leave resolved-in-code threads\n showing as unresolved in the GitHub UI.\n- Before marking the PR ready, verify zero unresolved threads remain.\n\n### Requesting re-review\n\nIf the PR is green but blocked on review approval and you've addressed all\nfeedback, you can request another look — but only when the user explicitly\nasks, or after confirming with them (avoid spamming humans):\n\n1. Leave a brief PR comment summarizing what changed:\n ```bash\n gh pr comment <pr> --body \"Addressed the requested changes in <sha>. Could you take another look?\"\n ```\n Do NOT tag humans.\n\n2. Re-request reviewers via the GitHub API:\n ```bash\n gh api -X POST repos/{owner}/{repo}/pulls/{number}/requested_reviewers \\\n -f reviewers[]=<reviewer>\n ```\n\nPrefer requesting review only once per new head SHA. If the API returns an\nerror indicating reviewers are already requested, treat it as non-fatal.\n\n## Polling cadence\n\n- CI pending or failing: every 30–60 seconds.\n- CI green, waiting for review/QA: start at 60s, back off exponentially\n (60s → 2m → 4m → 8m → 16m → 32m), cap at 1 hour.\n- Reset to 60s whenever anything changes (new SHA, check status, review\n comment, mergeability change).\n- If CI stops being green (new commit, rerun, regression): return to 30–60s.\n- After pushing a fix: re-poll immediately.\n- If any poll shows the PR is merged or closed: stop immediately.\n\n## Stop conditions\n\nStop **only** when:\n- All present verification layers passed on current SHA and PR is mergeable.\n- PR merged or closed (stop as soon as a poll confirms this).\n- Flaky retry budget exhausted (3 reruns per SHA).\n- Blocked on something requiring human input (infra outage, permissions,\n ambiguity that cannot be resolved safely).\n\n**Not** a stop condition:\n- You pushed a fix. That's one iteration — keep going.\n- You addressed review comments. The bot still needs to review new code.\n- CI is green but review bot hasn't re-reviewed yet. Wait.\n- CI is still running/queued.\n- CI is green but mergeability is unknown/pending.\n- CI is green and mergeable, but waiting for possible new review comments\n per the green-state cadence.\n- PR is green but blocked on review approval (`REVIEW_REQUIRED`); continue\n polling and surface new review comments without asking for confirmation.\n\n## When done — mark PR ready\n\nOnce all present verification layers pass on the current SHA:\n\n1. Verify all review threads are resolved (zero unresolved remaining).\n2. Convert the draft PR to ready for review:\n\n```bash\ngh pr ready\n```\n\nOnly do this at the very end, after the loop exits successfully.\n\n## Git safety\n\n- Work only on the PR head branch.\n- No destructive git commands.\n- Do not switch branches unless necessary to recover context.\n- Check for unrelated uncommitted changes before editing. If present, ask user.\n- After every fix, commit and push, then re-poll.\n- A push is not a terminal outcome; continue the monitoring loop.\n\nCommit message defaults:\n- `fix: CI failure on PR #<n>`\n- `chore: address PR review feedback (#<n>)`\n\n## Output\n\nProvide concise progress updates during monitoring:\n\n- During long unchanged periods, avoid emitting a full update on every poll;\n summarize only status changes plus occasional heartbeat updates.\n- Treat push confirmations, intermediate CI snapshots, and review-action\n updates as progress updates only; do not emit the final summary unless a\n strict stop condition is met.\n- When CI first transitions to all green for the current SHA, emit a one-time\n celebratory update. Preferred style:\n `🚀 CI is all green! 33/33 passed. Still watching for review.`\n\nFinal summary should include:\n- Final PR SHA\n- CI status summary\n- Mergeability / conflict status\n- Fixes pushed\n- Flaky retry cycles used\n- Review threads resolved (count)\n- Remaining unresolved failures or review comments\n\n## References\n\n- Verification stack (layers, signals, retriggering): `references/verification.md`\n- CI/review heuristics and decision tree: `references/heuristics.md`"
211
+ },
212
+ {
213
+ name: "jira-issue-to-pr",
214
+ description: "This skill should be used when the user asks to \"set up a Jira automation to create pull requests\", \"poll Jira for create-pr issues\", \"automatically create GitHub PRs from Jira tickets\", \"deploy a Jira issue-to-PR automation\", \"create a Jira to GitHub PR workflow\", or mentions automating GitHub PR creation from a Jira label. Deploys a cron-based OpenHands automation that watches a Jira Cloud project for issues labeled with a configurable label (default: \"create-pr\") and spawns an agent conversation to create a GitHub pull request for each new issue found. The target GitHub repository is read from the body of the Jira ticket - no repo parameter is required at deploy time.",
215
+ triggers: [],
216
+ content: "# Jira → GitHub PR Automation\n\nDeploys a cron automation that polls a Jira Cloud instance for open issues carrying a\nconfigurable label and, for each new issue, starts an OpenHands agent conversation that\nclones the GitHub repository specified in the ticket body, creates a branch, implements\nor placeholders the requested change, and opens a pull request. Once the conversation\nstarts, it also posts a comment on the Jira ticket: \"I'm on it: &lt;conversation URL&gt;\".\n\n## How It Works\n\n1. **Poll** - every N minutes, `POST /rest/api/3/search/jql` on the Jira Cloud instance\n to find open issues with the configured label.\n2. **Deduplicate** - on the very first run the script records a `first_run_at` baseline\n timestamp in the KV store; any issue whose `updated` timestamp predates that baseline\n is skipped (no backfill blast on first deploy). Using `updated` rather than `created`\n means an old issue that has its label added after the automation is deployed will still\n be picked up. Subsequent runs filter by both `first_run_at` and a KV-backed set of\n already-processed issue keys. A `max_new_per_run` cap (default 5) limits conversations\n started per cron firing as additional defense-in-depth.\n3. **Dispatch** - for each new issue, call `POST /api/conversations` on the agent server\n to start an independent agent conversation with a PR-creation prompt. The prompt\n instructs the agent to extract the target GitHub repository (`owner/repo`) from the\n ticket body.\n4. **Comment** - immediately after the conversation is created, post a Jira comment on the\n issue: `I'm on it: <conversation URL>`.\n5. **Persist** - record the processed issue key so re-runs never duplicate work.\n\nThe polling run is lightweight (stdlib only, no SDK install); LLM costs are incurred only\nwhen new issues are actually found.\n\n## Prerequisites\n\nBefore deploying, ensure the following are in place:\n\n| Requirement | Details |\n|---|---|\n| **Jira API token** | Stored as an OpenHands secret (see [Jira API token setup](#jira-api-token)) |\n| **GitHub token** | Must be stored as an OpenHands secret with `repo` + `workflow` scope so the spawned conversation can push branches and open PRs |\n| **Jira label** | The label to watch for (default: `create-pr`) must exist in the Jira project |\n| **GitHub repo** | The target repository must exist and the GitHub token must have write access |\n\n## Deploying the Automation\n\n### Step 1 - Collect parameters\n\nGather the following from the user before proceeding:\n\n| Parameter | Example | Notes |\n|---|---|---|\n| `jira_base_url` | `https://acme.atlassian.net` | No trailing slash |\n| `jira_email` | `alice@acme.com` | Atlassian account email for Basic auth |\n| `jira_token_secret` | `JIRA_CLOUD_KEY` | Name of the OpenHands secret holding the API token |\n| `jira_label` | `create-pr` | Label to watch for (optional, defaults to `create-pr`) |\n| `max_new_per_run` | `5` | Max conversations dispatched per cron firing (optional, defaults to `5`) |\n| `cron_schedule` | `*/5 * * * *` | Polling frequency in cron syntax |\n\n> **Note**: The GitHub repository is not configured here. Each Jira ticket body must include\n> a reference to the target GitHub repo in `owner/repo` format (e.g. `acme-org/backend`).\n> The spawned agent extracts it from the ticket text.\n\n### Step 2 - Create config.json\n\nCreate `config.json` next to `scripts/main.py` when packaging:\n\n```json\n{\n \"jira_base_url\": \"https://acme.atlassian.net\",\n \"jira_email\": \"alice@acme.com\",\n \"jira_token_secret\": \"JIRA_CLOUD_KEY\",\n \"jira_label\": \"create-pr\",\n \"max_new_per_run\": 5\n}\n```\n\n### Step 3 - Package the tarball\n\nCopy `scripts/main.py` from this skill and package it with the `config.json`:\n\n```bash\nWORK=$(mktemp -d)\ncp <skill-dir>/scripts/main.py \"$WORK/main.py\"\n# write config.json into $WORK/config.json (see Step 2)\ntar -czf /tmp/jira-issue-to-pr.tar.gz -C \"$WORK\" .\npython3 -m py_compile \"$WORK/main.py\" # validate syntax before uploading\n```\n\n### Step 4 - Upload the tarball\n\n```bash\nTARBALL_PATH=$(curl -s -X POST \\\n \"http://localhost:8000/api/automation/v1/uploads?name=jira-issue-to-pr\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/gzip\" \\\n --data-binary @/tmp/jira-issue-to-pr.tar.gz \\\n | python3 -c \"import sys,json; print(json.load(sys.stdin)['tarball_path'])\")\n```\n\n### Step 5 - Create the automation\n\n```bash\ncurl -s -X POST \"http://localhost:8000/api/automation/v1\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\n \\\"name\\\": \\\"Jira issue-to-PR Poller\\\",\n \\\"trigger\\\": {\n \\\"type\\\": \\\"cron\\\",\n \\\"schedule\\\": \\\"*/5 * * * *\\\",\n \\\"timezone\\\": \\\"UTC\\\"\n },\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 540\n }\" | python3 -m json.tool\n```\n\nSave the returned `id` - use it for updates and monitoring.\n\n### Step 6 - Verify with a test dispatch\n\n```bash\ncurl -s -X POST \\\n \"http://localhost:8000/api/automation/v1/<AUTOMATION_ID>/dispatch\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" | python3 -m json.tool\n\n# After ~30 seconds, check the run status:\ncurl -s \"http://localhost:8000/api/automation/v1/<AUTOMATION_ID>/runs?limit=1\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n | python3 -c \"import sys,json; r=json.load(sys.stdin)['runs'][0]; print(r['status'], r.get('error_detail'))\"\n```\n\n## Updating an Existing Deployment\n\nTo change configuration or update the script:\n\n1. Edit `config.json` with new values.\n2. Repackage and upload a new tarball (Steps 3-4 above).\n3. PATCH the existing automation with the new `tarball_path`:\n\n```bash\ncurl -s -X PATCH \\\n \"http://localhost:8000/api/automation/v1/<AUTOMATION_ID>\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\\\"tarball_path\\\": \\\"<NEW_TARBALL_PATH>\\\"}\"\n```\n\n## Resetting Processed State\n\nTo reprocess issues that were already handled (e.g., after testing), clear the KV store:\n\n```bash\ncurl -s -X DELETE \\\n \"http://localhost:8000/api/automation/v1/<KV_BASE>/v1/kv/state\" \\\n -H \"Authorization: Bearer $AUTOMATION_KV_TOKEN\"\n```\n\nOr delete and recreate the automation to start with a clean state.\n\n## Script Reference\n\nThe automation script lives at `scripts/main.py`. Key behaviors:\n\n- **No SDK dependencies** - pure Python stdlib; no `setup.sh` or `uv` install needed.\n- **Config file** - reads all parameters from `config.json` co-located with the script.\n- **First-run baseline** - on the very first execution the script writes `first_run_at` (UTC timestamp) into the KV store and exits without dispatching; issues whose `updated` timestamp predates that baseline are skipped on all subsequent runs. Using `updated` (not `created`) means an old issue that has its label applied after deployment is correctly treated as new.\n- **Per-run cap** - `max_new_per_run` (default 5) limits how many conversations are started per cron firing; any remaining new issues are dispatched on the next run.\n- **KV store** - persists `{\"processed_keys\": [...], \"first_run_at\": \"...\"}` between runs; falls back to a local file in dev environments where `AUTOMATION_KV_TOKEN` is absent.\n- **Jira API** - uses `POST /rest/api/3/search/jql` (the current non-deprecated endpoint).\n- **Conversation dispatch** - calls `POST /api/conversations` on the agent server with the current user's LLM/agent settings forwarded to the new conversation.\n- **Error transparency** - captures Jira HTTP response bodies in error messages for fast diagnosis.\n\n## Known Limitations\n\n### Pre-existing issues updated after deployment\n\nThe deduplication filter compares each issue's `fields.updated` timestamp against\n`first_run_at`. `updated` is Jira's last-modified timestamp for the issue as a whole —\nit advances whenever **any** field changes (comments, priority, description, status, etc.),\nnot only when the `create-pr` label is applied.\n\nThis means a pre-existing issue that already carried the label at deployment time can\nslip through the filter if it is later updated for an unrelated reason (e.g. someone adds\na comment), because its `updated` timestamp will have advanced past `first_run_at` while\nits key is not yet in `processed_keys`.\n\n**Workaround:** The only fully reliable way to detect exactly when a label was applied\nis the Jira changelog API (`GET /rest/api/3/issue/{key}/changelog`), which requires an\nextra HTTP call per issue. To avoid that overhead, keep the automation's scope narrow:\nuse a label that is exclusively added as a PR-creation signal and is not already present\non issues at the time of deployment.\n\nOnce an issue is successfully dispatched its key is written to `processed_keys` in the\nKV store and is **permanently skipped on every future run** — regardless of subsequent\nlabel changes, comments, or any other updates to the issue. The only way to re-trigger a\npreviously processed issue is to manually clear the KV store or delete and recreate the\nautomation. This means the risk window described above is finite: as soon as the\nautomation processes a pre-existing issue (even accidentally), it will never dispatch\nthat issue again.\n\n## Additional Resources\n\n- **`references/setup.md`** - Jira API token creation, GitHub token scopes, cron schedule reference, and troubleshooting guide."
211
217
  },
212
218
  {
213
219
  name: "jupyter",
214
220
  description: "Read, modify, execute, and convert Jupyter notebooks programmatically. Use when working with .ipynb files for data science workflows, including editing cells, clearing outputs, or converting to other formats.",
215
221
  triggers: ["ipynb", "jupyter"],
216
- content: "# Jupyter Notebook Guide\n\nNotebooks are JSON files. Cells are in `nb['cells']`, each has `source` (list of strings) and `cell_type` ('code', 'markdown', or 'raw').\n\n## Modifying Notebooks\n```python\nimport json\nwith open('notebook.ipynb') as f:\n nb = json.load(f)\n# Modify nb['cells'][i]['source'], then:\nwith open('notebook.ipynb', 'w') as f:\n json.dump(nb, f, indent=1)\n```\n\n## Executing & Converting\n```bash\njupyter nbconvert --to notebook --execute --inplace notebook.ipynb # Execute in place\njupyter nbconvert --to html notebook.ipynb # Convert to HTML\njupyter nbconvert --to script notebook.ipynb # Convert to Python\njupyter nbconvert --to markdown notebook.ipynb # Convert to Markdown\n```\n\n## Finding Code\n```bash\ngrep -n \"search_term\" notebook.ipynb\n```\n\n## Cell Structure\n```python\n# Code cell\n{\"cell_type\": \"code\", \"execution_count\": None, \"metadata\": {}, \"outputs\": [], \"source\": [\"code\\n\"]}\n# Markdown cell\n{\"cell_type\": \"markdown\", \"metadata\": {}, \"source\": [\"# Title\\n\"]}\n```\n\n## Clear Outputs\n```python\nfor cell in nb['cells']:\n if cell['cell_type'] == 'code':\n cell['outputs'] = []\n cell['execution_count'] = None\n```"
222
+ content: "# Jupyter Notebook Guide\n\nNotebooks are JSON files. Cells are in `nb['cells']`, each has `source` (list of strings) and `cell_type` ('code', 'markdown', or 'raw').\n\n## Modifying Notebooks\n```python\nimport json\nwith open('notebook.ipynb') as f:\n nb = json.load(f)\n# Modify nb['cells'][i]['source'], then:\nwith open('notebook.ipynb', 'w') as f:\n json.dump(nb, f, indent=1)\n```\n\n## Executing & Converting\n```bash\njupyter nbconvert --to notebook --execute --inplace notebook.ipynb # Execute in place\njupyter nbconvert --to html notebook.ipynb # Convert to HTML\njupyter nbconvert --to script notebook.ipynb # Convert to Python\njupyter nbconvert --to markdown notebook.ipynb # Convert to Markdown\n```\n\n## Finding Code\n```bash\ngrep -n \"search_term\" notebook.ipynb\n```\n\nPowerShell equivalent:\n\n```powershell\nSelect-String -Path notebook.ipynb -Pattern \"search_term\"\n```\n\n## Cell Structure\n```python\n# Code cell\n{\"cell_type\": \"code\", \"execution_count\": None, \"metadata\": {}, \"outputs\": [], \"source\": [\"code\\n\"]}\n# Markdown cell\n{\"cell_type\": \"markdown\", \"metadata\": {}, \"source\": [\"# Title\\n\"]}\n```\n\n## Clear Outputs\n```python\nfor cell in nb['cells']:\n if cell['cell_type'] == 'code':\n cell['outputs'] = []\n cell['execution_count'] = None\n```"
217
223
  },
218
224
  {
219
225
  name: "kubernetes",
@@ -223,7 +229,7 @@ var e = [
223
229
  "k8s",
224
230
  "kube"
225
231
  ],
226
- content: "# Kubernetes Local Development with KIND\n\n## KIND Installation and Setup\n\nKIND (Kubernetes IN Docker) is a tool for running local Kubernetes clusters using Docker containers as nodes. It's designed for testing Kubernetes applications locally.\n\nIMPORTANT: Before you proceed with installation, make sure you have docker installed locally.\n\n### Installation\n\nTo install KIND on a Debian/Ubuntu system:\n\n```bash\n# Download KIND binary\ncurl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-amd64\n# Make it executable\nchmod +x ./kind\n# Move to a directory in your PATH\nsudo mv ./kind /usr/local/bin/\n```\n\nTo install kubectl:\n\n```bash\n# Download kubectl\ncurl -LO \"https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl\"\n# Make it executable\nchmod +x kubectl\n# Move to a directory in your PATH\nsudo mv ./kubectl /usr/local/bin/\n```\n\n### Creating a Cluster\n\nCreate a basic KIND cluster:\n\n```bash\nkind create cluster\n```"
232
+ content: "# Kubernetes Local Development with KIND\n\n## KIND Installation and Setup\n\nKIND (Kubernetes IN Docker) is a tool for running local Kubernetes clusters using Docker containers as nodes. It's designed for testing Kubernetes applications locally.\n\nIMPORTANT: Before you proceed with installation, make sure you have docker installed locally.\nWindows PowerShell equivalents for installing KIND and kubectl are in `references/windows.md`.\n\n### Installation\n\nTo install KIND on a Debian/Ubuntu system:\n\n```bash\n# Download KIND binary\ncurl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-amd64\n# Make it executable\nchmod +x ./kind\n# Move to a directory in your PATH\nsudo mv ./kind /usr/local/bin/\n```\n\nTo install kubectl:\n\n```bash\n# Download kubectl\ncurl -LO \"https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl\"\n# Make it executable\nchmod +x kubectl\n# Move to a directory in your PATH\nsudo mv ./kubectl /usr/local/bin/\n```\n\n### Creating a Cluster\n\nCreate a basic KIND cluster:\n\n```bash\nkind create cluster\n```"
227
233
  },
228
234
  {
229
235
  name: "learn-from-code-review",
@@ -243,19 +249,19 @@ var e = [
243
249
  "ticket",
244
250
  "issue tracking"
245
251
  ],
246
- content: "# Linear\n\n<IMPORTANT>\nBefore performing any Linear operations, check if the required environment variable is set:\n\n```bash\n[ -n \"$LINEAR_API_KEY\" ] && echo \"LINEAR_API_KEY is set\" || echo \"LINEAR_API_KEY is NOT set\"\n```\n\nIf LINEAR_API_KEY is missing, ask the user to provide it before proceeding.\n</IMPORTANT>\n\n## Understanding Linear Identifiers\n\nLinear uses two types of identifiers for issues:\n\n- **Human-readable identifier** (e.g., `ALL-1234`): Displayed to users, used in search queries. This is the team key + number.\n- **UUID** (e.g., `a1b2c3d4-e5f6-7890-abcd-ef1234567890`): Required for all mutations (update, comment, etc.). Returned as `id` in query results.\n\n**Important workflow**: When working with issues, you must:\n1. Search or query using the human-readable identifier\n2. Extract the `id` (UUID) from the query result\n3. Use the UUID in any mutation operations\n\n## Authentication\n\nAll Linear API requests use GraphQL with the API key in the Authorization header:\n\n```bash\ncurl -s -X POST https://api.linear.app/graphql \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: $LINEAR_API_KEY\" \\\n -d '{\"query\": \"YOUR_GRAPHQL_QUERY\"}'\n```\n\n## Common Queries\n\n### Get Assigned Issues (Open)\n\n```bash\ncurl -s -X POST https://api.linear.app/graphql \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: $LINEAR_API_KEY\" \\\n -d '{\n \"query\": \"query { viewer { assignedIssues(first: 50, filter: { state: { type: { nin: [\\\"completed\\\", \\\"canceled\\\"] } } }) { nodes { id identifier title priority priorityLabel state { name type } description createdAt updatedAt } } } }\"\n }' | jq '.data.viewer.assignedIssues.nodes'\n```\n\n### Get Issues by Priority\n\nPriority values: 0 = No priority, 1 = Urgent, 2 = High, 3 = Medium, 4 = Low\n\n```bash\ncurl -s -X POST https://api.linear.app/graphql \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: $LINEAR_API_KEY\" \\\n -d '{\n \"query\": \"query { viewer { assignedIssues(first: 50, filter: { priority: { lte: 2 }, state: { type: { nin: [\\\"completed\\\", \\\"canceled\\\"] } } }) { nodes { id identifier title priority priorityLabel state { name } } } } }\"\n }' | jq '.data.viewer.assignedIssues.nodes'\n```\n\n### Get Issue Details\n\n```bash\ncurl -s -X POST https://api.linear.app/graphql \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: $LINEAR_API_KEY\" \\\n -d '{\n \"query\": \"query { issue(id: \\\"ISSUE_UUID\\\") { id identifier title description state { name } priority assignee { name email } labels { nodes { name } } comments { nodes { body createdAt user { name } } } } }\"\n }' | jq '.data.issue'\n```\n\n### Search Issues by Identifier\n\n```bash\ncurl -s -X POST https://api.linear.app/graphql \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: $LINEAR_API_KEY\" \\\n -d '{\n \"query\": \"query { issueSearch(query: \\\"ALL-1234\\\", first: 5) { nodes { id identifier title state { name } } } }\"\n }' | jq '.data.issueSearch.nodes'\n```\n\n## Common Mutations\n\n### Update Issue State\n\nFirst, get available workflow states:\n```bash\ncurl -s -X POST https://api.linear.app/graphql \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: $LINEAR_API_KEY\" \\\n -d '{\n \"query\": \"query { workflowStates { nodes { id name type } } }\"\n }' | jq '.data.workflowStates.nodes'\n```\n\nThen update the issue:\n```bash\ncurl -s -X POST https://api.linear.app/graphql \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: $LINEAR_API_KEY\" \\\n -d '{\n \"query\": \"mutation { issueUpdate(id: \\\"ISSUE_UUID\\\", input: { stateId: \\\"STATE_UUID\\\" }) { success issue { identifier state { name } } } }\"\n }' | jq '.data.issueUpdate'\n```\n\n### Add Comment to Issue\n\n```bash\ncurl -s -X POST https://api.linear.app/graphql \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: $LINEAR_API_KEY\" \\\n -d '{\n \"query\": \"mutation { commentCreate(input: { issueId: \\\"ISSUE_UUID\\\", body: \\\"Your comment here\\\" }) { success comment { id body } } }\"\n }' | jq '.data.commentCreate'\n```\n\n### Create New Issue\n\n```bash\ncurl -s -X POST https://api.linear.app/graphql \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: $LINEAR_API_KEY\" \\\n -d '{\n \"query\": \"mutation { issueCreate(input: { teamId: \\\"TEAM_UUID\\\", title: \\\"Issue Title\\\", description: \\\"Issue description\\\", priority: 2 }) { success issue { identifier title url } } }\"\n }' | jq '.data.issueCreate'\n```\n\n## End-to-End Workflow: Move Issue to \"In Progress\"\n\nThis example shows the complete flow to change an issue's state using its human-readable identifier:\n\n### Step 1: Search for the issue to get its UUID\n\n```bash\n# Search for issue ALL-1234 and extract its UUID\ncurl -s -X POST https://api.linear.app/graphql \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: $LINEAR_API_KEY\" \\\n -d '{\n \"query\": \"query { issueSearch(query: \\\"ALL-1234\\\", first: 1) { nodes { id identifier title state { name } } } }\"\n }' | jq '.data.issueSearch.nodes[0]'\n# Save the \"id\" value (UUID) from the response\n```\n\n### Step 2: Get available workflow states\n\n```bash\n# List all workflow states to find the \"In Progress\" state UUID\ncurl -s -X POST https://api.linear.app/graphql \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: $LINEAR_API_KEY\" \\\n -d '{\n \"query\": \"query { workflowStates { nodes { id name type } } }\"\n }' | jq '.data.workflowStates.nodes[] | select(.name == \"In Progress\")'\n# Save the \"id\" value of the desired state\n```\n\n### Step 3: Update the issue state\n\n```bash\n# Use the issue UUID and state UUID from previous steps\ncurl -s -X POST https://api.linear.app/graphql \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: $LINEAR_API_KEY\" \\\n -d '{\n \"query\": \"mutation { issueUpdate(id: \\\"ISSUE_UUID_FROM_STEP_1\\\", input: { stateId: \\\"STATE_UUID_FROM_STEP_2\\\" }) { success issue { identifier state { name } } } }\"\n }' | jq '.data.issueUpdate'\n```\n\n## Get Team Information\n\n```bash\ncurl -s -X POST https://api.linear.app/graphql \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: $LINEAR_API_KEY\" \\\n -d '{\n \"query\": \"query { teams { nodes { id name key } } }\"\n }' | jq '.data.teams.nodes'\n```\n\n## Priority Levels\n\n| Priority | Label | Recommended Action |\n|----------|-------|-------------------|\n| 1 | Urgent | Work on immediately |\n| 2 | High | Work on first |\n| 3 | Medium | Normal priority |\n| 4 | Low | When time permits |\n| 0 | None | Backlog |\n\n## State Types\n\n- `backlog` - Not yet started\n- `unstarted` - Todo\n- `started` - In Progress\n- `completed` - Done\n- `canceled` - Won't do\n\n## Documentation\n\n- [Linear API Documentation](https://developers.linear.app/docs/graphql/working-with-the-graphql-api)\n- [GraphQL Schema Reference](https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference)"
252
+ content: "# Linear\n\nWindows PowerShell equivalents for the repeated Linear GraphQL `curl` and environment-variable snippets are in `references/windows.md`.\n\n<IMPORTANT>\nBefore performing any Linear operations, check if the required environment variable is set:\n\n```bash\n[ -n \"$LINEAR_API_KEY\" ] && echo \"LINEAR_API_KEY is set\" || echo \"LINEAR_API_KEY is NOT set\"\n```\n\nIf LINEAR_API_KEY is missing, ask the user to provide it before proceeding.\n</IMPORTANT>\n\n## Understanding Linear Identifiers\n\nLinear uses two types of identifiers for issues:\n\n- **Human-readable identifier** (e.g., `ALL-1234`): Displayed to users, used in search queries. This is the team key + number.\n- **UUID** (e.g., `a1b2c3d4-e5f6-7890-abcd-ef1234567890`): Required for all mutations (update, comment, etc.). Returned as `id` in query results.\n\n**Important workflow**: When working with issues, you must:\n1. Search or query using the human-readable identifier\n2. Extract the `id` (UUID) from the query result\n3. Use the UUID in any mutation operations\n\n## Authentication\n\nAll Linear API requests use GraphQL with the API key in the Authorization header:\n\n```bash\ncurl -s -X POST https://api.linear.app/graphql \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: $LINEAR_API_KEY\" \\\n -d '{\"query\": \"YOUR_GRAPHQL_QUERY\"}'\n```\n\n## Common Queries\n\n### Get Assigned Issues (Open)\n\n```bash\ncurl -s -X POST https://api.linear.app/graphql \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: $LINEAR_API_KEY\" \\\n -d '{\n \"query\": \"query { viewer { assignedIssues(first: 50, filter: { state: { type: { nin: [\\\"completed\\\", \\\"canceled\\\"] } } }) { nodes { id identifier title priority priorityLabel state { name type } description createdAt updatedAt } } } }\"\n }' | jq '.data.viewer.assignedIssues.nodes'\n```\n\n### Get Issues by Priority\n\nPriority values: 0 = No priority, 1 = Urgent, 2 = High, 3 = Medium, 4 = Low\n\n```bash\ncurl -s -X POST https://api.linear.app/graphql \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: $LINEAR_API_KEY\" \\\n -d '{\n \"query\": \"query { viewer { assignedIssues(first: 50, filter: { priority: { lte: 2 }, state: { type: { nin: [\\\"completed\\\", \\\"canceled\\\"] } } }) { nodes { id identifier title priority priorityLabel state { name } } } } }\"\n }' | jq '.data.viewer.assignedIssues.nodes'\n```\n\n### Get Issue Details\n\n```bash\ncurl -s -X POST https://api.linear.app/graphql \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: $LINEAR_API_KEY\" \\\n -d '{\n \"query\": \"query { issue(id: \\\"ISSUE_UUID\\\") { id identifier title description state { name } priority assignee { name email } labels { nodes { name } } comments { nodes { body createdAt user { name } } } } }\"\n }' | jq '.data.issue'\n```\n\n### Search Issues by Identifier\n\n```bash\ncurl -s -X POST https://api.linear.app/graphql \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: $LINEAR_API_KEY\" \\\n -d '{\n \"query\": \"query { issueSearch(query: \\\"ALL-1234\\\", first: 5) { nodes { id identifier title state { name } } } }\"\n }' | jq '.data.issueSearch.nodes'\n```\n\n## Common Mutations\n\n### Update Issue State\n\nFirst, get available workflow states:\n```bash\ncurl -s -X POST https://api.linear.app/graphql \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: $LINEAR_API_KEY\" \\\n -d '{\n \"query\": \"query { workflowStates { nodes { id name type } } }\"\n }' | jq '.data.workflowStates.nodes'\n```\n\nThen update the issue:\n```bash\ncurl -s -X POST https://api.linear.app/graphql \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: $LINEAR_API_KEY\" \\\n -d '{\n \"query\": \"mutation { issueUpdate(id: \\\"ISSUE_UUID\\\", input: { stateId: \\\"STATE_UUID\\\" }) { success issue { identifier state { name } } } }\"\n }' | jq '.data.issueUpdate'\n```\n\n### Add Comment to Issue\n\n```bash\ncurl -s -X POST https://api.linear.app/graphql \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: $LINEAR_API_KEY\" \\\n -d '{\n \"query\": \"mutation { commentCreate(input: { issueId: \\\"ISSUE_UUID\\\", body: \\\"Your comment here\\\" }) { success comment { id body } } }\"\n }' | jq '.data.commentCreate'\n```\n\n### Create New Issue\n\n```bash\ncurl -s -X POST https://api.linear.app/graphql \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: $LINEAR_API_KEY\" \\\n -d '{\n \"query\": \"mutation { issueCreate(input: { teamId: \\\"TEAM_UUID\\\", title: \\\"Issue Title\\\", description: \\\"Issue description\\\", priority: 2 }) { success issue { identifier title url } } }\"\n }' | jq '.data.issueCreate'\n```\n\n## End-to-End Workflow: Move Issue to \"In Progress\"\n\nThis example shows the complete flow to change an issue's state using its human-readable identifier:\n\n### Step 1: Search for the issue to get its UUID\n\n```bash\n# Search for issue ALL-1234 and extract its UUID\ncurl -s -X POST https://api.linear.app/graphql \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: $LINEAR_API_KEY\" \\\n -d '{\n \"query\": \"query { issueSearch(query: \\\"ALL-1234\\\", first: 1) { nodes { id identifier title state { name } } } }\"\n }' | jq '.data.issueSearch.nodes[0]'\n# Save the \"id\" value (UUID) from the response\n```\n\n### Step 2: Get available workflow states\n\n```bash\n# List all workflow states to find the \"In Progress\" state UUID\ncurl -s -X POST https://api.linear.app/graphql \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: $LINEAR_API_KEY\" \\\n -d '{\n \"query\": \"query { workflowStates { nodes { id name type } } }\"\n }' | jq '.data.workflowStates.nodes[] | select(.name == \"In Progress\")'\n# Save the \"id\" value of the desired state\n```\n\n### Step 3: Update the issue state\n\n```bash\n# Use the issue UUID and state UUID from previous steps\ncurl -s -X POST https://api.linear.app/graphql \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: $LINEAR_API_KEY\" \\\n -d '{\n \"query\": \"mutation { issueUpdate(id: \\\"ISSUE_UUID_FROM_STEP_1\\\", input: { stateId: \\\"STATE_UUID_FROM_STEP_2\\\" }) { success issue { identifier state { name } } } }\"\n }' | jq '.data.issueUpdate'\n```\n\n## Get Team Information\n\n```bash\ncurl -s -X POST https://api.linear.app/graphql \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: $LINEAR_API_KEY\" \\\n -d '{\n \"query\": \"query { teams { nodes { id name key } } }\"\n }' | jq '.data.teams.nodes'\n```\n\n## Priority Levels\n\n| Priority | Label | Recommended Action |\n|----------|-------|-------------------|\n| 1 | Urgent | Work on immediately |\n| 2 | High | Work on first |\n| 3 | Medium | Normal priority |\n| 4 | Low | When time permits |\n| 0 | None | Backlog |\n\n## State Types\n\n- `backlog` - Not yet started\n- `unstarted` - Todo\n- `started` - In Progress\n- `completed` - Done\n- `canceled` - Won't do\n\n## Documentation\n\n- [Linear API Documentation](https://developers.linear.app/docs/graphql/working-with-the-graphql-api)\n- [GraphQL Schema Reference](https://studio.apollographql.com/public/Linear-API/variant/current/schema/reference)"
247
253
  },
248
254
  {
249
255
  name: "linear-triage",
250
256
  description: "Create an automation that triages new Linear issues. Inspects the issue title, description, team, customer, priority, and recent related issues via Linear MCP. Suggests labels, priority, likely owner, duplicates, and posts a clarifying comment.",
251
257
  triggers: ["/linear-triage:setup"],
252
- 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\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}`"
258
+ 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}`"
253
259
  },
254
260
  {
255
261
  name: "notion",
256
262
  description: "Create, search, and update Notion pages/databases using the Notion API. Use for documenting work, generating runbooks, and automating knowledge base updates.",
257
263
  triggers: ["notion"],
258
- content: "# Notion\n\n<IMPORTANT>\nIf authenticated Notion MCP tools are available in the environment, use them first. MCP tools do not require passing `NOTION_INTEGRATION_KEY` as a tool argument; authentication is handled by the configured MCP integration.\n\nUse the direct Notion REST API examples below only when MCP is unavailable or when you explicitly need raw API/curl access. For that direct-API path, first check whether the required environment variable is set:\n\n```bash\n[ -n \"$NOTION_INTEGRATION_KEY\" ] && echo \"NOTION_INTEGRATION_KEY is set\" || echo \"NOTION_INTEGRATION_KEY is NOT set\"\n```\n\nIf it’s missing and you need the direct API path, ask the user to provide it (or connect a Notion integration) before proceeding:\n- **NOTION_INTEGRATION_KEY**: Notion integration secret (starts with `ntn_...`)\n\nWhether you use MCP or the direct API, also confirm the configured integration has been **shared** with the target page/database in Notion.\n</IMPORTANT>\n\n## Base headers for direct API calls\n\n```bash\n-H \"Authorization: Bearer ${NOTION_INTEGRATION_KEY}\" \\\n-H \"Notion-Version: 2022-06-28\" \\\n-H \"Content-Type: application/json\"\n```\n\n## Find a page (search)\n\nUse Notion’s search endpoint to find a page by title.\n\n```bash\ncurl -s https://api.notion.com/v1/search \\\n -H \"Authorization: Bearer ${NOTION_INTEGRATION_KEY}\" \\\n -H \"Notion-Version: 2022-06-28\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"query\": \"OpenHands Wiki\",\n \"page_size\": 10\n }' | jq .\n```\n\n## Create a page under a parent page\n\n```bash\nPARENT_PAGE_ID=\"<parent_page_id>\"\n\ncurl -s https://api.notion.com/v1/pages \\\n -H \"Authorization: Bearer ${NOTION_INTEGRATION_KEY}\" \\\n -H \"Notion-Version: 2022-06-28\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"parent\": {\"type\": \"page_id\", \"page_id\": \"'\"${PARENT_PAGE_ID}\"'\"},\n \"properties\": {\n \"title\": {\n \"title\": [{\"type\": \"text\", \"text\": {\"content\": \"My new page\"}}]\n }\n },\n \"children\": [\n {\n \"object\": \"block\",\n \"type\": \"paragraph\",\n \"paragraph\": {\n \"rich_text\": [{\"type\": \"text\", \"text\": {\"content\": \"Hello from OpenHands.\"}}]\n }\n }\n ]\n }' | jq .\n```\n\n## Append blocks to an existing page\n\nUse the page’s block id (same as page id) to append children.\n\n```bash\nPAGE_ID=\"<page_id>\"\n\ncurl -s -X PATCH \"https://api.notion.com/v1/blocks/${PAGE_ID}/children\" \\\n -H \"Authorization: Bearer ${NOTION_INTEGRATION_KEY}\" \\\n -H \"Notion-Version: 2022-06-28\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"children\": [\n {\n \"object\": \"block\",\n \"type\": \"heading_2\",\n \"heading_2\": {\"rich_text\": [{\"type\": \"text\", \"text\": {\"content\": \"Appended section\"}}]}\n }\n ]\n }' | jq .\n```\n\n## Tips / gotchas\n\n- **Sharing is required**: even with a valid key, the integration can’t see a page/database until it has been shared with the integration in the Notion UI.\n- **Rate limits**: keep requests small; for large pages, create the page first and then append blocks in batches.\n- **IDs format**: Notion IDs may be returned with dashes; both dashed and non-dashed forms typically work in API calls.\n\n## Documentation\n\n- Notion API: https://developers.notion.com/reference/intro\n- Search: https://developers.notion.com/reference/post-search\n- Create a page: https://developers.notion.com/reference/post-page\n- Append block children: https://developers.notion.com/reference/patch-block-children"
264
+ content: "# Notion\n\nWindows PowerShell equivalents for the repeated Notion REST `curl`, environment-variable, and JSON-body snippets are in `references/windows.md`.\n\n<IMPORTANT>\nIf authenticated Notion MCP tools are available in the environment, use them first. MCP tools do not require passing `NOTION_INTEGRATION_KEY` as a tool argument; authentication is handled by the configured MCP integration.\n\nUse the direct Notion REST API examples below only when MCP is unavailable or when you explicitly need raw API/curl access. For that direct-API path, first check whether the required environment variable is set:\n\n```bash\n[ -n \"$NOTION_INTEGRATION_KEY\" ] && echo \"NOTION_INTEGRATION_KEY is set\" || echo \"NOTION_INTEGRATION_KEY is NOT set\"\n```\n\nIf it’s missing and you need the direct API path, ask the user to provide it (or connect a Notion integration) before proceeding:\n- **NOTION_INTEGRATION_KEY**: Notion integration secret (starts with `ntn_...`)\n\nWhether you use MCP or the direct API, also confirm the configured integration has been **shared** with the target page/database in Notion.\n</IMPORTANT>\n\n## Base headers for direct API calls\n\n```bash\n-H \"Authorization: Bearer ${NOTION_INTEGRATION_KEY}\" \\\n-H \"Notion-Version: 2022-06-28\" \\\n-H \"Content-Type: application/json\"\n```\n\n## Find a page (search)\n\nUse Notion’s search endpoint to find a page by title.\n\n```bash\ncurl -s https://api.notion.com/v1/search \\\n -H \"Authorization: Bearer ${NOTION_INTEGRATION_KEY}\" \\\n -H \"Notion-Version: 2022-06-28\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"query\": \"OpenHands Wiki\",\n \"page_size\": 10\n }' | jq .\n```\n\n## Create a page under a parent page\n\n```bash\nPARENT_PAGE_ID=\"<parent_page_id>\"\n\ncurl -s https://api.notion.com/v1/pages \\\n -H \"Authorization: Bearer ${NOTION_INTEGRATION_KEY}\" \\\n -H \"Notion-Version: 2022-06-28\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"parent\": {\"type\": \"page_id\", \"page_id\": \"'\"${PARENT_PAGE_ID}\"'\"},\n \"properties\": {\n \"title\": {\n \"title\": [{\"type\": \"text\", \"text\": {\"content\": \"My new page\"}}]\n }\n },\n \"children\": [\n {\n \"object\": \"block\",\n \"type\": \"paragraph\",\n \"paragraph\": {\n \"rich_text\": [{\"type\": \"text\", \"text\": {\"content\": \"Hello from OpenHands.\"}}]\n }\n }\n ]\n }' | jq .\n```\n\n## Append blocks to an existing page\n\nUse the page’s block id (same as page id) to append children.\n\n```bash\nPAGE_ID=\"<page_id>\"\n\ncurl -s -X PATCH \"https://api.notion.com/v1/blocks/${PAGE_ID}/children\" \\\n -H \"Authorization: Bearer ${NOTION_INTEGRATION_KEY}\" \\\n -H \"Notion-Version: 2022-06-28\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"children\": [\n {\n \"object\": \"block\",\n \"type\": \"heading_2\",\n \"heading_2\": {\"rich_text\": [{\"type\": \"text\", \"text\": {\"content\": \"Appended section\"}}]}\n }\n ]\n }' | jq .\n```\n\n## Tips / gotchas\n\n- **Sharing is required**: even with a valid key, the integration can’t see a page/database until it has been shared with the integration in the Notion UI.\n- **Rate limits**: keep requests small; for large pages, create the page first and then append blocks in batches.\n- **IDs format**: Notion IDs may be returned with dashes; both dashed and non-dashed forms typically work in API calls.\n\n## Documentation\n\n- Notion API: https://developers.notion.com/reference/intro\n- Search: https://developers.notion.com/reference/post-search\n- Create a page: https://developers.notion.com/reference/post-page\n- Append block children: https://developers.notion.com/reference/patch-block-children"
259
265
  },
260
266
  {
261
267
  name: "npm",
@@ -274,7 +280,7 @@ var e = [
274
280
  "oh-api-v1",
275
281
  "oh-cloud-api-v1"
276
282
  ],
277
- content: "This skill documents the **OpenHands Cloud API** (V1), commonly used **agent-server APIs**, and small, easy-to-copy clients.\n\nIt is intentionally focused on common OpenHands API workflows:\n\n- Defaults to OpenHands Cloud (`https://app.all-hands.dev`).\n- Targets the **V1 app server REST API** under `/api/v1/...`.\n- Includes a few **agent server** endpoints (inside a sandbox) that use `X-Session-API-Key`.\n- Covers the **multi-conversation delegation pattern**: start separate Cloud conversations when you want fresh context windows or background work.\n- Covers **local Agent Canvas backend conversations**: start or inspect conversations by calling a local agent server directly.\n\n## When to use this skill\n\nUse this skill when you need to:\n\n- start or inspect OpenHands Cloud conversations from code\n- monitor async startup via start-task polling\n- monitor execution status for long-running jobs\n- create separate Cloud conversations for parallel or background work\n- access sandbox agent-server endpoints once a conversation is running\n- start or inspect conversations on a local Agent Canvas backend or local agent server\n\n## Auth\n\n### App server (Cloud)\n\nUse Bearer auth:\n\n- Header: `Authorization: Bearer <OPENHANDS_CLOUD_API_KEY>`\n- Preferred env var: `OPENHANDS_CLOUD_API_KEY`\n- Backward-compatible env var: `OPENHANDS_API_KEY`\n\n### Agent server (inside a sandbox)\n\nUse session auth:\n\n- Header: `X-Session-API-Key: <session_api_key>`\n\nHow to obtain `agent_server_url` and `session_api_key`:\n\n1. Start or fetch an app conversation via the app server (Bearer auth), e.g.:\n - `POST /api/v1/app-conversations`\n - or `GET /api/v1/app-conversations?ids=<conversation_id>`\n2. In the returned JSON, look for sandbox/runtime connection fields (names vary slightly by deployment/version). Common patterns:\n - a sandbox object containing `agent_server_url` (or similar)\n - a session key such as `session_api_key` (or similar)\n3. Use those values to call the agent server directly:\n - Base: `{agent_server_url}/api/...`\n - Header: `X-Session-API-Key: <session_api_key>`\n\nExample (common field names; adjust to your deployment):\n\n```python\n# using the minimal Python client (`OpenHandsAPI`)\nconv = api.app_conversation_get(app_conversation_id)\n\nsession_api_key = conv.get(\"session_api_key\")\nconversation_url = conv.get(\"conversation_url\", \"\")\n\n# `conversation_url` often looks like: https://<runtime-host>/api/conversations/<id>\nagent_server_url = conversation_url.rsplit(\"/api/conversations\", 1)[0]\n```\n\n\nIf those fields are not present on the conversation record, list/search sandboxes (`GET /api/v1/sandboxes/search`) and use the sandbox referenced by the conversation to locate the agent server URL + session key.\n\n### Local Agent Canvas backend\n\nUse the local backend flow only for local Agent Canvas / agent-server development, such as `agent-canvas`, `agent-canvas --backend-only`, or `npm run dev` with ingress at `http://localhost:8000`. This calls the agent server directly with `X-Session-API-Key`. It is not an automation, and it is different from OpenHands Cloud delegation through `POST /api/v1/app-conversations`, which uses Bearer auth against the Cloud app API and may return asynchronous start-task records.\n\nWhen Agent Canvas runs locally, the launcher uses `LOCAL_BACKEND_API_KEY` when it is set. Otherwise it generates and persists the session API key at `~/.openhands/agent-canvas/api-key.txt`. Set `OH_SESSION_API_KEY_PATH` to override the persisted key path. Never print, log, or paste the actual key; use command substitution or an environment variable in examples and scripts.\n\n```bash\nLOCAL_AGENT_SERVER_URL=\"${LOCAL_AGENT_SERVER_URL:-http://localhost:8000}\"\nSESSION_API_KEY=\"${LOCAL_BACKEND_API_KEY:-$(cat \"${OH_SESSION_API_KEY_PATH:-$HOME/.openhands/agent-canvas/api-key.txt}\")}\"\n```\n\nCheck the local server before creating a backend conversation:\n\n```bash\ncurl -sS \"${LOCAL_AGENT_SERVER_URL}/server_info\" \\\n -H \"X-Session-API-Key: ${SESSION_API_KEY}\"\n```\n\nStart a backend conversation with `POST /api/conversations`. Include the agent settings and workspace expected by that backend. Local agent-server calls use an explicit `workspace` such as `{\"kind\": \"LocalWorkspace\", \"working_dir\": \"/workspace\"}`; Cloud app-conversation delegation instead uses app-server fields such as `selected_repository` and `selected_branch`. If you are starting the conversation from an existing Agent Canvas session, pass through the current configured settings or encrypted settings rather than hard-coding secrets into scripts.\n\n```bash\nCONVERSATION_JSON=$(curl -sS -X POST \"${LOCAL_AGENT_SERVER_URL}/api/conversations\" \\\n -H \"X-Session-API-Key: ${SESSION_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d @- <<'JSON'\n{\n \"agent\": {\n \"kind\": \"Agent\",\n \"llm\": {\n \"model\": \"your-model-provider/your-model-name\",\n \"api_key\": \"**********\"\n },\n \"tools\": [\n {\"name\": \"terminal\"},\n {\"name\": \"file_editor\"},\n {\"name\": \"task_tracker\"}\n ]\n },\n \"workspace\": {\"kind\": \"LocalWorkspace\", \"working_dir\": \"/workspace\"},\n \"initial_message\": {\n \"content\": [{\"text\": \"Summarize the current workspace.\"}],\n \"run\": true\n }\n}\nJSON\n)\nCONVERSATION_ID=$(python3 -c 'import json,sys; print(json.load(sys.stdin)[\"id\"])' <<<\"${CONVERSATION_JSON}\")\nprintf 'Conversation: %s/api/conversations/%s\\n' \"${LOCAL_AGENT_SERVER_URL}\" \"${CONVERSATION_ID}\"\n```\n\nPoll status and inspect recent events:\n\n```bash\ncurl -sS \"${LOCAL_AGENT_SERVER_URL}/api/conversations/${CONVERSATION_ID}\" \\\n -H \"X-Session-API-Key: ${SESSION_API_KEY}\"\n\ncurl -sS \"${LOCAL_AGENT_SERVER_URL}/api/conversations/${CONVERSATION_ID}/events/search?limit=20&sort_order=TIMESTAMP_DESC\" \\\n -H \"X-Session-API-Key: ${SESSION_API_KEY}\"\n```\n\nIf the same base URL serves the Agent Canvas UI, the browser route is:\n\n```bash\nprintf '%s/conversations/%s\\n' \"${LOCAL_AGENT_SERVER_URL}\" \"${CONVERSATION_ID}\"\n```\n\n## Common V1 app server endpoints\n\nThe following are the main endpoints implemented in the minimal client:\n\n- `GET /api/v1/users/me` — validate auth and inspect current account\n- `GET /api/v1/app-conversations/search?limit=...` — list recent conversations\n- `GET /api/v1/app-conversations?ids=...` — fetch conversation records by id (batch)\n- `GET /api/v1/app-conversations/count` — count conversations\n- `POST /api/v1/app-conversations` — start a new conversation (creates a sandbox)\n- `GET /api/v1/app-conversations/start-tasks?ids=...` — check async start-task status\n- `GET /api/v1/conversation/{app_conversation_id}/events/search?limit=...` — read conversation events\n- `GET /api/v1/conversation/{app_conversation_id}/events/count` — count events\n- `GET /api/v1/sandboxes/search?limit=...` — list sandboxes\n- `POST /api/v1/sandboxes/{sandbox_id}/pause` / `.../resume` — manage sandbox lifecycle\n- `GET /api/v1/app-conversations/{app_conversation_id}/download` — download trajectory zip\n\n## Delegating work with additional Cloud conversations\n\nUse the Cloud API when you want a **separate OpenHands conversation** with its own fresh context window.\nThis is useful for:\n\n- background jobs that can run independently\n- parallel investigations or implementation tasks\n- long-running work where you want to keep the current conversation focused\n- task-specific contexts, such as one conversation building a component while another runs tests\n\n### Delegation checklist\n\nWhen you start a delegated Cloud conversation:\n\n1. Write a **self-contained task description**. Do not assume the new conversation has any context from the current one.\n2. Include the **repository**, branch, relevant file paths, constraints, and expected output.\n3. Start the new conversation with `POST /api/v1/app-conversations`.\n4. Poll the start-task until `status` is `READY` and you have an `app_conversation_id`.\n5. Monitor the delegated conversation via `GET /api/v1/app-conversations?ids=...`.\n6. Share or store the Cloud URL: `https://app.all-hands.dev/conversations/<app_conversation_id>`.\n\n### Minimal cURL flow\n\n```bash\ncurl -X POST \"https://app.all-hands.dev/api/v1/app-conversations\" \\\n -H \"Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"initial_message\": {\n \"content\": [{\"type\": \"text\", \"text\": \"Investigate flaky tests in tests/test_api.py. Report the root cause and propose a fix.\"}]\n },\n \"selected_repository\": \"owner/repo\"\n }'\n```\n\nIf the response does not already include `app_conversation_id`, poll the start-task:\n\n```bash\ncurl -s \"https://app.all-hands.dev/api/v1/app-conversations/start-tasks?ids=${START_TASK_ID}\" \\\n -H \"Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY}\"\n```\n\nThen check execution status:\n\n```bash\ncurl -s \"https://app.all-hands.dev/api/v1/app-conversations?ids=${APP_CONVERSATION_ID}\" \\\n -H \"Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY}\"\n```\n\n### Minimal Python flow\n\n```python\nfrom openhands_api import OpenHandsAPI\n\napi = OpenHandsAPI() # prefers OPENHANDS_CLOUD_API_KEY\n\nstart = api.app_conversation_start(\n initial_message=(\n \"Implement the requested dashboard component in src/dashboard.tsx. \"\n \"Update any related tests and summarize the changes.\"\n ),\n selected_repository=\"owner/repo\",\n selected_branch=\"main\",\n title=\"Dashboard component task\",\n)\n\nready = start\nif not ready.get(\"app_conversation_id\"):\n ready = api.poll_start_task_until_ready(start[\"id\"])\n\nconversation_id = ready[\"app_conversation_id\"]\nprint(f\"Delegated conversation: {api.base_url}/conversations/{conversation_id}\")\n\nstatus = api.app_conversation_get(conversation_id)\nprint(status.get(\"sandbox_status\"), status.get(\"execution_status\"))\n\napi.close()\n```\n\n### Parallelism guidance\n\n- Prefer **5 or fewer** concurrently running delegated conversations.\n- Before starting more, check recent conversations and count how many are still `execution_status == \"running\"`.\n- Batch specific conversation lookups with `GET /api/v1/app-conversations?ids=...` when you already know their ids.\n\nExample:\n\n```python\nitems = api.app_conversations_search(limit=50).get(\"items\", [])\nrunning = [item for item in items if item.get(\"execution_status\") == \"running\"]\nif len(running) >= 5:\n print(\"Wait for some delegated conversations to finish before starting more.\")\n```\n\n\n### Start-task vs `app_conversation_id` (common pitfall)\n\nIn many deployments, `POST /api/v1/app-conversations` is **asynchronous** and returns a **start-task** object:\n\n- `id` is the **start_task_id**\n- `app_conversation_id` is the id you should use for conversation operations like:\n - `GET /api/v1/app-conversations/{app_conversation_id}/download`\n - `GET /api/v1/conversation/{app_conversation_id}/events/...`\n\nIf `app_conversation_id` is not present in the initial response, fetch it via:\n\n- `GET /api/v1/app-conversations/start-tasks?ids=<start_task_id>`\n\nIf you pass a **start_task_id** to `/download`, you will get `404 Not Found`.\n\n## Common agent server endpoints\n\nThese run against `agent_server_url` (not the app server):\n\n- `POST {agent_server_url}/api/bash/execute_bash_command`\n- `GET {agent_server_url}/api/file/download/<absolute_path>`\n- `POST {agent_server_url}/api/file/upload/<absolute_path>` (multipart)\n- `GET {agent_server_url}/api/conversations/{conversation_id}/events/search`\n- `GET {agent_server_url}/api/conversations/{conversation_id}/events/count`\n\n### Counting events (recommended approach)\n\nIf you need to know how many events a conversation has, you can:\n\n1. **App server count (fastest when working)**\n - `GET /api/v1/conversation/{app_conversation_id}/events/count`\n2. **Agent server count (reliable fallback)**\n - `GET {agent_server_url}/api/conversations/{app_conversation_id}/events/count`\n3. **Trajectory zip fallback (heavier, but still one call + gives full payloads)**\n - `GET /api/v1/app-conversations/{app_conversation_id}/download`\n - Unzip and count `event_*.json` files\n\nDo **not** rely on the last event `id` to infer the total number of events.\nIn the agent-server API, event IDs are UUIDs (not monotonically increasing integers).\n\n## Troubleshooting\n\nFor common issues and solutions, see [TROUBLESHOOTING.md](references/TROUBLESHOOTING.md).\n\n## Event structure (for debugging)\n\nEvents returned by:\n\n- app server: `GET /api/v1/conversation/{id}/events/search`\n- agent server: `GET {agent_server_url}/api/conversations/{id}/events/search`\n\n…share the same high-level shape.\n\nEach event typically includes:\n\n- `id` (UUID)\n- `timestamp`\n- `kind`\n- `source`\n\nCommon `kind` values:\n\n| kind | source (typical) | key fields (common) | purpose |\n|---|---|---|---|\n| `ActionEvent` | `agent` | `tool_name`, `tool_call_id`, `action` | tool call requested by the agent |\n| `ObservationEvent` | `environment` | `tool_name`, `tool_call_id`, `action_id`, `observation` | tool result produced by the sandbox/environment |\n| `MessageEvent` | `user` / `assistant` | `message` (or similar) | user/assistant chat messages |\n| `ConversationStateUpdateEvent` | `environment` | `key`, `value` | state transitions/metadata |\n\nLinking tool calls:\n\n- `ActionEvent.tool_call_id` == `ObservationEvent.tool_call_id`\n- `ObservationEvent.action_id` == `ActionEvent.id`\n\nExample (simplified):\n\n```json\n{\n \"id\": \"<action-event-uuid>\",\n \"kind\": \"ActionEvent\",\n \"source\": \"agent\",\n \"tool_name\": \"terminal\",\n \"tool_call_id\": \"toolu_...\",\n \"action\": {\"command\": \"ls\"}\n}\n```\n\n```json\n{\n \"id\": \"<observation-event-uuid>\",\n \"kind\": \"ObservationEvent\",\n \"source\": \"environment\",\n \"tool_name\": \"terminal\",\n \"tool_call_id\": \"toolu_...\",\n \"action_id\": \"<action-event-uuid>\",\n \"observation\": {\"exit_code\": 0, \"stdout\": \"...\"}\n}\n```\n\n## Debugging one-liners (events)\n\nThese assume you're querying the **app server** endpoint. For agent-server queries, swap the URL base + use `X-Session-API-Key`.\n\n### Print a quick timeline\n\n```bash\ncurl -s \"${BASE_URL:-https://app.all-hands.dev}/api/v1/conversation/${APP_CONVERSATION_ID}/events/search?limit=100\" \\\n -H \"Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY:-$OPENHANDS_API_KEY}\" \\\n -H \"Accept: application/json\" | \\\npython3 - <<'PY'\nimport json, sys\nitems = (json.load(sys.stdin) or {}).get(\"items\", [])\nfor i, e in enumerate(items):\n print(f\"{i:04d} {e.get('timestamp','')} {e.get('source','')} {e.get('kind','')}\")\nPY\n```\n\n### Find error-like events\n\n```bash\ncurl -s \"${BASE_URL:-https://app.all-hands.dev}/api/v1/conversation/${APP_CONVERSATION_ID}/events/search?limit=200\" \\\n -H \"Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY:-$OPENHANDS_API_KEY}\" \\\n -H \"Accept: application/json\" | \\\npython3 - <<'PY'\nimport json, sys\nitems = (json.load(sys.stdin) or {}).get(\"items\", [])\nfor i, e in enumerate(items):\n if e.get(\"kind\") == \"ErrorEvent\" or (\"code\" in e and \"detail\" in e):\n print(i, e.get(\"kind\"), e.get(\"code\"), str(e.get(\"detail\", \"\"))[:400])\nPY\n```\n\n### Check tool-call matching (unmatched actions / duplicate observations)\n\n```bash\ncurl -s \"${BASE_URL:-https://app.all-hands.dev}/api/v1/conversation/${APP_CONVERSATION_ID}/events/search?limit=200\" \\\n -H \"Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY:-$OPENHANDS_API_KEY}\" \\\n -H \"Accept: application/json\" | \\\npython3 - <<'PY'\nimport json, sys\nfrom collections import Counter\nitems = (json.load(sys.stdin) or {}).get(\"items\", [])\naction_ids = {e.get(\"id\") for e in items if e.get(\"kind\") == \"ActionEvent\"}\nobs_action_ids = [e.get(\"action_id\") for e in items if e.get(\"kind\") == \"ObservationEvent\" and e.get(\"action_id\")]\nobserved = set(obs_action_ids)\nprint(\"actions:\", len(action_ids))\nprint(\"observations:\", len(observed))\nunmatched = action_ids - observed\nprint(\"unmatched actions:\", list(unmatched)[:20] if unmatched else \"none\")\ndups = [aid for aid, c in Counter(obs_action_ids).items() if c > 1]\nprint(\"duplicate observation action_ids:\", list(dups)[:20] if dups else \"none\")\nPY\n```\n\n\n## Quick start (Python)\n\n```python\n# Copy `skills/openhands-api/scripts/openhands_api.py` into your project (e.g. as `openhands_api.py`),\n# then import it normally:\nfrom openhands_api import OpenHandsAPI\n\napi = OpenHandsAPI() # prefers OPENHANDS_CLOUD_API_KEY\n\nme = api.users_me()\nprint(me)\n\nrecent = api.app_conversations_search(limit=5)\nprint(recent)\n\napi.close()\n```\n\n## CLI examples\n\nSearch conversations:\n\n```bash\nexport OPENHANDS_CLOUD_API_KEY=\"...\"\npython skills/openhands-api/scripts/openhands_api.py search-conversations --limit 5\n```\n\nStart a conversation from a prompt file:\n\n```bash\npython skills/openhands-api/scripts/openhands_api.py start-conversation \\\n --prompt-file skills/openhands-api/references/example_prompt.md \\\n --repo owner/repo \\\n --branch main\n```\n\n## Notes for AI agents extending this client\n\n- Prefer `.../search` endpoints with a small `limit`.\n- Avoid loops that could generate many API calls.\n- Start conversations only when asked: it may create sandboxes and cost money.\n- For sandbox file operations and command execution, use the agent server endpoints with `X-Session-API-Key`.\n\nSee also:\n- `skills/openhands-api/scripts/openhands_api.py`\n- The original inspiration client: `enyst/llm-playground` → `openhands-api-client-v1/scripts/cloud_api_v1.py`\n- Troubleshooting content and real-world usage feedback → `https://github.com/jpshackelford/.openhands/tree/main/skills/openhands-cloud-api`\n\n## Source of truth\n\nThis skill is aligned against the current OpenHands API docs and implementation:\n\n- `OpenHands/docs/openhands/usage/cloud/cloud-api.mdx`\n- `OpenHands/docs/openhands/usage/agent-canvas/backend-setup/local.mdx`\n- `OpenHands/docs/sdk/arch/agent-server.mdx`\n- `OpenHands/docs/openhands/usage/api/v1.mdx`\n- `OpenHands/OpenHands/openhands/app_server/v1_router.py`\n- `OpenHands/OpenHands/openhands/app_server/app_conversation/app_conversation_router.py`\n- `OpenHands/OpenHands/openhands/app_server/app_conversation/app_conversation_models.py`"
283
+ content: "This skill documents the **OpenHands Cloud API** (V1), commonly used **agent-server APIs**, and small, easy-to-copy clients.\nWindows PowerShell equivalents for the shell examples in this skill are in `references/windows.md`.\n\nIt is intentionally focused on common OpenHands API workflows:\n\n- Defaults to OpenHands Cloud (`https://app.all-hands.dev`).\n- Targets the **V1 app server REST API** under `/api/v1/...`.\n- Includes a few **agent server** endpoints (inside a sandbox) that use `X-Session-API-Key`.\n- Covers the **multi-conversation delegation pattern**: start separate Cloud conversations when you want fresh context windows or background work.\n- Covers **local Agent Canvas backend conversations**: start or inspect conversations by calling a local agent server directly.\n\n## When to use this skill\n\nUse this skill when you need to:\n\n- start or inspect OpenHands Cloud conversations from code\n- monitor async startup via start-task polling\n- monitor execution status for long-running jobs\n- create separate Cloud conversations for parallel or background work\n- access sandbox agent-server endpoints once a conversation is running\n- start or inspect conversations on a local Agent Canvas backend or local agent server\n\n## Auth\n\n### App server (Cloud)\n\nUse Bearer auth:\n\n- Header: `Authorization: Bearer <OPENHANDS_CLOUD_API_KEY>`\n- Preferred env var: `OPENHANDS_CLOUD_API_KEY`\n- Backward-compatible env var: `OPENHANDS_API_KEY`\n\n### Agent server (inside a sandbox)\n\nUse session auth:\n\n- Header: `X-Session-API-Key: <session_api_key>`\n\nHow to obtain `agent_server_url` and `session_api_key`:\n\n1. Start or fetch an app conversation via the app server (Bearer auth), e.g.:\n - `POST /api/v1/app-conversations`\n - or `GET /api/v1/app-conversations?ids=<conversation_id>`\n2. In the returned JSON, look for sandbox/runtime connection fields (names vary slightly by deployment/version). Common patterns:\n - a sandbox object containing `agent_server_url` (or similar)\n - a session key such as `session_api_key` (or similar)\n3. Use those values to call the agent server directly:\n - Base: `{agent_server_url}/api/...`\n - Header: `X-Session-API-Key: <session_api_key>`\n\nExample (common field names; adjust to your deployment):\n\n```python\n# using the minimal Python client (`OpenHandsAPI`)\nconv = api.app_conversation_get(app_conversation_id)\n\nsession_api_key = conv.get(\"session_api_key\")\nconversation_url = conv.get(\"conversation_url\", \"\")\n\n# `conversation_url` often looks like: https://<runtime-host>/api/conversations/<id>\nagent_server_url = conversation_url.rsplit(\"/api/conversations\", 1)[0]\n```\n\n\nIf those fields are not present on the conversation record, list/search sandboxes (`GET /api/v1/sandboxes/search`) and use the sandbox referenced by the conversation to locate the agent server URL + session key.\n\n### Local Agent Canvas backend\n\nUse the local backend flow only for local Agent Canvas / agent-server development, such as `agent-canvas`, `agent-canvas --backend-only`, or `npm run dev` with ingress at `http://localhost:8000`. This calls the agent server directly with `X-Session-API-Key`. It is not an automation, and it is different from OpenHands Cloud delegation through `POST /api/v1/app-conversations`, which uses Bearer auth against the Cloud app API and may return asynchronous start-task records.\n\nWhen Agent Canvas runs locally, the launcher uses `LOCAL_BACKEND_API_KEY` when it is set. Otherwise it generates and persists the session API key at `~/.openhands/agent-canvas/api-key.txt`. Set `OH_SESSION_API_KEY_PATH` to override the persisted key path. Never print, log, or paste the actual key; use command substitution or an environment variable in examples and scripts.\n\n```bash\nLOCAL_AGENT_SERVER_URL=\"${LOCAL_AGENT_SERVER_URL:-http://localhost:8000}\"\nSESSION_API_KEY=\"${LOCAL_BACKEND_API_KEY:-$(cat \"${OH_SESSION_API_KEY_PATH:-$HOME/.openhands/agent-canvas/api-key.txt}\")}\"\n```\n\nCheck the local server before creating a backend conversation:\n\n```bash\ncurl -sS \"${LOCAL_AGENT_SERVER_URL}/server_info\" \\\n -H \"X-Session-API-Key: ${SESSION_API_KEY}\"\n```\n\nStart a backend conversation with `POST /api/conversations`. Include the agent settings and workspace expected by that backend. Local agent-server calls use an explicit `workspace` such as `{\"kind\": \"LocalWorkspace\", \"working_dir\": \"/workspace\"}`; Cloud app-conversation delegation instead uses app-server fields such as `selected_repository` and `selected_branch`. If you are starting the conversation from an existing Agent Canvas session, pass through the current configured settings or encrypted settings rather than hard-coding secrets into scripts.\n\n```bash\nCONVERSATION_JSON=$(curl -sS -X POST \"${LOCAL_AGENT_SERVER_URL}/api/conversations\" \\\n -H \"X-Session-API-Key: ${SESSION_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d @- <<'JSON'\n{\n \"agent\": {\n \"kind\": \"Agent\",\n \"llm\": {\n \"model\": \"your-model-provider/your-model-name\",\n \"api_key\": \"**********\"\n },\n \"tools\": [\n {\"name\": \"terminal\"},\n {\"name\": \"file_editor\"},\n {\"name\": \"task_tracker\"}\n ]\n },\n \"workspace\": {\"kind\": \"LocalWorkspace\", \"working_dir\": \"/workspace\"},\n \"initial_message\": {\n \"content\": [{\"text\": \"Summarize the current workspace.\"}],\n \"run\": true\n }\n}\nJSON\n)\nCONVERSATION_ID=$(python3 -c 'import json,sys; print(json.load(sys.stdin)[\"id\"])' <<<\"${CONVERSATION_JSON}\")\nprintf 'Conversation: %s/api/conversations/%s\\n' \"${LOCAL_AGENT_SERVER_URL}\" \"${CONVERSATION_ID}\"\n```\n\nPoll status and inspect recent events:\n\n```bash\ncurl -sS \"${LOCAL_AGENT_SERVER_URL}/api/conversations/${CONVERSATION_ID}\" \\\n -H \"X-Session-API-Key: ${SESSION_API_KEY}\"\n\ncurl -sS \"${LOCAL_AGENT_SERVER_URL}/api/conversations/${CONVERSATION_ID}/events/search?limit=20&sort_order=TIMESTAMP_DESC\" \\\n -H \"X-Session-API-Key: ${SESSION_API_KEY}\"\n```\n\nIf the same base URL serves the Agent Canvas UI, the browser route is:\n\n```bash\nprintf '%s/conversations/%s\\n' \"${LOCAL_AGENT_SERVER_URL}\" \"${CONVERSATION_ID}\"\n```\n\n## Common V1 app server endpoints\n\nThe following are the main endpoints implemented in the minimal client:\n\n- `GET /api/v1/users/me` — validate auth and inspect current account\n- `GET /api/v1/app-conversations/search?limit=...` — list recent conversations\n- `GET /api/v1/app-conversations?ids=...` — fetch conversation records by id (batch)\n- `GET /api/v1/app-conversations/count` — count conversations\n- `POST /api/v1/app-conversations` — start a new conversation (creates a sandbox)\n- `GET /api/v1/app-conversations/start-tasks?ids=...` — check async start-task status\n- `GET /api/v1/conversation/{app_conversation_id}/events/search?limit=...` — read conversation events\n- `GET /api/v1/conversation/{app_conversation_id}/events/count` — count events\n- `GET /api/v1/sandboxes/search?limit=...` — list sandboxes\n- `POST /api/v1/sandboxes/{sandbox_id}/pause` / `.../resume` — manage sandbox lifecycle\n- `GET /api/v1/app-conversations/{app_conversation_id}/download` — download trajectory zip\n\n## Delegating work with additional Cloud conversations\n\nUse the Cloud API when you want a **separate OpenHands conversation** with its own fresh context window.\nThis is useful for:\n\n- background jobs that can run independently\n- parallel investigations or implementation tasks\n- long-running work where you want to keep the current conversation focused\n- task-specific contexts, such as one conversation building a component while another runs tests\n\n### Delegation checklist\n\nWhen you start a delegated Cloud conversation:\n\n1. Write a **self-contained task description**. Do not assume the new conversation has any context from the current one.\n2. Include the **repository**, branch, relevant file paths, constraints, and expected output.\n3. Start the new conversation with `POST /api/v1/app-conversations`.\n4. Poll the start-task until `status` is `READY` and you have an `app_conversation_id`.\n5. Monitor the delegated conversation via `GET /api/v1/app-conversations?ids=...`.\n6. Share or store the Cloud URL: `https://app.all-hands.dev/conversations/<app_conversation_id>`.\n\n### Minimal cURL flow\n\n```bash\ncurl -X POST \"https://app.all-hands.dev/api/v1/app-conversations\" \\\n -H \"Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"initial_message\": {\n \"content\": [{\"type\": \"text\", \"text\": \"Investigate flaky tests in tests/test_api.py. Report the root cause and propose a fix.\"}]\n },\n \"selected_repository\": \"owner/repo\"\n }'\n```\n\nIf the response does not already include `app_conversation_id`, poll the start-task:\n\n```bash\ncurl -s \"https://app.all-hands.dev/api/v1/app-conversations/start-tasks?ids=${START_TASK_ID}\" \\\n -H \"Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY}\"\n```\n\nThen check execution status:\n\n```bash\ncurl -s \"https://app.all-hands.dev/api/v1/app-conversations?ids=${APP_CONVERSATION_ID}\" \\\n -H \"Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY}\"\n```\n\n### Minimal Python flow\n\n```python\nfrom openhands_api import OpenHandsAPI\n\napi = OpenHandsAPI() # prefers OPENHANDS_CLOUD_API_KEY\n\nstart = api.app_conversation_start(\n initial_message=(\n \"Implement the requested dashboard component in src/dashboard.tsx. \"\n \"Update any related tests and summarize the changes.\"\n ),\n selected_repository=\"owner/repo\",\n selected_branch=\"main\",\n title=\"Dashboard component task\",\n)\n\nready = start\nif not ready.get(\"app_conversation_id\"):\n ready = api.poll_start_task_until_ready(start[\"id\"])\n\nconversation_id = ready[\"app_conversation_id\"]\nprint(f\"Delegated conversation: {api.base_url}/conversations/{conversation_id}\")\n\nstatus = api.app_conversation_get(conversation_id)\nprint(status.get(\"sandbox_status\"), status.get(\"execution_status\"))\n\napi.close()\n```\n\n### Parallelism guidance\n\n- Prefer **5 or fewer** concurrently running delegated conversations.\n- Before starting more, check recent conversations and count how many are still `execution_status == \"running\"`.\n- Batch specific conversation lookups with `GET /api/v1/app-conversations?ids=...` when you already know their ids.\n\nExample:\n\n```python\nitems = api.app_conversations_search(limit=50).get(\"items\", [])\nrunning = [item for item in items if item.get(\"execution_status\") == \"running\"]\nif len(running) >= 5:\n print(\"Wait for some delegated conversations to finish before starting more.\")\n```\n\n\n### Start-task vs `app_conversation_id` (common pitfall)\n\nIn many deployments, `POST /api/v1/app-conversations` is **asynchronous** and returns a **start-task** object:\n\n- `id` is the **start_task_id**\n- `app_conversation_id` is the id you should use for conversation operations like:\n - `GET /api/v1/app-conversations/{app_conversation_id}/download`\n - `GET /api/v1/conversation/{app_conversation_id}/events/...`\n\nIf `app_conversation_id` is not present in the initial response, fetch it via:\n\n- `GET /api/v1/app-conversations/start-tasks?ids=<start_task_id>`\n\nIf you pass a **start_task_id** to `/download`, you will get `404 Not Found`.\n\n## Common agent server endpoints\n\nThese run against `agent_server_url` (not the app server):\n\n- `POST {agent_server_url}/api/bash/execute_bash_command`\n- `GET {agent_server_url}/api/file/download/<absolute_path>`\n- `POST {agent_server_url}/api/file/upload/<absolute_path>` (multipart)\n- `GET {agent_server_url}/api/conversations/{conversation_id}/events/search`\n- `GET {agent_server_url}/api/conversations/{conversation_id}/events/count`\n\n### Counting events (recommended approach)\n\nIf you need to know how many events a conversation has, you can:\n\n1. **App server count (fastest when working)**\n - `GET /api/v1/conversation/{app_conversation_id}/events/count`\n2. **Agent server count (reliable fallback)**\n - `GET {agent_server_url}/api/conversations/{app_conversation_id}/events/count`\n3. **Trajectory zip fallback (heavier, but still one call + gives full payloads)**\n - `GET /api/v1/app-conversations/{app_conversation_id}/download`\n - Unzip and count `event_*.json` files\n\nDo **not** rely on the last event `id` to infer the total number of events.\nIn the agent-server API, event IDs are UUIDs (not monotonically increasing integers).\n\n## Troubleshooting\n\nFor common issues and solutions, see [TROUBLESHOOTING.md](references/TROUBLESHOOTING.md).\n\n## Event structure (for debugging)\n\nEvents returned by:\n\n- app server: `GET /api/v1/conversation/{id}/events/search`\n- agent server: `GET {agent_server_url}/api/conversations/{id}/events/search`\n\n…share the same high-level shape.\n\nEach event typically includes:\n\n- `id` (UUID)\n- `timestamp`\n- `kind`\n- `source`\n\nCommon `kind` values:\n\n| kind | source (typical) | key fields (common) | purpose |\n|---|---|---|---|\n| `ActionEvent` | `agent` | `tool_name`, `tool_call_id`, `action` | tool call requested by the agent |\n| `ObservationEvent` | `environment` | `tool_name`, `tool_call_id`, `action_id`, `observation` | tool result produced by the sandbox/environment |\n| `MessageEvent` | `user` / `assistant` | `message` (or similar) | user/assistant chat messages |\n| `ConversationStateUpdateEvent` | `environment` | `key`, `value` | state transitions/metadata |\n\nLinking tool calls:\n\n- `ActionEvent.tool_call_id` == `ObservationEvent.tool_call_id`\n- `ObservationEvent.action_id` == `ActionEvent.id`\n\nExample (simplified):\n\n```json\n{\n \"id\": \"<action-event-uuid>\",\n \"kind\": \"ActionEvent\",\n \"source\": \"agent\",\n \"tool_name\": \"terminal\",\n \"tool_call_id\": \"toolu_...\",\n \"action\": {\"command\": \"ls\"}\n}\n```\n\n```json\n{\n \"id\": \"<observation-event-uuid>\",\n \"kind\": \"ObservationEvent\",\n \"source\": \"environment\",\n \"tool_name\": \"terminal\",\n \"tool_call_id\": \"toolu_...\",\n \"action_id\": \"<action-event-uuid>\",\n \"observation\": {\"exit_code\": 0, \"stdout\": \"...\"}\n}\n```\n\n## Debugging one-liners (events)\n\nThese assume you're querying the **app server** endpoint. For agent-server queries, swap the URL base + use `X-Session-API-Key`.\n\n### Print a quick timeline\n\n```bash\ncurl -s \"${BASE_URL:-https://app.all-hands.dev}/api/v1/conversation/${APP_CONVERSATION_ID}/events/search?limit=100\" \\\n -H \"Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY:-$OPENHANDS_API_KEY}\" \\\n -H \"Accept: application/json\" | \\\npython3 - <<'PY'\nimport json, sys\nitems = (json.load(sys.stdin) or {}).get(\"items\", [])\nfor i, e in enumerate(items):\n print(f\"{i:04d} {e.get('timestamp','')} {e.get('source','')} {e.get('kind','')}\")\nPY\n```\n\n### Find error-like events\n\n```bash\ncurl -s \"${BASE_URL:-https://app.all-hands.dev}/api/v1/conversation/${APP_CONVERSATION_ID}/events/search?limit=200\" \\\n -H \"Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY:-$OPENHANDS_API_KEY}\" \\\n -H \"Accept: application/json\" | \\\npython3 - <<'PY'\nimport json, sys\nitems = (json.load(sys.stdin) or {}).get(\"items\", [])\nfor i, e in enumerate(items):\n if e.get(\"kind\") == \"ErrorEvent\" or (\"code\" in e and \"detail\" in e):\n print(i, e.get(\"kind\"), e.get(\"code\"), str(e.get(\"detail\", \"\"))[:400])\nPY\n```\n\n### Check tool-call matching (unmatched actions / duplicate observations)\n\n```bash\ncurl -s \"${BASE_URL:-https://app.all-hands.dev}/api/v1/conversation/${APP_CONVERSATION_ID}/events/search?limit=200\" \\\n -H \"Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY:-$OPENHANDS_API_KEY}\" \\\n -H \"Accept: application/json\" | \\\npython3 - <<'PY'\nimport json, sys\nfrom collections import Counter\nitems = (json.load(sys.stdin) or {}).get(\"items\", [])\naction_ids = {e.get(\"id\") for e in items if e.get(\"kind\") == \"ActionEvent\"}\nobs_action_ids = [e.get(\"action_id\") for e in items if e.get(\"kind\") == \"ObservationEvent\" and e.get(\"action_id\")]\nobserved = set(obs_action_ids)\nprint(\"actions:\", len(action_ids))\nprint(\"observations:\", len(observed))\nunmatched = action_ids - observed\nprint(\"unmatched actions:\", list(unmatched)[:20] if unmatched else \"none\")\ndups = [aid for aid, c in Counter(obs_action_ids).items() if c > 1]\nprint(\"duplicate observation action_ids:\", list(dups)[:20] if dups else \"none\")\nPY\n```\n\n\n## Quick start (Python)\n\n```python\n# Copy `skills/openhands-api/scripts/openhands_api.py` into your project (e.g. as `openhands_api.py`),\n# then import it normally:\nfrom openhands_api import OpenHandsAPI\n\napi = OpenHandsAPI() # prefers OPENHANDS_CLOUD_API_KEY\n\nme = api.users_me()\nprint(me)\n\nrecent = api.app_conversations_search(limit=5)\nprint(recent)\n\napi.close()\n```\n\n## CLI examples\n\nSearch conversations:\n\n```bash\nexport OPENHANDS_CLOUD_API_KEY=\"...\"\npython skills/openhands-api/scripts/openhands_api.py search-conversations --limit 5\n```\n\nStart a conversation from a prompt file:\n\n```bash\npython skills/openhands-api/scripts/openhands_api.py start-conversation \\\n --prompt-file skills/openhands-api/references/example_prompt.md \\\n --repo owner/repo \\\n --branch main\n```\n\n## Notes for AI agents extending this client\n\n- Prefer `.../search` endpoints with a small `limit`.\n- Avoid loops that could generate many API calls.\n- Start conversations only when asked: it may create sandboxes and cost money.\n- For sandbox file operations and command execution, use the agent server endpoints with `X-Session-API-Key`.\n\nSee also:\n- `skills/openhands-api/scripts/openhands_api.py`\n- The original inspiration client: `enyst/llm-playground` → `openhands-api-client-v1/scripts/cloud_api_v1.py`\n- Troubleshooting content and real-world usage feedback → `https://github.com/jpshackelford/.openhands/tree/main/skills/openhands-cloud-api`\n\n## Source of truth\n\nThis skill is aligned against the current OpenHands API docs and implementation:\n\n- `OpenHands/docs/openhands/usage/cloud/cloud-api.mdx`\n- `OpenHands/docs/openhands/usage/agent-canvas/backend-setup/local.mdx`\n- `OpenHands/docs/sdk/arch/agent-server.mdx`\n- `OpenHands/docs/openhands/usage/api/v1.mdx`\n- `OpenHands/OpenHands/openhands/app_server/v1_router.py`\n- `OpenHands/OpenHands/openhands/app_server/app_conversation/app_conversation_router.py`\n- `OpenHands/OpenHands/openhands/app_server/app_conversation/app_conversation_models.py`"
278
284
  },
279
285
  {
280
286
  name: "openhands-automation",
@@ -293,7 +299,7 @@ var e = [
293
299
  "issue automation",
294
300
  "/automation:create"
295
301
  ],
296
- content: "# OpenHands Automations\n\nCreate and manage automations that run inside an OpenHands agent server — triggered by cron schedules or webhook events (GitHub, custom services).\n\n## Automation Creation Process\nThe agent must follow these steps when creating an automation:\n* Quickly check that you can access the correct automations backend using the auth mechanism below\n* Quickly check that you can access any necessary integrations (e.g. GitHub, Slack); if access fails, inform the user and stop\n* Ask the user for any necessary information, e.g. if you need the name of a Slack channel or GitHub repo to proceed\n* Write the code or prompt that will be sent to the automations backend _inside the current workspace_\n* Show the code to the user with the `canvas_ui` tool if available, otherwise present it in a fenced code block in your reply\n* Message the user with a concise summary of how the automation will behave, and ask if they are ready to deploy it\n\n## Architecture\n\nTwo components work together to run automations:\n\n**Automation Service** (API at `OPENHANDS_HOST/api/automation/v1`)\nManages the *when*: holds automation definitions, schedules cron-triggered runs, dispatches webhook-triggered runs, and receives completion callbacks to mark runs as done. This is the API you call to create, update, and manage automations.\n\n**Agent Server** (accessible as `AGENT_SERVER_URL` inside script runs)\nManages the *what*: the runtime environment where automation scripts execute and where conversations (AI agent interactions with tools, bash, file editing, etc.) run. When a run is triggered, the automation service uploads the automation's tarball to the agent server, which unpacks and runs the entrypoint script. The script connects back to the agent server using `AGENT_SERVER_URL` and a session API key to start, monitor, and stop conversations.\n\nThe agent server typically runs inside a **sandbox** (a Docker or Kubernetes container). Some deployments use sandboxless mode, where the agent server runs directly on a host.\n\n**Key environment variables:**\n\n| Variable | Availability | Description |\n|---|---|---|\n| `RUNTIME_URL` | Ambient in cloud environments | Public-facing URL of the **agent server** sandbox. Use this to determine whether external webhook delivery is possible — if unset or local, webhooks cannot be received. The automation service may run at a separate URL (see Determining the API Host). |\n| `AGENT_SERVER_URL` | Injected into scripts at run time only | Internal URL of the agent server. Available inside script execution context; **not** an ambient environment variable outside of a running script. |\n| `OPENHANDS_HOST` | Shell convention only — set manually | Base URL for the automation service API. **Not a real environment variable.** Set it from the `<HOST>` system-prompt value, or default to `https://app.all-hands.dev`. Used in all `curl` examples throughout this skill. |\n\n> **⚠️ CRITICAL — Agent behavior rules:**\n>\n> 0. **Does this task need an LLM at all? Check first.** Before picking a preset, ask whether the task actually requires reasoning, judgment, summarization, or open-ended tool use. If it is fully deterministic — fixed data transforms, scheduled HTTP calls, healthcheck pings, file rotation, picking from a known list, posting a templated message — an LLM-driven preset is overkill. Every run will consume LLM tokens, which adds up fast at high frequencies (every 5 min ≈ 288 runs/day). Surface the trade-off to the user and offer the custom-script path (see `references/custom-automation.md`) as the cheaper, more reliable option. Be especially careful for cron schedules tighter than hourly.\n>\n> **Instant-recognition patterns — these are always deterministic, never use an LLM preset:**\n> - \"post a quote / message / fact every N minutes\" (rotating from a list)\n> - \"send a scheduled reminder / standup / digest\"\n> - \"ping a health-check URL on a schedule\"\n> - \"post to Slack / webhook every N minutes\"\n> - Any task where the full output could be written as a static template right now\n>\n> 1. **For LLM-appropriate work, default to preset endpoints.** They handle all SDK boilerplate, tarball packaging, and upload automatically:\n> - **Prompt preset** (`POST /v1/preset/prompt`) — for tasks expressed as a natural language prompt that benefit from agent reasoning\n> - **Plugin preset** (`POST /v1/preset/plugin`) — when plugins with skills, MCP configs, or commands are needed\n> 2. **Do not silently create custom scripts.** Do not generate Python code, `setup.sh` files, or tarball uploads without user consent. But *do* proactively recommend the custom path (per rule 0) when the task is deterministic or high-frequency — surface the option and let the user choose.\n> 3. **If neither preset is the right fit**, do NOT silently fall back to custom automation. Instead, explain the available options to the user:\n> - **Prompt preset** — natural language prompt execution (LLM-driven)\n> - **Plugin preset** — load plugins with extended capabilities (skills, MCP, hooks, commands)\n> - **Custom script** — full control over code, with or without LLM; point them to `references/custom-automation.md`\n> - Let the user choose which approach to use.\n> 4. **Only create custom scripts after the user agrees to that path.** Refer to `references/custom-automation.md` for the full reference.\n> 5. **Before suggesting event-triggered (webhook) automations, check whether the deployment is publicly reachable.** Check `RUNTIME_URL`. Webhooks require an internet-accessible URL so that external services (GitHub, Slack, Linear, etc.) can deliver events to the automation service. If `RUNTIME_URL` is unset, empty, or resolves to a local or private address (`localhost`, `127.0.0.1`, `0.0.0.0`, or any RFC 1918 range: `10.x.x.x`, `192.168.x.x`, `172.16–31.x.x`), the service cannot receive inbound webhook traffic from the public internet. In that case:\n> - **Recommend a cron-based polling automation instead.** Have the automation run on a schedule and call the external service's API (e.g., the GitHub REST API) to check for new events since the last run.\n> - Explain the limitation clearly to the user: \"Because this is a local deployment, external services can't reach the webhook endpoint. I'll set up a polling automation using a cron schedule instead.\"\n\n### No-LLM Script Helpers\n\nWhen building a deterministic custom script, these two stdlib-only functions are required. Copy them verbatim — they use `AGENT_SERVER_URL` and `SESSION_API_KEY` injected by the automation service.\n\n```python\nimport json, os, urllib.request\n\ndef get_secret(name):\n \"\"\"Fetch a named secret stored in the agent server.\"\"\"\n url = os.environ.get(\"AGENT_SERVER_URL\", \"\").rstrip(\"/\")\n key = os.environ.get(\"SESSION_API_KEY\") or os.environ.get(\"OH_SESSION_API_KEYS_0\", \"\")\n with urllib.request.urlopen(urllib.request.Request(\n f\"{url}/api/settings/secrets/{name}\", headers={\"X-Session-API-Key\": key}\n )) as r:\n return r.read().decode().strip()\n\ndef fire_callback(status=\"COMPLETED\", error=None):\n \"\"\"Signal run completion. MUST be called on every exit path — success AND error.\"\"\"\n url = os.environ.get(\"AUTOMATION_CALLBACK_URL\", \"\")\n if not url: return\n body = {\"status\": status, \"run_id\": os.environ.get(\"AUTOMATION_RUN_ID\", \"\")}\n if error: body[\"error\"] = error\n try:\n urllib.request.urlopen(urllib.request.Request(url, data=json.dumps(body).encode(), headers={\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {os.environ.get('AUTOMATION_CALLBACK_API_KEY', '')}\",\n }))\n except Exception as e: print(f\"Callback error: {e}\")\n```\n\nEntrypoint must be `python3 main.py` (no `setup.sh` needed). Wrap your main logic in `try/except` and call `fire_callback(\"FAILED\", str(e))` in the except block.\n\n**State persistence between runs** — polling automations that track a \"last processed\" timestamp or active conversation IDs must use the built-in KV store rather than local files. Local files are lost when a run ends on a cloud pod. The KV store is available when `AUTOMATION_KV_TOKEN` is injected into the run environment. See `references/custom-automation.md#state-persistence-kv-store` for ready-to-copy `kv_get` / `kv_set` / `load_state` / `save_state` helpers.\n\n---\n\n## Authentication\n\nAll requests require Bearer authentication:\n\n```bash\n-H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n```\n\n## API Endpoints\n\n### Determining the API Host\n\n**Before making API calls, determine the correct host:**\n\nThe automation service may run at a different URL from the agent server. In the examples throughout this skill, `${OPENHANDS_HOST}` is a shell-variable convention for the automation service base URL — it is **not** a real environment variable. Set it from context before running any curl command:\n\n- Look for a `<HOST>` value in the system prompt. If present, use that URL.\n- Otherwise default to `https://app.all-hands.dev`.\n\n```bash\nOPENHANDS_HOST=\"https://app.all-hands.dev\" # replace with <HOST> if provided\n```\n\n\n### Automation Endpoints\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/api/automation/v1/preset/prompt` | POST | **Create automation from a prompt (recommended)** |\n| `/api/automation/v1/preset/plugin` | POST | **Create automation with plugins** |\n| `/api/automation/v1` | GET | List automations |\n| `/api/automation/v1/{id}` | GET | Get automation details |\n| `/api/automation/v1/{id}` | PATCH | Update automation |\n| `/api/automation/v1/{id}` | DELETE | Delete automation |\n| `/api/automation/v1/{id}/dispatch` | POST | Trigger a run manually |\n| `/api/automation/v1/{id}/runs` | GET | List automation runs |\n\n### Custom Webhook Endpoints\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/api/automation/v1/webhooks` | POST | Register a custom webhook source |\n| `/api/automation/v1/webhooks` | GET | List all custom webhooks |\n| `/api/automation/v1/webhooks/{id}` | GET | Get webhook details |\n| `/api/automation/v1/webhooks/{id}` | PATCH | Update webhook settings |\n| `/api/automation/v1/webhooks/{id}` | DELETE | Delete a webhook |\n| `/api/automation/v1/webhooks/{id}/rotate-secret` | POST | Rotate signing secret |\n\n---\n\n## Trigger Types\n\nAutomations support two trigger types:\n\n| Trigger Type | Use Case |\n|--------------|----------|\n| **Cron** | Run on a schedule (daily, weekly, hourly, etc.) |\n| **Event** | Run when a webhook event occurs (GitHub PR opened, issue commented, etc.) — **requires a publicly reachable deployment** |\n\n---\n\n## Creating Automations\n\nTwo preset endpoints simplify automation creation by handling SDK boilerplate, tarball packaging, and upload automatically:\n\n1. **Prompt Preset** — Execute a natural language prompt (simple tasks)\n2. **Plugin Preset** — Load plugins with skills, MCP configs, and commands (extended capabilities)\n\n---\n\n### Prompt Preset\n\nUse the **preset/prompt endpoint** for simple automations. Provide a natural language prompt describing the task.\n\n#### How It Works\n\n1. Send a prompt describing the task (e.g., \"Generate a weekly status report\")\n2. The automation service generates a Python script that: fetches LLM config and secrets from the agent server, starts an AI agent conversation with your prompt, and sends a completion callback when done\n3. The script is packaged as a tarball and the automation is registered; on each trigger, the automation service uploads the tarball to the agent server, which unpacks and runs the script inside its environment\n\n#### Request\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"My Automation Name\",\n \"prompt\": \"What the automation should do\",\n \"trigger\": {\n \"type\": \"cron\",\n \"schedule\": \"0 9 * * *\",\n \"timezone\": \"UTC\"\n }\n }'\n```\n\n#### Request Fields\n\n| Field | Required | Description |\n|-------|----------|-------------|\n| `name` | Yes | Name of the automation (1-500 characters) |\n| `prompt` | Yes | Natural language instructions (1-50,000 characters) |\n| `trigger` | Yes | Trigger configuration — either `cron` or `event` (see below) |\n| `timeout` | No | Max execution time in seconds (default: system maximum) |\n| `repos` | No | Repositories to clone (see [Repository Cloning](#repository-cloning)) |\n\n**Cron Trigger Fields:**\n\n| Field | Required | Description |\n|-------|----------|-------------|\n| `trigger.type` | Yes | `\"cron\"` |\n| `trigger.schedule` | Yes | Cron expression (5 fields: min hour day month weekday) |\n| `trigger.timezone` | No | IANA timezone (default: `\"UTC\"`) |\n\n**Event Trigger Fields:**\n\n| Field | Required | Description |\n|-------|----------|-------------|\n| `trigger.type` | Yes | `\"event\"` |\n| `trigger.source` | Yes | Event source: `\"github\"` or custom webhook source name |\n| `trigger.on` | Yes | Event key pattern(s) to match (see Event Keys below) |\n| `trigger.filter` | No | JMESPath expression for payload filtering (see Filter Expressions below) |\n\n#### Prompt Tips\n\nWrite the prompt as an instruction to an AI agent. The prompt executes inside a sandbox with full tool access (bash, file editing, etc.), the user's configured LLM, stored secrets, and MCP server integrations. Examples:\n\n- `\"Generate a weekly status report summarizing the team's GitHub activity and post it to Slack\"`\n- `\"Check the production API health endpoint every hour and alert if it returns non-200\"`\n- `\"Pull the latest data from our analytics API and update the dashboard spreadsheet\"`\n\n#### Cron Schedule\n\n| Field | Values | Description |\n|-------|--------|-------------|\n| Minute | 0-59 | Minute of the hour |\n| Hour | 0-23 | Hour of the day (24-hour) |\n| Day | 1-31 | Day of the month |\n| Month | 1-12 | Month of the year |\n| Weekday | 0-6 | Day of week (0=Sun, 6=Sat) |\n\nCommon schedules: `0 9 * * *` (daily 9 AM), `0 9 * * 1-5` (weekdays 9 AM), `0 9 * * 1` (Mondays 9 AM), `0 0 1 * *` (first of month), `*/15 * * * *` (every 15 min), `0 */6 * * *` (every 6 hours).\n\n#### Response (HTTP 201)\n\n```json\n{\n \"id\": \"123e4567-e89b-12d3-a456-426614174000\",\n \"name\": \"My Automation Name\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 9 * * *\", \"timezone\": \"UTC\"},\n \"enabled\": true,\n \"created_at\": \"2025-03-25T10:00:00Z\"\n}\n```\n\n#### Prompt Preset Examples\n\n**Daily report:**\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Daily Report\",\n \"prompt\": \"Generate a daily status report and save it to a file in the workspace\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 9 * * 1-5\", \"timezone\": \"America/New_York\"}\n }'\n```\n\n**Weekly cleanup:**\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Weekly Cleanup\",\n \"prompt\": \"Clean up temporary files older than 7 days and send a summary of what was removed\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 2 * * 0\", \"timezone\": \"UTC\"},\n \"timeout\": 300\n }'\n```\n\n---\n\n## Polling as a Webhook Alternative\n\nWhen the deployment cannot receive inbound webhook traffic (see rule 5), use a cron-triggered automation that calls the external service’s API on a schedule to check for new events.\n\n### Polling vs. Webhooks at a Glance\n\n| | Webhooks (Event trigger) | Polling (Cron trigger) |\n|---|---|---|\n| **Requires public URL** | Yes | No — works locally |\n| **Latency** | Near-instant | Up to one poll interval |\n| **API calls** | Only on real events | Every poll interval |\n| **Best for** | Cloud / public deployments | Local or private deployments |\n\n---\n\n## Event-Triggered Automations (Webhooks)\n\nEvent-triggered automations run when a webhook event occurs — like a GitHub PR being opened, an issue receiving a comment, or a custom service sending a notification.\n\n### Built-in Integrations\n\n**GitHub** is a built-in integration — no webhook registration needed. Just create automations with `\"source\": \"github\"`.\n\n### GitHub Event Keys\n\nEvents use the format `{event_type}.{action}` or just `{event_type}` (for events without actions like `push`).\n\n| Event Type | Event Keys | Description |\n|------------|------------|-------------|\n| `pull_request` | `pull_request.opened`, `pull_request.closed`, `pull_request.synchronize`, `pull_request.labeled`, `pull_request.unlabeled`, `pull_request.reopened`, `pull_request.edited`, `pull_request.ready_for_review` | PR activity |\n| `issues` | `issues.opened`, `issues.closed`, `issues.reopened`, `issues.labeled`, `issues.unlabeled`, `issues.edited`, `issues.assigned` | Issue activity |\n| `issue_comment` | `issue_comment.created`, `issue_comment.edited`, `issue_comment.deleted` | Comments on issues/PRs |\n| `push` | `push` | Code pushed to a branch |\n| `release` | `release.published`, `release.created`, `release.released`, `release.prereleased` | Release activity |\n| `pull_request_review` | `pull_request_review.submitted`, `pull_request_review.edited`, `pull_request_review.dismissed` | PR review activity |\n\n**Wildcards:** Use `*` to match any action — e.g., `pull_request.*` matches all PR events.\n\n**Multiple patterns:** The `on` field can be a string or array — e.g., `[\"push\", \"pull_request.opened\"]`.\n\n### Filter Expressions (JMESPath)\n\nFilters let you match events based on payload content using JMESPath expressions.\n\n#### Available Functions\n\n| Function | Description | Example |\n|----------|-------------|---------|\n| `glob(str, pattern)` | Wildcard pattern matching | `glob(repository.full_name, 'myorg/*')` |\n| `icontains(str, substr)` | Case-insensitive substring | `icontains(comment.body, '@openhands')` |\n| `contains(array, value)` | Array contains value | `contains(pull_request.labels[].name, 'bug')` |\n| `regex(str, pattern)` | Regular expression match | `regex(ref, '^refs/tags/v\\\\d+')` |\n| `starts_with(str, prefix)` | String starts with | `starts_with(ref, 'refs/heads/')` |\n| `ends_with(str, suffix)` | String ends with | `ends_with(ref, '/main')` |\n| `lower(str)` / `upper(str)` | Case conversion | `lower(sender.login) == 'admin'` |\n\n#### Boolean Operators\n\n- `&&` — AND\n- `||` — OR \n- `!` — NOT\n\n#### Filter Examples\n\n```javascript\n// Exact match on label name\n\"contains(pull_request.labels[].name, 'openhands')\"\n\n// Case-insensitive mention in comment\n\"icontains(comment.body, '@openhands')\"\n\n// Match specific repository\n\"repository.full_name == 'myorg/myrepo'\"\n\n// Match any repo in an org\n\"glob(repository.full_name, 'myorg/*')\"\n\n// PR with 'bug' label in any org repo\n\"glob(repository.full_name, 'myorg/*') && contains(pull_request.labels[].name, 'bug')\"\n\n// Push to main or release branches\n\"glob(ref, 'refs/heads/main') || glob(ref, 'refs/heads/release/*')\"\n\n// Issue opened by a specific user\n\"sender.login == 'dependabot[bot]'\"\n\n// Not a draft PR\n\"!pull_request.draft\"\n```\n\n---\n\n### Event-Triggered Examples\n\n#### GitHub: Respond to @openhands mentions in comments\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"OpenHands Mention Responder\",\n \"prompt\": \"Analyze the issue or PR context and provide a helpful response to the user'\\''s question. The comment body and context are available in the event payload.\",\n \"trigger\": {\n \"type\": \"event\",\n \"source\": \"github\",\n \"on\": \"issue_comment.created\",\n \"filter\": \"icontains(comment.body, '\\''@openhands'\\'')\"\n },\n \"timeout\": 300\n }'\n```\n\n#### GitHub: Auto-review PRs with the \"openhands\" label\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Auto Review PRs\",\n \"prompt\": \"Review this pull request for code quality, potential bugs, and best practices. Provide constructive feedback.\",\n \"trigger\": {\n \"type\": \"event\",\n \"source\": \"github\",\n \"on\": \"pull_request.labeled\",\n \"filter\": \"contains(pull_request.labels[].name, '\\''openhands'\\'')\"\n }\n }'\n```\n\n#### GitHub: Run tests on push to main\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Run Tests on Main\",\n \"prompt\": \"Clone the repository and run the test suite. Report any failures.\",\n \"trigger\": {\n \"type\": \"event\",\n \"source\": \"github\",\n \"on\": \"push\",\n \"filter\": \"ref == '\\''refs/heads/main'\\''\"\n }\n }'\n```\n\n#### GitHub: Triage new issues in specific repos\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Issue Triage Bot\",\n \"prompt\": \"Analyze this new issue and suggest appropriate labels. If it looks like a bug, try to identify the root cause.\",\n \"trigger\": {\n \"type\": \"event\",\n \"source\": \"github\",\n \"on\": \"issues.opened\",\n \"filter\": \"glob(repository.full_name, '\\''myorg/*'\\'')\"\n }\n }'\n```\n\n#### GitHub: Respond to multiple event types\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"PR Activity Bot\",\n \"prompt\": \"Process the PR event and take appropriate action based on the event type.\",\n \"trigger\": {\n \"type\": \"event\",\n \"source\": \"github\",\n \"on\": [\"pull_request.opened\", \"pull_request.synchronize\", \"pull_request.ready_for_review\"]\n }\n }'\n```\n\n---\n\n## Custom Webhooks\n\nFor services other than GitHub (Linear, Stripe, Slack, etc.), register a custom webhook first.\n\n> **Agent behavior:**\n> - **Always provide the curl request** to the user — do not attempt to register webhooks yourself.\n> - **Ask the user:** \"Do you have a webhook signing secret from [service], or should the system generate one?\"\n> - If they have one → include `webhook_secret` in the request\n> - If not → omit it; the response will contain a generated secret they must configure in their service\n\n### Register a Custom Webhook\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/webhooks\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Linear Issues\",\n \"source\": \"linear\",\n \"event_key_expr\": \"type\",\n \"signature_header\": \"Linear-Signature\",\n \"webhook_secret\": \"your-linear-webhook-secret\"\n }'\n```\n\n#### Webhook Fields\n\n| Field | Required | Description |\n|-------|----------|-------------|\n| `name` | Yes | Human-readable name for the webhook |\n| `source` | Yes | Unique source identifier (lowercase, alphanumeric with hyphens, 1-50 chars) |\n| `event_key_expr` | No | JMESPath expression to extract event type from payload (default: `\"type\"`) |\n| `signature_header` | No | HTTP header containing HMAC signature (default: `\"X-Signature-256\"`) |\n| `webhook_secret` | No | Signing secret — provide your own (from the external service) or let the system generate one |\n\n#### Response\n\n```json\n{\n \"id\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"webhook_url\": \"https://app.all-hands.dev/v1/events/{org_id}/linear\",\n \"source\": \"linear\",\n \"enabled\": true\n}\n```\n\n**Note:** When you provide your own `webhook_secret`, it won't be echoed back in the response. If you don't provide one, the system generates a secret and returns it once — store it securely.\n\n### Manage Custom Webhooks\n\n```bash\n# List all webhooks\ncurl \"${OPENHANDS_HOST}/api/automation/v1/webhooks\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n\n# Update a webhook\ncurl -X PATCH \"${OPENHANDS_HOST}/api/automation/v1/webhooks/{webhook_id}\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"enabled\": false}'\n\n# Rotate the signing secret\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/webhooks/{webhook_id}/rotate-secret\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n\n# Delete a webhook\ncurl -X DELETE \"${OPENHANDS_HOST}/api/automation/v1/webhooks/{webhook_id}\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n```\n\n### Custom Webhook Example: Linear\n\nLinear sends webhooks with:\n- Signature header: `Linear-Signature`\n- Event type in payload: `type` field (e.g., `Issue`, `Comment`, `Project`)\n- Action in payload: `action` field (e.g., `create`, `update`, `remove`)\n\n```bash\n# 1. Register the Linear webhook\n# - Get your webhook signing secret from Linear's webhook settings\n# - Use \"Linear-Signature\" as the signature header\n# - Use \"type\" to extract the event type from the payload\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/webhooks\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Linear Issues\",\n \"source\": \"linear\",\n \"event_key_expr\": \"type\",\n \"signature_header\": \"Linear-Signature\",\n \"webhook_secret\": \"lin_wh_xxxxxxxxxxxxx\"\n }'\n\n# Response includes webhook_url — configure this in Linear:\n# Settings → API → Webhooks → New webhook → paste the webhook_url\n\n# 2. Create an automation for new Linear issues\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Triage New Linear Issues\",\n \"prompt\": \"A new issue was created in Linear. Analyze the issue title and description, suggest appropriate labels, and add a comment with initial triage notes.\",\n \"trigger\": {\n \"type\": \"event\",\n \"source\": \"linear\",\n \"on\": \"Issue\",\n \"filter\": \"action == '\\''create'\\''\"\n }\n }'\n\n# 3. Create an automation for high-priority issue updates\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"High Priority Issue Alert\",\n \"prompt\": \"A high-priority issue was updated. Review the changes and notify the team if action is needed.\",\n \"trigger\": {\n \"type\": \"event\",\n \"source\": \"linear\",\n \"on\": \"Issue\",\n \"filter\": \"action == '\\''update'\\'' && data.priority == `1`\"\n }\n }'\n```\n\n### Common Signature Headers by Service\n\n| Service | Signature Header | Event Key Expression |\n|---------|-----------------|---------------------|\n| Linear | `Linear-Signature` | `type` |\n| Stripe | `Stripe-Signature` | `type` |\n| Slack | `X-Slack-Signature` | `type` |\n| Twilio | `X-Twilio-Signature` | `type` |\n| Generic | `X-Signature-256` | `type` |\n\n---\n\n### Plugin Preset\n\nUse the **preset/plugin endpoint** when you need to load one or more plugins that provide extended capabilities like skills, MCP configurations, hooks, and commands.\n\n> **💡 Finding plugins:** Browse the [OpenHands/extensions](https://github.com/OpenHands/extensions) repository for available skills and plugins. When given a broad use case, check this directory first to see if something already exists that fits your needs.\n\n#### How It Works\n\n1. Specify one or more plugins (from GitHub repos, git URLs, or monorepo subdirectories)\n2. Provide a prompt that can invoke plugin commands (e.g., `/plugin-name:command`)\n3. The service generates SDK boilerplate that loads all plugins at runtime, creates a conversation with plugin capabilities, and executes the prompt\n4. The service packages everything into a tarball, uploads it, and creates the automation\n\n#### Request\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/plugin\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"My Plugin Automation\",\n \"plugins\": [\n {\"source\": \"github:owner/repo\", \"ref\": \"v1.0.0\"},\n {\"source\": \"github:owner/another-plugin\"}\n ],\n \"prompt\": \"Use the plugin commands to perform the task\",\n \"trigger\": {\n \"type\": \"cron\",\n \"schedule\": \"0 9 * * 1\",\n \"timezone\": \"UTC\"\n }\n }'\n```\n\n#### Request Fields\n\n| Field | Required | Description |\n|-------|----------|-------------|\n| `name` | Yes | Name of the automation (1-500 characters) |\n| `plugins` | Yes | List of plugin sources (at least one required) |\n| `plugins[].source` | Yes | Plugin source: `github:owner/repo`, git URL, or local path |\n| `plugins[].ref` | No | Git ref: branch, tag, or commit SHA |\n| `plugins[].repo_path` | No | Subdirectory path for monorepos |\n| `prompt` | Yes | Instructions for the automation (1-50,000 characters) |\n| `trigger` | Yes | Trigger configuration — either `cron` or `event` (same as Prompt Preset) |\n| `timeout` | No | Max execution time in seconds (default: system maximum) |\n| `repos` | No | Repositories to clone (see [Repository Cloning](#repository-cloning)) |\n\n#### Plugin Source Formats\n\n| Format | Example | Description |\n|--------|---------|-------------|\n| GitHub shorthand | `github:owner/repo` | Fetches from GitHub |\n| Git URL | `https://github.com/owner/repo.git` | Any git repository |\n| With ref | `{\"source\": \"github:owner/repo\", \"ref\": \"v1.0.0\"}` | Specific branch/tag/commit |\n| Monorepo | `{\"source\": \"github:org/monorepo\", \"repo_path\": \"plugins/my-plugin\"}` | Subdirectory in repo |\n\n#### Response (HTTP 201)\n\n```json\n{\n \"id\": \"123e4567-e89b-12d3-a456-426614174000\",\n \"name\": \"My Plugin Automation\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 9 * * 1\", \"timezone\": \"UTC\"},\n \"enabled\": true,\n \"created_at\": \"2025-03-25T10:00:00Z\"\n}\n```\n\n#### Plugin Preset Examples\n\n**Single plugin with version:**\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/plugin\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Code Review Automation\",\n \"plugins\": [\n {\"source\": \"github:owner/code-review-plugin\", \"ref\": \"v2.0.0\"}\n ],\n \"prompt\": \"Review all Python files in the repository for code quality issues\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 9 * * 1-5\", \"timezone\": \"UTC\"}\n }'\n```\n\n**Multiple plugins:**\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/plugin\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Security Scan Automation\",\n \"plugins\": [\n {\"source\": \"github:owner/security-scanner\"},\n {\"source\": \"github:owner/report-generator\", \"ref\": \"main\"}\n ],\n \"prompt\": \"Run a security scan on the codebase and generate a report\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 2 * * 0\", \"timezone\": \"UTC\"},\n \"timeout\": 600\n }'\n```\n\n**Monorepo plugin:**\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/plugin\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Style Guide Enforcement\",\n \"plugins\": [\n {\"source\": \"github:company/monorepo\", \"repo_path\": \"plugins/style-guide\", \"ref\": \"main\"}\n ],\n \"prompt\": \"Check all files against the company style guide\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 8 * * 1\", \"timezone\": \"America/Los_Angeles\"}\n }'\n```\n\n---\n\n## Repository Cloning\n\nBoth presets support an optional `repos` field to clone repositories into the sandbox before execution. Cloned repos have their skills (AGENTS.md, `.agents/skills/`) automatically loaded.\n\n### Repo Source Formats\n\n| Format | Example | Description |\n|--------|---------|-------------|\n| Full URL | `\"https://github.com/owner/repo\"` | Provider auto-detected |\n| Full URL + ref | `{\"url\": \"https://github.com/owner/repo\", \"ref\": \"main\"}` | With branch/tag/SHA |\n| Short URL | `{\"url\": \"owner/repo\", \"provider\": \"github\"}` | Requires `provider` field |\n\n**Supported providers:** `github`, `gitlab`, `bitbucket`\n\n> **Note:** Short URLs (`owner/repo`) require an explicit `provider` field. Full URLs auto-detect the provider.\n\n### Examples\n\n**Single repo (full URL):**\n```json\n{\n \"repos\": [\"https://github.com/OpenHands/openhands-cli\"]\n}\n```\n\n**Multiple repos with refs:**\n```json\n{\n \"repos\": [\n {\"url\": \"https://github.com/owner/repo1\", \"ref\": \"main\"},\n {\"url\": \"https://gitlab.com/owner/repo2\", \"ref\": \"v1.0.0\"}\n ]\n}\n```\n\n**Short URL with provider:**\n```json\n{\n \"repos\": [\n {\"url\": \"owner/repo\", \"provider\": \"github\", \"ref\": \"main\"}\n ]\n}\n```\n\n### Complete Automation Example\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Analyze Codebase\",\n \"prompt\": \"Analyze the openhands-cli codebase and generate a summary report\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 9 * * 1\"},\n \"repos\": [\n {\"url\": \"https://github.com/OpenHands/openhands-cli\", \"ref\": \"main\"}\n ]\n }'\n```\n\n---\n\n## Managing Automations\n\n### List Automations\n\n```bash\ncurl \"${OPENHANDS_HOST}/api/automation/v1?limit=20\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n```\n\n### Get / Update / Delete\n\n```bash\n# Get details\ncurl \"${OPENHANDS_HOST}/api/automation/v1/{automation_id}\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n\n# Update (fields: name, trigger, enabled, timeout)\ncurl -X PATCH \"${OPENHANDS_HOST}/api/automation/v1/{automation_id}\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"enabled\": false}'\n\n# Delete\ncurl -X DELETE \"${OPENHANDS_HOST}/api/automation/v1/{automation_id}\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n```\n\n### Trigger and Monitor Runs\n\n```bash\n# Manually trigger a run\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/{automation_id}/dispatch\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n\n# List runs\ncurl \"${OPENHANDS_HOST}/api/automation/v1/{automation_id}/runs?limit=20\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n```\n\nRun status values: `PENDING` (waiting for dispatch), `RUNNING` (in progress), `COMPLETED` (success), `FAILED` (check `error_detail`).\n\n---\n\n## Run Lifecycle\n\nWhen a run completes, the automation service receives a callback and marks the run done. Any conversations started during the run remain accessible in the OpenHands UI — users can view the history and continue interacting. The agent server persists until it times out or is manually deleted.\n\nThe automation script itself controls when the callback fires (signalling completion). For simple synchronous scripts this happens naturally on exit. For scripts that start asynchronous conversations, the callback should be deferred until the conversation reaches an idle state (see `references/custom-automation.md` for patterns).\n\n---\n\n## Choosing the Right Preset\n\nPick based on **what the task needs**, not just **what is technically possible**. An LLM-driven preset can do almost anything, so \"the preset can satisfy this\" is not by itself a good reason to pick it — every run costs tokens and sandbox time.\n\n| Use Case | Recommended |\n|----------|-------------|\n| Reasoning, summarization, triage, code review, or open-ended tool use | **Prompt Preset** |\n| Needs plugin commands / skills / MCP configs / hooks | **Plugin Preset** |\n| Compare plugin versions or configurations across runs | **Plugin Preset with A/B testing** — see `references/ab-testing.md` |\n| **Deterministic task** (fixed data + scheduled action, e.g. healthcheck, Slack notification, rotating from a known list) — especially if it runs frequently | **Custom script, no LLM** — see `references/custom-automation.md#deterministic-script-no-llm` |\n| Custom Python dependencies, multi-file project, or direct SDK lifecycle control | **Custom script with SDK** — see `references/custom-automation.md#sdk-based-scripts` |\n\nThe **prompt preset** is the right default for genuinely agent-shaped work — anything that benefits from reasoning over context, calling tools dynamically, or producing a non-templated output. Use the **plugin preset** when you need extended capabilities from plugins (skills, MCP configurations, hooks, commands).\n\n**Watch for deterministic, high-frequency patterns.** Requests like \"send a daily standup reminder\", \"ping a healthcheck URL every minute\", \"post a random quote every 5 minutes\", or \"rotate a fact-of-the-day message\" do not need an LLM. Surface this to the user explicitly with a rough cost framing (e.g. \"this schedule will invoke your LLM ~288 times/day\") before defaulting to a preset. As a rule of thumb, any cron tighter than hourly deserves a deliberate \"should this really be agent-driven?\" check.\n\n**When neither preset is the right fit** (deterministic task, custom Python dependencies, non-Python entrypoint, multi-file project structure, direct SDK lifecycle control), explain the options to the user and let them decide. Do not attempt custom automation without explicit user agreement. If they choose the custom route, refer to `references/custom-automation.md`.\n\n## Reference Files\n\n- **`references/custom-automation.md`** — Detailed guide for custom automations: tarball uploads, code structure (SDK and no-LLM), state persistence via the KV store, environment variables, validation rules, and complete examples. Consult this whenever you need to evaluate or recommend the custom path (including for deterministic / cost-sensitive tasks per rule 0). Only *implement* a custom automation after the user agrees to that path.\n- **`references/ab-testing.md`** — A/B testing for plugin automations: defining variants with weights, experiment configuration, variant selection logic, observability via conversation tags, and complete examples. Consult this when a user wants to compare plugin versions or configurations."
302
+ content: "# OpenHands Automations\n\nCreate and manage automations that run inside an OpenHands agent server — triggered by cron schedules or webhook events (GitHub, custom services).\nWindows PowerShell equivalents for the automation API `curl` examples and shell-variable conventions are in `references/windows.md`.\n\n## Automation Creation Process\nThe agent must follow these steps when creating an automation:\n* Quickly check that you can access the correct automations backend using the auth mechanism below\n* Quickly check that you can access any necessary integrations (e.g. GitHub, Slack); if access fails, inform the user and stop\n* Ask the user for any necessary information, e.g. if you need the name of a Slack channel or GitHub repo to proceed\n* Write the code or prompt that will be sent to the automations backend _inside the current workspace_\n* Show the code to the user with the `canvas_ui` tool if available, otherwise present it in a fenced code block in your reply\n* Message the user with a concise summary of how the automation will behave, and ask if they are ready to deploy it\n\n## Architecture\n\nTwo components work together to run automations:\n\n**Automation Service** (API at `OPENHANDS_HOST/api/automation/v1`)\nManages the *when*: holds automation definitions, schedules cron-triggered runs, dispatches webhook-triggered runs, and receives completion callbacks to mark runs as done. This is the API you call to create, update, and manage automations.\n\n**Agent Server** (accessible as `AGENT_SERVER_URL` inside script runs)\nManages the *what*: the runtime environment where automation scripts execute and where conversations (AI agent interactions with tools, bash, file editing, etc.) run. When a run is triggered, the automation service uploads the automation's tarball to the agent server, which unpacks and runs the entrypoint script. The script connects back to the agent server using `AGENT_SERVER_URL` and a session API key to start, monitor, and stop conversations.\n\nThe agent server typically runs inside a **sandbox** (a Docker or Kubernetes container). Some deployments use sandboxless mode, where the agent server runs directly on a host.\n\n**Key environment variables:**\n\n| Variable | Availability | Description |\n|---|---|---|\n| `RUNTIME_URL` | Ambient in cloud environments | Public-facing URL of the **agent server** sandbox. Use this to determine whether external webhook delivery is possible — if unset or local, webhooks cannot be received. The automation service may run at a separate URL (see Determining the API Host). |\n| `AGENT_SERVER_URL` | Injected into scripts at run time only | Internal URL of the agent server. Available inside script execution context; **not** an ambient environment variable outside of a running script. |\n| `OPENHANDS_HOST` | Shell convention only — set manually | Base URL for the automation service API. **Not a real environment variable.** Set it from the `<HOST>` system-prompt value, or default to `https://app.all-hands.dev`. Used in all `curl` examples throughout this skill. |\n\n> **⚠️ CRITICAL — Agent behavior rules:**\n>\n> 0. **Does this task need an LLM at all? Check first.** Before picking a preset, ask whether the task actually requires reasoning, judgment, summarization, or open-ended tool use. If it is fully deterministic — fixed data transforms, scheduled HTTP calls, healthcheck pings, file rotation, picking from a known list, posting a templated message — an LLM-driven preset is overkill. Every run will consume LLM tokens, which adds up fast at high frequencies (every 5 min ≈ 288 runs/day). Surface the trade-off to the user and offer the custom-script path (see `references/custom-automation.md`) as the cheaper, more reliable option. Be especially careful for cron schedules tighter than hourly.\n>\n> **Instant-recognition patterns — these are always deterministic, never use an LLM preset:**\n> - \"post a quote / message / fact every N minutes\" (rotating from a list)\n> - \"send a scheduled reminder / standup / digest\"\n> - \"ping a health-check URL on a schedule\"\n> - \"post to Slack / webhook every N minutes\"\n> - Any task where the full output could be written as a static template right now\n>\n> 1. **For LLM-appropriate work, default to preset endpoints.** They handle all SDK boilerplate, tarball packaging, and upload automatically:\n> - **Prompt preset** (`POST /v1/preset/prompt`) — for tasks expressed as a natural language prompt that benefit from agent reasoning\n> - **Plugin preset** (`POST /v1/preset/plugin`) — when plugins with skills, MCP configs, or commands are needed\n> 2. **Do not silently create custom scripts.** Do not generate Python code, `setup.sh` files, or tarball uploads without user consent. But *do* proactively recommend the custom path (per rule 0) when the task is deterministic or high-frequency — surface the option and let the user choose.\n> 3. **If neither preset is the right fit**, do NOT silently fall back to custom automation. Instead, explain the available options to the user:\n> - **Prompt preset** — natural language prompt execution (LLM-driven)\n> - **Plugin preset** — load plugins with extended capabilities (skills, MCP, hooks, commands)\n> - **Custom script** — full control over code, with or without LLM; point them to `references/custom-automation.md`\n> - Let the user choose which approach to use.\n> 4. **Only create custom scripts after the user agrees to that path.** Refer to `references/custom-automation.md` for the full reference.\n> 5. **Before suggesting event-triggered (webhook) automations, check whether the deployment is publicly reachable.** Check `RUNTIME_URL`. Webhooks require an internet-accessible URL so that external services (GitHub, Slack, Linear, etc.) can deliver events to the automation service. If `RUNTIME_URL` is unset, empty, or resolves to a local or private address (`localhost`, `127.0.0.1`, `0.0.0.0`, or any RFC 1918 range: `10.x.x.x`, `192.168.x.x`, `172.16–31.x.x`), the service cannot receive inbound webhook traffic from the public internet. In that case:\n> - **Recommend a cron-based polling automation instead.** Have the automation run on a schedule and call the external service's API (e.g., the GitHub REST API) to check for new events since the last run.\n> - Explain the limitation clearly to the user: \"Because this is a local deployment, external services can't reach the webhook endpoint. I'll set up a polling automation using a cron schedule instead.\"\n\n### No-LLM Script Helpers\n\nWhen building a deterministic custom script, these two stdlib-only functions are required. Copy them verbatim — they use `AGENT_SERVER_URL` and `SESSION_API_KEY` injected by the automation service.\n\n```python\nimport json, os, urllib.request\n\ndef get_secret(name):\n \"\"\"Fetch a named secret stored in the agent server.\"\"\"\n url = os.environ.get(\"AGENT_SERVER_URL\", \"\").rstrip(\"/\")\n key = os.environ.get(\"SESSION_API_KEY\") or os.environ.get(\"OH_SESSION_API_KEYS_0\", \"\")\n with urllib.request.urlopen(urllib.request.Request(\n f\"{url}/api/settings/secrets/{name}\", headers={\"X-Session-API-Key\": key}\n )) as r:\n return r.read().decode().strip()\n\ndef fire_callback(status=\"COMPLETED\", error=None):\n \"\"\"Signal run completion. MUST be called on every exit path — success AND error.\"\"\"\n url = os.environ.get(\"AUTOMATION_CALLBACK_URL\", \"\")\n if not url: return\n body = {\"status\": status, \"run_id\": os.environ.get(\"AUTOMATION_RUN_ID\", \"\")}\n if error: body[\"error\"] = error\n try:\n urllib.request.urlopen(urllib.request.Request(url, data=json.dumps(body).encode(), headers={\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {os.environ.get('AUTOMATION_CALLBACK_API_KEY', '')}\",\n }))\n except Exception as e: print(f\"Callback error: {e}\")\n```\n\nEntrypoint must be `python3 main.py` (no `setup.sh` needed). Wrap your main logic in `try/except` and call `fire_callback(\"FAILED\", str(e))` in the except block.\n\n**State persistence between runs** — polling automations that track a \"last processed\" timestamp or active conversation IDs must use the built-in KV store rather than local files. Local files are lost when a run ends on a cloud pod. The KV store is available when `AUTOMATION_KV_TOKEN` is injected into the run environment. See `references/custom-automation.md#state-persistence-kv-store` for ready-to-copy `kv_get` / `kv_set` / `load_state` / `save_state` helpers.\n\n---\n\n## Authentication\n\nAll requests require Bearer authentication:\n\n```bash\n-H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n```\n\n## API Endpoints\n\n### Determining the API Host\n\n**Before making API calls, determine the correct host:**\n\nThe automation service may run at a different URL from the agent server. In the examples throughout this skill, `${OPENHANDS_HOST}` is a shell-variable convention for the automation service base URL — it is **not** a real environment variable. Set it from context before running any curl command:\n\n- Look for a `<HOST>` value in the system prompt. If present, use that URL.\n- Otherwise default to `https://app.all-hands.dev`.\n\n```bash\nOPENHANDS_HOST=\"https://app.all-hands.dev\" # replace with <HOST> if provided\n```\n\n\n### Automation Endpoints\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/api/automation/v1/preset/prompt` | POST | **Create automation from a prompt (recommended)** |\n| `/api/automation/v1/preset/plugin` | POST | **Create automation with plugins** |\n| `/api/automation/v1` | GET | List automations |\n| `/api/automation/v1/{id}` | GET | Get automation details |\n| `/api/automation/v1/{id}` | PATCH | Update automation |\n| `/api/automation/v1/{id}` | DELETE | Delete automation |\n| `/api/automation/v1/{id}/dispatch` | POST | Trigger a run manually |\n| `/api/automation/v1/{id}/runs` | GET | List automation runs |\n\n### Custom Webhook Endpoints\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/api/automation/v1/webhooks` | POST | Register a custom webhook source |\n| `/api/automation/v1/webhooks` | GET | List all custom webhooks |\n| `/api/automation/v1/webhooks/{id}` | GET | Get webhook details |\n| `/api/automation/v1/webhooks/{id}` | PATCH | Update webhook settings |\n| `/api/automation/v1/webhooks/{id}` | DELETE | Delete a webhook |\n| `/api/automation/v1/webhooks/{id}/rotate-secret` | POST | Rotate signing secret |\n\n---\n\n## Trigger Types\n\nAutomations support two trigger types:\n\n| Trigger Type | Use Case |\n|--------------|----------|\n| **Cron** | Run on a schedule (daily, weekly, hourly, etc.) |\n| **Event** | Run when a webhook event occurs (GitHub PR opened, issue commented, etc.) — **requires a publicly reachable deployment** |\n\n---\n\n## Creating Automations\n\nTwo preset endpoints simplify automation creation by handling SDK boilerplate, tarball packaging, and upload automatically:\n\n1. **Prompt Preset** — Execute a natural language prompt (simple tasks)\n2. **Plugin Preset** — Load plugins with skills, MCP configs, and commands (extended capabilities)\n\n---\n\n### Prompt Preset\n\nUse the **preset/prompt endpoint** for simple automations. Provide a natural language prompt describing the task.\n\n#### How It Works\n\n1. Send a prompt describing the task (e.g., \"Generate a weekly status report\")\n2. The automation service generates a Python script that: fetches LLM config and secrets from the agent server, starts an AI agent conversation with your prompt, and sends a completion callback when done\n3. The script is packaged as a tarball and the automation is registered; on each trigger, the automation service uploads the tarball to the agent server, which unpacks and runs the script inside its environment\n\n#### Request\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"My Automation Name\",\n \"prompt\": \"What the automation should do\",\n \"trigger\": {\n \"type\": \"cron\",\n \"schedule\": \"0 9 * * *\",\n \"timezone\": \"UTC\"\n }\n }'\n```\n\n#### Request Fields\n\n| Field | Required | Description |\n|-------|----------|-------------|\n| `name` | Yes | Name of the automation (1-500 characters) |\n| `prompt` | Yes | Natural language instructions (1-50,000 characters) |\n| `trigger` | Yes | Trigger configuration — either `cron` or `event` (see below) |\n| `timeout` | No | Max execution time in seconds (default: system maximum) |\n| `repos` | No | Repositories to clone (see [Repository Cloning](#repository-cloning)) |\n\n**Cron Trigger Fields:**\n\n| Field | Required | Description |\n|-------|----------|-------------|\n| `trigger.type` | Yes | `\"cron\"` |\n| `trigger.schedule` | Yes | Cron expression (5 fields: min hour day month weekday) |\n| `trigger.timezone` | No | IANA timezone (default: `\"UTC\"`) |\n\n**Event Trigger Fields:**\n\n| Field | Required | Description |\n|-------|----------|-------------|\n| `trigger.type` | Yes | `\"event\"` |\n| `trigger.source` | Yes | Event source: `\"github\"` or custom webhook source name |\n| `trigger.on` | Yes | Event key pattern(s) to match (see Event Keys below) |\n| `trigger.filter` | No | JMESPath expression for payload filtering (see Filter Expressions below) |\n\n#### Prompt Tips\n\nWrite the prompt as an instruction to an AI agent. The prompt executes inside a sandbox with full tool access (bash, file editing, etc.), the user's configured LLM, stored secrets, and MCP server integrations. Examples:\n\n- `\"Generate a weekly status report summarizing the team's GitHub activity and post it to Slack\"`\n- `\"Check the production API health endpoint every hour and alert if it returns non-200\"`\n- `\"Pull the latest data from our analytics API and update the dashboard spreadsheet\"`\n\n#### Cron Schedule\n\n| Field | Values | Description |\n|-------|--------|-------------|\n| Minute | 0-59 | Minute of the hour |\n| Hour | 0-23 | Hour of the day (24-hour) |\n| Day | 1-31 | Day of the month |\n| Month | 1-12 | Month of the year |\n| Weekday | 0-6 | Day of week (0=Sun, 6=Sat) |\n\nCommon schedules: `0 9 * * *` (daily 9 AM), `0 9 * * 1-5` (weekdays 9 AM), `0 9 * * 1` (Mondays 9 AM), `0 0 1 * *` (first of month), `*/15 * * * *` (every 15 min), `0 */6 * * *` (every 6 hours).\n\n#### Response (HTTP 201)\n\n```json\n{\n \"id\": \"123e4567-e89b-12d3-a456-426614174000\",\n \"name\": \"My Automation Name\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 9 * * *\", \"timezone\": \"UTC\"},\n \"enabled\": true,\n \"created_at\": \"2025-03-25T10:00:00Z\"\n}\n```\n\n#### Prompt Preset Examples\n\n**Daily report:**\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Daily Report\",\n \"prompt\": \"Generate a daily status report and save it to a file in the workspace\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 9 * * 1-5\", \"timezone\": \"America/New_York\"}\n }'\n```\n\n**Weekly cleanup:**\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Weekly Cleanup\",\n \"prompt\": \"Clean up temporary files older than 7 days and send a summary of what was removed\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 2 * * 0\", \"timezone\": \"UTC\"},\n \"timeout\": 300\n }'\n```\n\n---\n\n## Polling as a Webhook Alternative\n\nWhen the deployment cannot receive inbound webhook traffic (see rule 5), use a cron-triggered automation that calls the external service’s API on a schedule to check for new events.\n\n### Polling vs. Webhooks at a Glance\n\n| | Webhooks (Event trigger) | Polling (Cron trigger) |\n|---|---|---|\n| **Requires public URL** | Yes | No — works locally |\n| **Latency** | Near-instant | Up to one poll interval |\n| **API calls** | Only on real events | Every poll interval |\n| **Best for** | Cloud / public deployments | Local or private deployments |\n\n---\n\n## Event-Triggered Automations (Webhooks)\n\nEvent-triggered automations run when a webhook event occurs — like a GitHub PR being opened, an issue receiving a comment, or a custom service sending a notification.\n\n### Built-in Integrations\n\n**GitHub** is a built-in integration — no webhook registration needed. Just create automations with `\"source\": \"github\"`.\n\n### GitHub Event Keys\n\nEvents use the format `{event_type}.{action}` or just `{event_type}` (for events without actions like `push`).\n\n| Event Type | Event Keys | Description |\n|------------|------------|-------------|\n| `pull_request` | `pull_request.opened`, `pull_request.closed`, `pull_request.synchronize`, `pull_request.labeled`, `pull_request.unlabeled`, `pull_request.reopened`, `pull_request.edited`, `pull_request.ready_for_review` | PR activity |\n| `issues` | `issues.opened`, `issues.closed`, `issues.reopened`, `issues.labeled`, `issues.unlabeled`, `issues.edited`, `issues.assigned` | Issue activity |\n| `issue_comment` | `issue_comment.created`, `issue_comment.edited`, `issue_comment.deleted` | Comments on issues/PRs |\n| `push` | `push` | Code pushed to a branch |\n| `release` | `release.published`, `release.created`, `release.released`, `release.prereleased` | Release activity |\n| `pull_request_review` | `pull_request_review.submitted`, `pull_request_review.edited`, `pull_request_review.dismissed` | PR review activity |\n\n**Wildcards:** Use `*` to match any action — e.g., `pull_request.*` matches all PR events.\n\n**Multiple patterns:** The `on` field can be a string or array — e.g., `[\"push\", \"pull_request.opened\"]`.\n\n### Filter Expressions (JMESPath)\n\nFilters let you match events based on payload content using JMESPath expressions.\n\n#### Available Functions\n\n| Function | Description | Example |\n|----------|-------------|---------|\n| `glob(str, pattern)` | Wildcard pattern matching | `glob(repository.full_name, 'myorg/*')` |\n| `icontains(str, substr)` | Case-insensitive substring | `icontains(comment.body, '@openhands')` |\n| `contains(array, value)` | Array contains value | `contains(pull_request.labels[].name, 'bug')` |\n| `regex(str, pattern)` | Regular expression match | `regex(ref, '^refs/tags/v\\\\d+')` |\n| `starts_with(str, prefix)` | String starts with | `starts_with(ref, 'refs/heads/')` |\n| `ends_with(str, suffix)` | String ends with | `ends_with(ref, '/main')` |\n| `lower(str)` / `upper(str)` | Case conversion | `lower(sender.login) == 'admin'` |\n\n#### Boolean Operators\n\n- `&&` — AND\n- `||` — OR \n- `!` — NOT\n\n#### Filter Examples\n\n```javascript\n// Exact match on label name\n\"contains(pull_request.labels[].name, 'openhands')\"\n\n// Case-insensitive mention in comment\n\"icontains(comment.body, '@openhands')\"\n\n// Match specific repository\n\"repository.full_name == 'myorg/myrepo'\"\n\n// Match any repo in an org\n\"glob(repository.full_name, 'myorg/*')\"\n\n// PR with 'bug' label in any org repo\n\"glob(repository.full_name, 'myorg/*') && contains(pull_request.labels[].name, 'bug')\"\n\n// Push to main or release branches\n\"glob(ref, 'refs/heads/main') || glob(ref, 'refs/heads/release/*')\"\n\n// Issue opened by a specific user\n\"sender.login == 'dependabot[bot]'\"\n\n// Not a draft PR\n\"!pull_request.draft\"\n```\n\n---\n\n### Event-Triggered Examples\n\n#### GitHub: Respond to @openhands mentions in comments\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"OpenHands Mention Responder\",\n \"prompt\": \"Analyze the issue or PR context and provide a helpful response to the user'\\''s question. The comment body and context are available in the event payload.\",\n \"trigger\": {\n \"type\": \"event\",\n \"source\": \"github\",\n \"on\": \"issue_comment.created\",\n \"filter\": \"icontains(comment.body, '\\''@openhands'\\'')\"\n },\n \"timeout\": 300\n }'\n```\n\n#### GitHub: Auto-review PRs with the \"openhands\" label\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Auto Review PRs\",\n \"prompt\": \"Review this pull request for code quality, potential bugs, and best practices. Provide constructive feedback.\",\n \"trigger\": {\n \"type\": \"event\",\n \"source\": \"github\",\n \"on\": \"pull_request.labeled\",\n \"filter\": \"contains(pull_request.labels[].name, '\\''openhands'\\'')\"\n }\n }'\n```\n\n#### GitHub: Run tests on push to main\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Run Tests on Main\",\n \"prompt\": \"Clone the repository and run the test suite. Report any failures.\",\n \"trigger\": {\n \"type\": \"event\",\n \"source\": \"github\",\n \"on\": \"push\",\n \"filter\": \"ref == '\\''refs/heads/main'\\''\"\n }\n }'\n```\n\n#### GitHub: Triage new issues in specific repos\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Issue Triage Bot\",\n \"prompt\": \"Analyze this new issue and suggest appropriate labels. If it looks like a bug, try to identify the root cause.\",\n \"trigger\": {\n \"type\": \"event\",\n \"source\": \"github\",\n \"on\": \"issues.opened\",\n \"filter\": \"glob(repository.full_name, '\\''myorg/*'\\'')\"\n }\n }'\n```\n\n#### GitHub: Respond to multiple event types\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"PR Activity Bot\",\n \"prompt\": \"Process the PR event and take appropriate action based on the event type.\",\n \"trigger\": {\n \"type\": \"event\",\n \"source\": \"github\",\n \"on\": [\"pull_request.opened\", \"pull_request.synchronize\", \"pull_request.ready_for_review\"]\n }\n }'\n```\n\n---\n\n## Custom Webhooks\n\nFor services other than GitHub (Linear, Stripe, Slack, etc.), register a custom webhook first.\n\n> **Agent behavior:**\n> - **Always provide the curl request** to the user — do not attempt to register webhooks yourself.\n> - **Ask the user:** \"Do you have a webhook signing secret from [service], or should the system generate one?\"\n> - If they have one → include `webhook_secret` in the request\n> - If not → omit it; the response will contain a generated secret they must configure in their service\n\n### Register a Custom Webhook\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/webhooks\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Linear Issues\",\n \"source\": \"linear\",\n \"event_key_expr\": \"type\",\n \"signature_header\": \"Linear-Signature\",\n \"webhook_secret\": \"your-linear-webhook-secret\"\n }'\n```\n\n#### Webhook Fields\n\n| Field | Required | Description |\n|-------|----------|-------------|\n| `name` | Yes | Human-readable name for the webhook |\n| `source` | Yes | Unique source identifier (lowercase, alphanumeric with hyphens, 1-50 chars) |\n| `event_key_expr` | No | JMESPath expression to extract event type from payload (default: `\"type\"`) |\n| `signature_header` | No | HTTP header containing HMAC signature (default: `\"X-Signature-256\"`) |\n| `webhook_secret` | No | Signing secret — provide your own (from the external service) or let the system generate one |\n\n#### Response\n\n```json\n{\n \"id\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"webhook_url\": \"https://app.all-hands.dev/v1/events/{org_id}/linear\",\n \"source\": \"linear\",\n \"enabled\": true\n}\n```\n\n**Note:** When you provide your own `webhook_secret`, it won't be echoed back in the response. If you don't provide one, the system generates a secret and returns it once — store it securely.\n\n### Manage Custom Webhooks\n\n```bash\n# List all webhooks\ncurl \"${OPENHANDS_HOST}/api/automation/v1/webhooks\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n\n# Update a webhook\ncurl -X PATCH \"${OPENHANDS_HOST}/api/automation/v1/webhooks/{webhook_id}\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"enabled\": false}'\n\n# Rotate the signing secret\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/webhooks/{webhook_id}/rotate-secret\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n\n# Delete a webhook\ncurl -X DELETE \"${OPENHANDS_HOST}/api/automation/v1/webhooks/{webhook_id}\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n```\n\n### Custom Webhook Example: Linear\n\nLinear sends webhooks with:\n- Signature header: `Linear-Signature`\n- Event type in payload: `type` field (e.g., `Issue`, `Comment`, `Project`)\n- Action in payload: `action` field (e.g., `create`, `update`, `remove`)\n\n```bash\n# 1. Register the Linear webhook\n# - Get your webhook signing secret from Linear's webhook settings\n# - Use \"Linear-Signature\" as the signature header\n# - Use \"type\" to extract the event type from the payload\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/webhooks\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Linear Issues\",\n \"source\": \"linear\",\n \"event_key_expr\": \"type\",\n \"signature_header\": \"Linear-Signature\",\n \"webhook_secret\": \"lin_wh_xxxxxxxxxxxxx\"\n }'\n\n# Response includes webhook_url — configure this in Linear:\n# Settings → API → Webhooks → New webhook → paste the webhook_url\n\n# 2. Create an automation for new Linear issues\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Triage New Linear Issues\",\n \"prompt\": \"A new issue was created in Linear. Analyze the issue title and description, suggest appropriate labels, and add a comment with initial triage notes.\",\n \"trigger\": {\n \"type\": \"event\",\n \"source\": \"linear\",\n \"on\": \"Issue\",\n \"filter\": \"action == '\\''create'\\''\"\n }\n }'\n\n# 3. Create an automation for high-priority issue updates\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"High Priority Issue Alert\",\n \"prompt\": \"A high-priority issue was updated. Review the changes and notify the team if action is needed.\",\n \"trigger\": {\n \"type\": \"event\",\n \"source\": \"linear\",\n \"on\": \"Issue\",\n \"filter\": \"action == '\\''update'\\'' && data.priority == `1`\"\n }\n }'\n```\n\n### Common Signature Headers by Service\n\n| Service | Signature Header | Event Key Expression |\n|---------|-----------------|---------------------|\n| Linear | `Linear-Signature` | `type` |\n| Stripe | `Stripe-Signature` | `type` |\n| Slack | `X-Slack-Signature` | `type` |\n| Twilio | `X-Twilio-Signature` | `type` |\n| Generic | `X-Signature-256` | `type` |\n\n---\n\n### Plugin Preset\n\nUse the **preset/plugin endpoint** when you need to load one or more plugins that provide extended capabilities like skills, MCP configurations, hooks, and commands.\n\n> **💡 Finding plugins:** Browse the [OpenHands/extensions](https://github.com/OpenHands/extensions) repository for available skills and plugins. When given a broad use case, check this directory first to see if something already exists that fits your needs.\n\n#### How It Works\n\n1. Specify one or more plugins (from GitHub repos, git URLs, or monorepo subdirectories)\n2. Provide a prompt that can invoke plugin commands (e.g., `/plugin-name:command`)\n3. The service generates SDK boilerplate that loads all plugins at runtime, creates a conversation with plugin capabilities, and executes the prompt\n4. The service packages everything into a tarball, uploads it, and creates the automation\n\n#### Request\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/plugin\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"My Plugin Automation\",\n \"plugins\": [\n {\"source\": \"github:owner/repo\", \"ref\": \"v1.0.0\"},\n {\"source\": \"github:owner/another-plugin\"}\n ],\n \"prompt\": \"Use the plugin commands to perform the task\",\n \"trigger\": {\n \"type\": \"cron\",\n \"schedule\": \"0 9 * * 1\",\n \"timezone\": \"UTC\"\n }\n }'\n```\n\n#### Request Fields\n\n| Field | Required | Description |\n|-------|----------|-------------|\n| `name` | Yes | Name of the automation (1-500 characters) |\n| `plugins` | Yes | List of plugin sources (at least one required) |\n| `plugins[].source` | Yes | Plugin source: `github:owner/repo`, git URL, or local path |\n| `plugins[].ref` | No | Git ref: branch, tag, or commit SHA |\n| `plugins[].repo_path` | No | Subdirectory path for monorepos |\n| `prompt` | Yes | Instructions for the automation (1-50,000 characters) |\n| `trigger` | Yes | Trigger configuration — either `cron` or `event` (same as Prompt Preset) |\n| `timeout` | No | Max execution time in seconds (default: system maximum) |\n| `repos` | No | Repositories to clone (see [Repository Cloning](#repository-cloning)) |\n\n#### Plugin Source Formats\n\n| Format | Example | Description |\n|--------|---------|-------------|\n| GitHub shorthand | `github:owner/repo` | Fetches from GitHub |\n| Git URL | `https://github.com/owner/repo.git` | Any git repository |\n| With ref | `{\"source\": \"github:owner/repo\", \"ref\": \"v1.0.0\"}` | Specific branch/tag/commit |\n| Monorepo | `{\"source\": \"github:org/monorepo\", \"repo_path\": \"plugins/my-plugin\"}` | Subdirectory in repo |\n\n#### Response (HTTP 201)\n\n```json\n{\n \"id\": \"123e4567-e89b-12d3-a456-426614174000\",\n \"name\": \"My Plugin Automation\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 9 * * 1\", \"timezone\": \"UTC\"},\n \"enabled\": true,\n \"created_at\": \"2025-03-25T10:00:00Z\"\n}\n```\n\n#### Plugin Preset Examples\n\n**Single plugin with version:**\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/plugin\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Code Review Automation\",\n \"plugins\": [\n {\"source\": \"github:owner/code-review-plugin\", \"ref\": \"v2.0.0\"}\n ],\n \"prompt\": \"Review all Python files in the repository for code quality issues\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 9 * * 1-5\", \"timezone\": \"UTC\"}\n }'\n```\n\n**Multiple plugins:**\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/plugin\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Security Scan Automation\",\n \"plugins\": [\n {\"source\": \"github:owner/security-scanner\"},\n {\"source\": \"github:owner/report-generator\", \"ref\": \"main\"}\n ],\n \"prompt\": \"Run a security scan on the codebase and generate a report\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 2 * * 0\", \"timezone\": \"UTC\"},\n \"timeout\": 600\n }'\n```\n\n**Monorepo plugin:**\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/plugin\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Style Guide Enforcement\",\n \"plugins\": [\n {\"source\": \"github:company/monorepo\", \"repo_path\": \"plugins/style-guide\", \"ref\": \"main\"}\n ],\n \"prompt\": \"Check all files against the company style guide\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 8 * * 1\", \"timezone\": \"America/Los_Angeles\"}\n }'\n```\n\n---\n\n## Repository Cloning\n\nBoth presets support an optional `repos` field to clone repositories into the sandbox before execution. Cloned repos have their skills (AGENTS.md, `.agents/skills/`) automatically loaded.\n\n### Repo Source Formats\n\n| Format | Example | Description |\n|--------|---------|-------------|\n| Full URL | `\"https://github.com/owner/repo\"` | Provider auto-detected |\n| Full URL + ref | `{\"url\": \"https://github.com/owner/repo\", \"ref\": \"main\"}` | With branch/tag/SHA |\n| Short URL | `{\"url\": \"owner/repo\", \"provider\": \"github\"}` | Requires `provider` field |\n\n**Supported providers:** `github`, `gitlab`, `bitbucket`\n\n> **Note:** Short URLs (`owner/repo`) require an explicit `provider` field. Full URLs auto-detect the provider.\n\n### Examples\n\n**Single repo (full URL):**\n```json\n{\n \"repos\": [\"https://github.com/OpenHands/openhands-cli\"]\n}\n```\n\n**Multiple repos with refs:**\n```json\n{\n \"repos\": [\n {\"url\": \"https://github.com/owner/repo1\", \"ref\": \"main\"},\n {\"url\": \"https://gitlab.com/owner/repo2\", \"ref\": \"v1.0.0\"}\n ]\n}\n```\n\n**Short URL with provider:**\n```json\n{\n \"repos\": [\n {\"url\": \"owner/repo\", \"provider\": \"github\", \"ref\": \"main\"}\n ]\n}\n```\n\n### Complete Automation Example\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Analyze Codebase\",\n \"prompt\": \"Analyze the openhands-cli codebase and generate a summary report\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 9 * * 1\"},\n \"repos\": [\n {\"url\": \"https://github.com/OpenHands/openhands-cli\", \"ref\": \"main\"}\n ]\n }'\n```\n\n---\n\n## Managing Automations\n\n### List Automations\n\n```bash\ncurl \"${OPENHANDS_HOST}/api/automation/v1?limit=20\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n```\n\n### Get / Update / Delete\n\n```bash\n# Get details\ncurl \"${OPENHANDS_HOST}/api/automation/v1/{automation_id}\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n\n# Update (fields: name, trigger, enabled, timeout)\ncurl -X PATCH \"${OPENHANDS_HOST}/api/automation/v1/{automation_id}\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"enabled\": false}'\n\n# Delete\ncurl -X DELETE \"${OPENHANDS_HOST}/api/automation/v1/{automation_id}\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n```\n\n### Trigger and Monitor Runs\n\n```bash\n# Manually trigger a run\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/{automation_id}/dispatch\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n\n# List runs\ncurl \"${OPENHANDS_HOST}/api/automation/v1/{automation_id}/runs?limit=20\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n```\n\nRun status values: `PENDING` (waiting for dispatch), `RUNNING` (in progress), `COMPLETED` (success), `FAILED` (check `error_detail`).\n\n---\n\n## Run Lifecycle\n\nWhen a run completes, the automation service receives a callback and marks the run done. Any conversations started during the run remain accessible in the OpenHands UI — users can view the history and continue interacting. The agent server persists until it times out or is manually deleted.\n\nThe automation script itself controls when the callback fires (signalling completion). For simple synchronous scripts this happens naturally on exit. For scripts that start asynchronous conversations, the callback should be deferred until the conversation reaches an idle state (see `references/custom-automation.md` for patterns).\n\n---\n\n## Choosing the Right Preset\n\nPick based on **what the task needs**, not just **what is technically possible**. An LLM-driven preset can do almost anything, so \"the preset can satisfy this\" is not by itself a good reason to pick it — every run costs tokens and sandbox time.\n\n| Use Case | Recommended |\n|----------|-------------|\n| Reasoning, summarization, triage, code review, or open-ended tool use | **Prompt Preset** |\n| Needs plugin commands / skills / MCP configs / hooks | **Plugin Preset** |\n| Compare plugin versions or configurations across runs | **Plugin Preset with A/B testing** — see `references/ab-testing.md` |\n| **Deterministic task** (fixed data + scheduled action, e.g. healthcheck, Slack notification, rotating from a known list) — especially if it runs frequently | **Custom script, no LLM** — see `references/custom-automation.md#deterministic-script-no-llm` |\n| Custom Python dependencies, multi-file project, or direct SDK lifecycle control | **Custom script with SDK** — see `references/custom-automation.md#sdk-based-scripts` |\n\nThe **prompt preset** is the right default for genuinely agent-shaped work — anything that benefits from reasoning over context, calling tools dynamically, or producing a non-templated output. Use the **plugin preset** when you need extended capabilities from plugins (skills, MCP configurations, hooks, commands).\n\n**Watch for deterministic, high-frequency patterns.** Requests like \"send a daily standup reminder\", \"ping a healthcheck URL every minute\", \"post a random quote every 5 minutes\", or \"rotate a fact-of-the-day message\" do not need an LLM. Surface this to the user explicitly with a rough cost framing (e.g. \"this schedule will invoke your LLM ~288 times/day\") before defaulting to a preset. As a rule of thumb, any cron tighter than hourly deserves a deliberate \"should this really be agent-driven?\" check.\n\n**When neither preset is the right fit** (deterministic task, custom Python dependencies, non-Python entrypoint, multi-file project structure, direct SDK lifecycle control), explain the options to the user and let them decide. Do not attempt custom automation without explicit user agreement. If they choose the custom route, refer to `references/custom-automation.md`.\n\n## Reference Files\n\n- **`references/custom-automation.md`** — Detailed guide for custom automations: tarball uploads, code structure (SDK and no-LLM), state persistence via the KV store, environment variables, validation rules, and complete examples. Consult this whenever you need to evaluate or recommend the custom path (including for deterministic / cost-sensitive tasks per rule 0). Only *implement* a custom automation after the user agrees to that path.\n- **`references/ab-testing.md`** — A/B testing for plugin automations: defining variants with weights, experiment configuration, variant selection logic, observability via conversation tags, and complete examples. Consult this when a user wants to compare plugin versions or configurations."
297
303
  },
298
304
  {
299
305
  name: "openhands-sdk",
@@ -311,7 +317,7 @@ var e = [
311
317
  name: "pdflatex",
312
318
  description: "Install and use pdflatex to compile LaTeX documents into PDFs on Linux. Use when generating academic papers, research publications, or any documents written in LaTeX.",
313
319
  triggers: ["pdflatex"],
314
- content: "PdfLatex is a tool that converts Latex sources into PDF. This is specifically very important for researchers, as they use it to publish their findings. It could be installed very easily using Linux terminal, though this seems an annoying task on Windows. Installation commands are given below.\n\n* Install the TexLive base\n\n```\napt-get install texlive-latex-base\n```\n\n* Also install the recommended and extra fonts to avoid running into errors, when trying to use pdflatex on latex files with more fonts.\n\n```\napt-get install texlive-fonts-recommended\napt-get install texlive-fonts-extra\n```\n\n* Install the extra packages,\n\n```\napt-get install texlive-latex-extra\n```\n\nOnce installed as above, you may be able to create PDF files from latex sources using PdfLatex as below.\n```\npdflatex latex_source_name.tex\n```\n\nRef: http://kkpradeeban.blogspot.com/2014/04/installing-latexpdflatex-on-ubuntu.html"
320
+ content: "PdfLatex is a tool that converts Latex sources into PDF. This is specifically very important for researchers, as they use it to publish their findings. It could be installed very easily using Linux terminal, though this seems an annoying task on Windows. Installation commands are given below.\n\n* Install the TexLive base\n\n```\napt-get install texlive-latex-base\n```\n\nOn Windows, install MiKTeX or TeX Live with the native installer or a package manager such as `winget`. The `apt-get` commands only work in Linux or WSL.\n\n* Also install the recommended and extra fonts to avoid running into errors, when trying to use pdflatex on latex files with more fonts.\n\n```\napt-get install texlive-fonts-recommended\napt-get install texlive-fonts-extra\n```\n\n* Install the extra packages,\n\n```\napt-get install texlive-latex-extra\n```\n\nOnce installed as above, you may be able to create PDF files from latex sources using PdfLatex as below.\n```\npdflatex latex_source_name.tex\n```\n\nRef: http://kkpradeeban.blogspot.com/2014/04/installing-latexpdflatex-on-ubuntu.html"
315
321
  },
316
322
  {
317
323
  name: "plain-english-content",
@@ -349,7 +355,7 @@ var e = [
349
355
  name: "research-brief",
350
356
  description: "Create an automation that writes a recurring research brief. Uses Tavily MCP for web research and Notion MCP to publish the final brief with executive summary, implications, and source citations.",
351
357
  triggers: ["/research-brief:setup"],
352
- content: "# Research Brief Writer Automation\n\nSet up a recurring automation that researches a topic and publishes a brief\nto Notion.\n\n---\n\n## Prerequisites\n\n### Required integrations\n\nBoth MCP integrations must be installed in Settings → MCP:\n\n- **Tavily MCP** — for web research and source gathering\n- **Notion MCP** — to publish the research brief\n\n### Information to collect\n\nAsk the user for:\n\n1. **Topic** — what should be researched (e.g. \"AI code review tools\", \"competitor pricing changes\")\n2. **Keywords and competitors** — specific terms, companies, or products to track\n3. **Source quality rules** — any preferences on source types (e.g. prefer academic papers, exclude social media)\n4. **Cadence** — how often should the brief run? (daily, weekly, bi-weekly)\n5. **Notion destination** — which Notion database or page should receive the brief\n6. **Citation style** — inline links, footnotes, or a references section\n7. **Brief structure** — default: Executive Summary, Key Findings, Implications, Recommended Actions, Sources\n\n---\n\n## Setup Workflow\n\n### Step 1 — Verify MCP access\n\nTest each integration:\n```\nUse the Tavily MCP to search for a sample topic.\nUse the Notion MCP to search for the destination database.\n```\n\nIf any fail, tell the user which integration needs to be installed first.\n\n### Step 2 — Configure the schedule\n\nBased on the user's cadence preference, build a cron schedule:\n- Daily: `0 8 * * 1-5` (weekday mornings)\n- Weekly: `0 9 * * 1` (Monday morning)\n- Bi-weekly: `0 9 1,15 * *` (1st and 15th)\n\nAsk for timezone preference.\n\n### Step 3 — Build the research prompt\n\nConstruct a prompt that includes:\n- Research topic and keywords\n- Competitor/entity tracking list\n- Source quality preferences\n- Brief structure template\n- Notion destination details\n- Citation format\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\": \"Research Brief Writer\",\n \"prompt\": \"<constructed research prompt>\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"<schedule>\", \"timezone\": \"<tz>\"}\n }'\n```\n\n### Step 5 — Confirm\n\nTell the user:\n> ✅ **Research Brief Writer** is running!\n>\n> - Automation ID: `{id}`\n> - Topic: `{topic}`\n> - Schedule: `{cron description}`\n> - Notion destination: `{destination}`\n> - Citation style: `{style}`"
358
+ content: "# Research Brief Writer Automation\n\nSet up a recurring automation that researches a topic and publishes a brief\nto Notion.\n\n---\n\n## Prerequisites\n\n### Required integrations\n\nBoth MCP integrations must be installed in Settings → MCP:\n\n- **Tavily MCP** — for web research and source gathering\n- **Notion MCP** — to publish the research brief\n\n### Information to collect\n\nAsk the user for:\n\n1. **Topic** — what should be researched (e.g. \"AI code review tools\", \"competitor pricing changes\")\n2. **Keywords and competitors** — specific terms, companies, or products to track\n3. **Source quality rules** — any preferences on source types (e.g. prefer academic papers, exclude social media)\n4. **Cadence** — how often should the brief run? (daily, weekly, bi-weekly)\n5. **Notion destination** — which Notion database or page should receive the brief\n6. **Citation style** — inline links, footnotes, or a references section\n7. **Brief structure** — default: Executive Summary, Key Findings, Implications, Recommended Actions, Sources\n\n---\n\n## Setup Workflow\n\n### Step 1 — Verify MCP access\n\nTest each integration:\n```\nUse the Tavily MCP to search for a sample topic.\nUse the Notion MCP to search for the destination database.\n```\n\nIf any fail, tell the user which integration needs to be installed first.\n\n### Step 2 — Configure the schedule\n\nBased on the user's cadence preference, build a cron schedule:\n- Daily: `0 8 * * 1-5` (weekday mornings)\n- Weekly: `0 9 * * 1` (Monday morning)\n- Bi-weekly: `0 9 1,15 * *` (1st and 15th)\n\nAsk for timezone preference.\n\n### Step 3 — Build the research prompt\n\nConstruct a prompt that includes:\n- Research topic and keywords\n- Competitor/entity tracking list\n- Source quality preferences\n- Brief structure template\n- Notion destination details\n- Citation format\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\": \"Research Brief Writer\",\n \"prompt\": \"<constructed research prompt>\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"<schedule>\", \"timezone\": \"<tz>\"}\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> ✅ **Research Brief Writer** is running!\n>\n> - Automation ID: `{id}`\n> - Topic: `{topic}`\n> - Schedule: `{cron description}`\n> - Notion destination: `{destination}`\n> - Citation style: `{style}`"
353
359
  },
354
360
  {
355
361
  name: "security",
@@ -367,19 +373,19 @@ var e = [
367
373
  name: "skill-creator",
368
374
  description: "This skill should be used when the user wants to \"create a skill\", \"write a new skill\", \"improve skill description\", \"organize skill content\", or needs guidance on skill structure, progressive disclosure, or skill development best practices.",
369
375
  triggers: [],
370
- content: "# Skill Creator\n\nThis skill provides guidance for creating effective skills.\n\n## About Skills\n\nSkills are modular, self-contained packages that extend OpenHands's capabilities by providing\nspecialized knowledge, workflows, and tools. Think of them as \"onboarding guides\" for specific\ndomains or tasks—they transform OpenHands from a general-purpose agent into a specialized agent\nequipped with procedural knowledge that no model can fully possess.\n\n### What Skills Provide\n\n1. Specialized workflows - Multi-step procedures for specific domains\n2. Tool integrations - Instructions for working with specific file formats or APIs\n3. Domain expertise - Company-specific knowledge, schemas, business logic\n4. Bundled resources - Scripts, references, and assets for complex and repetitive tasks\n\n### Anatomy of a Skill\n\nEvery skill consists of a required SKILL.md file and optional bundled resources:\n\n```\nskill-name/\n├── SKILL.md (required)\n│ ├── YAML frontmatter metadata (required)\n│ │ ├── name: (required)\n│ │ └── description: (required)\n│ └── Markdown instructions (required)\n└── Bundled Resources (optional)\n ├── scripts/ - Executable code (Python/Bash/etc.)\n ├── references/ - Documentation intended to be loaded into context as needed\n └── assets/ - Files used in output (templates, icons, fonts, etc.)\n```\n\n#### SKILL.md (required)\n\n**Metadata Quality:** The `name` and `description` in YAML frontmatter determine when OpenHands will use the skill. Be specific about what the skill does and when to use it. Use the third-person (e.g. \"This skill should be used when...\" instead of \"Use this skill when...\").\n\n**Slash commands vs keyword triggers:** SKILL.md frontmatter supports an optional `triggers:` field for keyword-based activation (e.g., `triggers: [docker, container]`). For **slash commands** (e.g., `/codereview`, `/init`), prefer creating a `commands/command-name.md` file in the plugin's `commands/` directory instead of using slash triggers in SKILL.md. Slash triggers still work for backward compatibility but are deprecated in favor of the `commands/` approach. See the [Plugins guide](https://docs.openhands.dev/sdk/guides/plugins) for details.\n\n#### Bundled Resources (optional)\n\n##### Scripts (`scripts/`)\n\nExecutable code (Python/Bash/etc.) for tasks that require deterministic reliability or are repeatedly rewritten.\n\n- **When to include**: When the same code is being rewritten repeatedly or deterministic reliability is needed\n- **Example**: `scripts/rotate_pdf.py` for PDF rotation tasks\n- **Benefits**: Token efficient, deterministic, may be executed without loading into context\n- **Note**: Scripts may still need to be read by OpenHands for patching or environment-specific adjustments\n- **Python dependencies**: Use `uv` instead of `pip` or `pip3` for all Python dependency installs. `uv` is cross-platform, faster, and avoids the `pip`/`pip3` naming inconsistency across environments. Example: `uv venv .venv --quiet && uv pip install --quiet <package>`\n\n##### References (`references/`)\n\nDocumentation and reference material intended to be loaded as needed into context to inform OpenHands's process and thinking.\n\n- **When to include**: For documentation that OpenHands should reference while working\n- **Examples**: `references/finance.md` for financial schemas, `references/mnda.md` for company NDA template, `references/policies.md` for company policies, `references/api_docs.md` for API specifications\n- **Use cases**: Database schemas, API documentation, domain knowledge, company policies, detailed workflow guides\n- **Benefits**: Keeps SKILL.md lean, loaded only when OpenHands determines it's needed\n- **Best practice**: If files are large (>10k words), include grep search patterns in SKILL.md\n- **Avoid duplication**: Information should live in either SKILL.md or references files, not both. Prefer references files for detailed information unless it's truly core to the skill—this keeps SKILL.md lean while making information discoverable without hogging the context window. Keep only essential procedural instructions and workflow guidance in SKILL.md; move detailed reference material, schemas, and examples to references files.\n\n##### Assets (`assets/`)\n\nFiles not intended to be loaded into context, but rather used within the output OpenHands produces.\n\n- **When to include**: When the skill needs files that will be used in the final output\n- **Examples**: `assets/logo.png` for brand assets, `assets/slides.pptx` for PowerPoint templates, `assets/frontend-template/` for HTML/React boilerplate, `assets/font.ttf` for typography\n- **Use cases**: Templates, images, icons, boilerplate code, fonts, sample documents that get copied or modified\n- **Benefits**: Separates output resources from documentation, enables OpenHands to use files without loading them into context\n\n### Progressive Disclosure Design Principle\n\nSkills use a three-level loading system to manage context efficiently:\n\n1. **Metadata (name + description)** - Always in context (~100 words)\n2. **SKILL.md body** - When skill triggers (<5k words)\n3. **Bundled resources** - As needed by OpenHands (Unlimited*)\n\n*Unlimited because scripts can be executed without reading into context window.\n\n## Skill Creation Process\n\nTo create a skill, follow the \"Skill Creation Process\" in order, skipping steps only if there is a clear reason why they are not applicable.\n\n### Step 1: Understanding the Skill with Concrete Examples\n\nSkip this step only when the skill's usage patterns are already clearly understood. It remains valuable even when working with an existing skill.\n\nTo create an effective skill, clearly understand concrete examples of how the skill will be used. This understanding can come from either direct user examples or generated examples that are validated with user feedback.\n\nFor example, when building an image-editor skill, relevant questions include:\n\n- \"What functionality should the image-editor skill support? Editing, rotating, anything else?\"\n- \"Can you give some examples of how this skill would be used?\"\n- \"I can imagine users asking for things like 'Remove the red-eye from this image' or 'Rotate this image'. Are there other ways you imagine this skill being used?\"\n- \"What would a user say that should trigger this skill?\"\n\nTo avoid overwhelming users, avoid asking too many questions in a single message. Start with the most important questions and follow up as needed for better effectiveness.\n\nConclude this step when there is a clear sense of the functionality the skill should support.\n\n### Step 2: Planning the Reusable Skill Contents\n\nTo turn concrete examples into an effective skill, analyze each example by:\n\n1. Considering how to execute on the example from scratch\n2. Identifying what scripts, references, and assets would be helpful when executing these workflows repeatedly\n\nExample: When building a `pdf-editor` skill to handle queries like \"Help me rotate this PDF,\" the analysis shows:\n\n1. Rotating a PDF requires re-writing the same code each time\n2. A `scripts/rotate_pdf.py` script would be helpful to store in the skill\n\nExample: When designing a `frontend-webapp-builder` skill for queries like \"Build me a todo app\" or \"Build me a dashboard to track my steps,\" the analysis shows:\n\n1. Writing a frontend webapp requires the same boilerplate HTML/React each time\n2. An `assets/hello-world/` template containing the boilerplate HTML/React project files would be helpful to store in the skill\n\nExample: When building a `big-query` skill to handle queries like \"How many users have logged in today?\" the analysis shows:\n\n1. Querying BigQuery requires re-discovering the table schemas and relationships each time\n2. A `references/schema.md` file documenting the table schemas would be helpful to store in the skill\n\nTo establish the skill's contents, analyze each concrete example to create a list of the reusable resources to include: scripts, references, and assets.\n\n### Step 3: Create Skill Structure\n\nCreate the skill directory structure:\n\n```bash\nmkdir -p skill-name/{references,scripts,assets}\ntouch skill-name/SKILL.md\n```\n\nAlternatively, use the `init_skill.py` script to generate a template:\n\n```bash\nscripts/init_skill.py <skill-name> --path <output-directory>\n```\n\nThe script creates a skill directory with SKILL.md template and example resource directories.\n\n### Step 4: Edit the Skill\n\nWhen editing the (newly-created or existing) skill, remember that the skill is being created for another instance of OpenHands to use. Focus on including information that would be beneficial and non-obvious to OpenHands. Consider what procedural knowledge, domain-specific details, or reusable assets would help another OpenHands instance execute these tasks more effectively.\n\n#### Start with Reusable Skill Contents\n\nTo begin implementation, start with the reusable resources identified above: `scripts/`, `references/`, and `assets/` files. Note that this step may require user input. For example, when implementing a `brand-guidelines` skill, the user may need to provide brand assets or templates to store in `assets/`, or documentation to store in `references/`.\n\nAlso, delete any example files and directories not needed for the skill. Create only the directories you actually need (references/, scripts/, assets/).\n\n#### Update SKILL.md\n\n**Writing Style:** Write the entire skill using **imperative/infinitive form** (verb-first instructions), not second person. Use objective, instructional language (e.g., \"To accomplish X, do Y\" rather than \"You should do X\" or \"If you need to do X\"). This maintains consistency and clarity for AI consumption.\n\n**Description (Frontmatter):** Use third-person format with specific trigger phrases:\n\n```yaml\n---\nname: skill-name\ndescription: This skill should be used when the user asks to \"specific phrase 1\", \"specific phrase 2\", \"specific phrase 3\". Include exact phrases users would say that should trigger this skill. Be concrete and specific.\n---\n```\n\n**Good description examples:**\n```yaml\ndescription: This skill should be used when the user asks to \"create a hook\", \"add a PreToolUse hook\", \"validate tool use\", \"implement prompt-based hooks\", or mentions hook events (PreToolUse, PostToolUse, Stop).\n```\n\n**Bad description examples:**\n```yaml\ndescription: Use this skill when working with hooks. # Wrong person, vague\ndescription: Load when user needs hook help. # Not third person\ndescription: Provides hook guidance. # No trigger phrases\n```\n\nTo complete SKILL.md body, answer the following questions:\n\n1. What is the purpose of the skill, in a few sentences?\n2. When should the skill be used? (Include this in frontmatter description with specific triggers)\n3. In practice, how should OpenHands use the skill? All reusable skill contents developed above should be referenced so that OpenHands knows how to use them.\n\n**Keep SKILL.md lean:** Target 1,500-2,000 words for the body. Move detailed content to references/:\n- Detailed patterns → `references/patterns.md`\n- Advanced techniques → `references/advanced.md`\n- Migration guides → `references/migration.md`\n- API references → `references/api-reference.md`\n\n**Reference resources in SKILL.md:**\n```markdown\n## Additional Resources\n\n### Reference Files\n\nFor detailed patterns and techniques, consult:\n- **`references/patterns.md`** - Common patterns\n- **`references/advanced.md`** - Advanced use cases\n\n### Example Files\n\nWorking examples in `examples/`:\n- **`example-script.sh`** - Working example\n```\n\n### Step 5: Validate and Test\n\n1. **Check structure**: Skill directory contains SKILL.md\n2. **Validate SKILL.md**: Has frontmatter with name and description\n3. **Check trigger phrases**: Description includes specific user queries\n4. **Verify writing style**: Body uses imperative/infinitive form, not second person\n5. **Test progressive disclosure**: SKILL.md is lean (~1,500-2,000 words), detailed content in references/\n6. **Check references**: All referenced files exist\n7. **Validate scripts**: Scripts are executable and work correctly\n\nUse the validation script to check basic requirements:\n```bash\nscripts/quick_validate.py <path/to/skill-folder>\n```\n\n### Step 6: Iterate\n\nAfter testing the skill, users may request improvements. Often this happens right after using the skill, with fresh context of how the skill performed.\n\n**Iteration workflow:**\n1. Use the skill on real tasks\n2. Notice struggles or inefficiencies\n3. Identify how SKILL.md or bundled resources should be updated\n4. Implement changes and test again\n\n**Common improvements:**\n- Strengthen trigger phrases in description\n- Move long sections from SKILL.md to references/\n- Add missing examples or scripts\n- Clarify ambiguous instructions\n- Add edge case handling\n\n## Progressive Disclosure in Practice\n\n### What Goes in SKILL.md\n\n**Include (always loaded when skill triggers):**\n- Core concepts and overview\n- Essential procedures and workflows\n- Quick reference tables\n- Pointers to references/examples/scripts\n- Most common use cases\n\n**Keep under 3,000 words, ideally 1,500-2,000 words**\n\n### What Goes in references/\n\n**Move to references/ (loaded as needed):**\n- Detailed patterns and advanced techniques\n- Comprehensive API documentation\n- Migration guides\n- Edge cases and troubleshooting\n- Extensive examples and walkthroughs\n\n**Each reference file can be large (2,000-5,000+ words)**\n\n### What Goes in scripts/\n\n**Utility scripts:**\n- Validation tools\n- Testing helpers\n- Parsing utilities\n- Automation scripts\n\n**Should be executable and documented**\n\n## Writing Style Requirements\n\n### Imperative/Infinitive Form\n\nWrite using verb-first instructions, not second person:\n\n**Correct (imperative):**\n```\nTo create a hook, define the event type.\nConfigure the MCP server with authentication.\nValidate settings before use.\n```\n\n**Incorrect (second person):**\n```\nYou should create a hook by defining the event type.\nYou need to configure the MCP server.\nYou must validate settings before use.\n```\n\n### Third-Person in Description\n\nThe frontmatter description must use third person:\n\n**Correct:**\n```yaml\ndescription: This skill should be used when the user asks to \"create X\", \"configure Y\"...\n```\n\n**Incorrect:**\n```yaml\ndescription: Use this skill when you want to create X...\ndescription: Load this skill when user asks...\n```\n\n### Objective, Instructional Language\n\nFocus on what to do, not who should do it:\n\n**Correct:**\n```\nParse the frontmatter using sed.\nExtract fields with grep.\nValidate values before use.\n```\n\n**Incorrect:**\n```\nYou can parse the frontmatter...\nOpenHands should extract fields...\nThe user might validate values...\n```\n\n## Validation Checklist\n\nBefore finalizing a skill:\n\n**Structure:**\n- [ ] SKILL.md file exists with valid YAML frontmatter\n- [ ] Frontmatter has `name` and `description` fields\n- [ ] Markdown body is present and substantial\n- [ ] Referenced files actually exist\n\n**Description Quality:**\n- [ ] Uses third person (\"This skill should be used when...\")\n- [ ] Includes specific trigger phrases users would say\n- [ ] Lists concrete scenarios (\"create X\", \"configure Y\")\n- [ ] Not vague or generic\n\n**Content Quality:**\n- [ ] SKILL.md body uses imperative/infinitive form\n- [ ] Body is focused and lean (1,500-2,000 words ideal, <5k max)\n- [ ] Detailed content moved to references/\n- [ ] Examples are complete and working\n- [ ] Scripts are executable and documented\n\n**Progressive Disclosure:**\n- [ ] Core concepts in SKILL.md\n- [ ] Detailed docs in references/\n- [ ] Utilities in scripts/\n- [ ] SKILL.md references these resources\n\n**Testing:**\n- [ ] Skill triggers on expected user queries\n- [ ] Content is helpful for intended tasks\n- [ ] No duplicated information across files\n- [ ] References load when needed\n\n## Common Mistakes to Avoid\n\n### Mistake 1: Weak Trigger Description\n\n❌ **Bad:**\n```yaml\ndescription: Provides guidance for working with hooks.\n```\n\n**Why bad:** Vague, no specific trigger phrases, not third person\n\n✅ **Good:**\n```yaml\ndescription: This skill should be used when the user asks to \"create a hook\", \"add a PreToolUse hook\", \"validate tool use\", or mentions hook events. Provides comprehensive hooks API guidance.\n```\n\n**Why good:** Third person, specific phrases, concrete scenarios\n\n### Mistake 2: Too Much in SKILL.md\n\n❌ **Bad:**\n```\nskill-name/\n└── SKILL.md (8,000 words - everything in one file)\n```\n\n**Why bad:** Bloats context when skill loads, detailed content always loaded\n\n✅ **Good:**\n```\nskill-name/\n├── SKILL.md (1,800 words - core essentials)\n└── references/\n ├── patterns.md (2,500 words)\n └── advanced.md (3,700 words)\n```\n\n**Why good:** Progressive disclosure, detailed content loaded only when needed\n\n### Mistake 3: Second Person Writing\n\n❌ **Bad:**\n```markdown\nYou should start by reading the configuration file.\nYou need to validate the input.\nYou can use the grep tool to search.\n```\n\n**Why bad:** Second person, not imperative form\n\n✅ **Good:**\n```markdown\nStart by reading the configuration file.\nValidate the input before processing.\nUse the grep tool to search for patterns.\n```\n\n**Why good:** Imperative form, direct instructions\n\n### Mistake 4: Missing Resource References\n\n❌ **Bad:**\n```markdown\n# SKILL.md\n\n[Core content]\n\n[No mention of references/ or examples/]\n```\n\n**Why bad:** OpenHands doesn't know references exist\n\n✅ **Good:**\n```markdown\n# SKILL.md\n\n[Core content]\n\n## Additional Resources\n\n### Reference Files\n- **`references/patterns.md`** - Detailed patterns\n- **`references/advanced.md`** - Advanced techniques\n\n### Scripts\n- **`scripts/validate.sh`** - Validation utility\n```\n\n**Why good:** OpenHands knows where to find additional information\n\n## Quick Reference\n\n### Minimal Skill\n\n```\nskill-name/\n└── SKILL.md\n```\n\nGood for: Simple knowledge, no complex resources needed\n\n### Standard Skill (Recommended)\n\n```\nskill-name/\n├── SKILL.md\n├── references/\n│ └── detailed-guide.md\n└── scripts/\n └── helper.py\n```\n\nGood for: Most skills with detailed documentation\n\n### Complete Skill\n\n```\nskill-name/\n├── SKILL.md\n├── references/\n│ ├── patterns.md\n│ └── advanced.md\n├── scripts/\n│ └── validate.sh\n└── assets/\n └── template.txt\n```\n\nGood for: Complex domains with validation utilities\n\n## Best Practices Summary\n\n✅ **DO:**\n- Use third-person in description (\"This skill should be used when...\")\n- Include specific trigger phrases (\"create X\", \"configure Y\")\n- Keep SKILL.md lean (1,500-2,000 words)\n- Use progressive disclosure (move details to references/)\n- Write in imperative/infinitive form\n- Reference supporting files clearly\n- Provide working examples\n- Create utility scripts for common operations\n- Use `uv` for Python dependency installs in scripts (`uv venv .venv --quiet && uv pip install --quiet <pkg>`)\n\n❌ **DON'T:**\n- Use second person anywhere\n- Have vague trigger conditions\n- Put everything in SKILL.md (>3,000 words without references/)\n- Write in second person (\"You should...\")\n- Leave resources unreferenced\n- Include broken or incomplete examples\n- Skip validation\n- Use `pip` or `pip3` directly — `uv` is the cross-platform standard\n\n## Additional Resources\n\n### Reference Files\n\nFor detailed patterns and techniques, consult:\n- **`references/workflows.md`** - Sequential workflows and conditional logic patterns\n- **`references/output-patterns.md`** - Template and example patterns for specific output formats\n\n## Implementation Workflow\n\nTo create a skill:\n\n1. **Understand use cases**: Identify concrete examples of skill usage\n2. **Plan resources**: Determine what scripts/references/assets needed\n3. **Create structure**: `mkdir -p skill-name/{references,scripts,assets}`\n4. **Write SKILL.md**:\n - Frontmatter with third-person description and trigger phrases\n - Lean body (1,500-2,000 words) in imperative form\n - Reference supporting files\n5. **Add resources**: Create references/, scripts/, assets/ as needed\n6. **Validate**: Check description, writing style, organization\n7. **Test**: Verify skill loads on expected triggers\n8. **Iterate**: Improve based on usage\n\nFocus on strong trigger descriptions, progressive disclosure, and imperative writing style for effective skills that load when needed and provide targeted guidance."
376
+ content: "# Skill Creator\n\nThis skill provides guidance for creating effective skills.\nWindows PowerShell equivalents for the Unix shell commands used in examples are in `references/windows.md`.\n\n## About Skills\n\nSkills are modular, self-contained packages that extend OpenHands's capabilities by providing\nspecialized knowledge, workflows, and tools. Think of them as \"onboarding guides\" for specific\ndomains or tasks—they transform OpenHands from a general-purpose agent into a specialized agent\nequipped with procedural knowledge that no model can fully possess.\n\n### What Skills Provide\n\n1. Specialized workflows - Multi-step procedures for specific domains\n2. Tool integrations - Instructions for working with specific file formats or APIs\n3. Domain expertise - Company-specific knowledge, schemas, business logic\n4. Bundled resources - Scripts, references, and assets for complex and repetitive tasks\n\n### Anatomy of a Skill\n\nEvery skill consists of a required SKILL.md file and optional bundled resources:\n\n```\nskill-name/\n├── SKILL.md (required)\n│ ├── YAML frontmatter metadata (required)\n│ │ ├── name: (required)\n│ │ └── description: (required)\n│ └── Markdown instructions (required)\n└── Bundled Resources (optional)\n ├── scripts/ - Executable code (Python/Bash/etc.)\n ├── references/ - Documentation intended to be loaded into context as needed\n └── assets/ - Files used in output (templates, icons, fonts, etc.)\n```\n\n#### SKILL.md (required)\n\n**Metadata Quality:** The `name` and `description` in YAML frontmatter determine when OpenHands will use the skill. Be specific about what the skill does and when to use it. Use the third-person (e.g. \"This skill should be used when...\" instead of \"Use this skill when...\").\n\n**Slash commands vs keyword triggers:** SKILL.md frontmatter supports an optional `triggers:` field for keyword-based activation (e.g., `triggers: [docker, container]`). For **slash commands** (e.g., `/codereview`, `/init`), prefer creating a `commands/command-name.md` file in the plugin's `commands/` directory instead of using slash triggers in SKILL.md. Slash triggers still work for backward compatibility but are deprecated in favor of the `commands/` approach. See the [Plugins guide](https://docs.openhands.dev/sdk/guides/plugins) for details.\n\n#### Bundled Resources (optional)\n\n##### Scripts (`scripts/`)\n\nExecutable code (Python/Bash/etc.) for tasks that require deterministic reliability or are repeatedly rewritten.\n\n- **When to include**: When the same code is being rewritten repeatedly or deterministic reliability is needed\n- **Example**: `scripts/rotate_pdf.py` for PDF rotation tasks\n- **Benefits**: Token efficient, deterministic, may be executed without loading into context\n- **Note**: Scripts may still need to be read by OpenHands for patching or environment-specific adjustments\n- **Python dependencies**: Use `uv` instead of `pip` or `pip3` for all Python dependency installs. `uv` is cross-platform, faster, and avoids the `pip`/`pip3` naming inconsistency across environments. Example: `uv venv .venv --quiet && uv pip install --quiet <package>`\n\n##### References (`references/`)\n\nDocumentation and reference material intended to be loaded as needed into context to inform OpenHands's process and thinking.\n\n- **When to include**: For documentation that OpenHands should reference while working\n- **Examples**: `references/finance.md` for financial schemas, `references/mnda.md` for company NDA template, `references/policies.md` for company policies, `references/api_docs.md` for API specifications\n- **Use cases**: Database schemas, API documentation, domain knowledge, company policies, detailed workflow guides\n- **Benefits**: Keeps SKILL.md lean, loaded only when OpenHands determines it's needed\n- **Best practice**: If files are large (>10k words), include grep search patterns in SKILL.md\n- **Avoid duplication**: Information should live in either SKILL.md or references files, not both. Prefer references files for detailed information unless it's truly core to the skill—this keeps SKILL.md lean while making information discoverable without hogging the context window. Keep only essential procedural instructions and workflow guidance in SKILL.md; move detailed reference material, schemas, and examples to references files.\n\n##### Assets (`assets/`)\n\nFiles not intended to be loaded into context, but rather used within the output OpenHands produces.\n\n- **When to include**: When the skill needs files that will be used in the final output\n- **Examples**: `assets/logo.png` for brand assets, `assets/slides.pptx` for PowerPoint templates, `assets/frontend-template/` for HTML/React boilerplate, `assets/font.ttf` for typography\n- **Use cases**: Templates, images, icons, boilerplate code, fonts, sample documents that get copied or modified\n- **Benefits**: Separates output resources from documentation, enables OpenHands to use files without loading them into context\n\n### Progressive Disclosure Design Principle\n\nSkills use a three-level loading system to manage context efficiently:\n\n1. **Metadata (name + description)** - Always in context (~100 words)\n2. **SKILL.md body** - When skill triggers (<5k words)\n3. **Bundled resources** - As needed by OpenHands (Unlimited*)\n\n*Unlimited because scripts can be executed without reading into context window.\n\n## Skill Creation Process\n\nTo create a skill, follow the \"Skill Creation Process\" in order, skipping steps only if there is a clear reason why they are not applicable.\n\n### Step 1: Understanding the Skill with Concrete Examples\n\nSkip this step only when the skill's usage patterns are already clearly understood. It remains valuable even when working with an existing skill.\n\nTo create an effective skill, clearly understand concrete examples of how the skill will be used. This understanding can come from either direct user examples or generated examples that are validated with user feedback.\n\nFor example, when building an image-editor skill, relevant questions include:\n\n- \"What functionality should the image-editor skill support? Editing, rotating, anything else?\"\n- \"Can you give some examples of how this skill would be used?\"\n- \"I can imagine users asking for things like 'Remove the red-eye from this image' or 'Rotate this image'. Are there other ways you imagine this skill being used?\"\n- \"What would a user say that should trigger this skill?\"\n\nTo avoid overwhelming users, avoid asking too many questions in a single message. Start with the most important questions and follow up as needed for better effectiveness.\n\nConclude this step when there is a clear sense of the functionality the skill should support.\n\n### Step 2: Planning the Reusable Skill Contents\n\nTo turn concrete examples into an effective skill, analyze each example by:\n\n1. Considering how to execute on the example from scratch\n2. Identifying what scripts, references, and assets would be helpful when executing these workflows repeatedly\n\nExample: When building a `pdf-editor` skill to handle queries like \"Help me rotate this PDF,\" the analysis shows:\n\n1. Rotating a PDF requires re-writing the same code each time\n2. A `scripts/rotate_pdf.py` script would be helpful to store in the skill\n\nExample: When designing a `frontend-webapp-builder` skill for queries like \"Build me a todo app\" or \"Build me a dashboard to track my steps,\" the analysis shows:\n\n1. Writing a frontend webapp requires the same boilerplate HTML/React each time\n2. An `assets/hello-world/` template containing the boilerplate HTML/React project files would be helpful to store in the skill\n\nExample: When building a `big-query` skill to handle queries like \"How many users have logged in today?\" the analysis shows:\n\n1. Querying BigQuery requires re-discovering the table schemas and relationships each time\n2. A `references/schema.md` file documenting the table schemas would be helpful to store in the skill\n\nTo establish the skill's contents, analyze each concrete example to create a list of the reusable resources to include: scripts, references, and assets.\n\n### Step 3: Create Skill Structure\n\nCreate the skill directory structure:\n\n```bash\nmkdir -p skill-name/{references,scripts,assets}\ntouch skill-name/SKILL.md\n```\n\nAlternatively, use the `init_skill.py` script to generate a template:\n\n```bash\nscripts/init_skill.py <skill-name> --path <output-directory>\n```\n\nThe script creates a skill directory with SKILL.md template and example resource directories.\n\n### Step 4: Edit the Skill\n\nWhen editing the (newly-created or existing) skill, remember that the skill is being created for another instance of OpenHands to use. Focus on including information that would be beneficial and non-obvious to OpenHands. Consider what procedural knowledge, domain-specific details, or reusable assets would help another OpenHands instance execute these tasks more effectively.\n\n#### Start with Reusable Skill Contents\n\nTo begin implementation, start with the reusable resources identified above: `scripts/`, `references/`, and `assets/` files. Note that this step may require user input. For example, when implementing a `brand-guidelines` skill, the user may need to provide brand assets or templates to store in `assets/`, or documentation to store in `references/`.\n\nAlso, delete any example files and directories not needed for the skill. Create only the directories you actually need (references/, scripts/, assets/).\n\n#### Update SKILL.md\n\n**Writing Style:** Write the entire skill using **imperative/infinitive form** (verb-first instructions), not second person. Use objective, instructional language (e.g., \"To accomplish X, do Y\" rather than \"You should do X\" or \"If you need to do X\"). This maintains consistency and clarity for AI consumption.\n\n**Description (Frontmatter):** Use third-person format with specific trigger phrases:\n\n```yaml\n---\nname: skill-name\ndescription: This skill should be used when the user asks to \"specific phrase 1\", \"specific phrase 2\", \"specific phrase 3\". Include exact phrases users would say that should trigger this skill. Be concrete and specific.\n---\n```\n\n**Good description examples:**\n```yaml\ndescription: This skill should be used when the user asks to \"create a hook\", \"add a PreToolUse hook\", \"validate tool use\", \"implement prompt-based hooks\", or mentions hook events (PreToolUse, PostToolUse, Stop).\n```\n\n**Bad description examples:**\n```yaml\ndescription: Use this skill when working with hooks. # Wrong person, vague\ndescription: Load when user needs hook help. # Not third person\ndescription: Provides hook guidance. # No trigger phrases\n```\n\nTo complete SKILL.md body, answer the following questions:\n\n1. What is the purpose of the skill, in a few sentences?\n2. When should the skill be used? (Include this in frontmatter description with specific triggers)\n3. In practice, how should OpenHands use the skill? All reusable skill contents developed above should be referenced so that OpenHands knows how to use them.\n\n**Keep SKILL.md lean:** Target 1,500-2,000 words for the body. Move detailed content to references/:\n- Detailed patterns → `references/patterns.md`\n- Advanced techniques → `references/advanced.md`\n- Migration guides → `references/migration.md`\n- API references → `references/api-reference.md`\n\n**Reference resources in SKILL.md:**\n```markdown\n## Additional Resources\n\n### Reference Files\n\nFor detailed patterns and techniques, consult:\n- **`references/patterns.md`** - Common patterns\n- **`references/advanced.md`** - Advanced use cases\n\n### Example Files\n\nWorking examples in `examples/`:\n- **`example-script.sh`** - Working example\n```\n\n### Step 5: Validate and Test\n\n1. **Check structure**: Skill directory contains SKILL.md\n2. **Validate SKILL.md**: Has frontmatter with name and description\n3. **Check trigger phrases**: Description includes specific user queries\n4. **Verify writing style**: Body uses imperative/infinitive form, not second person\n5. **Test progressive disclosure**: SKILL.md is lean (~1,500-2,000 words), detailed content in references/\n6. **Check references**: All referenced files exist\n7. **Validate scripts**: Scripts are executable and work correctly\n\nUse the validation script to check basic requirements:\n```bash\nscripts/quick_validate.py <path/to/skill-folder>\n```\n\n### Step 6: Iterate\n\nAfter testing the skill, users may request improvements. Often this happens right after using the skill, with fresh context of how the skill performed.\n\n**Iteration workflow:**\n1. Use the skill on real tasks\n2. Notice struggles or inefficiencies\n3. Identify how SKILL.md or bundled resources should be updated\n4. Implement changes and test again\n\n**Common improvements:**\n- Strengthen trigger phrases in description\n- Move long sections from SKILL.md to references/\n- Add missing examples or scripts\n- Clarify ambiguous instructions\n- Add edge case handling\n\n## Progressive Disclosure in Practice\n\n### What Goes in SKILL.md\n\n**Include (always loaded when skill triggers):**\n- Core concepts and overview\n- Essential procedures and workflows\n- Quick reference tables\n- Pointers to references/examples/scripts\n- Most common use cases\n\n**Keep under 3,000 words, ideally 1,500-2,000 words**\n\n### What Goes in references/\n\n**Move to references/ (loaded as needed):**\n- Detailed patterns and advanced techniques\n- Comprehensive API documentation\n- Migration guides\n- Edge cases and troubleshooting\n- Extensive examples and walkthroughs\n\n**Each reference file can be large (2,000-5,000+ words)**\n\n### What Goes in scripts/\n\n**Utility scripts:**\n- Validation tools\n- Testing helpers\n- Parsing utilities\n- Automation scripts\n\n**Should be executable and documented**\n\n## Writing Style Requirements\n\n### Imperative/Infinitive Form\n\nWrite using verb-first instructions, not second person:\n\n**Correct (imperative):**\n```\nTo create a hook, define the event type.\nConfigure the MCP server with authentication.\nValidate settings before use.\n```\n\n**Incorrect (second person):**\n```\nYou should create a hook by defining the event type.\nYou need to configure the MCP server.\nYou must validate settings before use.\n```\n\n### Third-Person in Description\n\nThe frontmatter description must use third person:\n\n**Correct:**\n```yaml\ndescription: This skill should be used when the user asks to \"create X\", \"configure Y\"...\n```\n\n**Incorrect:**\n```yaml\ndescription: Use this skill when you want to create X...\ndescription: Load this skill when user asks...\n```\n\n### Objective, Instructional Language\n\nFocus on what to do, not who should do it:\n\n**Correct:**\n```\nParse the frontmatter using sed.\nExtract fields with grep.\nValidate values before use.\n```\n\n**Incorrect:**\n```\nYou can parse the frontmatter...\nOpenHands should extract fields...\nThe user might validate values...\n```\n\n## Validation Checklist\n\nBefore finalizing a skill:\n\n**Structure:**\n- [ ] SKILL.md file exists with valid YAML frontmatter\n- [ ] Frontmatter has `name` and `description` fields\n- [ ] Markdown body is present and substantial\n- [ ] Referenced files actually exist\n\n**Description Quality:**\n- [ ] Uses third person (\"This skill should be used when...\")\n- [ ] Includes specific trigger phrases users would say\n- [ ] Lists concrete scenarios (\"create X\", \"configure Y\")\n- [ ] Not vague or generic\n\n**Content Quality:**\n- [ ] SKILL.md body uses imperative/infinitive form\n- [ ] Body is focused and lean (1,500-2,000 words ideal, <5k max)\n- [ ] Detailed content moved to references/\n- [ ] Examples are complete and working\n- [ ] Scripts are executable and documented\n\n**Progressive Disclosure:**\n- [ ] Core concepts in SKILL.md\n- [ ] Detailed docs in references/\n- [ ] Utilities in scripts/\n- [ ] SKILL.md references these resources\n\n**Testing:**\n- [ ] Skill triggers on expected user queries\n- [ ] Content is helpful for intended tasks\n- [ ] No duplicated information across files\n- [ ] References load when needed\n\n## Common Mistakes to Avoid\n\n### Mistake 1: Weak Trigger Description\n\n❌ **Bad:**\n```yaml\ndescription: Provides guidance for working with hooks.\n```\n\n**Why bad:** Vague, no specific trigger phrases, not third person\n\n✅ **Good:**\n```yaml\ndescription: This skill should be used when the user asks to \"create a hook\", \"add a PreToolUse hook\", \"validate tool use\", or mentions hook events. Provides comprehensive hooks API guidance.\n```\n\n**Why good:** Third person, specific phrases, concrete scenarios\n\n### Mistake 2: Too Much in SKILL.md\n\n❌ **Bad:**\n```\nskill-name/\n└── SKILL.md (8,000 words - everything in one file)\n```\n\n**Why bad:** Bloats context when skill loads, detailed content always loaded\n\n✅ **Good:**\n```\nskill-name/\n├── SKILL.md (1,800 words - core essentials)\n└── references/\n ├── patterns.md (2,500 words)\n └── advanced.md (3,700 words)\n```\n\n**Why good:** Progressive disclosure, detailed content loaded only when needed\n\n### Mistake 3: Second Person Writing\n\n❌ **Bad:**\n```markdown\nYou should start by reading the configuration file.\nYou need to validate the input.\nYou can use the grep tool to search.\n```\n\n**Why bad:** Second person, not imperative form\n\n✅ **Good:**\n```markdown\nStart by reading the configuration file.\nValidate the input before processing.\nUse the grep tool to search for patterns.\n```\n\n**Why good:** Imperative form, direct instructions\n\n### Mistake 4: Missing Resource References\n\n❌ **Bad:**\n```markdown\n# SKILL.md\n\n[Core content]\n\n[No mention of references/ or examples/]\n```\n\n**Why bad:** OpenHands doesn't know references exist\n\n✅ **Good:**\n```markdown\n# SKILL.md\n\n[Core content]\n\n## Additional Resources\n\n### Reference Files\n- **`references/patterns.md`** - Detailed patterns\n- **`references/advanced.md`** - Advanced techniques\n\n### Scripts\n- **`scripts/validate.sh`** - Validation utility\n```\n\n**Why good:** OpenHands knows where to find additional information\n\n## Quick Reference\n\n### Minimal Skill\n\n```\nskill-name/\n└── SKILL.md\n```\n\nGood for: Simple knowledge, no complex resources needed\n\n### Standard Skill (Recommended)\n\n```\nskill-name/\n├── SKILL.md\n├── references/\n│ └── detailed-guide.md\n└── scripts/\n └── helper.py\n```\n\nGood for: Most skills with detailed documentation\n\n### Complete Skill\n\n```\nskill-name/\n├── SKILL.md\n├── references/\n│ ├── patterns.md\n│ └── advanced.md\n├── scripts/\n│ └── validate.sh\n└── assets/\n └── template.txt\n```\n\nGood for: Complex domains with validation utilities\n\n## Best Practices Summary\n\n✅ **DO:**\n- Use third-person in description (\"This skill should be used when...\")\n- Include specific trigger phrases (\"create X\", \"configure Y\")\n- Keep SKILL.md lean (1,500-2,000 words)\n- Use progressive disclosure (move details to references/)\n- Write in imperative/infinitive form\n- Reference supporting files clearly\n- Provide working examples\n- Create utility scripts for common operations\n- Use `uv` for Python dependency installs in scripts (`uv venv .venv --quiet && uv pip install --quiet <pkg>`)\n\n❌ **DON'T:**\n- Use second person anywhere\n- Have vague trigger conditions\n- Put everything in SKILL.md (>3,000 words without references/)\n- Write in second person (\"You should...\")\n- Leave resources unreferenced\n- Include broken or incomplete examples\n- Skip validation\n- Use `pip` or `pip3` directly — `uv` is the cross-platform standard\n\n## Additional Resources\n\n### Reference Files\n\nFor detailed patterns and techniques, consult:\n- **`references/workflows.md`** - Sequential workflows and conditional logic patterns\n- **`references/output-patterns.md`** - Template and example patterns for specific output formats\n\n## Implementation Workflow\n\nTo create a skill:\n\n1. **Understand use cases**: Identify concrete examples of skill usage\n2. **Plan resources**: Determine what scripts/references/assets needed\n3. **Create structure**: `mkdir -p skill-name/{references,scripts,assets}`\n4. **Write SKILL.md**:\n - Frontmatter with third-person description and trigger phrases\n - Lean body (1,500-2,000 words) in imperative form\n - Reference supporting files\n5. **Add resources**: Create references/, scripts/, assets/ as needed\n6. **Validate**: Check description, writing style, organization\n7. **Test**: Verify skill loads on expected triggers\n8. **Iterate**: Improve based on usage\n\nFocus on strong trigger descriptions, progressive disclosure, and imperative writing style for effective skills that load when needed and provide targeted guidance."
371
377
  },
372
378
  {
373
379
  name: "slack-channel-monitor",
374
380
  description: "This skill should be used when the user asks to \"monitor a Slack channel\", \"watch Slack for messages\", \"create a Slack bot that responds to mentions\", \"set up an OpenHands Slack integration\", \"trigger OpenHands from Slack\", \"respond to @openhands in Slack\", or \"poll Slack channels for a trigger phrase\". Guides the user through creating a cron automation that watches up to 10 Slack channels and starts an OpenHands conversation whenever a configurable trigger phrase is detected.",
375
381
  triggers: ["/slack-monitor:poll"],
376
- content: "# Slack Channel Monitor\n\nCreate a cron automation that polls up to 10 Slack channels every minute.\nWhen a message containing the **trigger phrase** (default: `@openhands`) is\ndetected it:\n\n1. Adds a 👀 reaction to the triggering message.\n2. Opens an OpenHands conversation with the message and recent channel context.\n3. Posts a reply in the Slack thread with a link to the conversation.\n\nOn every subsequent run:\n- New Slack thread replies are forwarded only when they contain the trigger\n phrase, so unrelated conversation in the thread is ignored.\n- When the conversation finishes (or errors), the agent's final response is\n posted back to the Slack thread.\n- Completed conversations stay in a short follow-up watch window, allowing\n triggered Slack replies to continue the same OpenHands conversation.\n\n> **Local mode only.** This automation targets the local OpenHands setup\n> (`dev:automation` stack). A cloud/webhook-based variant is out of scope here.\n\n---\n\n## Prerequisites\n\n### Required secrets\n\nVerify that at least one of the following secrets is set in\n**OpenHands Settings → Secrets** before proceeding:\n\n| Secret name | Token type | Minimum scopes |\n|---|---|---|\n| `SLACK_BOT_TOKEN` | Bot (`xoxb-…`) | `channels:history`, `channels:read`, `reactions:write`, `chat:write` |\n| `SLACK_USER_TOKEN` | User (`xoxp-…`) | Same as bot, plus `search:read` for multi-channel efficiency |\n\nCheck with:\n```bash\n# For bot token:\ncurl -s https://slack.com/api/auth.test -H \"Authorization: Bearer $SLACK_BOT_TOKEN\" \\\n | python3 -c \"import json,sys; d=json.load(sys.stdin); print('ok' if d.get('ok') else d.get('error'))\"\n\n# For user token:\ncurl -s https://slack.com/api/auth.test -H \"Authorization: Bearer $SLACK_USER_TOKEN\" \\\n | python3 -c \"import json,sys; d=json.load(sys.stdin); print('ok' if d.get('ok') else d.get('error'))\"\n```\n\nIf neither token is present, inform the user and stop - the automation cannot\nfunction without Slack credentials.\n\n### Optional secret\n\n| Secret name | Default | Purpose |\n|---|---|---|\n| `OPENHANDS_URL` | `http://localhost:8000` | Base URL used to build conversation links posted in Slack |\n\n---\n\n## Setup Workflow\n\nFollow these steps in order.\n\n### Step 1 - Collect channels\n\nAsk the user: *\"Which Slack channels should be monitored? You can provide\nchannel names (e.g. `#general`) or IDs (e.g. `C0123456789`).\"*\n\n**If the user provides channel names**, resolve them to IDs:\n\n```bash\nSLACK_TOKEN=\"${SLACK_BOT_TOKEN:-$SLACK_USER_TOKEN}\"\ncurl -s \"https://slack.com/api/conversations.list?types=public_channel,private_channel&limit=200&exclude_archived=true\" \\\n -H \"Authorization: Bearer $SLACK_TOKEN\" \\\n | python3 -c \"\nimport json, sys\ndata = json.load(sys.stdin)\nif not data.get('ok'):\n print('ERROR:', data.get('error'))\n exit(1)\nnames = set(n.lstrip('#') for n in ['CHANNEL_NAMES_HERE'.split(',')])\nfor ch in data.get('channels', []):\n if ch['name'] in names:\n print(f\\\"{ch['name']} → {ch['id']}\\\")\n\"\n```\n\nReplace `CHANNEL_NAMES_HERE` with the comma-separated names the user provided.\n\n**If `conversations.list` returns `missing_scope` or `not_authed`:**\nInform the user: *\"The token doesn't have permission to list channels. Please\nprovide the channel IDs directly (right-click a channel in Slack → Copy link - \nthe last path segment starting with `C` is the ID).\"*\n\n**If the bot token lacks `channels:read`** for private channels, the user can\neither invite the bot first (`/invite @botname`) or switch to a user token.\n\nCollect up to 10 channel IDs. Record them as a Python list literal, e.g.:\n```python\n[\"C0123456789\", \"C9876543210\"]\n```\n\n### Step 2 - Collect trigger phrase\n\nAsk the user: *\"What trigger phrase should OpenHands respond to?\n(Press Enter to use the default: `@openhands`)\"*\n\nAccepted values: any non-empty string unlikely to appear accidentally, e.g.\n`@openhands`, `jazz hands`, `take-me-to-funky-town`.\n\n### Step 3 - Generate the automation script\n\nRead `scripts/main.py` from this skill's directory and **copy it verbatim**.\nApply exactly three constant substitutions near the top of the file:\n\n> **Do not reimplement, simplify, or hand-write a replacement script.**\n> The template already contains the correct secret-loading, state-path,\n> conversation-creation, and context-forwarding logic. Only the three\n> configuration constants below should change unless syntax validation fails.\n\n| Placeholder | Replace with |\n|---|---|\n| `TRIGGER_PHRASE = \"@openhands\"` | `TRIGGER_PHRASE = \"{user_phrase}\"` |\n| `CHANNEL_IDS: list[str] = []` | `CHANNEL_IDS: list[str] = {channel_id_list}` |\n| `DEFAULT_OPENHANDS_URL = \"http://localhost:8000\"` | `DEFAULT_OPENHANDS_URL = \"{url}\"` (keep default if user has no preference) |\n\nWrite the customised script to a temporary directory:\n```bash\nmkdir -p /tmp/slack-monitor-build\n# copy scripts/main.py to /tmp/slack-monitor-build/main.py\n# then replace only the three constants above\n```\n\nValidate syntax before packaging:\n```bash\npython3 -m py_compile /tmp/slack-monitor-build/main.py && echo \"Syntax OK\"\n```\n\nThen run a quick integrity check to confirm the template structure is still\npresent and only the configuration block was customised:\n```bash\ngrep -n 'TRIGGER_PHRASE = \"' /tmp/slack-monitor-build/main.py\ngrep -n 'CHANNEL_IDS: list\\[str\\] =' /tmp/slack-monitor-build/main.py\ngrep -n 'DEFAULT_OPENHANDS_URL = \"' /tmp/slack-monitor-build/main.py\ngrep -n 'def get_secret' /tmp/slack-monitor-build/main.py\ngrep -n 'def _state_file_path' /tmp/slack-monitor-build/main.py\ngrep -n 'def create_conversation' /tmp/slack-monitor-build/main.py\n```\n\nIf any of those checks fail, stop and re-copy the template instead of trying to\nrepair a hand-written variant.\n\n### Step 4 - Package and upload\n\nDetermine the Automation backend URL and auth from the `<RUNTIME_SERVICES>`\nblock in your system context:\n- Use the **Automation backend** `url_from_agent` as `OPENHANDS_HOST`\n- Auth: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`\n\nIf no Automation backend is listed in `<RUNTIME_SERVICES>`, stop and tell\nthe user to start the full automation stack.\n\n```bash\ntar -czf /tmp/slack-monitor.tar.gz -C /tmp/slack-monitor-build .\n\n# OPENHANDS_HOST: read from <RUNTIME_SERVICES> Automation backend url_from_agent\nOPENHANDS_HOST=\"<automation-url-from-runtime-services>\"\n\nTARBALL_PATH=$(curl -s -X POST \\\n \"${OPENHANDS_HOST}/api/automation/v1/uploads?name=slack-channel-monitor\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/gzip\" \\\n --data-binary @/tmp/slack-monitor.tar.gz \\\n | python3 -c \"import json,sys; print(json.load(sys.stdin)['tarball_path'])\")\n\necho \"Uploaded: $TARBALL_PATH\"\n```\n\nIf the upload fails with a size error, the tarball must be under 1 MB.\n`main.py` is under 15 KB so this should never trigger.\n\n### Step 5 - Create 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\\\": \\\"Slack Channel Monitor\\\",\n \\\"trigger\\\": {\\\"type\\\": \\\"cron\\\", \\\"schedule\\\": \\\"* * * * *\\\"},\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 55\n }\" | python3 -m json.tool\n```\n\nA 55-second timeout keeps runs well within the 60-second cron window.\n\nRecord the returned `id` - share it with the user as confirmation.\n\n### Step 6 - Confirm\n\nTell the user:\n\n> ✅ **Slack Channel Monitor** is running!\n>\n> - Automation ID: `{id}`\n> - Channels: `{channel list}`\n> - Trigger phrase: `{phrase}`\n> - Polling every minute via cron `* * * * *`\n> - State file: `~/.openhands/workspaces/automation-state/slack_poller_{id}.json`\n>\n> Send a message containing `{phrase}` in any monitored channel to test it.\n> The bot will react with 👀 and reply with a link to the new conversation.\n\n---\n\n## Runtime Behaviour (per poll)\n\nEach cron run executes `main.py`, which runs **10 polling iterations** (every\n5 seconds) within the 55-second timeout window. Each iteration:\n\n1. **Loads state** from the JSON file (see `references/state-schema.md`).\n2. **Resolves the Slack token** - checks `SLACK_USER_TOKEN` then `SLACK_BOT_TOKEN`.\n3. **Fetches new messages:**\n - User token + `search:read` + > 1 channel → single `search.messages` call\n (searches for the trigger phrase across all channels).\n - Otherwise → one `conversations.history` call per channel.\n4. **Fetches due thread replies** - polls at most one tracked thread per\n iteration using per-thread exponential backoff to stay within Slack rate\n limits.\n5. **Processes messages** in chronological order:\n - Skips messages already in `processed_ts` (dedup across the overlap window).\n - Skips bot messages and any `ts` in `bot_message_ts`.\n - Reply in a tracked thread whose text contains the trigger phrase → forwards\n a follow-up request to the existing conversation and resets the follow-up\n watch window. Replies without the trigger phrase are marked processed and\n ignored.\n - Contains trigger phrase outside a tracked conversation → 👀 reaction, create\n a new conversation, post link.\n - Thread replies: agent receives full thread history for context.\n - Root messages: agent receives the trigger text only.\n6. **Checks conversation statuses** - for each active conversation where\n `time.time() - last_activity > 15 s`:\n - If status is `idle`, `finished`, `error`, or `stuck` → fetch the agent's\n final response via `/api/conversations/{id}/agent_final_response` and post\n it to the Slack thread using Slack's `markdown_text` field so Markdown\n formatting renders correctly. Mark the record `watching` for five minutes\n so triggered follow-up replies can continue the same conversation.\n7. **Advances `last_poll`** to `now - 10 s` (overlap window prevents boundary\n races). If a conversation creation failed, pins `last_poll` further back to\n retry on the next iteration.\n8. **Saves state** (including `processed_ts`) and continues to the next iteration.\n9. After all iterations, fires the completion callback.\n\nDebug output is written to both stdout and a persistent log at:\n```\n{WORKSPACE_BASE_ROOT}/automation-state/slack_poller_debug.log\n```\n\n---\n\n## Additional Resources\n\n### Reference Files\n\n- **`references/slack-api.md`** - Slack token types, required scopes, API\n endpoint reference, rate limits, and common error codes.\n- **`references/state-schema.md`** - State JSON schema, field definitions,\n example file, and conversation lifecycle diagram.\n\n### Script Template\n\n- **`scripts/main.py`** - The complete automation script. Customise the three\n constants at the top (`TRIGGER_PHRASE`, `CHANNEL_IDS`, `DEFAULT_OPENHANDS_URL`)\n before packaging.\n\n---\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---|---|---|\n| Bot doesn't react to messages | Token missing or bot not in channel | Verify token with `auth.test`; `/invite @botname` |\n| `not_in_channel` error in run logs | Bot token used but bot not a member | Invite bot or switch to user token |\n| `missing_scope` error | Token lacks required scopes | Re-install Slack app with correct scopes (see `references/slack-api.md`) |\n| No messages detected | `last_poll` timestamp is in the future | Delete the state file to reset; it will be recreated on next run |\n| Conversation link 404 | `OPENHANDS_URL` points to wrong host | Set the `OPENHANDS_URL` secret to the correct base URL |\n| Summary never posted | Conversation stuck in `running` state | Check conversation in the OpenHands UI; the agent may need intervention |\n| Duplicate conversations created | `processed_ts` state missing or corrupted | Delete the state file to reset; dedup will rebuild on next run |\n| Trigger message processed on each cron run | State file deleted between runs | Ensure `automation-state/` directory is persistent across runs |\n| Debug info needed | Need detailed per-message trace | Check `{WORKSPACE_BASE_ROOT}/automation-state/slack_poller_debug.log` |"
382
+ content: "# Slack Channel Monitor\n\nCreate a cron automation that polls up to 10 Slack channels every minute.\nWindows PowerShell equivalents for the setup, packaging, upload, and API-check shell snippets are in `references/windows.md`.\nWhen a message containing the **trigger phrase** (default: `@openhands`) is\ndetected it:\n\n1. Adds a 👀 reaction to the triggering message.\n2. Opens an OpenHands conversation with the message and recent channel context.\n3. Posts a reply in the Slack thread with a link to the conversation.\n\nOn every subsequent run:\n- New Slack thread replies are forwarded only when they contain the trigger\n phrase, so unrelated conversation in the thread is ignored.\n- When the conversation finishes (or errors), the agent's final response is\n posted back to the Slack thread.\n- Completed conversations stay in a short follow-up watch window, allowing\n triggered Slack replies to continue the same OpenHands conversation.\n\n> **Local mode only.** This automation targets the local OpenHands setup\n> (`dev:automation` stack). A cloud/webhook-based variant is out of scope here.\n\n---\n\n## Prerequisites\n\n### Required secrets\n\nVerify that at least one of the following secrets is set in\n**OpenHands Settings → Secrets** before proceeding:\n\n| Secret name | Token type | Minimum scopes |\n|---|---|---|\n| `SLACK_BOT_TOKEN` | Bot (`xoxb-…`) | `channels:history`, `channels:read`, `reactions:write`, `chat:write` |\n| `SLACK_USER_TOKEN` | User (`xoxp-…`) | Same as bot, plus `search:read` for multi-channel efficiency |\n\nCheck with:\n```bash\n# For bot token:\ncurl -s https://slack.com/api/auth.test -H \"Authorization: Bearer $SLACK_BOT_TOKEN\" \\\n | python3 -c \"import json,sys; d=json.load(sys.stdin); print('ok' if d.get('ok') else d.get('error'))\"\n\n# For user token:\ncurl -s https://slack.com/api/auth.test -H \"Authorization: Bearer $SLACK_USER_TOKEN\" \\\n | python3 -c \"import json,sys; d=json.load(sys.stdin); print('ok' if d.get('ok') else d.get('error'))\"\n```\n\nIf neither token is present, inform the user and stop - the automation cannot\nfunction without Slack credentials.\n\n### Optional secret\n\n| Secret name | Default | Purpose |\n|---|---|---|\n| `OPENHANDS_URL` | `http://localhost:8000` | Base URL used to build conversation links posted in Slack |\n\n---\n\n## Setup Workflow\n\nFollow these steps in order.\n\n### Step 1 - Collect channels\n\nAsk the user: *\"Which Slack channels should be monitored? You can provide\nchannel names (e.g. `#general`) or IDs (e.g. `C0123456789`).\"*\n\n**If the user provides channel names**, resolve them to IDs:\n\n```bash\nSLACK_TOKEN=\"${SLACK_BOT_TOKEN:-$SLACK_USER_TOKEN}\"\ncurl -s \"https://slack.com/api/conversations.list?types=public_channel,private_channel&limit=200&exclude_archived=true\" \\\n -H \"Authorization: Bearer $SLACK_TOKEN\" \\\n | python3 -c \"\nimport json, sys\ndata = json.load(sys.stdin)\nif not data.get('ok'):\n print('ERROR:', data.get('error'))\n exit(1)\nnames = set(n.lstrip('#') for n in ['CHANNEL_NAMES_HERE'.split(',')])\nfor ch in data.get('channels', []):\n if ch['name'] in names:\n print(f\\\"{ch['name']} → {ch['id']}\\\")\n\"\n```\n\nReplace `CHANNEL_NAMES_HERE` with the comma-separated names the user provided.\n\n**If `conversations.list` returns `missing_scope` or `not_authed`:**\nInform the user: *\"The token doesn't have permission to list channels. Please\nprovide the channel IDs directly (right-click a channel in Slack → Copy link - \nthe last path segment starting with `C` is the ID).\"*\n\n**If the bot token lacks `channels:read`** for private channels, the user can\neither invite the bot first (`/invite @botname`) or switch to a user token.\n\nCollect up to 10 channel IDs. Record them as a Python list literal, e.g.:\n```python\n[\"C0123456789\", \"C9876543210\"]\n```\n\n### Step 2 - Collect trigger phrase\n\nAsk the user: *\"What trigger phrase should OpenHands respond to?\n(Press Enter to use the default: `@openhands`)\"*\n\nAccepted values: any non-empty string unlikely to appear accidentally, e.g.\n`@openhands`, `jazz hands`, `take-me-to-funky-town`.\n\n### Step 3 - Generate the automation script\n\nRead `scripts/main.py` from this skill's directory and **copy it verbatim**.\nApply exactly three constant substitutions near the top of the file:\n\n> **Do not reimplement, simplify, or hand-write a replacement script.**\n> The template already contains the correct secret-loading, state-path,\n> conversation-creation, and context-forwarding logic. Only the three\n> configuration constants below should change unless syntax validation fails.\n\n| Placeholder | Replace with |\n|---|---|\n| `TRIGGER_PHRASE = \"@openhands\"` | `TRIGGER_PHRASE = \"{user_phrase}\"` |\n| `CHANNEL_IDS: list[str] = []` | `CHANNEL_IDS: list[str] = {channel_id_list}` |\n| `DEFAULT_OPENHANDS_URL = \"http://localhost:8000\"` | `DEFAULT_OPENHANDS_URL = \"{url}\"` (keep default if user has no preference) |\n\nWrite the customised script to a temporary directory:\n```bash\nmkdir -p /tmp/slack-monitor-build\n# copy scripts/main.py to /tmp/slack-monitor-build/main.py\n# then replace only the three constants above\n```\n\nValidate syntax before packaging:\n```bash\npython3 -m py_compile /tmp/slack-monitor-build/main.py && echo \"Syntax OK\"\n```\n\nThen run a quick integrity check to confirm the template structure is still\npresent and only the configuration block was customised:\n```bash\ngrep -n 'TRIGGER_PHRASE = \"' /tmp/slack-monitor-build/main.py\ngrep -n 'CHANNEL_IDS: list\\[str\\] =' /tmp/slack-monitor-build/main.py\ngrep -n 'DEFAULT_OPENHANDS_URL = \"' /tmp/slack-monitor-build/main.py\ngrep -n 'def get_secret' /tmp/slack-monitor-build/main.py\ngrep -n 'def _state_file_path' /tmp/slack-monitor-build/main.py\ngrep -n 'def create_conversation' /tmp/slack-monitor-build/main.py\n```\n\nIf any of those checks fail, stop and re-copy the template instead of trying to\nrepair a hand-written variant.\n\n### Step 4 - Package and upload\n\nDetermine the Automation backend URL and auth from the `<RUNTIME_SERVICES>`\nblock in your system context:\n- Use the **Automation backend** `url_from_agent` as `OPENHANDS_HOST`\n- Auth: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`\n\nIf no Automation backend is listed in `<RUNTIME_SERVICES>`, stop and tell\nthe user to start the full automation stack.\n\n```bash\ntar -czf /tmp/slack-monitor.tar.gz -C /tmp/slack-monitor-build .\n\n# OPENHANDS_HOST: read from <RUNTIME_SERVICES> Automation backend url_from_agent\nOPENHANDS_HOST=\"<automation-url-from-runtime-services>\"\n\nTARBALL_PATH=$(curl -s -X POST \\\n \"${OPENHANDS_HOST}/api/automation/v1/uploads?name=slack-channel-monitor\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/gzip\" \\\n --data-binary @/tmp/slack-monitor.tar.gz \\\n | python3 -c \"import json,sys; print(json.load(sys.stdin)['tarball_path'])\")\n\necho \"Uploaded: $TARBALL_PATH\"\n```\n\nIf the upload fails with a size error, the tarball must be under 1 MB.\n`main.py` is under 15 KB so this should never trigger.\n\n### Step 5 - Create 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\\\": \\\"Slack Channel Monitor\\\",\n \\\"trigger\\\": {\\\"type\\\": \\\"cron\\\", \\\"schedule\\\": \\\"* * * * *\\\"},\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 55\n }\" | python3 -m json.tool\n```\n\nA 55-second timeout keeps runs well within the 60-second cron window.\n\nRecord the returned `id` - share it with the user as confirmation.\n\n### Step 6 - Confirm\n\nTell the user:\n\n> ✅ **Slack Channel Monitor** is running!\n>\n> - Automation ID: `{id}`\n> - Channels: `{channel list}`\n> - Trigger phrase: `{phrase}`\n> - Polling every minute via cron `* * * * *`\n> - State file: `~/.openhands/workspaces/automation-state/slack_poller_{id}.json`\n>\n> Send a message containing `{phrase}` in any monitored channel to test it.\n> The bot will react with 👀 and reply with a link to the new conversation.\n\n---\n\n## Runtime Behaviour (per poll)\n\nEach cron run executes `main.py`, which runs **10 polling iterations** (every\n5 seconds) within the 55-second timeout window. Each iteration:\n\n1. **Loads state** from the JSON file (see `references/state-schema.md`).\n2. **Resolves the Slack token** - checks `SLACK_USER_TOKEN` then `SLACK_BOT_TOKEN`.\n3. **Fetches new messages:**\n - User token + `search:read` + > 1 channel → single `search.messages` call\n (searches for the trigger phrase across all channels).\n - Otherwise → one `conversations.history` call per channel.\n4. **Fetches due thread replies** - polls at most one tracked thread per\n iteration using per-thread exponential backoff to stay within Slack rate\n limits.\n5. **Processes messages** in chronological order:\n - Skips messages already in `processed_ts` (dedup across the overlap window).\n - Skips bot messages and any `ts` in `bot_message_ts`.\n - Reply in a tracked thread whose text contains the trigger phrase → forwards\n a follow-up request to the existing conversation and resets the follow-up\n watch window. Replies without the trigger phrase are marked processed and\n ignored.\n - Contains trigger phrase outside a tracked conversation → 👀 reaction, create\n a new conversation, post link.\n - Thread replies: agent receives full thread history for context.\n - Root messages: agent receives the trigger text only.\n6. **Checks conversation statuses** - for each active conversation where\n `time.time() - last_activity > 15 s`:\n - If status is `idle`, `finished`, `error`, or `stuck` → fetch the agent's\n final response via `/api/conversations/{id}/agent_final_response` and post\n it to the Slack thread using Slack's `markdown_text` field so Markdown\n formatting renders correctly. Mark the record `watching` for five minutes\n so triggered follow-up replies can continue the same conversation.\n7. **Advances `last_poll`** to `now - 10 s` (overlap window prevents boundary\n races). If a conversation creation failed, pins `last_poll` further back to\n retry on the next iteration.\n8. **Saves state** (including `processed_ts`) and continues to the next iteration.\n9. After all iterations, fires the completion callback.\n\nDebug output is written to both stdout and a persistent log at:\n```\n{WORKSPACE_BASE_ROOT}/automation-state/slack_poller_debug.log\n```\n\n---\n\n## Additional Resources\n\n### Reference Files\n\n- **`references/slack-api.md`** - Slack token types, required scopes, API\n endpoint reference, rate limits, and common error codes.\n- **`references/state-schema.md`** - State JSON schema, field definitions,\n example file, and conversation lifecycle diagram.\n\n### Script Template\n\n- **`scripts/main.py`** - The complete automation script. Customise the three\n constants at the top (`TRIGGER_PHRASE`, `CHANNEL_IDS`, `DEFAULT_OPENHANDS_URL`)\n before packaging.\n\n---\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---|---|---|\n| Bot doesn't react to messages | Token missing or bot not in channel | Verify token with `auth.test`; `/invite @botname` |\n| `not_in_channel` error in run logs | Bot token used but bot not a member | Invite bot or switch to user token |\n| `missing_scope` error | Token lacks required scopes | Re-install Slack app with correct scopes (see `references/slack-api.md`) |\n| No messages detected | `last_poll` timestamp is in the future | Delete the state file to reset; it will be recreated on next run |\n| Conversation link 404 | `OPENHANDS_URL` points to wrong host | Set the `OPENHANDS_URL` secret to the correct base URL |\n| Summary never posted | Conversation stuck in `running` state | Check conversation in the OpenHands UI; the agent may need intervention |\n| Duplicate conversations created | `processed_ts` state missing or corrupted | Delete the state file to reset; dedup will rebuild on next run |\n| Trigger message processed on each cron run | State file deleted between runs | Ensure `automation-state/` directory is persistent across runs |\n| Debug info needed | Need detailed per-message trace | Check `{WORKSPACE_BASE_ROOT}/automation-state/slack_poller_debug.log` |"
377
383
  },
378
384
  {
379
385
  name: "slack-standup-digest",
380
386
  description: "Create an automation that generates an async standup digest from Slack. Searches selected channels for messages since the previous workday, groups updates by project, highlights blockers and decisions, and posts a summary to a target channel.",
381
387
  triggers: ["/standup-digest:setup"],
382
- content: "# Slack Standup Digest Automation\n\nSet up a recurring automation that summarizes Slack activity into an async\nstandup digest.\n\n---\n\n## Prerequisites\n\n### Required integration\n\n- **Slack MCP** must be installed in Settings → MCP.\n\n### Information to collect\n\nAsk the user for:\n\n1. **Source channels** — which Slack channels to scan for updates (e.g. `#engineering`, `#frontend`, `#backend`)\n2. **Target channel** — where the digest should be posted (e.g. `#standup`, `#team-updates`)\n3. **Schedule** — when should the digest run? Default: weekday mornings at 9 AM\n4. **Timezone** — user's timezone (e.g. `America/New_York`, `Europe/London`)\n5. **Auto-post or draft** — should the digest post automatically, or be saved for the user to review and approve first?\n6. **Grouping** — how should updates be organized? Default: by project/channel, with sections for shipped work, active work, blockers, and decisions\n\n---\n\n## Setup Workflow\n\n### Step 1 — Verify Slack MCP access\n\nConfirm the Slack MCP integration is working:\n```\nUse the Slack MCP to search for recent messages in one of the source channels.\n```\n\nIf it fails, tell the user to install the Slack MCP integration first.\n\n### Step 2 — Configure the schedule\n\nBuild a cron schedule from the user's preferences:\n- Weekday mornings at 9 AM ET: `0 9 * * 1-5` with timezone `America/New_York`\n- Daily at 8 AM UTC: `0 8 * * *`\n\n### Step 3 — Build the digest prompt\n\nConstruct a prompt that includes:\n- Source channels to scan\n- Target channel for posting\n- Lookback window (typically \"since previous workday\" — Friday→Monday for Monday digests)\n- Grouping structure (by project, by channel, etc.)\n- Whether to auto-post or draft\n- What to highlight: blockers, decisions, shipped items, unanswered questions\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\": \"Slack Standup Digest\",\n \"prompt\": \"<constructed digest prompt>\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"<schedule>\", \"timezone\": \"<tz>\"}\n }'\n```\n\n### Step 5 — Confirm\n\nTell the user:\n> ✅ **Slack Standup Digest** is running!\n>\n> - Automation ID: `{id}`\n> - Source channels: `{channel list}`\n> - Target channel: `{target}`\n> - Schedule: `{cron description}`\n> - Mode: `{auto-post or draft}`"
388
+ content: "# Slack Standup Digest Automation\n\nSet up a recurring automation that summarizes Slack activity into an async\nstandup digest.\n\n---\n\n## Prerequisites\n\n### Required integration\n\n- **Slack MCP** must be installed in Settings → MCP.\n\n### Information to collect\n\nAsk the user for:\n\n1. **Source channels** — which Slack channels to scan for updates (e.g. `#engineering`, `#frontend`, `#backend`)\n2. **Target channel** — where the digest should be posted (e.g. `#standup`, `#team-updates`)\n3. **Schedule** — when should the digest run? Default: weekday mornings at 9 AM\n4. **Timezone** — user's timezone (e.g. `America/New_York`, `Europe/London`)\n5. **Auto-post or draft** — should the digest post automatically, or be saved for the user to review and approve first?\n6. **Grouping** — how should updates be organized? Default: by project/channel, with sections for shipped work, active work, blockers, and decisions\n\n---\n\n## Setup Workflow\n\n### Step 1 — Verify Slack MCP access\n\nConfirm the Slack MCP integration is working:\n```\nUse the Slack MCP to search for recent messages in one of the source channels.\n```\n\nIf it fails, tell the user to install the Slack MCP integration first.\n\n### Step 2 — Configure the schedule\n\nBuild a cron schedule from the user's preferences:\n- Weekday mornings at 9 AM ET: `0 9 * * 1-5` with timezone `America/New_York`\n- Daily at 8 AM UTC: `0 8 * * *`\n\n### Step 3 — Build the digest prompt\n\nConstruct a prompt that includes:\n- Source channels to scan\n- Target channel for posting\n- Lookback window (typically \"since previous workday\" — Friday→Monday for Monday digests)\n- Grouping structure (by project, by channel, etc.)\n- Whether to auto-post or draft\n- What to highlight: blockers, decisions, shipped items, unanswered questions\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\": \"Slack Standup Digest\",\n \"prompt\": \"<constructed digest prompt>\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"<schedule>\", \"timezone\": \"<tz>\"}\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> ✅ **Slack Standup Digest** is running!\n>\n> - Automation ID: `{id}`\n> - Source channels: `{channel list}`\n> - Target channel: `{target}`\n> - Schedule: `{cron description}`\n> - Mode: `{auto-post or draft}`"
383
389
  },
384
390
  {
385
391
  name: "spark-version-upgrade",
@@ -393,7 +399,7 @@ var e = [
393
399
  "spark 4",
394
400
  "pyspark upgrade"
395
401
  ],
396
- content: "Upgrade Apache Spark applications between major versions with a structured, phase-by-phase workflow.\n\n## When to Use\n\n- Migrating from Spark 2.x → 3.x or Spark 3.x → 4.x\n- Updating PySpark, Spark SQL, or Structured Streaming applications\n- Resolving deprecation warnings before a Spark version bump\n\n## Workflow Overview\n\n1. **Inventory & Impact Analysis** — Scan the codebase and assess scope\n2. **Build File Updates** — Bump Spark/Scala/Java dependencies\n3. **API Migration** — Replace deprecated and removed APIs\n4. **Configuration Migration** — Update Spark config properties\n5. **SQL & DataFrame Migration** — Fix query-level breaking changes\n6. **Test Validation** — Compile, run tests, verify results\n\n---\n\n## Phase 1: Inventory & Impact Analysis\n\nBefore changing any code, assess what needs to change. Read the official Apache Spark migration guide for the target version — it documents every API removal, config rename, and behavioral change per release:\nhttps://spark.apache.org/docs/latest/migration-guide.html\n\n### Checklist\n\n- [ ] Read the migration guide section for the target Spark version\n- [ ] Identify current Spark version (check `pom.xml`, `build.sbt`, `build.gradle`, or `requirements.txt`)\n- [ ] Identify target Spark version\n- [ ] Search for deprecated APIs: `grep -rn 'import org.apache.spark' --include='*.scala' --include='*.java' --include='*.py'`\n- [ ] List all Spark config properties: `grep -rn 'spark\\.' --include='*.conf' --include='*.properties' --include='*.scala' --include='*.java' --include='*.py' | grep -v 'test'`\n- [ ] Check for custom `SparkSession` or `SparkContext` extensions\n- [ ] Identify connector dependencies (Hive, Kafka, Cassandra, Delta, Iceberg)\n- [ ] Document findings in `spark_upgrade_impact.md`\n\n### Output\n\n```\nspark_upgrade_impact.md # Summary of affected files, APIs, and configs\n```\n\n---\n\n## Phase 2: Build File Updates\n\nUpdate dependency versions and resolve compilation.\n\n### Maven (`pom.xml`)\n\n```xml\n<!-- Update Spark version property -->\n<spark.version>3.5.1</spark.version> <!-- or 4.0.0 -->\n<scala.version>2.13.12</scala.version> <!-- Spark 3.x: 2.12/2.13; Spark 4.x: 2.13 -->\n\n<!-- Update artifact IDs if Scala cross-version changed -->\n<artifactId>spark-core_2.13</artifactId>\n<artifactId>spark-sql_2.13</artifactId>\n```\n\n### SBT (`build.sbt`)\n\n```scala\nval sparkVersion = \"3.5.1\" // or \"4.0.0\"\nscalaVersion := \"2.13.12\"\n\nlibraryDependencies += \"org.apache.spark\" %% \"spark-core\" % sparkVersion\nlibraryDependencies += \"org.apache.spark\" %% \"spark-sql\" % sparkVersion\n```\n\n### Gradle (`build.gradle`)\n\n```groovy\next {\n sparkVersion = '3.5.1' // or '4.0.0'\n}\ndependencies {\n implementation \"org.apache.spark:spark-core_2.13:${sparkVersion}\"\n implementation \"org.apache.spark:spark-sql_2.13:${sparkVersion}\"\n}\n```\n\n### PySpark (`requirements.txt` / `pyproject.toml`)\n\n```\npyspark==3.5.1 # or 4.0.0\n```\n\n### Checklist\n\n- [ ] Update Spark version in build file\n- [ ] Update Scala version if crossing 2.12→2.13 boundary\n- [ ] Update Java source/target level if required (Spark 4.x requires Java 17+)\n- [ ] Update connector library versions to match new Spark version\n- [ ] Resolve dependency conflicts (`mvn dependency:tree` / `sbt dependencyTree`)\n- [ ] Confirm project compiles (errors at this stage are expected — they guide Phase 3)\n\n---\n\n## Phase 3: API Migration\n\nReplace removed and deprecated APIs. Work through compiler errors systematically.\n\n### Common Patterns\n\nConsult the official Apache Spark migration guide for the complete list of changes for each version:\nhttps://spark.apache.org/docs/latest/migration-guide.html\n\n#### SparkSession Creation (2.x → 3.x)\n\n```scala\n// BEFORE (Spark 1.x/2.x)\nval sc = new SparkContext(conf)\nval sqlContext = new SQLContext(sc)\n\n// AFTER (Spark 2.x+/3.x)\nval spark = SparkSession.builder()\n .config(conf)\n .enableHiveSupport() // if needed\n .getOrCreate()\nval sc = spark.sparkContext\n```\n\n#### RDD to DataFrame (2.x → 3.x)\n\n```scala\n// BEFORE\nrdd.toDF() // implicit from SQLContext\n\n// AFTER\nimport spark.implicits._\nrdd.toDF() // implicit from SparkSession\n```\n\n#### Accumulator API (2.x → 3.x)\n\n```scala\n// BEFORE\nval acc = sc.accumulator(0)\n\n// AFTER\nval acc = sc.longAccumulator(\"name\")\n```\n\n### Checklist\n\n- [ ] Replace `SQLContext` / `HiveContext` with `SparkSession`\n- [ ] Replace deprecated `Accumulator` with `AccumulatorV2`\n- [ ] Update `DataFrame` → `Dataset[Row]` where needed\n- [ ] Replace removed `RDD.mapPartitionsWithContext` with `mapPartitions`\n- [ ] Fix `SparkConf` deprecated setters\n- [ ] Update custom `UserDefinedFunction` registration\n- [ ] Migrate `Experimental` / `DeveloperApi` usages that were removed\n- [ ] Verify all compilation errors from Phase 2 are resolved\n\n---\n\n## Phase 4: Configuration Migration\n\nSpark renames and removes configuration properties between versions. The official migration guide documents every renamed and removed property per release:\nhttps://spark.apache.org/docs/latest/migration-guide.html\n\n### Checklist\n\n- [ ] Rename deprecated config keys (e.g., `spark.shuffle.file.buffer.kb` → `spark.shuffle.file.buffer`)\n- [ ] Update removed configs to their replacements\n- [ ] Review `spark-defaults.conf`, application code, and submit scripts\n- [ ] Check for hardcoded config values in test fixtures\n- [ ] Verify `SparkSession.builder().config(...)` calls use current property names\n\n---\n\n## Phase 5: SQL & DataFrame Migration\n\nSpark SQL behavior changes between versions can silently alter query results.\n\n### Key Breaking Changes (2.x → 3.x)\n\n- `CAST` to integer no longer truncates silently — set `spark.sql.ansi.enabled` if needed\n- `FROM` clause is required in `SELECT` (no more `SELECT 1`)\n- Column resolution order changed in subqueries\n- `spark.sql.legacy.timeParserPolicy` controls date/time parsing behavior\n\n### Key Breaking Changes (3.x → 4.x)\n\n- ANSI mode is default (`spark.sql.ansi.enabled=true`)\n- Stricter type coercion in comparisons\n- `spark.sql.legacy.*` flags removed\n\n### Checklist\n\n- [ ] Audit SQL strings and DataFrame expressions for changed behavior\n- [ ] Add explicit `CAST` where implicit coercion relied on legacy behavior\n- [ ] Update date/time format patterns to match new parser\n- [ ] Test SQL queries with representative data and compare output to pre-upgrade baseline\n- [ ] Set `spark.sql.legacy.*` flags temporarily if needed for phased migration\n\n---\n\n## Phase 6: Test Validation\n\n### Checklist\n\n- [ ] All code compiles without errors\n- [ ] All existing unit tests pass\n- [ ] All existing integration tests pass\n- [ ] Run Spark jobs locally with sample data and compare output to pre-upgrade baseline\n- [ ] No deprecation warnings remain (or are documented with a migration timeline)\n- [ ] Update CI/CD pipeline to use new Spark version\n- [ ] Document any `spark.sql.legacy.*` flags that are set temporarily\n\n## Done When\n\n✓ Project compiles against target Spark version\n✓ All tests pass\n✓ No removed APIs remain in code\n✓ Configuration properties are current\n✓ SQL queries produce correct results\n✓ Upgrade impact documented in `spark_upgrade_impact.md`",
402
+ content: "Upgrade Apache Spark applications between major versions with a structured, phase-by-phase workflow.\n\n## When to Use\n\n- Migrating from Spark 2.x → 3.x or Spark 3.x → 4.x\n- Updating PySpark, Spark SQL, or Structured Streaming applications\n- Resolving deprecation warnings before a Spark version bump\n\n## Workflow Overview\n\n1. **Inventory & Impact Analysis** — Scan the codebase and assess scope\n2. **Build File Updates** — Bump Spark/Scala/Java dependencies\n3. **API Migration** — Replace deprecated and removed APIs\n4. **Configuration Migration** — Update Spark config properties\n5. **SQL & DataFrame Migration** — Fix query-level breaking changes\n6. **Test Validation** — Compile, run tests, verify results\n\n---\n\n## Phase 1: Inventory & Impact Analysis\n\nBefore changing any code, assess what needs to change. Read the official Apache Spark migration guide for the target version — it documents every API removal, config rename, and behavioral change per release:\nhttps://spark.apache.org/docs/latest/migration-guide.html\n\n### Checklist\n\n- [ ] Read the migration guide section for the target Spark version\n- [ ] Identify current Spark version (check `pom.xml`, `build.sbt`, `build.gradle`, or `requirements.txt`)\n- [ ] Identify target Spark version\n- [ ] Search for deprecated APIs: `grep -rn 'import org.apache.spark' --include='*.scala' --include='*.java' --include='*.py'`\n- [ ] List all Spark config properties: `grep -rn 'spark\\.' --include='*.conf' --include='*.properties' --include='*.scala' --include='*.java' --include='*.py' | grep -v 'test'`\n- [ ] On Windows PowerShell, use `Get-ChildItem -Recurse -Include *.scala,*.java,*.py | Select-String 'import org.apache.spark'` and adjust the extensions/pattern for config searches.\n- [ ] Check for custom `SparkSession` or `SparkContext` extensions\n- [ ] Identify connector dependencies (Hive, Kafka, Cassandra, Delta, Iceberg)\n- [ ] Document findings in `spark_upgrade_impact.md`\n\n### Output\n\n```\nspark_upgrade_impact.md # Summary of affected files, APIs, and configs\n```\n\n---\n\n## Phase 2: Build File Updates\n\nUpdate dependency versions and resolve compilation.\n\n### Maven (`pom.xml`)\n\n```xml\n<!-- Update Spark version property -->\n<spark.version>3.5.1</spark.version> <!-- or 4.0.0 -->\n<scala.version>2.13.12</scala.version> <!-- Spark 3.x: 2.12/2.13; Spark 4.x: 2.13 -->\n\n<!-- Update artifact IDs if Scala cross-version changed -->\n<artifactId>spark-core_2.13</artifactId>\n<artifactId>spark-sql_2.13</artifactId>\n```\n\n### SBT (`build.sbt`)\n\n```scala\nval sparkVersion = \"3.5.1\" // or \"4.0.0\"\nscalaVersion := \"2.13.12\"\n\nlibraryDependencies += \"org.apache.spark\" %% \"spark-core\" % sparkVersion\nlibraryDependencies += \"org.apache.spark\" %% \"spark-sql\" % sparkVersion\n```\n\n### Gradle (`build.gradle`)\n\n```groovy\next {\n sparkVersion = '3.5.1' // or '4.0.0'\n}\ndependencies {\n implementation \"org.apache.spark:spark-core_2.13:${sparkVersion}\"\n implementation \"org.apache.spark:spark-sql_2.13:${sparkVersion}\"\n}\n```\n\n### PySpark (`requirements.txt` / `pyproject.toml`)\n\n```\npyspark==3.5.1 # or 4.0.0\n```\n\n### Checklist\n\n- [ ] Update Spark version in build file\n- [ ] Update Scala version if crossing 2.12→2.13 boundary\n- [ ] Update Java source/target level if required (Spark 4.x requires Java 17+)\n- [ ] Update connector library versions to match new Spark version\n- [ ] Resolve dependency conflicts (`mvn dependency:tree` / `sbt dependencyTree`)\n- [ ] Confirm project compiles (errors at this stage are expected — they guide Phase 3)\n\n---\n\n## Phase 3: API Migration\n\nReplace removed and deprecated APIs. Work through compiler errors systematically.\n\n### Common Patterns\n\nConsult the official Apache Spark migration guide for the complete list of changes for each version:\nhttps://spark.apache.org/docs/latest/migration-guide.html\n\n#### SparkSession Creation (2.x → 3.x)\n\n```scala\n// BEFORE (Spark 1.x/2.x)\nval sc = new SparkContext(conf)\nval sqlContext = new SQLContext(sc)\n\n// AFTER (Spark 2.x+/3.x)\nval spark = SparkSession.builder()\n .config(conf)\n .enableHiveSupport() // if needed\n .getOrCreate()\nval sc = spark.sparkContext\n```\n\n#### RDD to DataFrame (2.x → 3.x)\n\n```scala\n// BEFORE\nrdd.toDF() // implicit from SQLContext\n\n// AFTER\nimport spark.implicits._\nrdd.toDF() // implicit from SparkSession\n```\n\n#### Accumulator API (2.x → 3.x)\n\n```scala\n// BEFORE\nval acc = sc.accumulator(0)\n\n// AFTER\nval acc = sc.longAccumulator(\"name\")\n```\n\n### Checklist\n\n- [ ] Replace `SQLContext` / `HiveContext` with `SparkSession`\n- [ ] Replace deprecated `Accumulator` with `AccumulatorV2`\n- [ ] Update `DataFrame` → `Dataset[Row]` where needed\n- [ ] Replace removed `RDD.mapPartitionsWithContext` with `mapPartitions`\n- [ ] Fix `SparkConf` deprecated setters\n- [ ] Update custom `UserDefinedFunction` registration\n- [ ] Migrate `Experimental` / `DeveloperApi` usages that were removed\n- [ ] Verify all compilation errors from Phase 2 are resolved\n\n---\n\n## Phase 4: Configuration Migration\n\nSpark renames and removes configuration properties between versions. The official migration guide documents every renamed and removed property per release:\nhttps://spark.apache.org/docs/latest/migration-guide.html\n\n### Checklist\n\n- [ ] Rename deprecated config keys (e.g., `spark.shuffle.file.buffer.kb` → `spark.shuffle.file.buffer`)\n- [ ] Update removed configs to their replacements\n- [ ] Review `spark-defaults.conf`, application code, and submit scripts\n- [ ] Check for hardcoded config values in test fixtures\n- [ ] Verify `SparkSession.builder().config(...)` calls use current property names\n\n---\n\n## Phase 5: SQL & DataFrame Migration\n\nSpark SQL behavior changes between versions can silently alter query results.\n\n### Key Breaking Changes (2.x → 3.x)\n\n- `CAST` to integer no longer truncates silently — set `spark.sql.ansi.enabled` if needed\n- `FROM` clause is required in `SELECT` (no more `SELECT 1`)\n- Column resolution order changed in subqueries\n- `spark.sql.legacy.timeParserPolicy` controls date/time parsing behavior\n\n### Key Breaking Changes (3.x → 4.x)\n\n- ANSI mode is default (`spark.sql.ansi.enabled=true`)\n- Stricter type coercion in comparisons\n- `spark.sql.legacy.*` flags removed\n\n### Checklist\n\n- [ ] Audit SQL strings and DataFrame expressions for changed behavior\n- [ ] Add explicit `CAST` where implicit coercion relied on legacy behavior\n- [ ] Update date/time format patterns to match new parser\n- [ ] Test SQL queries with representative data and compare output to pre-upgrade baseline\n- [ ] Set `spark.sql.legacy.*` flags temporarily if needed for phased migration\n\n---\n\n## Phase 6: Test Validation\n\n### Checklist\n\n- [ ] All code compiles without errors\n- [ ] All existing unit tests pass\n- [ ] All existing integration tests pass\n- [ ] Run Spark jobs locally with sample data and compare output to pre-upgrade baseline\n- [ ] No deprecation warnings remain (or are documented with a migration timeline)\n- [ ] Update CI/CD pipeline to use new Spark version\n- [ ] Document any `spark.sql.legacy.*` flags that are set temporarily\n\n## Done When\n\n✓ Project compiles against target Spark version\n✓ All tests pass\n✓ No removed APIs remain in code\n✓ Configuration properties are current\n✓ SQL queries produce correct results\n✓ Upgrade impact documented in `spark_upgrade_impact.md`",
397
403
  license: "MIT",
398
404
  compatibility: "Requires Java 8+/11+/17+, Scala 2.12/2.13, Maven/Gradle/SBT, Apache Spark"
399
405
  },
@@ -409,7 +415,7 @@ var e = [
409
415
  "secure shell",
410
416
  "ssh keys"
411
417
  ],
412
- content: "# SSH Skill\n\nThis skill provides capabilities for establishing and managing SSH connections to remote machines.\n\n## Capabilities\n\n- Establish SSH connections using password or key-based authentication\n- Generate and manage SSH key pairs\n- Configure SSH for easier connections\n- Execute commands on remote machines\n- Transfer files between local and remote machines\n- Manage SSH configurations and known hosts\n\n## Authentication Methods\n\n### Password Authentication\n\n```bash\nssh username@hostname\n```\n\nWhen prompted, you should ask the user for their password or a private key.\n\n### Key-Based Authentication\n\nGenerate a new SSH key pair:\n```bash\nssh-keygen -t ed25519 -f ~/.ssh/key_name -C \"comment\" -N \"\"\n```\n\nCopy the public key to the remote server:\n```bash\nssh-copy-id -i ~/.ssh/key_name.pub username@hostname\n```\n\nConnect using the private key:\n```bash\nssh -i ~/.ssh/key_name username@hostname\n```\n\n## SSH Configuration\n\nCreate or edit the SSH config file for easier connections:\n```bash\nmkdir -p ~/.ssh\ncat > ~/.ssh/config << 'EOF'\nHost alias\n HostName hostname_or_ip\n User username\n IdentityFile ~/.ssh/key_name\n Port 22\n ServerAliveInterval 60\nEOF\nchmod 600 ~/.ssh/config\n```\n\nThen connect using the alias:\n```bash\nssh alias\n```\n\n## Common SSH Options\n\n- `-p PORT`: Connect to a specific port\n- `-X`: Enable X11 forwarding\n- `-L local_port:remote_host:remote_port`: Set up local port forwarding\n- `-R remote_port:local_host:local_port`: Set up remote port forwarding\n- `-N`: Do not execute a remote command (useful for port forwarding)\n- `-f`: Run in background\n- `-v`: Verbose mode (add more v's for increased verbosity)\n\n## File Transfer with SCP\n\nCopy a file to the remote server:\n```bash\nscp /path/to/local/file username@hostname:/path/to/remote/directory/\n```\n\nCopy a file from the remote server:\n```bash\nscp username@hostname:/path/to/remote/file /path/to/local/directory/\n```\n\nCopy a directory recursively:\n```bash\nscp -r /path/to/local/directory username@hostname:/path/to/remote/directory/\n```\n\n## SSH Agent\n\nStart the SSH agent:\n```bash\neval \"$(ssh-agent -s)\"\n```\n\nAdd a key to the agent:\n```bash\nssh-add ~/.ssh/key_name\n```\n\n## Troubleshooting\n\n- Check SSH service status on remote: `systemctl status sshd`\n- Verify SSH port is open: `nc -zv hostname 22`\n- Debug connection issues: `ssh -vvv username@hostname`\n- Check permissions: SSH private keys should have 600 permissions (`chmod 600 ~/.ssh/key_name`)\n- Verify known_hosts: If host key changed, remove the old entry with `ssh-keygen -R hostname`\n\n## Secure SSH Key Management\n\n### Local Storage with Proper Permissions\n\nThe most basic approach is to ensure proper file permissions:\n\n```bash\n# Set correct permissions for private keys\nchmod 600 ~/.ssh/id_ed25519\n# Set correct permissions for public keys\nchmod 644 ~/.ssh/id_ed25519.pub\n# Set correct permissions for SSH directory\nchmod 700 ~/.ssh\n```"
418
+ content: "# SSH Skill\n\nThis skill provides capabilities for establishing and managing SSH connections to remote machines.\nWindows PowerShell equivalents for SSH config creation, key paths, ssh-agent, and permissions are in `references/windows.md`.\n\n## Capabilities\n\n- Establish SSH connections using password or key-based authentication\n- Generate and manage SSH key pairs\n- Configure SSH for easier connections\n- Execute commands on remote machines\n- Transfer files between local and remote machines\n- Manage SSH configurations and known hosts\n\n## Authentication Methods\n\n### Password Authentication\n\n```bash\nssh username@hostname\n```\n\nWhen prompted, you should ask the user for their password or a private key.\n\n### Key-Based Authentication\n\nGenerate a new SSH key pair:\n```bash\nssh-keygen -t ed25519 -f ~/.ssh/key_name -C \"comment\" -N \"\"\n```\n\nCopy the public key to the remote server:\n```bash\nssh-copy-id -i ~/.ssh/key_name.pub username@hostname\n```\n\nConnect using the private key:\n```bash\nssh -i ~/.ssh/key_name username@hostname\n```\n\n## SSH Configuration\n\nCreate or edit the SSH config file for easier connections:\n```bash\nmkdir -p ~/.ssh\ncat > ~/.ssh/config << 'EOF'\nHost alias\n HostName hostname_or_ip\n User username\n IdentityFile ~/.ssh/key_name\n Port 22\n ServerAliveInterval 60\nEOF\nchmod 600 ~/.ssh/config\n```\n\nThen connect using the alias:\n```bash\nssh alias\n```\n\n## Common SSH Options\n\n- `-p PORT`: Connect to a specific port\n- `-X`: Enable X11 forwarding\n- `-L local_port:remote_host:remote_port`: Set up local port forwarding\n- `-R remote_port:local_host:local_port`: Set up remote port forwarding\n- `-N`: Do not execute a remote command (useful for port forwarding)\n- `-f`: Run in background\n- `-v`: Verbose mode (add more v's for increased verbosity)\n\n## File Transfer with SCP\n\nCopy a file to the remote server:\n```bash\nscp /path/to/local/file username@hostname:/path/to/remote/directory/\n```\n\nCopy a file from the remote server:\n```bash\nscp username@hostname:/path/to/remote/file /path/to/local/directory/\n```\n\nCopy a directory recursively:\n```bash\nscp -r /path/to/local/directory username@hostname:/path/to/remote/directory/\n```\n\n## SSH Agent\n\nStart the SSH agent:\n```bash\neval \"$(ssh-agent -s)\"\n```\n\nAdd a key to the agent:\n```bash\nssh-add ~/.ssh/key_name\n```\n\n## Troubleshooting\n\n- Check SSH service status on remote: `systemctl status sshd`\n- Verify SSH port is open: `nc -zv hostname 22`\n- Debug connection issues: `ssh -vvv username@hostname`\n- Check permissions: SSH private keys should have 600 permissions (`chmod 600 ~/.ssh/key_name`)\n- Verify known_hosts: If host key changed, remove the old entry with `ssh-keygen -R hostname`\n\n## Secure SSH Key Management\n\n### Local Storage with Proper Permissions\n\nThe most basic approach is to ensure proper file permissions:\n\n```bash\n# Set correct permissions for private keys\nchmod 600 ~/.ssh/id_ed25519\n# Set correct permissions for public keys\nchmod 644 ~/.ssh/id_ed25519.pub\n# Set correct permissions for SSH directory\nchmod 700 ~/.ssh\n```"
413
419
  },
414
420
  {
415
421
  name: "swift-linux",
@@ -419,7 +425,7 @@ var e = [
419
425
  "swift-debian",
420
426
  "swift-installation"
421
427
  ],
422
- content: "# Swift Installation Guide for Debian Linux\n\nThis document provides instructions for installing Swift on Debian 12 (Bookworm).\n\n> This setup is intended for non-UI development tasks on Swift on Linux.\n\n## Prerequisites\n\nBefore installing Swift, you need to install the required dependencies for your system. You can find the most up-to-date list of dependencies for your specific Linux distribution and version at the [Swift.org tarball installation guide](https://www.swift.org/install/linux/tarball/).\n\nFOR EXAMPLE, the dependencies you may need to install for Debian 12 could be:\n\n```bash\nsudo apt-get update\nsudo apt-get install -y \\\n binutils-gold \\\n gcc \\\n git \\\n libcurl4-openssl-dev \\\n libedit-dev \\\n libicu-dev \\\n libncurses-dev \\\n libpython3-dev \\\n libsqlite3-dev \\\n libxml2-dev \\\n pkg-config \\\n tzdata \\\n uuid-dev\n```\n\n## Download and Install Swift\n\n1. Find the latest Swift version for Debian:\n\n Go to the [Swift.org download page](https://www.swift.org/download/) to find the latest Swift version compatible with Debian 12 (Bookworm).\n\n Look for a tarball named something like `swift-<VERSION>-RELEASE-debian12.tar.gz` (e.g., `swift-6.0.3-RELEASE-debian12.tar.gz`).\n\n The URL pattern is typically:\n ```\n https://download.swift.org/swift-<VERSION>-release/debian12/swift-<VERSION>-RELEASE/swift-<VERSION>-RELEASE-debian12.tar.gz\n ```\n\n Where `<VERSION>` is the Swift version number (e.g., `6.0.3`).\n\n2. Download the Swift binary for Debian 12:\n\n```bash\ncd /workspace\nwget https://download.swift.org/swift-6.0.3-release/debian12/swift-6.0.3-RELEASE/swift-6.0.3-RELEASE-debian12.tar.gz\n```\n\n3. Extract the archive:\n\n> **Note**: Make sure to install Swift in the `/workspace` directory, but outside the git repository to avoid committing the Swift binaries.\n\n4. Add Swift to your PATH by adding the following line to your `~/.bashrc` file:\n\n```bash\necho 'export PATH=/workspace/swift-6.0.3-RELEASE-debian12/usr/bin:$PATH' >> ~/.bashrc\nsource ~/.bashrc\n```\n\n> **Note**: Make sure to update the version number in the PATH to match the version you downloaded.\n\n## Verify Installation\n\nVerify that Swift is correctly installed by running:\n\n```bash\nswift --version\n```"
428
+ content: "# Swift Installation Guide for Debian Linux\n\nThis document provides instructions for installing Swift on Debian 12 (Bookworm).\n\n> This setup is intended for non-UI development tasks on Swift on Linux.\n> On Windows, run these Debian commands inside WSL2 or a Linux container. For native Windows Swift, use the Windows toolchain from Swift.org instead.\n\n## Prerequisites\n\nBefore installing Swift, you need to install the required dependencies for your system. You can find the most up-to-date list of dependencies for your specific Linux distribution and version at the [Swift.org tarball installation guide](https://www.swift.org/install/linux/tarball/).\n\nFOR EXAMPLE, the dependencies you may need to install for Debian 12 could be:\n\n```bash\nsudo apt-get update\nsudo apt-get install -y \\\n binutils-gold \\\n gcc \\\n git \\\n libcurl4-openssl-dev \\\n libedit-dev \\\n libicu-dev \\\n libncurses-dev \\\n libpython3-dev \\\n libsqlite3-dev \\\n libxml2-dev \\\n pkg-config \\\n tzdata \\\n uuid-dev\n```\n\n## Download and Install Swift\n\n1. Find the latest Swift version for Debian:\n\n Go to the [Swift.org download page](https://www.swift.org/download/) to find the latest Swift version compatible with Debian 12 (Bookworm).\n\n Look for a tarball named something like `swift-<VERSION>-RELEASE-debian12.tar.gz` (e.g., `swift-6.0.3-RELEASE-debian12.tar.gz`).\n\n The URL pattern is typically:\n ```\n https://download.swift.org/swift-<VERSION>-release/debian12/swift-<VERSION>-RELEASE/swift-<VERSION>-RELEASE-debian12.tar.gz\n ```\n\n Where `<VERSION>` is the Swift version number (e.g., `6.0.3`).\n\n2. Download the Swift binary for Debian 12:\n\n```bash\ncd /workspace\nwget https://download.swift.org/swift-6.0.3-release/debian12/swift-6.0.3-RELEASE/swift-6.0.3-RELEASE-debian12.tar.gz\n```\n\n3. Extract the archive:\n\n> **Note**: Make sure to install Swift in the `/workspace` directory, but outside the git repository to avoid committing the Swift binaries.\n\n4. Add Swift to your PATH by adding the following line to your `~/.bashrc` file:\n\n```bash\necho 'export PATH=/workspace/swift-6.0.3-RELEASE-debian12/usr/bin:$PATH' >> ~/.bashrc\nsource ~/.bashrc\n```\n\n> **Note**: Make sure to update the version number in the PATH to match the version you downloaded.\n\n## Verify Installation\n\nVerify that Swift is correctly installed by running:\n\n```bash\nswift --version\n```"
423
429
  },
424
430
  {
425
431
  name: "theme-factory",
@@ -438,7 +444,7 @@ var e = [
438
444
  name: "vercel",
439
445
  description: "Deploy and manage applications on Vercel, including preview deployments and deployment protection. Use when working with Vercel-hosted projects or configuring Vercel deployments.",
440
446
  triggers: ["vercel", "preview deployment"],
441
- content: "# Vercel Deployment Guide\n\n## Deployment Protection and Agent Access\n\nVercel deployments may have **Deployment Protection** enabled, which requires authentication to access preview deployments. This can block automated testing and agent access to preview URLs.\n\n### Identifying Protected Deployments\n\nIf you encounter a login page or authentication requirement when accessing a Vercel preview URL, the deployment has protection enabled. Signs include:\n- Redirect to `vercel.com/login` or SSO login page\n- 401/403 errors when accessing the deployment\n- Preview URLs that require Vercel team membership\n\n### Enabling Agent Access with Protection Bypass\n\nTo allow agents and automated systems to access protected deployments, users need to set up **Protection Bypass for Automation**:\n\n1. **Navigate to Project Settings**\n - Go to the Vercel Dashboard\n - Select the project\n - Click on **Settings** → **Deployment Protection**\n\n2. **Generate a Protection Bypass Secret**\n - Under \"Protection Bypass for Automation\", click **Generate Secret**\n - Copy the generated secret securely\n\n3. **Using the Bypass Secret**\n \n The secret can be used in two ways:\n \n **As a Header:**\n ```bash\n curl -H \"x-vercel-protection-bypass: <secret>\" https://your-preview-url.vercel.app\n ```\n \n **As a Query Parameter:**\n ```\n https://your-preview-url.vercel.app?x-vercel-protection-bypass=<secret>\n ```\n\n4. **For Browser-Based Testing**\n - Append `?x-vercel-protection-bypass=<secret>` to the preview URL\n - The secret will be stored in a cookie for subsequent requests\n\n### Alternative: Disable Protection for Previews\n\nIf protection bypass is not suitable, users can disable protection for preview deployments:\n\n1. Go to **Settings** → **Deployment Protection**\n2. Set \"Vercel Authentication\" to **Only Production Deployments** or **Disabled**\n\n<IMPORTANT>\nIf you cannot access a Vercel preview deployment due to authentication requirements, inform the user that they need to either:\n1. Set up a Protection Bypass secret and provide it to you, OR\n2. Disable Deployment Protection for preview deployments in their Vercel project settings\n\nDo NOT repeatedly attempt to access protected URLs without the bypass secret.\n</IMPORTANT>\n\n## Environment Variables\n\nSet environment variables in Vercel Dashboard under **Settings** → **Environment Variables**, or use the Vercel CLI:\n\n```bash\nvercel env add MY_SECRET\n```\n\nAccess in your application:\n```typescript\nconst secret = process.env.MY_SECRET;\n```\n\n## Vercel CLI Commands\n\nCommon Vercel CLI commands:\n\n```bash\n# Login to Vercel\nvercel login\n\n# Deploy to preview\nvercel\n\n# Deploy to production\nvercel --prod\n\n# List deployments\nvercel ls\n\n# View deployment logs\nvercel logs <deployment-url>\n\n# Pull environment variables locally\nvercel env pull\n```"
447
+ content: "# Vercel Deployment Guide\n\n## Deployment Protection and Agent Access\n\nVercel deployments may have **Deployment Protection** enabled, which requires authentication to access preview deployments. This can block automated testing and agent access to preview URLs.\n\n### Identifying Protected Deployments\n\nIf you encounter a login page or authentication requirement when accessing a Vercel preview URL, the deployment has protection enabled. Signs include:\n- Redirect to `vercel.com/login` or SSO login page\n- 401/403 errors when accessing the deployment\n- Preview URLs that require Vercel team membership\n\n### Enabling Agent Access with Protection Bypass\n\nTo allow agents and automated systems to access protected deployments, users need to set up **Protection Bypass for Automation**:\n\n1. **Navigate to Project Settings**\n - Go to the Vercel Dashboard\n - Select the project\n - Click on **Settings** → **Deployment Protection**\n\n2. **Generate a Protection Bypass Secret**\n - Under \"Protection Bypass for Automation\", click **Generate Secret**\n - Copy the generated secret securely\n\n3. **Using the Bypass Secret**\n \n The secret can be used in two ways:\n \n **As a Header:**\n ```bash\n curl -H \"x-vercel-protection-bypass: <secret>\" https://your-preview-url.vercel.app\n ```\n\n PowerShell equivalent:\n ```powershell\n Invoke-WebRequest -Headers @{ \"x-vercel-protection-bypass\" = \"<secret>\" } -Uri https://your-preview-url.vercel.app\n ```\n \n **As a Query Parameter:**\n ```\n https://your-preview-url.vercel.app?x-vercel-protection-bypass=<secret>\n ```\n\n4. **For Browser-Based Testing**\n - Append `?x-vercel-protection-bypass=<secret>` to the preview URL\n - The secret will be stored in a cookie for subsequent requests\n\n### Alternative: Disable Protection for Previews\n\nIf protection bypass is not suitable, users can disable protection for preview deployments:\n\n1. Go to **Settings** → **Deployment Protection**\n2. Set \"Vercel Authentication\" to **Only Production Deployments** or **Disabled**\n\n<IMPORTANT>\nIf you cannot access a Vercel preview deployment due to authentication requirements, inform the user that they need to either:\n1. Set up a Protection Bypass secret and provide it to you, OR\n2. Disable Deployment Protection for preview deployments in their Vercel project settings\n\nDo NOT repeatedly attempt to access protected URLs without the bypass secret.\n</IMPORTANT>\n\n## Environment Variables\n\nSet environment variables in Vercel Dashboard under **Settings** → **Environment Variables**, or use the Vercel CLI:\n\n```bash\nvercel env add MY_SECRET\n```\n\nAccess in your application:\n```typescript\nconst secret = process.env.MY_SECRET;\n```\n\n## Vercel CLI Commands\n\nCommon Vercel CLI commands:\n\n```bash\n# Login to Vercel\nvercel login\n\n# Deploy to preview\nvercel\n\n# Deploy to production\nvercel --prod\n\n# List deployments\nvercel ls\n\n# View deployment logs\nvercel logs <deployment-url>\n\n# Pull environment variables locally\nvercel env pull\n```"
442
448
  }
443
449
  ];
444
450
  //#endregion