@openhands/extensions 0.3.0 → 0.4.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 (36) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/README.md +3 -2
  3. package/automations/catalog/github-pr-reviewer.json +1 -1
  4. package/automations/catalog/github-repo-monitor.json +1 -1
  5. package/marketplaces/openhands-extensions.json +13 -0
  6. package/package.json +1 -1
  7. package/plugins/issue-duplicate-checker/action.yml +2 -2
  8. package/plugins/pr-review/README.md +1 -1
  9. package/plugins/qa-changes/action.yml +4 -2
  10. package/plugins/release-notes/action.yml +3 -2
  11. package/plugins/release-notes/workflows/release-notes.yml +10 -4
  12. package/plugins/vulnerability-remediation/README.md +4 -2
  13. package/plugins/vulnerability-remediation/action.yml +8 -4
  14. package/pyproject.toml +1 -1
  15. package/scripts/sync_extensions.py +2 -1
  16. package/skills/github-actions/.plugin/plugin.json +19 -0
  17. package/skills/github-actions/README.md +190 -0
  18. package/skills/github-actions/SKILL.md +44 -0
  19. package/skills/github-repo-monitor/README.md +1 -1
  20. package/skills/github-repo-monitor/SKILL.md +9 -9
  21. package/skills/github-repo-monitor/references/github-api.md +3 -3
  22. package/skills/github-repo-monitor/scripts/main.py +11 -11
  23. package/skills/index.js +14 -2
  24. package/skills/slack-channel-monitor/README.md +2 -1
  25. package/skills/slack-channel-monitor/SKILL.md +22 -4
  26. package/tests/test_sync_extensions.py +14 -0
  27. /package/plugins/issue-duplicate-checker/scripts/{post_duplicate_notice.js → post_duplicate_notice.cjs} +0 -0
  28. /package/plugins/issue-duplicate-checker/scripts/{remove_duplicate_candidate_label.js → remove_duplicate_candidate_label.cjs} +0 -0
  29. /package/skills/github-pr-reviewer/commands/{pr-reviewer:setup.md → pr-reviewer-setup.md} +0 -0
  30. /package/skills/github-repo-monitor/commands/{github-monitor:poll.md → github-monitor-poll.md} +0 -0
  31. /package/skills/incident-retrospective/commands/{incident-retro:setup.md → incident-retro-setup.md} +0 -0
  32. /package/skills/linear-triage/commands/{linear-triage:setup.md → linear-triage-setup.md} +0 -0
  33. /package/skills/openhands-automation/commands/{automation:create.md → automation-create.md} +0 -0
  34. /package/skills/research-brief/commands/{research-brief:setup.md → research-brief-setup.md} +0 -0
  35. /package/skills/slack-channel-monitor/commands/{slack-monitor:poll.md → slack-monitor-poll.md} +0 -0
  36. /package/skills/slack-standup-digest/commands/{standup-digest:setup.md → standup-digest-setup.md} +0 -0
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.3.0"
2
+ ".": "0.4.0"
3
3
  }
package/README.md CHANGED
@@ -60,7 +60,7 @@ See [`integrations/README.md`](integrations/README.md), [`automations/README.md`
60
60
  ## Extensions Catalog
61
61
 
62
62
  <!-- BEGIN AUTO-GENERATED CATALOG -->
63
- This repository contains **2 marketplace(s)** with **56 extensions** (46 skills, 10 plugins).
63
+ This repository contains **2 marketplace(s)** with **57 extensions** (47 skills, 10 plugins).
64
64
 
65
65
  ### large-codebase
66
66
 
@@ -79,7 +79,7 @@ OpenHands skills for interacting, improving, and refactoring large codebases
79
79
 
80
80
  Official skills and plugins for OpenHands — the open-source AI software engineer.
81
81
 
82
- **52 extensions** (44 skills, 8 plugins)
82
+ **53 extensions** (45 skills, 8 plugins)
83
83
 
84
84
  | Name | Type | Description | Commands |
85
85
  |------|------|-------------|----------|
@@ -100,6 +100,7 @@ Official skills and plugins for OpenHands — the open-source AI software engine
100
100
  | flarglebargle | skill | A test skill that responds to the magic word 'flarglebargle' with a compliment. Use for testing skill activation and ... | — |
101
101
  | frontend-design | skill | Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks ... | — |
102
102
  | github | skill | Interact with GitHub repositories, pull requests, issues, and workflows using the GITHUB_TOKEN environment variable a... | — |
103
+ | github-actions | skill | Create, debug, and test GitHub Actions workflows and custom actions. Use when building CI/CD pipelines, automating wo... | — |
103
104
  | github-pr-review | skill | Post structured PR reviews to GitHub with inline comments/suggestions in a single API call. | `/github-pr-review` |
104
105
  | github-pr-reviewer | skill | Create an automation that reviews GitHub pull requests when they are opened or updated. Inspects the diff, changed fi... | `/pr-reviewer:setup` |
105
106
  | github-repo-monitor | skill | Create a cron automation that polls a GitHub repository for issue and PR comments containing a configurable trigger p... | `/github-monitor:poll` |
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "github-pr-reviewer",
3
- "name": "GitHub PR review copilot",
3
+ "name": "GitHub Code Review Agent",
4
4
  "category": "Code review",
5
5
  "description": "Watch pull requests, inspect the diff, and leave a concise review with risks and suggested follow-ups.",
6
6
  "requiredIntegrationIds": [
@@ -7,5 +7,5 @@
7
7
  "popularityRank": 98,
8
8
  "estimatedSetupMinutes": 5,
9
9
  "prompt": "/github-monitor:poll",
10
- "exampleImplementation": "Trigger: cron, every minute (configurable)\nRequired secret: GITHUB_TOKEN\n\n1. Poll GitHub for new issue and PR comments since the last run.\n2. Match comments containing the trigger phrase (case-insensitive, default: @OpenHands).\n3. Post an acknowledgment comment with a link to the new OpenHands conversation.\n4. Forward follow-up replies in the same thread to the running conversation.\n5. Post the agent's final response back to GitHub when the conversation completes."
10
+ "exampleImplementation": "Trigger: cron, every minute (configurable)\nRequired secret: GITHUB_PERSONAL_ACCESS_TOKEN\n\n1. Poll GitHub for new issue and PR comments since the last run.\n2. Match comments containing the trigger phrase (case-insensitive, default: @OpenHands).\n3. Post an acknowledgment comment with a link to the new OpenHands conversation.\n4. Forward follow-up replies in the same thread to the running conversation.\n5. Post the agent's final response back to GitHub when the conversation completes."
11
11
  }
@@ -238,6 +238,19 @@
238
238
  "workflows"
239
239
  ]
240
240
  },
