@openhands/extensions 0.29.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 +47 -5
- package/.claude-plugin/marketplace.json +20 -5
- package/.codex-plugin/marketplace.json +20 -5
- package/.github/workflows/build-vscode-app.yml +3 -3
- package/.github/workflows/check-extensions.yml +1 -1
- package/.github/workflows/pr-artifacts.yml +4 -83
- package/.github/workflows/tests.yml +14 -1
- package/.plugin/marketplace.json +20 -5
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +4 -3
- package/README.md +6 -5
- package/apps/vscode/README.md +8 -6
- package/apps/vscode/canvas-extension.json +11 -3
- package/apps/vscode/extension.js +60 -12
- package/apps/vscode/scripts/package.mjs +20 -74
- package/automations/bundle-index.js +10 -8
- package/automations/catalog/github-issue-to-pr/manifest.json +1 -1
- package/automations/catalog/github-issue-triage/manifest.json +4 -3
- package/automations/catalog/github-pr-reviewer/manifest.json +7 -6
- package/automations/catalog/gitlab-issue-to-mr/manifest.json +3 -3
- package/automations/catalog/jira-issue-to-bitbucket-pr/manifest.json +4 -4
- package/automations/catalog/jira-issue-to-gitlab-mr/manifest.json +5 -5
- package/automations/catalog/jira-issue-to-pr/manifest.json +1 -1
- package/integrations/catalog/atlassian-rovo.json +4 -3
- package/integrations/catalog/bitbucket.json +16 -1
- package/integrations/catalog/github.json +1 -0
- package/integrations/catalog/gitlab.json +1 -1
- package/marketplaces/openhands-extensions.json +20 -5
- package/package.json +1 -1
- package/plugins/pr-review/README.md +1 -0
- package/plugins/pr-review/scripts/prompt.py +44 -11
- package/plugins/runtime-api-configs/SKILL.md +1 -1
- package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +224 -8
- package/pyproject.toml +1 -1
- package/python/openhands_extensions/_version.py +1 -1
- package/skills/canvas-extension-api/references/backend-safety.md +3 -3
- package/skills/canvas-extension-api/references/v1-contract.md +7 -4
- package/skills/canvas-extension-api/scripts/validate-extension.mjs +17 -1
- package/skills/canvas-extension-api/scripts/validate-extension.test.mjs +49 -0
- package/skills/github/scripts/agent_conversation.py +224 -8
- package/skills/github-issue-to-pr/scripts/agent_conversation.py +224 -8
- package/skills/github-issue-triage/README.md +73 -14
- package/skills/github-issue-triage/SKILL.md +26 -46
- package/skills/github-issue-triage/scripts/agent_conversation.py +224 -8
- package/skills/github-issue-triage/scripts/triage_publication.py +293 -0
- package/skills/github-issue-triage/scripts/worker.py +153 -103
- package/skills/github-pr-reviewer/README.md +109 -1
- package/skills/github-pr-reviewer/SKILL.md +42 -8
- package/skills/github-pr-reviewer/references/state-schema.md +8 -0
- package/skills/github-pr-reviewer/scripts/agent_conversation.py +224 -8
- package/skills/github-pr-reviewer/scripts/main.py +163 -39
- package/skills/github-pr-reviewer/scripts/publication.py +193 -0
- package/skills/github-pr-reviewer/scripts/worker.py +297 -8
- package/skills/github-pr-reviewer/tests/test_main.py +73 -5
- package/skills/github-stale-ci-pr-closer/scripts/worker.py +8 -2
- package/skills/gitlab-issue-to-mr/README.md +10 -3
- package/skills/gitlab-issue-to-mr/SKILL.md +43 -9
- package/skills/gitlab-issue-to-mr/references/state-schema.md +4 -0
- package/skills/gitlab-issue-to-mr/scripts/main.py +359 -25
- package/skills/index.js +18 -8
- package/skills/jira-issue-to-pr/.claude-plugin/plugin.json +4 -1
- package/skills/jira-issue-to-pr/.codex-plugin/plugin.json +4 -1
- package/skills/jira-issue-to-pr/.plugin/plugin.json +4 -1
- package/skills/jira-issue-to-pr/README.md +18 -11
- package/skills/jira-issue-to-pr/SKILL.md +72 -23
- package/skills/jira-issue-to-pr/references/setup.md +43 -12
- package/skills/jira-issue-to-pr/scripts/main.py +136 -26
- package/skills/slack-channel-monitor/README.md +12 -3
- package/skills/slack-channel-monitor/SKILL.md +35 -20
- package/skills/slack-channel-monitor/references/windows.md +6 -2
- package/skills/slack-channel-monitor/scripts/main.py +189 -12
- 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/skills/ticket-to-code-change/.claude-plugin/plugin.json +1 -2
- package/skills/ticket-to-code-change/.codex-plugin/plugin.json +1 -2
- package/skills/ticket-to-code-change/.plugin/plugin.json +1 -2
- package/skills/ticket-to-code-change/SKILL.md +5 -3
- package/skills/ticket-to-code-change/commands/ticket-to-code-change-setup.md +1 -1
- package/tests/fixtures/automations/github-pr-reviewer.json +9 -9
- package/tests/test_agent_conversation_dispatch.py +173 -6
- package/tests/test_automation_llm_provenance.py +1209 -0
- package/tests/test_github_review_publication.py +422 -0
- package/tests/test_github_reviewer_delivery.py +174 -21
- package/tests/test_github_stale_ci_pr_closer.py +52 -1
- package/tests/test_github_triage_delivery.py +411 -238
- package/tests/test_gitlab_issue_to_mr.py +284 -0
- package/tests/test_jira_issue_to_pr.py +200 -6
- package/tests/test_pr_review_prompt_injection.py +353 -0
- package/tests/test_slack_channel_monitor.py +32 -1
- package/tests/test_software_factory_script.py +231 -0
- package/tests/test_vscode_app.py +8 -2
- package/tests/vscode-app.test.mjs +8 -2
|
@@ -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
|
|
@@ -251,14 +259,21 @@ Each cron run executes `main.py`, which runs **10 polling iterations** (every
|
|
|
251
259
|
ignored.
|
|
252
260
|
- Contains trigger phrase outside a tracked conversation → 👀 reaction, create
|
|
253
261
|
a new conversation, post link.
|
|
262
|
+
- Records `default` and the concrete model from the server settings already
|
|
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.
|
|
254
268
|
- Thread replies: agent receives full thread history for context.
|
|
255
269
|
- Root messages: agent receives the trigger text only.
|
|
256
270
|
6. **Checks conversation statuses** - for each active conversation where
|
|
257
271
|
`time.time() - last_activity > 15 s`:
|
|
258
272
|
- If status is `idle`, `finished`, `error`, or `stuck` → fetch the agent's
|
|
259
|
-
final response via `/api/conversations/{id}/agent_final_response
|
|
260
|
-
it to the Slack thread using Slack's
|
|
261
|
-
formatting renders correctly. Mark the
|
|
273
|
+
final response via `/api/conversations/{id}/agent_final_response`, append
|
|
274
|
+
the LLM profile and model, and post it to the Slack thread using Slack's
|
|
275
|
+
`markdown_text` field so Markdown formatting renders correctly. Mark the
|
|
276
|
+
record `watching` for five minutes
|
|
262
277
|
so triggered follow-up replies can continue the same conversation.
|
|
263
278
|
7. **Advances `last_poll`** to `now - 10 s` (overlap window prevents boundary
|
|
264
279
|
races). If a conversation creation failed, pins `last_poll` further back to
|
|
@@ -298,7 +313,7 @@ Debug output is written to both stdout and a persistent log at:
|
|
|
298
313
|
| `not_in_channel` error in run logs | Bot token used but bot not a member | Invite bot or switch to user token |
|
|
299
314
|
| `missing_scope` error | Token lacks required scopes | Re-install Slack app with correct scopes (see `references/slack-api.md`) |
|
|
300
315
|
| No messages detected | `last_poll` timestamp is in the future | Delete the state file to reset; it will be recreated on next run |
|
|
301
|
-
| 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 |
|
|
302
317
|
| Summary never posted | Conversation stuck in `running` state | Check conversation in the OpenHands UI; the agent may need intervention |
|
|
303
318
|
| Duplicate conversations created | `processed_ts` state missing or corrupted | Delete the state file to reset; dedup will rebuild on next run |
|
|
304
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,17 +30,20 @@ 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
|
"""
|
|
33
38
|
|
|
34
39
|
import json
|
|
35
40
|
import os
|
|
41
|
+
import re
|
|
36
42
|
import sys
|
|
37
43
|
import time
|
|
38
44
|
import urllib.error
|
|
39
45
|
import urllib.request
|
|
46
|
+
import uuid
|
|
40
47
|
from datetime import datetime, timezone
|
|
41
48
|
from urllib.parse import urlencode
|
|
42
49
|
|
|
@@ -100,6 +107,12 @@ MAX_THREAD_REPLY_POLLS_PER_RUN = 1
|
|
|
100
107
|
|
|
101
108
|
# ── Stdlib helpers ─────────────────────────────────────────────────────────────
|
|
102
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
|
+
|
|
103
116
|
def _get_env_key() -> str:
|
|
104
117
|
return (
|
|
105
118
|
os.environ.get("SESSION_API_KEY")
|
|
@@ -109,13 +122,14 @@ def _get_env_key() -> str:
|
|
|
109
122
|
|
|
110
123
|
|
|
111
124
|
def get_secret(name: str) -> str:
|
|
112
|
-
"""Fetch a named secret from the agent server."""
|
|
113
|
-
url = os.environ.get("AGENT_SERVER_URL", "").rstrip("/")
|
|
125
|
+
"""Fetch a named secret from the agent server, or from the OpenHands API on cloud."""
|
|
114
126
|
key = _get_env_key()
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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})
|
|
119
133
|
with urllib.request.urlopen(req) as r:
|
|
120
134
|
return r.read().decode().strip()
|
|
121
135
|
|
|
@@ -157,6 +171,9 @@ def _ngrok_public_openhands_url() -> str | None:
|
|
|
157
171
|
|
|
158
172
|
|
|
159
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"
|
|
160
177
|
try:
|
|
161
178
|
configured = get_secret("OPENHANDS_URL").rstrip()
|
|
162
179
|
except Exception:
|
|
@@ -183,12 +200,14 @@ def fire_callback(
|
|
|
183
200
|
body["error"] = error
|
|
184
201
|
if conversation_id:
|
|
185
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", "")
|
|
186
205
|
req = urllib.request.Request(
|
|
187
206
|
url,
|
|
188
207
|
data=json.dumps(body).encode(),
|
|
189
208
|
headers={
|
|
190
209
|
"Content-Type": "application/json",
|
|
191
|
-
"Authorization": f"Bearer {
|
|
210
|
+
"Authorization": f"Bearer {api_key}",
|
|
192
211
|
},
|
|
193
212
|
)
|
|
194
213
|
try:
|
|
@@ -577,7 +596,101 @@ def _build_secrets_payload(agent_url: str, api_key: str) -> dict:
|
|
|
577
596
|
return secrets
|
|
578
597
|
|
|
579
598
|
|
|
580
|
-
def
|
|
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
|
+
|
|
687
|
+
def create_conversation(
|
|
688
|
+
agent_url: str,
|
|
689
|
+
api_key: str,
|
|
690
|
+
initial_message: str,
|
|
691
|
+
agent: dict | None = None,
|
|
692
|
+
title: str = "",
|
|
693
|
+
) -> str:
|
|
581
694
|
"""Create a conversation and return its ID.
|
|
582
695
|
|
|
583
696
|
The server auto-starts the agent when initial_message is provided
|
|
@@ -586,7 +699,13 @@ def create_conversation(agent_url: str, api_key: str, initial_message: str) -> s
|
|
|
586
699
|
|
|
587
700
|
Inherits the user's secrets (as LookupSecret references) and MCP
|
|
588
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).
|
|
589
705
|
"""
|
|
706
|
+
if IS_CLOUD:
|
|
707
|
+
return _create_cloud_conversation(initial_message, title)
|
|
708
|
+
|
|
590
709
|
# Use a dedicated directory for spawned conversations rather than the
|
|
591
710
|
# automation run's WORKSPACE_BASE, which may be cleaned up between runs.
|
|
592
711
|
workspace_base = os.environ.get("WORKSPACE_BASE", "")
|
|
@@ -597,10 +716,9 @@ def create_conversation(agent_url: str, api_key: str, initial_message: str) -> s
|
|
|
597
716
|
workspace_dir = os.path.join(root, "slack-monitor-conversations")
|
|
598
717
|
os.makedirs(workspace_dir, exist_ok=True)
|
|
599
718
|
|
|
600
|
-
agent = _get_agent_dict(agent_url, api_key)
|
|
601
719
|
payload: dict = {
|
|
602
720
|
"workspace": {"working_dir": workspace_dir},
|
|
603
|
-
"agent": agent,
|
|
721
|
+
"agent": agent or _get_agent_dict(agent_url, api_key),
|
|
604
722
|
"initial_message": {"content": [{"text": initial_message}]},
|
|
605
723
|
}
|
|
606
724
|
|
|
@@ -620,6 +738,15 @@ def create_conversation(agent_url: str, api_key: str, initial_message: str) -> s
|
|
|
620
738
|
|
|
621
739
|
def send_to_conversation(agent_url: str, api_key: str, conv_id: str, text: str) -> None:
|
|
622
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
|
|
623
750
|
_oh_request(agent_url, api_key, "POST", f"/api/conversations/{conv_id}/events", {
|
|
624
751
|
"role": "user",
|
|
625
752
|
"content": [{"text": text}],
|
|
@@ -628,17 +755,35 @@ def send_to_conversation(agent_url: str, api_key: str, conv_id: str, text: str)
|
|
|
628
755
|
|
|
629
756
|
|
|
630
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)
|
|
631
760
|
result = _oh_request(agent_url, api_key, "GET", f"/api/conversations/{conv_id}")
|
|
632
761
|
return result.get("execution_status", "unknown")
|
|
633
762
|
|
|
634
763
|
|
|
635
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)
|
|
636
767
|
result = _oh_request(
|
|
637
768
|
agent_url, api_key, "GET", f"/api/conversations/{conv_id}/agent_final_response"
|
|
638
769
|
)
|
|
639
770
|
return result.get("response", "")
|
|
640
771
|
|
|
641
772
|
|
|
773
|
+
def _llm_provenance(profile: str, model: str) -> str:
|
|
774
|
+
return f"LLM profile: `{profile}` · Model: `{model}`"
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def _with_llm_provenance(body: str, profile: str, model: str) -> str:
|
|
778
|
+
provenance = _llm_provenance(profile, model)
|
|
779
|
+
body = "\n".join(
|
|
780
|
+
line
|
|
781
|
+
for line in (body or "").splitlines()
|
|
782
|
+
if not re.fullmatch(r"LLM profile: .* · Model: .*", line.strip())
|
|
783
|
+
).strip()
|
|
784
|
+
return f"{body}\n\n{provenance}" if body else provenance
|
|
785
|
+
|
|
786
|
+
|
|
642
787
|
# ── Message filtering ──────────────────────────────────────────────────────────
|
|
643
788
|
|
|
644
789
|
def _is_human_message(msg: dict, bot_user_id: str, bot_message_ts: list[str]) -> bool:
|
|
@@ -937,7 +1082,13 @@ def _process_trigger_message(
|
|
|
937
1082
|
)
|
|
938
1083
|
|
|
939
1084
|
try:
|
|
940
|
-
|
|
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)
|
|
1088
|
+
conv_id = create_conversation(
|
|
1089
|
+
agent_url, api_key, initial_prompt, agent=agent,
|
|
1090
|
+
title=f"Slack: {request_part or text}"[:80],
|
|
1091
|
+
)
|
|
941
1092
|
conv_url = f"{openhands_url}/conversations/{conv_id}"
|
|
942
1093
|
|
|
943
1094
|
now = time.time()
|
|
@@ -946,6 +1097,8 @@ def _process_trigger_message(
|
|
|
946
1097
|
"channel_id": channel_id,
|
|
947
1098
|
"thread_ts": thread_root,
|
|
948
1099
|
"status": "active",
|
|
1100
|
+
"llm_profile": "default",
|
|
1101
|
+
"llm_model": (agent["llm"].get("model") if agent else None) or "unknown",
|
|
949
1102
|
"last_activity": now,
|
|
950
1103
|
"last_seen_reply_ts": msg_ts,
|
|
951
1104
|
"reply_poll_backoff_seconds": THREAD_REPLY_INITIAL_BACKOFF_SECONDS,
|
|
@@ -1003,6 +1156,22 @@ def _check_conversation_completion(
|
|
|
1003
1156
|
)
|
|
1004
1157
|
else:
|
|
1005
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}")
|
|
1167
|
+
# The values recorded when this script started the conversation; a
|
|
1168
|
+
# thread follow-up reuses it, so they still describe its LLM. A record
|
|
1169
|
+
# written before provenance was tracked cannot name either value.
|
|
1170
|
+
summary = _with_llm_provenance(
|
|
1171
|
+
summary,
|
|
1172
|
+
rec.get("llm_profile") or "unknown",
|
|
1173
|
+
rec.get("llm_model") or "unknown",
|
|
1174
|
+
)
|
|
1006
1175
|
|
|
1007
1176
|
ts_back = post_message(slack_token, channel_id, summary, thread_ts=thread_ts)
|
|
1008
1177
|
if ts_back:
|
|
@@ -1024,6 +1193,14 @@ def _check_conversation_completion(
|
|
|
1024
1193
|
|
|
1025
1194
|
def main() -> str | None:
|
|
1026
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
|
+
|
|
1027
1204
|
state = load_state()
|
|
1028
1205
|
|
|
1029
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.
|