@openhands/extensions 0.28.0 → 0.30.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.
Files changed (89) hide show
  1. package/.agents/skills/custom-codereview-guide.md +44 -1
  2. package/.claude-plugin/marketplace.json +7 -5
  3. package/.codex-plugin/marketplace.json +7 -5
  4. package/.github/workflows/build-vscode-app.yml +3 -3
  5. package/.github/workflows/check-extensions.yml +1 -1
  6. package/.github/workflows/tests.yml +1 -1
  7. package/.plugin/marketplace.json +7 -5
  8. package/.release-please-manifest.json +1 -1
  9. package/AGENTS.md +7 -2
  10. package/README.md +3 -3
  11. package/apps/vscode/README.md +8 -6
  12. package/apps/vscode/canvas-extension.json +11 -3
  13. package/apps/vscode/extension.js +60 -12
  14. package/apps/vscode/scripts/package.mjs +20 -74
  15. package/automations/bundle-index.js +9 -7
  16. package/automations/catalog/github-issue-to-pr/manifest.json +1 -1
  17. package/automations/catalog/github-issue-triage/manifest.json +4 -3
  18. package/automations/catalog/github-pr-reviewer/manifest.json +5 -4
  19. package/automations/catalog/gitlab-issue-to-mr/manifest.json +3 -3
  20. package/automations/catalog/jira-issue-to-bitbucket-pr/manifest.json +4 -4
  21. package/automations/catalog/jira-issue-to-gitlab-mr/manifest.json +5 -5
  22. package/automations/catalog/jira-issue-to-pr/manifest.json +1 -1
  23. package/integrations/catalog/atlassian-rovo.json +4 -3
  24. package/integrations/catalog/bitbucket.json +16 -1
  25. package/integrations/catalog/github.json +1 -0
  26. package/integrations/catalog/gitlab.json +1 -1
  27. package/marketplaces/openhands-extensions.json +7 -5
  28. package/package.json +1 -1
  29. package/plugins/openhands/skills/openhands-automation/references/custom-automation.md +76 -8
  30. package/plugins/pr-review/README.md +1 -0
  31. package/plugins/pr-review/scripts/prompt.py +44 -11
  32. package/plugins/runtime-api-configs/SKILL.md +1 -1
  33. package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +224 -8
  34. package/pyproject.toml +1 -1
  35. package/python/openhands_extensions/_version.py +1 -1
  36. package/skills/canvas-extension-api/SKILL.md +4 -1
  37. package/skills/canvas-extension-api/references/backend-safety.md +3 -3
  38. package/skills/canvas-extension-api/references/native-design.md +250 -0
  39. package/skills/canvas-extension-api/references/v1-contract.md +7 -4
  40. package/skills/canvas-extension-api/scripts/validate-extension.mjs +17 -1
  41. package/skills/canvas-extension-api/scripts/validate-extension.test.mjs +49 -0
  42. package/skills/github/scripts/agent_conversation.py +224 -8
  43. package/skills/github-issue-to-pr/scripts/agent_conversation.py +224 -8
  44. package/skills/github-issue-triage/README.md +73 -14
  45. package/skills/github-issue-triage/SKILL.md +26 -46
  46. package/skills/github-issue-triage/scripts/agent_conversation.py +224 -8
  47. package/skills/github-issue-triage/scripts/triage_publication.py +293 -0
  48. package/skills/github-issue-triage/scripts/worker.py +153 -103
  49. package/skills/github-pr-reviewer/README.md +103 -1
  50. package/skills/github-pr-reviewer/SKILL.md +35 -8
  51. package/skills/github-pr-reviewer/references/state-schema.md +8 -0
  52. package/skills/github-pr-reviewer/scripts/agent_conversation.py +224 -8
  53. package/skills/github-pr-reviewer/scripts/main.py +163 -39
  54. package/skills/github-pr-reviewer/scripts/publication.py +193 -0
  55. package/skills/github-pr-reviewer/scripts/worker.py +297 -8
  56. package/skills/github-pr-reviewer/tests/test_main.py +73 -5
  57. package/skills/gitlab-issue-to-mr/README.md +10 -3
  58. package/skills/gitlab-issue-to-mr/SKILL.md +43 -9
  59. package/skills/gitlab-issue-to-mr/references/state-schema.md +4 -0
  60. package/skills/gitlab-issue-to-mr/scripts/main.py +359 -25
  61. package/skills/index.js +10 -10
  62. package/skills/jira-issue-to-pr/.claude-plugin/plugin.json +4 -1
  63. package/skills/jira-issue-to-pr/.codex-plugin/plugin.json +4 -1
  64. package/skills/jira-issue-to-pr/.plugin/plugin.json +4 -1
  65. package/skills/jira-issue-to-pr/README.md +18 -11
  66. package/skills/jira-issue-to-pr/SKILL.md +72 -23
  67. package/skills/jira-issue-to-pr/references/setup.md +43 -12
  68. package/skills/jira-issue-to-pr/scripts/main.py +136 -26
  69. package/skills/openhands-automation/references/custom-automation.md +76 -8
  70. package/skills/slack-channel-monitor/README.md +6 -1
  71. package/skills/slack-channel-monitor/SKILL.md +6 -3
  72. package/skills/slack-channel-monitor/scripts/main.py +36 -4
  73. package/skills/ticket-to-code-change/.claude-plugin/plugin.json +1 -2
  74. package/skills/ticket-to-code-change/.codex-plugin/plugin.json +1 -2
  75. package/skills/ticket-to-code-change/.plugin/plugin.json +1 -2
  76. package/skills/ticket-to-code-change/SKILL.md +5 -3
  77. package/skills/ticket-to-code-change/commands/ticket-to-code-change-setup.md +1 -1
  78. package/tests/fixtures/automations/github-pr-reviewer.json +9 -9
  79. package/tests/test_agent_conversation_dispatch.py +173 -6
  80. package/tests/test_automation_llm_provenance.py +1209 -0
  81. package/tests/test_github_review_publication.py +422 -0
  82. package/tests/test_github_reviewer_delivery.py +174 -21
  83. package/tests/test_github_triage_delivery.py +411 -238
  84. package/tests/test_gitlab_issue_to_mr.py +284 -0
  85. package/tests/test_jira_issue_to_pr.py +200 -6
  86. package/tests/test_pr_review_prompt_injection.py +353 -0
  87. package/tests/test_slack_channel_monitor.py +32 -1
  88. package/tests/test_vscode_app.py +8 -2
  89. package/tests/vscode-app.test.mjs +8 -2