241
+ {
242
+ "name": "github-actions",
243
+ "source": "./skills/github-actions",
244
+ "description": "Create, debug, and test GitHub Actions workflows and custom actions. Use when building CI/CD pipelines, automating workflows, or troubleshooting GitHub Actions.",
245
+ "category": "integration",
246
+ "keywords": [
247
+ "github-actions",
248
+ "workflows",
249
+ "ci-cd",
250
+ "automation",
251
+ "actions"
252
+ ]
253
+ },
241
254
  {
242
255
  "name": "github-pr-review",
243
256
  "source": "./skills/github-pr-review",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openhands/extensions",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Public OpenHands extension catalogs for skills, plugins, integrations, and automation templates.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -258,7 +258,7 @@ runs:
258
258
  if: inputs.mode == 'issue-check' && steps.parsed_result.outputs.should_comment == 'true'
259
259
  uses: actions/github-script@v9
260
260
  env:
261
- ACTION_SCRIPT: ${{ github.action_path }}/scripts/post_duplicate_notice.js
261
+ ACTION_SCRIPT: ${{ github.action_path }}/scripts/post_duplicate_notice.cjs
262
262
  ISSUE_NUMBER: ${{ inputs.issue-number }}
263
263
  SUMMARY: ${{ steps.parsed_result.outputs.summary }}
264
264
  CANDIDATE_ISSUES_JSON: ${{ steps.parsed_result.outputs.candidate_issues_json }}
@@ -341,7 +341,7 @@ runs:
341
341
  if: inputs.mode == 'remove-label'
342
342
  uses: actions/github-script@v9
343
343
  env:
344
- ACTION_SCRIPT: ${{ github.action_path }}/scripts/remove_duplicate_candidate_label.js
344
+ ACTION_SCRIPT: ${{ github.action_path }}/scripts/remove_duplicate_candidate_label.cjs
345
345
  with:
346
346
  github-token: ${{ inputs.github-token }}
347
347
  script: |
@@ -69,7 +69,7 @@ Add the following secrets in your repository settings (**Settings → Secrets an
69
69
  | `GITHUB_TOKEN` | Auto | Provided automatically by GitHub Actions |
70
70
  | `LMNR_SKILLS_API_KEY` | No | Laminar API key (org-level secret; mapped to `LMNR_PROJECT_API_KEY` env var in workflows) |
71
71
 
72
- **Note**: For repositories that need to post review comments from a bot account, use `ALLHANDS_BOT_GITHUB_PAT` instead of `GITHUB_TOKEN`.
72
+ **Note**: To post review comments from a bot account instead of `GITHUB_TOKEN`, use a bot token scoped to the repository's trust boundary. On **public** repositories, use `OPENHANDS_BOT_GITHUB_PAT_PUBLIC` — a fine-grained token limited to public repositories. On **private** repositories, use a bot token scoped to that repository. Never give a public repository a token that can reach private repositories.
73
73
 
74
74
  ### 3. Customize the Workflow (Optional)
75
75
 
@@ -119,6 +119,8 @@ runs:
119
119
  env:
120
120
  LLM_API_KEY: ${{ inputs.llm-api-key }}
121
121
  GITHUB_TOKEN: ${{ inputs.github-token }}
122
+ GITHUB_EVENT_PULL_REQUEST_TITLE: ${{ github.event.pull_request.title }}
123
+ INPUTS_LLM_MODEL: ${{ inputs.llm-model }}
122
124
  run: |
123
125
  if [ -z "$LLM_API_KEY" ]; then
124
126
  echo "Error: llm-api-key is required."
@@ -130,9 +132,9 @@ runs:
130
132
  fi
131
133
 
132
134
  echo "PR Number: ${{ github.event.pull_request.number }}"
133
- echo "PR Title: ${{ github.event.pull_request.title }}"
135
+ echo "PR Title: $GITHUB_EVENT_PULL_REQUEST_TITLE"
134
136
  echo "Repository: ${{ github.repository }}"
135
- echo "LLM model: ${{ inputs.llm-model }}"
137
+ echo "LLM model: $INPUTS_LLM_MODEL"
136
138
 
137
139
  - name: Run QA validation
138
140
  if: steps.preflight.outputs.skip != 'true'
@@ -109,9 +109,10 @@ runs:
109
109
  shell: bash
110
110
  env:
111
111
  GITHUB_TOKEN: ${{ inputs.github-token }}
112
+ INPUTS_TAG: ${{ inputs.tag }}
112
113
  run: |
113
114
  if [ -f release_notes.md ]; then
114
- echo "Updating release notes for tag ${{ inputs.tag }}"
115
- gh release edit "${{ inputs.tag }}" --notes-file release_notes.md || \
115
+ echo "Updating release notes for tag $INPUTS_TAG"
116
+ gh release edit "$INPUTS_TAG" --notes-file release_notes.md || \
116
117
  echo "Note: Could not update release. Release may not exist yet."
117
118
  fi
@@ -38,11 +38,14 @@ jobs:
38
38
 
39
39
  - name: Determine tag
40
40
  id: get-tag
41
+ env:
42
+ GITHUB_EVENT_INPUTS_TAG: ${{ github.event.inputs.tag }}
43
+ GITHUB_REF_NAME: ${{ github.ref_name }}
41
44
  run: |
42
45
  if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
43
- echo "tag=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
46
+ echo "tag=$GITHUB_EVENT_INPUTS_TAG" >> $GITHUB_OUTPUT
44
47
  else
45
- echo "tag=${{ github.ref_name }}" >> $GITHUB_OUTPUT
48
+ echo "tag=$GITHUB_REF_NAME" >> $GITHUB_OUTPUT
46
49
  fi
47
50
 
48
51
  - name: Generate Release Notes
@@ -57,10 +60,13 @@ jobs:
57
60
  github-token: ${{ secrets.GITHUB_TOKEN }}
58
61
 
59
62
  - name: Display generated notes
63
+ env:
64
+ TAG: ${{ steps.get-tag.outputs.tag }}
65
+ PREVIOUS_TAG: ${{ steps.release-notes.outputs.previous-tag }}
60
66
  run: |
61
- echo "## Release Notes for ${{ steps.get-tag.outputs.tag }}"
67
+ echo "## Release Notes for $TAG"
62
68
  echo ""
63
- echo "Previous tag: ${{ steps.release-notes.outputs.previous-tag }}"
69
+ echo "Previous tag: $PREVIOUS_TAG"
64
70
  echo "Commits: ${{ steps.release-notes.outputs.commit-count }}"
65
71
  echo "Contributors: ${{ steps.release-notes.outputs.contributor-count }}"
66
72
  echo "New contributors: ${{ steps.release-notes.outputs.new-contributor-count }}"
@@ -90,12 +90,14 @@ on:
90
90
 
91
91
  ### Optional: Use a Bot Account
92
92
 
93
- For better PR attribution, use a bot PAT:
93
+ For better PR attribution, use a bot PAT scoped to the repository's trust boundary. On **public** repositories, use the public-scoped token:
94
94
 
95
95
  ```yaml
96
- github-token: ${{ secrets.ALLHANDS_BOT_GITHUB_PAT || secrets.GITHUB_TOKEN }}
96
+ github-token: ${{ secrets.OPENHANDS_BOT_GITHUB_PAT_PUBLIC || secrets.GITHUB_TOKEN }}
97
97
  ```
98
98
 
99
+ On **private** repositories, use a bot token scoped to that repository instead. Never give a public repository a token that can reach private repositories.
100
+
99
101
  ## Usage
100
102
 
101
103
  ### Automatic Scheduled Scans
@@ -96,6 +96,10 @@ runs:
96
96
  env:
97
97
  LLM_API_KEY: ${{ inputs.llm-api-key }}
98
98
  GITHUB_TOKEN: ${{ inputs.github-token }}
99
+ INPUTS_EXTENSIONS_VERSION: ${{ inputs.extensions-version }}
100
+ INPUTS_LLM_MODEL: ${{ inputs.llm-model }}
101
+ INPUTS_SEVERITY_THRESHOLD: ${{ inputs.severity-threshold }}
102
+ INPUTS_MAX_VULNERABILITIES: ${{ inputs.max-vulnerabilities }}
99
103
  run: |
100
104
  if [ -z "$LLM_API_KEY" ]; then
101
105
  echo "Error: llm-api-key is required."
@@ -108,10 +112,10 @@ runs:
108
112
  fi
109
113
 
110
114
  echo "Repository: ${{ github.repository }}"
111
- echo "Extensions Version: ${{ inputs.extensions-version }}"
112
- echo "LLM Model: ${{ inputs.llm-model }}"
113
- echo "Severity Threshold: ${{ inputs.severity-threshold }}"
114
- echo "Max Vulnerabilities: ${{ inputs.max-vulnerabilities }}"
115
+ echo "Extensions Version: $INPUTS_EXTENSIONS_VERSION"
116
+ echo "LLM Model: $INPUTS_LLM_MODEL"
117
+ echo "Severity Threshold: $INPUTS_SEVERITY_THRESHOLD"
118
+ echo "Max Vulnerabilities: $INPUTS_MAX_VULNERABILITIES"
115
119
 
116
120
  - name: Run vulnerability scan
117
121
  id: scan
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "extensions"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "OpenHands extensions, plugins, and skills"
5
5
  requires-python = ">=3.12"
6
6
 
@@ -143,7 +143,8 @@ def collect_needed_commands() -> list[CommandSpec]:
143
143
  meta = parse_frontmatter(skill_md.read_text())
144
144
  desc = str(meta.get("description", ""))
145
145
  for trigger in slash_triggers(meta):
146
- cmd_name = trigger.lstrip("/")
146
+ # Replace colons with dashes for cross-platform filename compatibility
147
+ cmd_name = trigger.lstrip("/").replace(":", "-")
147
148
  cmd_path = skill_dir / "commands" / f"{cmd_name}.md"
148
149
  needed.append(CommandSpec(path=cmd_path, trigger=trigger, description=desc))
149
150
  return needed
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "github-actions",
3
+ "version": "1.0.0",
4
+ "description": "Create, debug, and test GitHub Actions workflows and custom actions. Use when building CI/CD pipelines, automating workflows, or troubleshooting GitHub Actions.",
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
+ "github-actions",
14
+ "workflows",
15
+ "ci-cd",
16
+ "automation",
17
+ "actions"
18
+ ]
19
+ }
@@ -0,0 +1,190 @@
1
+ # GitHub Actions Skill
2
+
3
+ A skill focused on **effectiveness** when working with GitHub Actions — monitoring runs, reading logs, and developing actions with confidence rather than guessing.
4
+
5
+ ## Overview
6
+
7
+ This skill provides guidance for:
8
+ - Creating and structuring workflows
9
+ - Building custom composite and reusable actions
10
+ - Testing actions locally (before burning CI minutes) and in CI
11
+ - Debugging failed workflows from real evidence (logs), not assumptions
12
+ - Avoiding common pitfalls around permissions, secrets, and fork PRs
13
+
14
+ ## When to Use This Skill
15
+
16
+ - Creating new GitHub Actions workflows
17
+ - Building custom composite or reusable actions
18
+ - Debugging workflow failures
19
+ - Setting up CI/CD pipelines
20
+ - Troubleshooting permission or secret issues
21
+ - Testing actions before merging to main
22
+
23
+ ## Cost Awareness
24
+
25
+ Every workflow run consumes CI minutes (billed minutes on private repos, shared capacity on public ones).
26
+ Plan before you push:
27
+
28
+ - Prefer one targeted commit that exercises the change over multiple speculative pushes
29
+ - Use `workflow_dispatch` with inputs to manually trigger only what you need
30
+ - Use `paths:` filters and `if:` conditions so jobs only run when relevant
31
+ - Run with `act` locally before opening a PR when feasible
32
+ - Cancel obsolete runs with `concurrency:` groups (don't pay for runs you've already invalidated)
33
+ - Use the smallest matrix that proves the change works; expand only if needed
34
+
35
+ ## Develop With Confidence: The Loop
36
+
37
+ Don't change → push → hope. Use this loop:
38
+
39
+ 1. **Read** the existing workflow / action carefully. Note triggers, permissions, inputs, env, and secrets.
40
+ 2. **Reproduce locally** with `act` when possible (see below).
41
+ 3. **Add visibility** — debug steps that print non-secret inputs, refs, and intermediate state.
42
+ 4. **Push a single focused commit**, then watch the run live.
43
+ 5. **Read the full failed-job log** (not just the summary) before editing.
44
+ 6. **Form a hypothesis from the log**, change the smallest thing that tests it, push, watch again.
45
+
46
+ ## Monitoring Runs With `gh`
47
+
48
+ ```bash
49
+ # Watch the most recent run from the current branch in real time
50
+ gh run watch
51
+
52
+ # Watch a specific run
53
+ gh run watch <run-id>
54
+
55
+ # Auto-refresh PR checks until they finish (every 10s)
56
+ gh pr checks <pr-number> --watch --interval 10
57
+
58
+ # List recent runs for the repo / branch
59
+ gh run list --branch <branch> --limit 10
60
+
61
+ # Get the run ID for the latest run on a branch
62
+ gh run list --branch <branch> --limit 1 --json databaseId --jq '.[0].databaseId'
63
+ ```
64
+
65
+ ## Reading Logs (Don't Guess)
66
+
67
+ ```bash
68
+ # Full log for a run
69
+ gh run view <run-id> --log
70
+
71
+ # Just the failed steps (much shorter)
72
+ gh run view <run-id> --log-failed
73
+
74
+ # Log for a specific job (useful for matrix builds)
75
+ gh run view <run-id> --log --job <job-id>
76
+
77
+ # List jobs for a run to find the job ID
78
+ gh run view <run-id> --json jobs --jq '.jobs[] | {id, name, conclusion}'
79
+
80
+ # Rerun only the failed jobs (avoid paying to rerun green ones)
81
+ gh run rerun <run-id> --failed
82
+ ```
83
+
84
+ When a step fails, look for:
85
+ - The exact command that exited non-zero (above the `##[error]` line)
86
+ - The shell — bash on Linux, `pwsh` on Windows runners — error messages differ
87
+ - Values printed by your debug step; compare against what the workflow *thought* it was passing in
88
+
89
+ ## Adding Visibility to Actions
90
+
91
+ When creating a new action or hitting a tricky bug, add debug steps. Don't leave them in indefinitely — remove or guard them with `if: runner.debug == '1'` once the issue is understood.
92
+
93
+ ```yaml
94
+ steps:
95
+ # Print non-secret inputs and context at the start
96
+ - name: Debug - inputs and context
97
+ if: runner.debug == '1' || inputs.debug == 'true'
98
+ run: |
99
+ echo "Event: ${{ github.event_name }}"
100
+ echo "Ref: ${{ github.ref }}"
101
+ echo "SHA: ${{ github.sha }}"
102
+ echo "Actor: ${{ github.actor }}"
103
+ echo "PWD: $(pwd)"
104
+ echo "Input.my-param: ${{ inputs.my-param }}"
105
+ echo "Files:"
106
+ ls -la
107
+
108
+ # ... your real steps ...
109
+
110
+ # Verify outcome before the job ends
111
+ - name: Debug - verify outputs
112
+ if: always() && (runner.debug == '1' || inputs.debug == 'true')
113
+ run: |
114
+ echo "Generated files:"
115
+ ls -la dist/ || echo "dist/ not found"
116
+ ```
117
+
118
+ To enable `runner.debug == '1'` for a single run, re-run the workflow with **"Enable debug logging"** checked, or set the repo secret `ACTIONS_RUNNER_DEBUG=true`.
119
+
120
+ **Never** echo `${{ secrets.* }}` — GitHub masks them, but encoded forms (base64, hex, JSON-wrapped) can leak through.
121
+
122
+ ## Testing Locally With `act`
123
+
124
+ `act` ([nektos/act](https://github.com/nektos/act)) runs workflows in Docker locally (Docker must be installed and running). Use it to iterate without burning CI minutes.
125
+
126
+ ```bash
127
+ # Run the default push event
128
+ act
129
+
130
+ # Simulate a pull_request event
131
+ act pull_request
132
+
133
+ # Run a specific job
134
+ act -j build
135
+
136
+ # Pass secrets
137
+ act -s GITHUB_TOKEN="$GITHUB_TOKEN"
138
+
139
+ # Use a larger image closer to GitHub's runner
140
+ act -P ubuntu-latest=catthehacker/ubuntu:full-latest
141
+ ```
142
+
143
+ Caveats: `act` does not perfectly emulate GitHub's runners. Things that may differ: pre-installed tools, `GITHUB_TOKEN` permissions, OIDC, the `secrets.GITHUB_TOKEN` value, and macOS/Windows runners. Treat green-on-`act` as a strong signal, not a guarantee.
144
+
145
+ ## Testing Custom Actions Requires Merge to Main
146
+
147
+ A repository-local custom action referenced as `uses: ./.github/actions/my-action` works from any branch, but:
148
+
149
+ - A custom action published as `uses: owner/repo/action@ref` must have the action's files **on `ref`** to be resolvable
150
+ - `@main` or `@v1` must already exist when the workflow that consumes it runs
151
+ - This is why a brand-new action typically must land on `main` first, then be consumed from feature branches that reference `@main` (or a tag)
152
+
153
+ If you need to iterate on an action and its consumer together, point `uses:` at a SHA on your branch, or develop the action via the local `./.github/actions/…` path until it's stable.
154
+
155
+ ## Common Pitfalls
156
+
157
+ 1. **Custom action not found from a PR** — the action's ref doesn't exist yet. Merge to `main` (or push a tag), then consume.
158
+ 2. **`GITHUB_TOKEN` permissions** — defaults can be read-only. Set `permissions:` explicitly at the workflow or job level.
159
+ 3. **`pull_request` vs `pull_request_target`** — `pull_request` runs in the fork's context (no secrets); `pull_request_target` runs with repo secrets against the base — **dangerous** if you check out PR code and execute it. Don't.
160
+ 4. **Secrets in fork PRs** — even non-`*_target` workflows hide secrets from forks. Don't design workflows that require secrets to run on community PRs.
161
+ 5. **`schedule:` triggers only fire from the default branch** — a `schedule:` trigger added in a PR only fires after it lands on the default branch; `workflow_dispatch` similarly won't appear in the Actions tab UI until merged to default (though it can be triggered via API on any branch); `pull_request`-triggered workflows do run on the PR that introduces them.
162
+ 6. **Path filters are OR, not AND** — any matching path triggers; you can't require all paths.
163
+ 7. **Matrix `fail-fast: true`** (default) cancels other matrix legs on first failure — turn off when you need full coverage.
164
+ 8. **Artifacts** — files don't persist between jobs unless uploaded via `actions/upload-artifact` and downloaded in the consuming job.
165
+ 9. **`env:` scope** — env at workflow, job, and step levels all exist. Step-level shadows job-level shadows workflow-level. Surprises follow if you forget.
166
+ 10. **Pin versions** — use `@v4` (or better, a SHA) rather than `@main`. Floating refs break builds when upstream changes.
167
+
168
+ ## Best Practices
169
+
170
+ 1. **Pin action versions** — `@v4` or SHA, not `@main`
171
+ 2. **Least privilege** — set `permissions:` to the minimum needed
172
+ 3. **Debug steps when introducing or debugging** — remove or gate them with `runner.debug` afterward
173
+ 4. **Cache dependencies** — `actions/cache` saves real minutes
174
+ 5. **Concurrency** — cancel obsolete runs:
175
+ ```yaml
176
+ concurrency:
177
+ group: ${{ github.workflow }}-${{ github.ref }}
178
+ cancel-in-progress: true
179
+ ```
180
+ 6. **Timeouts** — set `timeout-minutes:` on jobs that can hang
181
+ 7. **Fail loudly** — `set -euo pipefail` in non-trivial bash steps
182
+ 8. **Document non-obvious triggers/inputs** at the top of the workflow file
183
+
184
+ ## Resources
185
+
186
+ - [GitHub Actions documentation](https://docs.github.com/en/actions)
187
+ - [`act` — run actions locally](https://github.com/nektos/act)
188
+ - [`gh run` CLI reference](https://cli.github.com/manual/gh_run)
189
+ - [github skill](https://github.com/OpenHands/extensions/tree/main/skills/github) — GitHub API, PRs, issues, and repos
190
+ - [github-pr-review skill](https://github.com/OpenHands/extensions/tree/main/skills/github-pr-review) — post structured code reviews via the GitHub API
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: github-actions
3
+ description: Create, debug, and test GitHub Actions workflows and custom actions. Use when building CI/CD pipelines, automating workflows, or troubleshooting GitHub Actions.
4
+ triggers:
5
+ - github actions
6
+ - github workflow
7
+ - actions workflow
8
+ - gh actions
9
+ - .github/workflows
10
+ ---
11
+
12
+ # GitHub Actions Guide
13
+
14
+ ## Critical Rules
15
+
16
+ **Custom Action Deployment:**
17
+ - New custom actions MUST be merged to the main branch before they can be used
18
+ - After the initial merge, they should be tested from feature branches
19
+
20
+ **Debug Steps:**
21
+ Add debug steps that print non-secret parameters when:
22
+ - Creating a new action, OR
23
+ - Troubleshooting a particularly tricky issue
24
+
25
+ (Not required for every workflow - use when needed)
26
+
27
+ ## Effectiveness Principles
28
+
29
+ Actions cost CI minutes. Be deliberate, not iterative:
30
+
31
+ 1. **Monitor, don't poll** - use `gh run watch` / `gh pr checks --watch` to follow runs live
32
+ 2. **Read logs, don't guess** - fetch the failed job's log before changing code
33
+ 3. **Print actual values** - debug steps reveal the real `inputs`/`github` context, not your assumptions
34
+ 4. **Test locally first** - `act` runs workflows on your machine and avoids burning CI minutes
35
+ 5. **Plan the smallest reproduction** - one job, minimal matrix, narrow trigger before scaling up
36
+
37
+ See [README.md](README.md) for the full debugging workflow, `gh` commands, and YAML debug-step examples.
38
+
39
+ ## Key Gotchas
40
+
41
+ 1. **Secrets unavailable in fork PRs** - `pull_request` has no secrets for forks; `pull_request_target` does but **never check out or execute fork PR code inside it** (RCE with write permissions)
42
+ 2. **Pin action versions** - Use `@v4` or SHA, not `@main` (prevents breaking changes)
43
+ 3. **Explicit permissions** - Set `permissions:` block for GITHUB_TOKEN operations
44
+ 4. **Artifacts for job-to-job data** - Files don't persist between jobs without `upload-artifact`/`download-artifact`
@@ -41,7 +41,7 @@ event-type selection, cron schedule, and automation creation automatically.
41
41
 
42
42
  ## Requirements
43
43
 
44
- - `GITHUB_TOKEN` secret set in OpenHands Settings → Secrets
44
+ - `GITHUB_PERSONAL_ACCESS_TOKEN` secret set in OpenHands Settings → Secrets
45
45
  - Classic PAT: `repo` (private repos) or `public_repo` (public repos)
46
46
  - Fine-grained PAT: Issues — Read and Write
47
47
  - The monitored repository must be accessible with that token
@@ -45,13 +45,13 @@ before proceeding:
45
45
 
46
46
  | Secret name | Token type | Minimum permissions |
47
47
  |---|---|---|
48
- | `GITHUB_TOKEN` | Classic PAT | `repo` (private repos) or `public_repo` (public repos) |
49
- | `GITHUB_TOKEN` | Fine-grained PAT | Issues: Read and Write |
48
+ | `GITHUB_PERSONAL_ACCESS_TOKEN` | Classic PAT | `repo` (private repos) or `public_repo` (public repos) |
49
+ | `GITHUB_PERSONAL_ACCESS_TOKEN` | Fine-grained PAT | Issues: Read and Write |
50
50
 
51
51
  Check with:
52
52
  ```bash
53
53
  curl -s https://api.github.com/user \
54
- -H "Authorization: Bearer $GITHUB_TOKEN" \
54
+ -H "Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN" \
55
55
  -H "Accept: application/vnd.github+json" \
56
56
  | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('login') or d.get('message'))"
57
57
  ```
@@ -71,12 +71,12 @@ function without GitHub credentials.
71
71
 
72
72
  Follow these steps in order.
73
73
 
74
- ### Step 1 - Verify GITHUB_TOKEN
74
+ ### Step 1 - Verify GITHUB_PERSONAL_ACCESS_TOKEN
75
75
 
76
76
  Fetch the secret and run the `curl` check above.
77
77
 
78
78
  - If the secret is absent: tell the user
79
- *"GITHUB_TOKEN is not set. Please add it in OpenHands Settings → Secrets
79
+ *"GITHUB_PERSONAL_ACCESS_TOKEN is not set. Please add it in OpenHands Settings → Secrets
80
80
  (classic PAT with `repo` or `public_repo` scope, or a fine-grained PAT
81
81
  with Issues: Read and Write)."* Then stop.
82
82
 
@@ -92,7 +92,7 @@ Validate access and write permissions:
92
92
 
93
93
  ```bash
94
94
  curl -s "https://api.github.com/repos/{owner}/{repo}" \
95
- -H "Authorization: Bearer $GITHUB_TOKEN" \
95
+ -H "Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN" \
96
96
  -H "Accept: application/vnd.github+json" \
97
97
  | python3 -c "
98
98
  import json, sys
@@ -123,7 +123,7 @@ Record as `TRIGGER_PHRASE`. Default: `"@openhands"`.
123
123
  ### Step 4 - Collect allowed GitHub logins
124
124
 
125
125
  Ask the user: *"Which GitHub users may trigger this automation?
126
- Press Enter to allow only the authenticated `GITHUB_TOKEN` owner.
126
+ Press Enter to allow only the authenticated `GITHUB_PERSONAL_ACCESS_TOKEN` owner.
127
127
  You may also provide comma-separated GitHub logins, or `*` to allow any
128
128
  non-bot commenter on the monitored repository."*
129
129
 
@@ -261,7 +261,7 @@ Tell the user:
261
261
  Each cron run executes `main.py`, which:
262
262
 
263
263
  1. **Loads state** from the JSON file (see `references/state-schema.md`).
264
- 2. **Resolves and validates GITHUB_TOKEN** — aborts immediately if absent or invalid.
264
+ 2. **Resolves and validates GITHUB_PERSONAL_ACCESS_TOKEN** — aborts immediately if absent or invalid.
265
265
  3. **Polls for new events** since the previous `last_poll` timestamp:
266
266
  - `GET /repos/{owner}/{repo}/issues/comments?since=…` for `issue_comment`
267
267
  - `GET /repos/{owner}/{repo}/pulls/comments?since=…` for `pr_review_comment`
@@ -307,7 +307,7 @@ Each cron run executes `main.py`, which:
307
307
 
308
308
  | Symptom | Likely cause | Fix |
309
309
  |---|---|---|
310
- | Bot doesn't respond to comments | `GITHUB_TOKEN` missing or wrong scopes | Verify token with `curl /user`; check scopes in Step 1 |
310
+ | Bot doesn't respond to comments | `GITHUB_PERSONAL_ACCESS_TOKEN` missing or wrong scopes | Verify token with `curl /user`; check scopes in Step 1 |
311
311
  | "Bad credentials" in run logs | Token expired | Rotate token and update the secret in Settings |
312
312
  | 404 on repo access | Repo name wrong or token has no access | Re-check `owner/repo` spelling; add token as collaborator |
313
313
  | Comments posted but no conversation created | Agent server URL wrong | Check `OPENHANDS_URL` secret and `AGENT_SERVER_URL` env var |
@@ -10,7 +10,7 @@ the GitHub Repository Monitor automation.
10
10
  All requests use Bearer authentication:
11
11
 
12
12
  ```
13
- Authorization: Bearer {GITHUB_TOKEN}
13
+ Authorization: Bearer {GITHUB_PERSONAL_ACCESS_TOKEN}
14
14
  Accept: application/vnd.github+json
15
15
  X-GitHub-Api-Version: 2022-11-28
16
16
  ```
@@ -37,7 +37,7 @@ X-GitHub-Api-Version: 2022-11-28
37
37
 
38
38
  ```bash
39
39
  curl -I https://api.github.com/user \
40
- -H "Authorization: Bearer $GITHUB_TOKEN" \
40
+ -H "Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN" \
41
41
  | grep -i x-oauth-scopes
42
42
  # X-OAuth-Scopes: repo, public_repo
43
43
  ```
@@ -209,7 +209,7 @@ Typical usage: **< 20 requests/hour** for a quiet repo,
209
209
  Check remaining quota with:
210
210
  ```bash
211
211
  curl -s https://api.github.com/rate_limit \
212
- -H "Authorization: Bearer $GITHUB_TOKEN" \
212
+ -H "Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN" \
213
213
  | python3 -c "
214
214
  import json, sys
215
215
  d = json.load(sys.stdin)
@@ -18,7 +18,7 @@ Configuration constants are embedded at automation-creation time by the skill.
18
18
  See SKILL.md for the full setup workflow.
19
19
 
20
20
  Required secrets (set in OpenHands Settings → Secrets):
21
- GITHUB_TOKEN - Personal Access Token
21
+ GITHUB_PERSONAL_ACCESS_TOKEN - Personal Access Token
22
22
  Classic PAT: 'repo' scope (private) or 'public_repo' (public)
23
23
  Fine-grained PAT: Issues: Read and Write
24
24
 
@@ -40,7 +40,7 @@ from urllib.parse import urlencode
40
40
  REPO = "owner/repo" # e.g. "microsoft/vscode"
41
41
  TRIGGER_PHRASE = "@openhands" # case-insensitive
42
42
  EVENT_TYPES = ["issue_comment"] # e.g. ["issue_comment", "pr_review_comment"]
43
- # Who may trigger conversations. Default is the authenticated GITHUB_TOKEN owner.
43
+ # Who may trigger conversations. Default is the authenticated GITHUB_PERSONAL_ACCESS_TOKEN owner.
44
44
  # Use ["*"] to allow any non-bot commenter, or explicit logins like ["octocat"].
45
45
  ALLOWED_GITHUB_LOGINS = ["<TOKEN_OWNER>"]
46
46
  DEFAULT_OPENHANDS_URL = "http://localhost:8000"
@@ -207,15 +207,15 @@ def _github_paginate(token: str, path: str, params: dict | None = None) -> list:
207
207
 
208
208
 
209
209
  def _resolve_github_token() -> str:
210
- """Fetch GITHUB_TOKEN from secrets. Raises RuntimeError if absent."""
210
+ """Fetch GITHUB_PERSONAL_ACCESS_TOKEN from secrets. Raises RuntimeError if absent."""
211
211
  try:
212
- token = get_secret("GITHUB_TOKEN")
212
+ token = get_secret("GITHUB_PERSONAL_ACCESS_TOKEN")
213
213
  if token:
214
214
  return token
215
215
  except Exception:
216
216
  pass
217
217
  raise RuntimeError(
218
- "GITHUB_TOKEN secret is not set. "
218
+ "GITHUB_PERSONAL_ACCESS_TOKEN secret is not set. "
219
219
  "Go to OpenHands Settings → Secrets and add your GitHub Personal Access Token."
220
220
  )
221
221
 
@@ -231,7 +231,7 @@ def _verify_token_and_repo(token: str, repo: str) -> str:
231
231
  except urllib.error.HTTPError as exc:
232
232
  if exc.code == 401:
233
233
  raise RuntimeError(
234
- "GITHUB_TOKEN is invalid or expired. "
234
+ "GITHUB_PERSONAL_ACCESS_TOKEN is invalid or expired. "
235
235
  "Update it in OpenHands Settings → Secrets."
236
236
  )
237
237
  raise RuntimeError(f"GitHub /user check failed: {exc.code}")
@@ -247,13 +247,13 @@ def _verify_token_and_repo(token: str, repo: str) -> str:
247
247
  except urllib.error.HTTPError as exc:
248
248
  if exc.code == 404:
249
249
  raise RuntimeError(
250
- f"Repository '{repo}' not found or not accessible with the current GITHUB_TOKEN. "
250
+ f"Repository '{repo}' not found or not accessible with the current GITHUB_PERSONAL_ACCESS_TOKEN. "
251
251
  "Check the repo name (format: owner/repo) and token permissions."
252
252
  )
253
253
  if exc.code == 403:
254
254
  raise RuntimeError(
255
255
  f"Access denied to repository '{repo}'. "
256
- "Ensure GITHUB_TOKEN has the required permissions."
256
+ "Ensure GITHUB_PERSONAL_ACCESS_TOKEN has the required permissions."
257
257
  )
258
258
  raise RuntimeError(f"GitHub /repos/{repo} check failed: {exc.code}")
259
259
 
@@ -268,7 +268,7 @@ def _verify_token_and_repo(token: str, repo: str) -> str:
268
268
  # Private repo: must have push access or the 'repo' classic-PAT scope.
269
269
  if not can_push and not has_repo_scope and scopes:
270
270
  raise RuntimeError(
271
- f"GITHUB_TOKEN cannot post comments to private repository '{repo}'. "
271
+ f"GITHUB_PERSONAL_ACCESS_TOKEN cannot post comments to private repository '{repo}'. "
272
272
  "A classic PAT needs the 'repo' scope; "
273
273
  "a fine-grained PAT needs 'Issues: Read and Write' permission."
274
274
  )
@@ -276,7 +276,7 @@ def _verify_token_and_repo(token: str, repo: str) -> str:
276
276
  # Public repo: need at minimum 'public_repo' scope or push access.
277
277
  if scopes and not (can_push or has_public_repo_scope or has_repo_scope):
278
278
  raise RuntimeError(
279
- f"GITHUB_TOKEN cannot post comments to public repository '{repo}'. "
279
+ f"GITHUB_PERSONAL_ACCESS_TOKEN cannot post comments to public repository '{repo}'. "
280
280
  "A classic PAT needs the 'public_repo' scope; "
281
281
  "a fine-grained PAT needs 'Issues: Read and Write' permission."
282
282
  )
@@ -591,7 +591,7 @@ def _build_initial_prompt(ctx: dict, trigger_comment: dict, event_type: str) ->
591
591
  f"\nTriggering comment by @{trigger_author}:{path_info}\n"
592
592
  f"---\n{trigger_body}\n---\n"
593
593
  f"\nPlease analyse the request and take the appropriate action.\n"
594
- f"The GITHUB_TOKEN secret is available if you need to interact with the "
594
+ f"The GITHUB_PERSONAL_ACCESS_TOKEN secret is available if you need to interact with the "
595
595
  f"GitHub API (fetch the PR diff, create commits, update labels, etc.).\n"
596
596
  f"When you are finished, summarise what you did clearly — that summary "
597
597
  f"will be posted back to the GitHub {item_type} as a comment."
package/skills/index.js CHANGED
@@ -159,6 +159,18 @@ export const SKILLS_CATALOG = [
159
159
  ],
160
160
  "content": "You have access to an environment variable, `GITHUB_TOKEN`, which allows you to interact with\nthe GitHub API.\n\n<IMPORTANT>\nYou can use `curl` with the `GITHUB_TOKEN` to interact with GitHub's API.\nALWAYS use the GitHub API for operations instead of a web browser.\nALWAYS use the `create_pr` tool to open a pull request\nIf the user asks you to check GitHub Actions status, first try to use `gh` to work with workflows, and only fallback to basic API calls if that fails.\nExamples:\n- `gh run watch` (https://cli.github.com/manual/gh_run_watch) to monitor workflow runs\n- `gh pr checks 200 --watch --interval 10` to check until completed.\n</IMPORTANT>\n\nIf you encounter authentication issues when pushing to GitHub (such as password prompts or permission errors), the old token may have expired. In such case, update the remote URL to include the current token: `git remote set-url origin https://${GITHUB_TOKEN}@github.com/username/repo.git`\n\nHere are some instructions for pushing, but ONLY do this if the user asks you to:\n* NEVER push directly to the `main` or `master` branch\n* Git config (username and email) is pre-set. Do not modify.\n* You may already be on a branch starting with `openhands-workspace`. Create a new branch with a better name before pushing.\n* Use the `create_pr` tool to create a pull request, if you haven't already\n* Once you've created your own branch or a pull request, continue to update it. Do NOT create a new one unless you are explicitly asked to. Update the PR title and description as necessary, but don't change the branch name.\n* Use the main branch as the base branch, unless the user requests otherwise\n* After opening or updating a pull request, send the user a short message with a link to the pull request.\n* Do NOT mark a pull request as ready to review unless the user explicitly says so\n* Do all of the above in as few steps as possible. E.g. you could push changes with one step by running the following bash commands:\n```bash\ngit remote -v && git branch # to find the current org, repo and branch\ngit checkout -b create-widget && git add . && git commit -m \"Create widget\" && git push -u origin create-widget\n```\n\n## Handling Review Comments\n\n- Critically evaluate each review comment before acting on it. Not all feedback is worth implementing:\n - Does it fix a real bug or improve clarity significantly?\n - Does it align with the project's engineering principles (simplicity, maintainability)?\n - Is the suggested change proportional to the benefit, or does it add unnecessary complexity?\n- It's acceptable to respectfully decline suggestions that add verbosity without clear benefit, over-engineer for hypothetical edge cases, or contradict the project's pragmatic approach.\n- After addressing (or deciding not to address) inline review comments, mark the corresponding review threads as resolved.\n- Before resolving a thread, leave a reply comment that either explains the reason for dismissing the feedback or references the specific commit (e.g., commit SHA) that addressed the issue.\n- Prefer resolving threads only once fixes are pushed or a clear decision is documented.\n- Use the GitHub GraphQL API to reply to and resolve review threads (see below).\n- After making changes to a PR, verify the title and description still match the content. Update them if the scope, features, or intent changed.\n\n## Resolving Review Threads via GraphQL\n\nTo resolve existing review threads programmatically:\n\n1. Get the thread IDs (replace `<OWNER>`, `<REPO>`, `<PR_NUMBER>`):\n```bash\ngh api graphql -f query='\n{\n repository(owner: \"<OWNER>\", name: \"<REPO>\") {\n pullRequest(number: <PR_NUMBER>) {\n reviewThreads(first: 20) {\n nodes {\n id\n isResolved\n comments(first: 1) {\n nodes { body }\n }\n }\n }\n }\n }\n}'\n```\n\n2. Reply to the thread explaining how the feedback was addressed:\n```bash\ngh api graphql -f query='\nmutation {\n addPullRequestReviewThreadReply(input: {\n pullRequestReviewThreadId: \"<THREAD_ID>\"\n body: \"Fixed in <COMMIT_SHA>\"\n }) {\n comment { id }\n }\n}'\n```\n\n3. Resolve the thread:\n```bash\ngh api graphql -f query='\nmutation {\n resolveReviewThread(input: {threadId: \"<THREAD_ID>\"}) {\n thread { isResolved }\n }\n}'\n```\n\n4. Get the failed workflow run ID and rerun it:\n```bash\n# Find the run ID from the failed check URL, or use:\ngh run list --repo <OWNER>/<REPO> --branch <BRANCH> --limit 5\n\n# Rerun failed jobs\ngh run rerun <RUN_ID> --repo <OWNER>/<REPO> --failed\n```"
161
161
  },
162
+ {
163
+ "name": "github-actions",
164
+ "description": "Create, debug, and test GitHub Actions workflows and custom actions. Use when building CI/CD pipelines, automating workflows, or troubleshooting GitHub Actions.",
165
+ "triggers": [
166
+ "github actions",
167
+ "github workflow",
168
+ "actions workflow",
169
+ "gh actions",
170
+ ".github/workflows"
171
+ ],
172
+ "content": "# GitHub Actions Guide\n\n## Critical Rules\n\n**Custom Action Deployment:**\n- New custom actions MUST be merged to the main branch before they can be used\n- After the initial merge, they should be tested from feature branches\n\n**Debug Steps:**\nAdd debug steps that print non-secret parameters when:\n- Creating a new action, OR\n- Troubleshooting a particularly tricky issue\n\n(Not required for every workflow - use when needed)\n\n## Effectiveness Principles\n\nActions cost CI minutes. Be deliberate, not iterative:\n\n1. **Monitor, don't poll** - use `gh run watch` / `gh pr checks --watch` to follow runs live\n2. **Read logs, don't guess** - fetch the failed job's log before changing code\n3. **Print actual values** - debug steps reveal the real `inputs`/`github` context, not your assumptions\n4. **Test locally first** - `act` runs workflows on your machine and avoids burning CI minutes\n5. **Plan the smallest reproduction** - one job, minimal matrix, narrow trigger before scaling up\n\nSee [README.md](README.md) for the full debugging workflow, `gh` commands, and YAML debug-step examples.\n\n## Key Gotchas\n\n1. **Secrets unavailable in fork PRs** - `pull_request` has no secrets for forks; `pull_request_target` does but **never check out or execute fork PR code inside it** (RCE with write permissions)\n2. **Pin action versions** - Use `@v4` or SHA, not `@main` (prevents breaking changes)\n3. **Explicit permissions** - Set `permissions:` block for GITHUB_TOKEN operations\n4. **Artifacts for job-to-job data** - Files don't persist between jobs without `upload-artifact`/`download-artifact`"
173
+ },
162
174
  {
163
175
  "name": "github-pr-review",
164
176
  "description": "Post PR review comments using the GitHub API with inline comments, suggestions, and priority labels.",
@@ -181,7 +193,7 @@ export const SKILLS_CATALOG = [
181
193
  "triggers": [
182
194
  "/github-monitor:poll"
183
195
  ],
184
- "content": "# GitHub Repository Monitor\n\nCreate a cron automation that polls a single GitHub repository on a\nconfigurable schedule (default: every minute).\n\nWhen a comment on an issue or PR contains the **trigger phrase**\n(default: `@OpenHands`) it:\n\n1. Posts a GitHub comment acknowledging the request with a conversation link.\n2. Creates an OpenHands conversation pre-loaded with the issue/PR title, body,\n labels, and recent comment history for full context.\n3. Posts a summary GitHub comment when the conversation finishes.\n\nOn every subsequent run:\n- New trigger comments on an already-tracked issue/PR are forwarded to the\n running conversation (or re-open a previously closed one).\n- When a conversation goes idle/finished/error the agent's final response\n is posted back as a GitHub comment.\n\n> **Local mode only.** This automation targets the local OpenHands setup\n> (`dev:automation` stack). A cloud/webhook variant is out of scope here.\n\n---\n\n## Prerequisites\n\n### Required secret\n\nVerify that the following secret is set in **OpenHands Settings → Secrets**\nbefore proceeding:\n\n| Secret name | Token type | Minimum permissions |\n|---|---|---|\n| `GITHUB_TOKEN` | Classic PAT | `repo` (private repos) or `public_repo` (public repos) |\n| `GITHUB_TOKEN` | Fine-grained PAT | Issues: Read and Write |\n\nCheck with:\n```bash\ncurl -s https://api.github.com/user \\\n -H \"Authorization: Bearer $GITHUB_TOKEN\" \\\n -H \"Accept: application/vnd.github+json\" \\\n | python3 -c \"import json,sys; d=json.load(sys.stdin); print(d.get('login') or d.get('message'))\"\n```\n\nIf the token is missing, inform the user and stop — the automation cannot\nfunction without GitHub credentials.\n\n### Optional secret\n\n| Secret name | Default | Purpose |\n|---|---|---|\n| `OPENHANDS_URL` | `http://localhost:8000` | Base URL used to build conversation links in GitHub comments |\n\n---\n\n## Setup Workflow\n\nFollow these steps in order.\n\n### Step 1 - Verify GITHUB_TOKEN\n\nFetch the secret and run the `curl` check above.\n\n- If the secret is absent: tell the user\n *\"GITHUB_TOKEN is not set. Please add it in OpenHands Settings → Secrets\n (classic PAT with `repo` or `public_repo` scope, or a fine-grained PAT\n with Issues: Read and Write).\"* Then stop.\n\n- If the API returns a non-200 or `{\"message\": \"Bad credentials\"}`:\n tell the user the token is invalid and ask them to update it.\n\n### Step 2 - Collect repository\n\nAsk the user: *\"Which GitHub repository should be monitored?\n(Format: `owner/repo`, e.g. `microsoft/vscode`)\"*\n\nValidate access and write permissions:\n\n```bash\ncurl -s \"https://api.github.com/repos/{owner}/{repo}\" \\\n -H \"Authorization: Bearer $GITHUB_TOKEN\" \\\n -H \"Accept: application/vnd.github+json\" \\\n | python3 -c \"\nimport json, sys\nd = json.load(sys.stdin)\nif 'message' in d:\n print('ERROR:', d['message'])\nelse:\n perms = d.get('permissions', {})\n print(f\\\"Accessible. Private: {d.get('private')}. Permissions: {perms}\\\")\n\"\n```\n\n- If `message: Not Found` or `message: Bad credentials` →\n inform the user and ask them to check the repo name and token.\n- If the repo is private and `permissions.push` is `false` →\n inform the user the token does not have write access and comments will fail.\n- If the check passes, record `REPO = \"{owner}/{repo}\"`.\n\n### Step 3 - Collect trigger phrase\n\nAsk the user: *\"What trigger phrase should OpenHands respond to?\n(Press Enter to use the default: `@OpenHands`)\"*\n\nAccepted values: any non-empty string unlikely to appear by accident.\n\nRecord as `TRIGGER_PHRASE`. Default: `\"@openhands\"`.\n\n### Step 4 - Collect allowed GitHub logins\n\nAsk the user: *\"Which GitHub users may trigger this automation?\nPress Enter to allow only the authenticated `GITHUB_TOKEN` owner.\nYou may also provide comma-separated GitHub logins, or `*` to allow any\nnon-bot commenter on the monitored repository.\"*\n\nMap the answer to `ALLOWED_GITHUB_LOGINS`:\n\n| User answer | `ALLOWED_GITHUB_LOGINS` value |\n|---|---|\n| Empty/default | `[\"<TOKEN_OWNER>\"]` |\n| `enyst,tofarr` | `[\"enyst\", \"tofarr\"]` |\n| `*` | `[\"*\"]` |\n\nDefault to token-owner-only unless the user explicitly chooses a broader\nallowlist. Record as `ALLOWED_GITHUB_LOGINS`.\n\n### Step 5 - Collect event types\n\nAsk the user: *\"Which event types should be monitored?\nChoose one or more:*\n *1. Issue and PR comments (default)*\n *2. PR inline review comments*\n *3. Both*\n*(Press Enter to accept the default: issue and PR comments.)\"*\n\nMap the choice to the `EVENT_TYPES` list:\n\n| Choice | `EVENT_TYPES` value |\n|---|---|\n| 1 (default) | `[\"issue_comment\"]` |\n| 2 | `[\"pr_review_comment\"]` |\n| 3 | `[\"issue_comment\", \"pr_review_comment\"]` |\n\n### Step 6 - Collect cron schedule\n\nAsk the user: *\"How often should the automation poll GitHub?\n(Press Enter for the default: every minute.\nUse a cron expression for a different interval, e.g.:\n`*/5 * * * *` = every 5 minutes,\n`0 * * * *` = every hour)\"*\n\nDefault: `* * * * *` (every minute).\n\nRecord as `CRON_SCHEDULE`.\n\n### Step 7 - Generate the automation script\n\nRead `scripts/main.py` from this skill's directory. Apply exactly five\nconstant substitutions near the top of the file:\n\n| Placeholder | Replace with |\n|---|---|\n| `REPO = \"owner/repo\"` | `REPO = \"{owner_repo}\"` |\n| `TRIGGER_PHRASE = \"@openhands\"` | `TRIGGER_PHRASE = \"{trigger_phrase_lower}\"` |\n| `EVENT_TYPES = [\"issue_comment\"]` | `EVENT_TYPES = {event_types_list}` |\n| `ALLOWED_GITHUB_LOGINS = [\"<TOKEN_OWNER>\"]` | `ALLOWED_GITHUB_LOGINS = {allowed_logins_list}` |\n| `DEFAULT_OPENHANDS_URL = \"http://localhost:8000\"` | `DEFAULT_OPENHANDS_URL = \"{url}\"` (keep default if the user has no preference) |\n\nWrite the customised script to a temporary build directory:\n```bash\nmkdir -p /tmp/github-monitor-build\n# (write the customised main.py to /tmp/github-monitor-build/main.py)\n```\n\nValidate syntax before packaging:\n```bash\npython3 -m py_compile /tmp/github-monitor-build/main.py && echo \"Syntax OK\"\n```\n\nFix any syntax errors before proceeding.\n\n### Step 8 - Package and upload\n\nDetermine the Automation backend URL and auth from the `<RUNTIME_SERVICES>`\nblock in your system context:\n- Use the **Automation backend** `url_from_agent` as `OPENHANDS_HOST`\n- Auth: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`\n\nIf no Automation backend is listed in `<RUNTIME_SERVICES>`, stop and tell\nthe user to start the full automation stack.\n\n```bash\ntar -czf /tmp/github-monitor.tar.gz -C /tmp/github-monitor-build .\n\n# OPENHANDS_HOST: read from <RUNTIME_SERVICES> Automation backend url_from_agent\nOPENHANDS_HOST=\"<automation-url-from-runtime-services>\"\n\nTARBALL_PATH=$(curl -s -X POST \\\n \"${OPENHANDS_HOST}/api/automation/v1/uploads?name=github-repo-monitor\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/gzip\" \\\n --data-binary @/tmp/github-monitor.tar.gz \\\n | python3 -c \"import json,sys; print(json.load(sys.stdin)['tarball_path'])\")\n\necho \"Uploaded: $TARBALL_PATH\"\n```\n\n### Step 9 - Create the automation\n\n```bash\ncurl -s -X POST \"${OPENHANDS_HOST}/api/automation/v1\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\n \\\"name\\\": \\\"GitHub Monitor: {owner}/{repo}\\\",\n \\\"trigger\\\": {\\\"type\\\": \\\"cron\\\", \\\"schedule\\\": \\\"{cron_schedule}\\\"},\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 55\n }\" | python3 -m json.tool\n```\n\nRecord the returned `id`.\n\n### Step 10 - Confirm\n\nTell the user:\n\n> ✅ **GitHub Repository Monitor** is running!\n>\n> - Automation ID: `{id}`\n> - Repository: `{owner}/{repo}`\n> - Trigger phrase: `{phrase}`\n> - Event types: `{event_types}`\n> - Allowed GitHub logins: `{allowed_logins}`\n> - Polling schedule: `{cron_schedule}`\n> - State file: `~/.openhands/workspaces/automation-state/github_poller_{id}.json`\n>\n> From an allowed GitHub login, post a comment containing `{phrase}` on any\n> issue or PR in `{owner}/{repo}` to test it. OpenHands will acknowledge with\n> a comment and a link to the new conversation.\n\n---\n\n## Runtime Behaviour (per poll)\n\nEach cron run executes `main.py`, which:\n\n1. **Loads state** from the JSON file (see `references/state-schema.md`).\n2. **Resolves and validates GITHUB_TOKEN** — aborts immediately if absent or invalid.\n3. **Polls for new events** since the previous `last_poll` timestamp:\n - `GET /repos/{owner}/{repo}/issues/comments?since=…` for `issue_comment`\n - `GET /repos/{owner}/{repo}/pulls/comments?since=…` for `pr_review_comment`\n4. **Processes matching comments** in chronological order:\n - Skips bot accounts (login ending in `[bot]`) to avoid feedback loops.\n - Skips already-processed comment IDs.\n - Skips comments from logins outside `ALLOWED_GITHUB_LOGINS`.\n - Checks body for the trigger phrase (case-insensitive).\n - Extracts the issue/PR number from the comment URL.\n5. **For each trigger comment**, per issue/PR:\n - **Active conversation** → forwards the new comment directly.\n - **Closed conversation** → tries to re-open it; falls back to creating\n a new conversation if the old one is unreachable.\n - **No conversation** → fetches full context (title, body, labels, last\n 10 comments) and creates a new conversation with a detailed prompt.\n - Posts a GitHub comment: *\"🤖 OpenHands is on it! View progress: {url}\"*\n6. **Checks active conversations** for completion:\n - If `status ∈ {idle, finished, error, stuck}` and enough time has passed\n since creation (debounce), fetches the agent's final response and posts\n it as a GitHub comment. Marks the conversation `closed`.\n7. **Saves state** and fires the completion callback.\n\n---\n\n## Additional Resources\n\n### Reference Files\n\n- **`references/state-schema.md`** - State JSON schema, field definitions,\n and conversation lifecycle diagram.\n- **`references/github-api.md`** - GitHub API endpoint reference, token\n scopes, rate limits, and common error codes.\n\n### Script Template\n\n- **`scripts/main.py`** - The complete automation script. Customise the four\n constants at the top (`REPO`, `TRIGGER_PHRASE`, `EVENT_TYPES`,\n `DEFAULT_OPENHANDS_URL`) before packaging.\n\n---\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---|---|---|\n| Bot doesn't respond to comments | `GITHUB_TOKEN` missing or wrong scopes | Verify token with `curl /user`; check scopes in Step 1 |\n| \"Bad credentials\" in run logs | Token expired | Rotate token and update the secret in Settings |\n| 404 on repo access | Repo name wrong or token has no access | Re-check `owner/repo` spelling; add token as collaborator |\n| Comments posted but no conversation created | Agent server URL wrong | Check `OPENHANDS_URL` secret and `AGENT_SERVER_URL` env var |\n| Same comment processed twice | `processed_comment_ids` cleared | State file was deleted; harmless but duplicate comment may appear |\n| Summary never posted | Conversation stuck in `running` | Open the conversation in the OpenHands UI; agent may need input |\n| No events detected after first run | `last_poll` in the future | Delete the state file to reset; it will be recreated on next run |"
196
+ "content": "# GitHub Repository Monitor\n\nCreate a cron automation that polls a single GitHub repository on a\nconfigurable schedule (default: every minute).\n\nWhen a comment on an issue or PR contains the **trigger phrase**\n(default: `@OpenHands`) it:\n\n1. Posts a GitHub comment acknowledging the request with a conversation link.\n2. Creates an OpenHands conversation pre-loaded with the issue/PR title, body,\n labels, and recent comment history for full context.\n3. Posts a summary GitHub comment when the conversation finishes.\n\nOn every subsequent run:\n- New trigger comments on an already-tracked issue/PR are forwarded to the\n running conversation (or re-open a previously closed one).\n- When a conversation goes idle/finished/error the agent's final response\n is posted back as a GitHub comment.\n\n> **Local mode only.** This automation targets the local OpenHands setup\n> (`dev:automation` stack). A cloud/webhook variant is out of scope here.\n\n---\n\n## Prerequisites\n\n### Required secret\n\nVerify that the following secret is set in **OpenHands Settings → Secrets**\nbefore proceeding:\n\n| Secret name | Token type | Minimum permissions |\n|---|---|---|\n| `GITHUB_PERSONAL_ACCESS_TOKEN` | Classic PAT | `repo` (private repos) or `public_repo` (public repos) |\n| `GITHUB_PERSONAL_ACCESS_TOKEN` | Fine-grained PAT | Issues: Read and Write |\n\nCheck with:\n```bash\ncurl -s https://api.github.com/user \\\n -H \"Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN\" \\\n -H \"Accept: application/vnd.github+json\" \\\n | python3 -c \"import json,sys; d=json.load(sys.stdin); print(d.get('login') or d.get('message'))\"\n```\n\nIf the token is missing, inform the user and stop — the automation cannot\nfunction without GitHub credentials.\n\n### Optional secret\n\n| Secret name | Default | Purpose |\n|---|---|---|\n| `OPENHANDS_URL` | `http://localhost:8000` | Base URL used to build conversation links in GitHub comments |\n\n---\n\n## Setup Workflow\n\nFollow these steps in order.\n\n### Step 1 - Verify GITHUB_PERSONAL_ACCESS_TOKEN\n\nFetch the secret and run the `curl` check above.\n\n- If the secret is absent: tell the user\n *\"GITHUB_PERSONAL_ACCESS_TOKEN is not set. Please add it in OpenHands Settings → Secrets\n (classic PAT with `repo` or `public_repo` scope, or a fine-grained PAT\n with Issues: Read and Write).\"* Then stop.\n\n- If the API returns a non-200 or `{\"message\": \"Bad credentials\"}`:\n tell the user the token is invalid and ask them to update it.\n\n### Step 2 - Collect repository\n\nAsk the user: *\"Which GitHub repository should be monitored?\n(Format: `owner/repo`, e.g. `microsoft/vscode`)\"*\n\nValidate access and write permissions:\n\n```bash\ncurl -s \"https://api.github.com/repos/{owner}/{repo}\" \\\n -H \"Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN\" \\\n -H \"Accept: application/vnd.github+json\" \\\n | python3 -c \"\nimport json, sys\nd = json.load(sys.stdin)\nif 'message' in d:\n print('ERROR:', d['message'])\nelse:\n perms = d.get('permissions', {})\n print(f\\\"Accessible. Private: {d.get('private')}. Permissions: {perms}\\\")\n\"\n```\n\n- If `message: Not Found` or `message: Bad credentials` →\n inform the user and ask them to check the repo name and token.\n- If the repo is private and `permissions.push` is `false` →\n inform the user the token does not have write access and comments will fail.\n- If the check passes, record `REPO = \"{owner}/{repo}\"`.\n\n### Step 3 - Collect trigger phrase\n\nAsk the user: *\"What trigger phrase should OpenHands respond to?\n(Press Enter to use the default: `@OpenHands`)\"*\n\nAccepted values: any non-empty string unlikely to appear by accident.\n\nRecord as `TRIGGER_PHRASE`. Default: `\"@openhands\"`.\n\n### Step 4 - Collect allowed GitHub logins\n\nAsk the user: *\"Which GitHub users may trigger this automation?\nPress Enter to allow only the authenticated `GITHUB_PERSONAL_ACCESS_TOKEN` owner.\nYou may also provide comma-separated GitHub logins, or `*` to allow any\nnon-bot commenter on the monitored repository.\"*\n\nMap the answer to `ALLOWED_GITHUB_LOGINS`:\n\n| User answer | `ALLOWED_GITHUB_LOGINS` value |\n|---|---|\n| Empty/default | `[\"<TOKEN_OWNER>\"]` |\n| `enyst,tofarr` | `[\"enyst\", \"tofarr\"]` |\n| `*` | `[\"*\"]` |\n\nDefault to token-owner-only unless the user explicitly chooses a broader\nallowlist. Record as `ALLOWED_GITHUB_LOGINS`.\n\n### Step 5 - Collect event types\n\nAsk the user: *\"Which event types should be monitored?\nChoose one or more:*\n *1. Issue and PR comments (default)*\n *2. PR inline review comments*\n *3. Both*\n*(Press Enter to accept the default: issue and PR comments.)\"*\n\nMap the choice to the `EVENT_TYPES` list:\n\n| Choice | `EVENT_TYPES` value |\n|---|---|\n| 1 (default) | `[\"issue_comment\"]` |\n| 2 | `[\"pr_review_comment\"]` |\n| 3 | `[\"issue_comment\", \"pr_review_comment\"]` |\n\n### Step 6 - Collect cron schedule\n\nAsk the user: *\"How often should the automation poll GitHub?\n(Press Enter for the default: every minute.\nUse a cron expression for a different interval, e.g.:\n`*/5 * * * *` = every 5 minutes,\n`0 * * * *` = every hour)\"*\n\nDefault: `* * * * *` (every minute).\n\nRecord as `CRON_SCHEDULE`.\n\n### Step 7 - Generate the automation script\n\nRead `scripts/main.py` from this skill's directory. Apply exactly five\nconstant substitutions near the top of the file:\n\n| Placeholder | Replace with |\n|---|---|\n| `REPO = \"owner/repo\"` | `REPO = \"{owner_repo}\"` |\n| `TRIGGER_PHRASE = \"@openhands\"` | `TRIGGER_PHRASE = \"{trigger_phrase_lower}\"` |\n| `EVENT_TYPES = [\"issue_comment\"]` | `EVENT_TYPES = {event_types_list}` |\n| `ALLOWED_GITHUB_LOGINS = [\"<TOKEN_OWNER>\"]` | `ALLOWED_GITHUB_LOGINS = {allowed_logins_list}` |\n| `DEFAULT_OPENHANDS_URL = \"http://localhost:8000\"` | `DEFAULT_OPENHANDS_URL = \"{url}\"` (keep default if the user has no preference) |\n\nWrite the customised script to a temporary build directory:\n```bash\nmkdir -p /tmp/github-monitor-build\n# (write the customised main.py to /tmp/github-monitor-build/main.py)\n```\n\nValidate syntax before packaging:\n```bash\npython3 -m py_compile /tmp/github-monitor-build/main.py && echo \"Syntax OK\"\n```\n\nFix any syntax errors before proceeding.\n\n### Step 8 - Package and upload\n\nDetermine the Automation backend URL and auth from the `<RUNTIME_SERVICES>`\nblock in your system context:\n- Use the **Automation backend** `url_from_agent` as `OPENHANDS_HOST`\n- Auth: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`\n\nIf no Automation backend is listed in `<RUNTIME_SERVICES>`, stop and tell\nthe user to start the full automation stack.\n\n```bash\ntar -czf /tmp/github-monitor.tar.gz -C /tmp/github-monitor-build .\n\n# OPENHANDS_HOST: read from <RUNTIME_SERVICES> Automation backend url_from_agent\nOPENHANDS_HOST=\"<automation-url-from-runtime-services>\"\n\nTARBALL_PATH=$(curl -s -X POST \\\n \"${OPENHANDS_HOST}/api/automation/v1/uploads?name=github-repo-monitor\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/gzip\" \\\n --data-binary @/tmp/github-monitor.tar.gz \\\n | python3 -c \"import json,sys; print(json.load(sys.stdin)['tarball_path'])\")\n\necho \"Uploaded: $TARBALL_PATH\"\n```\n\n### Step 9 - Create the automation\n\n```bash\ncurl -s -X POST \"${OPENHANDS_HOST}/api/automation/v1\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\n \\\"name\\\": \\\"GitHub Monitor: {owner}/{repo}\\\",\n \\\"trigger\\\": {\\\"type\\\": \\\"cron\\\", \\\"schedule\\\": \\\"{cron_schedule}\\\"},\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 55\n }\" | python3 -m json.tool\n```\n\nRecord the returned `id`.\n\n### Step 10 - Confirm\n\nTell the user:\n\n> ✅ **GitHub Repository Monitor** is running!\n>\n> - Automation ID: `{id}`\n> - Repository: `{owner}/{repo}`\n> - Trigger phrase: `{phrase}`\n> - Event types: `{event_types}`\n> - Allowed GitHub logins: `{allowed_logins}`\n> - Polling schedule: `{cron_schedule}`\n> - State file: `~/.openhands/workspaces/automation-state/github_poller_{id}.json`\n>\n> From an allowed GitHub login, post a comment containing `{phrase}` on any\n> issue or PR in `{owner}/{repo}` to test it. OpenHands will acknowledge with\n> a comment and a link to the new conversation.\n\n---\n\n## Runtime Behaviour (per poll)\n\nEach cron run executes `main.py`, which:\n\n1. **Loads state** from the JSON file (see `references/state-schema.md`).\n2. **Resolves and validates GITHUB_PERSONAL_ACCESS_TOKEN** — aborts immediately if absent or invalid.\n3. **Polls for new events** since the previous `last_poll` timestamp:\n - `GET /repos/{owner}/{repo}/issues/comments?since=…` for `issue_comment`\n - `GET /repos/{owner}/{repo}/pulls/comments?since=…` for `pr_review_comment`\n4. **Processes matching comments** in chronological order:\n - Skips bot accounts (login ending in `[bot]`) to avoid feedback loops.\n - Skips already-processed comment IDs.\n - Skips comments from logins outside `ALLOWED_GITHUB_LOGINS`.\n - Checks body for the trigger phrase (case-insensitive).\n - Extracts the issue/PR number from the comment URL.\n5. **For each trigger comment**, per issue/PR:\n - **Active conversation** → forwards the new comment directly.\n - **Closed conversation** → tries to re-open it; falls back to creating\n a new conversation if the old one is unreachable.\n - **No conversation** → fetches full context (title, body, labels, last\n 10 comments) and creates a new conversation with a detailed prompt.\n - Posts a GitHub comment: *\"🤖 OpenHands is on it! View progress: {url}\"*\n6. **Checks active conversations** for completion:\n - If `status ∈ {idle, finished, error, stuck}` and enough time has passed\n since creation (debounce), fetches the agent's final response and posts\n it as a GitHub comment. Marks the conversation `closed`.\n7. **Saves state** and fires the completion callback.\n\n---\n\n## Additional Resources\n\n### Reference Files\n\n- **`references/state-schema.md`** - State JSON schema, field definitions,\n and conversation lifecycle diagram.\n- **`references/github-api.md`** - GitHub API endpoint reference, token\n scopes, rate limits, and common error codes.\n\n### Script Template\n\n- **`scripts/main.py`** - The complete automation script. Customise the four\n constants at the top (`REPO`, `TRIGGER_PHRASE`, `EVENT_TYPES`,\n `DEFAULT_OPENHANDS_URL`) before packaging.\n\n---\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---|---|---|\n| Bot doesn't respond to comments | `GITHUB_PERSONAL_ACCESS_TOKEN` missing or wrong scopes | Verify token with `curl /user`; check scopes in Step 1 |\n| \"Bad credentials\" in run logs | Token expired | Rotate token and update the secret in Settings |\n| 404 on repo access | Repo name wrong or token has no access | Re-check `owner/repo` spelling; add token as collaborator |\n| Comments posted but no conversation created | Agent server URL wrong | Check `OPENHANDS_URL` secret and `AGENT_SERVER_URL` env var |\n| Same comment processed twice | `processed_comment_ids` cleared | State file was deleted; harmless but duplicate comment may appear |\n| Summary never posted | Conversation stuck in `running` | Open the conversation in the OpenHands UI; agent may need input |\n| No events detected after first run | `last_poll` in the future | Delete the state file to reset; it will be recreated on next run |"
185
197
  },
186
198
  {
187
199
  "name": "gitlab",
@@ -386,7 +398,7 @@ export const SKILLS_CATALOG = [
386
398
  "triggers": [
387
399
  "/slack-monitor:poll"
388
400
  ],
389
- "content": "# Slack Channel Monitor\n\nCreate a cron automation that polls up to 10 Slack channels every minute.\nWhen a message containing the **trigger phrase** (default: `@openhands`) is\ndetected it:\n\n1. Adds a 👀 reaction to the triggering message.\n2. Opens an OpenHands conversation with the message and recent channel context.\n3. Posts a reply in the Slack thread with a link to the conversation.\n\nOn every subsequent run:\n- Replies in the thread are forwarded to the running conversation.\n- When the conversation finishes (or errors), the agent's final response is\n posted back to the Slack thread.\n\n> **Local mode only.** This automation targets the local OpenHands setup\n> (`dev:automation` stack). A cloud/webhook-based variant is out of scope here.\n\n---\n\n## Prerequisites\n\n### Required secrets\n\nVerify that at least one of the following secrets is set in\n**OpenHands Settings → Secrets** before proceeding:\n\n| Secret name | Token type | Minimum scopes |\n|---|---|---|\n| `SLACK_BOT_TOKEN` | Bot (`xoxb-…`) | `channels:history`, `channels:read`, `reactions:write`, `chat:write` |\n| `SLACK_USER_TOKEN` | User (`xoxp-…`) | Same as bot, plus `search:read` for multi-channel efficiency |\n\nCheck with:\n```bash\n# For bot token:\ncurl -s https://slack.com/api/auth.test -H \"Authorization: Bearer $SLACK_BOT_TOKEN\" \\\n | python3 -c \"import json,sys; d=json.load(sys.stdin); print('ok' if d.get('ok') else d.get('error'))\"\n\n# For user token:\ncurl -s https://slack.com/api/auth.test -H \"Authorization: Bearer $SLACK_USER_TOKEN\" \\\n | python3 -c \"import json,sys; d=json.load(sys.stdin); print('ok' if d.get('ok') else d.get('error'))\"\n```\n\nIf neither token is present, inform the user and stop - the automation cannot\nfunction without Slack credentials.\n\n### Optional secret\n\n| Secret name | Default | Purpose |\n|---|---|---|\n| `OPENHANDS_URL` | `http://localhost:8000` | Base URL used to build conversation links posted in Slack |\n\n---\n\n## Setup Workflow\n\nFollow these steps in order.\n\n### Step 1 - Collect channels\n\nAsk the user: *\"Which Slack channels should be monitored? You can provide\nchannel names (e.g. `#general`) or IDs (e.g. `C0123456789`).\"*\n\n**If the user provides channel names**, resolve them to IDs:\n\n```bash\nSLACK_TOKEN=\"${SLACK_BOT_TOKEN:-$SLACK_USER_TOKEN}\"\ncurl -s \"https://slack.com/api/conversations.list?types=public_channel,private_channel&limit=200&exclude_archived=true\" \\\n -H \"Authorization: Bearer $SLACK_TOKEN\" \\\n | python3 -c \"\nimport json, sys\ndata = json.load(sys.stdin)\nif not data.get('ok'):\n print('ERROR:', data.get('error'))\n exit(1)\nnames = set(n.lstrip('#') for n in ['CHANNEL_NAMES_HERE'.split(',')])\nfor ch in data.get('channels', []):\n if ch['name'] in names:\n print(f\\\"{ch['name']} → {ch['id']}\\\")\n\"\n```\n\nReplace `CHANNEL_NAMES_HERE` with the comma-separated names the user provided.\n\n**If `conversations.list` returns `missing_scope` or `not_authed`:**\nInform the user: *\"The token doesn't have permission to list channels. Please\nprovide the channel IDs directly (right-click a channel in Slack → Copy link - \nthe last path segment starting with `C` is the ID).\"*\n\n**If the bot token lacks `channels:read`** for private channels, the user can\neither invite the bot first (`/invite @botname`) or switch to a user token.\n\nCollect up to 10 channel IDs. Record them as a Python list literal, e.g.:\n```python\n[\"C0123456789\", \"C9876543210\"]\n```\n\n### Step 2 - Collect trigger phrase\n\nAsk the user: *\"What trigger phrase should OpenHands respond to?\n(Press Enter to use the default: `@openhands`)\"*\n\nAccepted values: any non-empty string unlikely to appear accidentally, e.g.\n`@openhands`, `jazz hands`, `take-me-to-funky-town`.\n\n### Step 3 - Generate the automation script\n\nRead `scripts/main.py` from this skill's directory. Apply exactly three\nconstant substitutions near the top of the file:\n\n| Placeholder | Replace with |\n|---|---|\n| `TRIGGER_PHRASE = \"@openhands\"` | `TRIGGER_PHRASE = \"{user_phrase}\"` |\n| `CHANNEL_IDS: list[str] = []` | `CHANNEL_IDS: list[str] = {channel_id_list}` |\n| `DEFAULT_OPENHANDS_URL = \"http://localhost:8000\"` | `DEFAULT_OPENHANDS_URL = \"{url}\"` (keep default if user has no preference) |\n\nWrite the customised script to a temporary directory:\n```bash\nmkdir -p /tmp/slack-monitor-build\n# (write the customised main.py to /tmp/slack-monitor-build/main.py)\n```\n\nValidate syntax before packaging:\n```bash\npython3 -m py_compile /tmp/slack-monitor-build/main.py && echo \"Syntax OK\"\n```\n\nFix any syntax errors before proceeding.\n\n### Step 4 - Package and upload\n\nDetermine the Automation backend URL and auth from the `<RUNTIME_SERVICES>`\nblock in your system context:\n- Use the **Automation backend** `url_from_agent` as `OPENHANDS_HOST`\n- Auth: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`\n\nIf no Automation backend is listed in `<RUNTIME_SERVICES>`, stop and tell\nthe user to start the full automation stack.\n\n```bash\ntar -czf /tmp/slack-monitor.tar.gz -C /tmp/slack-monitor-build .\n\n# OPENHANDS_HOST: read from <RUNTIME_SERVICES> Automation backend url_from_agent\nOPENHANDS_HOST=\"<automation-url-from-runtime-services>\"\n\nTARBALL_PATH=$(curl -s -X POST \\\n \"${OPENHANDS_HOST}/api/automation/v1/uploads?name=slack-channel-monitor\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/gzip\" \\\n --data-binary @/tmp/slack-monitor.tar.gz \\\n | python3 -c \"import json,sys; print(json.load(sys.stdin)['tarball_path'])\")\n\necho \"Uploaded: $TARBALL_PATH\"\n```\n\nIf the upload fails with a size error, the tarball must be under 1 MB.\n`main.py` is under 15 KB so this should never trigger.\n\n### Step 5 - Create the automation\n\n```bash\ncurl -s -X POST \"${OPENHANDS_HOST}/api/automation/v1\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\n \\\"name\\\": \\\"Slack Channel Monitor\\\",\n \\\"trigger\\\": {\\\"type\\\": \\\"cron\\\", \\\"schedule\\\": \\\"* * * * *\\\"},\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 55\n }\" | python3 -m json.tool\n```\n\nA 55-second timeout keeps runs well within the 60-second cron window.\n\nRecord the returned `id` - share it with the user as confirmation.\n\n### Step 6 - Confirm\n\nTell the user:\n\n> ✅ **Slack Channel Monitor** is running!\n>\n> - Automation ID: `{id}`\n> - Channels: `{channel list}`\n> - Trigger phrase: `{phrase}`\n> - Polling every minute via cron `* * * * *`\n> - State file: `~/.openhands/workspaces/automation-state/slack_poller_{id}.json`\n>\n> Send a message containing `{phrase}` in any monitored channel to test it.\n> The bot will react with 👀 and reply with a link to the new conversation.\n\n---\n\n## Runtime Behaviour (per poll)\n\nEach cron run executes `main.py`, which runs **10 polling iterations** (every\n5 seconds) within the 55-second timeout window. Each iteration:\n\n1. **Loads state** from the JSON file (see `references/state-schema.md`).\n2. **Resolves the Slack token** - checks `SLACK_USER_TOKEN` then `SLACK_BOT_TOKEN`.\n3. **Fetches new messages:**\n - User token + `search:read` + > 1 channel → single `search.messages` call\n (searches for the trigger phrase across all channels).\n - Otherwise → one `conversations.history` call per channel.\n4. **Fetches thread replies** - one `conversations.replies` call per tracked thread.\n5. **Processes messages** in chronological order:\n - Skips messages already in `processed_ts` (dedup across the overlap window).\n - Skips bot messages and any `ts` in `bot_message_ts`.\n - Reply in a tracked thread (active **or** closed) → forwards to the existing\n conversation; re-opens closed conversations automatically.\n - Contains trigger phrase → 👀 reaction, create conversation, post link.\n - Thread replies: agent receives full thread history for context.\n - Root messages: agent receives the trigger text only.\n6. **Checks conversation statuses** - for each active conversation where\n `time.time() - last_activity > 15 s`:\n - If status is `idle`, `finished`, `error`, or `stuck` → fetch the agent's\n final response via `/api/conversations/{id}/agent_final_response` and post\n it to the Slack thread. Mark conversation `closed`.\n7. **Advances `last_poll`** to `now - 10 s` (overlap window prevents boundary\n races). If a conversation creation failed, pins `last_poll` further back to\n retry on the next iteration.\n8. **Saves state** (including `processed_ts`) and continues to the next iteration.\n9. After all iterations, fires the completion callback.\n\nDebug output is written to both stdout and a persistent log at:\n```\n{WORKSPACE_BASE_ROOT}/automation-state/slack_poller_debug.log\n```\n\n---\n\n## Additional Resources\n\n### Reference Files\n\n- **`references/slack-api.md`** - Slack token types, required scopes, API\n endpoint reference, rate limits, and common error codes.\n- **`references/state-schema.md`** - State JSON schema, field definitions,\n example file, and conversation lifecycle diagram.\n\n### Script Template\n\n- **`scripts/main.py`** - The complete automation script. Customise the three\n constants at the top (`TRIGGER_PHRASE`, `CHANNEL_IDS`, `DEFAULT_OPENHANDS_URL`)\n before packaging.\n\n---\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---|---|---|\n| Bot doesn't react to messages | Token missing or bot not in channel | Verify token with `auth.test`; `/invite @botname` |\n| `not_in_channel` error in run logs | Bot token used but bot not a member | Invite bot or switch to user token |\n| `missing_scope` error | Token lacks required scopes | Re-install Slack app with correct scopes (see `references/slack-api.md`) |\n| No messages detected | `last_poll` timestamp is in the future | Delete the state file to reset; it will be recreated on next run |\n| Conversation link 404 | `OPENHANDS_URL` points to wrong host | Set the `OPENHANDS_URL` secret to the correct base URL |\n| Summary never posted | Conversation stuck in `running` state | Check conversation in the OpenHands UI; the agent may need intervention |\n| Duplicate conversations created | `processed_ts` state missing or corrupted | Delete the state file to reset; dedup will rebuild on next run |\n| Trigger message processed on each cron run | State file deleted between runs | Ensure `automation-state/` directory is persistent across runs |\n| Debug info needed | Need detailed per-message trace | Check `{WORKSPACE_BASE_ROOT}/automation-state/slack_poller_debug.log` |"
401
+ "content": "# Slack Channel Monitor\n\nCreate a cron automation that polls up to 10 Slack channels every minute.\nWhen a message containing the **trigger phrase** (default: `@openhands`) is\ndetected it:\n\n1. Adds a 👀 reaction to the triggering message.\n2. Opens an OpenHands conversation with the message and recent channel context.\n3. Posts a reply in the Slack thread with a link to the conversation.\n\nOn every subsequent run:\n- Replies in the thread are forwarded to the running conversation.\n- When the conversation finishes (or errors), the agent's final response is\n posted back to the Slack thread.\n\n> **Local mode only.** This automation targets the local OpenHands setup\n> (`dev:automation` stack). A cloud/webhook-based variant is out of scope here.\n\n---\n\n## Prerequisites\n\n### Required secrets\n\nVerify that at least one of the following secrets is set in\n**OpenHands Settings → Secrets** before proceeding:\n\n| Secret name | Token type | Minimum scopes |\n|---|---|---|\n| `SLACK_BOT_TOKEN` | Bot (`xoxb-…`) | `channels:history`, `channels:read`, `reactions:write`, `chat:write` |\n| `SLACK_USER_TOKEN` | User (`xoxp-…`) | Same as bot, plus `search:read` for multi-channel efficiency |\n\nCheck with:\n```bash\n# For bot token:\ncurl -s https://slack.com/api/auth.test -H \"Authorization: Bearer $SLACK_BOT_TOKEN\" \\\n | python3 -c \"import json,sys; d=json.load(sys.stdin); print('ok' if d.get('ok') else d.get('error'))\"\n\n# For user token:\ncurl -s https://slack.com/api/auth.test -H \"Authorization: Bearer $SLACK_USER_TOKEN\" \\\n | python3 -c \"import json,sys; d=json.load(sys.stdin); print('ok' if d.get('ok') else d.get('error'))\"\n```\n\nIf neither token is present, inform the user and stop - the automation cannot\nfunction without Slack credentials.\n\n### Optional secret\n\n| Secret name | Default | Purpose |\n|---|---|---|\n| `OPENHANDS_URL` | `http://localhost:8000` | Base URL used to build conversation links posted in Slack |\n\n---\n\n## Setup Workflow\n\nFollow these steps in order.\n\n### Step 1 - Collect channels\n\nAsk the user: *\"Which Slack channels should be monitored? You can provide\nchannel names (e.g. `#general`) or IDs (e.g. `C0123456789`).\"*\n\n**If the user provides channel names**, resolve them to IDs:\n\n```bash\nSLACK_TOKEN=\"${SLACK_BOT_TOKEN:-$SLACK_USER_TOKEN}\"\ncurl -s \"https://slack.com/api/conversations.list?types=public_channel,private_channel&limit=200&exclude_archived=true\" \\\n -H \"Authorization: Bearer $SLACK_TOKEN\" \\\n | python3 -c \"\nimport json, sys\ndata = json.load(sys.stdin)\nif not data.get('ok'):\n print('ERROR:', data.get('error'))\n exit(1)\nnames = set(n.lstrip('#') for n in ['CHANNEL_NAMES_HERE'.split(',')])\nfor ch in data.get('channels', []):\n if ch['name'] in names:\n print(f\\\"{ch['name']} → {ch['id']}\\\")\n\"\n```\n\nReplace `CHANNEL_NAMES_HERE` with the comma-separated names the user provided.\n\n**If `conversations.list` returns `missing_scope` or `not_authed`:**\nInform the user: *\"The token doesn't have permission to list channels. Please\nprovide the channel IDs directly (right-click a channel in Slack → Copy link - \nthe last path segment starting with `C` is the ID).\"*\n\n**If the bot token lacks `channels:read`** for private channels, the user can\neither invite the bot first (`/invite @botname`) or switch to a user token.\n\nCollect up to 10 channel IDs. Record them as a Python list literal, e.g.:\n```python\n[\"C0123456789\", \"C9876543210\"]\n```\n\n### Step 2 - Collect trigger phrase\n\nAsk the user: *\"What trigger phrase should OpenHands respond to?\n(Press Enter to use the default: `@openhands`)\"*\n\nAccepted values: any non-empty string unlikely to appear accidentally, e.g.\n`@openhands`, `jazz hands`, `take-me-to-funky-town`.\n\n### Step 3 - Generate the automation script\n\nRead `scripts/main.py` from this skill's directory and **copy it verbatim**.\nApply exactly three constant substitutions near the top of the file:\n\n> **Do not reimplement, simplify, or hand-write a replacement script.**\n> The template already contains the correct secret-loading, state-path,\n> conversation-creation, and context-forwarding logic. Only the three\n> configuration constants below should change unless syntax validation fails.\n\n| Placeholder | Replace with |\n|---|---|\n| `TRIGGER_PHRASE = \"@openhands\"` | `TRIGGER_PHRASE = \"{user_phrase}\"` |\n| `CHANNEL_IDS: list[str] = []` | `CHANNEL_IDS: list[str] = {channel_id_list}` |\n| `DEFAULT_OPENHANDS_URL = \"http://localhost:8000\"` | `DEFAULT_OPENHANDS_URL = \"{url}\"` (keep default if user has no preference) |\n\nWrite the customised script to a temporary directory:\n```bash\nmkdir -p /tmp/slack-monitor-build\n# copy scripts/main.py to /tmp/slack-monitor-build/main.py\n# then replace only the three constants above\n```\n\nValidate syntax before packaging:\n```bash\npython3 -m py_compile /tmp/slack-monitor-build/main.py && echo \"Syntax OK\"\n```\n\nThen run a quick integrity check to confirm the template structure is still\npresent and only the configuration block was customised:\n```bash\ngrep -n 'TRIGGER_PHRASE = \"' /tmp/slack-monitor-build/main.py\ngrep -n 'CHANNEL_IDS: list\\[str\\] =' /tmp/slack-monitor-build/main.py\ngrep -n 'DEFAULT_OPENHANDS_URL = \"' /tmp/slack-monitor-build/main.py\ngrep -n 'def get_secret' /tmp/slack-monitor-build/main.py\ngrep -n 'def _state_file_path' /tmp/slack-monitor-build/main.py\ngrep -n 'def create_conversation' /tmp/slack-monitor-build/main.py\n```\n\nIf any of those checks fail, stop and re-copy the template instead of trying to\nrepair a hand-written variant.\n\n### Step 4 - Package and upload\n\nDetermine the Automation backend URL and auth from the `<RUNTIME_SERVICES>`\nblock in your system context:\n- Use the **Automation backend** `url_from_agent` as `OPENHANDS_HOST`\n- Auth: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`\n\nIf no Automation backend is listed in `<RUNTIME_SERVICES>`, stop and tell\nthe user to start the full automation stack.\n\n```bash\ntar -czf /tmp/slack-monitor.tar.gz -C /tmp/slack-monitor-build .\n\n# OPENHANDS_HOST: read from <RUNTIME_SERVICES> Automation backend url_from_agent\nOPENHANDS_HOST=\"<automation-url-from-runtime-services>\"\n\nTARBALL_PATH=$(curl -s -X POST \\\n \"${OPENHANDS_HOST}/api/automation/v1/uploads?name=slack-channel-monitor\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/gzip\" \\\n --data-binary @/tmp/slack-monitor.tar.gz \\\n | python3 -c \"import json,sys; print(json.load(sys.stdin)['tarball_path'])\")\n\necho \"Uploaded: $TARBALL_PATH\"\n```\n\nIf the upload fails with a size error, the tarball must be under 1 MB.\n`main.py` is under 15 KB so this should never trigger.\n\n### Step 5 - Create the automation\n\n```bash\ncurl -s -X POST \"${OPENHANDS_HOST}/api/automation/v1\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\n \\\"name\\\": \\\"Slack Channel Monitor\\\",\n \\\"trigger\\\": {\\\"type\\\": \\\"cron\\\", \\\"schedule\\\": \\\"* * * * *\\\"},\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 55\n }\" | python3 -m json.tool\n```\n\nA 55-second timeout keeps runs well within the 60-second cron window.\n\nRecord the returned `id` - share it with the user as confirmation.\n\n### Step 6 - Confirm\n\nTell the user:\n\n> ✅ **Slack Channel Monitor** is running!\n>\n> - Automation ID: `{id}`\n> - Channels: `{channel list}`\n> - Trigger phrase: `{phrase}`\n> - Polling every minute via cron `* * * * *`\n> - State file: `~/.openhands/workspaces/automation-state/slack_poller_{id}.json`\n>\n> Send a message containing `{phrase}` in any monitored channel to test it.\n> The bot will react with 👀 and reply with a link to the new conversation.\n\n---\n\n## Runtime Behaviour (per poll)\n\nEach cron run executes `main.py`, which runs **10 polling iterations** (every\n5 seconds) within the 55-second timeout window. Each iteration:\n\n1. **Loads state** from the JSON file (see `references/state-schema.md`).\n2. **Resolves the Slack token** - checks `SLACK_USER_TOKEN` then `SLACK_BOT_TOKEN`.\n3. **Fetches new messages:**\n - User token + `search:read` + > 1 channel → single `search.messages` call\n (searches for the trigger phrase across all channels).\n - Otherwise → one `conversations.history` call per channel.\n4. **Fetches thread replies** - one `conversations.replies` call per tracked thread.\n5. **Processes messages** in chronological order:\n - Skips messages already in `processed_ts` (dedup across the overlap window).\n - Skips bot messages and any `ts` in `bot_message_ts`.\n - Reply in a tracked thread (active **or** closed) → forwards to the existing\n conversation; re-opens closed conversations automatically.\n - Contains trigger phrase → 👀 reaction, create conversation, post link.\n - Thread replies: agent receives full thread history for context.\n - Root messages: agent receives the trigger text only.\n6. **Checks conversation statuses** - for each active conversation where\n `time.time() - last_activity > 15 s`:\n - If status is `idle`, `finished`, `error`, or `stuck` → fetch the agent's\n final response via `/api/conversations/{id}/agent_final_response` and post\n it to the Slack thread. Mark conversation `closed`.\n7. **Advances `last_poll`** to `now - 10 s` (overlap window prevents boundary\n races). If a conversation creation failed, pins `last_poll` further back to\n retry on the next iteration.\n8. **Saves state** (including `processed_ts`) and continues to the next iteration.\n9. After all iterations, fires the completion callback.\n\nDebug output is written to both stdout and a persistent log at:\n```\n{WORKSPACE_BASE_ROOT}/automation-state/slack_poller_debug.log\n```\n\n---\n\n## Additional Resources\n\n### Reference Files\n\n- **`references/slack-api.md`** - Slack token types, required scopes, API\n endpoint reference, rate limits, and common error codes.\n- **`references/state-schema.md`** - State JSON schema, field definitions,\n example file, and conversation lifecycle diagram.\n\n### Script Template\n\n- **`scripts/main.py`** - The complete automation script. Customise the three\n constants at the top (`TRIGGER_PHRASE`, `CHANNEL_IDS`, `DEFAULT_OPENHANDS_URL`)\n before packaging.\n\n---\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---|---|---|\n| Bot doesn't react to messages | Token missing or bot not in channel | Verify token with `auth.test`; `/invite @botname` |\n| `not_in_channel` error in run logs | Bot token used but bot not a member | Invite bot or switch to user token |\n| `missing_scope` error | Token lacks required scopes | Re-install Slack app with correct scopes (see `references/slack-api.md`) |\n| No messages detected | `last_poll` timestamp is in the future | Delete the state file to reset; it will be recreated on next run |\n| Conversation link 404 | `OPENHANDS_URL` points to wrong host | Set the `OPENHANDS_URL` secret to the correct base URL |\n| Summary never posted | Conversation stuck in `running` state | Check conversation in the OpenHands UI; the agent may need intervention |\n| Duplicate conversations created | `processed_ts` state missing or corrupted | Delete the state file to reset; dedup will rebuild on next run |\n| Trigger message processed on each cron run | State file deleted between runs | Ensure `automation-state/` directory is persistent across runs |\n| Debug info needed | Need detailed per-message trace | Check `{WORKSPACE_BASE_ROOT}/automation-state/slack_poller_debug.log` |"
390
402
  },
391
403
  {
392
404
  "name": "slack-standup-digest",
@@ -66,7 +66,8 @@ The skill will:
66
66
  1. Verify your Slack token is available
67
67
  2. Resolve channel names to IDs
68
68
  3. Confirm the trigger phrase (or use the default `@openhands`)
69
- 4. Generate and upload a customised automation script
69
+ 4. Generate and upload a customised automation script by copying the template
70
+ and changing only the configuration constants
70
71
  5. Create the automation with cron schedule `* * * * *`
71
72
 
72
73
  ## How It Works
@@ -119,8 +119,13 @@ Accepted values: any non-empty string unlikely to appear accidentally, e.g.
119
119
 
120
120
  ### Step 3 - Generate the automation script
121
121
 
122
- Read `scripts/main.py` from this skill's directory. Apply exactly three
123
- constant substitutions near the top of the file:
122
+ Read `scripts/main.py` from this skill's directory and **copy it verbatim**.
123
+ Apply exactly three constant substitutions near the top of the file:
124
+
125
+ > **Do not reimplement, simplify, or hand-write a replacement script.**
126
+ > The template already contains the correct secret-loading, state-path,
127
+ > conversation-creation, and context-forwarding logic. Only the three
128
+ > configuration constants below should change unless syntax validation fails.
124
129
 
125
130
  | Placeholder | Replace with |
126
131
  |---|---|
@@ -131,7 +136,8 @@ constant substitutions near the top of the file:
131
136
  Write the customised script to a temporary directory:
132
137
  ```bash
133
138
  mkdir -p /tmp/slack-monitor-build
134
- # (write the customised main.py to /tmp/slack-monitor-build/main.py)
139
+ # copy scripts/main.py to /tmp/slack-monitor-build/main.py
140
+ # then replace only the three constants above
135
141
  ```
136
142
 
137
143
  Validate syntax before packaging:
@@ -139,7 +145,19 @@ Validate syntax before packaging:
139
145
  python3 -m py_compile /tmp/slack-monitor-build/main.py && echo "Syntax OK"
140
146
  ```
141
147
 
142
- Fix any syntax errors before proceeding.
148
+ Then run a quick integrity check to confirm the template structure is still
149
+ present and only the configuration block was customised:
150
+ ```bash
151
+ grep -n 'TRIGGER_PHRASE = "' /tmp/slack-monitor-build/main.py
152
+ grep -n 'CHANNEL_IDS: list\[str\] =' /tmp/slack-monitor-build/main.py
153
+ grep -n 'DEFAULT_OPENHANDS_URL = "' /tmp/slack-monitor-build/main.py
154
+ grep -n 'def get_secret' /tmp/slack-monitor-build/main.py
155
+ grep -n 'def _state_file_path' /tmp/slack-monitor-build/main.py
156
+ grep -n 'def create_conversation' /tmp/slack-monitor-build/main.py
157
+ ```
158
+
159
+ If any of those checks fail, stop and re-copy the template instead of trying to
160
+ repair a hand-written variant.
143
161
 
144
162
  ### Step 4 - Package and upload
145
163
 
@@ -198,6 +198,20 @@ class TestCollectNeededCommands:
198
198
  assert spec.path.parent.name == "commands"
199
199
  assert spec.path.suffix == ".md"
200
200
 
201
+ def test_colon_triggers_are_normalized_for_filenames(self, tmp_path, monkeypatch):
202
+ skill_dir = tmp_path / "skills" / "test-skill"
203
+ skill_dir.mkdir(parents=True)
204
+ (skill_dir / "SKILL.md").write_text(
205
+ "---\nname: test-skill\ndescription: Test\ntriggers:\n - /test:command\n---\nBody\n"
206
+ )
207
+
208
+ monkeypatch.setattr("sync_extensions.SKILL_DIRS", [tmp_path / "skills"])
209
+
210
+ specs = collect_needed_commands()
211
+ assert [spec.path.relative_to(tmp_path).as_posix() for spec in specs] == [
212
+ "skills/test-skill/commands/test-command.md"
213
+ ]
214
+
201
215
 
202
216
  # ── load_marketplaces ────────────────────────────────────────────────
203
217