diff --git a/AGENTS.md b/AGENTS.md index 9d35cf4d..4a16fb58 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -92,6 +92,8 @@ When editing or adding skills in this repo, follow these rules (and add new skil - If you change top-level documentation, ensure links still resolve. - `integrations/catalog/*.json` and `integrations/index.js` are the source of truth consumed by `@openhands/extensions`; agent-canvas and integrations-hub import this package directly, so integration marketplace fixes belong here rather than in app-local constants. When upstream MCP projects move repos, verify both `docsUrl` and the connection option (`transport`, `command`/`args`, or URL), not just links. - For Python test runs, prefer `uv sync --group test` followed by `uv run pytest -q`; the full suite depends on `openhands-sdk`, which is not available in the base environment. +- Windows gotcha: regenerating `skills/index.js` from a working tree with CRLF-converted skill files can inject literal `\r` sequences into catalog strings. On Windows, generate the catalog from a checkout or temp clone with `core.autocrlf=false`, then copy the clean `skills/index.js` back. + - Agent-driven plugins (for example `plugins/pr-review` and `plugins/release-notes`) use `uv run --with openhands-sdk --with openhands-tools ...` and require an `LLM_API_KEY` in addition to `GITHUB_TOKEN`. - For OpenHands Cloud API guidance, automations, and CLI integration, use `plugins/openhands`. It is the canonical unified OpenHands plugin covering the V1 Cloud API, Automations API, and CLI. The individual skills (`skills/openhands-api`, `skills/openhands-automation`) are also available standalone. - When reviewing or editing `skills/openhands-sdk`, validate copy-paste imports against the released packages with `uv run --with openhands-tools --with openhands-workspace --with openhands-agent-server python ...`. In the current released workspace package, the exported remote workspace classes are `APIRemoteWorkspace` / `OpenHandsCloudWorkspace`; `RemoteAPIWorkspace` is not available. diff --git a/scripts/build-skills-catalog.mjs b/scripts/build-skills-catalog.mjs index 4f1ebb98..00f5a9c8 100644 --- a/scripts/build-skills-catalog.mjs +++ b/scripts/build-skills-catalog.mjs @@ -61,7 +61,9 @@ export function buildCatalog(skillsDir) { const skillMd = join(dirPath, "SKILL.md"); if (!existsSync(skillMd)) continue; - const raw = readFileSync(skillMd, "utf-8"); + const raw = readFileSync(skillMd, "utf-8") + .replace(/\r\n/g, "\n") + .replace(/\r/g, "\n"); const parts = raw.split("---"); if (parts.length < 3) { console.warn(`Warning: ${skillMd} missing frontmatter sections, skipping`); @@ -89,7 +91,7 @@ const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.arg if (isMain) { const entries = buildCatalog(SKILLS_DIR); - const source = `// Auto-generated by scripts/build-skills-catalog.mjs — do not edit. + const source = `// Auto-generated by scripts/build-skills-catalog.mjs - do not edit. // Source of truth: skills/*/SKILL.md export const SKILLS_CATALOG = ${JSON.stringify(entries, null, 2)}; export default SKILLS_CATALOG; diff --git a/skills/github-pr-review/README.md b/skills/github-pr-review/README.md index 9735cabf..88c0cb92 100644 --- a/skills/github-pr-review/README.md +++ b/skills/github-pr-review/README.md @@ -20,23 +20,40 @@ Bundle ALL comments into a **single review API call**. Do not post comments indi ## Posting a Review -Use the GitHub CLI (`gh`). The `GITHUB_TOKEN` is automatically available. - -```bash -gh api \ - -X POST \ - repos/{owner}/{repo}/pulls/{pr_number}/reviews \ - -f commit_id='{commit_sha}' \ - -f event='COMMENT' \ - -f body='Brief 1-3 sentence summary.' \ - -f comments[][path]='path/to/file.py' \ - -F comments[][line]=42 \ - -f comments[][side]='RIGHT' \ - -f comments[][body]='🟠 Important: Your comment here.' \ - -f comments[][path]='another/file.js' \ - -F comments[][line]=15 \ - -f comments[][side]='RIGHT' \ - -f comments[][body]='🟔 Suggestion: Another comment.' +Use the GitHub CLI (`gh`) with a JSON input file. The `GITHUB_TOKEN` is automatically available. + +**Important**: Always use `--input` with a JSON file instead of inline `-F` flags. This avoids shell quoting issues with special characters in comment bodies and works cleanly across Windows, macOS, and Linux. + +### Step 1: Create a JSON file + +Write the review payload to a JSON file under the system temporary directory (for example `/review.json`). Use the file editor or any shell-appropriate file-writing command. Replace `` with an absolute path for the current OS temp directory. + +```json +{ + "commit_id": "{commit_sha}", + "event": "COMMENT", + "body": "Brief 1-3 sentence summary.", + "comments": [ + { + "path": "path/to/file.py", + "line": 42, + "side": "RIGHT", + "body": "🟠 Important: Your comment here." + }, + { + "path": "another/file.js", + "line": 15, + "side": "RIGHT", + "body": "🟔 Suggestion: Another comment." + } + ] +} +``` + +### Step 2: Post the review + +```text +gh api -X POST repos/{owner}/{repo}/pulls/{pr_number}/reviews --input /review.json ``` ### Parameters @@ -54,32 +71,29 @@ gh api \ For comments spanning multiple lines, add `start_line` to specify the range: -```bash - -f comments[][path]='path/to/file.py' \ - -F comments[][start_line]=10 \ - -F comments[][line]=12 \ - -f comments[][side]='RIGHT' \ - -f comments[][body]='🟔 Suggestion: Refactor this block: - -```suggestion -line_one = "new" -line_two = "code" -line_three = "here" -```' +```json +{ + "path": "path/to/file.py", + "start_line": 10, + "line": 12, + "side": "RIGHT", + "body": "🟔 Suggestion: Refactor this block:\n\n```suggestion\nline_one = \"new\"\nline_two = \"code\"\nline_three = \"here\"\n```" +} ``` -**Important**: The suggestion must have the same number of lines as the range (e.g., lines 10-12 = 3 lines). +**`start_line`/`line` define the range that will be REPLACED.** The suggestion block may have any number of lines — it does **not** have to match the range size. ## Priority Labels -Start each comment with a priority label: +Start each comment with a priority label. **Minimize nits** — leave minor style issues to linters. | Label | When to Use | |-------|-------------| | šŸ”“ **Critical** | Must fix: security vulnerabilities, bugs, data loss risks | | 🟠 **Important** | Should fix: logic errors, performance issues, missing error handling | -| 🟔 **Suggestion** | Nice to have: better naming, code organization | -| 🟢 **Nit** | Optional: formatting, minor style preferences | +| 🟔 **Suggestion** | Worth considering: significant improvements to clarity or maintainability | + +**Do NOT post 🟢 Nit or 🟢 Acceptable comments.** If code is fine, simply don't comment on it. **Example:** ``` @@ -107,39 +121,23 @@ Avoid for: large refactors, architectural changes, ambiguous improvements. ## Finding Line Numbers -```bash -# From diff header: @@ -old_start,old_count +new_start,new_count @@ -# Count from new_start for added/modified lines - -grep -n "pattern" filename # Find line number -head -n 42 filename | tail -1 # Verify line content -``` +Use the file editor, your code editor's line numbers, or another shell-appropriate search command. Verify the exact lines to be replaced before posting a suggestion; do not rely on POSIX-only `grep`, `sed`, or `head | tail` snippets. ## Fallback: curl -If `gh` is unavailable: - -```bash -curl -X POST \ - -H "Authorization: token $GITHUB_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - "https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}/reviews" \ - -d '{ - "commit_id": "{commit_sha}", - "event": "COMMENT", - "body": "Review summary.", - "comments": [ - {"path": "file.py", "line": 42, "side": "RIGHT", "body": "Comment"}, - {"path": "file.py", "start_line": 10, "line": 12, "side": "RIGHT", "body": "Multi-line"} - ] - }' +If `gh` is unavailable, use any HTTP client that can POST the saved JSON file. Example: + +```text +curl -X POST -H "Authorization: token $GITHUB_TOKEN" -H "Accept: application/vnd.github+json" -H "Content-Type: application/json" https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}/reviews --data-binary @/review.json ``` ## Summary -1. Analyze the code and identify issues -2. Post **ONE** review with all inline comments bundled -3. Use priority labels (šŸ”“šŸŸ šŸŸ”šŸŸ¢) on every comment -4. Use suggestion syntax for concrete code changes -5. Keep the review body brief (details go in inline comments) -6. If no issues: post a short approval message \ No newline at end of file +1. Analyze the code and identify important issues (minimize nits) +2. Write review data to a JSON file under the system temporary directory (for example `/review.json`) +3. Post **ONE** review using `gh api --input /review.json` +4. Use priority labels (šŸ”“šŸŸ šŸŸ”) on every comment +5. Do NOT post comments for code that is acceptable — only comment when action is needed +6. Use suggestion syntax for concrete code changes, but verify the resulting code first +7. Keep the review body brief (details go in inline comments) +8. If no issues: post a short approval message with no inline comments \ No newline at end of file diff --git a/skills/github-pr-review/SKILL.md b/skills/github-pr-review/SKILL.md index 706c5716..0f2ead85 100644 --- a/skills/github-pr-review/SKILL.md +++ b/skills/github-pr-review/SKILL.md @@ -17,12 +17,13 @@ Bundle ALL comments into a **single review API call**. Do not post comments indi Use the GitHub CLI (`gh`) with a JSON input file. The `GITHUB_TOKEN` is automatically available. -**Important**: Always use `--input` with a JSON file instead of `-F` flags. This avoids shell quoting issues with special characters in comment bodies (quotes, backticks, newlines, etc.) and eliminates the need for complex heredoc scripts. +**Important**: Always use `--input` with a JSON file instead of inline `-F` flags. This avoids shell quoting issues with special characters in comment bodies (quotes, backticks, newlines, etc.) and eliminates the need for bash-only heredoc scripts. ### Step 1: Create a JSON file -```bash -cat > /tmp/review.json << 'EOF' +Write the review payload to a JSON file under the system temporary directory (for example `/review.json`). Use the file editor or any shell-appropriate file-writing command. Replace `` with an absolute path for the current OS temp directory. + +```json { "commit_id": "{commit_sha}", "event": "COMMENT", @@ -42,13 +43,12 @@ cat > /tmp/review.json << 'EOF' } ] } -EOF ``` ### Step 2: Post the review -```bash -gh api -X POST repos/{owner}/{repo}/pulls/{pr_number}/reviews --input /tmp/review.json +```text +gh api -X POST repos/{owner}/{repo}/pulls/{pr_number}/reviews --input /review.json ``` ### Parameters @@ -145,7 +145,7 @@ Writing the wrong combination of `start_line`/`line` and suggestion body is what For every comment that contains a ` ```suggestion ``` ` block, do this check before adding it to the review JSON: -1. Read the actual file lines that will be replaced: `sed -n ',p' ` (or `sed -n 'p' ` for a single-line target). +1. Read the actual file lines that will be replaced using the file editor, your code editor, or another shell-appropriate file-view command. 2. Mentally apply the suggestion: drop those lines, splice in the suggestion body, and look at the result in context. 3. Confirm the resulting code matches **exactly** what your prose description promises — no extra duplicated line above/below, no original line accidentally dropped, no off-by-one. 4. If the change cannot be expressed cleanly as a contiguous replacement (e.g., it touches non-adjacent lines, or it depends on edits elsewhere in the file), do **not** use a suggestion block — describe the change in prose instead. @@ -154,31 +154,21 @@ If you are not 100% sure the suggestion will produce the exact code you describe ## Finding Line Numbers -```bash -# From diff header: @@ -old_start,old_count +new_start,new_count @@ -# Count from new_start for added/modified lines - -grep -n "pattern" filename # Find line number -head -n 42 filename | tail -1 # Verify line content -``` +Use the file editor, your code editor's line numbers, or another shell-appropriate search command. Verify the exact lines to be replaced before posting a suggestion; do not rely on POSIX-only `grep`, `sed`, or `head | tail` snippets. ## Fallback: curl -If `gh` is unavailable, use curl with the JSON file: +If `gh` is unavailable, use any HTTP client that can POST the saved JSON file. Example: -```bash -curl -X POST \ - -H "Authorization: token $GITHUB_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - "https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}/reviews" \ - -d @/tmp/review.json +```text +curl -X POST -H "Authorization: token $GITHUB_TOKEN" -H "Accept: application/vnd.github+json" -H "Content-Type: application/json" https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}/reviews --data-binary @/review.json ``` ## Summary 1. Analyze the code and identify important issues (minimize nits) -2. Write review data to a JSON file (e.g., `/tmp/review.json`) -3. Post **ONE** review using `gh api --input /tmp/review.json` +2. Write review data to a JSON file under the system temporary directory (for example `/review.json`) +3. Post **ONE** review using `gh api --input /review.json` 4. Use priority labels (šŸ”“šŸŸ šŸŸ”) on every comment 5. Do NOT post comments for code that is acceptable — only comment when action is needed 6. Use suggestion syntax for concrete code changes, but only after verifying the resulting code matches your description (see "How Suggestions Actually Work") diff --git a/skills/github-pr-reviewer/README.md b/skills/github-pr-reviewer/README.md index 71cc11b1..155bb6cd 100644 --- a/skills/github-pr-reviewer/README.md +++ b/skills/github-pr-reviewer/README.md @@ -18,6 +18,8 @@ This skill is activated by: - Uses a real cloned checkout and full PR context instead of only a truncated diff - Posts acknowledgement and final review comments with AI disclosure - Configurable review tone and polling schedule +- Uses reusable helper scripts for packaging and automation creation +- Keeps generated build files in the system temporary directory instead of the repository ## Prerequisites @@ -35,6 +37,12 @@ Ask OpenHands: After setup, apply the configured label to a pull request to queue a review. To request another review later, remove and re-apply the label. +## Helper Scripts + +- `scripts/main.py` - automation script template to customize before upload +- `scripts/package_upload.py` - packages and uploads a prepared build directory +- `scripts/create_automation.py` - creates the cron automation from the uploaded tarball + ## See Also - [SKILL.md](SKILL.md) - Full setup workflow reference diff --git a/skills/github-pr-reviewer/SKILL.md b/skills/github-pr-reviewer/SKILL.md index 9c00be6b..df7b3716 100644 --- a/skills/github-pr-reviewer/SKILL.md +++ b/skills/github-pr-reviewer/SKILL.md @@ -32,11 +32,27 @@ Verify that the following secret is set in **OpenHands Settings -> Secrets**: | `GITHUB_PERSONAL_ACCESS_TOKEN` | Classic PAT | `repo` for private repos or `public_repo` for public repos | | `GITHUB_PERSONAL_ACCESS_TOKEN` | Fine-grained PAT | Contents: Read, Metadata: Read, Pull requests: Read, Issues: Read and Write | -Check with: -```bash -curl -s https://api.github.com/user \ - -H "Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN" \ - | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('login') or d.get('message'))" +Check with any shell-appropriate HTTP client or a short Python script. The +important part is to call `GET https://api.github.com/user` with +`Authorization: Bearer ` and inspect either the authenticated login or +an error message. + +Example Python snippet: +```python +import json +import urllib.request + +token = "" +req = urllib.request.Request( + "https://api.github.com/user", + headers={ + "Authorization": f"Bearer {token}", + "Accept": "application/vnd.github+json", + }, +) +with urllib.request.urlopen(req) as response: + data = json.load(response) +print(data.get("login") or data.get("message")) ``` If the token is missing or invalid, inform the user and stop. @@ -49,7 +65,7 @@ Follow these steps in order. ### Step 1 - Verify `GITHUB_PERSONAL_ACCESS_TOKEN` -Run the `curl` check above. +Run the check above. - If absent: *"GITHUB_PERSONAL_ACCESS_TOKEN is not set. Please add it in OpenHands Settings -> Secrets."* Stop. @@ -61,18 +77,30 @@ Run the `curl` check above. Ask: *"Which GitHub repository should be monitored? (Format: `owner/repo`, e.g. `myorg/backend`)"* -Validate access: -```bash -curl -s "https://api.github.com/repos/{owner}/{repo}" \ - -H "Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN" \ - | python3 -c " -import json, sys -d = json.load(sys.stdin) -if 'message' in d: - print('ERROR:', d['message']) +Validate access with any shell-appropriate HTTP client or Python. The +important part is to call `GET https://api.github.com/repos/{owner}/{repo}` +with the same bearer token and inspect either `message` or `permissions`. + +Example Python snippet: +```python +import json +import urllib.request + +owner_repo = "{owner}/{repo}" +token = "" +req = urllib.request.Request( + f"https://api.github.com/repos/{owner_repo}", + headers={ + "Authorization": f"Bearer {token}", + "Accept": "application/vnd.github+json", + }, +) +with urllib.request.urlopen(req) as response: + data = json.load(response) +if "message" in data: + print("ERROR:", data["message"]) else: - print(f\"Accessible. Private: {d.get('private')}. Permissions: {d.get('permissions')}\") -" + print(f"Accessible. Private: {data.get('private')}. Permissions: {data.get('permissions')}") ``` Record `REPO = "{owner}/{repo}"`. @@ -132,15 +160,15 @@ substitutions near the top of the file: Use a safe string writer such as `json.dumps(value)` when inserting user-provided repository names, labels, or style instructions into Python string literals. -Write the customized script to a temporary build directory: -```bash -mkdir -p /tmp/pr-reviewer-build -# write the customized main.py to /tmp/pr-reviewer-build/main.py -``` +Write the customized script to a build directory under the system temporary +directory, for example `Path(tempfile.gettempdir()) / "github-pr-reviewer-build" / "main.py"` +in Python. Use the file editor or a short Python helper so the path works on +Windows, macOS, and Linux without leaving temp files in the repository. -Validate syntax before packaging: -```bash -python3 -m py_compile /tmp/pr-reviewer-build/main.py && echo "Syntax OK" +Validate syntax before packaging using the current environment's Python +launcher (`python`, `python3`, or `py`): +```text + -m py_compile /main.py ``` Fix any syntax errors before proceeding. @@ -152,35 +180,28 @@ block in your system context: - **OPENHANDS_HOST**: the Automation backend `url_from_agent` - **Auth**: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY` -```bash -tar -czf /tmp/pr-reviewer.tar.gz -C /tmp/pr-reviewer-build . - -TARBALL_PATH=$(curl -s -X POST \ - "${OPENHANDS_HOST}/api/automation/v1/uploads?name=github-pr-reviewer" \ - -H "X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY" \ - -H "Content-Type: application/gzip" \ - --data-binary @/tmp/pr-reviewer.tar.gz \ - | python3 -c "import json,sys; print(json.load(sys.stdin)['tarball_path'])") +Prefer the reusable helper script at `scripts/package_upload.py`. It creates +the tarball under the system temporary directory and prints JSON containing the +remote `tarball_path` plus the local tarball path for debugging. -echo "Uploaded: $TARBALL_PATH" +```text + skills/github-pr-reviewer/scripts/package_upload.py --build-dir --openhands-host --upload-name github-pr-reviewer ``` +Record the returned `tarball_path` as `TARBALL_PATH`. + ### Step 8 - Register the automation -```bash -curl -s -X POST "${OPENHANDS_HOST}/api/automation/v1" \ - -H "X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY" \ - -H "Content-Type: application/json" \ - -d "{ - \"name\": \"GitHub PR Reviewer: {owner}/{repo} label {trigger_label}\", - \"trigger\": {\"type\": \"cron\", \"schedule\": \"{cron_schedule}\"}, - \"tarball_path\": \"$TARBALL_PATH\", - \"entrypoint\": \"python3 main.py\", - \"timeout\": 300 - }" | python3 -m json.tool +Set `entrypoint` to the same launcher that worked in Step 6 (for example +`python main.py`, `python3 main.py`, or `py -3 main.py`). Then call the +reusable helper script at `scripts/create_automation.py`: + +```text + skills/github-pr-reviewer/scripts/create_automation.py --openhands-host --name "GitHub PR Reviewer: {owner}/{repo} label {trigger_label}" --schedule "{cron_schedule}" --tarball-path --entrypoint " main.py" --timeout 300 ``` -Record the returned `id`. +Use shell-appropriate quoting for arguments that contain spaces. Record the +returned `id`. ### Step 9 - Confirm @@ -236,6 +257,10 @@ Each cron run executes `main.py`, which: review lifecycle diagram. - **`scripts/main.py`** - The complete automation script. Customize the five constants at the top before packaging. +- **`scripts/package_upload.py`** - Packages a prepared build directory, writes + the tarball to the system temporary directory, and uploads it. +- **`scripts/create_automation.py`** - Registers the automation from the + uploaded tarball metadata. --- diff --git a/skills/github-pr-reviewer/scripts/create_automation.py b/skills/github-pr-reviewer/scripts/create_automation.py new file mode 100644 index 00000000..fd42e4ff --- /dev/null +++ b/skills/github-pr-reviewer/scripts/create_automation.py @@ -0,0 +1,54 @@ +import argparse +import json +import os +import urllib.request + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Create an OpenHands automation from a packaged tarball." + ) + parser.add_argument("--openhands-host", required=True, help="Automation backend base URL") + parser.add_argument("--name", required=True, help="Automation display name") + parser.add_argument("--schedule", required=True, help="Cron schedule") + parser.add_argument("--tarball-path", required=True, help="Uploaded tarball_path value") + parser.add_argument("--entrypoint", required=True, help="Automation entrypoint command") + parser.add_argument("--timeout", type=int, required=True, help="Automation timeout in seconds") + parser.add_argument( + "--api-key-env", + default="OPENHANDS_AUTOMATION_API_KEY", + help="Environment variable that stores the automation API key", + ) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + api_key = os.environ.get(args.api_key_env, "") + if not api_key: + raise SystemExit(f"Environment variable {args.api_key_env} is not set") + + payload = { + "name": args.name, + "trigger": {"type": "cron", "schedule": args.schedule}, + "tarball_path": args.tarball_path, + "entrypoint": args.entrypoint, + "timeout": args.timeout, + } + request = urllib.request.Request( + f"{args.openhands_host.rstrip('/')}/api/automation/v1", + data=json.dumps(payload).encode(), + headers={ + "X-Session-API-Key": api_key, + "Content-Type": "application/json", + }, + method="POST", + ) + with urllib.request.urlopen(request) as response: + automation = json.load(response) + + print(json.dumps(automation, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/github-pr-reviewer/scripts/package_upload.py b/skills/github-pr-reviewer/scripts/package_upload.py new file mode 100644 index 00000000..f7201ac1 --- /dev/null +++ b/skills/github-pr-reviewer/scripts/package_upload.py @@ -0,0 +1,78 @@ +import argparse +import json +import os +import tarfile +import tempfile +import urllib.request +from pathlib import Path + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Package a build directory and upload it to the OpenHands automation service." + ) + parser.add_argument("--build-dir", required=True, help="Directory to package") + parser.add_argument( + "--openhands-host", + required=True, + help="Automation backend base URL, for example https://app.all-hands.dev", + ) + parser.add_argument( + "--upload-name", + default="github-pr-reviewer", + help="Upload name to send to the automation service", + ) + parser.add_argument( + "--api-key-env", + default="OPENHANDS_AUTOMATION_API_KEY", + help="Environment variable that stores the automation API key", + ) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + build_dir = Path(args.build_dir).expanduser().resolve() + if not build_dir.is_dir(): + raise SystemExit(f"Build directory not found: {build_dir}") + + api_key = os.environ.get(args.api_key_env, "") + if not api_key: + raise SystemExit(f"Environment variable {args.api_key_env} is not set") + + temp_file = tempfile.NamedTemporaryFile( + prefix=f"{args.upload_name}-", + suffix=".tar.gz", + delete=False, + ) + temp_file.close() + tarball_path = Path(temp_file.name) + + with tarfile.open(tarball_path, "w:gz") as tar: + tar.add(build_dir, arcname=".") + + request = urllib.request.Request( + f"{args.openhands_host.rstrip('/')}/api/automation/v1/uploads?name={args.upload_name}", + data=tarball_path.read_bytes(), + headers={ + "X-Session-API-Key": api_key, + "Content-Type": "application/gzip", + }, + method="POST", + ) + with urllib.request.urlopen(request) as response: + upload_data = json.load(response) + + print( + json.dumps( + { + "tarball_path": upload_data["tarball_path"], + "local_tarball_path": str(tarball_path), + }, + indent=2, + ) + ) + + +if __name__ == "__main__": + main() diff --git a/skills/github-repo-monitor/README.md b/skills/github-repo-monitor/README.md index d4bdc376..a5e9cd06 100644 --- a/skills/github-repo-monitor/README.md +++ b/skills/github-repo-monitor/README.md @@ -4,6 +4,10 @@ An OpenHands skill that creates a cron automation to monitor a GitHub repository for issue and PR comments, routing them to OpenHands conversations and posting results back as GitHub comments. +The setup flow is written to stay cross-platform: it uses reusable helper +scripts and the system temporary directory instead of repository-local temp +files, bash-only `/tmp`, heredoc, or `tar` workflows. + ## What it does 1. **Polls** a GitHub repository for new comments on issues and PRs. @@ -24,7 +28,9 @@ github-repo-monitor/ ā”œā”€ā”€ SKILL.md ← agent instructions (loaded automatically) ā”œā”€ā”€ README.md ← this file ā”œā”€ā”€ scripts/ -│ └── main.py ← automation script template +│ ā”œā”€ā”€ main.py ← automation script template +│ ā”œā”€ā”€ package_upload.py ← packages/uploads a prepared build directory +│ └── create_automation.py ← creates the cron automation from the tarball └── references/ ā”œā”€ā”€ state-schema.md ← JSON state file documentation └── github-api.md ← GitHub API endpoints and rate-limit notes diff --git a/skills/github-repo-monitor/SKILL.md b/skills/github-repo-monitor/SKILL.md index ca96b256..ccf7e13b 100644 --- a/skills/github-repo-monitor/SKILL.md +++ b/skills/github-repo-monitor/SKILL.md @@ -48,12 +48,24 @@ before proceeding: | `GITHUB_PERSONAL_ACCESS_TOKEN` | Classic PAT | `repo` (private repos) or `public_repo` (public repos) | | `GITHUB_PERSONAL_ACCESS_TOKEN` | Fine-grained PAT | Issues: Read and Write | -Check with: -```bash -curl -s https://api.github.com/user \ - -H "Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('login') or d.get('message'))" +Check with any shell-appropriate HTTP client or a short Python script. The important part is to call `GET https://api.github.com/user` with `Authorization: Bearer ` and `Accept: application/vnd.github+json`, then read either the authenticated login or the error message. + +Example Python snippet: +```python +import json +import urllib.request + +token = "" +req = urllib.request.Request( + "https://api.github.com/user", + headers={ + "Authorization": f"Bearer {token}", + "Accept": "application/vnd.github+json", + }, +) +with urllib.request.urlopen(req) as response: + data = json.load(response) +print(data.get("login") or data.get("message")) ``` If the token is missing, inform the user and stop — the automation cannot @@ -88,21 +100,28 @@ Fetch the secret and run the `curl` check above. Ask the user: *"Which GitHub repository should be monitored? (Format: `owner/repo`, e.g. `microsoft/vscode`)"* -Validate access and write permissions: - -```bash -curl -s "https://api.github.com/repos/{owner}/{repo}" \ - -H "Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - | python3 -c " -import json, sys -d = json.load(sys.stdin) -if 'message' in d: - print('ERROR:', d['message']) +Validate access and write permissions with any shell-appropriate HTTP client or Python. The important part is to call `GET https://api.github.com/repos/{owner}/{repo}` with the same bearer token and inspect either `message` or `permissions`. + +Example Python snippet: +```python +import json +import urllib.request + +owner_repo = "{owner}/{repo}" +token = "" +req = urllib.request.Request( + f"https://api.github.com/repos/{owner_repo}", + headers={ + "Authorization": f"Bearer {token}", + "Accept": "application/vnd.github+json", + }, +) +with urllib.request.urlopen(req) as response: + data = json.load(response) +if "message" in data: + print("ERROR:", data["message"]) else: - perms = d.get('permissions', {}) - print(f\"Accessible. Private: {d.get('private')}. Permissions: {perms}\") -" + print(f"Accessible. Private: {data.get('private')}. Permissions: {data.get('permissions', {})}") ``` - If `message: Not Found` or `message: Bad credentials` → @@ -180,15 +199,14 @@ constant substitutions near the top of the file: | `ALLOWED_GITHUB_LOGINS = [""]` | `ALLOWED_GITHUB_LOGINS = {allowed_logins_list}` | | `DEFAULT_OPENHANDS_URL = "http://localhost:8000"` | `DEFAULT_OPENHANDS_URL = "{url}"` (keep default if the user has no preference) | -Write the customised script to a temporary build directory: -```bash -mkdir -p /tmp/github-monitor-build -# (write the customised main.py to /tmp/github-monitor-build/main.py) -``` +Write the customised script to a build directory under the system temporary +directory, for example `Path(tempfile.gettempdir()) / "github-monitor-build" / "main.py"` +in Python. Use the file editor or a short Python helper so the path works on +Windows, macOS, and Linux without leaving temp files in the repository. -Validate syntax before packaging: -```bash -python3 -m py_compile /tmp/github-monitor-build/main.py && echo "Syntax OK" +Validate syntax before packaging using the current environment's Python launcher (`python`, `python3`, or `py`): +```text + -m py_compile /main.py ``` Fix any syntax errors before proceeding. @@ -203,38 +221,26 @@ block in your system context: If no Automation backend is listed in ``, stop and tell the user to start the full automation stack. -```bash -tar -czf /tmp/github-monitor.tar.gz -C /tmp/github-monitor-build . - -# OPENHANDS_HOST: read from Automation backend url_from_agent -OPENHANDS_HOST="" - -TARBALL_PATH=$(curl -s -X POST \ - "${OPENHANDS_HOST}/api/automation/v1/uploads?name=github-repo-monitor" \ - -H "X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY" \ - -H "Content-Type: application/gzip" \ - --data-binary @/tmp/github-monitor.tar.gz \ - | python3 -c "import json,sys; print(json.load(sys.stdin)['tarball_path'])") +Prefer the reusable helper script at `scripts/package_upload.py`. It creates +the tarball under the system temporary directory and prints JSON containing the +remote `tarball_path` plus the local tarball path for debugging. -echo "Uploaded: $TARBALL_PATH" +```text + skills/github-repo-monitor/scripts/package_upload.py --build-dir --openhands-host --upload-name github-repo-monitor ``` +Record the returned `tarball_path` as `TARBALL_PATH`. + ### Step 9 - Create the automation -```bash -curl -s -X POST "${OPENHANDS_HOST}/api/automation/v1" \ - -H "X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY" \ - -H "Content-Type: application/json" \ - -d "{ - \"name\": \"GitHub Monitor: {owner}/{repo}\", - \"trigger\": {\"type\": \"cron\", \"schedule\": \"{cron_schedule}\"}, - \"tarball_path\": \"$TARBALL_PATH\", - \"entrypoint\": \"python3 main.py\", - \"timeout\": 55 - }" | python3 -m json.tool +Set `entrypoint` to the same launcher that worked in Step 7 (for example `python main.py`, `python3 main.py`, or `py -3 main.py`). Then call the reusable helper script at `scripts/create_automation.py`: + +```text + skills/github-repo-monitor/scripts/create_automation.py --openhands-host --name "GitHub Monitor: {owner}/{repo}" --schedule "{cron_schedule}" --tarball-path --entrypoint " main.py" --timeout 55 ``` -Record the returned `id`. +Use shell-appropriate quoting for arguments that contain spaces. Record the +returned `id`. ### Step 10 - Confirm @@ -297,9 +303,9 @@ Each cron run executes `main.py`, which: ### Script Template -- **`scripts/main.py`** - The complete automation script. Customise the four +- **`scripts/main.py`** - The complete automation script. Customise the five constants at the top (`REPO`, `TRIGGER_PHRASE`, `EVENT_TYPES`, - `DEFAULT_OPENHANDS_URL`) before packaging. + `ALLOWED_GITHUB_LOGINS`, `DEFAULT_OPENHANDS_URL`) before packaging. --- diff --git a/skills/github-repo-monitor/scripts/create_automation.py b/skills/github-repo-monitor/scripts/create_automation.py new file mode 100644 index 00000000..fd42e4ff --- /dev/null +++ b/skills/github-repo-monitor/scripts/create_automation.py @@ -0,0 +1,54 @@ +import argparse +import json +import os +import urllib.request + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Create an OpenHands automation from a packaged tarball." + ) + parser.add_argument("--openhands-host", required=True, help="Automation backend base URL") + parser.add_argument("--name", required=True, help="Automation display name") + parser.add_argument("--schedule", required=True, help="Cron schedule") + parser.add_argument("--tarball-path", required=True, help="Uploaded tarball_path value") + parser.add_argument("--entrypoint", required=True, help="Automation entrypoint command") + parser.add_argument("--timeout", type=int, required=True, help="Automation timeout in seconds") + parser.add_argument( + "--api-key-env", + default="OPENHANDS_AUTOMATION_API_KEY", + help="Environment variable that stores the automation API key", + ) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + api_key = os.environ.get(args.api_key_env, "") + if not api_key: + raise SystemExit(f"Environment variable {args.api_key_env} is not set") + + payload = { + "name": args.name, + "trigger": {"type": "cron", "schedule": args.schedule}, + "tarball_path": args.tarball_path, + "entrypoint": args.entrypoint, + "timeout": args.timeout, + } + request = urllib.request.Request( + f"{args.openhands_host.rstrip('/')}/api/automation/v1", + data=json.dumps(payload).encode(), + headers={ + "X-Session-API-Key": api_key, + "Content-Type": "application/json", + }, + method="POST", + ) + with urllib.request.urlopen(request) as response: + automation = json.load(response) + + print(json.dumps(automation, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/github-repo-monitor/scripts/package_upload.py b/skills/github-repo-monitor/scripts/package_upload.py new file mode 100644 index 00000000..28477289 --- /dev/null +++ b/skills/github-repo-monitor/scripts/package_upload.py @@ -0,0 +1,78 @@ +import argparse +import json +import os +import tarfile +import tempfile +import urllib.request +from pathlib import Path + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Package a build directory and upload it to the OpenHands automation service." + ) + parser.add_argument("--build-dir", required=True, help="Directory to package") + parser.add_argument( + "--openhands-host", + required=True, + help="Automation backend base URL, for example https://app.all-hands.dev", + ) + parser.add_argument( + "--upload-name", + default="github-repo-monitor", + help="Upload name to send to the automation service", + ) + parser.add_argument( + "--api-key-env", + default="OPENHANDS_AUTOMATION_API_KEY", + help="Environment variable that stores the automation API key", + ) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + build_dir = Path(args.build_dir).expanduser().resolve() + if not build_dir.is_dir(): + raise SystemExit(f"Build directory not found: {build_dir}") + + api_key = os.environ.get(args.api_key_env, "") + if not api_key: + raise SystemExit(f"Environment variable {args.api_key_env} is not set") + + temp_file = tempfile.NamedTemporaryFile( + prefix=f"{args.upload_name}-", + suffix=".tar.gz", + delete=False, + ) + temp_file.close() + tarball_path = Path(temp_file.name) + + with tarfile.open(tarball_path, "w:gz") as tar: + tar.add(build_dir, arcname=".") + + request = urllib.request.Request( + f"{args.openhands_host.rstrip('/')}/api/automation/v1/uploads?name={args.upload_name}", + data=tarball_path.read_bytes(), + headers={ + "X-Session-API-Key": api_key, + "Content-Type": "application/gzip", + }, + method="POST", + ) + with urllib.request.urlopen(request) as response: + upload_data = json.load(response) + + print( + json.dumps( + { + "tarball_path": upload_data["tarball_path"], + "local_tarball_path": str(tarball_path), + }, + indent=2, + ) + ) + + +if __name__ == "__main__": + main() diff --git a/skills/github/README.md b/skills/github/README.md index d3c54768..6bf6d809 100644 --- a/skills/github/README.md +++ b/skills/github/README.md @@ -24,7 +24,7 @@ Examples: - `gh pr checks 200 --watch --interval 10` to check until completed. -If 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` +If 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 using the environment-variable syntax for the current shell. Keep the URL shape `https://@github.com/username/repo.git`, and match the active shell instead of defaulting to bash-only examples. Here are some instructions for pushing, but ONLY do this if the user asks you to: * NEVER push directly to the `main` or `master` branch @@ -35,8 +35,12 @@ Here are some instructions for pushing, but ONLY do this if the user asks you to * Use the main branch as the base branch, unless the user requests otherwise * After opening or updating a pull request, send the user a short message with a link to the pull request. * Do NOT mark a pull request as ready to review unless the user explicitly says so -* 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: -```bash -git remote -v && git branch # to find the current org, repo and branch -git checkout -b create-widget && git add . && git commit -m "Create widget" && git push -u origin create-widget +* Do all of the above in as few steps as possible. For example: +```text +git remote -v +git branch +git switch -c create-widget +git add . +git commit -m "Create widget" +git push -u origin create-widget ``` \ No newline at end of file diff --git a/skills/github/SKILL.md b/skills/github/SKILL.md index 4d22785b..b49dc396 100644 --- a/skills/github/SKILL.md +++ b/skills/github/SKILL.md @@ -19,7 +19,7 @@ Examples: - `gh pr checks 200 --watch --interval 10` to check until completed. -If 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` +If 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 using the environment-variable syntax for the current shell. Keep the URL shape `https://@github.com/username/repo.git`, and match the active shell instead of defaulting to bash-only examples. Here are some instructions for pushing, but ONLY do this if the user asks you to: * NEVER push directly to the `main` or `master` branch @@ -30,10 +30,14 @@ Here are some instructions for pushing, but ONLY do this if the user asks you to * Use the main branch as the base branch, unless the user requests otherwise * After opening or updating a pull request, send the user a short message with a link to the pull request. * Do NOT mark a pull request as ready to review unless the user explicitly says so -* 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: -```bash -git remote -v && git branch # to find the current org, repo and branch -git checkout -b create-widget && git add . && git commit -m "Create widget" && git push -u origin create-widget +* Do all of the above in as few steps as possible. For example: +```text +git remote -v +git branch +git switch -c create-widget +git add . +git commit -m "Create widget" +git push -u origin create-widget ``` ## Handling Review Comments @@ -53,54 +57,29 @@ git checkout -b create-widget && git add . && git commit -m "Create widget" && g To resolve existing review threads programmatically: -1. Get the thread IDs (replace ``, ``, ``): -```bash -gh api graphql -f query=' +1. Get the thread IDs (replace ``, ``, ``). Write the GraphQL body to a JSON file under the system temporary directory such as `/review-threads.json`, then call `gh api graphql --input `: +```json { - repository(owner: "", name: "") { - pullRequest(number: ) { - reviewThreads(first: 20) { - nodes { - id - isResolved - comments(first: 1) { - nodes { body } - } - } - } - } - } -}' + "query": "query { repository(owner: \"\", name: \"\") { pullRequest(number: ) { reviewThreads(first: 20) { nodes { id isResolved comments(first: 1) { nodes { body } } } } } } }" +} ``` -2. Reply to the thread explaining how the feedback was addressed: -```bash -gh api graphql -f query=' -mutation { - addPullRequestReviewThreadReply(input: { - pullRequestReviewThreadId: "" - body: "Fixed in " - }) { - comment { id } - } -}' +2. Reply to the thread explaining how the feedback was addressed by updating the JSON body and reusing the same `gh api graphql --input ` pattern: +```json +{ + "query": "mutation { addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: \"\", body: \"Fixed in \" }) { comment { id } } }" +} ``` -3. Resolve the thread: -```bash -gh api graphql -f query=' -mutation { - resolveReviewThread(input: {threadId: ""}) { - thread { isResolved } - } -}' +3. Resolve the thread with the same file-based GraphQL pattern: +```json +{ + "query": "mutation { resolveReviewThread(input: {threadId: \"\"}) { thread { isResolved } } }" +} ``` 4. Get the failed workflow run ID and rerun it: -```bash -# Find the run ID from the failed check URL, or use: +```text gh run list --repo / --branch --limit 5 - -# Rerun failed jobs gh run rerun --repo / --failed ``` \ No newline at end of file diff --git a/skills/index.js b/skills/index.js index 0737c2a2..3ca7fc3b 100644 --- a/skills/index.js +++ b/skills/index.js @@ -1,4 +1,4 @@ -// Auto-generated by scripts/build-skills-catalog.mjs — do not edit. +// Auto-generated by scripts/build-skills-catalog.mjs - do not edit. // Source of truth: skills/*/SKILL.md export const SKILLS_CATALOG = [ { @@ -157,7 +157,7 @@ export const SKILLS_CATALOG = [ "github", "git" ], - "content": "You have access to an environment variable, `GITHUB_TOKEN`, which allows you to interact with\nthe GitHub API.\n\n\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\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 ``, ``, ``):\n```bash\ngh api graphql -f query='\n{\n repository(owner: \"\", name: \"\") {\n pullRequest(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: \"\"\n body: \"Fixed in \"\n }) {\n comment { id }\n }\n}'\n```\n\n3. Resolve the thread:\n```bash\ngh api graphql -f query='\nmutation {\n resolveReviewThread(input: {threadId: \"\"}) {\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 / --branch --limit 5\n\n# Rerun failed jobs\ngh run rerun --repo / --failed\n```" + "content": "You have access to an environment variable, `GITHUB_TOKEN`, which allows you to interact with\nthe GitHub API.\n\n\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\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 using the environment-variable syntax for the current shell. Keep the URL shape `https://@github.com/username/repo.git`, and match the active shell instead of defaulting to bash-only examples.\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. For example:\n```text\ngit remote -v\ngit branch\ngit switch -c create-widget\ngit add .\ngit commit -m \"Create widget\"\ngit 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 ``, ``, ``). Write the GraphQL body to a JSON file under the system temporary directory such as `/review-threads.json`, then call `gh api graphql --input `:\n```json\n{\n \"query\": \"query { repository(owner: \\\"\\\", name: \\\"\\\") { pullRequest(number: ) { reviewThreads(first: 20) { nodes { id isResolved comments(first: 1) { nodes { body } } } } } } }\"\n}\n```\n\n2. Reply to the thread explaining how the feedback was addressed by updating the JSON body and reusing the same `gh api graphql --input ` pattern:\n```json\n{\n \"query\": \"mutation { addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: \\\"\\\", body: \\\"Fixed in \\\" }) { comment { id } } }\"\n}\n```\n\n3. Resolve the thread with the same file-based GraphQL pattern:\n```json\n{\n \"query\": \"mutation { resolveReviewThread(input: {threadId: \\\"\\\"}) { thread { isResolved } } }\"\n}\n```\n\n4. Get the failed workflow run ID and rerun it:\n```text\ngh run list --repo / --branch --limit 5\ngh run rerun --repo / --failed\n```" }, { "name": "github-actions", @@ -177,7 +177,7 @@ export const SKILLS_CATALOG = [ "triggers": [ "/github-pr-review" ], - "content": "# GitHub PR Review\n\nPost structured code review feedback using the GitHub API with inline comments on specific lines.\n\n## Key Rule: One API Call\n\nBundle ALL comments into a **single review API call**. Do not post comments individually.\n\n## Posting a Review\n\nUse the GitHub CLI (`gh`) with a JSON input file. The `GITHUB_TOKEN` is automatically available.\n\n**Important**: Always use `--input` with a JSON file instead of `-F` flags. This avoids shell quoting issues with special characters in comment bodies (quotes, backticks, newlines, etc.) and eliminates the need for complex heredoc scripts.\n\n### Step 1: Create a JSON file\n\n```bash\ncat > /tmp/review.json << 'EOF'\n{\n \"commit_id\": \"{commit_sha}\",\n \"event\": \"COMMENT\",\n \"body\": \"Brief 1-3 sentence summary.\",\n \"comments\": [\n {\n \"path\": \"path/to/file.py\",\n \"line\": 42,\n \"side\": \"RIGHT\",\n \"body\": \"🟠 Important: Your comment here.\"\n },\n {\n \"path\": \"another/file.js\",\n \"line\": 15,\n \"side\": \"RIGHT\",\n \"body\": \"🟔 Suggestion: Another comment.\"\n }\n ]\n}\nEOF\n```\n\n### Step 2: Post the review\n\n```bash\ngh api -X POST repos/{owner}/{repo}/pulls/{pr_number}/reviews --input /tmp/review.json\n```\n\n### Parameters\n\n| Parameter | Description |\n|-----------|-------------|\n| `commit_id` | Commit SHA to comment on (use `git rev-parse HEAD`) |\n| `event` | `COMMENT`, `APPROVE`, or `REQUEST_CHANGES` |\n| `path` | File path as shown in the diff |\n| `line` | Line number in the NEW version (right side of diff) |\n| `side` | `RIGHT` for new/added lines, `LEFT` for deleted lines |\n| `body` | Comment text with priority label |\n\n### Multi-Line Comments\n\nFor comments spanning multiple lines, add `start_line` to specify the range:\n\n```json\n{\n \"path\": \"path/to/file.py\",\n \"start_line\": 10,\n \"line\": 12,\n \"side\": \"RIGHT\",\n \"body\": \"🟔 Suggestion: Refactor this block:\\n\\n```suggestion\\nline_one = \\\"new\\\"\\nline_two = \\\"code\\\"\\nline_three = \\\"here\\\"\\n```\"\n}\n```\n\n**`start_line`/`line` define the range that will be REPLACED.** The suggestion block may have any number of lines — it does **not** have to match the range size. See the next section for the exact semantics; getting this wrong is how suggestions silently delete or duplicate code.\n\n## Priority Labels\n\nStart each comment with a priority label. **Minimize nits** - leave minor style issues to linters.\n\n| Label | When to Use |\n|-------|-------------|\n| šŸ”“ **Critical** | Must fix: security vulnerabilities, bugs, data loss risks |\n| 🟠 **Important** | Should fix: logic errors, performance issues, missing error handling |\n| 🟔 **Suggestion** | Worth considering: significant improvements to clarity or maintainability |\n\n**Do NOT post 🟢 Nit or 🟢 Acceptable comments.** If code is fine, simply don't comment on it. Inline comments that say \"this looks good\" or \"acceptable trade-off\" are noise — they create review threads that must be resolved without providing actionable value.\n\n**Example:**\n```\n🟠 Important: This function doesn't handle None, which could cause an AttributeError.\n\n```suggestion\nif user is None:\n raise ValueError(\"User cannot be None\")\n```\n```\n\n## GitHub Suggestions\n\nFor small code changes, use the suggestion syntax for one-click apply:\n\n~~~\n```suggestion\nimproved_code_here()\n```\n~~~\n\nUse suggestions for: renaming, typos, small refactors (1-5 lines), type hints, docstrings.\n\nAvoid for: large refactors, architectural changes, ambiguous improvements.\n\n### How Suggestions Actually Work (READ THIS BEFORE WRITING ONE)\n\nA suggestion block **replaces** the targeted range with its contents. The replaced range is:\n\n- `line` only → the single line `line` (replaces 1 line)\n- `start_line` + `line` → the inclusive range `start_line..line` (replaces `line - start_line + 1` lines)\n\nThe suggestion content can be **any number of lines** — 0 (deletion), 1, or many. It does not have to match the range size. Whatever is between the ` ```suggestion ` and closing ` ``` ` fences becomes the new content of those lines.\n\nWriting the wrong combination of `start_line`/`line` and suggestion body is what causes accepted suggestions to **duplicate** or **delete** code. Use the table below as your contract:\n\n| Intent | `start_line` | `line` | Suggestion body must contain |\n|--------|--------------|--------|-------------------------------|\n| Change line N | omit | N | the new content for line N |\n| Change lines N..M | N | M | the new content for the whole block |\n| **Add** a line **after** line N (keep line N) | omit | N | line N's exact current text, then the new line(s) |\n| **Add** a line **before** line N (keep line N) | omit | N | the new line(s), then line N's exact current text |\n| **Insert** lines inside range N..M (keep N..M) | N | M | every original line in N..M plus the new lines, in the final desired order |\n| **Delete** line N | omit | N | empty body (just an empty ` ```suggestion ``` ` block) |\n| **Delete** lines N..M | N | M | empty body |\n\n### Common Mistakes That Break Code\n\n1. **Duplicated lines.** You copy a neighboring line (N-1 or N+1) into the suggestion body as context — that line is still present in the file outside the replaced range, so accepting the suggestion inserts a second copy of it. Fix: only include lines that fall within the targeted range, plus any genuinely new content.\n2. **Disappearing lines.** You target `start_line=10, line=12` to comment on a 3-line block, but your suggestion body only contains 1 line because you \"only want to change line 11\". Accepting that suggestion deletes lines 10 and 12. Fix: either narrow the range to just line 11, or include lines 10 and 12 verbatim in the body.\n3. **Description does not match the suggestion.** The prose says \"rename this variable\" but the suggestion replaces an entire function. Or the prose says \"add a None check\" but the suggestion only contains the check (deleting the original code). Fix: after writing the suggestion, re-read the prose and confirm the resulting file would match it line-for-line.\n\n### Mandatory Verification Before Posting\n\nFor every comment that contains a ` ```suggestion ``` ` block, do this check before adding it to the review JSON:\n\n1. Read the actual file lines that will be replaced: `sed -n ',p' ` (or `sed -n 'p' ` for a single-line target).\n2. Mentally apply the suggestion: drop those lines, splice in the suggestion body, and look at the result in context.\n3. Confirm the resulting code matches **exactly** what your prose description promises — no extra duplicated line above/below, no original line accidentally dropped, no off-by-one.\n4. If the change cannot be expressed cleanly as a contiguous replacement (e.g., it touches non-adjacent lines, or it depends on edits elsewhere in the file), do **not** use a suggestion block — describe the change in prose instead.\n\nIf you are not 100% sure the suggestion will produce the exact code you described, drop the ` ```suggestion ``` ` block and leave a regular inline comment. A correct prose comment is always better than a one-click suggestion that silently corrupts the file.\n\n## Finding Line Numbers\n\n```bash\n# From diff header: @@ -old_start,old_count +new_start,new_count @@\n# Count from new_start for added/modified lines\n\ngrep -n \"pattern\" filename # Find line number\nhead -n 42 filename | tail -1 # Verify line content\n```\n\n## Fallback: curl\n\nIf `gh` is unavailable, use curl with the JSON file:\n\n```bash\ncurl -X POST \\\n -H \"Authorization: token $GITHUB_TOKEN\" \\\n -H \"Accept: application/vnd.github+json\" \\\n \"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}/reviews\" \\\n -d @/tmp/review.json\n```\n\n## Summary\n\n1. Analyze the code and identify important issues (minimize nits)\n2. Write review data to a JSON file (e.g., `/tmp/review.json`)\n3. Post **ONE** review using `gh api --input /tmp/review.json`\n4. Use priority labels (šŸ”“šŸŸ šŸŸ”) on every comment\n5. Do NOT post comments for code that is acceptable — only comment when action is needed\n6. Use suggestion syntax for concrete code changes, but only after verifying the resulting code matches your description (see \"How Suggestions Actually Work\")\n7. Keep the review body brief (details go in inline comments)\n8. If no issues: post a short approval message with no inline comments" + "content": "# GitHub PR Review\n\nPost structured code review feedback using the GitHub API with inline comments on specific lines.\n\n## Key Rule: One API Call\n\nBundle ALL comments into a **single review API call**. Do not post comments individually.\n\n## Posting a Review\n\nUse the GitHub CLI (`gh`) with a JSON input file. The `GITHUB_TOKEN` is automatically available.\n\n**Important**: Always use `--input` with a JSON file instead of inline `-F` flags. This avoids shell quoting issues with special characters in comment bodies (quotes, backticks, newlines, etc.) and eliminates the need for bash-only heredoc scripts.\n\n### Step 1: Create a JSON file\n\nWrite the review payload to a JSON file under the system temporary directory (for example `/review.json`). Use the file editor or any shell-appropriate file-writing command. Replace `` with an absolute path for the current OS temp directory.\n\n```json\n{\n \"commit_id\": \"{commit_sha}\",\n \"event\": \"COMMENT\",\n \"body\": \"Brief 1-3 sentence summary.\",\n \"comments\": [\n {\n \"path\": \"path/to/file.py\",\n \"line\": 42,\n \"side\": \"RIGHT\",\n \"body\": \"🟠 Important: Your comment here.\"\n },\n {\n \"path\": \"another/file.js\",\n \"line\": 15,\n \"side\": \"RIGHT\",\n \"body\": \"🟔 Suggestion: Another comment.\"\n }\n ]\n}\n```\n\n### Step 2: Post the review\n\n```text\ngh api -X POST repos/{owner}/{repo}/pulls/{pr_number}/reviews --input /review.json\n```\n\n### Parameters\n\n| Parameter | Description |\n|-----------|-------------|\n| `commit_id` | Commit SHA to comment on (use `git rev-parse HEAD`) |\n| `event` | `COMMENT`, `APPROVE`, or `REQUEST_CHANGES` |\n| `path` | File path as shown in the diff |\n| `line` | Line number in the NEW version (right side of diff) |\n| `side` | `RIGHT` for new/added lines, `LEFT` for deleted lines |\n| `body` | Comment text with priority label |\n\n### Multi-Line Comments\n\nFor comments spanning multiple lines, add `start_line` to specify the range:\n\n```json\n{\n \"path\": \"path/to/file.py\",\n \"start_line\": 10,\n \"line\": 12,\n \"side\": \"RIGHT\",\n \"body\": \"🟔 Suggestion: Refactor this block:\\n\\n```suggestion\\nline_one = \\\"new\\\"\\nline_two = \\\"code\\\"\\nline_three = \\\"here\\\"\\n```\"\n}\n```\n\n**`start_line`/`line` define the range that will be REPLACED.** The suggestion block may have any number of lines — it does **not** have to match the range size. See the next section for the exact semantics; getting this wrong is how suggestions silently delete or duplicate code.\n\n## Priority Labels\n\nStart each comment with a priority label. **Minimize nits** - leave minor style issues to linters.\n\n| Label | When to Use |\n|-------|-------------|\n| šŸ”“ **Critical** | Must fix: security vulnerabilities, bugs, data loss risks |\n| 🟠 **Important** | Should fix: logic errors, performance issues, missing error handling |\n| 🟔 **Suggestion** | Worth considering: significant improvements to clarity or maintainability |\n\n**Do NOT post 🟢 Nit or 🟢 Acceptable comments.** If code is fine, simply don't comment on it. Inline comments that say \"this looks good\" or \"acceptable trade-off\" are noise — they create review threads that must be resolved without providing actionable value.\n\n**Example:**\n```\n🟠 Important: This function doesn't handle None, which could cause an AttributeError.\n\n```suggestion\nif user is None:\n raise ValueError(\"User cannot be None\")\n```\n```\n\n## GitHub Suggestions\n\nFor small code changes, use the suggestion syntax for one-click apply:\n\n~~~\n```suggestion\nimproved_code_here()\n```\n~~~\n\nUse suggestions for: renaming, typos, small refactors (1-5 lines), type hints, docstrings.\n\nAvoid for: large refactors, architectural changes, ambiguous improvements.\n\n### How Suggestions Actually Work (READ THIS BEFORE WRITING ONE)\n\nA suggestion block **replaces** the targeted range with its contents. The replaced range is:\n\n- `line` only → the single line `line` (replaces 1 line)\n- `start_line` + `line` → the inclusive range `start_line..line` (replaces `line - start_line + 1` lines)\n\nThe suggestion content can be **any number of lines** — 0 (deletion), 1, or many. It does not have to match the range size. Whatever is between the ` ```suggestion ` and closing ` ``` ` fences becomes the new content of those lines.\n\nWriting the wrong combination of `start_line`/`line` and suggestion body is what causes accepted suggestions to **duplicate** or **delete** code. Use the table below as your contract:\n\n| Intent | `start_line` | `line` | Suggestion body must contain |\n|--------|--------------|--------|-------------------------------|\n| Change line N | omit | N | the new content for line N |\n| Change lines N..M | N | M | the new content for the whole block |\n| **Add** a line **after** line N (keep line N) | omit | N | line N's exact current text, then the new line(s) |\n| **Add** a line **before** line N (keep line N) | omit | N | the new line(s), then line N's exact current text |\n| **Insert** lines inside range N..M (keep N..M) | N | M | every original line in N..M plus the new lines, in the final desired order |\n| **Delete** line N | omit | N | empty body (just an empty ` ```suggestion ``` ` block) |\n| **Delete** lines N..M | N | M | empty body |\n\n### Common Mistakes That Break Code\n\n1. **Duplicated lines.** You copy a neighboring line (N-1 or N+1) into the suggestion body as context — that line is still present in the file outside the replaced range, so accepting the suggestion inserts a second copy of it. Fix: only include lines that fall within the targeted range, plus any genuinely new content.\n2. **Disappearing lines.** You target `start_line=10, line=12` to comment on a 3-line block, but your suggestion body only contains 1 line because you \"only want to change line 11\". Accepting that suggestion deletes lines 10 and 12. Fix: either narrow the range to just line 11, or include lines 10 and 12 verbatim in the body.\n3. **Description does not match the suggestion.** The prose says \"rename this variable\" but the suggestion replaces an entire function. Or the prose says \"add a None check\" but the suggestion only contains the check (deleting the original code). Fix: after writing the suggestion, re-read the prose and confirm the resulting file would match it line-for-line.\n\n### Mandatory Verification Before Posting\n\nFor every comment that contains a ` ```suggestion ``` ` block, do this check before adding it to the review JSON:\n\n1. Read the actual file lines that will be replaced using the file editor, your code editor, or another shell-appropriate file-view command.\n2. Mentally apply the suggestion: drop those lines, splice in the suggestion body, and look at the result in context.\n3. Confirm the resulting code matches **exactly** what your prose description promises — no extra duplicated line above/below, no original line accidentally dropped, no off-by-one.\n4. If the change cannot be expressed cleanly as a contiguous replacement (e.g., it touches non-adjacent lines, or it depends on edits elsewhere in the file), do **not** use a suggestion block — describe the change in prose instead.\n\nIf you are not 100% sure the suggestion will produce the exact code you described, drop the ` ```suggestion ``` ` block and leave a regular inline comment. A correct prose comment is always better than a one-click suggestion that silently corrupts the file.\n\n## Finding Line Numbers\n\nUse the file editor, your code editor's line numbers, or another shell-appropriate search command. Verify the exact lines to be replaced before posting a suggestion; do not rely on POSIX-only `grep`, `sed`, or `head | tail` snippets.\n\n## Fallback: curl\n\nIf `gh` is unavailable, use any HTTP client that can POST the saved JSON file. Example:\n\n```text\ncurl -X POST -H \"Authorization: token $GITHUB_TOKEN\" -H \"Accept: application/vnd.github+json\" -H \"Content-Type: application/json\" https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}/reviews --data-binary @/review.json\n```\n\n## Summary\n\n1. Analyze the code and identify important issues (minimize nits)\n2. Write review data to a JSON file under the system temporary directory (for example `/review.json`)\n3. Post **ONE** review using `gh api --input /review.json`\n4. Use priority labels (šŸ”“šŸŸ šŸŸ”) on every comment\n5. Do NOT post comments for code that is acceptable — only comment when action is needed\n6. Use suggestion syntax for concrete code changes, but only after verifying the resulting code matches your description (see \"How Suggestions Actually Work\")\n7. Keep the review body brief (details go in inline comments)\n8. If no issues: post a short approval message with no inline comments" }, { "name": "github-pr-reviewer", @@ -185,7 +185,7 @@ export const SKILLS_CATALOG = [ "triggers": [ "/pr-reviewer:setup" ], - "content": "# GitHub PR Reviewer Automation\n\nCreate a cron automation that watches a GitHub repository for pull requests\nwith a review trigger label, starts an OpenHands review conversation once per\nlabel event, and posts the AI review as a GitHub comment.\n\nThe automation script is deterministic: PR discovery, label-event tracking,\nstate persistence, stale-result suppression, and GitHub comment posting are\nhandled in Python. The LLM is invoked only for the review itself.\n\n---\n\n## Prerequisites\n\n### Required secret\n\nVerify that the following secret is set in **OpenHands Settings -> Secrets**:\n\n| Secret name | Token type | Minimum permissions |\n|---|---|---|\n| `GITHUB_PERSONAL_ACCESS_TOKEN` | Classic PAT | `repo` for private repos or `public_repo` for public repos |\n| `GITHUB_PERSONAL_ACCESS_TOKEN` | Fine-grained PAT | Contents: Read, Metadata: Read, Pull requests: Read, 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 | 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 or invalid, inform the user and stop.\n\n---\n\n## Setup Workflow\n\nFollow these steps in order.\n\n### Step 1 - Verify `GITHUB_PERSONAL_ACCESS_TOKEN`\n\nRun the `curl` check above.\n\n- If absent: *\"GITHUB_PERSONAL_ACCESS_TOKEN is not set. Please add it in\n OpenHands Settings -> Secrets.\"* Stop.\n- If the API returns `{\"message\": \"Bad credentials\"}`: tell the user the\n token is invalid and ask them to update it. Stop.\n\n### Step 2 - Collect repository\n\nAsk: *\"Which GitHub repository should be monitored?\n(Format: `owner/repo`, e.g. `myorg/backend`)\"*\n\nValidate access:\n```bash\ncurl -s \"https://api.github.com/repos/{owner}/{repo}\" \\\n -H \"Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN\" \\\n | python3 -c \"\nimport json, sys\nd = json.load(sys.stdin)\nif 'message' in d:\n print('ERROR:', d['message'])\nelse:\n print(f\\\"Accessible. Private: {d.get('private')}. Permissions: {d.get('permissions')}\\\")\n\"\n```\n\nRecord `REPO = \"{owner}/{repo}\"`.\n\n### Step 3 - Collect trigger label\n\nAsk: *\"Which PR label should trigger a review?\n(Press Enter for the default: `openhands-review`.)\"*\n\nRecord the answer as `TRIGGER_LABEL`. If the label does not exist yet, tell the\nuser that GitHub will still record the event once the label is created and\napplied to a PR.\n\nThe automation reviews a PR when it sees the latest matching `labeled` event for\nthat label. To request another review later, remove and re-apply the label.\n\n### Step 4 - Collect review tone\n\nAsk: *\"What review tone should the reviewer use?\n 1. Thorough (default) - comprehensive coverage of correctness, security, tests, style\n 2. Concise - high-signal only, skips minor style feedback\n 3. Friendly - constructive and encouraging\n(Press Enter for Thorough, or type your choice or any custom style description)\"*\n\nMap the choice to `REVIEW_TONE`:\n\n| Answer | `REVIEW_TONE` | `REVIEW_STYLE_INSTRUCTIONS` |\n|---|---|---|\n| 1 / Enter | `\"thorough\"` | `\"\"` |\n| 2 | `\"concise\"` | `\"\"` |\n| 3 | `\"friendly\"` | `\"\"` |\n| Custom text, e.g. `strict but kind` | `\"thorough\"` | the custom text verbatim |\n\n### Step 5 - Collect cron schedule\n\nAsk: *\"How often should the automation poll for labeled PRs?\n(Press Enter for the default: every 5 minutes.\nUse a cron expression for a different interval, e.g. `0 * * * *` = hourly)\"*\n\nDefault: `*/5 * * * *`.\n\nRecord as `CRON_SCHEDULE`.\n\n### Step 6 - Generate the automation script\n\nRead `scripts/main.py` from this skill's directory. Apply exactly five constant\nsubstitutions near the top of the file:\n\n| Placeholder | Replace with |\n|---|---|\n| `REPO = \"owner/repo\"` | `REPO = \"{owner_repo}\"` |\n| `TRIGGER_LABEL = \"openhands-review\"` | `TRIGGER_LABEL = \"{trigger_label}\"` |\n| `REVIEW_TONE = \"thorough\"` | `REVIEW_TONE = \"{review_tone}\"` |\n| `REVIEW_STYLE_INSTRUCTIONS = \"\"` | `REVIEW_STYLE_INSTRUCTIONS = \"{style_instructions}\"` |\n| `DEFAULT_OPENHANDS_URL = \"http://localhost:8000\"` | leave unchanged unless the user has a preference |\n\nUse a safe string writer such as `json.dumps(value)` when inserting user-provided\nrepository names, labels, or style instructions into Python string literals.\n\nWrite the customized script to a temporary build directory:\n```bash\nmkdir -p /tmp/pr-reviewer-build\n# write the customized main.py to /tmp/pr-reviewer-build/main.py\n```\n\nValidate syntax before packaging:\n```bash\npython3 -m py_compile /tmp/pr-reviewer-build/main.py && echo \"Syntax OK\"\n```\n\nFix any syntax errors before proceeding.\n\n### Step 7 - Package and upload\n\nDetermine the Automation backend URL and auth from the ``\nblock in your system context:\n- **OPENHANDS_HOST**: the Automation backend `url_from_agent`\n- **Auth**: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`\n\n```bash\ntar -czf /tmp/pr-reviewer.tar.gz -C /tmp/pr-reviewer-build .\n\nTARBALL_PATH=$(curl -s -X POST \\\n \"${OPENHANDS_HOST}/api/automation/v1/uploads?name=github-pr-reviewer\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/gzip\" \\\n --data-binary @/tmp/pr-reviewer.tar.gz \\\n | python3 -c \"import json,sys; print(json.load(sys.stdin)['tarball_path'])\")\n\necho \"Uploaded: $TARBALL_PATH\"\n```\n\n### Step 8 - Register 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 PR Reviewer: {owner}/{repo} label {trigger_label}\\\",\n \\\"trigger\\\": {\\\"type\\\": \\\"cron\\\", \\\"schedule\\\": \\\"{cron_schedule}\\\"},\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 300\n }\" | python3 -m json.tool\n```\n\nRecord the returned `id`.\n\n### Step 9 - Confirm\n\nTell the user:\n\n> āœ… **GitHub PR Reviewer** is running!\n>\n> - Automation ID: `{id}`\n> - Repository: `{owner}/{repo}`\n> - Trigger label: `{trigger_label}`\n> - Review tone: `{tone}`\n> - Polling schedule: `{cron_schedule}`\n> - State file: `~/.openhands/workspaces/automation-state/github_pr_reviewer_label_event_{id}.json`\n>\n> Apply the `{trigger_label}` label to a pull request to queue a review. Each\n> label event is processed once. To request another review, remove and re-apply\n> the label.\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` and repository access.\n3. Lists open PRs, newest-updated first.\n4. For each open PR carrying `TRIGGER_LABEL`:\n - Refetches current PR metadata to avoid acting on stale list data.\n - Finds the latest matching GitHub `labeled` issue event.\n - Skips the event if it has already been tracked.\n - Starts an OpenHands conversation with a review prompt that includes PR\n metadata, the exact head SHA, label event details, and instructions to\n clone the repo, inspect PR discussion, review comments, changed files,\n diff, and surrounding code.\n - Posts an acknowledgement comment with the label event, head SHA, and\n conversation link.\n - Records the label-event review in state with `status: \"active\"`.\n5. For each active review conversation:\n - Marks it closed without posting if the PR has closed or merged.\n - Suppresses stale results if the PR head SHA changed after the review was\n queued.\n - When the conversation reaches `idle`, `finished`, `error`, or `stuck`,\n posts the agent's final response as a GitHub comment and marks the review\n closed.\n6. Saves state atomically and fires the completion callback.\n\n---\n\n## Additional Resources\n\n- **`references/state-schema.md`** - State JSON schema, field definitions, and\n review lifecycle diagram.\n- **`scripts/main.py`** - The complete automation script. Customize the five\n constants at the top before packaging.\n\n---\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---|---|---|\n| Bot never queues reviews | Trigger label not present or no matching `labeled` event | Apply the configured label to the PR |\n| \"Bad credentials\" in run logs | Token expired | Rotate and update `GITHUB_PERSONAL_ACCESS_TOKEN` |\n| 404 on repo access | Repo name wrong or no access | Re-check `owner/repo` and token permissions |\n| Same PR not reviewed after new commits | Label event was already processed | Remove and re-apply the trigger label |\n| Review result never posts | Conversation still running or stuck | Open the conversation link from the acknowledgement comment |\n| Stale review suppressed | PR head SHA changed while the agent was reviewing | Re-apply the trigger label after the latest commit |" + "content": "# GitHub PR Reviewer Automation\n\nCreate a cron automation that watches a GitHub repository for pull requests\nwith a review trigger label, starts an OpenHands review conversation once per\nlabel event, and posts the AI review as a GitHub comment.\n\nThe automation script is deterministic: PR discovery, label-event tracking,\nstate persistence, stale-result suppression, and GitHub comment posting are\nhandled in Python. The LLM is invoked only for the review itself.\n\n---\n\n## Prerequisites\n\n### Required secret\n\nVerify that the following secret is set in **OpenHands Settings -> Secrets**:\n\n| Secret name | Token type | Minimum permissions |\n|---|---|---|\n| `GITHUB_PERSONAL_ACCESS_TOKEN` | Classic PAT | `repo` for private repos or `public_repo` for public repos |\n| `GITHUB_PERSONAL_ACCESS_TOKEN` | Fine-grained PAT | Contents: Read, Metadata: Read, Pull requests: Read, Issues: Read and Write |\n\nCheck with any shell-appropriate HTTP client or a short Python script. The\nimportant part is to call `GET https://api.github.com/user` with\n`Authorization: Bearer ` and inspect either the authenticated login or\nan error message.\n\nExample Python snippet:\n```python\nimport json\nimport urllib.request\n\ntoken = \"\"\nreq = urllib.request.Request(\n \"https://api.github.com/user\",\n headers={\n \"Authorization\": f\"Bearer {token}\",\n \"Accept\": \"application/vnd.github+json\",\n },\n)\nwith urllib.request.urlopen(req) as response:\n data = json.load(response)\nprint(data.get(\"login\") or data.get(\"message\"))\n```\n\nIf the token is missing or invalid, inform the user and stop.\n\n---\n\n## Setup Workflow\n\nFollow these steps in order.\n\n### Step 1 - Verify `GITHUB_PERSONAL_ACCESS_TOKEN`\n\nRun the check above.\n\n- If absent: *\"GITHUB_PERSONAL_ACCESS_TOKEN is not set. Please add it in\n OpenHands Settings -> Secrets.\"* Stop.\n- If the API returns `{\"message\": \"Bad credentials\"}`: tell the user the\n token is invalid and ask them to update it. Stop.\n\n### Step 2 - Collect repository\n\nAsk: *\"Which GitHub repository should be monitored?\n(Format: `owner/repo`, e.g. `myorg/backend`)\"*\n\nValidate access with any shell-appropriate HTTP client or Python. The\nimportant part is to call `GET https://api.github.com/repos/{owner}/{repo}`\nwith the same bearer token and inspect either `message` or `permissions`.\n\nExample Python snippet:\n```python\nimport json\nimport urllib.request\n\nowner_repo = \"{owner}/{repo}\"\ntoken = \"\"\nreq = urllib.request.Request(\n f\"https://api.github.com/repos/{owner_repo}\",\n headers={\n \"Authorization\": f\"Bearer {token}\",\n \"Accept\": \"application/vnd.github+json\",\n },\n)\nwith urllib.request.urlopen(req) as response:\n data = json.load(response)\nif \"message\" in data:\n print(\"ERROR:\", data[\"message\"])\nelse:\n print(f\"Accessible. Private: {data.get('private')}. Permissions: {data.get('permissions')}\")\n```\n\nRecord `REPO = \"{owner}/{repo}\"`.\n\n### Step 3 - Collect trigger label\n\nAsk: *\"Which PR label should trigger a review?\n(Press Enter for the default: `openhands-review`.)\"*\n\nRecord the answer as `TRIGGER_LABEL`. If the label does not exist yet, tell the\nuser that GitHub will still record the event once the label is created and\napplied to a PR.\n\nThe automation reviews a PR when it sees the latest matching `labeled` event for\nthat label. To request another review later, remove and re-apply the label.\n\n### Step 4 - Collect review tone\n\nAsk: *\"What review tone should the reviewer use?\n 1. Thorough (default) - comprehensive coverage of correctness, security, tests, style\n 2. Concise - high-signal only, skips minor style feedback\n 3. Friendly - constructive and encouraging\n(Press Enter for Thorough, or type your choice or any custom style description)\"*\n\nMap the choice to `REVIEW_TONE`:\n\n| Answer | `REVIEW_TONE` | `REVIEW_STYLE_INSTRUCTIONS` |\n|---|---|---|\n| 1 / Enter | `\"thorough\"` | `\"\"` |\n| 2 | `\"concise\"` | `\"\"` |\n| 3 | `\"friendly\"` | `\"\"` |\n| Custom text, e.g. `strict but kind` | `\"thorough\"` | the custom text verbatim |\n\n### Step 5 - Collect cron schedule\n\nAsk: *\"How often should the automation poll for labeled PRs?\n(Press Enter for the default: every 5 minutes.\nUse a cron expression for a different interval, e.g. `0 * * * *` = hourly)\"*\n\nDefault: `*/5 * * * *`.\n\nRecord as `CRON_SCHEDULE`.\n\n### Step 6 - Generate the automation script\n\nRead `scripts/main.py` from this skill's directory. Apply exactly five constant\nsubstitutions near the top of the file:\n\n| Placeholder | Replace with |\n|---|---|\n| `REPO = \"owner/repo\"` | `REPO = \"{owner_repo}\"` |\n| `TRIGGER_LABEL = \"openhands-review\"` | `TRIGGER_LABEL = \"{trigger_label}\"` |\n| `REVIEW_TONE = \"thorough\"` | `REVIEW_TONE = \"{review_tone}\"` |\n| `REVIEW_STYLE_INSTRUCTIONS = \"\"` | `REVIEW_STYLE_INSTRUCTIONS = \"{style_instructions}\"` |\n| `DEFAULT_OPENHANDS_URL = \"http://localhost:8000\"` | leave unchanged unless the user has a preference |\n\nUse a safe string writer such as `json.dumps(value)` when inserting user-provided\nrepository names, labels, or style instructions into Python string literals.\n\nWrite the customized script to a build directory under the system temporary\ndirectory, for example `Path(tempfile.gettempdir()) / \"github-pr-reviewer-build\" / \"main.py\"`\nin Python. Use the file editor or a short Python helper so the path works on\nWindows, macOS, and Linux without leaving temp files in the repository.\n\nValidate syntax before packaging using the current environment's Python\nlauncher (`python`, `python3`, or `py`):\n```text\n -m py_compile /main.py\n```\n\nFix any syntax errors before proceeding.\n\n### Step 7 - Package and upload\n\nDetermine the Automation backend URL and auth from the ``\nblock in your system context:\n- **OPENHANDS_HOST**: the Automation backend `url_from_agent`\n- **Auth**: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`\n\nPrefer the reusable helper script at `scripts/package_upload.py`. It creates\nthe tarball under the system temporary directory and prints JSON containing the\nremote `tarball_path` plus the local tarball path for debugging.\n\n```text\n skills/github-pr-reviewer/scripts/package_upload.py --build-dir --openhands-host --upload-name github-pr-reviewer\n```\n\nRecord the returned `tarball_path` as `TARBALL_PATH`.\n\n### Step 8 - Register the automation\n\nSet `entrypoint` to the same launcher that worked in Step 6 (for example\n`python main.py`, `python3 main.py`, or `py -3 main.py`). Then call the\nreusable helper script at `scripts/create_automation.py`:\n\n```text\n skills/github-pr-reviewer/scripts/create_automation.py --openhands-host --name \"GitHub PR Reviewer: {owner}/{repo} label {trigger_label}\" --schedule \"{cron_schedule}\" --tarball-path --entrypoint \" main.py\" --timeout 300\n```\n\nUse shell-appropriate quoting for arguments that contain spaces. Record the\nreturned `id`.\n\n### Step 9 - Confirm\n\nTell the user:\n\n> āœ… **GitHub PR Reviewer** is running!\n>\n> - Automation ID: `{id}`\n> - Repository: `{owner}/{repo}`\n> - Trigger label: `{trigger_label}`\n> - Review tone: `{tone}`\n> - Polling schedule: `{cron_schedule}`\n> - State file: `~/.openhands/workspaces/automation-state/github_pr_reviewer_label_event_{id}.json`\n>\n> Apply the `{trigger_label}` label to a pull request to queue a review. Each\n> label event is processed once. To request another review, remove and re-apply\n> the label.\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` and repository access.\n3. Lists open PRs, newest-updated first.\n4. For each open PR carrying `TRIGGER_LABEL`:\n - Refetches current PR metadata to avoid acting on stale list data.\n - Finds the latest matching GitHub `labeled` issue event.\n - Skips the event if it has already been tracked.\n - Starts an OpenHands conversation with a review prompt that includes PR\n metadata, the exact head SHA, label event details, and instructions to\n clone the repo, inspect PR discussion, review comments, changed files,\n diff, and surrounding code.\n - Posts an acknowledgement comment with the label event, head SHA, and\n conversation link.\n - Records the label-event review in state with `status: \"active\"`.\n5. For each active review conversation:\n - Marks it closed without posting if the PR has closed or merged.\n - Suppresses stale results if the PR head SHA changed after the review was\n queued.\n - When the conversation reaches `idle`, `finished`, `error`, or `stuck`,\n posts the agent's final response as a GitHub comment and marks the review\n closed.\n6. Saves state atomically and fires the completion callback.\n\n---\n\n## Additional Resources\n\n- **`references/state-schema.md`** - State JSON schema, field definitions, and\n review lifecycle diagram.\n- **`scripts/main.py`** - The complete automation script. Customize the five\n constants at the top before packaging.\n- **`scripts/package_upload.py`** - Packages a prepared build directory, writes\n the tarball to the system temporary directory, and uploads it.\n- **`scripts/create_automation.py`** - Registers the automation from the\n uploaded tarball metadata.\n\n---\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---|---|---|\n| Bot never queues reviews | Trigger label not present or no matching `labeled` event | Apply the configured label to the PR |\n| \"Bad credentials\" in run logs | Token expired | Rotate and update `GITHUB_PERSONAL_ACCESS_TOKEN` |\n| 404 on repo access | Repo name wrong or no access | Re-check `owner/repo` and token permissions |\n| Same PR not reviewed after new commits | Label event was already processed | Remove and re-apply the trigger label |\n| Review result never posts | Conversation still running or stuck | Open the conversation link from the acknowledgement comment |\n| Stale review suppressed | PR head SHA changed while the agent was reviewing | Re-apply the trigger label after the latest commit |" }, { "name": "github-repo-monitor", @@ -193,7 +193,7 @@ export const SKILLS_CATALOG = [ "triggers": [ "/github-monitor:poll" ], - "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 | `[\"\"]` |\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 = [\"\"]` | `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 ``\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 ``, 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 Automation backend url_from_agent\nOPENHANDS_HOST=\"\"\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 |" + "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 any shell-appropriate HTTP client or a short Python script. The important part is to call `GET https://api.github.com/user` with `Authorization: Bearer ` and `Accept: application/vnd.github+json`, then read either the authenticated login or the error message.\n\nExample Python snippet:\n```python\nimport json\nimport urllib.request\n\ntoken = \"\"\nreq = urllib.request.Request(\n \"https://api.github.com/user\",\n headers={\n \"Authorization\": f\"Bearer {token}\",\n \"Accept\": \"application/vnd.github+json\",\n },\n)\nwith urllib.request.urlopen(req) as response:\n data = json.load(response)\nprint(data.get(\"login\") or data.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 with any shell-appropriate HTTP client or Python. The important part is to call `GET https://api.github.com/repos/{owner}/{repo}` with the same bearer token and inspect either `message` or `permissions`.\n\nExample Python snippet:\n```python\nimport json\nimport urllib.request\n\nowner_repo = \"{owner}/{repo}\"\ntoken = \"\"\nreq = urllib.request.Request(\n f\"https://api.github.com/repos/{owner_repo}\",\n headers={\n \"Authorization\": f\"Bearer {token}\",\n \"Accept\": \"application/vnd.github+json\",\n },\n)\nwith urllib.request.urlopen(req) as response:\n data = json.load(response)\nif \"message\" in data:\n print(\"ERROR:\", data[\"message\"])\nelse:\n print(f\"Accessible. Private: {data.get('private')}. Permissions: {data.get('permissions', {})}\")\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 | `[\"\"]` |\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 = [\"\"]` | `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 build directory under the system temporary\ndirectory, for example `Path(tempfile.gettempdir()) / \"github-monitor-build\" / \"main.py\"`\nin Python. Use the file editor or a short Python helper so the path works on\nWindows, macOS, and Linux without leaving temp files in the repository.\n\nValidate syntax before packaging using the current environment's Python launcher (`python`, `python3`, or `py`):\n```text\n -m py_compile /main.py\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 ``\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 ``, stop and tell\nthe user to start the full automation stack.\n\nPrefer the reusable helper script at `scripts/package_upload.py`. It creates\nthe tarball under the system temporary directory and prints JSON containing the\nremote `tarball_path` plus the local tarball path for debugging.\n\n```text\n skills/github-repo-monitor/scripts/package_upload.py --build-dir --openhands-host --upload-name github-repo-monitor\n```\n\nRecord the returned `tarball_path` as `TARBALL_PATH`.\n\n### Step 9 - Create the automation\n\nSet `entrypoint` to the same launcher that worked in Step 7 (for example `python main.py`, `python3 main.py`, or `py -3 main.py`). Then call the reusable helper script at `scripts/create_automation.py`:\n\n```text\n skills/github-repo-monitor/scripts/create_automation.py --openhands-host --name \"GitHub Monitor: {owner}/{repo}\" --schedule \"{cron_schedule}\" --tarball-path --entrypoint \" main.py\" --timeout 55\n```\n\nUse shell-appropriate quoting for arguments that contain spaces. Record the\nreturned `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 five\n constants at the top (`REPO`, `TRIGGER_PHRASE`, `EVENT_TYPES`,\n `ALLOWED_GITHUB_LOGINS`, `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 |" }, { "name": "gitlab",