@@ -2,19 +2,26 @@
2
2
  Jira issue-to-PR poller — reads all configuration from config.json in the same directory.
3
3
 
4
4
  config.json fields:
5
- jira_base_url e.g. "https://yourcompany.atlassian.net"
6
- jira_email Atlassian account email used for Basic auth
5
+ jira_mcp_server Settings name of the connected Atlassian Rovo MCP server. When set,
6
+ Jira is reached through it (OpenHands Cloud and Enterprise only)
7
+ and no Jira credential is configured here
8
+ jira_cloud_id Atlassian cloud id of the Jira site; required with jira_mcp_server
9
+ jira_base_url e.g. "https://yourcompany.atlassian.net"; without jira_mcp_server
10
+ jira_email Atlassian account email used for Basic auth; without jira_mcp_server
7
11
  jira_token_secret Name of the OpenHands secret holding the Jira API token
8
12
  jira_label Label to watch for (default: "create-pr")
9
13
  max_new_per_run Max conversations dispatched per run (default: 5)
14
+ git_provider Where the ticket's repository lives: "github" (default), "gitlab"
15
+ or "bitbucket". It only changes what the conversation is asked
10
16
 
11
- The target GitHub repository is NOT configured here. Each Jira ticket body must include
12
- the repo in "owner/repo" format; the spawned agent extracts it from the ticket text.
17
+ The target repository is NOT configured here. Each Jira ticket body must name it
18
+ (owner/repo on GitHub, group/project or a URL on GitLab, workspace/repo on Bitbucket);
19
+ the spawned agent extracts it from the ticket text.
13
20
 
14
21
  Runs against a local agent server (AGENT_SERVER_URL is injected) or on OpenHands Cloud
15
22
  (OPENHANDS_CLOUD_API_URL / OPENHANDS_API_KEY / SANDBOX_ID are injected instead).
