@openhands/extensions 0.30.0 → 0.31.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 +3 -4
- package/.claude-plugin/marketplace.json +13 -0
- package/.codex-plugin/marketplace.json +13 -0
- package/.github/workflows/pr-artifacts.yml +4 -83
- package/.github/workflows/tests.yml +13 -0
- package/.plugin/marketplace.json +13 -0
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +4 -3
- package/README.md +3 -2
- package/automations/bundle-index.js +1 -1
- package/automations/catalog/github-pr-reviewer/manifest.json +2 -2
- package/marketplaces/openhands-extensions.json +13 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/python/openhands_extensions/_version.py +1 -1
- package/skills/github-pr-reviewer/README.md +6 -0
- package/skills/github-pr-reviewer/SKILL.md +7 -0
- package/skills/github-stale-ci-pr-closer/scripts/worker.py +8 -2
- package/skills/index.js +12 -2
- package/skills/slack-channel-monitor/README.md +6 -2
- package/skills/slack-channel-monitor/SKILL.md +29 -17
- package/skills/slack-channel-monitor/references/windows.md +6 -2
- package/skills/slack-channel-monitor/scripts/main.py +156 -11
- package/skills/software-factory/.claude-plugin/plugin.json +19 -0
- package/skills/software-factory/.codex-plugin/plugin.json +19 -0
- package/skills/software-factory/.plugin/plugin.json +19 -0
- package/skills/software-factory/README.md +64 -0
- package/skills/software-factory/SKILL.md +229 -0
- package/skills/software-factory/commands/software-factory.md +8 -0
- package/skills/software-factory/scripts/factory.py +630 -0
- package/tests/test_github_stale_ci_pr_closer.py +52 -1
- package/tests/test_software_factory_script.py +231 -0
|
@@ -17,6 +17,10 @@ On subsequent runs:
|
|
|
17
17
|
Configuration constants are embedded at automation-creation time by the skill.
|
|
18
18
|
See SKILL.md for the full setup workflow.
|
|
19
19
|
|
|
20
|
+
Runs against a local agent server (AGENT_SERVER_URL is injected) or on OpenHands
|
|
21
|
+
Cloud and Enterprise (OPENHANDS_CLOUD_API_URL / OPENHANDS_API_KEY / SANDBOX_ID are
|
|
22
|
+
injected instead).
|
|
23
|
+
|
|
20
24
|
Required secrets (set in OpenHands Settings → Secrets):
|
|
21
25
|
SLACK_BOT_TOKEN - bot token (xoxb-…) with scopes:
|
|
22
26
|
channels:history, channels:read,
|
|
@@ -26,7 +30,8 @@ Required secrets (set in OpenHands Settings → Secrets):
|
|
|
26
30
|
channels:history, search:read (for multi-channel),
|
|
27
31
|
reactions:write, chat:write
|
|
28
32
|
|
|
29
|
-
Optional secret
|
|
33
|
+
Optional secret (local Agent Canvas only; on OpenHands Cloud and Enterprise the
|
|
34
|
+
links open the deployment's own Canvas):
|
|
30
35
|
OPENHANDS_URL - base URL of your OpenHands instance for conversation
|
|
31
36
|
links (default: http://localhost:8000)
|
|
32
37
|
"""
|
|
@@ -38,6 +43,7 @@ import sys
|
|
|
38
43
|
import time
|
|
39
44
|
import urllib.error
|
|
40
45
|
import urllib.request
|
|
46
|
+
import uuid
|
|
41
47
|
from datetime import datetime, timezone
|
|
42
48
|
from urllib.parse import urlencode
|
|
43
49
|
|
|
@@ -101,6 +107,12 @@ MAX_THREAD_REPLY_POLLS_PER_RUN = 1
|
|
|
101
107
|
|
|
102
108
|
# ── Stdlib helpers ─────────────────────────────────────────────────────────────
|
|
103
109
|
|
|
110
|
+
# A local Agent Canvas run is handed the Agent Server URL. A run on OpenHands
|
|
111
|
+
# Cloud or Enterprise is handed the OpenHands API instead.
|
|
112
|
+
_CLOUD_API_URL = os.environ.get("OPENHANDS_CLOUD_API_URL", "").rstrip("/")
|
|
113
|
+
IS_CLOUD = bool(_CLOUD_API_URL) and not os.environ.get("AGENT_SERVER_URL")
|
|
114
|
+
|
|
115
|
+
|
|
104
116
|
def _get_env_key() -> str:
|
|
105
117
|
return (
|
|
106
118
|
os.environ.get("SESSION_API_KEY")
|
|
@@ -110,13 +122,14 @@ def _get_env_key() -> str:
|
|
|
110
122
|
|
|
111
123
|
|
|
112
124
|
def get_secret(name: str) -> str:
|
|
113
|
-
"""Fetch a named secret from the agent server."""
|
|
114
|
-
url = os.environ.get("AGENT_SERVER_URL", "").rstrip("/")
|
|
125
|
+
"""Fetch a named secret from the agent server, or from the OpenHands API on cloud."""
|
|
115
126
|
key = _get_env_key()
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
127
|
+
if IS_CLOUD:
|
|
128
|
+
# The OpenHands API serves the secrets of this run's sandbox.
|
|
129
|
+
url = f"{_CLOUD_API_URL}/api/v1/sandboxes/{os.environ['SANDBOX_ID']}/settings/secrets/{name}"
|
|
130
|
+
else:
|
|
131
|
+
url = f"{os.environ.get('AGENT_SERVER_URL', '').rstrip('/')}/api/settings/secrets/{name}"
|
|
132
|
+
req = urllib.request.Request(url, headers={"X-Session-API-Key": key})
|
|
120
133
|
with urllib.request.urlopen(req) as r:
|
|
121
134
|
return r.read().decode().strip()
|
|
122
135
|
|
|
@@ -158,6 +171,9 @@ def _ngrok_public_openhands_url() -> str | None:
|
|
|
158
171
|
|
|
159
172
|
|
|
160
173
|
def resolve_openhands_url() -> str:
|
|
174
|
+
if IS_CLOUD:
|
|
175
|
+
# Conversations are opened in the Canvas of the deployment itself.
|
|
176
|
+
return f"{_CLOUD_API_URL}/canvas"
|
|
161
177
|
try:
|
|
162
178
|
configured = get_secret("OPENHANDS_URL").rstrip()
|
|
163
179
|
except Exception:
|
|
@@ -184,12 +200,14 @@ def fire_callback(
|
|
|
184
200
|
body["error"] = error
|
|
185
201
|
if conversation_id:
|
|
186
202
|
body["conversation_id"] = conversation_id
|
|
203
|
+
# A cloud run is not given a callback key; it authenticates with its API key.
|
|
204
|
+
api_key = os.environ.get("AUTOMATION_CALLBACK_API_KEY") or os.environ.get("OPENHANDS_API_KEY", "")
|
|
187
205
|
req = urllib.request.Request(
|
|
188
206
|
url,
|
|
189
207
|
data=json.dumps(body).encode(),
|
|
190
208
|
headers={
|
|
191
209
|
"Content-Type": "application/json",
|
|
192
|
-
"Authorization": f"Bearer {
|
|
210
|
+
"Authorization": f"Bearer {api_key}",
|
|
193
211
|
},
|
|
194
212
|
)
|
|
195
213
|
try:
|
|
@@ -578,11 +596,100 @@ def _build_secrets_payload(agent_url: str, api_key: str) -> dict:
|
|
|
578
596
|
return secrets
|
|
579
597
|
|
|
580
598
|
|
|
599
|
+
def _cloud_request(method: str, path: str, body: dict | None = None):
|
|
600
|
+
"""Call the OpenHands API with the API key this cloud run was given."""
|
|
601
|
+
req = urllib.request.Request(
|
|
602
|
+
f"{_CLOUD_API_URL}{path}",
|
|
603
|
+
data=json.dumps(body).encode() if body is not None else None,
|
|
604
|
+
headers={
|
|
605
|
+
"Authorization": f"Bearer {os.environ.get('OPENHANDS_API_KEY', '')}",
|
|
606
|
+
"Content-Type": "application/json",
|
|
607
|
+
},
|
|
608
|
+
method=method,
|
|
609
|
+
)
|
|
610
|
+
try:
|
|
611
|
+
with urllib.request.urlopen(req) as r:
|
|
612
|
+
raw = r.read()
|
|
613
|
+
return json.loads(raw) if raw.strip() else {}
|
|
614
|
+
except urllib.error.HTTPError as exc:
|
|
615
|
+
body_text = exc.read().decode()
|
|
616
|
+
raise RuntimeError(f"OpenHands API {method} {path} → {exc.code}: {body_text}") from exc
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _create_cloud_conversation(initial_message: str, title: str) -> str:
|
|
620
|
+
"""Start a conversation through the OpenHands API and return its id.
|
|
621
|
+
|
|
622
|
+
OpenHands runs it in a sandbox of its own, which outlives this run's, with
|
|
623
|
+
the user's own model, MCP servers and secrets, so none of the local
|
|
624
|
+
forwarding in create_conversation applies. The API answers with the task
|
|
625
|
+
that starts the conversation, not with a started conversation.
|
|
626
|
+
"""
|
|
627
|
+
conversation_id = uuid.uuid4().hex
|
|
628
|
+
task = _cloud_request(
|
|
629
|
+
"POST",
|
|
630
|
+
"/api/v1/app-conversations",
|
|
631
|
+
{
|
|
632
|
+
"conversation_id": conversation_id,
|
|
633
|
+
"title": title,
|
|
634
|
+
"initial_message": {
|
|
635
|
+
"role": "user",
|
|
636
|
+
"content": [{"type": "text", "text": initial_message}],
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
)
|
|
640
|
+
if task.get("status") == "ERROR":
|
|
641
|
+
raise RuntimeError(f"Starting a conversation failed: {task.get('detail') or 'no detail'}")
|
|
642
|
+
return conversation_id
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _cloud_conversation(conv_id: str) -> dict | None:
|
|
646
|
+
"""The conversation's record, or None while OpenHands is still bringing it up."""
|
|
647
|
+
found = _cloud_request("GET", f"/api/v1/app-conversations?ids={conv_id}")
|
|
648
|
+
return found[0] if found else None
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _cloud_conversation_status(conv_id: str) -> str:
|
|
652
|
+
conversation = _cloud_conversation(conv_id)
|
|
653
|
+
if conversation is None:
|
|
654
|
+
return "starting"
|
|
655
|
+
if conversation.get("execution_status"):
|
|
656
|
+
return conversation["execution_status"]
|
|
657
|
+
# The status is only reported while the conversation's sandbox is running.
|
|
658
|
+
# Once it is paused, the last status the agent reported is in its events.
|
|
659
|
+
events = _cloud_request(
|
|
660
|
+
"GET",
|
|
661
|
+
f"/api/v1/conversation/{conv_id}/events/search"
|
|
662
|
+
"?kind__eq=ConversationStateUpdateEvent&sort_order=TIMESTAMP_DESC&limit=100",
|
|
663
|
+
)
|
|
664
|
+
for event in events.get("items", []):
|
|
665
|
+
if event.get("key") == "execution_status":
|
|
666
|
+
return event.get("value") or "unknown"
|
|
667
|
+
return "unknown"
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def _cloud_conversation_final_response(conv_id: str) -> str:
|
|
671
|
+
"""The agent's closing message, read from the conversation's stored events."""
|
|
672
|
+
events = _cloud_request(
|
|
673
|
+
"GET",
|
|
674
|
+
f"/api/v1/conversation/{conv_id}/events/search?sort_order=TIMESTAMP_DESC&limit=100",
|
|
675
|
+
)
|
|
676
|
+
for event in events.get("items", []):
|
|
677
|
+
if event.get("source") != "agent":
|
|
678
|
+
continue
|
|
679
|
+
if event.get("kind") == "ActionEvent" and event.get("tool_name") == "finish":
|
|
680
|
+
return (event.get("action") or {}).get("message") or ""
|
|
681
|
+
if event.get("kind") == "MessageEvent":
|
|
682
|
+
content = (event.get("llm_message") or {}).get("content") or []
|
|
683
|
+
return "".join(part.get("text", "") for part in content)
|
|
684
|
+
return ""
|
|
685
|
+
|
|
686
|
+
|
|
581
687
|
def create_conversation(
|
|
582
688
|
agent_url: str,
|
|
583
689
|
api_key: str,
|
|
584
690
|
initial_message: str,
|
|
585
691
|
agent: dict | None = None,
|
|
692
|
+
title: str = "",
|
|
586
693
|
) -> str:
|
|
587
694
|
"""Create a conversation and return its ID.
|
|
588
695
|
|
|
@@ -592,7 +699,13 @@ def create_conversation(
|
|
|
592
699
|
|
|
593
700
|
Inherits the user's secrets (as LookupSecret references) and MCP
|
|
594
701
|
server configuration so the spawned agent has the same capabilities.
|
|
702
|
+
|
|
703
|
+
On OpenHands Cloud and Enterprise the conversation is started through
|
|
704
|
+
the OpenHands API instead (see _create_cloud_conversation).
|
|
595
705
|
"""
|
|
706
|
+
if IS_CLOUD:
|
|
707
|
+
return _create_cloud_conversation(initial_message, title)
|
|
708
|
+
|
|
596
709
|
# Use a dedicated directory for spawned conversations rather than the
|
|
597
710
|
# automation run's WORKSPACE_BASE, which may be cleaned up between runs.
|
|
598
711
|
workspace_base = os.environ.get("WORKSPACE_BASE", "")
|
|
@@ -625,6 +738,15 @@ def create_conversation(
|
|
|
625
738
|
|
|
626
739
|
def send_to_conversation(agent_url: str, api_key: str, conv_id: str, text: str) -> None:
|
|
627
740
|
"""Send a user message to an existing conversation and resume the agent."""
|
|
741
|
+
if IS_CLOUD:
|
|
742
|
+
# Answers 409 while the conversation's sandbox is paused and 404 while
|
|
743
|
+
# OpenHands is still bringing it up; the caller logs either.
|
|
744
|
+
_cloud_request("POST", f"/api/v1/app-conversations/{conv_id}/send-message", {
|
|
745
|
+
"role": "user",
|
|
746
|
+
"content": [{"type": "text", "text": text}],
|
|
747
|
+
"run": True,
|
|
748
|
+
})
|
|
749
|
+
return
|
|
628
750
|
_oh_request(agent_url, api_key, "POST", f"/api/conversations/{conv_id}/events", {
|
|
629
751
|
"role": "user",
|
|
630
752
|
"content": [{"text": text}],
|
|
@@ -633,11 +755,15 @@ def send_to_conversation(agent_url: str, api_key: str, conv_id: str, text: str)
|
|
|
633
755
|
|
|
634
756
|
|
|
635
757
|
def conversation_status(agent_url: str, api_key: str, conv_id: str) -> str:
|
|
758
|
+
if IS_CLOUD:
|
|
759
|
+
return _cloud_conversation_status(conv_id)
|
|
636
760
|
result = _oh_request(agent_url, api_key, "GET", f"/api/conversations/{conv_id}")
|
|
637
761
|
return result.get("execution_status", "unknown")
|
|
638
762
|
|
|
639
763
|
|
|
640
764
|
def conversation_final_response(agent_url: str, api_key: str, conv_id: str) -> str:
|
|
765
|
+
if IS_CLOUD:
|
|
766
|
+
return _cloud_conversation_final_response(conv_id)
|
|
641
767
|
result = _oh_request(
|
|
642
768
|
agent_url, api_key, "GET", f"/api/conversations/{conv_id}/agent_final_response"
|
|
643
769
|
)
|
|
@@ -956,9 +1082,12 @@ def _process_trigger_message(
|
|
|
956
1082
|
)
|
|
957
1083
|
|
|
958
1084
|
try:
|
|
959
|
-
|
|
1085
|
+
# A cloud conversation runs with the user's own settings; its model is
|
|
1086
|
+
# read back from the conversation once it is up.
|
|
1087
|
+
agent = None if IS_CLOUD else _get_agent_dict(agent_url, api_key)
|
|
960
1088
|
conv_id = create_conversation(
|
|
961
|
-
agent_url, api_key, initial_prompt, agent=agent
|
|
1089
|
+
agent_url, api_key, initial_prompt, agent=agent,
|
|
1090
|
+
title=f"Slack: {request_part or text}"[:80],
|
|
962
1091
|
)
|
|
963
1092
|
conv_url = f"{openhands_url}/conversations/{conv_id}"
|
|
964
1093
|
|
|
@@ -969,7 +1098,7 @@ def _process_trigger_message(
|
|
|
969
1098
|
"thread_ts": thread_root,
|
|
970
1099
|
"status": "active",
|
|
971
1100
|
"llm_profile": "default",
|
|
972
|
-
"llm_model": agent["llm"].get("model") or "unknown",
|
|
1101
|
+
"llm_model": (agent["llm"].get("model") if agent else None) or "unknown",
|
|
973
1102
|
"last_activity": now,
|
|
974
1103
|
"last_seen_reply_ts": msg_ts,
|
|
975
1104
|
"reply_poll_backoff_seconds": THREAD_REPLY_INITIAL_BACKOFF_SECONDS,
|
|
@@ -1027,6 +1156,14 @@ def _check_conversation_completion(
|
|
|
1027
1156
|
)
|
|
1028
1157
|
else:
|
|
1029
1158
|
summary = final if final else "Success (no message available)."
|
|
1159
|
+
if IS_CLOUD:
|
|
1160
|
+
# The model is the conversation's own, known once OpenHands started it.
|
|
1161
|
+
try:
|
|
1162
|
+
rec["llm_model"] = (
|
|
1163
|
+
(_cloud_conversation(conv_id) or {}).get("llm_model") or rec.get("llm_model")
|
|
1164
|
+
)
|
|
1165
|
+
except Exception as exc:
|
|
1166
|
+
print(f" Warning: could not read the model for {conv_id}: {exc}")
|
|
1030
1167
|
# The values recorded when this script started the conversation; a
|
|
1031
1168
|
# thread follow-up reuses it, so they still describe its LLM. A record
|
|
1032
1169
|
# written before provenance was tracked cannot name either value.
|
|
@@ -1056,6 +1193,14 @@ def _check_conversation_completion(
|
|
|
1056
1193
|
|
|
1057
1194
|
def main() -> str | None:
|
|
1058
1195
|
"""Run one polling cycle. Returns the last conversation ID created, if any."""
|
|
1196
|
+
if IS_CLOUD and not _kv_available():
|
|
1197
|
+
# A cloud run starts in a fresh sandbox, so a state file would not
|
|
1198
|
+
# survive it and every run would start over.
|
|
1199
|
+
raise RuntimeError(
|
|
1200
|
+
"The automation KV store is required on OpenHands Cloud and Enterprise, "
|
|
1201
|
+
"but AUTOMATION_KV_TOKEN is not set for this run."
|
|
1202
|
+
)
|
|
1203
|
+
|
|
1059
1204
|
state = load_state()
|
|
1060
1205
|
|
|
1061
1206
|
agent_url = os.environ.get("AGENT_SERVER_URL", "").rstrip("/")
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "software-factory",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Set up a GitHub software factory fast: ask only for the repositories and the ticket source (GitHub issues, Jira or Linear), create a working issue-to-PR and PR-review pipeline from catalog automations with defaults, then let the user stop or extend it step by step.",
|
|
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
|
+
"onboarding",
|
|
14
|
+
"software-factory",
|
|
15
|
+
"github",
|
|
16
|
+
"automation",
|
|
17
|
+
"pull-request"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "software-factory",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Set up a GitHub software factory fast: ask only for the repositories and the ticket source (GitHub issues, Jira or Linear), create a working issue-to-PR and PR-review pipeline from catalog automations with defaults, then let the user stop or extend it step by step.",
|
|
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
|
+
"onboarding",
|
|
14
|
+
"software-factory",
|
|
15
|
+
"github",
|
|
16
|
+
"automation",
|
|
17
|
+
"pull-request"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "software-factory",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Set up a GitHub software factory fast: ask only for the repositories and the ticket source (GitHub issues, Jira or Linear), create a working issue-to-PR and PR-review pipeline from catalog automations with defaults, then let the user stop or extend it step by step.",
|
|
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
|
+
"onboarding",
|
|
14
|
+
"software-factory",
|
|
15
|
+
"github",
|
|
16
|
+
"automation",
|
|
17
|
+
"pull-request"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Software Factory
|
|
2
|
+
|
|
3
|
+
Set up a GitHub "software factory" fast, then grow it step by step. The skill
|
|
4
|
+
asks only for the repositories, creates a working pipeline from catalog
|
|
5
|
+
automations with their defaults, shows what runs, and lets the user stop there
|
|
6
|
+
or extend it.
|
|
7
|
+
|
|
8
|
+
## Trigger
|
|
9
|
+
|
|
10
|
+
This skill is activated by:
|
|
11
|
+
|
|
12
|
+
- `/software-factory`
|
|
13
|
+
- the phrase "software factory" (only when the skill is enabled)
|
|
14
|
+
|
|
15
|
+
## What it sets up
|
|
16
|
+
|
|
17
|
+
| Station | Catalog id | Phase |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| Issue to PR | `github-issue-to-pr`, or the Jira or Linear station | 1 |
|
|
20
|
+
| PR reviewer | `github-pr-reviewer` | 1 |
|
|
21
|
+
| AGENTS.md maintainer | `github-agents-md-maintainer` | 1 |
|
|
22
|
+
| Issue triage | `github-issue-triage` | add-on |
|
|
23
|
+
| Auto-merge watchdog | `github-delivery-watchdog` | add-on |
|
|
24
|
+
| Stale-CI PR closer | `github-stale-ci-pr-closer` | add-on |
|
|
25
|
+
| QA changes | `qa-changes` | add-on (webhooks only) |
|
|
26
|
+
|
|
27
|
+
`scripts/factory.py` (Python standard library, no LLM) does the catalog and API
|
|
28
|
+
work - preflight checks, bundle upload and create, in-place tune, smoke run and
|
|
29
|
+
enable - so Phase 1 takes four agent steps. The skill adds no automation logic of its own. Each station is the unmodified
|
|
30
|
+
catalog bundle from `automations/catalog/<id>/`, configured from its `setup`
|
|
31
|
+
block, so fixes to a station reach factories created by this skill too. A Jira
|
|
32
|
+
or Linear issue to PR station is created by its own skill instead.
|
|
33
|
+
|
|
34
|
+
## Flow
|
|
35
|
+
|
|
36
|
+
1. **Phase 1**: verify `GITHUB_PERSONAL_ACCESS_TOKEN` (asked for with the
|
|
37
|
+
`request_secret` tool if missing, never in chat), ask for the repositories
|
|
38
|
+
and the ticket source (GitHub issues, Jira or Linear),
|
|
39
|
+
create the three Phase 1 stations with defaults, smoke-run issue to PR and
|
|
40
|
+
the reviewer, and enable those that pass. The AGENTS.md maintainer is
|
|
41
|
+
enabled at once and opens an `AGENTS.md` pull request per repository.
|
|
42
|
+
2. **Checkpoint**: show the stations, their settings and how to use the
|
|
43
|
+
factory. The user can stop here or pick a next step: try it with a test
|
|
44
|
+
issue, tune settings, or add a station.
|
|
45
|
+
3. **Phase 2**: one round per pick, asking only the questions for it, then back
|
|
46
|
+
to the Checkpoint.
|
|
47
|
+
|
|
48
|
+
## Requirements
|
|
49
|
+
|
|
50
|
+
- An automation backend whose `/api/automation/v1/capabilities` reports the
|
|
51
|
+
features each station's manifest requires (`customTarball`, plus
|
|
52
|
+
`agentProfiles` for issue to PR, reviewer and triage; QA needs webhook delivery).
|
|
53
|
+
- A GitHub token with Contents, Issues, Pull requests and Workflows read and
|
|
54
|
+
write on every selected repository.
|
|
55
|
+
- An OpenHands (not ACP) agent profile that may use that token.
|
|
56
|
+
- Jira tickets: a connected Atlassian Rovo server or a Jira API token. Linear
|
|
57
|
+
tickets: a Linear connection.
|
|
58
|
+
|
|
59
|
+
## Scope
|
|
60
|
+
|
|
61
|
+
Code lives on GitHub. Tickets can come from GitHub issues, Jira (through the
|
|
62
|
+
`jira-issue-to-pr` skill) or Linear (through the `ticket-to-code-change`
|
|
63
|
+
skill); only the issue to PR station changes. GitLab and Bitbucket code hosts
|
|
64
|
+
are not part of this flow.
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: software-factory
|
|
3
|
+
description: >
|
|
4
|
+
Set up an OpenHands "software factory" on GitHub fast: ask only for the
|
|
5
|
+
repositories and where tickets live (GitHub issues, Jira or Linear), create a working issue-to-PR and PR-review pipeline from the
|
|
6
|
+
automation catalog with defaults, show what runs, then let the user stop or
|
|
7
|
+
extend it one step at a time (tune settings, triage, auto-merge, stale-CI and
|
|
8
|
+
QA add-ons).
|
|
9
|
+
triggers:
|
|
10
|
+
- /software-factory
|
|
11
|
+
- software factory
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Software Factory
|
|
15
|
+
|
|
16
|
+
Build a GitHub software factory from unmodified automation catalog bundles
|
|
17
|
+
(`automations/catalog/<id>/manifest.json`); a Jira or Linear station is created
|
|
18
|
+
by its own skill. This skill adds no automation logic.
|
|
19
|
+
|
|
20
|
+
Two phases: **Phase 1** gets a working factory after one question. **Phase 2**
|
|
21
|
+
extends it in short rounds, only as far as the user wants. After Phase 1 and
|
|
22
|
+
after every round, the **Checkpoint** shows what exists and lets the user stop.
|
|
23
|
+
|
|
24
|
+
Code lives on GitHub. Tickets can come from GitHub issues (default), Jira or
|
|
25
|
+
Linear: only the issue to PR station changes (see "Ticket source"). GitLab and
|
|
26
|
+
Bitbucket code hosts are not part of this flow.
|
|
27
|
+
|
|
28
|
+
## Stations
|
|
29
|
+
|
|
30
|
+
| Station | Catalog id | Phase | Needs agent profile |
|
|
31
|
+
|---|---|---|---|
|
|
32
|
+
| Issue to PR | `github-issue-to-pr`, or the Jira or Linear station | 1 | yes (GitHub issues only) |
|
|
33
|
+
| PR reviewer | `github-pr-reviewer` | 1 | yes |
|
|
34
|
+
| AGENTS.md maintainer | `github-agents-md-maintainer` | 1 | no |
|
|
35
|
+
| Issue triage | `github-issue-triage`, or `linear-triage-assistant` | add-on | yes (GitHub issues only) |
|
|
36
|
+
| Auto-merge watchdog | `github-delivery-watchdog` | add-on | no |
|
|
37
|
+
| Stale-CI PR closer | `github-stale-ci-pr-closer` | add-on | no |
|
|
38
|
+
| QA changes | `qa-changes` | add-on | no |
|
|
39
|
+
|
|
40
|
+
## Helper script
|
|
41
|
+
|
|
42
|
+
`scripts/factory.py` in this skill's directory does every catalog and API step
|
|
43
|
+
and prints one JSON document; it exits non-zero with `{"error": ...}` on
|
|
44
|
+
failure. Its usage is below: do not read its source, and do not load the
|
|
45
|
+
`openhands-automation` skill for Phase 1.
|
|
46
|
+
|
|
47
|
+
Set `OPENHANDS_HOST` to the **Ingress** URL in `<RUNTIME_SERVICES>` on a local
|
|
48
|
+
Agent Canvas (it serves both `/api/automation` and `/api`; the automation
|
|
49
|
+
backend URL does not), or to `<HOST>` on OpenHands Cloud. Run every command as:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
GITHUB_PERSONAL_ACCESS_TOKEN="$GITHUB_PERSONAL_ACCESS_TOKEN" \
|
|
53
|
+
OPENHANDS_AUTOMATION_API_KEY="$OPENHANDS_AUTOMATION_API_KEY" \
|
|
54
|
+
OPENHANDS_API_KEY="$OPENHANDS_API_KEY" \
|
|
55
|
+
python3 <this skill's directory>/scripts/factory.py <command> ...
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`$F` below stands for that whole line. Keep the secret names in it: the agent
|
|
59
|
+
server passes a saved secret into a command only when the command names it.
|
|
60
|
+
The script reads the token and never prints it.
|
|
61
|
+
|
|
62
|
+
| Command | Does |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `$F preflight --repos a/b,c/d` | Which stations this deployment supports, the agent profile, the GitHub login, push access per repository. |
|
|
65
|
+
| `$F create --repos a/b --ids ID,ID [--set FIELD=VALUE] [--set ID.FIELD=VALUE]` | Fills each station's form (answers, then derived values, then manifest defaults), uploads the bundle and creates it disabled. `FIELD=VALUE` applies to every listed station with that field. Stations that already exist are reported under `existing`, not recreated. |
|
|
66
|
+
| `$F tune --repos a/b --ids ID,ID [--set FIELD=VALUE] [--set ID.FIELD=VALUE]` | Applies new settings to an existing station by updating it in place. Use this to change a value that is fixed at creation. |
|
|
67
|
+
| `$F verify ID... [--no-wait ID...]` | Dispatches once, enables the ones whose run `COMPLETED` and disables the rest, reporting their `error_detail`. `--no-wait` enables without waiting. |
|
|
68
|
+
| `$F status ID...` | The latest run of each. |
|
|
69
|
+
| `$F prs --repos a/b [--branch-prefix P]` | Open pull requests whose branch starts with `P` (default `openhands/agents-md`, the AGENTS.md maintainer's). |
|
|
70
|
+
| `$F reset-token` | Deletes the saved `GITHUB_PERSONAL_ACCESS_TOKEN` (local Agent Canvas), so `request_secret` opens its form again. |
|
|
71
|
+
|
|
72
|
+
`create` and `verify` return each automation's `url`, its page in Agent Canvas.
|
|
73
|
+
Trust the script's output: do not open pages in the browser to check them, and
|
|
74
|
+
do not use the task tracker for this flow.
|
|
75
|
+
|
|
76
|
+
Add `--profile NAME` to `preflight` or `create` to use a profile other than the
|
|
77
|
+
active one.
|
|
78
|
+
|
|
79
|
+
## Rules
|
|
80
|
+
|
|
81
|
+
- **Supported stations**: create only those `preflight` reports `ok`; say why
|
|
82
|
+
the others are skipped.
|
|
83
|
+
- **Derived values** (the script fills them; never ask): every
|
|
84
|
+
`githubTokenSecret` is `GITHUB_PERSONAL_ACCESS_TOKEN`. The watchdog's
|
|
85
|
+
`branchPrefix` must equal issue to PR's: when it was changed, pass
|
|
86
|
+
`--set github-delivery-watchdog.branchPrefix=<same value>`.
|
|
87
|
+
- **Agent profile**: if `preflight` reports a profile error, skip the stations
|
|
88
|
+
that need one and point the user to the station's catalog card in Agent
|
|
89
|
+
Canvas (Automations), which has a profile picker.
|
|
90
|
+
- **Token**: never ask for its value in chat, echo it, or put it in a tool
|
|
91
|
+
argument or file.
|
|
92
|
+
- **Failures**: if `create` fails, its output lists the stations it already
|
|
93
|
+
created; report them with the error and ask whether to retry or delete them
|
|
94
|
+
(`DELETE ${OPENHANDS_HOST}/api/automation/v1/{id}`).
|
|
95
|
+
- **Existing stations**: a station `create` reports under `existing` was already
|
|
96
|
+
set up by an earlier run and was left unchanged. Say so rather than presenting
|
|
97
|
+
it as new, and change it with `$F tune` if the user wants different settings.
|
|
98
|
+
|
|
99
|
+
## Phase 1 - Working factory
|
|
100
|
+
|
|
101
|
+
1. Ask, in one message: "Which GitHub repositories (`owner/repo`)? Where do
|
|
102
|
+
tickets live: GitHub issues (default), Jira or Linear?" Say that everything
|
|
103
|
+
else uses defaults that can be changed afterwards. If the answer names only
|
|
104
|
+
repositories, use GitHub issues; do not ask again.
|
|
105
|
+
2. Run `$F preflight --repos <repos>`. Drop repositories without `push`, and
|
|
106
|
+
say which.
|
|
107
|
+
3. If `github_login` is null, act on `token_error`:
|
|
108
|
+
- `not_set`, and the `request_secret` tool is available: call it once with
|
|
109
|
+
`name: "GITHUB_PERSONAL_ACCESS_TOKEN"` and a `reason` asking for Contents,
|
|
110
|
+
Issues, Pull requests and Workflows read and write (classic PAT: `repo`,
|
|
111
|
+
`workflow`). End your turn, wait for the `[secret-request]` message,
|
|
112
|
+
then rerun `preflight`.
|
|
113
|
+
- `rejected`: GitHub refuses the saved token (invalid or expired), and
|
|
114
|
+
`request_secret` cannot replace it: it answers `saved` at once for a name
|
|
115
|
+
that exists. Say so in one sentence and ask "Shall I remove it and open
|
|
116
|
+
the form for a new one?" On yes, run `$F reset-token`, then handle it as
|
|
117
|
+
`not_set`. If `reset-token` fails, tell the user to replace it in
|
|
118
|
+
Settings -> Secrets and say when done.
|
|
119
|
+
- `not_set` without the tool: tell the user to add it in Settings ->
|
|
120
|
+
Secrets and stop.
|
|
121
|
+
4. `$F create --repos <repos> --ids github-pr-reviewer,github-agents-md-maintainer`,
|
|
122
|
+
adding `github-issue-to-pr` for GitHub issues. For Jira or Linear, also set
|
|
123
|
+
up the issue to PR station as "Ticket source" describes.
|
|
124
|
+
5. `$F verify <reviewer id> [<issue to PR id>] --no-wait <maintainer id>`. The
|
|
125
|
+
maintainer's run opens one `AGENTS.md` pull request per repository.
|
|
126
|
+
6. Go to the Checkpoint.
|
|
127
|
+
|
|
128
|
+
## Checkpoint
|
|
129
|
+
|
|
130
|
+
Write one short message, in this order:
|
|
131
|
+
|
|
132
|
+
1. One plain sentence on what runs and how to use it: "Your factory is running
|
|
133
|
+
on `<repos>`. Add the `<trigger label>` label to a ticket to get a draft pull
|
|
134
|
+
request; the reviewer reviews it and you merge it." (On Jira, add that the
|
|
135
|
+
ticket body must name the `owner/repo`.)
|
|
136
|
+
2. The `AGENTS.md` pull requests from `$F prs --repos <repos>`, asking the user
|
|
137
|
+
to merge them: they give the build agent the repository's build, test and
|
|
138
|
+
lint commands. One opened before this setup started (`created_at`) is
|
|
139
|
+
"still open from an earlier run", not new; the maintainer works once a week
|
|
140
|
+
per repository. If there are none yet, say they are on their way.
|
|
141
|
+
3. A small table of the automations, each linked to its `url`:
|
|
142
|
+
|
|
143
|
+
| Automation | Schedule | Status |
|
|
144
|
+
|---|---|---|
|
|
145
|
+
| [Issue to PR](<url>) | every 15 minutes | on |
|
|
146
|
+
|
|
147
|
+
Status is `on`, or `off` with the reason in a few words. No automation ids.
|
|
148
|
+
4. The settings in effect, as a short list (the Tune table's current values).
|
|
149
|
+
Mention that the reviewer also reviews any open, non-draft, green pull
|
|
150
|
+
request with no review yet (at most 2 new per scan).
|
|
151
|
+
5. End with only a list of possible next steps, one line each, and nothing
|
|
152
|
+
after it. Mark the first one not done yet as recommended:
|
|
153
|
+
- **Try it**: I open a small test issue with the `<trigger label>` label in
|
|
154
|
+
`<repo>`, only if you say so, and report the pull request.
|
|
155
|
+
- **Tune**: change a setting from the list above.
|
|
156
|
+
- **Add a station**: triage, auto-merge watchdog, stale-CI closer (QA only
|
|
157
|
+
if `preflight` reports it `ok`).
|
|
158
|
+
- **Stop here**: the factory keeps running as it is.
|
|
159
|
+
|
|
160
|
+
## Phase 2 - Extend, one round at a time
|
|
161
|
+
|
|
162
|
+
Do only what the user picked. Ask only the questions for it, in one message,
|
|
163
|
+
each with its current value. Apply, then return to the Checkpoint.
|
|
164
|
+
|
|
165
|
+
### Tune
|
|
166
|
+
|
|
167
|
+
| Setting | Default | `--set` |
|
|
168
|
+
|---|---|---|
|
|
169
|
+
| Issue trigger label (GitHub issues) | `openhands` | `github-issue-to-pr.triggerLabel` |
|
|
170
|
+
| Draft or ready pull requests | `draft` | `pullRequestMode` (issue to PR, AGENTS.md maintainer) |
|
|
171
|
+
| Branch prefix (GitHub issues) | `openhands/issue` | `github-issue-to-pr.branchPrefix` (and the watchdog's, if present) |
|
|
172
|
+
| Which pull requests get reviewed | label `openhands-review`, reviewer `all-hands-bot` | `github-pr-reviewer.triggerLabel`, `triggerReviewer` |
|
|
173
|
+
| Review tone: concise, thorough, friendly | `concise` | `reviewTone` |
|
|
174
|
+
| Human reviewers requested after agent approval (two or more logins) | none | `maintainers` |
|
|
175
|
+
| Schedules and timezone | issue to PR `*/15 * * * *`, reviewer `*/5 * * * *`, AGENTS.md maintainer `0 9 * * 1`, add-ons per manifest; `UTC` | `ID.schedule`, `timezone` |
|
|
176
|
+
| Agent profile | active profile | `--profile NAME` |
|
|
177
|
+
|
|
178
|
+
Apply:
|
|
179
|
+
|
|
180
|
+
- Schedule or timezone only:
|
|
181
|
+
`PATCH ${OPENHANDS_HOST}/api/automation/v1/{id}` with `{"trigger": {...}}`.
|
|
182
|
+
- Jira or Linear station settings: change them as its skill describes.
|
|
183
|
+
- Anything else is fixed at creation: `$F tune --repos <repos> --ids <id>` with
|
|
184
|
+
the new `--set` values (repeat every earlier non-default value), then
|
|
185
|
+
`$F verify <id>`. `tune` updates the station in place, so there is no
|
|
186
|
+
replacement to delete and no template-id collision.
|
|
187
|
+
|
|
188
|
+
### Add a station
|
|
189
|
+
|
|
190
|
+
Ask only the question below, then `$F create` it and `$F verify` it.
|
|
191
|
+
|
|
192
|
+
- **Auto-merge watchdog** (`github-delivery-watchdog`, GitHub issues only,
|
|
193
|
+
since it merges only branches with the issue to PR prefix): confirm the user
|
|
194
|
+
wants the agent to merge pull requests itself once review, tests and CI are
|
|
195
|
+
green.
|
|
196
|
+
- **Stale-CI closer** (`github-stale-ci-pr-closer`): confirm it may warn on,
|
|
197
|
+
then close, pull requests whose CI keeps failing.
|
|
198
|
+
- **Triage** (`github-issue-triage`): nothing. For Linear, follow the
|
|
199
|
+
`linear-triage` skill instead; for Jira there is none.
|
|
200
|
+
- **QA changes**: which base branch (default `main`). Offer QA only if
|
|
201
|
+
`preflight` reports it `ok` and the `openhands-automation` webhook
|
|
202
|
+
reachability check passes. It has no bundle, so the script cannot create it:
|
|
203
|
+
create it as the `openhands-automation` skill describes for an
|
|
204
|
+
event-triggered prompt preset, with its manifest `prompt` and `filter`, one
|
|
205
|
+
per repository (it takes a single `repository`). It runs only on pull request
|
|
206
|
+
events, so leave it enabled and skip `verify`.
|
|
207
|
+
|
|
208
|
+
## Ticket source
|
|
209
|
+
|
|
210
|
+
- **GitHub issues**: `github-issue-to-pr`, created with the others in Phase 1.
|
|
211
|
+
- **Jira**: follow the `jira-issue-to-pr` skill with `git_provider` `github`
|
|
212
|
+
and its defaults (label `create-pr`, `*/5 * * * *`). Ask only what it cannot
|
|
213
|
+
look up (the Jira site if several, or the API-token details when Atlassian
|
|
214
|
+
Rovo is not connected). Tell the user each ticket body must name its
|
|
215
|
+
`owner/repo`.
|
|
216
|
+
- **Linear**: follow the `ticket-to-code-change` skill with GitHub as the
|
|
217
|
+
provider. Ask its questions in one message, proposing the repositories above
|
|
218
|
+
and `*/15 * * * *` as defaults.
|
|
219
|
+
|
|
220
|
+
Once its skill has created the station, run `$F verify <id>` on it.
|
|
221
|
+
|
|
222
|
+
## Troubleshooting
|
|
223
|
+
|
|
224
|
+
| Symptom | Cause and fix |
|
|
225
|
+
|---|---|
|
|
226
|
+
| Create returns 422 | A value does not match the manifest `setup` block; fix the field at the path the error names. |
|
|
227
|
+
| Issue to PR fails at push | The token lacks Contents or Workflows write, or does not cover that repository. |
|
|
228
|
+
| Reviewer never reviews | The pull request has neither the trigger label nor a request for the trigger reviewer, and is not green. |
|
|
229
|
+
| Watchdog never merges | Its branch prefix differs from issue to PR's, or the pull request lacks an independent approval or green CI. |
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
# auto-generated by sync_extensions.py
|
|
3
|
+
description: Set up an OpenHands "software factory" on GitHub fast: ask only for the repositories and where tickets live (GitHub issues, Jira or Linear), create a working issue-to-PR and PR-review pipeline from the automation catalog with defaults, show what runs, then let the user stop or extend it one step at a time (tune settings, triage, auto-merge, stale-CI and QA add-ons).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Read and follow the complete instructions in the SKILL.md file located in this skill's directory.
|
|
7
|
+
|
|
8
|
+
$ARGUMENTS
|