@openhands/extensions 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (347) hide show
  1. package/.agents/skills/custom-codereview-guide.md +25 -0
  2. package/.github/pull_request_template.md +38 -0
  3. package/.github/release.yml +14 -0
  4. package/.github/workflows/check-extensions.yml +72 -0
  5. package/.github/workflows/npm-publish.yml +89 -0
  6. package/.github/workflows/pr.yml +30 -0
  7. package/.github/workflows/release.yml +24 -0
  8. package/.github/workflows/tests.yml +25 -0
  9. package/.github/workflows/vulnerability-scan.yml +87 -0
  10. package/.release-please-manifest.json +3 -0
  11. package/AGENTS.md +132 -0
  12. package/README.md +10 -0
  13. package/analysis_results.md +162 -0
  14. package/marketplaces/large-codebase.json +66 -0
  15. package/marketplaces/openhands-extensions.json +682 -0
  16. package/package.json +4 -10
  17. package/plugins/README.md +30 -0
  18. package/plugins/city-weather/.plugin/plugin.json +13 -0
  19. package/plugins/city-weather/README.md +145 -0
  20. package/plugins/city-weather/commands/now.md +56 -0
  21. package/plugins/cobol-modernization/.plugin/plugin.json +19 -0
  22. package/plugins/cobol-modernization/README.md +201 -0
  23. package/plugins/cobol-modernization/references/troubleshooting.md +18 -0
  24. package/plugins/cobol-modernization/skills/build-setup/SKILL.md +78 -0
  25. package/plugins/cobol-modernization/skills/build-setup/scripts/install-gnucobol.sh +32 -0
  26. package/plugins/cobol-modernization/skills/cobol-modernization-overview/SKILL.md +113 -0
  27. package/plugins/cobol-modernization/skills/mainfraime-removal/SKILL.md +62 -0
  28. package/plugins/cobol-modernization/skills/mainfraime-removal/references/cics-transformation-examples.md +45 -0
  29. package/plugins/cobol-modernization/skills/mainframe-planning/SKILL.md +78 -0
  30. package/plugins/cobol-modernization/skills/to-java-migration/SKILL.md +59 -0
  31. package/plugins/cobol-modernization/skills/to-java-migration/references/cobol-to-java-example.md +58 -0
  32. package/plugins/cobol-modernization/skills/to-java-migration/references/datatype-mappings.md +19 -0
  33. package/plugins/issue-duplicate-checker/.plugin/plugin.json +13 -0
  34. package/plugins/issue-duplicate-checker/README.md +51 -0
  35. package/plugins/issue-duplicate-checker/action.yml +349 -0
  36. package/plugins/issue-duplicate-checker/scripts/auto_close_duplicate_issues.py +569 -0
  37. package/plugins/issue-duplicate-checker/scripts/issue_duplicate_check_openhands.py +681 -0
  38. package/plugins/issue-duplicate-checker/scripts/post_duplicate_notice.js +220 -0
  39. package/plugins/issue-duplicate-checker/scripts/remove_duplicate_candidate_label.js +27 -0
  40. package/plugins/magic-test/.plugin/plugin.json +13 -0
  41. package/plugins/magic-test/skills/magic-word/SKILL.md +33 -0
  42. package/plugins/migration-scoring/.plugin/plugin.json +19 -0
  43. package/plugins/migration-scoring/README.md +244 -0
  44. package/plugins/migration-scoring/skills/migration-mapping/SKILL.md +72 -0
  45. package/plugins/migration-scoring/skills/migration-report/SKILL.md +118 -0
  46. package/plugins/migration-scoring/skills/migration-scoring-overview/SKILL.md +126 -0
  47. package/plugins/migration-scoring/skills/score-quality/SKILL.md +54 -0
  48. package/plugins/migration-scoring/skills/score-quality/references/scoring-criteria.md +30 -0
  49. package/plugins/migration-scoring/skills/score-style/SKILL.md +106 -0
  50. package/plugins/onboarding/.plugin/plugin.json +20 -0
  51. package/plugins/onboarding/README.md +30 -0
  52. package/plugins/onboarding/references/criteria.md +144 -0
  53. package/plugins/onboarding/skills/agent-readiness-report/README.md +23 -0
  54. package/plugins/onboarding/skills/agent-readiness-report/SKILL.md +122 -0
  55. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_agent_instructions.sh +88 -0
  56. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_build_env.sh +114 -0
  57. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_feedback_loops.sh +133 -0
  58. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_policy.sh +113 -0
  59. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_workflows.sh +127 -0
  60. package/plugins/onboarding/skills/improve-agent-readiness/README.md +19 -0
  61. package/plugins/onboarding/skills/improve-agent-readiness/SKILL.md +167 -0
  62. package/plugins/onboarding/skills/setup-agents-md/README.md +15 -0
  63. package/plugins/onboarding/skills/setup-agents-md/SKILL.md +150 -0
  64. package/plugins/onboarding/skills/setup-openhands/README.md +20 -0
  65. package/plugins/onboarding/skills/setup-openhands/SKILL.md +56 -0
  66. package/plugins/onboarding/skills/setup-pr-review/README.md +23 -0
  67. package/plugins/onboarding/skills/setup-pr-review/SKILL.md +72 -0
  68. package/plugins/openhands/.plugin/plugin.json +13 -0
  69. package/plugins/openhands/README.md +52 -0
  70. package/plugins/openhands/SKILL.md +61 -0
  71. package/plugins/openhands/commands/create.md +55 -0
  72. package/plugins/openhands/commands/openhands-cloud.md +8 -0
  73. package/plugins/openhands/scripts/run.sh +69 -0
  74. package/plugins/pr-review/.plugin/plugin.json +13 -0
  75. package/plugins/pr-review/README.md +393 -0
  76. package/plugins/pr-review/action.yml +298 -0
  77. package/plugins/pr-review/scripts/agent_script.py +1282 -0
  78. package/plugins/pr-review/scripts/evaluate_review.py +655 -0
  79. package/plugins/pr-review/scripts/prompt.py +260 -0
  80. package/plugins/pr-review/workflows/pr-review-by-openhands.yml +51 -0
  81. package/plugins/pr-review/workflows/pr-review-evaluation.yml +85 -0
  82. package/plugins/qa-changes/.plugin/plugin.json +11 -0
  83. package/plugins/qa-changes/README.md +185 -0
  84. package/plugins/qa-changes/action.yml +181 -0
  85. package/plugins/qa-changes/scripts/agent_script.py +406 -0
  86. package/plugins/qa-changes/scripts/evaluate_qa_changes.py +385 -0
  87. package/plugins/qa-changes/scripts/prompt.py +174 -0
  88. package/plugins/qa-changes/workflows/qa-changes-by-openhands.yml +50 -0
  89. package/plugins/qa-changes/workflows/qa-changes-evaluation.yml +85 -0
  90. package/plugins/release-notes/.plugin/plugin.json +19 -0
  91. package/plugins/release-notes/README.md +283 -0
  92. package/plugins/release-notes/SKILL.md +83 -0
  93. package/plugins/release-notes/action.yml +117 -0
  94. package/plugins/release-notes/commands/release-notes.md +8 -0
  95. package/plugins/release-notes/scripts/agent_script.py +292 -0
  96. package/plugins/release-notes/scripts/generate_release_notes.py +733 -0
  97. package/plugins/release-notes/scripts/prompt.py +90 -0
  98. package/plugins/release-notes/scripts/validate_release_notes.py +328 -0
  99. package/plugins/release-notes/workflows/release-notes.yml +76 -0
  100. package/plugins/vulnerability-remediation/.plugin/plugin.json +19 -0
  101. package/plugins/vulnerability-remediation/README.md +217 -0
  102. package/plugins/vulnerability-remediation/action.yml +187 -0
  103. package/plugins/vulnerability-remediation/scripts/scan_and_remediate.py +561 -0
  104. package/plugins/vulnerability-remediation/workflows/vulnerability-scan.yml +87 -0
  105. package/pyproject.toml +12 -0
  106. package/release-please-config.json +16 -0
  107. package/scripts/sync_extensions.py +494 -0
  108. package/scripts/sync_openhands_sdk_skill.py +264 -0
  109. package/skills/README.md +159 -0
  110. package/skills/add-javadoc/.plugin/plugin.json +18 -0
  111. package/skills/add-javadoc/README.md +40 -0
  112. package/skills/add-javadoc/SKILL.md +35 -0
  113. package/skills/add-javadoc/references/example.md +32 -0
  114. package/skills/add-skill/.plugin/plugin.json +18 -0
  115. package/skills/add-skill/README.md +67 -0
  116. package/skills/add-skill/SKILL.md +47 -0
  117. package/skills/add-skill/scripts/fetch_skill.py +259 -0
  118. package/skills/agent-creator/.plugin/plugin.json +20 -0
  119. package/skills/agent-creator/README.md +104 -0
  120. package/skills/agent-creator/SKILL.md +190 -0
  121. package/skills/agent-creator/commands/agent-creator.md +8 -0
  122. package/skills/agent-creator/references/fallback.md +117 -0
  123. package/skills/agent-memory/.plugin/plugin.json +18 -0
  124. package/skills/agent-memory/README.md +35 -0
  125. package/skills/agent-memory/SKILL.md +30 -0
  126. package/skills/agent-memory/commands/remember.md +8 -0
  127. package/skills/agent-sdk-builder/.plugin/plugin.json +18 -0
  128. package/skills/agent-sdk-builder/README.md +40 -0
  129. package/skills/agent-sdk-builder/SKILL.md +37 -0
  130. package/skills/agent-sdk-builder/commands/agent-builder.md +8 -0
  131. package/skills/azure-devops/.plugin/plugin.json +18 -0
  132. package/skills/azure-devops/README.md +55 -0
  133. package/skills/azure-devops/SKILL.md +50 -0
  134. package/skills/bitbucket/.plugin/plugin.json +17 -0
  135. package/skills/bitbucket/README.md +50 -0
  136. package/skills/bitbucket/SKILL.md +45 -0
  137. package/skills/code-review/.plugin/plugin.json +19 -0
  138. package/skills/code-review/README.md +18 -0
  139. package/skills/code-review/SKILL.md +208 -0
  140. package/skills/code-review/commands/codereview-roasted.md +8 -0
  141. package/skills/code-review/commands/codereview.md +8 -0
  142. package/skills/code-review/references/risk-evaluation.md +41 -0
  143. package/skills/code-review/references/supply-chain-security.md +31 -0
  144. package/skills/code-simplifier/.plugin/plugin.json +21 -0
  145. package/skills/code-simplifier/README.md +30 -0
  146. package/skills/code-simplifier/SKILL.md +91 -0
  147. package/skills/code-simplifier/commands/simplify.md +8 -0
  148. package/skills/code-simplifier/references/code-quality-review.md +86 -0
  149. package/skills/code-simplifier/references/code-reuse-review.md +63 -0
  150. package/skills/code-simplifier/references/efficiency-review.md +81 -0
  151. package/skills/datadog/.plugin/plugin.json +19 -0
  152. package/skills/datadog/README.md +100 -0
  153. package/skills/datadog/SKILL.md +95 -0
  154. package/skills/deno/.plugin/plugin.json +18 -0
  155. package/skills/deno/README.md +5 -0
  156. package/skills/deno/SKILL.md +99 -0
  157. package/skills/deno/references/README.md +6 -0
  158. package/skills/discord/.plugin/plugin.json +18 -0
  159. package/skills/discord/README.md +31 -0
  160. package/skills/discord/SKILL.md +109 -0
  161. package/skills/discord/__init__.py +0 -0
  162. package/skills/discord/references/REFERENCE.md +78 -0
  163. package/skills/discord/scripts/__init__.py +0 -0
  164. package/skills/discord/scripts/_http.py +127 -0
  165. package/skills/discord/scripts/post_webhook.py +106 -0
  166. package/skills/discord/scripts/send_message.py +102 -0
  167. package/skills/docker/.plugin/plugin.json +17 -0
  168. package/skills/docker/README.md +34 -0
  169. package/skills/docker/SKILL.md +29 -0
  170. package/skills/evidence-based-citations/.plugin/plugin.json +20 -0
  171. package/skills/evidence-based-citations/README.md +31 -0
  172. package/skills/evidence-based-citations/SKILL.md +59 -0
  173. package/skills/flarglebargle/.plugin/plugin.json +16 -0
  174. package/skills/flarglebargle/README.md +14 -0
  175. package/skills/flarglebargle/SKILL.md +9 -0
  176. package/skills/frontend-design/.plugin/plugin.json +21 -0
  177. package/skills/frontend-design/LICENSE.txt +177 -0
  178. package/skills/frontend-design/README.md +42 -0
  179. package/skills/frontend-design/SKILL.md +42 -0
  180. package/skills/github/.plugin/plugin.json +19 -0
  181. package/skills/github/README.md +42 -0
  182. package/skills/github/SKILL.md +106 -0
  183. package/skills/github-pr-review/.plugin/plugin.json +18 -0
  184. package/skills/github-pr-review/README.md +145 -0
  185. package/skills/github-pr-review/SKILL.md +148 -0
  186. package/skills/github-pr-review/commands/github-pr-review.md +8 -0
  187. package/skills/github-pr-reviewer/.plugin/plugin.json +20 -0
  188. package/skills/github-pr-reviewer/README.md +34 -0
  189. package/skills/github-pr-reviewer/SKILL.md +89 -0
  190. package/skills/github-pr-reviewer/commands/pr-reviewer:setup.md +8 -0
  191. package/skills/github-repo-monitor/.plugin/plugin.json +22 -0
  192. package/skills/github-repo-monitor/README.md +70 -0
  193. package/skills/github-repo-monitor/SKILL.md +316 -0
  194. package/skills/github-repo-monitor/commands/github-monitor:poll.md +8 -0
  195. package/skills/github-repo-monitor/references/github-api.md +241 -0
  196. package/skills/github-repo-monitor/references/state-schema.md +160 -0
  197. package/skills/github-repo-monitor/scripts/main.py +915 -0
  198. package/skills/github-repo-monitor/tests/test_main.py +400 -0
  199. package/skills/gitlab/.plugin/plugin.json +17 -0
  200. package/skills/gitlab/README.md +37 -0
  201. package/skills/gitlab/SKILL.md +32 -0
  202. package/skills/incident-retrospective/.plugin/plugin.json +21 -0
  203. package/skills/incident-retrospective/README.md +34 -0
  204. package/skills/incident-retrospective/SKILL.md +98 -0
  205. package/skills/incident-retrospective/commands/incident-retro:setup.md +8 -0
  206. package/skills/iterate/.plugin/plugin.json +13 -0
  207. package/skills/iterate/README.md +25 -0
  208. package/skills/iterate/SKILL.md +399 -0
  209. package/skills/iterate/commands/babysit.md +8 -0
  210. package/skills/iterate/commands/iterate.md +8 -0
  211. package/skills/iterate/commands/verify.md +8 -0
  212. package/skills/iterate/references/heuristics.md +58 -0
  213. package/skills/iterate/references/verification.md +96 -0
  214. package/skills/jupyter/.plugin/plugin.json +18 -0
  215. package/skills/jupyter/README.md +55 -0
  216. package/skills/jupyter/SKILL.md +50 -0
  217. package/skills/kubernetes/.plugin/plugin.json +18 -0
  218. package/skills/kubernetes/README.md +53 -0
  219. package/skills/kubernetes/SKILL.md +48 -0
  220. package/skills/learn-from-code-review/.plugin/plugin.json +19 -0
  221. package/skills/learn-from-code-review/README.md +64 -0
  222. package/skills/learn-from-code-review/SKILL.md +186 -0
  223. package/skills/learn-from-code-review/commands/learn-from-reviews.md +8 -0
  224. package/skills/linear/.plugin/plugin.json +19 -0
  225. package/skills/linear/README.md +58 -0
  226. package/skills/linear/SKILL.md +213 -0
  227. package/skills/linear-triage/.plugin/plugin.json +21 -0
  228. package/skills/linear-triage/README.md +34 -0
  229. package/skills/linear-triage/SKILL.md +91 -0
  230. package/skills/linear-triage/commands/linear-triage:setup.md +8 -0
  231. package/skills/notion/.plugin/plugin.json +17 -0
  232. package/skills/notion/README.md +114 -0
  233. package/skills/notion/SKILL.md +109 -0
  234. package/skills/npm/.plugin/plugin.json +17 -0
  235. package/skills/npm/README.md +14 -0
  236. package/skills/npm/SKILL.md +9 -0
  237. package/skills/openhands-api/.plugin/plugin.json +22 -0
  238. package/skills/openhands-api/README.md +48 -0
  239. package/skills/openhands-api/SKILL.md +399 -0
  240. package/skills/openhands-api/references/README.md +33 -0
  241. package/skills/openhands-api/references/TROUBLESHOOTING.md +81 -0
  242. package/skills/openhands-api/references/example_prompt.md +12 -0
  243. package/skills/openhands-api/scripts/openhands_api.py +606 -0
  244. package/skills/openhands-api/scripts/openhands_api.ts +252 -0
  245. package/skills/openhands-automation/.plugin/plugin.json +19 -0
  246. package/skills/openhands-automation/README.md +89 -0
  247. package/skills/openhands-automation/SKILL.md +875 -0
  248. package/skills/openhands-automation/commands/automation:create.md +8 -0
  249. package/skills/openhands-automation/references/ab-testing.md +185 -0
  250. package/skills/openhands-automation/references/custom-automation.md +644 -0
  251. package/skills/openhands-sdk/.plugin/plugin.json +20 -0
  252. package/skills/openhands-sdk/README.md +22 -0
  253. package/skills/openhands-sdk/SKILL.md +229 -0
  254. package/skills/openhands-sdk/commands/sdk.md +8 -0
  255. package/skills/pdflatex/.plugin/plugin.json +18 -0
  256. package/skills/pdflatex/README.md +39 -0
  257. package/skills/pdflatex/SKILL.md +34 -0
  258. package/skills/prd/.plugin/plugin.json +19 -0
  259. package/skills/prd/README.md +28 -0
  260. package/skills/prd/SKILL.md +237 -0
  261. package/skills/prd/commands/prd.md +8 -0
  262. package/skills/qa-changes/README.md +18 -0
  263. package/skills/qa-changes/SKILL.md +229 -0
  264. package/skills/qa-changes/commands/qa-changes.md +8 -0
  265. package/skills/release-notes/README.md +24 -0
  266. package/skills/release-notes/SKILL.md +19 -0
  267. package/skills/release-notes/commands/release-notes.md +8 -0
  268. package/skills/research-brief/.plugin/plugin.json +20 -0
  269. package/skills/research-brief/README.md +34 -0
  270. package/skills/research-brief/SKILL.md +99 -0
  271. package/skills/research-brief/commands/research-brief:setup.md +8 -0
  272. package/skills/security/.plugin/plugin.json +18 -0
  273. package/skills/security/README.md +38 -0
  274. package/skills/security/SKILL.md +33 -0
  275. package/skills/skill-creator/.plugin/plugin.json +17 -0
  276. package/skills/skill-creator/LICENSE.txt +202 -0
  277. package/skills/skill-creator/README.md +182 -0
  278. package/skills/skill-creator/SKILL.md +545 -0
  279. package/skills/skill-creator/references/output-patterns.md +82 -0
  280. package/skills/skill-creator/references/workflows.md +28 -0
  281. package/skills/skill-creator/scripts/init_skill.py +303 -0
  282. package/skills/skill-creator/scripts/quick_validate.py +95 -0
  283. package/skills/slack-channel-monitor/.plugin/plugin.json +21 -0
  284. package/skills/slack-channel-monitor/README.md +91 -0
  285. package/skills/slack-channel-monitor/SKILL.md +276 -0
  286. package/skills/slack-channel-monitor/commands/slack-monitor:poll.md +8 -0
  287. package/skills/slack-channel-monitor/references/slack-api.md +207 -0
  288. package/skills/slack-channel-monitor/references/state-schema.md +180 -0
  289. package/skills/slack-channel-monitor/scripts/main.py +962 -0
  290. package/skills/slack-standup-digest/.plugin/plugin.json +21 -0
  291. package/skills/slack-standup-digest/README.md +34 -0
  292. package/skills/slack-standup-digest/SKILL.md +92 -0
  293. package/skills/slack-standup-digest/commands/standup-digest:setup.md +8 -0
  294. package/skills/spark-version-upgrade/.plugin/plugin.json +20 -0
  295. package/skills/spark-version-upgrade/README.md +54 -0
  296. package/skills/spark-version-upgrade/SKILL.md +233 -0
  297. package/skills/ssh/.plugin/plugin.json +18 -0
  298. package/skills/ssh/README.md +140 -0
  299. package/skills/ssh/SKILL.md +135 -0
  300. package/skills/swift-linux/.plugin/plugin.json +17 -0
  301. package/skills/swift-linux/README.md +86 -0
  302. package/skills/swift-linux/SKILL.md +81 -0
  303. package/skills/theme-factory/.plugin/plugin.json +19 -0
  304. package/skills/theme-factory/LICENSE.txt +202 -0
  305. package/skills/theme-factory/README.md +58 -0
  306. package/skills/theme-factory/SKILL.md +59 -0
  307. package/skills/theme-factory/theme-showcase.pdf +0 -0
  308. package/skills/theme-factory/themes/arctic-frost.md +19 -0
  309. package/skills/theme-factory/themes/botanical-garden.md +19 -0
  310. package/skills/theme-factory/themes/desert-rose.md +19 -0
  311. package/skills/theme-factory/themes/forest-canopy.md +19 -0
  312. package/skills/theme-factory/themes/golden-hour.md +19 -0
  313. package/skills/theme-factory/themes/midnight-galaxy.md +19 -0
  314. package/skills/theme-factory/themes/modern-minimalist.md +19 -0
  315. package/skills/theme-factory/themes/ocean-depths.md +19 -0
  316. package/skills/theme-factory/themes/sunset-boulevard.md +19 -0
  317. package/skills/theme-factory/themes/tech-innovation.md +19 -0
  318. package/skills/uv/.plugin/plugin.json +18 -0
  319. package/skills/uv/README.md +5 -0
  320. package/skills/uv/SKILL.md +95 -0
  321. package/skills/uv/references/README.md +5 -0
  322. package/skills/vercel/.plugin/plugin.json +18 -0
  323. package/skills/vercel/README.md +108 -0
  324. package/skills/vercel/SKILL.md +103 -0
  325. package/tests/test_add_skill_installs_to_agents_dir.py +42 -0
  326. package/tests/test_catalogs.py +109 -0
  327. package/tests/test_code_review_risk_evaluation.py +94 -0
  328. package/tests/test_issue_duplicate_checker.py +240 -0
  329. package/tests/test_openhands_api_python.py +152 -0
  330. package/tests/test_plugin_manifest.py +83 -0
  331. package/tests/test_pr_review_diff_payload.py +202 -0
  332. package/tests/test_pr_review_feedback.py +263 -0
  333. package/tests/test_pr_review_prompt.py +152 -0
  334. package/tests/test_pr_review_review_context.py +253 -0
  335. package/tests/test_qa_changes.py +232 -0
  336. package/tests/test_qa_changes_evaluation.py +259 -0
  337. package/tests/test_release_notes_generator.py +990 -0
  338. package/tests/test_sdk_loading.py +150 -0
  339. package/tests/test_skill_plugin_loading.py +149 -0
  340. package/tests/test_skills_have_readme.py +66 -0
  341. package/tests/test_sync_extensions.py +292 -0
  342. package/tests/test_workflow_sync.py +46 -0
  343. package/utils/analysis/README.md +7 -0
  344. package/utils/analysis/laminar_signals/README.md +211 -0
  345. package/utils/analysis/laminar_signals/analyze.py +780 -0
  346. package/utils/analysis/laminar_signals/templates/default.j2 +49 -0
  347. package/utils/analysis/laminar_signals/templates/pr_review.j2 +61 -0
