@openhands/extensions 0.17.0 → 0.19.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 +7 -0
- package/.github/dependabot.yml +24 -0
- package/.github/scripts/check_issue_readiness.py +272 -0
- package/.github/scripts/check_pr_description.py +226 -0
- package/.github/scripts/post-readiness-comment.mjs +184 -0
- package/.github/scripts/refresh_linked_pr_checks.py +171 -0
- package/.github/workflows/issue-readiness-check.yml +142 -0
- package/.github/workflows/pr-description-check.yml +37 -0
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +16 -0
- package/README.md +13 -2
- package/automations/README.md +3 -2
- package/automations/bundle-index.js +10 -1
- package/automations/catalog/github-agents-md-maintainer/manifest.json +101 -0
- package/automations/catalog/github-issue-to-pr/manifest.json +112 -0
- package/automations/catalog/github-pr-reviewer/manifest.json +6 -0
- package/automations/catalog/github-repo-monitor/manifest.json +6 -0
- package/automations/catalog/jira-issue-to-bitbucket-pr/manifest.json +23 -0
- package/automations/catalog/jira-issue-to-gitlab-mr/manifest.json +23 -0
- package/automations/catalog/linear-issue-to-bitbucket-pr/manifest.json +23 -0
- package/automations/catalog/linear-issue-to-github-pr/manifest.json +23 -0
- package/automations/catalog/linear-issue-to-gitlab-mr/manifest.json +23 -0
- package/automations/catalog/news-digest/manifest.json +81 -0
- package/automations/catalog/qa-changes/manifest.json +72 -0
- package/automations/catalog/slack-github-linear-daily-organization.json +27 -0
- package/automations/catalog-index.js +25 -7
- package/automations/catalog.schema.json +35 -2
- package/automations/index.d.ts +34 -1
- package/automations/interface.json +3 -1
- package/integrations/catalog/datadog.json +2 -2
- package/integrations/catalog/gitlab.json +41 -0
- package/integrations/catalog/notion.json +1 -1
- package/integrations/catalog-index.js +71 -69
- package/integrations/index.d.ts +1 -1
- package/marketplaces/openhands-extensions.json +59 -6
- package/package.json +1 -1
- package/plugins/pr-review/scripts/prompt.py +2 -0
- package/pyproject.toml +1 -1
- package/python/openhands_extensions/_version.py +1 -1
- package/skills/github-agents-md-maintainer/.plugin/plugin.json +19 -0
- package/skills/github-agents-md-maintainer/README.md +59 -0
- package/skills/github-agents-md-maintainer/SKILL.md +271 -0
- package/skills/github-agents-md-maintainer/commands/agents-md-setup.md +8 -0
- package/skills/github-agents-md-maintainer/references/state-schema.md +113 -0
- package/skills/github-agents-md-maintainer/scripts/main.py +1187 -0
- package/skills/github-issue-to-pr/.plugin/plugin.json +19 -0
- package/skills/github-issue-to-pr/README.md +84 -0
- package/skills/github-issue-to-pr/SKILL.md +373 -0
- package/skills/github-issue-to-pr/commands/issue-to-pr-setup.md +8 -0
- package/skills/github-issue-to-pr/references/state-schema.md +139 -0
- package/skills/github-issue-to-pr/scripts/main.py +1305 -0
- package/skills/github-pr-reviewer/SKILL.md +2 -1
- package/skills/github-pr-reviewer/scripts/main.py +45 -4
- package/skills/github-pr-reviewer/tests/test_main.py +74 -0
- package/skills/index.js +38 -2
- package/skills/news-digest/.plugin/plugin.json +19 -0
- package/skills/news-digest/README.md +71 -0
- package/skills/news-digest/SKILL.md +284 -0
- package/skills/news-digest/commands/news-digest-setup.md +8 -0
- package/skills/news-digest/references/state-schema.md +178 -0
- package/skills/news-digest/scripts/main.py +1250 -0
- package/skills/openhands-sdk/SKILL.md +1 -0
- package/skills/ticket-to-code-change/.plugin/plugin.json +21 -0
- package/skills/ticket-to-code-change/README.md +5 -0
- package/skills/ticket-to-code-change/SKILL.md +50 -0
- package/skills/ticket-to-code-change/commands/ticket-to-code-change-setup.md +8 -0
- package/tests/fixtures/automations/github-repo-monitor.json +77 -0
- package/tests/test_automation_setup.py +56 -0
- package/tests/test_catalogs.py +5 -3
- package/tests/test_check_issue_readiness.py +135 -0
- package/tests/test_check_pr_description.py +228 -0
- package/tests/test_github_agents_md_maintainer.py +352 -0
- package/tests/test_github_issue_to_pr.py +539 -0
- package/tests/test_news_digest.py +646 -0
- package/tests/test_pr_review_prompt.py +10 -0
- package/tests/test_refresh_linked_pr_checks.py +130 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Post (or update) a `ready-for-dev` readiness comment on an issue.
|
|
5
|
+
*
|
|
6
|
+
* Upserts by a hidden HTML marker so repeated runs update the same comment
|
|
7
|
+
* instead of spamming duplicates. Uses the GITHUB_TOKEN available to the
|
|
8
|
+
* workflow via the GH_TOKEN env var.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node post-readiness-comment.mjs \
|
|
12
|
+
* --issue-number 123 \
|
|
13
|
+
* --repo OpenHands/extensions \
|
|
14
|
+
* --reasons-file /tmp/reasons.txt \
|
|
15
|
+
* --ready # optional: post a "ready" message instead of "not ready"
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
19
|
+
|
|
20
|
+
const MARKER = "<!-- issue-readiness-check -->";
|
|
21
|
+
const API_ROOT = process.env.GITHUB_API_URL ?? "https://api.github.com";
|
|
22
|
+
|
|
23
|
+
function parseArgs(argv) {
|
|
24
|
+
const args = {};
|
|
25
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
26
|
+
const arg = argv[i];
|
|
27
|
+
if (!arg.startsWith("--")) continue;
|
|
28
|
+
const [rawKey, inlineValue] = arg.slice(2).split("=", 2);
|
|
29
|
+
const key = rawKey.replaceAll("-", "_");
|
|
30
|
+
if (inlineValue !== undefined) {
|
|
31
|
+
args[key] = inlineValue;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const next = argv[i + 1];
|
|
35
|
+
if (next && !next.startsWith("--")) {
|
|
36
|
+
args[key] = next;
|
|
37
|
+
i += 1;
|
|
38
|
+
} else {
|
|
39
|
+
args[key] = "";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return args;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function requireValue(name, value) {
|
|
46
|
+
if (!value) throw new Error(`Missing required value: ${name}`);
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function listComments(repo, issueNumber, token) {
|
|
51
|
+
// Paginate through all comments so the marker is found even on issues
|
|
52
|
+
// with >100 comments.
|
|
53
|
+
const all = [];
|
|
54
|
+
let page = 1;
|
|
55
|
+
while (true) {
|
|
56
|
+
const url = `${API_ROOT}/repos/${repo}/issues/${issueNumber}/comments?per_page=100&page=${page}`;
|
|
57
|
+
const resp = await fetch(url, {
|
|
58
|
+
headers: {
|
|
59
|
+
Authorization: `Bearer ${token}`,
|
|
60
|
+
Accept: "application/vnd.github+json",
|
|
61
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
if (!resp.ok) {
|
|
65
|
+
throw new Error(`Failed to list comments: ${resp.status} ${await resp.text()}`);
|
|
66
|
+
}
|
|
67
|
+
const batch = await resp.json();
|
|
68
|
+
all.push(...batch);
|
|
69
|
+
if (batch.length < 100) break;
|
|
70
|
+
page += 1;
|
|
71
|
+
}
|
|
72
|
+
return all;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function createComment(repo, issueNumber, body, token) {
|
|
76
|
+
const url = `${API_ROOT}/repos/${repo}/issues/${issueNumber}/comments`;
|
|
77
|
+
const resp = await fetch(url, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: {
|
|
80
|
+
Authorization: `Bearer ${token}`,
|
|
81
|
+
Accept: "application/vnd.github+json",
|
|
82
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
},
|
|
85
|
+
body: JSON.stringify({ body }),
|
|
86
|
+
});
|
|
87
|
+
if (!resp.ok) {
|
|
88
|
+
throw new Error(`Failed to create comment: ${resp.status} ${await resp.text()}`);
|
|
89
|
+
}
|
|
90
|
+
return resp.json();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function updateComment(repo, commentId, body, token) {
|
|
94
|
+
const url = `${API_ROOT}/repos/${repo}/issues/comments/${commentId}`;
|
|
95
|
+
const resp = await fetch(url, {
|
|
96
|
+
method: "PATCH",
|
|
97
|
+
headers: {
|
|
98
|
+
Authorization: `Bearer ${token}`,
|
|
99
|
+
Accept: "application/vnd.github+json",
|
|
100
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
101
|
+
"Content-Type": "application/json",
|
|
102
|
+
},
|
|
103
|
+
body: JSON.stringify({ body }),
|
|
104
|
+
});
|
|
105
|
+
if (!resp.ok) {
|
|
106
|
+
throw new Error(`Failed to update comment: ${resp.status} ${await resp.text()}`);
|
|
107
|
+
}
|
|
108
|
+
return resp.json();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function main() {
|
|
112
|
+
const args = parseArgs(process.argv.slice(2));
|
|
113
|
+
const issueNumber = requireValue("issue-number", args.issue_number);
|
|
114
|
+
const repo = requireValue("repo", args.repo);
|
|
115
|
+
const token = process.env.GH_TOKEN;
|
|
116
|
+
if (!token) throw new Error("GH_TOKEN env var is required");
|
|
117
|
+
|
|
118
|
+
const isReady = args.ready === "" || args.ready === "true";
|
|
119
|
+
const reasonsFile = args.reasons_file || args.reasons;
|
|
120
|
+
const reasons = reasonsFile
|
|
121
|
+
? readFileSync(reasonsFile, "utf8")
|
|
122
|
+
.split("\n")
|
|
123
|
+
.map((line) => line.trim())
|
|
124
|
+
.filter(Boolean)
|
|
125
|
+
: [];
|
|
126
|
+
|
|
127
|
+
const reasonList = reasons.length
|
|
128
|
+
? reasons.map((r) => `- ${r}`).join("\n")
|
|
129
|
+
: "- The issue does not meet the ready-for-dev criteria.";
|
|
130
|
+
|
|
131
|
+
const body = isReady
|
|
132
|
+
? [
|
|
133
|
+
MARKER,
|
|
134
|
+
"### \u2705 `ready-for-dev` label applied",
|
|
135
|
+
"",
|
|
136
|
+
"This issue meets the readiness criteria and is ready for contribution.",
|
|
137
|
+
"A contributor can now open a PR that links this issue (`Fixes #<number>`).",
|
|
138
|
+
"",
|
|
139
|
+
"_This is an automated readiness check posted by a workflow._",
|
|
140
|
+
].join("\n")
|
|
141
|
+
: [
|
|
142
|
+
MARKER,
|
|
143
|
+
"### \u26a0\ufe0f Not yet `ready-for-dev`",
|
|
144
|
+
"",
|
|
145
|
+
"This issue does not yet meet the readiness criteria, so the",
|
|
146
|
+
"`ready-for-dev` label has not been applied (or has been removed).",
|
|
147
|
+
"The criteria are type-specific:",
|
|
148
|
+
"",
|
|
149
|
+
"**Bug reports** (`bug` label):",
|
|
150
|
+
"- The `### Actual Behavior` section must describe reproducible steps",
|
|
151
|
+
" and include a command such as `uv run pytest`, `pytest`, `python`,",
|
|
152
|
+
" `pip`, `npm run`, or `node`.",
|
|
153
|
+
"- An `### Acceptance Criteria` section with at least one checklist item (`- [ ] ...`).",
|
|
154
|
+
"",
|
|
155
|
+
"**Enhancements** (`enhancement` label):",
|
|
156
|
+
"- A `### Desired Behavior` section.",
|
|
157
|
+
"- An `### Acceptance Criteria` section with at least one checklist item.",
|
|
158
|
+
"",
|
|
159
|
+
"What is missing:",
|
|
160
|
+
"",
|
|
161
|
+
reasonList,
|
|
162
|
+
"",
|
|
163
|
+
"Edit the issue to address the gaps and the `ready-for-dev` label will be",
|
|
164
|
+
"applied automatically.",
|
|
165
|
+
"",
|
|
166
|
+
"_This is an automated readiness check posted by a workflow._",
|
|
167
|
+
].join("\n");
|
|
168
|
+
|
|
169
|
+
const comments = await listComments(repo, issueNumber, token);
|
|
170
|
+
const existing = comments.find((comment) => comment.body?.includes(MARKER));
|
|
171
|
+
|
|
172
|
+
if (existing) {
|
|
173
|
+
await updateComment(repo, existing.id, body, token);
|
|
174
|
+
console.log(`Updated readiness comment ${existing.id} on issue #${issueNumber}`);
|
|
175
|
+
} else {
|
|
176
|
+
const created = await createComment(repo, issueNumber, body, token);
|
|
177
|
+
console.log(`Created readiness comment ${created.id} on issue #${issueNumber}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
main().catch((error) => {
|
|
182
|
+
console.error(error);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Re-run the PR Description Check for open PRs linked to an issue.
|
|
2
|
+
|
|
3
|
+
Invoked from the issue-readiness workflow when the `ready-for-dev` label is
|
|
4
|
+
added to or removed from an issue, so the PR gate reflects the settled
|
|
5
|
+
readiness state without waiting for the next PR event.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from check_pr_description import extract_linked_issue_numbers
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _run(args: list[str]) -> subprocess.CompletedProcess[str]:
|
|
20
|
+
return subprocess.run(args, capture_output=True, text=True, check=False)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _linked_open_prs(repo: str, issue_number: int) -> list[dict]:
|
|
24
|
+
"""Return open PRs that cross-reference ``issue_number``."""
|
|
25
|
+
owner, name = repo.split("/", 1)
|
|
26
|
+
query = """
|
|
27
|
+
query($owner: String!, $name: String!, $num: Int!) {
|
|
28
|
+
repository(owner: $owner, name: $name) {
|
|
29
|
+
issue(number: $num) {
|
|
30
|
+
timelineItems(first: 100, itemTypes: [CROSS_REFERENCED_EVENT]) {
|
|
31
|
+
nodes {
|
|
32
|
+
__typename
|
|
33
|
+
... on CrossReferencedEvent {
|
|
34
|
+
source {
|
|
35
|
+
__typename
|
|
36
|
+
... on PullRequest { number headRefOid state }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
"""
|
|
45
|
+
result = _run(
|
|
46
|
+
[
|
|
47
|
+
"gh",
|
|
48
|
+
"api",
|
|
49
|
+
"graphql",
|
|
50
|
+
"-f",
|
|
51
|
+
f"query={query}",
|
|
52
|
+
"-F",
|
|
53
|
+
f"owner={owner}",
|
|
54
|
+
"-F",
|
|
55
|
+
f"name={name}",
|
|
56
|
+
"-F",
|
|
57
|
+
f"num={issue_number}",
|
|
58
|
+
"--jq",
|
|
59
|
+
".data.repository.issue.timelineItems.nodes",
|
|
60
|
+
]
|
|
61
|
+
)
|
|
62
|
+
if result.returncode != 0:
|
|
63
|
+
print(f"::warning::Could not query linked PRs: {result.stderr.strip()}")
|
|
64
|
+
return []
|
|
65
|
+
try:
|
|
66
|
+
nodes = json.loads(result.stdout)
|
|
67
|
+
except json.JSONDecodeError as exc:
|
|
68
|
+
print(f"::warning::Unexpected linked-PR query output: {exc}")
|
|
69
|
+
return []
|
|
70
|
+
prs = []
|
|
71
|
+
for node in nodes:
|
|
72
|
+
source = node.get("source") if isinstance(node, dict) else None
|
|
73
|
+
if not isinstance(source, dict):
|
|
74
|
+
continue
|
|
75
|
+
if source.get("__typename") != "PullRequest":
|
|
76
|
+
continue
|
|
77
|
+
if source.get("state") != "OPEN":
|
|
78
|
+
continue
|
|
79
|
+
prs.append(source)
|
|
80
|
+
return prs
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _rerun_pr_description_check(repo: str, head_sha: str) -> bool:
|
|
84
|
+
"""Re-run the latest PR Description Check for the given head SHA."""
|
|
85
|
+
runs_result = _run(
|
|
86
|
+
[
|
|
87
|
+
"gh",
|
|
88
|
+
"api",
|
|
89
|
+
"-X",
|
|
90
|
+
"GET",
|
|
91
|
+
f"repos/{repo}/actions/runs",
|
|
92
|
+
"-f",
|
|
93
|
+
f"head_sha={head_sha}",
|
|
94
|
+
"-f",
|
|
95
|
+
"per_page=100",
|
|
96
|
+
"--jq",
|
|
97
|
+
r'.workflow_runs[] | select(.name=="PR Description Check") | '
|
|
98
|
+
r'select(.event=="pull_request_target") | "\(.id) \(.created_at)"',
|
|
99
|
+
]
|
|
100
|
+
)
|
|
101
|
+
if runs_result.returncode != 0:
|
|
102
|
+
print(f"::warning::Could not list PR checks: {runs_result.stderr.strip()}")
|
|
103
|
+
return False
|
|
104
|
+
lines = [line.split() for line in runs_result.stdout.splitlines() if line.strip()]
|
|
105
|
+
if not lines:
|
|
106
|
+
return False
|
|
107
|
+
# Created-at is sortable; pick the most recent run for this commit.
|
|
108
|
+
latest = sorted(lines, key=lambda item: " ".join(item[1:]))[-1][0]
|
|
109
|
+
rerun = _run(
|
|
110
|
+
["gh", "api", "-X", "POST", f"repos/{repo}/actions/runs/{latest}/rerun"]
|
|
111
|
+
)
|
|
112
|
+
if rerun.returncode != 0:
|
|
113
|
+
print(
|
|
114
|
+
f"::warning::Could not re-run PR description check ({latest}): "
|
|
115
|
+
f"{rerun.stderr.strip()}"
|
|
116
|
+
)
|
|
117
|
+
return False
|
|
118
|
+
print(f"Re-ran PR description check (run {latest}) for linked open PR.")
|
|
119
|
+
return True
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def main() -> int:
|
|
123
|
+
payload = json.loads(Path(os.environ["GITHUB_EVENT_PATH"]).read_text())
|
|
124
|
+
if {"action", "issue", "label", "repository"} - set(payload):
|
|
125
|
+
return 0
|
|
126
|
+
action = payload["action"]
|
|
127
|
+
raw_label = payload.get("label")
|
|
128
|
+
label = raw_label.get("name") if isinstance(raw_label, dict) else None
|
|
129
|
+
if action not in ("labeled", "unlabeled") or label != "ready-for-dev":
|
|
130
|
+
return 0
|
|
131
|
+
raw_repo = payload.get("repository")
|
|
132
|
+
repo = raw_repo.get("full_name") if isinstance(raw_repo, dict) else None
|
|
133
|
+
raw_issue = payload.get("issue")
|
|
134
|
+
issue_number = raw_issue.get("number") if isinstance(raw_issue, dict) else None
|
|
135
|
+
if not repo or not issue_number:
|
|
136
|
+
return 0
|
|
137
|
+
|
|
138
|
+
print(
|
|
139
|
+
f"ready-for-dev {action}: refreshing PR gates linked to issue #{issue_number}."
|
|
140
|
+
)
|
|
141
|
+
refreshed = 0
|
|
142
|
+
for pr in _linked_open_prs(repo, issue_number):
|
|
143
|
+
pr_number = pr["number"]
|
|
144
|
+
body_result = _run(
|
|
145
|
+
[
|
|
146
|
+
"gh",
|
|
147
|
+
"pr",
|
|
148
|
+
"view",
|
|
149
|
+
str(pr_number),
|
|
150
|
+
"--repo",
|
|
151
|
+
repo,
|
|
152
|
+
"--json",
|
|
153
|
+
"body",
|
|
154
|
+
"--jq",
|
|
155
|
+
".body",
|
|
156
|
+
]
|
|
157
|
+
)
|
|
158
|
+
if body_result.returncode != 0:
|
|
159
|
+
continue
|
|
160
|
+
if issue_number not in extract_linked_issue_numbers(body_result.stdout):
|
|
161
|
+
# Cross-referenced but not treated as a linked issue by the gate;
|
|
162
|
+
# nothing to refresh.
|
|
163
|
+
continue
|
|
164
|
+
if _rerun_pr_description_check(repo, pr["headRefOid"]):
|
|
165
|
+
refreshed += 1
|
|
166
|
+
print(f"Refreshed PR gates for {refreshed} linked PR(s).")
|
|
167
|
+
return 0
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
if __name__ == "__main__":
|
|
171
|
+
sys.exit(main())
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Issue Readiness Check
|
|
3
|
+
|
|
4
|
+
# Manages the `ready-for-dev` label based on type-specific readiness criteria
|
|
5
|
+
# tailored to extension contributions (skills, plugins, integrations,
|
|
6
|
+
# automations).
|
|
7
|
+
#
|
|
8
|
+
# Bug reports (label `bug`): the Actual Behavior section must reference a
|
|
9
|
+
# reproducible command (`uv run pytest`, `pytest`, `python`, `pip`, `npm run`,
|
|
10
|
+
# or `node`), plus an Acceptance Criteria section with at least one checklist
|
|
11
|
+
# item.
|
|
12
|
+
#
|
|
13
|
+
# Enhancements (label `enhancement`): the body must contain Desired Behavior and
|
|
14
|
+
# Acceptance Criteria sections, the latter with at least one checklist item.
|
|
15
|
+
#
|
|
16
|
+
# When criteria are met the label is added (idempotently); when they are not the
|
|
17
|
+
# label is removed. A feedback comment is posted (or updated) in three cases:
|
|
18
|
+
# 1. The issue is first opened or reopened - always comment so the author
|
|
19
|
+
# knows whether their issue is ready or what is missing.
|
|
20
|
+
# 2. The label is being added (issue became ready) - comment celebrating it.
|
|
21
|
+
# 3. The label is being removed (issue was ready but is no longer) - comment
|
|
22
|
+
# explaining what changed.
|
|
23
|
+
# Edits that do not change the label state do not produce a new comment. All
|
|
24
|
+
# comments are upserted by a hidden marker so there is at most one per issue.
|
|
25
|
+
#
|
|
26
|
+
# The readiness step uses `--json`, which always exits 0, so a not-ready result
|
|
27
|
+
# never aborts the workflow under `set -euo pipefail`; label and comment steps
|
|
28
|
+
# run for both outcomes.
|
|
29
|
+
|
|
30
|
+
on:
|
|
31
|
+
issues:
|
|
32
|
+
types: [opened, edited, reopened, labeled, unlabeled]
|
|
33
|
+
|
|
34
|
+
permissions:
|
|
35
|
+
issues: write
|
|
36
|
+
pull-requests: read
|
|
37
|
+
contents: read
|
|
38
|
+
|
|
39
|
+
concurrency:
|
|
40
|
+
group: issue-readiness-${{ github.event.issue.number }}
|
|
41
|
+
cancel-in-progress: false
|
|
42
|
+
|
|
43
|
+
jobs:
|
|
44
|
+
check:
|
|
45
|
+
if: github.event.issue.state == 'open' && github.event.issue.pull_request == null
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
timeout-minutes: 5
|
|
48
|
+
steps:
|
|
49
|
+
- name: Checkout trusted workflow scripts
|
|
50
|
+
uses: actions/checkout@v4
|
|
51
|
+
|
|
52
|
+
- name: Evaluate issue readiness
|
|
53
|
+
id: readiness
|
|
54
|
+
env:
|
|
55
|
+
GITHUB_EVENT_PATH: ${{ github.event_path }}
|
|
56
|
+
run: |
|
|
57
|
+
set -euo pipefail
|
|
58
|
+
python .github/scripts/check_issue_readiness.py --json > /tmp/result.json
|
|
59
|
+
echo "ready=$(jq -r '.ready' /tmp/result.json)" >> "$GITHUB_OUTPUT"
|
|
60
|
+
# Write reasons to a file so the comment step can read them
|
|
61
|
+
# without shell-escaping multiline text.
|
|
62
|
+
jq -r '.reasons[]?' /tmp/result.json > /tmp/reasons.txt || true
|
|
63
|
+
echo "reasons_file=/tmp/reasons.txt" >> "$GITHUB_OUTPUT"
|
|
64
|
+
|
|
65
|
+
- name: Add ready-for-dev label
|
|
66
|
+
if: steps.readiness.outputs.ready == 'true'
|
|
67
|
+
env:
|
|
68
|
+
GH_TOKEN: ${{ github.token }}
|
|
69
|
+
run: |
|
|
70
|
+
set -euo pipefail
|
|
71
|
+
# Capture stderr — tolerate "label already present" (exit 0
|
|
72
|
+
# from gh) but surface unexpected errors to the step summary.
|
|
73
|
+
if ! gh issue edit "${{ github.event.issue.number }}" \
|
|
74
|
+
--repo "${{ github.repository }}" \
|
|
75
|
+
--add-label "ready-for-dev" 2>/tmp/gh-err.txt; then
|
|
76
|
+
echo "::warning::Failed to add ready-for-dev label:" >> "$GITHUB_STEP_SUMMARY"
|
|
77
|
+
cat /tmp/gh-err.txt >> "$GITHUB_STEP_SUMMARY"
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
- name: Remove ready-for-dev label
|
|
81
|
+
if: steps.readiness.outputs.ready != 'true' && contains(github.event.issue.labels.*.name, 'ready-for-dev')
|
|
82
|
+
env:
|
|
83
|
+
GH_TOKEN: ${{ github.token }}
|
|
84
|
+
run: |
|
|
85
|
+
set -euo pipefail
|
|
86
|
+
# Capture stderr — tolerate "label already removed" but
|
|
87
|
+
# surface unexpected errors.
|
|
88
|
+
if ! gh issue edit "${{ github.event.issue.number }}" \
|
|
89
|
+
--repo "${{ github.repository }}" \
|
|
90
|
+
--remove-label "ready-for-dev" 2>/tmp/gh-err.txt; then
|
|
91
|
+
echo "::warning::Failed to remove ready-for-dev label:" >> "$GITHUB_STEP_SUMMARY"
|
|
92
|
+
cat /tmp/gh-err.txt >> "$GITHUB_STEP_SUMMARY"
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
- name: Post feedback comment
|
|
96
|
+
if: >-
|
|
97
|
+
github.event.action == 'opened'
|
|
98
|
+
|| github.event.action == 'reopened'
|
|
99
|
+
|| (steps.readiness.outputs.ready == 'true' && !contains(github.event.issue.labels.*.name, 'ready-for-dev'))
|
|
100
|
+
|| (steps.readiness.outputs.ready != 'true' && contains(github.event.issue.labels.*.name, 'ready-for-dev'))
|
|
101
|
+
env:
|
|
102
|
+
GH_TOKEN: ${{ github.token }}
|
|
103
|
+
run: |
|
|
104
|
+
set -euo pipefail
|
|
105
|
+
if [ "${{ steps.readiness.outputs.ready }}" = "true" ]; then
|
|
106
|
+
node .github/scripts/post-readiness-comment.mjs \
|
|
107
|
+
--issue-number "${{ github.event.issue.number }}" \
|
|
108
|
+
--repo "${{ github.repository }}" \
|
|
109
|
+
--ready
|
|
110
|
+
else
|
|
111
|
+
node .github/scripts/post-readiness-comment.mjs \
|
|
112
|
+
--issue-number "${{ github.event.issue.number }}" \
|
|
113
|
+
--repo "${{ github.repository }}" \
|
|
114
|
+
--reasons-file "${{ steps.readiness.outputs.reasons_file }}"
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
# When the ready-for-dev label is added or removed, re-run the PR
|
|
118
|
+
# description check for every open PR that links this issue, so the
|
|
119
|
+
# `ready-for-dev` gate does not go stale between PR events. Runs after
|
|
120
|
+
# `check` so the label reflects the settled readiness state.
|
|
121
|
+
refresh-linked-pr-gates:
|
|
122
|
+
name: Refresh PR gates for linked issues
|
|
123
|
+
needs: check
|
|
124
|
+
if: >-
|
|
125
|
+
github.event.issue.state == 'open'
|
|
126
|
+
&& github.event.issue.pull_request == null
|
|
127
|
+
&& (github.event.action == 'labeled' || github.event.action == 'unlabeled')
|
|
128
|
+
&& github.event.label.name == 'ready-for-dev'
|
|
129
|
+
runs-on: ubuntu-latest
|
|
130
|
+
timeout-minutes: 10
|
|
131
|
+
permissions:
|
|
132
|
+
contents: read
|
|
133
|
+
pull-requests: read
|
|
134
|
+
actions: write
|
|
135
|
+
steps:
|
|
136
|
+
- name: Checkout trusted workflow scripts
|
|
137
|
+
uses: actions/checkout@v4
|
|
138
|
+
|
|
139
|
+
- name: Re-run PR description checks for linked open PRs
|
|
140
|
+
env:
|
|
141
|
+
GH_TOKEN: ${{ github.token }}
|
|
142
|
+
run: python .github/scripts/refresh_linked_pr_checks.py
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: PR Description Check
|
|
3
|
+
|
|
4
|
+
# Use pull_request_target so fork PR descriptions can be checked, but only run
|
|
5
|
+
# trusted validation code from the base branch checkout below.
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
pull_request_target:
|
|
9
|
+
types: [opened, edited, reopened, ready_for_review, synchronize]
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
pull-requests: read
|
|
14
|
+
issues: read
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
validate-pr-description:
|
|
18
|
+
name: Validate PR description
|
|
19
|
+
# Draft PRs may still have incomplete descriptions; validate when review starts.
|
|
20
|
+
# Release PRs are generated by the trusted prepare-release workflow without
|
|
21
|
+
# the standard human-authored PR template.
|
|
22
|
+
if: >-
|
|
23
|
+
github.event.pull_request.draft == false &&
|
|
24
|
+
github.event.pull_request.user.login != 'dependabot[bot]' &&
|
|
25
|
+
!(github.event.pull_request.head.repo.full_name == github.repository &&
|
|
26
|
+
startsWith(github.event.pull_request.head.ref, 'release-please-'))
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
steps:
|
|
29
|
+
- name: Checkout trusted workflow scripts
|
|
30
|
+
uses: actions/checkout@v4
|
|
31
|
+
with:
|
|
32
|
+
ref: ${{ github.event.pull_request.base.sha }}
|
|
33
|
+
|
|
34
|
+
- name: Validate PR template sections and linked-issue readiness
|
|
35
|
+
env:
|
|
36
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
37
|
+
run: python .github/scripts/check_pr_description.py
|
package/AGENTS.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
This repository (`OpenHands/extensions`) is the **public extensions registry** for OpenHands.
|
|
4
4
|
It contains **shareable skills and plugins** that can be loaded by OpenHands (CLI/GUI/Cloud) and by client code using the **Software Agent SDK**.
|
|
5
5
|
|
|
6
|
+
|
|
7
|
+
## Cross-Repository Boundaries
|
|
8
|
+
|
|
9
|
+
This repository owns the public registry of reusable OpenHands skills, plugins, automations, and integrations. These extensions are consumed by OpenHands applications and SDK-based clients.
|
|
10
|
+
|
|
11
|
+
Related repositories have distinct responsibilities:
|
|
12
|
+
|
|
13
|
+
- [`OpenHands/software-agent-sdk`](https://github.com/OpenHands/software-agent-sdk) owns the Python SDK, Agent Server, agent/tool behavior, conversations, workspaces, events, and canonical API.
|
|
14
|
+
- [`OpenHands/typescript-client`](https://github.com/OpenHands/typescript-client) owns the browser-compatible typed Agent Server client.
|
|
15
|
+
- [`OpenHands/OpenHands`](https://github.com/OpenHands/OpenHands) owns Agent Canvas UI and local-stack orchestration.
|
|
16
|
+
- [`OpenHands/automation`](https://github.com/OpenHands/automation) owns scheduling, webhooks, run history, dispatch, and sandbox lifecycle orchestration.
|
|
17
|
+
|
|
18
|
+
Put reusable skills, plugins, automations, and integrations here; put backend execution behavior in the SDK, typed API access in `typescript-client`, application UI in Agent Canvas, and scheduling/dispatch lifecycle code in `automation`. If a PR is opened in the wrong repository, explicitly recommend closing and moving it to the owning repository. PRs must follow this repository's applicable contribution and code-review guidance.
|
|
19
|
+
|
|
6
20
|
## What this repo contains
|
|
7
21
|
|
|
8
22
|
- `skills/` — a catalog of skills, **one directory per skill**.
|
|
@@ -110,6 +124,8 @@ When editing or adding skills in this repo, follow these rules (and add new skil
|
|
|
110
124
|
- `scripts/sync_extensions.py` keeps generated artifacts in sync: Claude Code command files, README catalog section, coverage checks, and vendor symlinks. Run `python scripts/sync_extensions.py --check` (or just push — CI runs it) to verify everything is consistent. Run without `--check` to auto-fix. The "Quick Start" section in `README.md` (OpenHands SDK, Claude Code, and Codex setup instructions) is **manually maintained** above the auto-generated catalog markers and is intentionally not generated by the sync script.
|
|
111
125
|
- The sync script uses PyYAML to parse SKILL.md frontmatter. If you add a skill with a slash trigger (e.g., `triggers: ["/mycommand"]`), the script auto-generates `commands/mycommand.md`. **Note:** Slash triggers in SKILL.md frontmatter are deprecated — prefer adding a `commands/command-name.md` file to the plugin's `commands/` directory instead. Keyword triggers (non-slash) remain the recommended way to activate skills by topic.
|
|
112
126
|
|
|
127
|
+
- The `ready-for-dev` gates live in `.github/workflows/issue-readiness-check.yml` and `.github/workflows/pr-description-check.yml`, backed by `.github/scripts/check_issue_readiness.py`, `check_pr_description.py`, `refresh_linked_pr_checks.py`, and `post-readiness-comment.mjs`. Issues labeled `bug` or `enhancement` must meet type-specific readiness criteria (reproducible command plus Acceptance Criteria checklist for bugs; Desired Behavior plus Acceptance Criteria checklist for enhancements) to receive the `ready-for-dev` label. Non-draft PRs must keep the `## Why`, `## Summary`, and `## How to Test` template sections filled, and any issue linked via a closing keyword or the `## Issue Number` section must carry `ready-for-dev` unless it predates the 2026-08-25 rollout. Label transitions on an issue re-run the PR Description Check for linked open PRs. Tests live in `tests/test_check_issue_readiness.py`, `tests/test_check_pr_description.py`, and `tests/test_refresh_linked_pr_checks.py`.
|
|
128
|
+
|
|
113
129
|
## OpenHands SDK documentation policy
|
|
114
130
|
|
|
115
131
|
- **Do NOT add SDK-specific or SDK-related documentation to this repo.** The canonical source of truth for SDK documentation is the [OpenHands docs site](https://docs.openhands.dev/sdk) and its structured index at <https://docs.openhands.dev/llms.txt>.
|
package/README.md
CHANGED
|
@@ -6,6 +6,13 @@ It contains reusable, shareable skills and plugins that customize agent behavior
|
|
|
6
6
|
- Skills overview docs: https://docs.openhands.dev/overview/skills
|
|
7
7
|
- SDK skill guide: https://docs.openhands.dev/sdk/guides/skill
|
|
8
8
|
|
|
9
|
+
|
|
10
|
+
## Repository boundaries
|
|
11
|
+
|
|
12
|
+
`OpenHands/extensions` is the public registry for reusable skills, plugins, automations, and integrations. [`OpenHands/software-agent-sdk`](https://github.com/OpenHands/software-agent-sdk) owns Agent Server execution and the canonical API, [`OpenHands/typescript-client`](https://github.com/OpenHands/typescript-client) owns typed browser access to that API, [`OpenHands/OpenHands`](https://github.com/OpenHands/OpenHands) owns Agent Canvas UI, and [`OpenHands/automation`](https://github.com/OpenHands/automation) owns scheduling, webhooks, run history, dispatch, and sandbox lifecycle orchestration.
|
|
13
|
+
|
|
14
|
+
Put reusable extension artifacts here rather than in application repositories. If a PR is opened in the wrong repository, close and move it to the repository that owns the change.
|
|
15
|
+
|
|
9
16
|
## Repository Layout
|
|
10
17
|
|
|
11
18
|
### Skills
|
|
@@ -89,7 +96,7 @@ The JS and Python versions are kept in lock-step by `release-please` and guarded
|
|
|
89
96
|
## Extensions Catalog
|
|
90
97
|
|
|
91
98
|
<!-- BEGIN AUTO-GENERATED CATALOG -->
|
|
92
|
-
This repository contains **2 marketplace(s)** with **
|
|
99
|
+
This repository contains **2 marketplace(s)** with **68 extensions** (58 skills, 10 plugins).
|
|
93
100
|
|
|
94
101
|
### large-codebase
|
|
95
102
|
|
|
@@ -108,7 +115,7 @@ OpenHands skills for interacting, improving, and refactoring large codebases
|
|
|
108
115
|
|
|
109
116
|
Official skills and plugins for OpenHands — the open-source AI software engineer.
|
|
110
117
|
|
|
111
|
-
**
|
|
118
|
+
**64 extensions** (56 skills, 8 plugins)
|
|
112
119
|
|
|
113
120
|
| Name | Type | Description | Commands |
|
|
114
121
|
|------|------|-------------|----------|
|
|
@@ -133,6 +140,8 @@ Official skills and plugins for OpenHands — the open-source AI software engine
|
|
|
133
140
|
| frontend-design | skill | Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks ... | — |
|
|
134
141
|
| github | skill | Interact with GitHub repositories, pull requests, issues, and workflows using the GITHUB_TOKEN environment variable a... | — |
|
|
135
142
|
| github-actions | skill | Create, debug, and test GitHub Actions workflows and custom actions. Use when building CI/CD pipelines, automating wo... | — |
|
|
143
|
+
| github-agents-md-maintainer | skill | Create an automation that keeps AGENTS.md current in one or more GitHub repositories. On a schedule an agent reads th... | `/agents-md:setup` |
|
|
144
|
+
| github-issue-to-pr | skill | Create an automation that implements GitHub issues when a configurable trigger label is applied. Clones the default b... | `/issue-to-pr:setup` |
|
|
136
145
|
| github-pr-review | skill | Post structured PR reviews to GitHub with inline comments/suggestions in a single API call. | `/github-pr-review` |
|
|
137
146
|
| github-pr-reviewer | skill | Create an automation that reviews GitHub pull requests when they are opened or updated. Inspects the diff, changed fi... | `/pr-reviewer:setup` |
|
|
138
147
|
| github-repo-monitor | skill | Create a cron automation that polls a GitHub repository for issue and PR comments containing a configurable trigger p... | `/github-monitor:poll` |
|
|
@@ -146,6 +155,7 @@ Official skills and plugins for OpenHands — the open-source AI software engine
|
|
|
146
155
|
| linear | skill | Interact with Linear project management - query issues, update status, create tickets using the Linear GraphQL API. | — |
|
|
147
156
|
| linear-triage | skill | Create an automation that triages new Linear issues by inspecting title, description, team, and recent related issues... | `/linear-triage:setup` |
|
|
148
157
|
| magic-test | plugin | A simple test plugin for verifying plugin loading. Triggers on magic words (alakazam, abracadabra) and returns a spec... | — |
|
|
158
|
+
| news-digest | skill | Create an automation that reads public RSS and Atom feeds on a schedule, keeps what is new and matches the configured... | `/news-digest:setup` |
|
|
149
159
|
| notion | skill | Create, search, and update Notion pages/databases using the Notion API. Use for documenting work, generating runbooks... | — |
|
|
150
160
|
| npm | skill | Handle npm package installation in non-interactive environments by piping confirmations. Use when installing Node.js ... | — |
|
|
151
161
|
| onboarding | plugin | Assess repository agent-readiness across five pillars, propose high-impact fixes, and generate repo-specific AGENTS.m... | — |
|
|
@@ -168,6 +178,7 @@ Official skills and plugins for OpenHands — the open-source AI software engine
|
|
|
168
178
|
| swift-linux | skill | Install and configure Swift programming language on Debian Linux for server-side development. Use when building Swift... | — |
|
|
169
179
|
| technical-writing | skill | Write and revise technical explanations in flowing, direct, conversational prose that stays concise without becoming ... | — |
|
|
170
180
|
| theme-factory | skill | Toolkit for styling artifacts with a theme. These artifacts can be slides, docs, reportings, HTML landing pages, etc.... | — |
|
|
181
|
+
| ticket-to-code-change | skill | Set up Jira or Linear ticket-to-code-change automations for GitHub, GitLab, and Bitbucket. | `/ticket-to-code-change:setup` |
|
|
171
182
|
| upstream-fork-sync | skill | Keep a long-lived fork in sync with its upstream. Creates a cron automation that fetches upstream changes, rebases lo... | `/upstream-fork-sync:setup` |
|
|
172
183
|
| uv | skill | Common project, dependency, and environment operations using uv. | — |
|
|
173
184
|
| vercel | skill | Deploy and manage applications on Vercel, including preview deployments and deployment protection. | — |
|
package/automations/README.md
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
`catalog/<id>/` is one automation. Its `manifest.json` is the single hand-authored source of truth: the
|
|
4
4
|
card metadata Agent Canvas renders today and, optionally, a nested `setup` block, the extension-owned
|
|
5
|
-
configuration experience for that automation.
|
|
6
|
-
|
|
5
|
+
configuration experience for that automation. It is the only file there. A script an automation uploads as
|
|
6
|
+
a `.tar.gz` is not copied into this directory: `setup.bundle.files` names where it already lives, so a
|
|
7
|
+
script shipped by both a skill and the catalog has one copy rather than two.
|
|
7
8
|
|
|
8
9
|
```
|
|
9
10
|
catalog/<id>/manifest.json one automation per directory - card metadata plus an optional `setup` block
|