@openhands/extensions 0.27.0 → 0.29.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 (70) hide show
  1. package/.agents/skills/custom-codereview-guide.md +31 -0
  2. package/.claude-plugin/marketplace.json +9 -0
  3. package/.codex-plugin/marketplace.json +9 -0
  4. package/.github/workflows/build-vscode-app.yml +3 -3
  5. package/.github/workflows/deprecation-check.yml +1 -1
  6. package/.github/workflows/pypi-publish.yml +2 -2
  7. package/.github/workflows/tests.yml +1 -1
  8. package/.plugin/marketplace.json +9 -0
  9. package/.release-please-manifest.json +1 -1
  10. package/AGENTS.md +7 -2
  11. package/README.md +3 -2
  12. package/automations/bundle-index.js +5 -5
  13. package/automations/catalog/github-issue-to-pr/manifest.json +2 -2
  14. package/automations/catalog/github-issue-triage/manifest.json +2 -2
  15. package/automations/catalog/github-pr-reviewer/manifest.json +2 -2
  16. package/marketplaces/openhands-extensions.json +9 -0
  17. package/package.json +1 -1
  18. package/plugins/openhands/skills/openhands-automation/README.md +6 -4
  19. package/plugins/openhands/skills/openhands-automation/SKILL.md +103 -21
  20. package/plugins/openhands/skills/openhands-automation/references/custom-automation.md +79 -12
  21. package/plugins/openhands/skills/openhands-sdk/SKILL.md +2 -1
  22. package/plugins/qa-changes/README.md +4 -0
  23. package/plugins/qa-changes/skills/qa-changes/README.md +3 -1
  24. package/plugins/qa-changes/skills/qa-changes/SKILL.md +42 -0
  25. package/plugins/vulnerability-remediation/skills/github/README.md +5 -1
  26. package/plugins/vulnerability-remediation/skills/github/SKILL.md +27 -0
  27. package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +51 -4
  28. package/pyproject.toml +1 -1
  29. package/python/openhands_extensions/_version.py +1 -1
  30. package/scripts/sync_openhands_sdk_skill.py +16 -13
  31. package/skills/azure-devops/README.md +3 -1
  32. package/skills/azure-devops/SKILL.md +4 -0
  33. package/skills/bitbucket/README.md +3 -1
  34. package/skills/bitbucket/SKILL.md +4 -2
  35. package/skills/bitbucket-cloud/README.md +2 -0
  36. package/skills/bitbucket-cloud/SKILL.md +2 -0
  37. package/skills/bitbucket-data-center/README.md +2 -0
  38. package/skills/bitbucket-data-center/SKILL.md +2 -0
  39. package/skills/canvas-extension-api/SKILL.md +4 -1
  40. package/skills/canvas-extension-api/references/native-design.md +250 -0
  41. package/skills/github/README.md +5 -1
  42. package/skills/github/SKILL.md +27 -0
  43. package/skills/github/scripts/agent_conversation.py +51 -4
  44. package/skills/github-issue-to-pr/scripts/agent_conversation.py +51 -4
  45. package/skills/github-issue-triage/scripts/agent_conversation.py +51 -4
  46. package/skills/github-pr-reviewer/README.md +5 -0
  47. package/skills/github-pr-reviewer/SKILL.md +12 -0
  48. package/skills/github-pr-reviewer/scripts/agent_conversation.py +51 -4
  49. package/skills/github-pr-reviewer/scripts/main.py +20 -6
  50. package/skills/github-pr-reviewer/scripts/worker.py +77 -15
  51. package/skills/github-pr-reviewer/tests/test_main.py +23 -0
  52. package/skills/gitlab/README.md +3 -1
  53. package/skills/gitlab/SKILL.md +4 -0
  54. package/skills/index.js +25 -13
  55. package/skills/linear/README.md +2 -0
  56. package/skills/linear/SKILL.md +6 -2
  57. package/skills/openhands-automation/README.md +6 -4
  58. package/skills/openhands-automation/SKILL.md +103 -21
  59. package/skills/openhands-automation/references/custom-automation.md +79 -12
  60. package/skills/openhands-sdk/SKILL.md +2 -1
  61. package/skills/qa-changes/README.md +3 -1
  62. package/skills/qa-changes/SKILL.md +42 -0
  63. package/skills/simplified-technical-english/.claude-plugin/plugin.json +15 -0
  64. package/skills/simplified-technical-english/.codex-plugin/plugin.json +15 -0
  65. package/skills/simplified-technical-english/.plugin/plugin.json +15 -0
  66. package/skills/simplified-technical-english/README.md +21 -0
  67. package/skills/simplified-technical-english/SKILL.md +65 -0
  68. package/tests/fixtures/automations/github-pr-reviewer.json +9 -9
  69. package/tests/test_agent_conversation_dispatch.py +130 -0
  70. package/tests/test_github_reviewer_delivery.py +279 -2
@@ -32,6 +32,11 @@ _CLOUD_START_GRACE_SECONDS = 10 * 60
32
32
  # How long to wait for a paused conversation's sandbox to come back up.
33
33
  _CLOUD_RESUME_TIMEOUT_SECONDS = 120
34
34
  _CLOUD_POLL_SECONDS = 3
