@openhands/extensions 0.0.1-alpha → 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.
- package/.agents/skills/custom-codereview-guide.md +25 -0
- package/.github/pull_request_template.md +38 -0
- package/.github/release.yml +14 -0
- package/.github/workflows/check-extensions.yml +72 -0
- package/.github/workflows/npm-publish.yml +89 -0
- package/.github/workflows/pr.yml +30 -0
- package/.github/workflows/release.yml +24 -0
- package/.github/workflows/tests.yml +25 -0
- package/.github/workflows/vulnerability-scan.yml +87 -0
- package/.release-please-manifest.json +3 -0
- package/AGENTS.md +132 -0
- package/README.md +10 -0
- package/analysis_results.md +162 -0
- package/marketplaces/large-codebase.json +66 -0
- package/marketplaces/openhands-extensions.json +682 -0
- package/package.json +4 -10
- package/plugins/README.md +30 -0
- package/plugins/city-weather/.plugin/plugin.json +13 -0
- package/plugins/city-weather/README.md +145 -0
- package/plugins/city-weather/commands/now.md +56 -0
- package/plugins/cobol-modernization/.plugin/plugin.json +19 -0
- package/plugins/cobol-modernization/README.md +201 -0
- package/plugins/cobol-modernization/references/troubleshooting.md +18 -0
- package/plugins/cobol-modernization/skills/build-setup/SKILL.md +78 -0
- package/plugins/cobol-modernization/skills/build-setup/scripts/install-gnucobol.sh +32 -0
- package/plugins/cobol-modernization/skills/cobol-modernization-overview/SKILL.md +113 -0
- package/plugins/cobol-modernization/skills/mainfraime-removal/SKILL.md +62 -0
- package/plugins/cobol-modernization/skills/mainfraime-removal/references/cics-transformation-examples.md +45 -0
- package/plugins/cobol-modernization/skills/mainframe-planning/SKILL.md +78 -0
- package/plugins/cobol-modernization/skills/to-java-migration/SKILL.md +59 -0
- package/plugins/cobol-modernization/skills/to-java-migration/references/cobol-to-java-example.md +58 -0
- package/plugins/cobol-modernization/skills/to-java-migration/references/datatype-mappings.md +19 -0
- package/plugins/issue-duplicate-checker/.plugin/plugin.json +13 -0
- package/plugins/issue-duplicate-checker/README.md +51 -0
- package/plugins/issue-duplicate-checker/action.yml +349 -0
- package/plugins/issue-duplicate-checker/scripts/auto_close_duplicate_issues.py +569 -0
- package/plugins/issue-duplicate-checker/scripts/issue_duplicate_check_openhands.py +681 -0
- package/plugins/issue-duplicate-checker/scripts/post_duplicate_notice.js +220 -0
- package/plugins/issue-duplicate-checker/scripts/remove_duplicate_candidate_label.js +27 -0
- package/plugins/magic-test/.plugin/plugin.json +13 -0
- package/plugins/magic-test/skills/magic-word/SKILL.md +33 -0
- package/plugins/migration-scoring/.plugin/plugin.json +19 -0
- package/plugins/migration-scoring/README.md +244 -0
- package/plugins/migration-scoring/skills/migration-mapping/SKILL.md +72 -0
- package/plugins/migration-scoring/skills/migration-report/SKILL.md +118 -0
- package/plugins/migration-scoring/skills/migration-scoring-overview/SKILL.md +126 -0
- package/plugins/migration-scoring/skills/score-quality/SKILL.md +54 -0
- package/plugins/migration-scoring/skills/score-quality/references/scoring-criteria.md +30 -0
- package/plugins/migration-scoring/skills/score-style/SKILL.md +106 -0
- package/plugins/onboarding/.plugin/plugin.json +20 -0
- package/plugins/onboarding/README.md +30 -0
- package/plugins/onboarding/references/criteria.md +144 -0
- package/plugins/onboarding/skills/agent-readiness-report/README.md +23 -0
- package/plugins/onboarding/skills/agent-readiness-report/SKILL.md +122 -0
- package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_agent_instructions.sh +88 -0
- package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_build_env.sh +114 -0
- package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_feedback_loops.sh +133 -0
- package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_policy.sh +113 -0
- package/plugins/onboarding/skills/agent-readiness-report/scripts/scan_workflows.sh +127 -0
- package/plugins/onboarding/skills/improve-agent-readiness/README.md +19 -0
- package/plugins/onboarding/skills/improve-agent-readiness/SKILL.md +167 -0
- package/plugins/onboarding/skills/setup-agents-md/README.md +15 -0
- package/plugins/onboarding/skills/setup-agents-md/SKILL.md +150 -0
- package/plugins/onboarding/skills/setup-openhands/README.md +20 -0
- package/plugins/onboarding/skills/setup-openhands/SKILL.md +56 -0
- package/plugins/onboarding/skills/setup-pr-review/README.md +23 -0
- package/plugins/onboarding/skills/setup-pr-review/SKILL.md +72 -0
- package/plugins/openhands/.plugin/plugin.json +13 -0
- package/plugins/openhands/README.md +52 -0
- package/plugins/openhands/SKILL.md +61 -0
- package/plugins/openhands/commands/create.md +55 -0
- package/plugins/openhands/commands/openhands-cloud.md +8 -0
- package/plugins/openhands/scripts/run.sh +69 -0
- package/plugins/pr-review/.plugin/plugin.json +13 -0
- package/plugins/pr-review/README.md +393 -0
- package/plugins/pr-review/action.yml +298 -0
- package/plugins/pr-review/scripts/agent_script.py +1282 -0
- package/plugins/pr-review/scripts/evaluate_review.py +655 -0
- package/plugins/pr-review/scripts/prompt.py +260 -0
- package/plugins/pr-review/workflows/pr-review-by-openhands.yml +51 -0
- package/plugins/pr-review/workflows/pr-review-evaluation.yml +85 -0
- package/plugins/qa-changes/.plugin/plugin.json +11 -0
- package/plugins/qa-changes/README.md +185 -0
- package/plugins/qa-changes/action.yml +181 -0
- package/plugins/qa-changes/scripts/agent_script.py +406 -0
- package/plugins/qa-changes/scripts/evaluate_qa_changes.py +385 -0
- package/plugins/qa-changes/scripts/prompt.py +174 -0
- package/plugins/qa-changes/workflows/qa-changes-by-openhands.yml +50 -0
- package/plugins/qa-changes/workflows/qa-changes-evaluation.yml +85 -0
- package/plugins/release-notes/.plugin/plugin.json +19 -0
- package/plugins/release-notes/README.md +283 -0
- package/plugins/release-notes/SKILL.md +83 -0
- package/plugins/release-notes/action.yml +117 -0
- package/plugins/release-notes/commands/release-notes.md +8 -0
- package/plugins/release-notes/scripts/agent_script.py +292 -0
- package/plugins/release-notes/scripts/generate_release_notes.py +733 -0
- package/plugins/release-notes/scripts/prompt.py +90 -0
- package/plugins/release-notes/scripts/validate_release_notes.py +328 -0
- package/plugins/release-notes/workflows/release-notes.yml +76 -0
- package/plugins/vulnerability-remediation/.plugin/plugin.json +19 -0
- package/plugins/vulnerability-remediation/README.md +217 -0
- package/plugins/vulnerability-remediation/action.yml +187 -0
- package/plugins/vulnerability-remediation/scripts/scan_and_remediate.py +561 -0
- package/plugins/vulnerability-remediation/workflows/vulnerability-scan.yml +87 -0
- package/pyproject.toml +12 -0
- package/release-please-config.json +16 -0
- package/scripts/sync_extensions.py +494 -0
- package/scripts/sync_openhands_sdk_skill.py +264 -0
- package/skills/README.md +159 -0
- package/skills/add-javadoc/.plugin/plugin.json +18 -0
- package/skills/add-javadoc/README.md +40 -0
- package/skills/add-javadoc/SKILL.md +35 -0
- package/skills/add-javadoc/references/example.md +32 -0
- package/skills/add-skill/.plugin/plugin.json +18 -0
- package/skills/add-skill/README.md +67 -0
- package/skills/add-skill/SKILL.md +47 -0
- package/skills/add-skill/scripts/fetch_skill.py +259 -0
- package/skills/agent-creator/.plugin/plugin.json +20 -0
- package/skills/agent-creator/README.md +104 -0
- package/skills/agent-creator/SKILL.md +190 -0
- package/skills/agent-creator/commands/agent-creator.md +8 -0
- package/skills/agent-creator/references/fallback.md +117 -0
- package/skills/agent-memory/.plugin/plugin.json +18 -0
- package/skills/agent-memory/README.md +35 -0
- package/skills/agent-memory/SKILL.md +30 -0
- package/skills/agent-memory/commands/remember.md +8 -0
- package/skills/agent-sdk-builder/.plugin/plugin.json +18 -0
- package/skills/agent-sdk-builder/README.md +40 -0
- package/skills/agent-sdk-builder/SKILL.md +37 -0
- package/skills/agent-sdk-builder/commands/agent-builder.md +8 -0
- package/skills/azure-devops/.plugin/plugin.json +18 -0
- package/skills/azure-devops/README.md +55 -0
- package/skills/azure-devops/SKILL.md +50 -0
- package/skills/bitbucket/.plugin/plugin.json +17 -0
- package/skills/bitbucket/README.md +50 -0
- package/skills/bitbucket/SKILL.md +45 -0
- package/skills/code-review/.plugin/plugin.json +19 -0
- package/skills/code-review/README.md +18 -0
- package/skills/code-review/SKILL.md +208 -0
- package/skills/code-review/commands/codereview-roasted.md +8 -0
- package/skills/code-review/commands/codereview.md +8 -0
- package/skills/code-review/references/risk-evaluation.md +41 -0
- package/skills/code-review/references/supply-chain-security.md +31 -0
- package/skills/code-simplifier/.plugin/plugin.json +21 -0
- package/skills/code-simplifier/README.md +30 -0
- package/skills/code-simplifier/SKILL.md +91 -0
- package/skills/code-simplifier/commands/simplify.md +8 -0
- package/skills/code-simplifier/references/code-quality-review.md +86 -0
- package/skills/code-simplifier/references/code-reuse-review.md +63 -0
- package/skills/code-simplifier/references/efficiency-review.md +81 -0
- package/skills/datadog/.plugin/plugin.json +19 -0
- package/skills/datadog/README.md +100 -0
- package/skills/datadog/SKILL.md +95 -0
- package/skills/deno/.plugin/plugin.json +18 -0
- package/skills/deno/README.md +5 -0
- package/skills/deno/SKILL.md +99 -0
- package/skills/deno/references/README.md +6 -0
- package/skills/discord/.plugin/plugin.json +18 -0
- package/skills/discord/README.md +31 -0
- package/skills/discord/SKILL.md +109 -0
- package/skills/discord/__init__.py +0 -0
- package/skills/discord/references/REFERENCE.md +78 -0
- package/skills/discord/scripts/__init__.py +0 -0
- package/skills/discord/scripts/_http.py +127 -0
- package/skills/discord/scripts/post_webhook.py +106 -0
- package/skills/discord/scripts/send_message.py +102 -0
- package/skills/docker/.plugin/plugin.json +17 -0
- package/skills/docker/README.md +34 -0
- package/skills/docker/SKILL.md +29 -0
- package/skills/evidence-based-citations/.plugin/plugin.json +20 -0
- package/skills/evidence-based-citations/README.md +31 -0
- package/skills/evidence-based-citations/SKILL.md +59 -0
- package/skills/flarglebargle/.plugin/plugin.json +16 -0
- package/skills/flarglebargle/README.md +14 -0
- package/skills/flarglebargle/SKILL.md +9 -0
- package/skills/frontend-design/.plugin/plugin.json +21 -0
- package/skills/frontend-design/LICENSE.txt +177 -0
- package/skills/frontend-design/README.md +42 -0
- package/skills/frontend-design/SKILL.md +42 -0
- package/skills/github/.plugin/plugin.json +19 -0
- package/skills/github/README.md +42 -0
- package/skills/github/SKILL.md +106 -0
- package/skills/github-pr-review/.plugin/plugin.json +18 -0
- package/skills/github-pr-review/README.md +145 -0
- package/skills/github-pr-review/SKILL.md +148 -0
- package/skills/github-pr-review/commands/github-pr-review.md +8 -0
- package/skills/github-pr-reviewer/.plugin/plugin.json +20 -0
- package/skills/github-pr-reviewer/README.md +34 -0
- package/skills/github-pr-reviewer/SKILL.md +89 -0
- package/skills/github-pr-reviewer/commands/pr-reviewer:setup.md +8 -0
- package/skills/github-repo-monitor/.plugin/plugin.json +22 -0
- package/skills/github-repo-monitor/README.md +70 -0
- package/skills/github-repo-monitor/SKILL.md +316 -0
- package/skills/github-repo-monitor/commands/github-monitor:poll.md +8 -0
- package/skills/github-repo-monitor/references/github-api.md +241 -0
- package/skills/github-repo-monitor/references/state-schema.md +160 -0
- package/skills/github-repo-monitor/scripts/main.py +915 -0
- package/skills/github-repo-monitor/tests/test_main.py +400 -0
- package/skills/gitlab/.plugin/plugin.json +17 -0
- package/skills/gitlab/README.md +37 -0
- package/skills/gitlab/SKILL.md +32 -0
- package/skills/incident-retrospective/.plugin/plugin.json +21 -0
- package/skills/incident-retrospective/README.md +34 -0
- package/skills/incident-retrospective/SKILL.md +98 -0
- package/skills/incident-retrospective/commands/incident-retro:setup.md +8 -0
- package/skills/iterate/.plugin/plugin.json +13 -0
- package/skills/iterate/README.md +25 -0
- package/skills/iterate/SKILL.md +399 -0
- package/skills/iterate/commands/babysit.md +8 -0
- package/skills/iterate/commands/iterate.md +8 -0
- package/skills/iterate/commands/verify.md +8 -0
- package/skills/iterate/references/heuristics.md +58 -0
- package/skills/iterate/references/verification.md +96 -0
- package/skills/jupyter/.plugin/plugin.json +18 -0
- package/skills/jupyter/README.md +55 -0
- package/skills/jupyter/SKILL.md +50 -0
- package/skills/kubernetes/.plugin/plugin.json +18 -0
- package/skills/kubernetes/README.md +53 -0
- package/skills/kubernetes/SKILL.md +48 -0
- package/skills/learn-from-code-review/.plugin/plugin.json +19 -0
- package/skills/learn-from-code-review/README.md +64 -0
- package/skills/learn-from-code-review/SKILL.md +186 -0
- package/skills/learn-from-code-review/commands/learn-from-reviews.md +8 -0
- package/skills/linear/.plugin/plugin.json +19 -0
- package/skills/linear/README.md +58 -0
- package/skills/linear/SKILL.md +213 -0
- package/skills/linear-triage/.plugin/plugin.json +21 -0
- package/skills/linear-triage/README.md +34 -0
- package/skills/linear-triage/SKILL.md +91 -0
- package/skills/linear-triage/commands/linear-triage:setup.md +8 -0
- package/skills/notion/.plugin/plugin.json +17 -0
- package/skills/notion/README.md +114 -0
- package/skills/notion/SKILL.md +109 -0
- package/skills/npm/.plugin/plugin.json +17 -0
- package/skills/npm/README.md +14 -0
- package/skills/npm/SKILL.md +9 -0
- package/skills/openhands-api/.plugin/plugin.json +22 -0
- package/skills/openhands-api/README.md +48 -0
- package/skills/openhands-api/SKILL.md +399 -0
- package/skills/openhands-api/references/README.md +33 -0
- package/skills/openhands-api/references/TROUBLESHOOTING.md +81 -0
- package/skills/openhands-api/references/example_prompt.md +12 -0
- package/skills/openhands-api/scripts/openhands_api.py +606 -0
- package/skills/openhands-api/scripts/openhands_api.ts +252 -0
- package/skills/openhands-automation/.plugin/plugin.json +19 -0
- package/skills/openhands-automation/README.md +89 -0
- package/skills/openhands-automation/SKILL.md +875 -0
- package/skills/openhands-automation/commands/automation:create.md +8 -0
- package/skills/openhands-automation/references/ab-testing.md +185 -0
- package/skills/openhands-automation/references/custom-automation.md +644 -0
- package/skills/openhands-sdk/.plugin/plugin.json +20 -0
- package/skills/openhands-sdk/README.md +22 -0
- package/skills/openhands-sdk/SKILL.md +229 -0
- package/skills/openhands-sdk/commands/sdk.md +8 -0
- package/skills/pdflatex/.plugin/plugin.json +18 -0
- package/skills/pdflatex/README.md +39 -0
- package/skills/pdflatex/SKILL.md +34 -0
- package/skills/prd/.plugin/plugin.json +19 -0
- package/skills/prd/README.md +28 -0
- package/skills/prd/SKILL.md +237 -0
- package/skills/prd/commands/prd.md +8 -0
- package/skills/qa-changes/README.md +18 -0
- package/skills/qa-changes/SKILL.md +229 -0
- package/skills/qa-changes/commands/qa-changes.md +8 -0
- package/skills/release-notes/README.md +24 -0
- package/skills/release-notes/SKILL.md +19 -0
- package/skills/release-notes/commands/release-notes.md +8 -0
- package/skills/research-brief/.plugin/plugin.json +20 -0
- package/skills/research-brief/README.md +34 -0
- package/skills/research-brief/SKILL.md +99 -0
- package/skills/research-brief/commands/research-brief:setup.md +8 -0
- package/skills/security/.plugin/plugin.json +18 -0
- package/skills/security/README.md +38 -0
- package/skills/security/SKILL.md +33 -0
- package/skills/skill-creator/.plugin/plugin.json +17 -0
- package/skills/skill-creator/LICENSE.txt +202 -0
- package/skills/skill-creator/README.md +182 -0
- package/skills/skill-creator/SKILL.md +545 -0
- package/skills/skill-creator/references/output-patterns.md +82 -0
- package/skills/skill-creator/references/workflows.md +28 -0
- package/skills/skill-creator/scripts/init_skill.py +303 -0
- package/skills/skill-creator/scripts/quick_validate.py +95 -0
- package/skills/slack-channel-monitor/.plugin/plugin.json +21 -0
- package/skills/slack-channel-monitor/README.md +91 -0
- package/skills/slack-channel-monitor/SKILL.md +276 -0
- package/skills/slack-channel-monitor/commands/slack-monitor:poll.md +8 -0
- package/skills/slack-channel-monitor/references/slack-api.md +207 -0
- package/skills/slack-channel-monitor/references/state-schema.md +180 -0
- package/skills/slack-channel-monitor/scripts/main.py +962 -0
- package/skills/slack-standup-digest/.plugin/plugin.json +21 -0
- package/skills/slack-standup-digest/README.md +34 -0
- package/skills/slack-standup-digest/SKILL.md +92 -0
- package/skills/slack-standup-digest/commands/standup-digest:setup.md +8 -0
- package/skills/spark-version-upgrade/.plugin/plugin.json +20 -0
- package/skills/spark-version-upgrade/README.md +54 -0
- package/skills/spark-version-upgrade/SKILL.md +233 -0
- package/skills/ssh/.plugin/plugin.json +18 -0
- package/skills/ssh/README.md +140 -0
- package/skills/ssh/SKILL.md +135 -0
- package/skills/swift-linux/.plugin/plugin.json +17 -0
- package/skills/swift-linux/README.md +86 -0
- package/skills/swift-linux/SKILL.md +81 -0
- package/skills/theme-factory/.plugin/plugin.json +19 -0
- package/skills/theme-factory/LICENSE.txt +202 -0
- package/skills/theme-factory/README.md +58 -0
- package/skills/theme-factory/SKILL.md +59 -0
- package/skills/theme-factory/theme-showcase.pdf +0 -0
- package/skills/theme-factory/themes/arctic-frost.md +19 -0
- package/skills/theme-factory/themes/botanical-garden.md +19 -0
- package/skills/theme-factory/themes/desert-rose.md +19 -0
- package/skills/theme-factory/themes/forest-canopy.md +19 -0
- package/skills/theme-factory/themes/golden-hour.md +19 -0
- package/skills/theme-factory/themes/midnight-galaxy.md +19 -0
- package/skills/theme-factory/themes/modern-minimalist.md +19 -0
- package/skills/theme-factory/themes/ocean-depths.md +19 -0
- package/skills/theme-factory/themes/sunset-boulevard.md +19 -0
- package/skills/theme-factory/themes/tech-innovation.md +19 -0
- package/skills/uv/.plugin/plugin.json +18 -0
- package/skills/uv/README.md +5 -0
- package/skills/uv/SKILL.md +95 -0
- package/skills/uv/references/README.md +5 -0
- package/skills/vercel/.plugin/plugin.json +18 -0
- package/skills/vercel/README.md +108 -0
- package/skills/vercel/SKILL.md +103 -0
- package/tests/test_add_skill_installs_to_agents_dir.py +42 -0
- package/tests/test_catalogs.py +109 -0
- package/tests/test_code_review_risk_evaluation.py +94 -0
- package/tests/test_issue_duplicate_checker.py +240 -0
- package/tests/test_openhands_api_python.py +152 -0
- package/tests/test_plugin_manifest.py +83 -0
- package/tests/test_pr_review_diff_payload.py +202 -0
- package/tests/test_pr_review_feedback.py +263 -0
- package/tests/test_pr_review_prompt.py +152 -0
- package/tests/test_pr_review_review_context.py +253 -0
- package/tests/test_qa_changes.py +232 -0
- package/tests/test_qa_changes_evaluation.py +259 -0
- package/tests/test_release_notes_generator.py +990 -0
- package/tests/test_sdk_loading.py +150 -0
- package/tests/test_skill_plugin_loading.py +149 -0
- package/tests/test_skills_have_readme.py +66 -0
- package/tests/test_sync_extensions.py +292 -0
- package/tests/test_workflow_sync.py +46 -0
- package/utils/analysis/README.md +7 -0
- package/utils/analysis/laminar_signals/README.md +211 -0
- package/utils/analysis/laminar_signals/analyze.py +780 -0
- package/utils/analysis/laminar_signals/templates/default.j2 +49 -0
- 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
|
+
}
|