16
23
  """
17
- import base64, json, os, re, sys, tempfile, urllib.error, urllib.request, uuid
24
+ import base64, json, os, re, sys, tempfile, urllib.error, urllib.parse, urllib.request, uuid
18
25
  from datetime import datetime, timezone
19
26
  from pathlib import Path
20
27
 
@@ -23,11 +30,67 @@ _HERE = Path(__file__).parent
23
30
  with open(_HERE / "config.json") as _f:
24
31
  _cfg = json.load(_f)
25
32
 
26
- JIRA_BASE_URL = _cfg["jira_base_url"].rstrip("/")
27
- JIRA_EMAIL = _cfg["jira_email"]
33
+ # Jira is reached through this connected MCP server when it is named, and through
34
+ # Jira's REST API with an API token otherwise.
35
+ JIRA_MCP_SERVER = _cfg.get("jira_mcp_server")
36
+ JIRA_CLOUD_ID = _cfg["jira_cloud_id"] if JIRA_MCP_SERVER else None
37
+ JIRA_BASE_URL = None if JIRA_MCP_SERVER else _cfg["jira_base_url"].rstrip("/")
38
+ JIRA_EMAIL = None if JIRA_MCP_SERVER else _cfg["jira_email"]
28
39
  JIRA_TOKEN_SECRET = _cfg.get("jira_token_secret", "JIRA_CLOUD_KEY")
29
40
  JIRA_LABEL = _cfg.get("jira_label", "create-pr")
41
+ # Atlassian renamed the comment tool in v2 of its MCP server and moves v1 connections
42
+ # to the v2 tools on its own schedule, so a deployment can meet either name.
43
+ JIRA_COMMENT_TOOLS = ("addOrEditJiraIssueComment", "addCommentToJiraIssue")
30
44
  MAX_NEW_PER_RUN = int(_cfg.get("max_new_per_run", 5))
45
+ GIT_PROVIDER = _cfg.get("git_provider", "github")
46
+ # What the conversation is told about the provider the ticket names its repository on.
47
+ # Only the prompt differs: the conversation pushes with the user's connected integration,
48
+ # and without one it delivers through the tools of the connected MCP server instead.
49
+ GIT_PROVIDERS = {
50
+ "github": {
51
+ "name": "GitHub",
52
+ "request": "Pull Request",
53
+ "short": "PR",
54
+ "reference": '"owner/repo" format (e.g. "acme-org/backend") or a full GitHub URL\n'
55
+ ' (e.g. "https://github.com/acme-org/backend")',
56
+ "clone": "https://github.com/<owner>/<repo>",
57
+ "delivery": "",
58
+ },
59
+ "gitlab": {
60
+ "name": "GitLab",
61
+ "request": "Merge Request",
62
+ "short": "MR",
63
+ "reference": '"group/project" format, subgroups included (e.g. "acme-org/platform/backend"),\n'
64
+ ' or a full GitLab URL, which may be on a self-managed host\n'
65
+ ' (e.g. "https://gitlab.example.com/acme-org/platform/backend")',
66
+ "clone": "https://gitlab.com/<group>/<project>, or the host the ticket's URL names",
67
+ "delivery": """
68
+ Delivering through GitLab:
69
+ - If GitLab tools from a connected MCP server are available to you, prefer them for reading
70
+ the issue and for opening the merge request.
71
+ - Without a `GITLAB_TOKEN` you cannot push with git: create the branch with `add_branch`,
72
+ commit your changes with `add_commit` (one call, a file action per changed file), and open
73
+ the merge request with `save_merge_request`. A private project cannot be cloned without a
74
+ credential; read it with `list_repository_tree` and `get_repository_file` instead.
75
+ """,
76
+ },
77
+ "bitbucket": {
78
+ "name": "Bitbucket",
79
+ "request": "Pull Request",
80
+ "short": "PR",
81
+ "reference": '"workspace/repo" format (e.g. "acme-org/backend") or a full Bitbucket URL\n'
82
+ ' (e.g. "https://bitbucket.org/acme-org/backend")',
83
+ "clone": "https://bitbucket.org/<workspace>/<repo>",
84
+ "delivery": """
85
+ Delivering through Bitbucket:
86
+ - Without a `BITBUCKET_TOKEN`, use the Bitbucket tools of the connected Atlassian Rovo MCP
87
+ server: read the repository with `getBitbucketRepoFileContent`, create the branch with
88
+ `createBitbucketRepoBranch`, commit your changes with `createBitbucketRepoCommit`, and open
89
+ the pull request with `createBitbucketRepoPullRequest`. Tools the server does not list
90
+ directly are reached through `discover` and `executeRead` / `executeWrite`.
91
+ """,
92
+ },
93
+ }
31
94
  # How often a conversation that OpenHands Cloud failed to start is started again.
32
95
  MAX_START_ATTEMPTS = 3
33
96
 
@@ -163,14 +226,39 @@ def extract_adf_text(adf):
163
226
  return " ".join(p for p in parts if p).strip()
164
227
 
165
228
 
229
+ def call_mcp_tool(name, arguments):
230
+ """Call a tool on the connected MCP server and return its text output.
231
+
232
+ OpenHands makes the call with the credentials the user stored when connecting
233
+ the server, so this run holds no Jira credential of its own.
234
+ """
235
+ server = urllib.parse.quote(JIRA_MCP_SERVER, safe="")
236
+ result = _cloud_request("POST", f"/api/v1/mcp/servers/{server}/call-tool", {
237
+ "name": name, "arguments": arguments,
238
+ })
239
+ if result.get("is_error"):
240
+ raise RuntimeError(f"{name} failed: {str(result.get('text'))[:500]}")
241
+ return result.get("text") or ""
242
+
243
+
166
244
  def fetch_labeled_issues(auth_header):
167
- """Return open Jira issues with JIRA_LABEL using the current v3 search endpoint."""
168
- url = f"{JIRA_BASE_URL}/rest/api/3/search/jql"
169
- body = json.dumps({
245
+ """Return open Jira issues with JIRA_LABEL.
246
+
247
+ Uses the connected MCP server's JQL search, or the current v3 search endpoint.
248
+ """
249
+ search = {
170
250
  "jql": f'labels = "{JIRA_LABEL}" AND statusCategory != Done',
171
251
  "fields": ["key", "summary", "description", "status", "updated"],
172
252
  "maxResults": 50,
173
- }).encode()
253
+ }
254
+ if JIRA_MCP_SERVER:
255
+ text = call_mcp_tool("searchJiraIssuesUsingJql", {"cloudId": JIRA_CLOUD_ID, **search})
256
+ try:
257
+ return json.loads(text).get("issues", [])
258
+ except (ValueError, AttributeError) as exc:
259
+ raise RuntimeError(f"Unexpected Jira search result: {text[:500]}") from exc
260
+ url = f"{JIRA_BASE_URL}/rest/api/3/search/jql"
261
+ body = json.dumps(search).encode()
174
262
  req = urllib.request.Request(
175
263
  url, data=body,
176
264
  headers={
@@ -188,7 +276,19 @@ def fetch_labeled_issues(auth_header):
188
276
 
189
277
 
190
278
  def post_jira_comment(issue_key, auth_header, text):
191
- """Post a plain-text comment on a Jira issue using ADF."""
279
+ """Post a plain-text comment on a Jira issue."""
280
+ if JIRA_MCP_SERVER:
281
+ errors = []
282
+ for tool in JIRA_COMMENT_TOOLS:
283
+ try:
284
+ call_mcp_tool(tool, {
285
+ "cloudId": JIRA_CLOUD_ID, "issueIdOrKey": issue_key, "commentBody": text,
286
+ })
287
+ return
288
+ except RuntimeError as exc:
289
+ errors.append(str(exc))
290
+ print(f"Warning: failed to post Jira comment on {issue_key}: {'; '.join(errors)}")
291
+ return
192
292
  url = f"{JIRA_BASE_URL}/rest/api/3/issue/{issue_key}/comment"
193
293
  body = json.dumps({
194
294
  "body": {
@@ -321,10 +421,21 @@ try:
321
421
  raise RuntimeError("The automation KV store is required on OpenHands Cloud, "
322
422
  "but AUTOMATION_KV_TOKEN is not set for this run.")
323
423
 
324
- jira_token = get_secret(JIRA_TOKEN_SECRET)
325
- auth_header = "Basic " + base64.b64encode(
326
- f"{JIRA_EMAIL}:{jira_token}".encode()
327
- ).decode()
424
+ if GIT_PROVIDER not in GIT_PROVIDERS:
425
+ raise RuntimeError(f"Unknown git_provider {GIT_PROVIDER!r}; "
426
+ f"expected one of: {', '.join(GIT_PROVIDERS)}")
427
+ git = GIT_PROVIDERS[GIT_PROVIDER]
428
+
429
+ auth_header = None
430
+ if not JIRA_MCP_SERVER:
431
+ jira_token = get_secret(JIRA_TOKEN_SECRET)
432
+ auth_header = "Basic " + base64.b64encode(
433
+ f"{JIRA_EMAIL}:{jira_token}".encode()
434
+ ).decode()
435
+ elif IS_LOCAL:
436
+ # Only OpenHands Cloud and Enterprise call a connected MCP server for a run.
437
+ raise RuntimeError("jira_mcp_server needs OpenHands Cloud or Enterprise; "
438
+ "a local run reaches Jira with jira_token_secret.")
328
439
 
329
440
  state = load_state()
330
441
  processed_keys = set(state.get("processed_keys", []))
@@ -343,7 +454,7 @@ try:
343
454
  if not IS_LOCAL:
344
455
  settle_cloud_starts(state, processed_keys, auth_header)
345
456
 
346
- print(f"Polling {JIRA_BASE_URL} for issues labeled '{JIRA_LABEL}'…")
457
+ print(f"Polling {JIRA_BASE_URL or JIRA_MCP_SERVER} for issues labeled '{JIRA_LABEL}'…")
347
458
  issues = fetch_labeled_issues(auth_header)
348
459
  print(f"Total matching issues : {len(issues)}")
349
460
 
@@ -393,28 +504,27 @@ try:
393
504
  description = extract_adf_text(issue["fields"].get("description"))
394
505
  branch = f"jira/{key.lower()}"
395
506
 
396
- prompt = f"""Create a GitHub Pull Request for the following Jira issue.
507
+ prompt = f"""Create a {git['name']} {git['request']} for the following Jira issue.
397
508
 
398
509
  Jira Issue : {key}
399
510
  Summary : {summary}
400
511
  Description: {description or "No description provided."}
401
512
 
402
513
  Steps:
403
- 1. Find the target GitHub repository in the Description above. Look for a reference in
404
- "owner/repo" format (e.g. "acme-org/backend") or a full GitHub URL
405
- (e.g. "https://github.com/acme-org/backend"). Use that repository.
514
+ 1. Find the target {git['name']} repository in the Description above. Look for a reference in
515
+ {git['reference']}. Use that repository.
406
516
  If no repository is mentioned, create a file `jira/{key}/notes.md` with the issue
407
- details and print a message explaining that no GitHub repo was found in the ticket.
408
- 2. Clone the repository (e.g. https://github.com/<owner>/<repo>).
517
+ details and print a message explaining that no {git['name']} repo was found in the ticket.
518
+ 2. Clone the repository (e.g. {git['clone']}).
409
519
  3. Create branch `{branch}` from the default branch.
410
520
  4. Implement the changes described in the issue.
411
521
  If the description is vague or missing, create `jira/{key}/notes.md`
412
522
  with the issue key, summary, and description as a placeholder.
413
- 5. Commit, push the branch, and open a Pull Request:
523
+ 5. Commit, push the branch, and open a {git['request']}:
414
524
  - Title : [{key}] {summary}
415
525
  - Body : Reference the Jira issue key and describe the changes made.
416
- 6. Print the PR URL when done.
417
- """
526
+ 6. Print the {git['short']} URL when done.
527
+ {git['delivery']}"""
418
528
  if IS_LOCAL:
419
529
  workdir = tempfile.mkdtemp(prefix=f"jira-{key.lower()}-")
420
530
  payload = {
@@ -249,6 +249,7 @@ uv pip install -q openhands-sdk openhands-workspace openhands-tools
249
249
  import os
250
250
 
251
251
  from openhands.sdk import Conversation
252
+ from openhands.sdk.automation import automation_conversation_kwargs
252
253
  from openhands.tools.preset.default import get_default_agent
253
254
  from openhands.workspace import OpenHandsCloudWorkspace
254
255
 
@@ -266,13 +267,48 @@ with OpenHandsCloudWorkspace(
266
267
  ) as workspace:
267
268
  llm = workspace.get_llm()
268
269
  agent = get_default_agent(llm=llm, cli_mode=True)
269
- conversation = Conversation(agent=agent, workspace=workspace)
270
+ conversation = Conversation(
271
+ agent=agent,
272
+ workspace=workspace,
273
+ **automation_conversation_kwargs(),
274
+ )
270
275
  conversation.send_message("Your automation prompt here")
271
276
  conversation.run()
272
277
  conversation.close()
273
278
  # OpenHandsCloudWorkspace.__exit__ fires the completion callback here.
274
279
  ```
275
280
 
281
+ ### Link automation conversations to run telemetry
282
+
283
+ When an automation script starts a conversation, preserve the trace context that the automation service injected into the run. This links the conversation span to the triggering webhook, scheduler dispatch, and run record.
284
+
285
+ - SDK conversations: pass `**automation_conversation_kwargs()` into every `Conversation(...)` call.
286
+ - Direct agent-server API calls: merge `automation_observability_headers()` into the headers for each `POST /api/conversations` request.
287
+ - If you add your own conversation tags, pass them through `conversation_tags={...}` instead of replacing the helper output.
288
+
289
+ ```python
290
+ from openhands.sdk import Conversation
291
+ from openhands.sdk.automation import (
292
+ automation_conversation_kwargs,
293
+ automation_observability_headers,
294
+ )
295
+
296
+ conversation = Conversation(
297
+ agent=agent,
298
+ workspace=workspace,
299
+ **automation_conversation_kwargs(),
300
+ )
301
+
302
+ headers = {
303
+ "X-Session-API-Key": api_key,
304
+ **automation_observability_headers(span_name="automation.conversation"),
305
+ }
306
+ # Use these headers when creating a backend conversation directly.
307
+ # httpx.post(f"{agent_url}/api/conversations", headers=headers, json=payload)
308
+ ```
309
+
310
+ The built-in prompt and plugin presets use this helper. Custom scripts should do the same for every new conversation so traces, observability metadata, and automation conversation tags stay attached.
311
+
276
312
  ### Selecting an LLM profile
277
313
 
278
314
  The automation service injects an `AUTOMATION_MODEL` environment variable holding the **LLM profile name** the automation should run with. This is the profile a user selects when editing the automation, or — when none is selected — a snapshot of the user's active profile taken when the automation was created. To honor it on every run, read `AUTOMATION_MODEL` and pass it to `workspace.get_llm(profile_name=...)`:
@@ -299,13 +335,27 @@ Conversations started during a run remain accessible in the OpenHands UI after t
299
335
 
300
336
  ```python
301
337
  # Default: conversation persists after close (users can view/continue it)
302
- conversation = Conversation(agent=agent, workspace=workspace)
338
+ conversation = Conversation(
339
+ agent=agent,
340
+ workspace=workspace,
341
+ **automation_conversation_kwargs(),
342
+ )
303
343
 
304
344
  # Explicitly persist (same as default)
305
- conversation = Conversation(agent=agent, workspace=workspace, delete_on_close=False)
345
+ conversation = Conversation(
346
+ agent=agent,
347
+ workspace=workspace,
348
+ delete_on_close=False,
349
+ **automation_conversation_kwargs(),
350
+ )
306
351
 
307
352
  # Delete conversation resources on close
308
- conversation = Conversation(agent=agent, workspace=workspace, delete_on_close=True)
353
+ conversation = Conversation(
354
+ agent=agent,
355
+ workspace=workspace,
356
+ delete_on_close=True,
357
+ **automation_conversation_kwargs(),
358
+ )
309
359
  ```
310
360
 
311
361
  The agent server itself persists until it times out or is manually deleted; this is managed by the automation service, not by the workspace.
@@ -339,7 +389,11 @@ else:
339
389
  with OpenHandsCloudWorkspace(local_agent_server_mode=True, cloud_api_url=api_url, cloud_api_key=api_key) as workspace:
340
390
  llm = workspace.get_llm()
341
391
  agent = get_default_agent(llm=llm, cli_mode=True)
342
- conversation = Conversation(agent=agent, workspace=workspace)
392
+ conversation = Conversation(
393
+ agent=agent,
394
+ workspace=workspace,
395
+ **automation_conversation_kwargs(),
396
+ )
343
397
  conversation.send_message(f"Investigate these alerts and open GitHub issues: {alerts}")
344
398
  conversation.run()
345
399
  conversation.close()
@@ -358,7 +412,11 @@ from openhands.sdk.conversation.state import ConversationExecutionStatus
358
412
  with OpenHandsCloudWorkspace(local_agent_server_mode=True, cloud_api_url=api_url, cloud_api_key=api_key) as workspace:
359
413
  llm = workspace.get_llm()
360
414
  agent = get_default_agent(llm=llm, cli_mode=True)
361
- conversation = Conversation(agent=agent, workspace=workspace)
415
+ conversation = Conversation(
416
+ agent=agent,
417
+ workspace=workspace,
418
+ **automation_conversation_kwargs(),
419
+ )
362
420
  conversation.send_message("Run a long analysis task")
363
421
  # Conversation is now running asynchronously in the agent server.
364
422
 
@@ -404,7 +462,12 @@ stop_hook = HookConfig(
404
462
  with OpenHandsCloudWorkspace(local_agent_server_mode=True, cloud_api_url=api_url, cloud_api_key=api_key) as workspace:
405
463
  llm = workspace.get_llm()
406
464
  agent = get_default_agent(llm=llm, cli_mode=True)
407
- conversation = Conversation(agent=agent, workspace=workspace, hook_config=stop_hook)
465
+ conversation = Conversation(
466
+ agent=agent,
467
+ workspace=workspace,
468
+ hook_config=stop_hook,
469
+ **automation_conversation_kwargs(),
470
+ )
408
471
  conversation.send_message("Do some long-running work")
409
472
  # Don't call run() — the conversation runs asynchronously.
410
473
  # When the agent stops, the stop hook will fire the callback.
@@ -765,6 +828,7 @@ import os
765
828
  import json
766
829
 
767
830
  from openhands.sdk import Conversation
831
+ from openhands.sdk.automation import automation_conversation_kwargs
768
832
  from openhands.tools.preset.default import get_default_agent
769
833
  from openhands.workspace import OpenHandsCloudWorkspace
770
834
 
@@ -781,7 +845,11 @@ with OpenHandsCloudWorkspace(
781
845
  ) as workspace:
782
846
  llm = workspace.get_llm()
783
847
  agent = get_default_agent(llm=llm, cli_mode=True)
784
- conversation = Conversation(agent=agent, workspace=workspace)
848
+ conversation = Conversation(
849
+ agent=agent,
850
+ workspace=workspace,
851
+ **automation_conversation_kwargs(),
852
+ )
785
853
  conversation.send_message("Generate a weekly status report")
786
854
  conversation.run()
787
855
  conversation.close()
@@ -37,12 +37,17 @@ This skill is activated by keywords:
37
37
  thread immediately on trigger detection
38
38
  - **Automatic summaries**: when the conversation reaches a terminal state the
39
39
  agent's final response is posted back to the thread; error/stuck states
40
- receive a clear error notice
40
+ receive a clear error notice, and every result shows the LLM settings source
41
+ and concrete model
41
42
  - **Persistent state**: conversation tracking and poll timestamps are stored
42
43
  in `automation-state/slack_poller_{automation_id}.json` across runs
43
44
 
44
45
  ## Prerequisites
45
46
 
47
+ The footer reports `default` and the concrete model from the server settings
48
+ already used to start the conversation. Follow-up replies keep those recorded
49
+ values because they reuse the same conversation.
50
+
46
51
  Set at least one of the following in **OpenHands Settings - Secrets**:
47
52
 
48
53
  | Secret | Token type | Minimum scopes |
@@ -251,14 +251,17 @@ Each cron run executes `main.py`, which runs **10 polling iterations** (every
251
251
  ignored.
252
252
  - Contains trigger phrase outside a tracked conversation → 👀 reaction, create
253
253
  a new conversation, post link.
254
+ - Records `default` and the concrete model from the server settings already
255
+ used to start the conversation, for the response footer.
254
256
  - Thread replies: agent receives full thread history for context.
255
257
  - Root messages: agent receives the trigger text only.
256
258
  6. **Checks conversation statuses** - for each active conversation where
257
259
  `time.time() - last_activity > 15 s`:
258
260
  - If status is `idle`, `finished`, `error`, or `stuck` → fetch the agent's
259
- final response via `/api/conversations/{id}/agent_final_response` and post
260
- it to the Slack thread using Slack's `markdown_text` field so Markdown
261
- formatting renders correctly. Mark the record `watching` for five minutes
261
+ final response via `/api/conversations/{id}/agent_final_response`, append
262
+ the LLM profile and model, and post it to the Slack thread using Slack's
263
+ `markdown_text` field so Markdown formatting renders correctly. Mark the
264
+ record `watching` for five minutes
262
265
  so triggered follow-up replies can continue the same conversation.
263
266
  7. **Advances `last_poll`** to `now - 10 s` (overlap window prevents boundary
264
267
  races). If a conversation creation failed, pins `last_poll` further back to
@@ -33,6 +33,7 @@ Optional secret:
33
33
 
34
34
  import json
35
35
  import os
36
+ import re
36
37
  import sys
37
38
  import time
38
39
  import urllib.error
@@ -577,7 +578,12 @@ def _build_secrets_payload(agent_url: str, api_key: str) -> dict:
577
578
  return secrets
578
579
 
579
580
 
580
- def create_conversation(agent_url: str, api_key: str, initial_message: str) -> str:
581
+ def create_conversation(
582
+ agent_url: str,
583
+ api_key: str,
584
+ initial_message: str,
585
+ agent: dict | None = None,
586
+ ) -> str:
581
587
  """Create a conversation and return its ID.