35
+ # How many times a matched delivery whose conversation ended in ERROR is sent
36
+ # again. The delivery key never changes for a stable subject, so without a
37
+ # retry its work is stranded; without a bound, a conversation that fails the
38
+ # same way every time would be re-run on every scan.
39
+ _MAX_ERROR_RETRIES = 2
35
40
 
36
41
 
37
42
  def _register_tools() -> None:
@@ -204,14 +209,23 @@ class AgentConversationDispatcher:
204
209
 
205
210
  same_delivery = record.get("delivery") == delivery
206
211
  same_head = bool(head) and record.get("head") == head
212
+ # Error retries count against one delivery; a new delivery starts over.
213
+ error_retries = int(record.get("error_retries") or 0) if same_delivery else 0
214
+ can_retry = error_retries < _MAX_ERROR_RETRIES
207
215
 
208
216
  if self._cloud:
209
217
  disposition, conversation_id = self._deliver_cloud(
210
- record, conversation_id, subject, prompt, same_delivery, same_head
218
+ record,
219
+ conversation_id,
220
+ subject,
221
+ prompt,
222
+ same_delivery,
223
+ same_head,
224
+ can_retry,
211
225
  )
212
226
  else:
213
227
  disposition = self._deliver_local(
214
- conversation_id, prompt, same_delivery, same_head
228
+ conversation_id, prompt, same_delivery, same_head, can_retry
215
229
  )
216
230
 
217
231
  if disposition in ("deduplicated", "in_progress"):
@@ -228,6 +242,10 @@ class AgentConversationDispatcher:
228
242
  }
229
243
  if disposition == "created" and self._cloud:
230
244
  new_record["started_at"] = time.time()
245
+ if disposition == "retried":
246
+ error_retries += 1
247
+ if error_retries:
248
+ new_record["error_retries"] = error_retries
231
249
  _kv_request(state_key, "PUT", new_record)
232
250
  return {
233
251
  "disposition": disposition,
@@ -235,7 +253,14 @@ class AgentConversationDispatcher:
235
253
  }
236
254
 
237
255
  def _deliver_cloud(
238
- self, record, conversation_id, subject, prompt, same_delivery, same_head
256
+ self,
257
+ record,
258
+ conversation_id,
259
+ subject,
260
+ prompt,
261
+ same_delivery,
262
+ same_head,
263
+ can_retry,
239
264
  ):
