@openhands/extensions 0.30.0 → 0.32.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 +14 -0
- package/.codex-plugin/marketplace.json +14 -0
- package/.github/workflows/pr-artifacts.yml +4 -83
- package/.github/workflows/tests.yml +13 -0
- package/.plugin/marketplace.json +14 -0
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +4 -3
- package/README.md +3 -2
- package/automations/README.md +9 -2
- package/automations/bundle-index.js +1 -1
- package/automations/catalog/github-pr-reviewer/manifest.json +2 -2
- package/automations/catalog/incident-retrospective-drafter/manifest.json +4 -0
- package/automations/catalog/jira-issue-to-bitbucket-pr/manifest.json +3 -0
- package/automations/catalog/jira-issue-to-gitlab-mr/manifest.json +3 -0
- package/automations/catalog/jira-issue-to-pr/manifest.json +3 -0
- package/automations/catalog/linear-issue-to-bitbucket-pr/manifest.json +4 -0
- package/automations/catalog/linear-issue-to-github-pr/manifest.json +4 -0
- package/automations/catalog/linear-issue-to-gitlab-mr/manifest.json +4 -0
- package/automations/catalog/linear-triage-assistant/manifest.json +4 -0
- package/automations/catalog/research-brief-writer/manifest.json +3 -0
- package/automations/catalog/slack-channel-monitor/manifest.json +3 -0
- package/automations/catalog/slack-standup-digest/manifest.json +3 -0
- package/automations/catalog.schema.json +18 -1
- package/automations/index.d.ts +8 -0
- package/marketplaces/openhands-extensions.json +14 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/python/openhands_extensions/_version.py +1 -1
- package/skills/bitbucket-data-center/SKILL.md +26 -9
- 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 +15 -4
- 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
|
@@ -31,8 +31,10 @@ On every subsequent run:
|
|
|
31
31
|
- Completed conversations stay in a short follow-up watch window, allowing
|
|
32
32
|
triggered Slack replies to continue the same OpenHands conversation.
|
|
33
33
|
|
|
34
|
-
>
|
|
35
|
-
>
|
|
34
|
+
> Runs on a local Agent Canvas (`dev:automation` stack) and on OpenHands Cloud
|
|
35
|
+
> and Enterprise. On Cloud and Enterprise each conversation is started through
|
|
36
|
+
> the OpenHands API in a sandbox of its own, with the user's own settings, and
|
|
37
|
+
> the automation keeps its state in the automation service's KV store.
|
|
36
38
|
|
|
37
39
|
---
|
|
38
40
|
|
|
@@ -66,7 +68,7 @@ function without Slack credentials.
|
|
|
66
68
|
|
|
67
69
|
| Secret name | Default | Purpose |
|
|
68
70
|
|---|---|---|
|
|
69
|
-
| `OPENHANDS_URL` | `http://localhost:8000` | Base URL used to build conversation links posted in Slack |
|
|
71
|
+
| `OPENHANDS_URL` | `http://localhost:8000` | Base URL used to build conversation links posted in Slack (local Agent Canvas only; on OpenHands Cloud and Enterprise the links open the deployment's own Canvas) |
|
|
70
72
|
|
|
71
73
|
---
|
|
72
74
|
|
|
@@ -165,23 +167,27 @@ repair a hand-written variant.
|
|
|
165
167
|
|
|
166
168
|
### Step 4 - Package and upload
|
|
167
169
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
-
|
|
170
|
+
Set `OPENHANDS_HOST` and `AUTH_HEADER` for the commands below:
|
|
171
|
+
- **OpenHands Cloud or Enterprise** - a `<HOST>` value is in your system prompt:
|
|
172
|
+
use it as `OPENHANDS_HOST`, with `AUTH_HEADER="Authorization: Bearer $OPENHANDS_API_KEY"`
|
|
173
|
+
- **Local Agent Canvas** - an **Automation backend** is listed in the
|
|
174
|
+
`<RUNTIME_SERVICES>` block of your system context: use its `url_from_agent` as
|
|
175
|
+
`OPENHANDS_HOST`, with `AUTH_HEADER="X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY"`
|
|
172
176
|
|
|
173
|
-
If
|
|
174
|
-
|
|
177
|
+
If neither is available, stop and tell the user to start the full automation
|
|
178
|
+
stack.
|
|
179
|
+
|
|
180
|
+
On OpenHands Cloud or Enterprise each run starts in a fresh sandbox, so the
|
|
181
|
+
automation keeps its state in the KV store: check that `kvStore` is listed under
|
|
182
|
+
`features` in `GET ${OPENHANDS_HOST}/api/automation/v1/capabilities` before
|
|
183
|
+
deploying.
|
|
175
184
|
|
|
176
185
|
```bash
|
|
177
186
|
tar -czf /tmp/slack-monitor.tar.gz -C /tmp/slack-monitor-build .
|
|
178
187
|
|
|
179
|
-
# OPENHANDS_HOST: read from <RUNTIME_SERVICES> Automation backend url_from_agent
|
|
180
|
-
OPENHANDS_HOST="<automation-url-from-runtime-services>"
|
|
181
|
-
|
|
182
188
|
TARBALL_PATH=$(curl -s -X POST \
|
|
183
189
|
"${OPENHANDS_HOST}/api/automation/v1/uploads?name=slack-channel-monitor" \
|
|
184
|
-
-H "
|
|
190
|
+
-H "$AUTH_HEADER" \
|
|
185
191
|
-H "Content-Type: application/gzip" \
|
|
186
192
|
--data-binary @/tmp/slack-monitor.tar.gz \
|
|
187
193
|
| python3 -c "import json,sys; print(json.load(sys.stdin)['tarball_path'])")
|
|
@@ -196,7 +202,7 @@ If the upload fails with a size error, the tarball must be under 1 MB.
|
|
|
196
202
|
|
|
197
203
|
```bash
|
|
198
204
|
curl -s -X POST "${OPENHANDS_HOST}/api/automation/v1" \
|
|
199
|
-
-H "
|
|
205
|
+
-H "$AUTH_HEADER" \
|
|
200
206
|
-H "Content-Type: application/json" \
|
|
201
207
|
-d "{
|
|
202
208
|
\"name\": \"Slack Channel Monitor\",
|
|
@@ -221,7 +227,8 @@ Tell the user:
|
|
|
221
227
|
> - Channels: `{channel list}`
|
|
222
228
|
> - Trigger phrase: `{phrase}`
|
|
223
229
|
> - Polling every minute via cron `* * * * *`
|
|
224
|
-
> - State
|
|
230
|
+
> - State: the automation service's KV store (a local Agent Canvas without one
|
|
231
|
+
> uses `~/.openhands/workspaces/automation-state/slack_poller_{id}.json`)
|
|
225
232
|
>
|
|
226
233
|
> Send a message containing `{phrase}` in any monitored channel to test it.
|
|
227
234
|
> The bot will react with 👀 and reply with a link to the new conversation.
|
|
@@ -233,7 +240,8 @@ Tell the user:
|
|
|
233
240
|
Each cron run executes `main.py`, which runs **10 polling iterations** (every
|
|
234
241
|
5 seconds) within the 55-second timeout window. Each iteration:
|
|
235
242
|
|
|
236
|
-
1. **Loads state** from the JSON file
|
|
243
|
+
1. **Loads state** from the KV store, or from the JSON file on a local Agent
|
|
244
|
+
Canvas without one (see `references/state-schema.md`).
|
|
237
245
|
2. **Resolves the Slack token** - checks `SLACK_USER_TOKEN` then `SLACK_BOT_TOKEN`.
|
|
238
246
|
3. **Fetches new messages:**
|
|
239
247
|
- User token + `search:read` + > 1 channel → single `search.messages` call
|
|
@@ -253,6 +261,10 @@ Each cron run executes `main.py`, which runs **10 polling iterations** (every
|
|
|
253
261
|
a new conversation, post link.
|
|
254
262
|
- Records `default` and the concrete model from the server settings already
|
|
255
263
|
used to start the conversation, for the response footer.
|
|
264
|
+
- On OpenHands Cloud and Enterprise the conversation is started through the
|
|
265
|
+
OpenHands API in a sandbox of its own, with the user's own settings; the
|
|
266
|
+
link opens it in the deployment's Canvas, and the footer names the model
|
|
267
|
+
the conversation reports once it is up.
|
|
256
268
|
- Thread replies: agent receives full thread history for context.
|
|
257
269
|
- Root messages: agent receives the trigger text only.
|
|
258
270
|
6. **Checks conversation statuses** - for each active conversation where
|
|
@@ -301,7 +313,7 @@ Debug output is written to both stdout and a persistent log at:
|
|
|
301
313
|
| `not_in_channel` error in run logs | Bot token used but bot not a member | Invite bot or switch to user token |
|
|
302
314
|
| `missing_scope` error | Token lacks required scopes | Re-install Slack app with correct scopes (see `references/slack-api.md`) |
|
|
303
315
|
| No messages detected | `last_poll` timestamp is in the future | Delete the state file to reset; it will be recreated on next run |
|
|
304
|
-
| Conversation link 404 | `OPENHANDS_URL` points to wrong host | Set the `OPENHANDS_URL` secret to the correct base URL |
|
|
316
|
+
| Conversation link 404 | `OPENHANDS_URL` points to wrong host (local Agent Canvas) | Set the `OPENHANDS_URL` secret to the correct base URL |
|
|
305
317
|
| Summary never posted | Conversation stuck in `running` state | Check conversation in the OpenHands UI; the agent may need intervention |
|
|
306
318
|
| Duplicate conversations created | `processed_ts` state missing or corrupted | Delete the state file to reset; dedup will rebuild on next run |
|
|
307
319
|
| Trigger message processed on each cron run | State file deleted between runs | Ensure `automation-state/` directory is persistent across runs |
|
|
@@ -31,13 +31,17 @@ Select-String -Path $main -Pattern 'def create_conversation'
|
|
|
31
31
|
|
|
32
32
|
## Package and Upload
|
|
33
33
|
|
|
34
|
+
Set `$env:OPENHANDS_HOST` and `$env:AUTH_HEADER` as SKILL.md Step 4 describes:
|
|
35
|
+
`"Authorization: Bearer $env:OPENHANDS_API_KEY"` on OpenHands Cloud or Enterprise,
|
|
36
|
+
`"X-Session-API-Key: $env:OPENHANDS_AUTOMATION_API_KEY"` on a local Agent Canvas.
|
|
37
|
+
|
|
34
38
|
```powershell
|
|
35
39
|
$tarball = Join-Path $env:TEMP "slack-monitor.tar.gz"
|
|
36
40
|
tar.exe -czf $tarball -C $buildDir .
|
|
37
41
|
|
|
38
42
|
$upload = curl.exe -s -X POST `
|
|
39
43
|
"$env:OPENHANDS_HOST/api/automation/v1/uploads?name=slack-channel-monitor" `
|
|
40
|
-
-H
|
|
44
|
+
-H $env:AUTH_HEADER `
|
|
41
45
|
-H "Content-Type: application/gzip" `
|
|
42
46
|
--data-binary "@$tarball" | ConvertFrom-Json
|
|
43
47
|
|
|
@@ -58,7 +62,7 @@ $body = @"
|
|
|
58
62
|
"@
|
|
59
63
|
|
|
60
64
|
curl.exe -s -X POST "$env:OPENHANDS_HOST/api/automation/v1" `
|
|
61
|
-
-H
|
|
65
|
+
-H $env:AUTH_HEADER `
|
|
62
66
|
-H "Content-Type: application/json" `
|
|
63
67
|
-d $body
|
|
64
68
|
```
|
|
@@ -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.
|