@@ -0,0 +1,400 @@
1
+ """Unit tests for github-repo-monitor main.py.
2
+
3
+ Run from the skill root:
4
+ python -m pytest tests/
5
+ or with the standard library runner:
6
+ python -m unittest discover tests
7
+ """
8
+
9
+ import json
10
+ import os
11
+ import sys
12
+ import tempfile
13
+ import time
14
+ import unittest
15
+ from pathlib import Path
16
+ from unittest.mock import MagicMock, patch, call
17
+
18
+ # Allow importing main.py from the sibling scripts/ directory.
19
+ sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
20
+ import main # noqa: E402
21
+
22
+
23
+ # ── Helpers ────────────────────────────────────────────────────────────────────
24
+
25
+ def _make_comment(body="hello @openhands", login="octocat", user_type="User"):
26
+ return {"id": 1, "body": body, "user": {"login": login, "type": user_type},
27
+ "issue_url": "https://api.github.com/repos/owner/repo/issues/7"}
28
+
29
+
30
+ # ── State file tests ───────────────────────────────────────────────────────────
31
+
32
+ class TestLoadState(unittest.TestCase):
33
+
34
+ def test_missing_file_returns_default(self):
35
+ state = main.load_state("/nonexistent/path/state.json")
36
+ self.assertIn("conversations", state)
37
+ self.assertIn("processed_comment_ids", state)
38
+ self.assertEqual(state["version"], 1)
39
+
40
+ def test_valid_json_is_loaded(self):
41
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
42
+ json.dump({"version": 1, "custom": "value", "conversations": {}}, f)
43
+ path = f.name
44
+ try:
45
+ state = main.load_state(path)
46
+ self.assertEqual(state["custom"], "value")
47
+ finally:
48
+ os.unlink(path)
49
+
50
+ def test_corrupted_json_returns_default(self):
51
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
52
+ f.write("{this is not valid json!!!}")
53
+ path = f.name
54
+ try:
55
+ state = main.load_state(path)
56
+ # Should return the default state rather than raising.
57
+ self.assertIn("conversations", state)
58
+ self.assertEqual(state["version"], 1)
59
+ finally:
60
+ os.unlink(path)
61
+
62
+ def test_empty_file_returns_default(self):
63
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
64
+ f.write("")
65
+ path = f.name
66
+ try:
67
+ state = main.load_state(path)
68
+ self.assertIn("conversations", state)
69
+ finally:
70
+ os.unlink(path)
71
+
72
+
73
+ class TestSaveAndLoadRoundtrip(unittest.TestCase):
74
+
75
+ def test_roundtrip(self):
76
+ data = {
77
+ "version": 1,
78
+ "conversations": {"42": {"conversation_id": "abc", "status": "active"}},
79
+ "processed_comment_ids": [1, 2, 3],
80
+ }
81
+ with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as f:
82
+ path = f.name
83
+ try:
84
+ main.save_state(path, data)
85
+ loaded = main.load_state(path)
86
+ self.assertEqual(loaded["conversations"]["42"]["conversation_id"], "abc")
87
+ self.assertEqual(loaded["processed_comment_ids"], [1, 2, 3])
88
+ finally:
89
+ os.unlink(path)
90
+
91
+
92
+ # ── Bot detection tests ────────────────────────────────────────────────────────
93
+
94
+ class TestIsBotComment(unittest.TestCase):
95
+
96
+ def test_login_ends_with_bot_suffix(self):
97
+ self.assertTrue(main._is_bot_comment(
98
+ {"user": {"login": "dependabot[bot]", "type": "Bot"}}
99
+ ))
100
+
101
+ def test_login_ends_with_bot_suffix_human_type(self):
102
+ # Login suffix alone is sufficient.
103
+ self.assertTrue(main._is_bot_comment(
104
+ {"user": {"login": "mybot[bot]", "type": "User"}}
105
+ ))
106
+
107
+ def test_user_type_bot_without_suffix(self):
108
+ self.assertTrue(main._is_bot_comment(
109
+ {"user": {"login": "AutomationService", "type": "Bot"}}
110
+ ))
111
+
112
+ def test_human_user_returns_false(self):
113
+ self.assertFalse(main._is_bot_comment(
114
+ {"user": {"login": "octocat", "type": "User"}}
115
+ ))
116
+
117
+ def test_missing_user_returns_false(self):
118
+ self.assertFalse(main._is_bot_comment({}))
119
+
120
+ def test_null_user_returns_false(self):
121
+ self.assertFalse(main._is_bot_comment({"user": None}))
122
+
123
+ def test_login_containing_but_not_ending_with_bot(self):
124
+ # "botuser" does not end with "[bot]" — should be treated as human.
125
+ self.assertFalse(main._is_bot_comment(
126
+ {"user": {"login": "botuser", "type": "User"}}
127
+ ))
128
+
129
+
130
+ # ── Author authorization tests ────────────────────────────────────────────────
131
+
132
+ class TestAllowedCommentAuthor(unittest.TestCase):
133
+
134
+ def setUp(self):
135
+ self._original_allowed = main.ALLOWED_GITHUB_LOGINS
136
+
137
+ def tearDown(self):
138
+ main.ALLOWED_GITHUB_LOGINS = self._original_allowed
139
+
140
+ def test_default_token_owner_allows_owner(self):
141
+ main.ALLOWED_GITHUB_LOGINS = ["<TOKEN_OWNER>"]
142
+ comment = _make_comment(login="OctoCat")
143
+ self.assertTrue(main._is_allowed_comment_author(comment, "octocat"))
144
+
145
+ def test_default_token_owner_rejects_other_user(self):
146
+ main.ALLOWED_GITHUB_LOGINS = ["<TOKEN_OWNER>"]
147
+ comment = _make_comment(login="enyst")
148
+ self.assertFalse(main._is_allowed_comment_author(comment, "tofarr"))
149
+
150
+ def test_explicit_allowlist_is_case_insensitive(self):
151
+ main.ALLOWED_GITHUB_LOGINS = ["Enyst", "tofarr"]
152
+ comment = _make_comment(login="enyst")
153
+ self.assertTrue(main._is_allowed_comment_author(comment, "someone-else"))
154
+
155
+ def test_wildcard_allows_any_commenter(self):
156
+ main.ALLOWED_GITHUB_LOGINS = ["*"]
157
+ comment = _make_comment(login="anyone")
158
+ self.assertTrue(main._is_allowed_comment_author(comment, "octocat"))
159
+
160
+ def test_missing_author_is_rejected(self):
161
+ main.ALLOWED_GITHUB_LOGINS = ["*"]
162
+ self.assertFalse(main._is_allowed_comment_author({}, "octocat"))
163
+
164
+
165
+ # ── Trigger phrase tests ───────────────────────────────────────────────────────
166
+
167
+ class TestHasTrigger(unittest.TestCase):
168
+
169
+ def test_exact_match(self):
170
+ c = _make_comment(body="Please fix this @openhands")
171
+ self.assertTrue(main._has_trigger(c, "@openhands"))
172
+
173
+ def test_case_insensitive_upper(self):
174
+ c = _make_comment(body="Hey @OpenHands can you help?")
175
+ self.assertTrue(main._has_trigger(c, "@openhands"))
176
+
177
+ def test_case_insensitive_phrase_uppercase(self):
178
+ c = _make_comment(body="@openhands please look at this")
179
+ self.assertTrue(main._has_trigger(c, "@OPENHANDS"))
180
+
181
+ def test_custom_trigger_phrase(self):
182
+ c = _make_comment(body="yeehaw! this needs fixing")
183
+ self.assertTrue(main._has_trigger(c, "yeehaw!"))
184
+
185
+ def test_absent_phrase_returns_false(self):
186
+ c = _make_comment(body="Just a regular comment, nothing special")
187
+ self.assertFalse(main._has_trigger(c, "@openhands"))
188
+
189
+ def test_empty_body_returns_false(self):
190
+ c = _make_comment(body="")
191
+ self.assertFalse(main._has_trigger(c, "@openhands"))
192
+
193
+ def test_none_body_returns_false(self):
194
+ c = {"id": 1, "body": None, "user": {"login": "u", "type": "User"}}
195
+ self.assertFalse(main._has_trigger(c, "@openhands"))
196
+
197
+ def test_missing_body_returns_false(self):
198
+ c = {"id": 1, "user": {"login": "u", "type": "User"}}
199
+ self.assertFalse(main._has_trigger(c, "@openhands"))
200
+
201
+
202
+ # ── Processed-ID deduplication tests ──────────────────────────────────────────
203
+
204
+ class TestProcessedIdDeduplication(unittest.TestCase):
205
+ """
206
+ The dedup logic lives in main() but the set membership check is trivial.
207
+ These tests verify the state schema: processed_comment_ids is persisted
208
+ and re-hydrated correctly across simulated runs.
209
+ """
210
+
211
+ def test_ids_survive_save_and_load(self):
212
+ with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as f:
213
+ path = f.name
214
+ try:
215
+ state = {"version": 1, "conversations": {},
216
+ "processed_comment_ids": [101, 202, 303]}
217
+ main.save_state(path, state)
218
+ loaded = main.load_state(path)
219
+ self.assertIn(101, loaded["processed_comment_ids"])
220
+ self.assertIn(303, loaded["processed_comment_ids"])
221
+ self.assertNotIn(404, loaded["processed_comment_ids"])
222
+ finally:
223
+ os.unlink(path)
224
+
225
+
226
+ # ── Conversation state transition tests ───────────────────────────────────────
227
+
228
+ class TestEnsureConversation(unittest.TestCase):
229
+ """Tests for _ensure_conversation using mocked API calls."""
230
+
231
+ BASE_ARGS = dict(
232
+ agent_url="http://agent",
233
+ api_key="key",
234
+ conv_key="7",
235
+ issue_number=7,
236
+ is_pr=False,
237
+ html_url="https://github.com/owner/repo/issues/7",
238
+ prompt="Do something",
239
+ comment=_make_comment(),
240
+ item_type="issue",
241
+ )
242
+
243
+ @patch("main.create_conversation", return_value="new-conv-id")
244
+ def test_creates_new_when_no_existing(self, mock_create):
245
+ conversations = {}
246
+ conv_id, resumed = main._ensure_conversation(conversations=conversations,
247
+ **self.BASE_ARGS)
248
+ self.assertEqual(conv_id, "new-conv-id")
249
+ self.assertFalse(resumed)
250
+ mock_create.assert_called_once()
251
+ self.assertEqual(conversations["7"]["status"], "active")
252
+
253
+ @patch("main.send_to_conversation")
254
+ def test_reopens_closed_conversation(self, mock_send):
255
+ conversations = {
256
+ "7": {"conversation_id": "old-conv-id", "status": "closed",
257
+ "issue_number": 7, "last_activity": 0.0}
258
+ }
259
+ conv_id, resumed = main._ensure_conversation(conversations=conversations,
260
+ **self.BASE_ARGS)
261
+ self.assertEqual(conv_id, "old-conv-id")
262
+ self.assertTrue(resumed)
263
+ mock_send.assert_called_once()
264
+ self.assertEqual(conversations["7"]["status"], "active")
265
+
266
+ @patch("main.create_conversation", return_value="fallback-conv-id")
267
+ @patch("main.send_to_conversation", side_effect=RuntimeError("gone"))
268
+ def test_fallback_to_new_when_closed_unreachable(self, mock_send, mock_create):
269
+ conversations = {
270
+ "7": {"conversation_id": "stale-conv-id", "status": "closed",
271
+ "issue_number": 7, "last_activity": 0.0}
272
+ }
273
+ conv_id, resumed = main._ensure_conversation(conversations=conversations,
274
+ **self.BASE_ARGS)
275
+ self.assertEqual(conv_id, "fallback-conv-id")
276
+ self.assertFalse(resumed)
277
+ mock_create.assert_called_once()
278
+ self.assertEqual(conversations["7"]["status"], "active")
279
+
280
+ @patch("main.create_conversation", return_value="brand-new-id")
281
+ def test_creates_new_when_status_unknown(self, mock_create):
282
+ # An entry with an unrecognised status should be treated as missing.
283
+ conversations = {
284
+ "7": {"conversation_id": "weird-id", "status": "unknown"}
285
+ }
286
+ conv_id, resumed = main._ensure_conversation(conversations=conversations,
287
+ **self.BASE_ARGS)
288
+ self.assertEqual(conv_id, "brand-new-id")
289
+ self.assertFalse(resumed)
290
+
291
+ @patch("main.create_conversation", side_effect=RuntimeError("API down"))
292
+ def test_raises_when_create_fails(self, _mock_create):
293
+ conversations = {}
294
+ with self.assertRaises(RuntimeError):
295
+ main._ensure_conversation(conversations=conversations, **self.BASE_ARGS)
296
+
297
+
298
+ # ── Acknowledgement message tests ─────────────────────────────────────────────
299
+
300
+ class TestPostAcknowledgement(unittest.TestCase):
301
+
302
+ @patch("main._post_github_comment")
303
+ def test_new_conversation_message(self, mock_post):
304
+ main._post_acknowledgement(
305
+ github_token="tok", repo="o/r", issue_number=5,
306
+ item_type="issue", conv_url="http://app/conv/1", resumed=False,
307
+ )
308
+ body = mock_post.call_args[0][3]
309
+ self.assertIn("OpenHands is on it!", body)
310
+ self.assertNotIn("resuming", body.lower())
311
+
312
+ @patch("main._post_github_comment")
313
+ def test_resumed_conversation_message(self, mock_post):
314
+ main._post_acknowledgement(
315
+ github_token="tok", repo="o/r", issue_number=5,
316
+ item_type="pull request", conv_url="http://app/conv/2", resumed=True,
317
+ )
318
+ body = mock_post.call_args[0][3]
319
+ self.assertIn("resuming", body.lower())
320
+ self.assertNotIn("OpenHands is on it!", body)
321
+
322
+ @patch("main._post_github_comment")
323
+ def test_trigger_phrase_in_footer(self, mock_post):
324
+ original = main.TRIGGER_PHRASE
325
+ main.TRIGGER_PHRASE = "yeehaw!"
326
+ try:
327
+ main._post_acknowledgement(
328
+ github_token="tok", repo="o/r", issue_number=1,
329
+ item_type="issue", conv_url="http://x", resumed=False,
330
+ )
331
+ body = mock_post.call_args[0][3]
332
+ self.assertIn("yeehaw!", body)
333
+ finally:
334
+ main.TRIGGER_PHRASE = original
335
+
336
+
337
+ # ── _get_agent_dict tests ──────────────────────────────────────────────────────
338
+
339
+ class TestGetAgentDict(unittest.TestCase):
340
+ """Regression tests for agent-name resolution from /api/settings."""
341
+
342
+ def _mock_settings(self, agent_value, llm_value=None):
343
+ """Return a mock urlopen context manager that yields the given settings."""
344
+ payload = json.dumps({
345
+ "agent_settings": {"agent": agent_value, "llm": llm_value or {}}
346
+ }).encode()
347
+ mock_resp = MagicMock()
348
+ mock_resp.__enter__ = MagicMock(return_value=mock_resp)
349
+ mock_resp.__exit__ = MagicMock(return_value=False)
350
+ mock_resp.read = MagicMock(return_value=payload)
351
+ return mock_resp
352
+
353
+ @patch("urllib.request.urlopen")
354
+ def test_null_agent_falls_back_to_agent(self, mock_urlopen):
355
+ """agent=null in settings must fall back to 'Agent', not propagate as null."""
356
+ mock_urlopen.return_value = self._mock_settings(agent_value=None)
357
+ result = main._get_agent_dict("http://agent", "key")
358
+ self.assertEqual(result["kind"], "Agent")
359
+
360
+ @patch("urllib.request.urlopen")
361
+ def test_tools_always_included(self, mock_urlopen):
362
+ """terminal and file_editor must always be present so the agent has bash.
363
+
364
+ The runtime-registered names ('terminal', 'file_editor') must be used,
365
+ not the Python class names ('TerminalTool', 'FileEditorTool').
366
+ """
367
+ mock_urlopen.return_value = self._mock_settings(agent_value=None)
368
+ result = main._get_agent_dict("http://agent", "key")
369
+ tool_names = [t["name"] for t in result.get("tools", [])]
370
+ self.assertIn("terminal", tool_names)
371
+ self.assertIn("file_editor", tool_names)
372
+
373
+ @patch("urllib.request.urlopen")
374
+ def test_full_app_agent_name_not_forwarded(self, mock_urlopen):
375
+ """Full-app agent names (CodeActAgent, BrowsingAgent, …) must not be forwarded.
376
+
377
+ settings["agent_settings"]["agent"] belongs to the full OpenHands app
378
+ registry. The automation SDK only accepts 'Agent' / 'ACPAgent'.
379
+ Forwarding 'CodeActAgent' causes a 500 with 'Unknown kind' in production.
380
+ """
381
+ for app_agent in ("CodeActAgent", "BrowsingAgent", "SomeFutureAgent"):
382
+ with self.subTest(app_agent=app_agent):
383
+ mock_urlopen.return_value = self._mock_settings(agent_value=app_agent)
384
+ result = main._get_agent_dict("http://agent", "key")
385
+ self.assertEqual(result["kind"], "Agent")
386
+
387
+ @patch("urllib.request.urlopen")
388
+ def test_missing_agent_key_falls_back_to_agent(self, mock_urlopen):
389
+ payload = json.dumps({"agent_settings": {"llm": {}}}).encode()
390
+ mock_resp = MagicMock()
391
+ mock_resp.__enter__ = MagicMock(return_value=mock_resp)
392
+ mock_resp.__exit__ = MagicMock(return_value=False)
393
+ mock_resp.read = MagicMock(return_value=payload)
394
+ mock_urlopen.return_value = mock_resp
395
+ result = main._get_agent_dict("http://agent", "key")
396
+ self.assertEqual(result["kind"], "Agent")
397
+
398
+
399
+ if __name__ == "__main__":
400
+ unittest.main()
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "gitlab",
3
+ "version": "1.0.0",
4
+ "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.",
5
+ "author": {
6
+ "name": "OpenHands",
7
+ "email": "contact@all-hands.dev"
8
+ },
9
+ "homepage": "https://github.com/OpenHands/extensions",
10
+ "repository": "https://github.com/OpenHands/extensions",
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "gitlab",
14
+ "git",
15
+ "merge-request"
16
+ ]
17
+ }
@@ -0,0 +1,37 @@
1
+ # Gitlab
2
+
3
+ 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.
4
+
5
+ ## Triggers
6
+
7
+ This skill is activated by the following keywords:
8
+
9
+ - `gitlab`
10
+ - `git`
11
+
12
+ ## Details
13
+
14
+ You have access to an environment variable, `GITLAB_TOKEN`, which allows you to interact with
15
+ the GitLab API.
16
+
17
+ <IMPORTANT>
18
+ You can use `curl` with the `GITLAB_TOKEN` to interact with GitLab's API.
19
+ ALWAYS use the GitLab API for operations instead of a web browser.
20
+ ALWAYS use the `create_mr` tool to open a merge request
21
+ </IMPORTANT>
22
+
23
+ If 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`
24
+
25
+ Here are some instructions for pushing, but ONLY do this if the user asks you to:
26
+ * NEVER push directly to the `main` or `master` branch
27
+ * Git config (username and email) is pre-set. Do not modify.
28
+ * You may already be on a branch starting with `openhands-workspace`. Create a new branch with a better name before pushing.
29
+ * Use the `create_mr` tool to create a merge request, if you haven't already
30
+ * 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.
31
+ * Use the main branch as the base branch, unless the user requests otherwise
32
+ * After opening or updating a merge request, send the user a short message with a link to the merge request.
33
+ * 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:
34
+ ```bash
35
+ git remote -v && git branch # to find the current org, repo and branch
36
+ git checkout -b create-widget && git add . && git commit -m "Create widget" && git push -u origin create-widget
37
+ ```
@@ -0,0 +1,32 @@
1
+ ---
2
+ name: gitlab
3
+ 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.
4
+ triggers:
5
+ - gitlab
6
+ - git
7
+ ---
8
+
9
+ You have access to an environment variable, `GITLAB_TOKEN`, which allows you to interact with
10
+ the GitLab API.
11
+
12
+ <IMPORTANT>
13
+ You can use `curl` with the `GITLAB_TOKEN` to interact with GitLab's API.
14
+ ALWAYS use the GitLab API for operations instead of a web browser.
15
+ ALWAYS use the `create_mr` tool to open a merge request
16
+ </IMPORTANT>
17
+
18
+ If 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`
19
+
20
+ Here are some instructions for pushing, but ONLY do this if the user asks you to:
21
+ * NEVER push directly to the `main` or `master` branch
22
+ * Git config (username and email) is pre-set. Do not modify.
23
+ * You may already be on a branch starting with `openhands-workspace`. Create a new branch with a better name before pushing.
24
+ * Use the `create_mr` tool to create a merge request, if you haven't already
25
+ * 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.
26
+ * Use the main branch as the base branch, unless the user requests otherwise
27
+ * After opening or updating a merge request, send the user a short message with a link to the merge request.
28
+ * 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:
29
+ ```bash
30
+ git remote -v && git branch # to find the current org, repo and branch
31
+ git checkout -b create-widget && git add . && git commit -m "Create widget" && git push -u origin create-widget
32
+ ```
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "incident-retrospective",
3
+ "version": "1.0.0",
4
+ "description": "Create an automation that drafts incident retrospectives by gathering incident-channel messages from Slack, collecting linked tickets from Linear, and publishing a retrospective draft to Notion with timeline, impact summary, root-cause hypotheses, and action items.",
5
+ "author": {
6
+ "name": "OpenHands",
7
+ "email": "contact@all-hands.dev"
8
+ },
9
+ "homepage": "https://github.com/OpenHands/extensions",
10
+ "repository": "https://github.com/OpenHands/extensions",
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "incident",
14
+ "retrospective",
15
+ "postmortem",
16
+ "slack",
17
+ "linear",
18
+ "notion",
19
+ "automation"
20
+ ]
21
+ }
@@ -0,0 +1,34 @@
1
+ # Incident Retrospective Drafter
2
+
3
+ Create an automation that drafts incident retrospectives from Slack, Linear, and Notion.
4
+
5
+ ## Triggers
6
+
7
+ This skill is activated by keywords:
8
+
9
+ - `draft incident retrospective`
10
+ - `incident postmortem`
11
+ - `retro automation`
12
+
13
+ ## Features
14
+
15
+ - **Gathers incident-channel messages from Slack**
16
+ - **Collects linked tickets and follow-ups from Linear**
17
+ - **Publishes retrospective draft to Notion**
18
+ - **Customizable template**: timeline, impact, root cause, action items
19
+ - **Supports manual, cron, or event-based triggers**
20
+
21
+ ## Prerequisites
22
+
23
+ Slack MCP, Linear MCP, and Notion MCP installed in Settings → MCP
24
+
25
+ ## Quick Start
26
+
27
+ Ask OpenHands:
28
+
29
+ > "Set up an incident retro automation that pulls from #incidents in
30
+ > Slack, checks Linear for follow-ups, and publishes to our Notion retro database"
31
+
32
+ ## See Also
33
+
34
+ - [SKILL.md](SKILL.md) — Full setup workflow reference
@@ -0,0 +1,98 @@
1
+ ---
2
+ name: incident-retrospective
3
+ description: >
4
+ Create an automation that drafts incident retrospectives. Gathers
5
+ incident-channel messages from Slack, collects linked tickets and follow-ups
6
+ from Linear, and publishes a retrospective draft to Notion with a timeline,
7
+ impact summary, root-cause hypotheses, and action items.
8
+ triggers:
9
+ - /incident-retro:setup
10
+ ---
11
+
12
+ # Incident Retrospective Drafter Automation
13
+
14
+ Set up an automation that drafts incident retrospectives by pulling data from
15
+ Slack, Linear, and Notion.
16
+
17
+ ---
18
+
19
+ ## Prerequisites
20
+
21
+ ### Required integrations
22
+
23
+ All three MCP integrations must be installed in Settings → MCP:
24
+
25
+ - **Slack MCP** — to gather incident-channel messages
26
+ - **Linear MCP** — to collect linked tickets and follow-ups
27
+ - **Notion MCP** — to publish the retrospective draft
28
+
29
+ ### Information to collect
30
+
31
+ Ask the user for:
32
+
33
+ 1. **Incident identification** — how are incidents identified? (e.g. Slack channel naming convention like `#inc-*`, a Linear label, or manual trigger)
34
+ 2. **Slack channels** — which channels contain incident chatter (e.g. `#incidents`, `#inc-*` pattern)
35
+ 3. **Linear teams** — which Linear teams/projects to inspect for follow-up tickets
36
+ 4. **Retrospective template** — what sections should the retro include? Default: Timeline, Impact, Root Cause, Action Items, Lessons Learned
37
+ 5. **Notion destination** — which Notion database or page should receive the draft
38
+ 6. **Trigger type** — manual dispatch, cron schedule, or triggered by an incident label being added
39
+
40
+ ---
41
+
42
+ ## Setup Workflow
43
+
44
+ ### Step 1 — Verify MCP access
45
+
46
+ Test each integration:
47
+ ```
48
+ Use the Slack MCP to list recent messages in an incident channel.
49
+ Use the Linear MCP to list recent issues for the target team.
50
+ Use the Notion MCP to search for the destination database.
51
+ ```
52
+
53
+ If any fail, tell the user which integration needs to be installed first.
54
+
55
+ ### Step 2 — Determine trigger type
56
+
57
+ Ask the user how retros should be triggered:
58
+ - **Manual** — dispatch from the automations page when an incident wraps up
59
+ - **Cron** — run daily/weekly to check for recent incidents
60
+ - **Event** — triggered by a Linear label change or Slack message
61
+
62
+ ### Step 3 — Build the retro prompt
63
+
64
+ Construct a prompt that includes:
65
+ - How to identify the incident (channel pattern, label, etc.)
66
+ - Which Slack channels and Linear teams to query
67
+ - The retrospective template/sections
68
+ - Where to publish in Notion
69
+
70
+ ### Step 4 — Create the automation
71
+
72
+ Read the Automation backend URL and auth from `<RUNTIME_SERVICES>`:
73
+ - Use the **Automation backend** `url_from_agent` as `OPENHANDS_HOST`
74
+ - Auth: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`
75
+
76
+ Use the **prompt preset** endpoint:
77
+ ```bash
78
+ curl -s -X POST "${OPENHANDS_HOST}/api/automation/v1/preset/prompt" \
79
+ -H "X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY" \
80
+ -H "Content-Type: application/json" \
81
+ -d '{
82
+ "name": "Incident Retrospective Drafter",
83
+ "prompt": "<constructed retro prompt>",
84
+ "trigger": <trigger config from step 2>
85
+ }'
86
+ ```
87
+
88
+ ### Step 5 — Confirm
89
+
90
+ Tell the user:
91
+ > ✅ **Incident Retrospective Drafter** is running!
92
+ >
93
+ > - Automation ID: `{id}`
94
+ > - Incident source: `{identification method}`
95
+ > - Slack channels: `{channels}`
96
+ > - Linear teams: `{teams}`
97
+ > - Notion destination: `{destination}`
98
+ > - Trigger: `{trigger description}`
@@ -0,0 +1,8 @@
1
+ ---
2
+ # auto-generated by sync_extensions.py
3
+ 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.
4
+ ---
5
+
6
+ Read and follow the complete instructions in the SKILL.md file located in this skill's directory.
7
+
8
+ $ARGUMENTS
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "iterate",
3
+ "version": "1.0.0",
4
+ "description": "Iterate on a GitHub pull request — drive it through CI, code review, and QA until it is merge-ready.",
5
+ "author": {
6
+ "name": "OpenHands",
7
+ "email": "contact@all-hands.dev"
8
+ },
9
+ "homepage": "https://github.com/OpenHands/extensions",
10
+ "repository": "https://github.com/OpenHands/extensions",
11
+ "license": "MIT",
12
+ "keywords": ["github", "ci", "review", "qa", "pull-request", "iterate"]
13
+ }