240
265
  """Deliver through the OpenHands API; return the disposition and the id.
241
266
 
@@ -272,11 +297,21 @@ class AgentConversationDispatcher:
272
297
  if status in ("idle", "paused"):
273
298
  self._cloud.run(conversation)
274
299
  return "resumed", current_id
300
+ if status == "error" and can_retry:
301
+ # The delivery matched but its conversation died before the work
302
+ # finished, so send the revision again rather than strand it.
303
+ # The API reports an execution status only while the sandbox
304
+ # runs, so an errored conversation whose sandbox was already
305
+ # paused is not seen here and keeps reporting deduplicated.
306
+ self._cloud.send(conversation, prompt)
307
+ return "retried", current_id
275
308
  return "deduplicated", current_id
276
309
  self._cloud.send(conversation, prompt)
277
310
  return "resumed", current_id
278
311
 
279
- def _deliver_local(self, conversation_id, prompt, same_delivery, same_head) -> str:
312
+ def _deliver_local(
313
+ self, conversation_id, prompt, same_delivery, same_head, can_retry
314
+ ) -> str:
280
315
  if self._workspace is None:
281
316
  raise RuntimeError("AgentConversationDispatcher must be used as a context")
282
317
 
@@ -321,6 +356,18 @@ class AgentConversationDispatcher:
321
356
  ):
322
357
  conversation.update_secrets(self._secrets)
323
358
  conversation.run(blocking=False)
359
+ elif (
360
+ conversation.state.execution_status
361
+ == ConversationExecutionStatus.ERROR
362
+ and can_retry
363
+ ):
364
+ # The delivery matched but its conversation died before
365
+ # the work finished, so send the revision again rather
366
+ # than strand it.
367
+ conversation.update_secrets(self._secrets)
368
+ conversation.send_message(prompt)
369
+ conversation.run(blocking=False)
370
+ disposition = "retried"
324
371
  else:
325
372
  disposition = "deduplicated"
326
373
  else:
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openhands-extensions"
3
- version = "0.27.0"
3
+ version = "0.29.0"
4
4
  description = "OpenHands extensions, plugins, and skills (Python bindings for the integration catalog)"
5
5
  requires-python = ">=3.12"
6
6
  dependencies = [
@@ -18,7 +18,7 @@ from importlib.metadata import PackageNotFoundError, version
18
18
  #: Kept in lock-step with pyproject.toml/package.json by the version test.
19
19
  #: release-please only rewrites a semver found on the annotated line itself, so
20
20
  #: the annotation must stay on the assignment; on its own line it is a no-op.
21
- _FALLBACK_VERSION = "0.27.0" # x-release-please-version
21
+ _FALLBACK_VERSION = "0.29.0" # x-release-please-version
22
22
 
23
23
  try:
24
24
  __version__: str = version("openhands-extensions")
@@ -25,6 +25,7 @@ from pathlib import Path
25
25
 
26
26
  SKILL_PATH = Path(__file__).resolve().parent.parent / "skills" / "openhands-sdk" / "SKILL.md"
27
27
  LLMS_TXT_URL = "https://docs.openhands.dev/llms.txt"
28
+ SDK_URL_PREFIX = "https://docs.openhands.dev/sdk/"
28
29
  GH_API = "https://api.github.com/repos/OpenHands/software-agent-sdk/contents/examples"
29
30
  GH_RAW = "https://raw.githubusercontent.com/OpenHands/software-agent-sdk/main"
30
31
  GH_BROWSE = "https://github.com/OpenHands/software-agent-sdk/blob/main/examples"
@@ -90,18 +91,20 @@ def _fetch_or(url: str, fallback, **kw):
90
91
  # -- llms.txt parsing --
91
92
 
92
93
  def parse_sdk_entries(llms_txt: str) -> list[dict]:
93
- """Extract entries from the 'OpenHands Software Agent SDK' section."""
94
- entries, in_section = [], False
95
- for line in llms_txt.splitlines():
96
- stripped = line.strip()
97
- if stripped == "## OpenHands Software Agent SDK":
98
- in_section = True
99
- continue
100
- if in_section and line.startswith("## "):
101
- break
102
- if in_section and (m := _ENTRY_RE.match(stripped)):
103
- entries.append(m.groupdict())
104
- return entries
94
+ """Extract entries for the OpenHands Software Agent SDK.
95
+
96
+ The docs index (llms.txt) no longer groups entries under an
97
+ "OpenHands Software Agent SDK" heading, so select entries by their
98
+ canonical URL prefix instead. Sort by title (case-insensitively) to
99
+ keep the generated skill stable across runs.
100
+ """
101
+ entries = [
102
+ m.groupdict()
103
+ for line in llms_txt.splitlines()
104
+ if (m := _ENTRY_RE.match(line.strip()))
105
+ and m.group("url").startswith(SDK_URL_PREFIX)
106
+ ]
107
+ return sorted(entries, key=lambda e: e["title"].lower())
105
108
 
106
109
 
107
110
  # -- content builders --
@@ -123,7 +126,7 @@ def build_classes_table(entries: list[dict]) -> str:
123
126
  def build_api_refs(entries: list[dict]) -> str:
124
127
  return ", ".join(
125
128
  f"[`{e['title']}`]({e['url']})"
126
- for e in entries if "/api-reference/" in e["url"]
129
+ for e in entries if "/sdk/api-reference/" in e["url"]
127
130
  )
128
131
 
129
132
 
@@ -11,6 +11,8 @@ This skill is activated by the following keywords:
11
11
 
12
12
  ## Details
13
13
 
14
+ Use authenticated Azure DevOps MCP tools first when they are available in the agent's environment. Detect the connection through tool availability rather than a particular server name. Fall back to the documented `AZURE_DEVOPS_TOKEN` and direct API flow when those tools are unavailable or raw API or `curl` access is explicitly needed.
15
+
14
16
  You have access to an environment variable, `AZURE_DEVOPS_TOKEN`, which allows you to interact with
15
17
  the Azure DevOps API.
16
18
 
@@ -52,4 +54,4 @@ Common API endpoints:
52
54
  - List repositories: `https://dev.azure.com/{organization}/{project}/_apis/git/repositories?api-version=7.1`
53
55
  - Get repository details: `https://dev.azure.com/{organization}/{project}/_apis/git/repositories/{repositoryId}?api-version=7.1`
54
56
  - List pull requests: `https://dev.azure.com/{organization}/{project}/_apis/git/pullrequests?api-version=7.1`
55
- - Create pull request: `https://dev.azure.com/{organization}/{project}/_apis/git/repositories/{repositoryId}/pullrequests?api-version=7.1` (POST)
57
+ - Create pull request: `https://dev.azure.com/{organization}/{project}/_apis/git/repositories/{repositoryId}/pullrequests?api-version=7.1` (POST)
@@ -6,6 +6,10 @@ triggers:
6
6
  - azure
7
7
  ---
8
8
 
9
+ ## Connection selection
10
+
11
+ Use authenticated Azure DevOps MCP tools first when they are available in the agent's environment. Detect this by tool availability, without depending on a particular MCP server name. Use the token and direct API instructions below only when Azure DevOps MCP tools are unavailable or when raw API or `curl` access is explicitly needed.
12
+
9
13
  You have access to an environment variable, `AZURE_DEVOPS_TOKEN`, which allows you to interact with
10
14
  the Azure DevOps API.
11
15
 
@@ -3,6 +3,8 @@
3
3
  Bitbucket integration **hub** skill. It detects whether the repository is on Bitbucket
4
4
  Cloud or Bitbucket Data Center and directs the agent to the matching detailed skill.
5
5
 
6
+ Authenticated Bitbucket MCP tools are preferred when available, detected generically by tool availability rather than a particular server name. The hub still routes to Cloud or Data Center, and the documented token and direct API paths remain the fallback when MCP tools are unavailable or raw API or `curl` access is explicitly needed.
7
+
6
8
  ## Triggers
7
9
 
8
10
  This skill is activated by the following keywords:
@@ -30,4 +32,4 @@ when the words "data center" never appear. It then:
30
32
  The hub also carries a small quick-reference table as a fallback in case the detailed skill
31
33
  cannot be loaded.
32
34
 
33
- See [`SKILL.md`](SKILL.md) for the full content.
35
+ See [`SKILL.md`](SKILL.md) for the full content.
@@ -5,6 +5,8 @@ triggers:
5
5
  - bitbucket
6
6
  ---
7
7
 
8
+ Use authenticated Bitbucket MCP tools first when they are available in the agent's environment. Detect this by tool availability, without depending on a particular MCP server name. Still identify Cloud versus Data Center and load the matching detailed skill. Use the token and direct API instructions only when Bitbucket MCP tools are unavailable or when raw API or `curl` access is explicitly needed.
9
+
8
10
  You are working with **Bitbucket**, which ships as two distinct products that behave
9
11
  differently:
10
12
 
@@ -19,7 +21,7 @@ detailed skill for full instructions.
19
21
 
20
22
  ## Step 1 — Detect which Bitbucket you are on
21
23
 
22
- Check which token environment variable is present. Environment variable names are
24
+ First use the repository host or available Bitbucket MCP tool context to identify the product. For the token fallback, check which token environment variable is present. Environment variable names are
23
25
  case-sensitive, so look for it case-insensitively:
24
26
 
25
27
  ```bash
@@ -53,4 +55,4 @@ Bitbucket API (not a web browser) and always use the listed PR tool to open a pu
53
55
  | Host | `bitbucket.org` | self-hosted domain |
54
56
  | REST API base | `https://api.bitbucket.org/2.0` | `https://<host>/rest/api/1.0` |
55
57
  | Repository identifier | `workspace/repo_slug` | `PROJECT/repo_slug` (project key) |
56
- | Pull request tool | `create_bitbucket_pr` | `create_bitbucket_data_center_pr` |
58
+ | Pull request tool | `create_bitbucket_pr` | `create_bitbucket_data_center_pr` |
@@ -11,5 +11,7 @@ it detects a Bitbucket Cloud environment (i.e. `BITBUCKET_TOKEN` is set).
11
11
 
12
12
  ## Details
13
13
 
14
+ Use authenticated Bitbucket Cloud MCP tools first when they are available, detected by tool availability rather than a particular server name. Fall back to the documented `BITBUCKET_TOKEN` and direct REST API flow when those tools are unavailable or raw API or `curl` access is explicitly needed.
15
+
14
16
  See [`SKILL.md`](SKILL.md) for the full content, including authenticated git remote
15
17
  construction and pull request instructions.
@@ -3,6 +3,8 @@ name: bitbucket-cloud
3
3
  description: Bitbucket Cloud (bitbucket.org) specifics — authenticate with BITBUCKET_TOKEN, use the REST API v2, workspace/repo_slug repositories, and the create_bitbucket_pr tool. Loaded on demand by the bitbucket skill once a Cloud environment is detected.
4
4
  ---
5
5
 
6
+ Use authenticated Bitbucket Cloud MCP tools first when they are available in the agent's environment. Detect this by tool availability, without depending on a particular MCP server name. Use the token and direct REST API instructions below only when those tools are unavailable or when raw API or `curl` access is explicitly needed.
7
+
6
8
  You are working with **Bitbucket Cloud** (`bitbucket.org`). You have access to an
7
9
  environment variable, `BITBUCKET_TOKEN`, which allows you to interact with the Bitbucket
8
10
  Cloud API.
@@ -12,6 +12,8 @@ it detects a Bitbucket Data Center environment (i.e. `BITBUCKET_DATA_CENTER_TOKE
12
12
 
13
13
  ## Details
14
14
 
15
+ Use authenticated Bitbucket Data Center MCP tools first when they are available, detected by tool availability rather than a particular server name. Fall back to the documented `BITBUCKET_DATA_CENTER_TOKEN` and direct REST API flow when those tools are unavailable or raw API or `curl` access is explicitly needed.
16
+
15
17
  See [`SKILL.md`](SKILL.md) for the full content, including authenticated `scm/` git remote
16
18
  construction and pull request instructions. Note the key differences from Bitbucket Cloud:
17
19
  the REST API is version `1.0` under `https://{domain}/rest/api/1.0`, repositories are
@@ -3,6 +3,8 @@ name: bitbucket-data-center
3
3
  description: Bitbucket Data Center (self-hosted Bitbucket Server) specifics — authenticate with BITBUCKET_DATA_CENTER_TOKEN, use the REST API 1.0, PROJECT/repo_slug repositories, scm/ git remotes, and the create_bitbucket_data_center_pr tool. Loaded on demand by the bitbucket skill once a Data Center environment is detected.
4
4
  ---
5
5
 
6
+ Use authenticated Bitbucket Data Center MCP tools first when they are available in the agent's environment. Detect this by tool availability, without depending on a particular MCP server name. Use the token and direct REST API instructions below only when those tools are unavailable or when raw API or `curl` access is explicitly needed.
7
+
6
8
  You are working with **Bitbucket Data Center** (self-hosted Bitbucket Server). You have
7
9
  access to an environment variable, `BITBUCKET_DATA_CENTER_TOKEN`, which contains a basic
8
10
  auth token in the format `username:your-token` that allows you to interact with the git
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: canvas-extension-api
3
- description: This skill should be used when the user asks to "create an OpenHands App", "scaffold a Canvas App", "build an app with the Canvas Extensions API", "build an Agent Canvas App", "bundle a single-file Canvas App", "build a Sidecar-backed Canvas App", "add onboarding for an App service", "add a custom interface to Agent Canvas", "validate an OpenHands App", or mentions Canvas Apps, Agent Canvas extensions, Blob-importable app bundles, registerPage, app pages, Sidecars, or canvas extension packages.
3
+ description: This skill should be used when the user asks to "create an OpenHands App", "scaffold a Canvas App", "build an app with the Canvas Extensions API", "build an Agent Canvas App", "bundle a single-file Canvas App", "build a Sidecar-backed Canvas App", "add onboarding for an App service", "add a custom interface to Agent Canvas", "validate an OpenHands App", "make a Canvas App look native", "match Agent Canvas styles", "style a canvas extension", "reuse OpenHands design tokens", "add native Tailwind classes to an app", or mentions Canvas Apps, Agent Canvas extensions, Blob-importable app bundles, registerPage, app pages, Sidecars, or canvas extension packages.
4
4
  ---
5
5
 
6
6
  # Canvas Extensions API
@@ -195,6 +195,8 @@ Assume activation, mounting, and disposal may happen repeatedly during hot enabl
195
195
 
196
196
  Render within the supplied `container`; do not replace unrelated Canvas DOM. Scope CSS under an app-specific root class. Prefer Canvas CSS variables with sensible fallbacks rather than copying host implementation classes.
197
197
 
198
+ Read `references/native-design.md` before styling an App page. It covers the core rule (reuse tokens, not host internals), the three implementation paths (dependency-free, Tailwind-bundled, existing CSS), the Tailwind v4 theme mapping to host variables, layout and density conventions, copy-ready component recipes, the raw scoped CSS alternative, and a mount-safe style helper. Treat it as the authoritative design reference for Apps.
199
+
198
200
  Provide:
199
201
 
200
202
  - semantic structure and headings inside the supplied container. Canvas already owns the outer `<main aria-label={page title}>`, so do not add a nested `main` or duplicate page landmark label;
@@ -281,5 +283,6 @@ Summarize:
281
283
  - `references/testing-and-installation.md` - test strategy, manual Canvas workflow, and installation coordinates.
282
284
  - `references/packaging-recipes.md` - one-file Vite, CSS, assets, Workers, and WASM rules.
283
285
  - `references/backend-safety.md` - authenticated requests, command safety, persistence, and prerequisite onboarding.
286
+ - `references/native-design.md` - native Agent Canvas styling: token reuse, Tailwind mapping, layout, component recipes, scoped CSS, and style injection.
284
287
  - `references/acceptance-checklist.md` - automated checks and the local install/enable/reload lifecycle.
285
288
  - `scripts/validate-extension.mjs` - dependency-free static artifact validator for an App package directory.
@@ -0,0 +1,250 @@
1
+ # Native OpenHands App design
2
+
3
+ Build Apps for Agent Canvas that visually belong in the host while remaining a self-contained, safe extension package. Use this reference together with the main `SKILL.md` workflow before changing an App's UI.
4
+
5
+ ## Core rule: reuse tokens, not host internals
6
+
7
+ Treat an App bundle as an independent frontend. Do not import Agent Canvas React components, utility modules, CSS files, font files, Tailwind configuration, aliases, or private DOM classes. The host imports the App as a self-contained Blob ESM module, so those imports will fail or create fragile coupling.
8
+
9
+ Reuse the host's stable CSS custom properties instead. Scope every selector under a unique App root class and provide literal fallbacks so the page stays legible outside a current Canvas build. Tailwind utility names such as `bg-base` or `text-contrast` are **not globally available** to an App: they work only after the App's own Tailwind build maps them to the host variables.
10
+
11
+ Use semantic names (`surface`, `foreground`, `border`, `primary`) rather than copying cool-grey hex values.
12
+
13
+ ## Choose the implementation path
14
+
15
+ Choose the smallest path that matches the App package:
16
+
17
+ | App implementation | Recommended styling approach |
18
+ | --- | --- |
19
+ | Dependency-free JavaScript | Inject one scoped CSS string from the entrypoint. Use host custom properties directly. |
20
+ | React/TypeScript with Tailwind | Add an App-local Tailwind theme mapping to the host variables, then compile every class into the single `extension.js` artifact. |
21
+ | Existing CSS module or CSS-in-JS setup | Define the semantic colors and component recipes in the App's own scoped stylesheet. |
22
+
23
+ Never expect the host Tailwind compiler to scan or compile App source. Never use a CDN Tailwind runtime. Never attach App styles to `:root`, `html`, `body`, or global element selectors.
24
+
25
+ ## Tailwind setup for a bundled App
26
+
27
+ For Tailwind v4, place an App-local mapping in the App stylesheet. This lets the App use Canvas-like classes while preserving portability:
28
+
29
+ ```css
30
+ @import "tailwindcss";
31
+
32
+ @theme inline {
33
+ --color-base: var(--oh-color-base);
34
+ --color-base-secondary: var(--oh-color-base-secondary);
35
+ --color-surface: var(--oh-surface);
36
+ --color-surface-raised: var(--oh-surface-raised);
37
+ --color-foreground: var(--oh-foreground);
38
+ --color-contrast: var(--oh-contrast);
39
+ --color-muted: var(--oh-muted);
40
+ --color-tertiary-alt: var(--oh-text-dim);
41
+ --color-border: var(--oh-border);
42
+ --color-border-subtle: var(--oh-border-subtle);
43
+ --color-focus: var(--oh-focus);
44
+ --color-interactive-hover: var(--oh-interactive-hover);
45
+ --color-primary: var(--oh-color-primary);
46
+ --color-on-primary: var(--oh-accent-foreground);
47
+ --color-danger: var(--oh-danger);
48
+ }
49
+ ```
50
+
51
+ Compile this stylesheet into `extension.js`; do not leave a linked CSS asset. For an App without Tailwind, use the matching raw CSS below instead. Use utility recipes only after adding this mapping.
52
+
53
+ ## Layout and density
54
+
55
+ Let Canvas own the outer page and scrolling region. Do not add a second page shell, `h-screen`, global background, sidebar, or top navigation. Start with `min-h-full`, use `p-4 sm:p-6 lg:p-8`, and constrain dense content with `mx-auto w-full max-w-5xl`. Use `gap-4` for related controls, `gap-6` within sections, and `gap-8` between major sections. Use `rounded-lg` for controls and `rounded-xl` for cards and messages.
56
+
57
+ Avoid gradients, glass effects, oversize headings, floating shadows, dense borders, and hard-coded dark greys. The native visual language is restrained: dark layered surfaces, cool-grey borders, warm primary actions, direct typography, and short transitions.
58
+
59
+ ## Interaction and state checklist
60
+
61
+ - Apply `text-contrast` to headings and key labels; use `text-muted` or `text-tertiary-alt` for supporting copy.
62
+ - Use the primary recipe for one dominant action. Use a bordered secondary action for alternatives. Do not make every button primary.
63
+ - Use a 36px (`h-9`) control height and `rounded-lg` for input, select, and standard button controls.
64
+ - Use `focus-visible` rather than removing outlines. Preserve disabled affordance with `disabled:cursor-not-allowed disabled:opacity-60`.
65
+ - Animate only background, border, box-shadow, opacity, or transforms and include `motion-reduce:transition-none`.
66
+ - Scope icon sizing and color within the App. Do not assume HeroUI, Lucide, or host icon packages exist.
67
+ - Use `textContent`, framework escaping, or safe DOM creation for data from requests. Never inject response text with `innerHTML`.
68
+
69
+ ## Semantic token map
70
+
71
+ | Intent | Host variable | Fallback |
72
+ | --- | --- | --- |
73
+ | Deep page base | `--oh-color-base` | `#0B0E14` |
74
+ | Standard panel | `--oh-color-base-secondary` | `#21252F` |
75
+ | Raised surface | `--oh-surface-raised` | `#2C313F` |
76
+ | Main text | `--oh-contrast` | `#FFFFFF` |
77
+ | Standard text | `--oh-foreground` | `#EEF2F7` |
78
+ | Secondary text | `--oh-muted` | `#A3B0C4` |
79
+ | Muted placeholder text | `--oh-text-dim` | `#7E8A9E` |
80
+ | Standard border | `--oh-border` | `#4B5468` |
81
+ | Subtle divider | `--oh-border-subtle` | `#383F50` |
82
+ | Focus ring | `--oh-focus` | `#FFFFFF` |
83
+ | Primary surface | `--oh-color-primary` | `#C9B974` |
84
+ | Primary text | `--oh-accent-foreground` | `#0B0E14` |
85
+ | Danger | `--oh-danger` | `#E76A5E` |
86
+ | Success | `--oh-success` | `#A5E75E` |
87
+
88
+ ## Copy-ready Tailwind recipes
89
+
90
+ Use these recipes only after mapping the semantic colors in the App-local Tailwind configuration above.
91
+
92
+ ### Content shell
93
+
94
+ ```tsx
95
+ <div className="min-h-full p-4 sm:p-6 lg:p-8 text-foreground">
96
+ <div className="mx-auto flex w-full max-w-5xl flex-col gap-8">...</div>
97
+ </div>
98
+ ```
99
+
100
+ Do not use `h-screen`, `min-h-screen`, a host-like global background, or a nested `<main>`; Canvas supplies the page landmark and scroll context.
101
+
102
+ ### Page heading
103
+
104
+ ```tsx
105
+ <header className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
106
+ <div className="min-w-0">
107
+ <h1 className="text-xl font-semibold tracking-tight text-contrast">Page title</h1>
108
+ <p className="mt-1 text-sm text-muted">Short supporting description.</p>
109
+ </div>
110
+ </header>
111
+ ```
112
+
113
+ ### Card or section
114
+
115
+ ```tsx
116
+ <section className="rounded-xl border border-border bg-base-secondary p-4 sm:p-6">
117
+ <h2 className="text-base font-medium text-contrast">Section title</h2>
118
+ <p className="mt-1 text-sm text-muted">Explain the section briefly.</p>
119
+ <div className="mt-5 flex flex-col gap-4">...</div>
120
+ </section>
121
+ ```
122
+
123
+ Use `border-border-subtle` for dividers inside an existing card. Avoid default card shadows.
124
+
125
+ ### Controls and buttons
126
+
127
+ ```tsx
128
+ const control =
129
+ "h-9 min-h-9 rounded-lg transition-[background-color,border-color,box-shadow,opacity] duration-75 motion-reduce:transition-none";
130
+
131
+ const field = `${control} w-full min-w-0 border border-border bg-base-secondary px-3 text-sm text-contrast placeholder:text-tertiary-alt focus:border-contrast/40 focus:ring-1 focus:ring-contrast/20 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60`;
132
+
133
+ const primaryButton = `${control} inline-flex w-fit items-center justify-center gap-2 bg-primary px-3 text-sm font-normal text-on-primary hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-30`;
134
+
135
+ const secondaryButton = `${control} inline-flex w-fit items-center justify-center gap-2 border border-border bg-base-secondary px-3 text-sm font-normal text-contrast hover:bg-surface-raised disabled:cursor-not-allowed disabled:opacity-30`;
136
+ ```
137
+
138
+ Use `label` plus `htmlFor`, keep field hints in `text-xs text-muted`, and express validation with `aria-invalid`, a `border-red-500`, and a linked `role="alert"` message using `text-xs text-red-400`.
139
+
140
+ ### Toolbar and responsive grid
141
+
142
+ ```tsx
143
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
144
+ <div className="min-w-0">...</div>
145
+ <div className="flex flex-wrap items-center gap-2">...</div>
146
+ </div>
147
+
148
+ <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">...</div>
149
+ ```
150
+
151
+ ### Informational, error, and empty state
152
+
153
+ ```tsx
154
+ <div className="rounded-xl border border-border bg-base-secondary p-6 text-center">
155
+ <h2 className="text-base font-medium text-contrast">Nothing here yet</h2>
156
+ <p className="mt-2 text-sm text-muted">Describe the next useful action.</p>
157
+ <button className={`${primaryButton} mt-4`}>Create item</button>
158
+ </div>
159
+
160
+ <div role="alert" className="rounded-xl border border-red-500/40 bg-red-500/10 px-4 py-3 text-sm text-red-200">
161
+ The request could not be completed.
162
+ </div>
163
+ ```
164
+
165
+ Use `animate-pulse` with `motion-reduce:animate-none` only for short loading placeholders; reserve space with shapes matching loaded cards or rows.
166
+
167
+ ### Compact tabs
168
+
169
+ ```tsx
170
+ <div role="tablist" aria-label="View options" className="flex w-fit gap-1 rounded-lg border border-border bg-base-secondary p-1">
171
+ <button role="tab" aria-selected="true" className="rounded-md bg-interactive-hover px-3 py-1.5 text-sm text-contrast">Overview</button>
172
+ <button role="tab" aria-selected="false" className="rounded-md px-3 py-1.5 text-sm text-muted hover:bg-surface-raised hover:text-contrast">Activity</button>
173
+ </div>
174
+ ```
175
+
176
+ Implement keyboard tab behavior when tabs change content; use regular buttons for simple filters.
177
+
178
+ ## Raw scoped CSS alternative
179
+
180
+ Use this approach for dependency-free Apps. Apply it only after inserting an App root such as `.acme-dashboard`.
181
+
182
+ ```css
183
+ .acme-dashboard {
184
+ color: var(--oh-foreground, #EEF2F7);
185
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
186
+ }
187
+ .acme-dashboard .card {
188
+ border: 1px solid var(--oh-border, #4B5468);
189
+ border-radius: 12px;
190
+ background: var(--oh-color-base-secondary, #21252F);
191
+ padding: 1.5rem;
192
+ }
193
+ .acme-dashboard .field,
194
+ .acme-dashboard .button {
195
+ min-height: 2.25rem;
196
+ border-radius: 8px;
197
+ transition: background-color 75ms, border-color 75ms, box-shadow 75ms, opacity 75ms;
198
+ }
199
+ .acme-dashboard .field {
200
+ width: 100%; border: 1px solid var(--oh-border, #4B5468);
201
+ background: var(--oh-color-base-secondary, #21252F);
202
+ color: var(--oh-contrast, #FFF); padding: 0 0.75rem;
203
+ }
204
+ .acme-dashboard .field:focus-visible,
205
+ .acme-dashboard .button:focus-visible {
206
+ outline: none; border-color: color-mix(in srgb, var(--oh-contrast, #FFF) 40%, transparent);
207
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--oh-contrast, #FFF) 20%, transparent);
208
+ }
209
+ .acme-dashboard .button--primary {
210
+ border: 0; background: var(--oh-color-primary, #C9B974);
211
+ color: var(--oh-accent-foreground, #0B0E14); padding: 0 0.75rem;
212
+ }
213
+ @media (prefers-reduced-motion: reduce) {
214
+ .acme-dashboard .field, .acme-dashboard .button { transition: none; }
215
+ }
216
+ ```
217
+
218
+ ## Mount-safe injected style helper
219
+
220
+ Use one marker per mounted App root and remove the node during cleanup.
221
+
222
+ ```js
223
+ function installStyles(css, marker) {
224
+ const style = document.createElement("style");
225
+ style.dataset.canvasAppStyle = marker;
226
+ style.textContent = css;
227
+ document.head.append(style);
228
+ return () => style.remove();
229
+ }
230
+
231
+ // During mount:
232
+ const disposeStyles = installStyles(STYLES, "acme-dashboard");
233
+ const root = document.createElement("div");
234
+ root.className = "acme-dashboard";
235
+ container.append(root);
236
+ return () => {
237
+ disposeStyles();
238
+ root.remove();
239
+ };
240
+ ```
241
+
242
+ If more than one page can mount concurrently, ref-count the marker or inject its stylesheet once in `activate` and remove it in the activation disposer.
243
+
244
+ ## Verify before delivery
245
+
246
+ - Confirm extension markup uses a unique root class and no App CSS leaks outside it.
247
+ - Confirm styles survive a real Canvas mount, route remount, disable/re-enable cycle, and host dark surface backgrounds.
248
+ - Confirm `extension.js` has no unresolved CSS import, bare package import, extra CSS chunk, or Node global.
249
+ - Run the Canvas extension static validator plus the App's lint, tests, and build.
250
+ - Report whether the App uses direct host variables or an App-local Tailwind mapping, and list the tested states.
@@ -11,6 +11,8 @@ This skill is activated by the following keywords:
11
11
 
12
12
  ## Details
13
13
 
14
+ Use authenticated GitHub MCP tools first when they are available in the agent's environment. Detect the connection through tool availability rather than a particular server name. Fall back to the documented `GITHUB_TOKEN` and direct API flow when those tools are unavailable or raw API or `curl` access is explicitly needed.
15
+
14
16
  You have access to an environment variable, `GITHUB_TOKEN`, which allows you to interact with
15
17
  the GitHub API.
16
18
 
@@ -39,4 +41,6 @@ Here are some instructions for pushing, but ONLY do this if the user asks you to
39
41
  ```bash
40
42
  git remote -v && git branch # to find the current org, repo and branch
41
43
  git checkout -b create-widget && git add . && git commit -m "Create widget" && git push -u origin create-widget
42
- ```
44
+ ```
45
+
46
+ When asked to merge a pull request, the skill uses GitHub's async merge API (`PUT /repos/{owner}/{repo}/pulls/{pull_number}/merge-async`, then poll the returned UUID). It supports stacked PRs and merge queues. Docs: https://docs.github.com/en/rest/pulls/pulls#merge-a-pull-request-asynchronously
@@ -5,6 +5,10 @@ triggers:
5
5
  - github
6
6
  ---
7
7
 
8
+ ## Connection selection
9
+
10
+ Use authenticated GitHub MCP tools first when they are available in the agent's environment. Detect this by tool availability, without depending on a particular MCP server name. Use the token and direct API instructions below only when GitHub MCP tools are unavailable or when raw API or `curl` access is explicitly needed.
11
+
8
12
  You have access to an environment variable, `GITHUB_TOKEN`, which allows you to interact with
9
13
  the GitHub API.
10
14
 
@@ -37,6 +41,29 @@ git remote -v && git branch # to find the current org, repo and branch
37
41
  git checkout -b create-widget && git add . && git commit -m "Create widget" && git push -u origin create-widget
38
42
  ```
39
43
 
44
+ ## Merging Pull Requests (async merge API)
45
+
46
+ IMPORTANT:
47
+ Only merge if the user asks you to, without ambiguity.
48
+
49
+ GitHub released an async merge API in October 2026; it is now the
50
+ recommended way to merge PRs programmatically, and the only merge API that supports stacked PRs. It can also add a PR to a merge queue.
51
+ Prefer it over `PUT .../pulls/{n}/merge` and the GraphQL `mergePullRequest` mutation.
52
+ A GitHub MCP merge tool may still use the older synchronous endpoint; for stacked PRs or merge queues, use the calls below.
53
+ Docs: https://docs.github.com/en/rest/pulls/pulls#merge-a-pull-request-asynchronously
54
+
55
+ 1. Request the merge (`merge_action`: `default` uses the merge queue if the branch has one, else merges directly; or `direct_merge` / `merge_queue`). `merge_method`, `commit_title` and `commit_message` apply only to direct merges:
56
+ ```bash
57
+ gh api -X PUT repos/<OWNER>/<REPO>/pulls/<PR_NUMBER>/merge-async -f merge_method=squash -f sha=<HEAD_SHA>
58
+ ```
59
+ 2. A `202` returns `details.uuid`; poll it every few seconds until `status` is no longer `pending`:
60
+ ```bash
61
+ gh api repos/<OWNER>/<REPO>/pulls/<PR_NUMBER>/merge-async/<UUID>
62
+ ```
63
+
64
+ Final `status` is `merged`, `enqueued` (added to the merge queue, NOT merged yet), or `failed` (see `details.message`).
65
+ A `200` on the PUT means the PR is already merged or queued; a `409` means a merge request is already pending (its UUID is returned).
66
+
40
67
  ## Handling Review Comments
41
68
 
42
69
  - Critically evaluate each review comment before acting on it. Not all feedback is worth implementing: