@openhands/extensions 0.1.0 → 0.3.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 (354) 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/index.d.ts +2 -0
  15. package/index.js +1 -0
  16. package/marketplaces/large-codebase.json +66 -0
  17. package/marketplaces/openhands-extensions.json +682 -0
  18. package/package.json +12 -10
  19. package/plugins/README.md +30 -0
  20. package/plugins/city-weather/.plugin/plugin.json +13 -0
  21. package/plugins/city-weather/README.md +145 -0
  22. package/plugins/city-weather/commands/now.md +56 -0
  23. package/plugins/cobol-modernization/.plugin/plugin.json +19 -0
  24. package/plugins/cobol-modernization/README.md +201 -0
  25. package/plugins/cobol-modernization/references/troubleshooting.md +18 -0
  26. package/plugins/cobol-modernization/skills/build-setup/SKILL.md +78 -0
  27. package/plugins/cobol-modernization/skills/build-setup/scripts/install-gnucobol.sh +32 -0
  28. package/plugins/cobol-modernization/skills/cobol-modernization-overview/SKILL.md +113 -0
  29. package/plugins/cobol-modernization/skills/mainfraime-removal/SKILL.md +62 -0
  30. package/plugins/cobol-modernization/skills/mainfraime-removal/references/cics-transformation-examples.md +45 -0
  31. package/plugins/cobol-modernization/skills/mainframe-planning/SKILL.md +78 -0
  32. package/plugins/cobol-modernization/skills/to-java-migration/SKILL.md +59 -0
  33. package/plugins/cobol-modernization/skills/to-java-migration/references/cobol-to-java-example.md +58 -0
  34. package/plugins/cobol-modernization/skills/to-java-migration/references/datatype-mappings.md +19 -0
  35. package/plugins/issue-duplicate-checker/.plugin/plugin.json +13 -0
  36. package/plugins/issue-duplicate-checker/README.md +51 -0
  37. package/plugins/issue-duplicate-checker/action.yml +349 -0
  38. package/plugins/issue-duplicate-checker/scripts/auto_close_duplicate_issues.py +569 -0
  39. package/plugins/issue-duplicate-checker/scripts/issue_duplicate_check_openhands.py +681 -0
  40. package/plugins/issue-duplicate-checker/scripts/post_duplicate_notice.js +220 -0
  41. package/plugins/issue-duplicate-checker/scripts/remove_duplicate_candidate_label.js +27 -0
  42. package/plugins/magic-test/.plugin/plugin.json +13 -0
  43. package/plugins/magic-test/skills/magic-word/SKILL.md +33 -0
  44. package/plugins/migration-scoring/.plugin/plugin.json +19 -0
  45. package/plugins/migration-scoring/README.md +244 -0
  46. package/plugins/migration-scoring/skills/migration-mapping/SKILL.md +72 -0
  47. package/plugins/migration-scoring/skills/migration-report/SKILL.md +118 -0
  48. package/plugins/migration-scoring/skills/migration-scoring-overview/SKILL.md +126 -0
  49. package/plugins/migration-scoring/skills/score-quality/SKILL.md +54 -0
  50. package/plugins/migration-scoring/skills/score-quality/references/scoring-criteria.md +30 -0
  51. package/plugins/migration-scoring/skills/score-style/SKILL.md +106 -0
  52. package/plugins/onboarding/.plugin/plugin.json +20 -0
  53. package/plugins/onboarding/README.md +30 -0
  54. package/plugins/onboarding/references/criteria.md +144 -0
  55. package/plugins/onboarding/skills/agent-readiness-report/README.md +23 -0
  56. package/plugins/onboarding/skills/agent-readiness-report/SKILL.md +122 -0
  57. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_agent_instructions.sh +88 -0
  58. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_build_env.sh +114 -0
  59. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_feedback_loops.sh +133 -0
  60. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_policy.sh +113 -0
  61. package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_workflows.sh +127 -0
  62. package/plugins/onboarding/skills/improve-agent-readiness/README.md +19 -0
  63. package/plugins/onboarding/skills/improve-agent-readiness/SKILL.md +167 -0
  64. package/plugins/onboarding/skills/setup-agents-md/README.md +15 -0
  65. package/plugins/onboarding/skills/setup-agents-md/SKILL.md +150 -0
  66. package/plugins/onboarding/skills/setup-openhands/README.md +20 -0
  67. package/plugins/onboarding/skills/setup-openhands/SKILL.md +56 -0
  68. package/plugins/onboarding/skills/setup-pr-review/README.md +23 -0
  69. package/plugins/onboarding/skills/setup-pr-review/SKILL.md +72 -0
  70. package/plugins/openhands/.plugin/plugin.json +13 -0
  71. package/plugins/openhands/README.md +52 -0
  72. package/plugins/openhands/SKILL.md +61 -0
  73. package/plugins/openhands/commands/create.md +55 -0
  74. package/plugins/openhands/commands/openhands-cloud.md +8 -0
  75. package/plugins/openhands/scripts/run.sh +69 -0
  76. package/plugins/pr-review/.plugin/plugin.json +13 -0
  77. package/plugins/pr-review/README.md +393 -0
  78. package/plugins/pr-review/action.yml +298 -0
  79. package/plugins/pr-review/scripts/agent_script.py +1282 -0
  80. package/plugins/pr-review/scripts/evaluate_review.py +655 -0
  81. package/plugins/pr-review/scripts/prompt.py +260 -0
  82. package/plugins/pr-review/workflows/pr-review-by-openhands.yml +51 -0
  83. package/plugins/pr-review/workflows/pr-review-evaluation.yml +85 -0
  84. package/plugins/qa-changes/.plugin/plugin.json +11 -0
  85. package/plugins/qa-changes/README.md +185 -0
  86. package/plugins/qa-changes/action.yml +181 -0
  87. package/plugins/qa-changes/scripts/agent_script.py +406 -0
  88. package/plugins/qa-changes/scripts/evaluate_qa_changes.py +385 -0
  89. package/plugins/qa-changes/scripts/prompt.py +174 -0
  90. package/plugins/qa-changes/workflows/qa-changes-by-openhands.yml +50 -0
  91. package/plugins/qa-changes/workflows/qa-changes-evaluation.yml +85 -0
  92. package/plugins/release-notes/.plugin/plugin.json +19 -0
  93. package/plugins/release-notes/README.md +283 -0
  94. package/plugins/release-notes/SKILL.md +83 -0
  95. package/plugins/release-notes/action.yml +117 -0
  96. package/plugins/release-notes/commands/release-notes.md +8 -0
  97. package/plugins/release-notes/scripts/agent_script.py +292 -0
  98. package/plugins/release-notes/scripts/generate_release_notes.py +733 -0
  99. package/plugins/release-notes/scripts/prompt.py +90 -0
  100. package/plugins/release-notes/scripts/validate_release_notes.py +328 -0
  101. package/plugins/release-notes/workflows/release-notes.yml +76 -0
  102. package/plugins/vulnerability-remediation/.plugin/plugin.json +19 -0
  103. package/plugins/vulnerability-remediation/README.md +217 -0
  104. package/plugins/vulnerability-remediation/action.yml +187 -0
  105. package/plugins/vulnerability-remediation/scripts/scan_and_remediate.py +561 -0
  106. package/plugins/vulnerability-remediation/workflows/vulnerability-scan.yml +87 -0
  107. package/pyproject.toml +12 -0
  108. package/release-please-config.json +16 -0
  109. package/scripts/build-skills-catalog.mjs +100 -0
  110. package/scripts/sync_extensions.py +494 -0
  111. package/scripts/sync_openhands_sdk_skill.py +264 -0
  112. package/skills/README.md +159 -0
  113. package/skills/add-javadoc/.plugin/plugin.json +18 -0
  114. package/skills/add-javadoc/README.md +40 -0
  115. package/skills/add-javadoc/SKILL.md +35 -0
  116. package/skills/add-javadoc/references/example.md +32 -0
  117. package/skills/add-skill/.plugin/plugin.json +18 -0
  118. package/skills/add-skill/README.md +67 -0
  119. package/skills/add-skill/SKILL.md +47 -0
  120. package/skills/add-skill/scripts/fetch_skill.py +259 -0
  121. package/skills/agent-creator/.plugin/plugin.json +20 -0
  122. package/skills/agent-creator/README.md +104 -0
  123. package/skills/agent-creator/SKILL.md +190 -0
  124. package/skills/agent-creator/commands/agent-creator.md +8 -0
  125. package/skills/agent-creator/references/fallback.md +117 -0
  126. package/skills/agent-memory/.plugin/plugin.json +18 -0
  127. package/skills/agent-memory/README.md +35 -0
  128. package/skills/agent-memory/SKILL.md +30 -0
  129. package/skills/agent-memory/commands/remember.md +8 -0
  130. package/skills/agent-sdk-builder/.plugin/plugin.json +18 -0
  131. package/skills/agent-sdk-builder/README.md +40 -0
  132. package/skills/agent-sdk-builder/SKILL.md +37 -0
  133. package/skills/agent-sdk-builder/commands/agent-builder.md +8 -0
  134. package/skills/azure-devops/.plugin/plugin.json +18 -0
  135. package/skills/azure-devops/README.md +55 -0
  136. package/skills/azure-devops/SKILL.md +50 -0
  137. package/skills/bitbucket/.plugin/plugin.json +17 -0
  138. package/skills/bitbucket/README.md +50 -0
  139. package/skills/bitbucket/SKILL.md +45 -0
  140. package/skills/code-review/.plugin/plugin.json +19 -0
  141. package/skills/code-review/README.md +18 -0
  142. package/skills/code-review/SKILL.md +208 -0
  143. package/skills/code-review/commands/codereview-roasted.md +8 -0
  144. package/skills/code-review/commands/codereview.md +8 -0
  145. package/skills/code-review/references/risk-evaluation.md +41 -0
  146. package/skills/code-review/references/supply-chain-security.md +31 -0
  147. package/skills/code-simplifier/.plugin/plugin.json +21 -0
  148. package/skills/code-simplifier/README.md +30 -0
  149. package/skills/code-simplifier/SKILL.md +91 -0
  150. package/skills/code-simplifier/commands/simplify.md +8 -0
  151. package/skills/code-simplifier/references/code-quality-review.md +86 -0
  152. package/skills/code-simplifier/references/code-reuse-review.md +63 -0
  153. package/skills/code-simplifier/references/efficiency-review.md +81 -0
  154. package/skills/datadog/.plugin/plugin.json +19 -0
  155. package/skills/datadog/README.md +100 -0
  156. package/skills/datadog/SKILL.md +95 -0
  157. package/skills/deno/.plugin/plugin.json +18 -0
  158. package/skills/deno/README.md +5 -0
  159. package/skills/deno/SKILL.md +99 -0
  160. package/skills/deno/references/README.md +6 -0
  161. package/skills/discord/.plugin/plugin.json +18 -0
  162. package/skills/discord/README.md +31 -0
  163. package/skills/discord/SKILL.md +109 -0
  164. package/skills/discord/__init__.py +0 -0
  165. package/skills/discord/references/REFERENCE.md +78 -0
  166. package/skills/discord/scripts/__init__.py +0 -0
  167. package/skills/discord/scripts/_http.py +127 -0
  168. package/skills/discord/scripts/post_webhook.py +106 -0
  169. package/skills/discord/scripts/send_message.py +102 -0
  170. package/skills/docker/.plugin/plugin.json +17 -0
  171. package/skills/docker/README.md +34 -0
  172. package/skills/docker/SKILL.md +29 -0
  173. package/skills/evidence-based-citations/.plugin/plugin.json +20 -0
  174. package/skills/evidence-based-citations/README.md +31 -0
  175. package/skills/evidence-based-citations/SKILL.md +59 -0
  176. package/skills/flarglebargle/.plugin/plugin.json +16 -0
  177. package/skills/flarglebargle/README.md +14 -0
  178. package/skills/flarglebargle/SKILL.md +9 -0
  179. package/skills/frontend-design/.plugin/plugin.json +21 -0
  180. package/skills/frontend-design/LICENSE.txt +177 -0
  181. package/skills/frontend-design/README.md +42 -0
  182. package/skills/frontend-design/SKILL.md +42 -0
  183. package/skills/github/.plugin/plugin.json +19 -0
  184. package/skills/github/README.md +42 -0
  185. package/skills/github/SKILL.md +106 -0
  186. package/skills/github-pr-review/.plugin/plugin.json +18 -0
  187. package/skills/github-pr-review/README.md +145 -0
  188. package/skills/github-pr-review/SKILL.md +186 -0
  189. package/skills/github-pr-review/commands/github-pr-review.md +8 -0
  190. package/skills/github-pr-reviewer/.plugin/plugin.json +20 -0
  191. package/skills/github-pr-reviewer/README.md +34 -0
  192. package/skills/github-pr-reviewer/SKILL.md +89 -0
  193. package/skills/github-pr-reviewer/commands/pr-reviewer:setup.md +8 -0
  194. package/skills/github-repo-monitor/.plugin/plugin.json +22 -0
  195. package/skills/github-repo-monitor/README.md +70 -0
  196. package/skills/github-repo-monitor/SKILL.md +316 -0
  197. package/skills/github-repo-monitor/commands/github-monitor:poll.md +8 -0
  198. package/skills/github-repo-monitor/references/github-api.md +241 -0
  199. package/skills/github-repo-monitor/references/state-schema.md +160 -0
  200. package/skills/github-repo-monitor/scripts/main.py +915 -0
  201. package/skills/github-repo-monitor/tests/test_main.py +400 -0
  202. package/skills/gitlab/.plugin/plugin.json +17 -0
  203. package/skills/gitlab/README.md +37 -0
  204. package/skills/gitlab/SKILL.md +32 -0
  205. package/skills/incident-retrospective/.plugin/plugin.json +21 -0
  206. package/skills/incident-retrospective/README.md +34 -0
  207. package/skills/incident-retrospective/SKILL.md +98 -0
  208. package/skills/incident-retrospective/commands/incident-retro:setup.md +8 -0
  209. package/skills/index.d.ts +11 -0
  210. package/skills/index.js +465 -0
  211. package/skills/iterate/.plugin/plugin.json +13 -0
  212. package/skills/iterate/README.md +25 -0
  213. package/skills/iterate/SKILL.md +399 -0
  214. package/skills/iterate/commands/babysit.md +8 -0
  215. package/skills/iterate/commands/iterate.md +8 -0
  216. package/skills/iterate/commands/verify.md +8 -0
  217. package/skills/iterate/references/heuristics.md +58 -0
  218. package/skills/iterate/references/verification.md +96 -0
  219. package/skills/jupyter/.plugin/plugin.json +18 -0
  220. package/skills/jupyter/README.md +55 -0
  221. package/skills/jupyter/SKILL.md +50 -0
  222. package/skills/kubernetes/.plugin/plugin.json +18 -0
  223. package/skills/kubernetes/README.md +53 -0
  224. package/skills/kubernetes/SKILL.md +48 -0
  225. package/skills/learn-from-code-review/.plugin/plugin.json +19 -0
  226. package/skills/learn-from-code-review/README.md +64 -0
  227. package/skills/learn-from-code-review/SKILL.md +186 -0
  228. package/skills/learn-from-code-review/commands/learn-from-reviews.md +8 -0
  229. package/skills/linear/.plugin/plugin.json +19 -0
  230. package/skills/linear/README.md +58 -0
  231. package/skills/linear/SKILL.md +213 -0
  232. package/skills/linear-triage/.plugin/plugin.json +21 -0
  233. package/skills/linear-triage/README.md +34 -0
  234. package/skills/linear-triage/SKILL.md +91 -0
  235. package/skills/linear-triage/commands/linear-triage:setup.md +8 -0
  236. package/skills/notion/.plugin/plugin.json +17 -0
  237. package/skills/notion/README.md +114 -0
  238. package/skills/notion/SKILL.md +109 -0
  239. package/skills/npm/.plugin/plugin.json +17 -0
  240. package/skills/npm/README.md +14 -0
  241. package/skills/npm/SKILL.md +9 -0
  242. package/skills/openhands-api/.plugin/plugin.json +22 -0
  243. package/skills/openhands-api/README.md +48 -0
  244. package/skills/openhands-api/SKILL.md +399 -0
  245. package/skills/openhands-api/references/README.md +33 -0
  246. package/skills/openhands-api/references/TROUBLESHOOTING.md +81 -0
  247. package/skills/openhands-api/references/example_prompt.md +12 -0
  248. package/skills/openhands-api/scripts/openhands_api.py +606 -0
  249. package/skills/openhands-api/scripts/openhands_api.ts +252 -0
  250. package/skills/openhands-automation/.plugin/plugin.json +19 -0
  251. package/skills/openhands-automation/README.md +89 -0
  252. package/skills/openhands-automation/SKILL.md +875 -0
  253. package/skills/openhands-automation/commands/automation:create.md +8 -0
  254. package/skills/openhands-automation/references/ab-testing.md +185 -0
  255. package/skills/openhands-automation/references/custom-automation.md +644 -0
  256. package/skills/openhands-sdk/.plugin/plugin.json +20 -0
  257. package/skills/openhands-sdk/README.md +22 -0
  258. package/skills/openhands-sdk/SKILL.md +230 -0
  259. package/skills/openhands-sdk/commands/sdk.md +8 -0
  260. package/skills/pdflatex/.plugin/plugin.json +18 -0
  261. package/skills/pdflatex/README.md +39 -0
  262. package/skills/pdflatex/SKILL.md +34 -0
  263. package/skills/prd/.plugin/plugin.json +19 -0
  264. package/skills/prd/README.md +28 -0
  265. package/skills/prd/SKILL.md +237 -0
  266. package/skills/prd/commands/prd.md +8 -0
  267. package/skills/qa-changes/README.md +18 -0
  268. package/skills/qa-changes/SKILL.md +229 -0
  269. package/skills/qa-changes/commands/qa-changes.md +8 -0
  270. package/skills/release-notes/README.md +24 -0
  271. package/skills/release-notes/SKILL.md +19 -0
  272. package/skills/release-notes/commands/release-notes.md +8 -0
  273. package/skills/research-brief/.plugin/plugin.json +20 -0
  274. package/skills/research-brief/README.md +34 -0
  275. package/skills/research-brief/SKILL.md +99 -0
  276. package/skills/research-brief/commands/research-brief:setup.md +8 -0
  277. package/skills/security/.plugin/plugin.json +18 -0
  278. package/skills/security/README.md +38 -0
  279. package/skills/security/SKILL.md +33 -0
  280. package/skills/skill-creator/.plugin/plugin.json +17 -0
  281. package/skills/skill-creator/LICENSE.txt +202 -0
  282. package/skills/skill-creator/README.md +182 -0
  283. package/skills/skill-creator/SKILL.md +545 -0
  284. package/skills/skill-creator/references/output-patterns.md +82 -0
  285. package/skills/skill-creator/references/workflows.md +28 -0
  286. package/skills/skill-creator/scripts/init_skill.py +303 -0
  287. package/skills/skill-creator/scripts/quick_validate.py +95 -0
  288. package/skills/slack-channel-monitor/.plugin/plugin.json +21 -0
  289. package/skills/slack-channel-monitor/README.md +91 -0
  290. package/skills/slack-channel-monitor/SKILL.md +276 -0
  291. package/skills/slack-channel-monitor/commands/slack-monitor:poll.md +8 -0
  292. package/skills/slack-channel-monitor/references/slack-api.md +207 -0
  293. package/skills/slack-channel-monitor/references/state-schema.md +180 -0
  294. package/skills/slack-channel-monitor/scripts/main.py +962 -0
  295. package/skills/slack-standup-digest/.plugin/plugin.json +21 -0
  296. package/skills/slack-standup-digest/README.md +34 -0
  297. package/skills/slack-standup-digest/SKILL.md +92 -0
  298. package/skills/slack-standup-digest/commands/standup-digest:setup.md +8 -0
  299. package/skills/spark-version-upgrade/.plugin/plugin.json +20 -0
  300. package/skills/spark-version-upgrade/README.md +54 -0
  301. package/skills/spark-version-upgrade/SKILL.md +233 -0
  302. package/skills/ssh/.plugin/plugin.json +18 -0
  303. package/skills/ssh/README.md +140 -0
  304. package/skills/ssh/SKILL.md +135 -0
  305. package/skills/swift-linux/.plugin/plugin.json +17 -0
  306. package/skills/swift-linux/README.md +86 -0
  307. package/skills/swift-linux/SKILL.md +81 -0
  308. package/skills/theme-factory/.plugin/plugin.json +19 -0
  309. package/skills/theme-factory/LICENSE.txt +202 -0
  310. package/skills/theme-factory/README.md +58 -0
  311. package/skills/theme-factory/SKILL.md +59 -0
  312. package/skills/theme-factory/theme-showcase.pdf +0 -0
  313. package/skills/theme-factory/themes/arctic-frost.md +19 -0
  314. package/skills/theme-factory/themes/botanical-garden.md +19 -0
  315. package/skills/theme-factory/themes/desert-rose.md +19 -0
  316. package/skills/theme-factory/themes/forest-canopy.md +19 -0
  317. package/skills/theme-factory/themes/golden-hour.md +19 -0
  318. package/skills/theme-factory/themes/midnight-galaxy.md +19 -0
  319. package/skills/theme-factory/themes/modern-minimalist.md +19 -0
  320. package/skills/theme-factory/themes/ocean-depths.md +19 -0
  321. package/skills/theme-factory/themes/sunset-boulevard.md +19 -0
  322. package/skills/theme-factory/themes/tech-innovation.md +19 -0
  323. package/skills/uv/.plugin/plugin.json +18 -0
  324. package/skills/uv/README.md +5 -0
  325. package/skills/uv/SKILL.md +95 -0
  326. package/skills/uv/references/README.md +5 -0
  327. package/skills/vercel/.plugin/plugin.json +18 -0
  328. package/skills/vercel/README.md +108 -0
  329. package/skills/vercel/SKILL.md +103 -0
  330. package/tests/test_add_skill_installs_to_agents_dir.py +42 -0
  331. package/tests/test_catalogs.py +109 -0
  332. package/tests/test_code_review_risk_evaluation.py +94 -0
  333. package/tests/test_github_pr_review_skill.py +114 -0
  334. package/tests/test_issue_duplicate_checker.py +240 -0
  335. package/tests/test_openhands_api_python.py +152 -0
  336. package/tests/test_plugin_manifest.py +83 -0
  337. package/tests/test_pr_review_diff_payload.py +202 -0
  338. package/tests/test_pr_review_feedback.py +263 -0
  339. package/tests/test_pr_review_prompt.py +152 -0
  340. package/tests/test_pr_review_review_context.py +253 -0
  341. package/tests/test_qa_changes.py +232 -0
  342. package/tests/test_qa_changes_evaluation.py +259 -0
  343. package/tests/test_release_notes_generator.py +990 -0
  344. package/tests/test_sdk_loading.py +150 -0
  345. package/tests/test_skill_plugin_loading.py +149 -0
  346. package/tests/test_skills_catalog.py +336 -0
  347. package/tests/test_skills_have_readme.py +66 -0
  348. package/tests/test_sync_extensions.py +292 -0
  349. package/tests/test_workflow_sync.py +46 -0
  350. package/utils/analysis/README.md +7 -0
  351. package/utils/analysis/laminar_signals/README.md +211 -0
  352. package/utils/analysis/laminar_signals/analyze.py +780 -0
  353. package/utils/analysis/laminar_signals/templates/default.j2 +49 -0
  354. package/utils/analysis/laminar_signals/templates/pr_review.j2 +61 -0
@@ -0,0 +1,109 @@
1
+ import json
2
+ import re
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+
7
+ ROOT = Path(__file__).resolve().parents[1]
8
+
9
+
10
+ def load_catalog_entries(relative_path: str):
11
+ entries = []
12
+ for entry_path in sorted((ROOT / relative_path).glob("*.json")):
13
+ entry = json.loads(entry_path.read_text())
14
+ assert entry["id"] == entry_path.stem
15
+ entries.append(entry)
16
+ return entries
17
+
18
+
19
+ def test_catalog_ids_are_unique_and_automations_reference_existing_integrations():
20
+ integrations = load_catalog_entries("integrations/catalog")
21
+ automations = load_catalog_entries("automations/catalog")
22
+
23
+ integration_ids = [entry["id"] for entry in integrations]
24
+ automation_ids = [entry["id"] for entry in automations]
25
+
26
+ assert len(integration_ids) == len(set(integration_ids))
27
+ assert len(automation_ids) == len(set(automation_ids))
28
+
29
+ known_integration_ids = set(integration_ids)
30
+ for automation in automations:
31
+ assert automation["requiredIntegrationIds"]
32
+ missing_ids = (
33
+ set(automation["requiredIntegrationIds"]) - known_integration_ids
34
+ )
35
+ assert missing_ids == set()
36
+
37
+
38
+ def test_catalog_entries_have_required_fields():
39
+ for entry in load_catalog_entries("integrations/catalog"):
40
+ assert entry["id"]
41
+ assert entry["name"]
42
+ assert entry["description"]
43
+ assert entry["kind"] in {"mcp", "http"}
44
+ assert entry["iconBg"]
45
+ assert entry["connectionOptions"]
46
+ assert entry["defaultConnectionOptionId"]
47
+ for option in entry["connectionOptions"]:
48
+ assert option["id"]
49
+ assert option["provider"] in {"mcp", "http"}
50
+ assert option["auth"]["strategy"] in {
51
+ "none",
52
+ "api_key",
53
+ "bearer",
54
+ "basic",
55
+ "oauth2",
56
+ }
57
+ if option["provider"] == "mcp":
58
+ assert option["transport"]["kind"] in {"stdio", "shttp", "sse"}
59
+ if option["transport"]["kind"] == "stdio":
60
+ assert option["transport"]["serverName"]
61
+ assert option["transport"]["command"]
62
+ assert isinstance(option["transport"]["args"], list)
63
+ else:
64
+ assert option["transport"]["url"].startswith("https://")
65
+
66
+ for entry in load_catalog_entries("automations/catalog"):
67
+ assert entry["id"]
68
+ assert entry["name"]
69
+ assert entry["prompt"]
70
+ assert entry["exampleImplementation"]
71
+ assert isinstance(entry["popularityRank"], int)
72
+ assert isinstance(entry["estimatedSetupMinutes"], int)
73
+
74
+
75
+ def test_credential_fields_have_helper_text_and_link():
76
+ """All password fields must have helperText plus a link (either a helperLink field or a
77
+ markdown link embedded in helperText) so users know how to get credentials."""
78
+ markdown_link_re = re.compile(r"\[.+?\]\(https://[^)]+\)")
79
+
80
+ for entry in load_catalog_entries("integrations/catalog"):
81
+ for option in entry["connectionOptions"]:
82
+ transport = option.get("transport", {})
83
+ for field_group in ("envFields", "argFields"):
84
+ for field in transport.get(field_group, []):
85
+ if field.get("type") == "password":
86
+ field_key = field.get("key", "<unknown>")
87
+ assert "helperText" in field, (
88
+ f"{entry['id']}: password field '{field_key}' is missing helperText"
89
+ )
90
+ assert field["helperText"], (
91
+ f"{entry['id']}: password field '{field_key}' has empty helperText"
92
+ )
93
+ has_helper_link = field.get("helperLink", "").startswith("https://")
94
+ has_inline_link = bool(markdown_link_re.search(field["helperText"]))
95
+ assert has_helper_link or has_inline_link, (
96
+ f"{entry['id']}: password field '{field_key}' must have a helperLink "
97
+ f"or a markdown link in helperText"
98
+ )
99
+
100
+
101
+ def test_node_package_exports_catalogs():
102
+ script = """
103
+ import { INTEGRATION_CATALOG, AUTOMATION_CATALOG } from './index.js';
104
+ if (!Array.isArray(INTEGRATION_CATALOG) || INTEGRATION_CATALOG.length === 0) process.exit(1);
105
+ if (!Array.isArray(AUTOMATION_CATALOG) || AUTOMATION_CATALOG.length === 0) process.exit(1);
106
+ if (!INTEGRATION_CATALOG.some((entry) => entry.id === 'github')) process.exit(1);
107
+ if (!AUTOMATION_CATALOG.some((entry) => entry.id === 'github-pr-reviewer')) process.exit(1);
108
+ """
109
+ subprocess.run(["node", "--input-type=module", "-e", script], cwd=ROOT, check=True)
@@ -0,0 +1,94 @@
1
+ """Test that risk/safety evaluation is integrated into the unified code review skill."""
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ def get_repo_root():
7
+ return Path(__file__).parent.parent
8
+
9
+
10
+ def get_code_review_skill():
11
+ return (get_repo_root() / "skills" / "code-review" / "SKILL.md").read_text()
12
+
13
+
14
+ def get_risk_evaluation_reference():
15
+ return (
16
+ get_repo_root()
17
+ / "skills"
18
+ / "code-review"
19
+ / "references"
20
+ / "risk-evaluation.md"
21
+ ).read_text()
22
+
23
+
24
+ class TestRiskEvaluationReference:
25
+ """Verify the risk-evaluation.md reference file is complete."""
26
+
27
+ def test_reference_file_exists(self):
28
+ path = (
29
+ get_repo_root()
30
+ / "skills"
31
+ / "code-review"
32
+ / "references"
33
+ / "risk-evaluation.md"
34
+ )
35
+ assert path.exists()
36
+
37
+ def test_has_all_risk_levels(self):
38
+ content = get_risk_evaluation_reference()
39
+ assert "Low Risk" in content
40
+ assert "Medium Risk" in content
41
+ assert "High Risk" in content
42
+
43
+ def test_has_risk_factors(self):
44
+ content = get_risk_evaluation_reference()
45
+ factors = [
46
+ "Pattern conformance",
47
+ "Security sensitivity",
48
+ "Infrastructure dependencies",
49
+ "Blast radius",
50
+ "Core system impact",
51
+ ]
52
+ for factor in factors:
53
+ assert factor in content, f"Missing risk factor: {factor}"
54
+
55
+ def test_has_high_risk_escalation_guidance(self):
56
+ content = get_risk_evaluation_reference()
57
+ assert "not auto-merg" in content.lower()
58
+ assert "human" in content.lower()
59
+
60
+ def test_has_repo_specific_risk_rules(self):
61
+ content = get_risk_evaluation_reference()
62
+ assert "AGENTS.md" in content
63
+ assert "repo-specific" in content.lower() or "Repo-specific" in content
64
+
65
+ def test_has_risk_assessment_output_format(self):
66
+ content = get_risk_evaluation_reference()
67
+ assert "Risk Assessment" in content
68
+
69
+
70
+ class TestCodeReviewSkillReferencesRisk:
71
+ """Verify the unified code-review skill references the risk evaluation."""
72
+
73
+ def test_has_risk_evaluation_scenario(self):
74
+ content = get_code_review_skill()
75
+ assert "Risk and Safety Evaluation" in content
76
+
77
+ def test_references_risk_evaluation_file(self):
78
+ content = get_code_review_skill()
79
+ assert "references/risk-evaluation.md" in content
80
+
81
+ def test_risk_section_appears_after_dependency_section(self):
82
+ content = get_code_review_skill()
83
+ dep_pos = content.index("8. **Dependency Changes**")
84
+ risk_pos = content.index("9. **Risk and Safety Evaluation**")
85
+ assert risk_pos > dep_pos
86
+
87
+ def test_always_include_risk_instruction(self):
88
+ content = get_code_review_skill()
89
+ assert "Always include the" in content
90
+ assert "Risk and Safety Evaluation" in content
91
+
92
+ def test_has_risk_assessment_in_output_format(self):
93
+ content = get_code_review_skill()
94
+ assert "RISK ASSESSMENT" in content
@@ -0,0 +1,114 @@
1
+ """Tests for the github-pr-review skill content.
2
+
3
+ These tests guard the guidance the agent reads when it builds a GitHub review
4
+ with inline suggestions. The skill must teach the agent how GitHub
5
+ ``suggestion`` blocks actually behave (range replacement), so accepted
6
+ suggestions don't silently duplicate or delete lines, and so the prose
7
+ description always matches the resulting change.
8
+
9
+ See: https://github.com/OpenHands/extensions/issues/292
10
+ """
11
+
12
+ from pathlib import Path
13
+
14
+ import pytest
15
+
16
+ SKILL_PATH = (
17
+ Path(__file__).parent.parent / "skills" / "github-pr-review" / "SKILL.md"
18
+ )
19
+
20
+
21
+ @pytest.fixture(scope="module")
22
+ def skill_text() -> str:
23
+ return SKILL_PATH.read_text(encoding="utf-8")
24
+
25
+
26
+ def test_skill_file_exists():
27
+ assert SKILL_PATH.is_file(), f"missing skill file: {SKILL_PATH}"
28
+
29
+
30
+ def test_skill_does_not_claim_suggestion_must_match_range_length(skill_text: str):
31
+ """Regression: the old "same number of lines as the range" rule was wrong.
32
+
33
+ GitHub replaces the targeted range with the suggestion body regardless of
34
+ how many lines the body contains. Telling the agent the counts must match
35
+ pushed it to either delete lines (when shrinking a block) or duplicate
36
+ lines (when expanding one), depending on which side it adjusted to satisfy
37
+ the bogus rule. The skill must no longer state that constraint.
38
+ """
39
+ forbidden_phrases = [
40
+ "suggestion must have the same number of lines as the range",
41
+ "must have the same number of lines as the range",
42
+ ]
43
+ for phrase in forbidden_phrases:
44
+ assert phrase not in skill_text, (
45
+ f"SKILL.md still contains the misleading rule {phrase!r}; "
46
+ "GitHub suggestions replace the range with any number of lines."
47
+ )
48
+
49
+
50
+ def test_skill_explains_replace_semantics(skill_text: str):
51
+ """The skill must say in plain language that suggestions REPLACE the range."""
52
+ assert "How Suggestions Actually Work" in skill_text
53
+ # Mention replacement explicitly so the agent does not treat the suggestion
54
+ # body as "extra lines added on top of the original".
55
+ lowered = skill_text.lower()
56
+ assert "replace" in lowered
57
+ assert "any number of lines" in lowered
58
+
59
+
60
+ def test_skill_covers_add_remove_change_patterns(skill_text: str):
61
+ """The skill should give explicit recipes for the three intents that
62
+ most commonly produced broken suggestions: changing, adding, and
63
+ deleting lines. Without these, the agent invents wrong patterns.
64
+ """
65
+ for keyword in ("Change line", "Add", "Delete"):
66
+ assert keyword in skill_text, (
67
+ f"SKILL.md is missing guidance for intent {keyword!r}; "
68
+ "see issue #292."
69
+ )
70
+
71
+
72
+ def test_skill_warns_about_duplicated_and_deleted_lines(skill_text: str):
73
+ """The skill must call out the two failure modes from issue #292:
74
+ accepted suggestions that duplicate or remove lines."""
75
+ lowered = skill_text.lower()
76
+ assert "duplicat" in lowered, (
77
+ "SKILL.md should warn about suggestions that duplicate lines."
78
+ )
79
+ # "disappear"/"disappearing" is unique to the failure-mode description in
80
+ # the "Common Mistakes" section. The generic terms "delete" and "remove"
81
+ # also appear in the recipe table for intentional deletions, so checking
82
+ # for those would let the failure-mode guidance be removed silently.
83
+ assert "disappear" in lowered, (
84
+ "SKILL.md should warn about suggestions that silently drop lines "
85
+ "(see 'Disappearing lines' under Common Mistakes)."
86
+ )
87
+
88
+
89
+ def test_skill_requires_description_to_match_suggestion(skill_text: str):
90
+ """The skill must explicitly require the prose description to match the
91
+ actual code change in the suggestion block (the third bug from #292)."""
92
+ lowered = skill_text.lower()
93
+ assert any(phrase in lowered for phrase in ("description does not match", "prose description", "description must match"))
94
+ # The text should connect the description to the resulting code change.
95
+ assert (
96
+ "match" in lowered
97
+ ), "SKILL.md should say the description must match the change."
98
+
99
+
100
+ def test_skill_has_pre_post_verification_step(skill_text: str):
101
+ """Before posting, the agent must verify the suggestion against the file."""
102
+ assert "Mandatory Verification" in skill_text or "Verification" in skill_text
103
+ # Should point the agent at the real file content, not just the diff.
104
+ assert "sed -n" in skill_text or "read the actual file" in skill_text.lower()
105
+
106
+
107
+ def test_skill_offers_prose_fallback(skill_text: str):
108
+ """When unsure, the agent should drop the suggestion block and use prose
109
+ instead — a correct comment beats a broken one-click fix."""
110
+ lowered = skill_text.lower()
111
+ assert "prose" in lowered, (
112
+ "SKILL.md should tell the agent to fall back to a prose comment "
113
+ "when the suggestion cannot be verified."
114
+ )
@@ -0,0 +1,240 @@
1
+ import importlib.util
2
+ import io
3
+ import urllib.error
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+
9
+ ROOT = Path(__file__).resolve().parents[1]
10
+ PLUGIN_DIR = ROOT / "plugins" / "issue-duplicate-checker" / "scripts"
11
+
12
+
13
+ def load_script(name: str):
14
+ path = PLUGIN_DIR / f"{name}.py"
15
+ spec = importlib.util.spec_from_file_location(name, path)
16
+ module = importlib.util.module_from_spec(spec)
17
+ assert spec.loader is not None
18
+ spec.loader.exec_module(module)
19
+ return module
20
+
21
+
22
+ def test_action_shell_blocks_do_not_interpolate_expressions_directly():
23
+ action_path = ROOT / "plugins" / "issue-duplicate-checker" / "action.yml"
24
+ lines = action_path.read_text().splitlines()
25
+ in_block = False
26
+ block_indent = 0
27
+ for line_number, line in enumerate(lines, start=1):
28
+ stripped = line.lstrip()
29
+ indent = len(line) - len(stripped)
30
+ if in_block and stripped and indent <= block_indent:
31
+ in_block = False
32
+ if stripped.startswith(("run:", "script:")):
33
+ block_scalar = stripped.split(":", 1)[1].strip()
34
+ if block_scalar.startswith(("|", ">")):
35
+ in_block = True
36
+ block_indent = indent
37
+ continue
38
+ assert not (in_block and "${{" in line), (
39
+ f"Move GitHub expression on line {line_number} into env before using it"
40
+ )
41
+
42
+
43
+ def test_normalize_result_preserves_model_should_comment_false():
44
+ script = load_script("issue_duplicate_check_openhands")
45
+
46
+ result = script.normalize_result(
47
+ {
48
+ "should_comment": False,
49
+ "is_duplicate": True,
50
+ "auto_close_candidate": True,
51
+ "classification": "duplicate",
52
+ "confidence": "high",
53
+ "canonical_issue_number": 123,
54
+ "candidate_issues": [{"number": 123}],
55
+ }
56
+ )
57
+
58
+ assert result["should_comment"] is False
59
+ assert result["auto_close_candidate"] is False
60
+
61
+
62
+ def test_github_headers_require_token(monkeypatch):
63
+ script = load_script("issue_duplicate_check_openhands")
64
+ monkeypatch.delenv("GITHUB_TOKEN", raising=False)
65
+
66
+ with pytest.raises(
67
+ RuntimeError,
68
+ match="GITHUB_TOKEN environment variable is required",
69
+ ):
70
+ script.github_headers()
71
+
72
+ monkeypatch.setenv("GITHUB_TOKEN", "token")
73
+ headers = script.github_headers()
74
+ assert headers["Authorization"] == "Bearer token"
75
+
76
+
77
+ def test_issue_check_request_json_raises_structured_http_error(monkeypatch):
78
+ script = load_script("issue_duplicate_check_openhands")
79
+
80
+ def fake_urlopen(*args, **kwargs):
81
+ raise urllib.error.HTTPError(
82
+ url="https://api.example.test/example",
83
+ code=403,
84
+ msg="Forbidden",
85
+ hdrs=None,
86
+ fp=io.BytesIO(b'{"message":"rate limited"}'),
87
+ )
88
+
89
+ monkeypatch.setattr(script.urllib.request, "urlopen", fake_urlopen)
90
+
91
+ with pytest.raises(script.HTTPError) as exc_info:
92
+ script.request_json("https://api.example.test", "/example")
93
+
94
+ assert exc_info.value.status_code == 403
95
+ assert exc_info.value.url == "https://api.example.test/example"
96
+
97
+
98
+ def test_parse_agent_json_extracts_fenced_json():
99
+ script = load_script("issue_duplicate_check_openhands")
100
+
101
+ result = script.parse_agent_json(
102
+ 'Here is the result:\n```json\n'
103
+ '{"classification":"duplicate","confidence":"high"}\n```'
104
+ )
105
+
106
+ assert result == {"classification": "duplicate", "confidence": "high"}
107
+
108
+
109
+ def test_normalize_result_guards_auto_close_and_infers_canonical_issue():
110
+ script = load_script("issue_duplicate_check_openhands")
111
+
112
+ overlapping = script.normalize_result(
113
+ {
114
+ "should_comment": True,
115
+ "is_duplicate": False,
116
+ "auto_close_candidate": True,
117
+ "classification": "overlapping-scope",
118
+ "confidence": "high",
119
+ "candidate_issues": [{"number": 10}],
120
+ }
121
+ )
122
+ assert overlapping["should_comment"] is True
123
+ assert overlapping["auto_close_candidate"] is False
124
+
125
+ low_confidence = script.normalize_result(
126
+ {
127
+ "should_comment": True,
128
+ "is_duplicate": True,
129
+ "auto_close_candidate": True,
130
+ "classification": "duplicate",
131
+ "confidence": "low",
132
+ "candidate_issues": [{"number": 11}],
133
+ }
134
+ )
135
+ assert low_confidence["should_comment"] is False
136
+ assert low_confidence["auto_close_candidate"] is False
137
+
138
+ inferred = script.normalize_result(
139
+ {
140
+ "should_comment": True,
141
+ "is_duplicate": True,
142
+ "auto_close_candidate": True,
143
+ "classification": "duplicate",
144
+ "confidence": "high",
145
+ "canonical_issue_number": None,
146
+ "candidate_issues": [{"number": "12"}],
147
+ }
148
+ )
149
+ assert inferred["auto_close_candidate"] is True
150
+ assert inferred["canonical_issue_number"] == 12
151
+
152
+
153
+ def test_build_prompt_includes_required_schema_keys():
154
+ script = load_script("issue_duplicate_check_openhands")
155
+
156
+ prompt = script.build_prompt(
157
+ "OpenHands/extensions",
158
+ {
159
+ "number": 123,
160
+ "title": "Duplicate bug",
161
+ "body": "Looks related",
162
+ "html_url": "https://github.com/OpenHands/extensions/issues/123",
163
+ },
164
+ )
165
+
166
+ for key in [
167
+ "should_comment",
168
+ "is_duplicate",
169
+ "auto_close_candidate",
170
+ "classification",
171
+ "confidence",
172
+ "canonical_issue_number",
173
+ "candidate_issues",
174
+ ]:
175
+ assert key in prompt
176
+
177
+
178
+ def test_find_latest_auto_close_comment_uses_newest_auto_close_marker():
179
+ script = load_script("auto_close_duplicate_issues")
180
+
181
+ latest, canonical = script.find_latest_auto_close_comment(
182
+ [
183
+ {
184
+ "id": 1,
185
+ "created_at": "2026-01-01T00:00:00Z",
186
+ "body": "<!-- openhands-duplicate-check canonical=10 auto-close=true -->",
187
+ },
188
+ {
189
+ "id": 2,
190
+ "created_at": "2026-01-03T00:00:00Z",
191
+ "body": "<!-- openhands-duplicate-check canonical=20 auto-close=false -->",
192
+ },
193
+ {
194
+ "id": 3,
195
+ "created_at": None,
196
+ "body": "<!-- openhands-duplicate-check canonical=30 auto-close=true -->",
197
+ },
198
+ {
199
+ "id": 4,
200
+ "created_at": "2026-01-02T00:00:00Z",
201
+ "body": "<!-- openhands-duplicate-check canonical=40 auto-close=true -->",
202
+ },
203
+ ]
204
+ )
205
+
206
+ assert latest["id"] == 4
207
+ assert canonical == 40
208
+
209
+
210
+ def test_request_json_raises_structured_http_error(monkeypatch):
211
+ script = load_script("auto_close_duplicate_issues")
212
+ monkeypatch.setenv("GITHUB_TOKEN", "token")
213
+
214
+ def fake_urlopen(*args, **kwargs):
215
+ raise urllib.error.HTTPError(
216
+ url="https://api.github.com/example",
217
+ code=404,
218
+ msg="Not Found",
219
+ hdrs=None,
220
+ fp=io.BytesIO(b'{"message":"missing"}'),
221
+ )
222
+
223
+ monkeypatch.setattr(script.urllib.request, "urlopen", fake_urlopen)
224
+
225
+ with pytest.raises(script.HTTPError) as exc_info:
226
+ script.request_json("/example")
227
+
228
+ assert exc_info.value.status_code == 404
229
+ assert exc_info.value.path == "/example"
230
+
231
+
232
+ def test_fetch_issue_returns_none_on_404(monkeypatch):
233
+ script = load_script("auto_close_duplicate_issues")
234
+
235
+ def fake_request_json(path):
236
+ raise script.HTTPError("GET", path, 404, "missing")
237
+
238
+ monkeypatch.setattr(script, "request_json", fake_request_json)
239
+
240
+ assert script.fetch_issue("OpenHands/extensions", 123) is None