582
588
 
583
589
  The server auto-starts the agent when initial_message is provided
@@ -597,10 +603,9 @@ def create_conversation(agent_url: str, api_key: str, initial_message: str) -> s
597
603
  workspace_dir = os.path.join(root, "slack-monitor-conversations")
598
604
  os.makedirs(workspace_dir, exist_ok=True)
599
605
 
600
- agent = _get_agent_dict(agent_url, api_key)
601
606
  payload: dict = {
602
607
  "workspace": {"working_dir": workspace_dir},
603
- "agent": agent,
608
+ "agent": agent or _get_agent_dict(agent_url, api_key),
604
609
  "initial_message": {"content": [{"text": initial_message}]},
605
610
  }
606
611
 
@@ -639,6 +644,20 @@ def conversation_final_response(agent_url: str, api_key: str, conv_id: str) -> s
639
644
  return result.get("response", "")
640
645
 
641
646
 
647
+ def _llm_provenance(profile: str, model: str) -> str:
648
+ return f"LLM profile: `{profile}` · Model: `{model}`"
649
+
650
+
651
+ def _with_llm_provenance(body: str, profile: str, model: str) -> str:
652
+ provenance = _llm_provenance(profile, model)
653
+ body = "\n".join(
654
+ line
655
+ for line in (body or "").splitlines()
656
+ if not re.fullmatch(r"LLM profile: .* · Model: .*", line.strip())
657
+ ).strip()
658
+ return f"{body}\n\n{provenance}" if body else provenance
659
+
660
+
642
661
  # ── Message filtering ──────────────────────────────────────────────────────────
643
662
 
644
663
  def _is_human_message(msg: dict, bot_user_id: str, bot_message_ts: list[str]) -> bool:
@@ -937,7 +956,10 @@ def _process_trigger_message(
937
956
  )
938
957
 
939
958
  try:
940
- conv_id = create_conversation(agent_url, api_key, initial_prompt)
959
+ agent = _get_agent_dict(agent_url, api_key)
960
+ conv_id = create_conversation(
961
+ agent_url, api_key, initial_prompt, agent=agent
962
+ )
941
963
  conv_url = f"{openhands_url}/conversations/{conv_id}"
942
964
 
943
965
  now = time.time()
@@ -946,6 +968,8 @@ def _process_trigger_message(
946
968
  "channel_id": channel_id,
947
969
  "thread_ts": thread_root,
948
970
  "status": "active",
971
+ "llm_profile": "default",
972
+ "llm_model": agent["llm"].get("model") or "unknown",
949
973
  "last_activity": now,
950
974
  "last_seen_reply_ts": msg_ts,
951
975
  "reply_poll_backoff_seconds": THREAD_REPLY_INITIAL_BACKOFF_SECONDS,
@@ -1003,6 +1027,14 @@ def _check_conversation_completion(
1003
1027
  )
1004
1028
  else:
1005
1029
  summary = final if final else "Success (no message available)."
1030
+ # The values recorded when this script started the conversation; a
1031
+ # thread follow-up reuses it, so they still describe its LLM. A record
1032
+ # written before provenance was tracked cannot name either value.
1033
+ summary = _with_llm_provenance(
1034
+ summary,
1035
+ rec.get("llm_profile") or "unknown",
1036
+ rec.get("llm_model") or "unknown",
1037
+ )
1006
1038
 
1007
1039
  ts_back = post_message(slack_token, channel_id, summary, thread_ts=thread_ts)
1008
1040
  if ts_back:
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ticket-to-code-change",
3
3
  "version": "1.0.0",
4
- "description": "Create pull or merge requests from Jira or Linear tickets across GitHub, GitLab, and Bitbucket.",
4
+ "description": "Create pull or merge requests from Linear tickets across GitHub, GitLab, and Bitbucket.",
5
5
  "author": {
6
6
  "name": "OpenHands",
7
7
  "email": "contact@all-hands.dev"
@@ -10,7 +10,6 @@
10
10
  "repository": "https://github.com/OpenHands/extensions",
11
11
  "license": "MIT",
12
12
  "keywords": [
13
- "jira",
14
13
  "linear",
15
14
  "github",
16
15
  "gitlab",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ticket-to-code-change",
3
3
  "version": "1.0.0",
4
- "description": "Create pull or merge requests from Jira or Linear tickets across GitHub, GitLab, and Bitbucket.",
4
+ "description": "Create pull or merge requests from Linear tickets across GitHub, GitLab, and Bitbucket.",
5
5
  "author": {
6
6
  "name": "OpenHands",
7
7
  "email": "contact@all-hands.dev"
@@ -10,7 +10,6 @@
10
10
  "repository": "https://github.com/OpenHands/extensions",
11
11
  "license": "MIT",
12
12
  "keywords": [
13
- "jira",
14
13
  "linear",
15
14
  "github",
16
15
  "gitlab",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ticket-to-code-change",
3
3
  "version": "1.0.0",
4
- "description": "Create pull or merge requests from Jira or Linear tickets across GitHub, GitLab, and Bitbucket.",
4
+ "description": "Create pull or merge requests from Linear tickets across GitHub, GitLab, and Bitbucket.",
5
5
  "author": {
6
6
  "name": "OpenHands",
7
7
  "email": "contact@all-hands.dev"
@@ -10,7 +10,6 @@
10
10
  "repository": "https://github.com/OpenHands/extensions",
11
11
  "license": "MIT",
12
12
  "keywords": [
13
- "jira",
14
13
  "linear",
15
14
  "github",
16
15
  "gitlab",
@@ -1,8 +1,9 @@
1
1
  ---
2
2
  name: ticket-to-code-change
3
3
  description: >
4
- Set up a ticket-to-code-change automation using Jira or Linear as the issue
5
- tracker and GitHub, GitLab, or Bitbucket as the source-control provider.
4
+ Set up a ticket-to-code-change automation using Linear as the issue tracker
5
+ and GitHub, GitLab, or Bitbucket as the source-control provider. The Jira
6
+ templates are served by the jira-issue-to-pr skill instead.
6
7
  Watches for implementation-ready tickets, starts an OpenHands conversation
7
8
  to implement and test the request, opens a pull or merge request, and links
8
9
  the result back to the ticket.
@@ -19,7 +20,8 @@ requests or merge requests.
19
20
 
20
21
  Ask the user for:
21
22
 
22
- 1. The issue tracker: Jira Cloud or Linear.
23
+ 1. The issue tracker: Linear. Jira automations are set up with the
24
+ `jira-issue-to-pr` skill instead, which ships a poller.
23
25
  2. The source-control provider: GitHub, GitLab, or Bitbucket Cloud.
24
26
  3. The project or team to watch and the label or workflow state that means
25
27
  "implementation ready".
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  # auto-generated by sync_extensions.py
3
- description: Set up a ticket-to-code-change automation using Jira or Linear as the issue tracker and GitHub, GitLab, or Bitbucket as the source-control provider. Watches for implementation-ready tickets, starts an OpenHands conversation to implement and test the request, opens a pull or merge request, and links the result back to the ticket.
3
+ description: Set up a ticket-to-code-change automation using Linear as the issue tracker and GitHub, GitLab, or Bitbucket as the source-control provider. The Jira templates are served by the jira-issue-to-pr skill instead. Watches for implementation-ready tickets, starts an OpenHands conversation to implement and test the request, opens a pull or merge request, and links the result back to the ticket.
4
4
  ---
5
5
 
6
6
  Read and follow the complete instructions in the SKILL.md file located in this skill's directory.