@openhands/extensions 0.30.0 โ 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/custom-codereview-guide.md +3 -4
- package/.claude-plugin/marketplace.json +13 -0
- package/.codex-plugin/marketplace.json +13 -0
- package/.github/workflows/pr-artifacts.yml +4 -83
- package/.github/workflows/tests.yml +13 -0
- package/.plugin/marketplace.json +13 -0
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +4 -3
- package/README.md +3 -2
- package/automations/bundle-index.js +1 -1
- package/automations/catalog/github-pr-reviewer/manifest.json +2 -2
- package/marketplaces/openhands-extensions.json +13 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/python/openhands_extensions/_version.py +1 -1
- package/skills/github-pr-reviewer/README.md +6 -0
- package/skills/github-pr-reviewer/SKILL.md +7 -0
- package/skills/github-stale-ci-pr-closer/scripts/worker.py +8 -2
- package/skills/index.js +12 -2
- package/skills/slack-channel-monitor/README.md +6 -2
- package/skills/slack-channel-monitor/SKILL.md +29 -17
- package/skills/slack-channel-monitor/references/windows.md +6 -2
- package/skills/slack-channel-monitor/scripts/main.py +156 -11
- package/skills/software-factory/.claude-plugin/plugin.json +19 -0
- package/skills/software-factory/.codex-plugin/plugin.json +19 -0
- package/skills/software-factory/.plugin/plugin.json +19 -0
- package/skills/software-factory/README.md +64 -0
- package/skills/software-factory/SKILL.md +229 -0
- package/skills/software-factory/commands/software-factory.md +8 -0
- package/skills/software-factory/scripts/factory.py +630 -0
- package/tests/test_github_stale_ci_pr_closer.py +52 -1
- package/tests/test_software_factory_script.py +231 -0
package/skills/index.js
CHANGED
|
@@ -283,7 +283,7 @@ export const SKILLS_CATALOG = [
|
|
|
283
283
|
"triggers": [
|
|
284
284
|
"/pr-reviewer:setup"
|
|
285
285
|
],
|
|
286
|
-
"content": "# GitHub PR Reviewer Automation\n\n## Agent Canvas catalog\n\nFor new Agent Canvas installations, use the **GitHub code review** catalog\nentry. Its deterministic `worker.py` delegates each requested exact head to a\nstable conversation using the selected agent profile. It supports GitHub\nreviewer-request events and scheduled label scans. The manual upload flow below\nremains for existing deployments and is deprecated for new installations.\n\nCreate a cron automation that watches one or more GitHub repositories for pull\nrequests with a review trigger label, starts an OpenHands review conversation\nonce per label event, and publishes the AI review to GitHub.\nWindows PowerShell equivalents for the setup, packaging, upload, and API-check shell snippets are in `references/windows.md`.\n\nThe automation script is deterministic: PR discovery, label-event tracking,\nhead eligibility, state persistence, stale-result suppression, the repository\ncheckout, and its removal are all handled in Python. The LLM is invoked only for\nthe review itself.\n\nBefore any review conversation is created, a scheduled scan evaluates the\ncurrent head's GitHub-required checks, read through the GraphQL `isRequired`\nsignal for the pull request. That signal is the merge policy's own source of\ntruth and is pull-request-scoped, so it stays correct for a stacked PR whose\nsymbolic base branch carries no branch rules of its own:\n\n- Only checks GitHub reports as required decide the scheduled gate. An optional\n workflow that fails - including one that fails before creating any check run -\n does not block a head whose required checks pass.\n- A completed required run on the exact head whose conclusion is `failure`,\n `cancelled`, or `timed_out` blocks the review. Any other unrecognized\n conclusion fails closed as a block rather than silently approving.\n- A completed required run whose conclusion is `success`, `neutral`, or\n `skipped` does not block. A required `commit status` context is classified\n alongside required check runs.\n- A `queued` or `in_progress` required run on the exact head, and a required\n context that has not reported a current-head check run at all, make the run\n exit with a **waiting on checks** outcome. Unfulfilled is never approval. The\n worker does not hold a slot polling; the next scheduled scan or explicit\n review request retries.\n- When the required-check signal cannot be read or is empty, the gate falls back\n to every current-head check run and workflow run, so a red head still blocks.\n In that fallback, workflow runs are read as well as check runs, because a\n workflow can fail before creating any check run - a workflow-level error, or a\n `pull_request` run whose jobs never start. Such a run leaves a failed check\n suite with no check runs under it, so the commit's check-run rollup and\n `gh pr checks` both report success and only the workflow run reveals the red\n CI. A workflow run whose check suite already reported check runs is left to\n those runs, so a workflow is never counted twice.\n- A scheduled scan considers the trigger label **and** every open PR, including drafts,\n that still holds an outstanding `all-hands-bot` review request. That is what\n resumes a request made while CI was running: the request is keyed by its own\n `review_requested` event, so repeated scans reuse one conversation and one\n review instead of creating duplicates, and a PR whose request was answered or\n withdrawn simply drops out of `requested_reviewers`.\n- The scheduled scan also reviews open, non-draft PRs that **nobody requested**\n and that carry no trigger label, once their current head has no completed\n review by this account. That is what keeps reviewing PRs after the outstanding\n requests are exhausted. The delivery key is the stable\n `scan:{repository}:{number}:{head}`, so repeated scans reuse one conversation\n and one native review, and a changed head becomes eligible again under its new\n SHA.\n- Each scheduled scan classifies the **whole open backlog**, including\n unrequested PRs. Blocked, pending, draft, or already-reviewed heads therefore\n cannot hide an eligible head behind an inspection window. The separate\n `max_new_per_run` quota limits only newly created review conversations, not the\n number of PRs inspected; explicit requests and trigger labels retain priority\n when eligible candidates are drained.\n- A gate stop on an **unrequested** head posts no managed comment. A PR nobody\n asked about that is merely red or pending is simply skipped without starting an\n agent, which is what keeps a scan over a large backlog from posting a gate\n comment on every PR. The managed comment is the answer to an explicit request,\n so a requested or labeled head that is red or pending still gets its\n explanation.\n- Workflow runs are read as well as check runs, because a workflow can fail\n before creating any check run - a workflow-level error, or a `pull_request`\n run whose jobs never start. Such a run leaves a failed check suite with no\n check runs under it, so the commit's check-run rollup and `gh pr checks` both\n report success and only the workflow run reveals the red CI. A workflow run\n whose check suite already reported check runs is left to those runs, so a\n workflow is never counted twice.\n- Runs attributed to any other (obsolete) head SHA are ignored, so a stale\n failure cannot block the push that fixed it. This applies to workflow runs\n too.\n- Only the latest run of each logical check or workflow counts. A logical check\n is its name plus the reporting app identity, and a logical workflow is its\n name plus workflow ID. The latest is chosen by the run ID (the reliable\n creation sequence) with the start time as a tie-break, so a re-run that fixed\n a check supersedes its earlier failure on the same SHA while a newer queued or\n in-progress re-run supersedes an earlier success and makes the head wait.\n Ordering by the run ID keeps a run whose start time is still absent in its\n true creation position in both directions.\n\nAn explicit `all-hands-bot` review request is the intake-policy exception: the\ncaller asked for that head by name, so the request dispatches even when required\nCI is red or pending, and the gate leaves no explanatory comment. Draft, scope,\nexact-head, and delivery-deduplication safeguards still apply. A blocked or\nwaiting scheduled stop consumes no trigger: once the head's required checks are\nnon-blocking, the scheduled scan, or a new `all-hands-bot` review request,\nstarts the normal review.\n\nThe gate needs no configured list of check names. When it stops a review it\nleaves one concise explanation on the PR, identified by a hidden marker carrying\nthe head SHA and gate category, so a later run for a different head updates that\ncomment instead of posting another. Only a marker this reviewer account authored\ncounts as its own comment; every other PR comment is untrusted and can neither\nsuppress the explanation nor be edited. If the repository's own workflow already\nposted a deterministic remediation comment that names the same current-head\nchecks, the gate adds nothing; a disclosure about some other check does not\nsuppress it. The gate applies to scheduled label scans; an explicit\n`all-hands-bot` review request bypasses it by design.\n\nEach explicit review request - a re-applied trigger label, or a new reviewer\nrequest in event mode - is a fresh review. The conversation for a PR is reused,\nbut its earlier turns must not be trusted as current: before deciding a verdict\nthe reviewer re-fetches the mutable GitHub state (the exact head, the PR body,\nreview comments and threads, review requests, the linked issues' bodies and\nlabels, and the current-head Actions results) and ignores any earlier finding,\nverdict, or label/priority claim that state no longer supports. Repository\nanalysis the conversation already did, such as reading `AGENTS.md`, stays\nuseful and is not repeated.\n\nThe waiting and blocked explanations name the retry the deployment actually has.\nA scheduled run proves a scan is configured, so it says the next scan will\nretry. An event-only run does not, so it tells the reader to remove the\noutstanding `all-hands-bot` request and request `all-hands-bot` again instead of\npromising a scan that does not exist: GitHub will not accept a second request\nwhile the first is still outstanding.\n\nThe gate leaves only one explanation per head. When the same head keeps the same\nhidden marker but the retry wording changes -- an event-only comment on a head\nwhose automation is later switched to a cron scan -- the scheduled run rewrites\nthat managed comment in place, so the comment always names the retry that is\nactually deployed, and an unchanged body is left untouched.\n\n## Bounded intake per scheduled scan\n\nA scheduled scan drains outstanding reviewer requests fairly, but starting an\nagent for every eligible pull request at once would overload the deployment, so\nthe scan starts at most `MAX_NEW_PER_RUN` new review conversations, counted\n**across every configured repository** rather than per repository. The default\nis `2`, and the rendered `config.json` overrides it through the\n`max_new_per_run` key that `github-issue-to-pr` and `gitlab-issue-to-mr` already\nuse.\n\n- Eligible candidates are ordered deterministically: an explicit `all-hands-bot`\n request (or a trigger label) is ordered before an unrequested PR, then by the\n oldest `all-hands-bot` `review_requested` event for a requested PR or the PR's\n own creation time (oldest first) for an unrequested one, then by repository,\n then by pull-request number, so the oldest work drains first and a later scan\n reaches the remainder.\n- The bound counts the conversations a scan **starts**. A delivery that only\n deduplicates or reports an already-running conversation reuses a runtime and\n consumes no slot, so repeated scans make progress on the backlog instead of\n re-spending the bound on work already in flight.\n- Reaching the bound never cuts the scan short. The scan still evaluates the\n exact-head checks of the remaining candidates, reconciles completed reviews,\n and runs the maintainer handoff. A candidate whose checks are pending or\n failing starts no conversation and consumes no slot, so it cannot block a later\n green candidate. Only an explicit request or a trigger label gets a waiting or\n blocked gate comment; an unrequested head that is merely red or pending is\n skipped silently.\n- A dispatch that raises is reported and does not consume a slot or abort the\n scan, so the candidates behind it are still considered.\n- The explicit `review_requested` event path and the trigger-label scan are\n unchanged: an explicit request still starts its conversation immediately, and\n only the scheduled scan's new conversations are bounded.\n\nThe review prompt starts with a scope gate: using the repository's own guidance\n(its scope categories and ownership boundaries, not a list of individual PR\nnumbers), the reviewer decides whether the change belongs in this repository\nand has the product/architecture direction it needs. When it does not, the review\nstops with a single `event: COMMENT` review that says whether the change should\nmove repositories, close, or receive a maintainer decision, and ends with the\n`๐ MAINTAINER DECISION REQUIRED` verdict. That outcome is **neither an approval\nnor a change request**: it does not approve or merge the PR. The completion\nhandler recognizes the verdict and requests one configured maintainer through the\nsame handoff used after an approval. Both paths require a successfully fetched\nsame-repository closing issue labeled `priority:medium` or `priority:high`; a\nmissing link or priority label, low/normal priority, unresolved reference, or PR\nreference does not authorize handoff. One qualifying issue is sufficient even\nwith other low-priority or missing references. Existing manual review requests\nare unchanged. An in-scope change continues the existing review unchanged.\n\nBefore approving a change to user-visible UI behavior, the prompt requires live\nevidence from a real running application when the repository's guidance demands\nit: a screenshot, screen recording, or equivalent capture of the running app\nthat exercises the production-facing path. Unit tests, CSS-token or contract\nassertions, generated mockups, and reconstructed captures may support the review\nbut cannot substitute for that evidence. When the required evidence is missing,\nthe reviewer publishes one `event: COMMENT` review naming exactly what is\nmissing and ends with `๐ CHANGES REQUESTED`, so approval is withheld and the\ndeterministic maintainer handoff does not fire. Non-UI changes keep the existing\nrequirement of the real command and its observed output, with tests insufficient\nas sole proof.\n\nEvery review ends with the LLM profile and model of the conversation that wrote\nit, after the verdict: `LLM profile: <profile> ยท Model: <model>`. The agent does\nnot write this footer. The prompt gives it a hidden marker naming its own\nconversation, `<!-- openhands-review-run: <conversation-id> -->`, to copy under\nthe disclosure, and the deterministic script appends the footer to the reviews\nthat carry exactly that marker and no other. Login, head, and submission time\ncannot tell two automations that share a bot account apart, so a review without\nthe marker, with another run's marker, or with several markers is never edited.\nThe catalog worker reads the values back from the marked conversation: the\nmodel it runs, and the LLM profile its agent profile referenced at launch\n(`unknown` once that agent profile has been edited). It never substitutes the\nscanner's own settings, and a reused conversation keeps reporting the LLM it\nwas launched with. A review is stamped once, so a later agent profile edit\ncannot rewrite its footer, and a provenance failure is logged without holding\nup the verdict or the maintainer handoff. The completion handler reads the\nverdict above the footer.\n\nThe script prepares each review's workspace before the agent starts: the pull\nrequest's head commit is downloaded as a tarball and extracted to a directory of\nits own, which becomes the conversation's working directory. The agent is told\nnot to clone, fetch, check out, or delete anything, and the script removes the\ncheckout once the conversation has stopped. Nothing accumulates between runs.\n\n---\n\nThe script imports shared GitHub transport from\n`scripts/github_client.py`, installed with this skill. Include it beside\n`main.py` when packaging manually, as shown below; catalog bundles include it\nautomatically.\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 and Write**, Issues: Read and Write |\n\nPull-request **write** access is required because the agent publishes a pull\nrequest review, not just an issue comment. The Agent Canvas catalog worker may\nalso request a configured human reviewer after approval. A token with only Pull\nrequests: Read will poll happily and then fail at the point of publishing or\nrequesting the handoff.\n\nWhen several repositories are monitored, the token must cover all of them.\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 repositories\n\nAsk: *\"Which GitHub repositories should be monitored?\n(Format: `owner/repo`, e.g. `myorg/backend`. List several separated by commas to\nreview them all from one automation.)\"*\n\nValidate access to **each** repository:\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 every accepted repository into `REPOS = [\"{owner}/{repo}\", ...]`. If one\nrepository fails the check, say which and ask whether to continue without it.\n\nEach repository is polled independently and keeps its own state, so pull-request\nnumbers never collide between them. The trigger label, tone, and schedule are\nshared by all of them; a repository needing different settings wants its own\nautomation.\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 seven constant\nsubstitutions near the top of the file:\n\n> The script also reads a `config.json` shipped beside it, if there is one, over\n> these constants. That is how the catalog entry\n> (`automations/catalog/github-pr-reviewer/`) configures an unmodified copy,\n> since a declarative host cannot rewrite Python. This setup path substitutes the\n> constants and ships no `config.json`, so the two never collide.\n\n| Placeholder | Replace with |\n|---|---|\n| `REPOS = [\"owner/repo\"]` | `REPOS = [\"{owner_repo}\", ...]` - one entry per repository collected in Step 2 |\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| `REPO_REVIEW_GUIDE_PATH = \".agents/skills/custom-codereview-guide.md\"` | leave unchanged to auto-load a repo review guide at this path, or set to `\"\"` to disable |\n| `MAX_NEW_PER_RUN = 2` | leave unchanged to bound a scheduled scan to two new review conversations across all repositories, or raise it if the deployment can hold more agents at once |\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`json.dumps(list_of_repos)` produces the whole `REPOS` list safely in one step.\n\nRun these commands from this skill's directory and write the customized script\nto a temporary build directory:\n```bash\nmkdir -p /tmp/pr-reviewer-build\ncp -L scripts/github_client.py /tmp/pr-reviewer-build/github_client.py\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 `<RUNTIME_SERVICES>`\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: {repo_summary} label {trigger_label}\\\",\n \\\"trigger\\\": {\\\"type\\\": \\\"cron\\\", \\\"schedule\\\": \\\"{cron_schedule}\\\"},\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 600\n }\" | python3 -m json.tool\n```\n\nUse the single repository as `{repo_summary}` when there is one, and something\nlike `3 repos` when there are several. A poll now downloads a tarball per queued\nreview, so the timeout allows for that; a run never waits for a review to\nfinish, only for it to be started.\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> - Repositories: `{owner}/{repo}`, ... (one line each)\n> - Trigger label: `{trigger_label}`\n> - Review tone: `{tone}`\n> - Polling schedule: `{cron_schedule}`\n> - State file per repository:\n> `~/.openhands/workspaces/automation-state/github_pr_reviewer_label_event_{id}_{owner}__{repo}.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> The review is published as a pull request review on the head commit, with\n> inline comments where a finding maps to a changed line.\n\n---\n\n## Runtime Behaviour (per poll)\n\nEach cron run executes `main.py`, which resolves and validates\n`GITHUB_PERSONAL_ACCESS_TOKEN` once, then processes every repository in `REPOS`\nindependently. One repository failing does not stop the others; the run fails\nonly if every repository fails.\n\nFor each repository:\n\n1. Loads that repository's state (see `references/state-schema.md`).\n2. Verifies 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 - Downloads the PR's head commit as a tarball and extracts it to\n `{WORKSPACE_BASE}/repositories/{owner}__{repo}/pr-{number}-{sha12}`. The\n archive is checked as it is unpacked: a single root, no absolute or `..`\n paths, and symlinks skipped rather than materialised.\n - Records `default` and the concrete model from the server settings already\n used to start the conversation, for the review footer.\n - Starts an OpenHands conversation **whose working directory is that\n checkout**, under an id chosen before it starts, with a review prompt\n carrying PR metadata, the exact head SHA, label event details, the\n requirement to re-fetch the current mutable GitHub state before deciding\n a verdict, and that conversation's review marker.\n - Posts an acknowledgement comment with the label event, head SHA, and\n conversation link.\n - Records the review in state with `status: \"active\"` and the checkout path.\n - If the checkout or the conversation cannot be created, the checkout is\n removed and nothing is recorded, so the next poll retries the label event.\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 verifies a submitted review by the token's own user at that head SHA and\n appends the recorded LLM profile and model to each review carrying this\n conversation's marker before marking the review complete. Reviews without\n that marker are confirmed but never edited. If no review exists, the\n agent's final response is posted as a comment with provenance. Failed\n verification or publication is retried on the next poll.\n - Abandons a conversation that has not reached a terminal status within two\n hours, so its checkout can be reclaimed.\n6. Removes the checkout of every finished review, but only after confirming the\n conversation has stopped - deleting it under a running agent would remove its\n working directory. When that cannot be confirmed the directory is left alone\n and the next poll tries again.\n7. Saves that repository's state atomically.\n\nThe completion callback fires once for the whole run.\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- **`tests/test_main.py`** - Unit tests for the checkout, its removal, and state\n handling. Run them from the skill root with `python -m pytest tests/` after\n editing the script.\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 the entry in `REPOS` and the token's permissions |\n| One repository is skipped, others work | That repository failed its access check | Read the `=== owner/repo ===` block in the run log |\n| Same PR not reviewed after new commits | Label event was already processed | Remove and re-apply the trigger label |\n| Review paused with a failing-check comment | A current-head required check reported `failure`, `cancelled`, or `timed_out` | Fix the named checks and push; the review starts on the new head, or request `all-hands-bot` to review immediately |\n| Review reported waiting on checks | A current-head required check is `queued` or `in_progress`, or has not reported yet | No action; a later scan or a new review request retries |\n| Optional workflow failed but no review was paused | The failed workflow is not required, so the required-only scheduled gate ignored it | No action; only GitHub-required checks gate scheduled discovery |\n| Only a few reviews start on a large backlog | The per-scan `max_new_per_run` bound (default 2) reached | No action; later scans drain the remaining oldest eligible PRs, or raise `max_new_per_run` if the deployment can hold more agents |\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 |\n| Review arrives as a plain comment, not a review | Publishing failed, so the script posted the text as a fallback | Check that the token has Pull requests: Read and Write |\n| Agent reports it cannot clone the repo | Prompt asked it not to; the workspace is already the checkout | No action - the code is at the head SHA in its working directory |\n| Checkouts remain under `repositories/` | Their conversations had not stopped yet | They are removed by a later poll once the conversation is terminal |",
|
|
286
|
+
"content": "# GitHub PR Reviewer Automation\n\n## Agent Canvas catalog\n\nFor new Agent Canvas installations, use the **GitHub code review** catalog\nentry. Its deterministic `worker.py` delegates each requested exact head to a\nstable conversation using the selected agent profile. It supports GitHub\nreviewer-request events and scheduled label scans. The manual upload flow below\nremains for existing deployments and is deprecated for new installations.\n\nCreate a cron automation that watches one or more GitHub repositories for pull\nrequests with a review trigger label, starts an OpenHands review conversation\nonce per label event, and publishes the AI review to GitHub.\nWindows PowerShell equivalents for the setup, packaging, upload, and API-check shell snippets are in `references/windows.md`.\n\nThe automation script is deterministic: PR discovery, label-event tracking,\nhead eligibility, state persistence, stale-result suppression, the repository\ncheckout, and its removal are all handled in Python. The LLM is invoked only for\nthe review itself.\n\nBefore any review conversation is created, a scheduled scan evaluates the\ncurrent head's GitHub-required checks, read through the GraphQL `isRequired`\nsignal for the pull request. That signal is the merge policy's own source of\ntruth and is pull-request-scoped, so it stays correct for a stacked PR whose\nsymbolic base branch carries no branch rules of its own:\n\n- Only checks GitHub reports as required decide the scheduled gate. An optional\n workflow that fails - including one that fails before creating any check run -\n does not block a head whose required checks pass.\n- A completed required run on the exact head whose conclusion is `failure`,\n `cancelled`, or `timed_out` blocks the review. Any other unrecognized\n conclusion fails closed as a block rather than silently approving.\n- A completed required run whose conclusion is `success`, `neutral`, or\n `skipped` does not block. A required `commit status` context is classified\n alongside required check runs.\n- A `queued` or `in_progress` required run on the exact head, and a required\n context that has not reported a current-head check run at all, make the run\n exit with a **waiting on checks** outcome. Unfulfilled is never approval. The\n worker does not hold a slot polling; the next scheduled scan or explicit\n review request retries.\n- When the required-check signal cannot be read or is empty, the gate falls back\n to every current-head check run and workflow run, so a red head still blocks.\n In that fallback, workflow runs are read as well as check runs, because a\n workflow can fail before creating any check run - a workflow-level error, or a\n `pull_request` run whose jobs never start. Such a run leaves a failed check\n suite with no check runs under it, so the commit's check-run rollup and\n `gh pr checks` both report success and only the workflow run reveals the red\n CI. A workflow run whose check suite already reported check runs is left to\n those runs, so a workflow is never counted twice.\n- A scheduled scan considers the trigger label **and** every open PR, including drafts,\n that still holds an outstanding `all-hands-bot` review request. That is what\n resumes a request made while CI was running: the request is keyed by its own\n `review_requested` event, so repeated scans reuse one conversation and one\n review instead of creating duplicates, and a PR whose request was answered or\n withdrawn simply drops out of `requested_reviewers`.\n- The scheduled scan also reviews open, non-draft PRs that **nobody requested**\n and that carry no trigger label, once their current head has no completed\n review by this account. That is what keeps reviewing PRs after the outstanding\n requests are exhausted. The delivery key is the stable\n `scan:{repository}:{number}:{head}`, so repeated scans reuse one conversation\n and one native review, and a changed head becomes eligible again under its new\n SHA.\n- Each scheduled scan classifies the **whole open backlog**, including\n unrequested PRs. Blocked, pending, draft, or already-reviewed heads therefore\n cannot hide an eligible head behind an inspection window. The separate\n `max_new_per_run` quota limits only newly created review conversations, not the\n number of PRs inspected; explicit requests and trigger labels retain priority\n when eligible candidates are drained.\n- A gate stop on an **unrequested** head posts no managed comment. A PR nobody\n asked about that is merely red or pending is simply skipped without starting an\n agent, which is what keeps a scan over a large backlog from posting a gate\n comment on every PR. The managed comment is the answer to an explicit request,\n so a requested or labeled head that is red or pending still gets its\n explanation.\n- Workflow runs are read as well as check runs, because a workflow can fail\n before creating any check run - a workflow-level error, or a `pull_request`\n run whose jobs never start. Such a run leaves a failed check suite with no\n check runs under it, so the commit's check-run rollup and `gh pr checks` both\n report success and only the workflow run reveals the red CI. A workflow run\n whose check suite already reported check runs is left to those runs, so a\n workflow is never counted twice.\n- Runs attributed to any other (obsolete) head SHA are ignored, so a stale\n failure cannot block the push that fixed it. This applies to workflow runs\n too.\n- Only the latest run of each logical check or workflow counts. A logical check\n is its name plus the reporting app identity, and a logical workflow is its\n name plus workflow ID. The latest is chosen by the run ID (the reliable\n creation sequence) with the start time as a tie-break, so a re-run that fixed\n a check supersedes its earlier failure on the same SHA while a newer queued or\n in-progress re-run supersedes an earlier success and makes the head wait.\n Ordering by the run ID keeps a run whose start time is still absent in its\n true creation position in both directions.\n\nAn explicit `all-hands-bot` review request is the intake-policy exception: the\ncaller asked for that head by name, so the request dispatches even when required\nCI is red or pending, and the gate leaves no explanatory comment. Draft, scope,\nexact-head, and delivery-deduplication safeguards still apply. A blocked or\nwaiting scheduled stop consumes no trigger: once the head's required checks are\nnon-blocking, the scheduled scan, or a new `all-hands-bot` review request,\nstarts the normal review.\n\nThe gate needs no configured list of check names. When it stops a review it\nleaves one concise explanation on the PR, identified by a hidden marker carrying\nthe head SHA and gate category, so a later run for a different head updates that\ncomment instead of posting another. Only a marker this reviewer account authored\ncounts as its own comment; every other PR comment is untrusted and can neither\nsuppress the explanation nor be edited. If the repository's own workflow already\nposted a deterministic remediation comment that names the same current-head\nchecks, the gate adds nothing; a disclosure about some other check does not\nsuppress it. The gate applies to scheduled label scans; an explicit\n`all-hands-bot` review request bypasses it by design.\n\nEach explicit review request - a re-applied trigger label, or a new reviewer\nrequest in event mode - is a fresh review. The conversation for a PR is reused,\nbut its earlier turns must not be trusted as current: before deciding a verdict\nthe reviewer re-fetches the mutable GitHub state (the exact head, the PR body,\nreview comments and threads, review requests, the linked issues' bodies and\nlabels, and the current-head Actions results) and ignores any earlier finding,\nverdict, or label/priority claim that state no longer supports. Repository\nanalysis the conversation already did, such as reading `AGENTS.md`, stays\nuseful and is not repeated.\n\nThe waiting and blocked explanations name the retry the deployment actually has.\nA scheduled run proves a scan is configured, so it says the next scan will\nretry. An event-only run does not, so it tells the reader to remove the\noutstanding `all-hands-bot` request and request `all-hands-bot` again instead of\npromising a scan that does not exist: GitHub will not accept a second request\nwhile the first is still outstanding.\n\nThe gate leaves only one explanation per head. When the same head keeps the same\nhidden marker but the retry wording changes -- an event-only comment on a head\nwhose automation is later switched to a cron scan -- the scheduled run rewrites\nthat managed comment in place, so the comment always names the retry that is\nactually deployed, and an unchanged body is left untouched.\n\n## Bounded intake per scheduled scan\n\nA scheduled scan drains outstanding reviewer requests fairly, but starting an\nagent for every eligible pull request at once would overload the deployment, so\nthe scan starts at most `MAX_NEW_PER_RUN` new review conversations, counted\n**across every configured repository** rather than per repository. The default\nis `2`, and the rendered `config.json` overrides it through the\n`max_new_per_run` key that `github-issue-to-pr` and `gitlab-issue-to-mr` already\nuse.\n\n- Eligible candidates are ordered deterministically: an explicit `all-hands-bot`\n request (or a trigger label) is ordered before an unrequested PR, then by the\n oldest `all-hands-bot` `review_requested` event for a requested PR or the PR's\n own creation time (oldest first) for an unrequested one, then by repository,\n then by pull-request number, so the oldest work drains first and a later scan\n reaches the remainder.\n- The bound counts the conversations a scan **starts**. A delivery that only\n deduplicates or reports an already-running conversation reuses a runtime and\n consumes no slot, so repeated scans make progress on the backlog instead of\n re-spending the bound on work already in flight.\n- Reaching the bound never cuts the scan short. The scan still evaluates the\n exact-head checks of the remaining candidates, reconciles completed reviews,\n and runs the maintainer handoff. A candidate whose checks are pending or\n failing starts no conversation and consumes no slot, so it cannot block a later\n green candidate. Only an explicit request or a trigger label gets a waiting or\n blocked gate comment; an unrequested head that is merely red or pending is\n skipped silently.\n- A dispatch that raises is reported and does not consume a slot or abort the\n scan, so the candidates behind it are still considered.\n- The explicit `review_requested` event path and the trigger-label scan are\n unchanged: an explicit request still starts its conversation immediately, and\n only the scheduled scan's new conversations are bounded.\n\nThe review prompt starts with a scope gate: using the repository's own guidance\n(its scope categories and ownership boundaries, not a list of individual PR\nnumbers), the reviewer decides whether the change belongs in this repository\nand has the product/architecture direction it needs. When it does not, the review\nstops with a single `event: COMMENT` review that says whether the change should\nmove repositories, close, or receive a maintainer decision, and ends with the\n`๐ MAINTAINER DECISION REQUIRED` verdict. That outcome is **neither an approval\nnor a change request**: it does not approve or merge the PR. The completion\nhandler recognizes the verdict and requests one configured maintainer through the\nsame handoff used after an approval. Both paths require a successfully fetched\nsame-repository closing issue labeled `priority:medium` or `priority:high`; a\nmissing link or priority label, low/normal priority, unresolved reference, or PR\nreference does not authorize handoff. One qualifying issue is sufficient even\nwith other low-priority or missing references. Existing manual review requests\nare unchanged. An in-scope change continues the existing review unchanged.\n\nBefore approving a change to user-visible UI behavior, the prompt requires live\nevidence from a real running application when the repository's guidance demands\nit: a screenshot, screen recording, or equivalent capture of the running app\nthat exercises the production-facing path. Unit tests, CSS-token or contract\nassertions, generated mockups, and reconstructed captures may support the review\nbut cannot substitute for that evidence. When the required evidence is missing,\nthe reviewer publishes one `event: COMMENT` review naming exactly what is\nmissing and ends with `๐ CHANGES REQUESTED`, so approval is withheld and the\ndeterministic maintainer handoff does not fire. Non-UI changes keep the existing\nrequirement of the real command and its observed output, with tests insufficient\nas sole proof.\n\nEvery review ends with the LLM profile and model of the conversation that wrote\nit, after the verdict: `LLM profile: <profile> ยท Model: <model>`. The agent does\nnot write this footer. The prompt gives it a hidden marker naming its own\nconversation, `<!-- openhands-review-run: <conversation-id> -->`, to copy under\nthe disclosure, and the deterministic script appends the footer to the reviews\nthat carry exactly that marker and no other. Login, head, and submission time\ncannot tell two automations that share a bot account apart, so a review without\nthe marker, with another run's marker, or with several markers is never edited.\nThe catalog worker reads the values back from the marked conversation: the\nmodel it runs, and the LLM profile its agent profile referenced at launch\n(`unknown` once that agent profile has been edited). It never substitutes the\nscanner's own settings, and a reused conversation keeps reporting the LLM it\nwas launched with. A review is stamped once, so a later agent profile edit\ncannot rewrite its footer, and a provenance failure is logged without holding\nup the verdict or the maintainer handoff. The completion handler reads the\nverdict above the footer.\n\nThe script prepares each review's workspace before the agent starts: the pull\nrequest's head commit is downloaded as a tarball and extracted to a directory of\nits own, which becomes the conversation's working directory. The agent is told\nnot to clone, fetch, check out, or delete anything, and the script removes the\ncheckout once the conversation has stopped. Nothing accumulates between runs.\n\n---\n\nThe script imports shared GitHub transport from\n`scripts/github_client.py`, installed with this skill. Include it beside\n`main.py` when packaging manually, as shown below; catalog bundles include it\nautomatically.\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 and Write**, Issues: Read and Write |\n\nPull-request **write** access is required because the agent publishes a pull\nrequest review, not just an issue comment. The Agent Canvas catalog worker may\nalso request a configured human reviewer after approval. A token with only Pull\nrequests: Read will poll happily and then fail at the point of publishing or\nrequesting the handoff.\n\nWhen several repositories are monitored, the token must cover all of them.\n\nOn OpenHands Cloud and Enterprise, the Agent Canvas catalog template needs no\nsaved secret when GitHub is connected under Settings -> Integrations: a secret\nsaved under the configured name is used if present, otherwise the connected\naccount's token, and the review is published by that account. The requested\nreviewer should be that account's login. The steps below apply to the local\nsetup through this skill.\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 repositories\n\nAsk: *\"Which GitHub repositories should be monitored?\n(Format: `owner/repo`, e.g. `myorg/backend`. List several separated by commas to\nreview them all from one automation.)\"*\n\nValidate access to **each** repository:\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 every accepted repository into `REPOS = [\"{owner}/{repo}\", ...]`. If one\nrepository fails the check, say which and ask whether to continue without it.\n\nEach repository is polled independently and keeps its own state, so pull-request\nnumbers never collide between them. The trigger label, tone, and schedule are\nshared by all of them; a repository needing different settings wants its own\nautomation.\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 seven constant\nsubstitutions near the top of the file:\n\n> The script also reads a `config.json` shipped beside it, if there is one, over\n> these constants. That is how the catalog entry\n> (`automations/catalog/github-pr-reviewer/`) configures an unmodified copy,\n> since a declarative host cannot rewrite Python. This setup path substitutes the\n> constants and ships no `config.json`, so the two never collide.\n\n| Placeholder | Replace with |\n|---|---|\n| `REPOS = [\"owner/repo\"]` | `REPOS = [\"{owner_repo}\", ...]` - one entry per repository collected in Step 2 |\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| `REPO_REVIEW_GUIDE_PATH = \".agents/skills/custom-codereview-guide.md\"` | leave unchanged to auto-load a repo review guide at this path, or set to `\"\"` to disable |\n| `MAX_NEW_PER_RUN = 2` | leave unchanged to bound a scheduled scan to two new review conversations across all repositories, or raise it if the deployment can hold more agents at once |\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`json.dumps(list_of_repos)` produces the whole `REPOS` list safely in one step.\n\nRun these commands from this skill's directory and write the customized script\nto a temporary build directory:\n```bash\nmkdir -p /tmp/pr-reviewer-build\ncp -L scripts/github_client.py /tmp/pr-reviewer-build/github_client.py\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 `<RUNTIME_SERVICES>`\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: {repo_summary} label {trigger_label}\\\",\n \\\"trigger\\\": {\\\"type\\\": \\\"cron\\\", \\\"schedule\\\": \\\"{cron_schedule}\\\"},\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 600\n }\" | python3 -m json.tool\n```\n\nUse the single repository as `{repo_summary}` when there is one, and something\nlike `3 repos` when there are several. A poll now downloads a tarball per queued\nreview, so the timeout allows for that; a run never waits for a review to\nfinish, only for it to be started.\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> - Repositories: `{owner}/{repo}`, ... (one line each)\n> - Trigger label: `{trigger_label}`\n> - Review tone: `{tone}`\n> - Polling schedule: `{cron_schedule}`\n> - State file per repository:\n> `~/.openhands/workspaces/automation-state/github_pr_reviewer_label_event_{id}_{owner}__{repo}.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> The review is published as a pull request review on the head commit, with\n> inline comments where a finding maps to a changed line.\n\n---\n\n## Runtime Behaviour (per poll)\n\nEach cron run executes `main.py`, which resolves and validates\n`GITHUB_PERSONAL_ACCESS_TOKEN` once, then processes every repository in `REPOS`\nindependently. One repository failing does not stop the others; the run fails\nonly if every repository fails.\n\nFor each repository:\n\n1. Loads that repository's state (see `references/state-schema.md`).\n2. Verifies 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 - Downloads the PR's head commit as a tarball and extracts it to\n `{WORKSPACE_BASE}/repositories/{owner}__{repo}/pr-{number}-{sha12}`. The\n archive is checked as it is unpacked: a single root, no absolute or `..`\n paths, and symlinks skipped rather than materialised.\n - Records `default` and the concrete model from the server settings already\n used to start the conversation, for the review footer.\n - Starts an OpenHands conversation **whose working directory is that\n checkout**, under an id chosen before it starts, with a review prompt\n carrying PR metadata, the exact head SHA, label event details, the\n requirement to re-fetch the current mutable GitHub state before deciding\n a verdict, and that conversation's review marker.\n - Posts an acknowledgement comment with the label event, head SHA, and\n conversation link.\n - Records the review in state with `status: \"active\"` and the checkout path.\n - If the checkout or the conversation cannot be created, the checkout is\n removed and nothing is recorded, so the next poll retries the label event.\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 verifies a submitted review by the token's own user at that head SHA and\n appends the recorded LLM profile and model to each review carrying this\n conversation's marker before marking the review complete. Reviews without\n that marker are confirmed but never edited. If no review exists, the\n agent's final response is posted as a comment with provenance. Failed\n verification or publication is retried on the next poll.\n - Abandons a conversation that has not reached a terminal status within two\n hours, so its checkout can be reclaimed.\n6. Removes the checkout of every finished review, but only after confirming the\n conversation has stopped - deleting it under a running agent would remove its\n working directory. When that cannot be confirmed the directory is left alone\n and the next poll tries again.\n7. Saves that repository's state atomically.\n\nThe completion callback fires once for the whole run.\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- **`tests/test_main.py`** - Unit tests for the checkout, its removal, and state\n handling. Run them from the skill root with `python -m pytest tests/` after\n editing the script.\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 the entry in `REPOS` and the token's permissions |\n| One repository is skipped, others work | That repository failed its access check | Read the `=== owner/repo ===` block in the run log |\n| Same PR not reviewed after new commits | Label event was already processed | Remove and re-apply the trigger label |\n| Review paused with a failing-check comment | A current-head required check reported `failure`, `cancelled`, or `timed_out` | Fix the named checks and push; the review starts on the new head, or request `all-hands-bot` to review immediately |\n| Review reported waiting on checks | A current-head required check is `queued` or `in_progress`, or has not reported yet | No action; a later scan or a new review request retries |\n| Optional workflow failed but no review was paused | The failed workflow is not required, so the required-only scheduled gate ignored it | No action; only GitHub-required checks gate scheduled discovery |\n| Only a few reviews start on a large backlog | The per-scan `max_new_per_run` bound (default 2) reached | No action; later scans drain the remaining oldest eligible PRs, or raise `max_new_per_run` if the deployment can hold more agents |\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 |\n| Review arrives as a plain comment, not a review | Publishing failed, so the script posted the text as a fallback | Check that the token has Pull requests: Read and Write |\n| Agent reports it cannot clone the repo | Prompt asked it not to; the workspace is already the checkout | No action - the code is at the head SHA in its working directory |\n| Checkouts remain under `repositories/` | Their conversations had not stopped yet | They are removed by a later poll once the conversation is terminal |",
|
|
287
287
|
"category": "automations"
|
|
288
288
|
},
|
|
289
289
|
{
|
|
@@ -596,7 +596,7 @@ export const SKILLS_CATALOG = [
|
|
|
596
596
|
"triggers": [
|
|
597
597
|
"/slack-monitor:poll"
|
|
598
598
|
],
|
|
599
|
-
"content": "# Slack Channel Monitor\n\nCreate a cron automation that polls up to 10 Slack channels every minute.\nWindows PowerShell equivalents for the setup, packaging, upload, and API-check shell snippets are in `references/windows.md`.\nWhen a message containing the **trigger phrase** (default: `@openhands`) is\ndetected it:\n\n1. Adds a ๐ reaction to the triggering message.\n2. Opens an OpenHands conversation with the message and recent channel context.\n3. Posts a reply in the Slack thread with a link to the conversation.\n\nOn every subsequent run:\n- New Slack thread replies are forwarded only when they contain the trigger\n phrase, so unrelated conversation in the thread is ignored.\n- When the conversation finishes (or errors), the agent's final response is\n posted back to the Slack thread.\n- Completed conversations stay in a short follow-up watch window, allowing\n triggered Slack replies to continue the same OpenHands conversation.\n\n> **Local mode only.** This automation targets the local OpenHands setup\n> (`dev:automation` stack). A cloud/webhook-based variant is out of scope here.\n\n---\n\n## Prerequisites\n\n### Required secrets\n\nVerify that at least one of the following secrets is set in\n**OpenHands Settings โ Secrets** before proceeding:\n\n| Secret name | Token type | Minimum scopes |\n|---|---|---|\n| `SLACK_BOT_TOKEN` | Bot (`xoxb-โฆ`) | `channels:history`, `channels:read`, `reactions:write`, `chat:write` |\n| `SLACK_USER_TOKEN` | User (`xoxp-โฆ`) | Same as bot, plus `search:read` for multi-channel efficiency |\n\nCheck with:\n```bash\n# For bot token:\ncurl -s https://slack.com/api/auth.test -H \"Authorization: Bearer $SLACK_BOT_TOKEN\" \\\n | python3 -c \"import json,sys; d=json.load(sys.stdin); print('ok' if d.get('ok') else d.get('error'))\"\n\n# For user token:\ncurl -s https://slack.com/api/auth.test -H \"Authorization: Bearer $SLACK_USER_TOKEN\" \\\n | python3 -c \"import json,sys; d=json.load(sys.stdin); print('ok' if d.get('ok') else d.get('error'))\"\n```\n\nIf neither token is present, inform the user and stop - the automation cannot\nfunction without Slack credentials.\n\n### Optional secret\n\n| Secret name | Default | Purpose |\n|---|---|---|\n| `OPENHANDS_URL` | `http://localhost:8000` | Base URL used to build conversation links posted in Slack |\n\n---\n\n## Setup Workflow\n\nFollow these steps in order.\n\n### Step 1 - Collect channels\n\nAsk the user: *\"Which Slack channels should be monitored? You can provide\nchannel names (e.g. `#general`) or IDs (e.g. `C0123456789`).\"*\n\n**If the user provides channel names**, resolve them to IDs:\n\n```bash\nSLACK_TOKEN=\"${SLACK_BOT_TOKEN:-$SLACK_USER_TOKEN}\"\ncurl -s \"https://slack.com/api/conversations.list?types=public_channel,private_channel&limit=200&exclude_archived=true\" \\\n -H \"Authorization: Bearer $SLACK_TOKEN\" \\\n | python3 -c \"\nimport json, sys\ndata = json.load(sys.stdin)\nif not data.get('ok'):\n print('ERROR:', data.get('error'))\n exit(1)\nnames = set(n.lstrip('#') for n in ['CHANNEL_NAMES_HERE'.split(',')])\nfor ch in data.get('channels', []):\n if ch['name'] in names:\n print(f\\\"{ch['name']} โ {ch['id']}\\\")\n\"\n```\n\nReplace `CHANNEL_NAMES_HERE` with the comma-separated names the user provided.\n\n**If `conversations.list` returns `missing_scope` or `not_authed`:**\nInform the user: *\"The token doesn't have permission to list channels. Please\nprovide the channel IDs directly (right-click a channel in Slack โ Copy link - \nthe last path segment starting with `C` is the ID).\"*\n\n**If the bot token lacks `channels:read`** for private channels, the user can\neither invite the bot first (`/invite @botname`) or switch to a user token.\n\nCollect up to 10 channel IDs. Record them as a Python list literal, e.g.:\n```python\n[\"C0123456789\", \"C9876543210\"]\n```\n\n### Step 2 - Collect trigger phrase\n\nAsk the user: *\"What trigger phrase should OpenHands respond to?\n(Press Enter to use the default: `@openhands`)\"*\n\nAccepted values: any non-empty string unlikely to appear accidentally, e.g.\n`@openhands`, `jazz hands`, `take-me-to-funky-town`.\n\n### Step 3 - Generate the automation script\n\nRead `scripts/main.py` from this skill's directory and **copy it verbatim**.\nApply exactly three constant substitutions near the top of the file:\n\n> **Do not reimplement, simplify, or hand-write a replacement script.**\n> The template already contains the correct secret-loading, state-path,\n> conversation-creation, and context-forwarding logic. Only the three\n> configuration constants below should change unless syntax validation fails.\n\n| Placeholder | Replace with |\n|---|---|\n| `TRIGGER_PHRASE = \"@openhands\"` | `TRIGGER_PHRASE = \"{user_phrase}\"` |\n| `CHANNEL_IDS: list[str] = []` | `CHANNEL_IDS: list[str] = {channel_id_list}` |\n| `DEFAULT_OPENHANDS_URL = \"http://localhost:8000\"` | `DEFAULT_OPENHANDS_URL = \"{url}\"` (keep default if user has no preference) |\n\nWrite the customised script to a temporary directory:\n```bash\nmkdir -p /tmp/slack-monitor-build\n# copy scripts/main.py to /tmp/slack-monitor-build/main.py\n# then replace only the three constants above\n```\n\nValidate syntax before packaging:\n```bash\npython3 -m py_compile /tmp/slack-monitor-build/main.py && echo \"Syntax OK\"\n```\n\nThen run a quick integrity check to confirm the template structure is still\npresent and only the configuration block was customised:\n```bash\ngrep -n 'TRIGGER_PHRASE = \"' /tmp/slack-monitor-build/main.py\ngrep -n 'CHANNEL_IDS: list\\[str\\] =' /tmp/slack-monitor-build/main.py\ngrep -n 'DEFAULT_OPENHANDS_URL = \"' /tmp/slack-monitor-build/main.py\ngrep -n 'def get_secret' /tmp/slack-monitor-build/main.py\ngrep -n 'def _state_file_path' /tmp/slack-monitor-build/main.py\ngrep -n 'def create_conversation' /tmp/slack-monitor-build/main.py\n```\n\nIf any of those checks fail, stop and re-copy the template instead of trying to\nrepair a hand-written variant.\n\n### Step 4 - Package and upload\n\nDetermine the Automation backend URL and auth from the `<RUNTIME_SERVICES>`\nblock in your system context:\n- Use the **Automation backend** `url_from_agent` as `OPENHANDS_HOST`\n- Auth: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`\n\nIf no Automation backend is listed in `<RUNTIME_SERVICES>`, stop and tell\nthe user to start the full automation stack.\n\n```bash\ntar -czf /tmp/slack-monitor.tar.gz -C /tmp/slack-monitor-build .\n\n# OPENHANDS_HOST: read from <RUNTIME_SERVICES> Automation backend url_from_agent\nOPENHANDS_HOST=\"<automation-url-from-runtime-services>\"\n\nTARBALL_PATH=$(curl -s -X POST \\\n \"${OPENHANDS_HOST}/api/automation/v1/uploads?name=slack-channel-monitor\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/gzip\" \\\n --data-binary @/tmp/slack-monitor.tar.gz \\\n | python3 -c \"import json,sys; print(json.load(sys.stdin)['tarball_path'])\")\n\necho \"Uploaded: $TARBALL_PATH\"\n```\n\nIf the upload fails with a size error, the tarball must be under 1 MB.\n`main.py` is under 15 KB so this should never trigger.\n\n### Step 5 - Create the automation\n\n```bash\ncurl -s -X POST \"${OPENHANDS_HOST}/api/automation/v1\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\n \\\"name\\\": \\\"Slack Channel Monitor\\\",\n \\\"trigger\\\": {\\\"type\\\": \\\"cron\\\", \\\"schedule\\\": \\\"* * * * *\\\"},\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 55\n }\" | python3 -m json.tool\n```\n\nA 55-second timeout keeps runs well within the 60-second cron window.\n\nRecord the returned `id` - share it with the user as confirmation.\n\n### Step 6 - Confirm\n\nTell the user:\n\n> โ
**Slack Channel Monitor** is running!\n>\n> - Automation ID: `{id}`\n> - Channels: `{channel list}`\n> - Trigger phrase: `{phrase}`\n> - Polling every minute via cron `* * * * *`\n> - State file: `~/.openhands/workspaces/automation-state/slack_poller_{id}.json`\n>\n> Send a message containing `{phrase}` in any monitored channel to test it.\n> The bot will react with ๐ and reply with a link to the new conversation.\n\n---\n\n## Runtime Behaviour (per poll)\n\nEach cron run executes `main.py`, which runs **10 polling iterations** (every\n5 seconds) within the 55-second timeout window. Each iteration:\n\n1. **Loads state** from the JSON file (see `references/state-schema.md`).\n2. **Resolves the Slack token** - checks `SLACK_USER_TOKEN` then `SLACK_BOT_TOKEN`.\n3. **Fetches new messages:**\n - User token + `search:read` + > 1 channel โ single `search.messages` call\n (searches for the trigger phrase across all channels).\n - Otherwise โ one `conversations.history` call per channel.\n4. **Fetches due thread replies** - polls at most one tracked thread per\n iteration using per-thread exponential backoff to stay within Slack rate\n limits.\n5. **Processes messages** in chronological order:\n - Skips messages already in `processed_ts` (dedup across the overlap window).\n - Skips bot messages and any `ts` in `bot_message_ts`.\n - Reply in a tracked thread whose text contains the trigger phrase โ forwards\n a follow-up request to the existing conversation and resets the follow-up\n watch window. Replies without the trigger phrase are marked processed and\n ignored.\n - Contains trigger phrase outside a tracked conversation โ ๐ reaction, create\n a new conversation, post link.\n - Records `default` and the concrete model from the server settings already\n used to start the conversation, for the response footer.\n - Thread replies: agent receives full thread history for context.\n - Root messages: agent receives the trigger text only.\n6. **Checks conversation statuses** - for each active conversation where\n `time.time() - last_activity > 15 s`:\n - If status is `idle`, `finished`, `error`, or `stuck` โ fetch the agent's\n final response via `/api/conversations/{id}/agent_final_response`, append\n the LLM profile and model, and post it to the Slack thread using Slack's\n `markdown_text` field so Markdown formatting renders correctly. Mark the\n record `watching` for five minutes\n so triggered follow-up replies can continue the same conversation.\n7. **Advances `last_poll`** to `now - 10 s` (overlap window prevents boundary\n races). If a conversation creation failed, pins `last_poll` further back to\n retry on the next iteration.\n8. **Saves state** (including `processed_ts`) and continues to the next iteration.\n9. After all iterations, fires the completion callback.\n\nDebug output is written to both stdout and a persistent log at:\n```\n{WORKSPACE_BASE_ROOT}/automation-state/slack_poller_debug.log\n```\n\n---\n\n## Additional Resources\n\n### Reference Files\n\n- **`references/slack-api.md`** - Slack token types, required scopes, API\n endpoint reference, rate limits, and common error codes.\n- **`references/state-schema.md`** - State JSON schema, field definitions,\n example file, and conversation lifecycle diagram.\n\n### Script Template\n\n- **`scripts/main.py`** - The complete automation script. Customise the three\n constants at the top (`TRIGGER_PHRASE`, `CHANNEL_IDS`, `DEFAULT_OPENHANDS_URL`)\n before packaging.\n\n---\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---|---|---|\n| Bot doesn't react to messages | Token missing or bot not in channel | Verify token with `auth.test`; `/invite @botname` |\n| `not_in_channel` error in run logs | Bot token used but bot not a member | Invite bot or switch to user token |\n| `missing_scope` error | Token lacks required scopes | Re-install Slack app with correct scopes (see `references/slack-api.md`) |\n| No messages detected | `last_poll` timestamp is in the future | Delete the state file to reset; it will be recreated on next run |\n| Conversation link 404 | `OPENHANDS_URL` points to wrong host | Set the `OPENHANDS_URL` secret to the correct base URL |\n| Summary never posted | Conversation stuck in `running` state | Check conversation in the OpenHands UI; the agent may need intervention |\n| Duplicate conversations created | `processed_ts` state missing or corrupted | Delete the state file to reset; dedup will rebuild on next run |\n| Trigger message processed on each cron run | State file deleted between runs | Ensure `automation-state/` directory is persistent across runs |\n| Debug info needed | Need detailed per-message trace | Check `{WORKSPACE_BASE_ROOT}/automation-state/slack_poller_debug.log` |",
|
|
599
|
+
"content": "# Slack Channel Monitor\n\nCreate a cron automation that polls up to 10 Slack channels every minute.\nWindows PowerShell equivalents for the setup, packaging, upload, and API-check shell snippets are in `references/windows.md`.\nWhen a message containing the **trigger phrase** (default: `@openhands`) is\ndetected it:\n\n1. Adds a ๐ reaction to the triggering message.\n2. Opens an OpenHands conversation with the message and recent channel context.\n3. Posts a reply in the Slack thread with a link to the conversation.\n\nOn every subsequent run:\n- New Slack thread replies are forwarded only when they contain the trigger\n phrase, so unrelated conversation in the thread is ignored.\n- When the conversation finishes (or errors), the agent's final response is\n posted back to the Slack thread.\n- Completed conversations stay in a short follow-up watch window, allowing\n triggered Slack replies to continue the same OpenHands conversation.\n\n> Runs on a local Agent Canvas (`dev:automation` stack) and on OpenHands Cloud\n> and Enterprise. On Cloud and Enterprise each conversation is started through\n> the OpenHands API in a sandbox of its own, with the user's own settings, and\n> the automation keeps its state in the automation service's KV store.\n\n---\n\n## Prerequisites\n\n### Required secrets\n\nVerify that at least one of the following secrets is set in\n**OpenHands Settings โ Secrets** before proceeding:\n\n| Secret name | Token type | Minimum scopes |\n|---|---|---|\n| `SLACK_BOT_TOKEN` | Bot (`xoxb-โฆ`) | `channels:history`, `channels:read`, `reactions:write`, `chat:write` |\n| `SLACK_USER_TOKEN` | User (`xoxp-โฆ`) | Same as bot, plus `search:read` for multi-channel efficiency |\n\nCheck with:\n```bash\n# For bot token:\ncurl -s https://slack.com/api/auth.test -H \"Authorization: Bearer $SLACK_BOT_TOKEN\" \\\n | python3 -c \"import json,sys; d=json.load(sys.stdin); print('ok' if d.get('ok') else d.get('error'))\"\n\n# For user token:\ncurl -s https://slack.com/api/auth.test -H \"Authorization: Bearer $SLACK_USER_TOKEN\" \\\n | python3 -c \"import json,sys; d=json.load(sys.stdin); print('ok' if d.get('ok') else d.get('error'))\"\n```\n\nIf neither token is present, inform the user and stop - the automation cannot\nfunction without Slack credentials.\n\n### Optional secret\n\n| Secret name | Default | Purpose |\n|---|---|---|\n| `OPENHANDS_URL` | `http://localhost:8000` | Base URL used to build conversation links posted in Slack (local Agent Canvas only; on OpenHands Cloud and Enterprise the links open the deployment's own Canvas) |\n\n---\n\n## Setup Workflow\n\nFollow these steps in order.\n\n### Step 1 - Collect channels\n\nAsk the user: *\"Which Slack channels should be monitored? You can provide\nchannel names (e.g. `#general`) or IDs (e.g. `C0123456789`).\"*\n\n**If the user provides channel names**, resolve them to IDs:\n\n```bash\nSLACK_TOKEN=\"${SLACK_BOT_TOKEN:-$SLACK_USER_TOKEN}\"\ncurl -s \"https://slack.com/api/conversations.list?types=public_channel,private_channel&limit=200&exclude_archived=true\" \\\n -H \"Authorization: Bearer $SLACK_TOKEN\" \\\n | python3 -c \"\nimport json, sys\ndata = json.load(sys.stdin)\nif not data.get('ok'):\n print('ERROR:', data.get('error'))\n exit(1)\nnames = set(n.lstrip('#') for n in ['CHANNEL_NAMES_HERE'.split(',')])\nfor ch in data.get('channels', []):\n if ch['name'] in names:\n print(f\\\"{ch['name']} โ {ch['id']}\\\")\n\"\n```\n\nReplace `CHANNEL_NAMES_HERE` with the comma-separated names the user provided.\n\n**If `conversations.list` returns `missing_scope` or `not_authed`:**\nInform the user: *\"The token doesn't have permission to list channels. Please\nprovide the channel IDs directly (right-click a channel in Slack โ Copy link - \nthe last path segment starting with `C` is the ID).\"*\n\n**If the bot token lacks `channels:read`** for private channels, the user can\neither invite the bot first (`/invite @botname`) or switch to a user token.\n\nCollect up to 10 channel IDs. Record them as a Python list literal, e.g.:\n```python\n[\"C0123456789\", \"C9876543210\"]\n```\n\n### Step 2 - Collect trigger phrase\n\nAsk the user: *\"What trigger phrase should OpenHands respond to?\n(Press Enter to use the default: `@openhands`)\"*\n\nAccepted values: any non-empty string unlikely to appear accidentally, e.g.\n`@openhands`, `jazz hands`, `take-me-to-funky-town`.\n\n### Step 3 - Generate the automation script\n\nRead `scripts/main.py` from this skill's directory and **copy it verbatim**.\nApply exactly three constant substitutions near the top of the file:\n\n> **Do not reimplement, simplify, or hand-write a replacement script.**\n> The template already contains the correct secret-loading, state-path,\n> conversation-creation, and context-forwarding logic. Only the three\n> configuration constants below should change unless syntax validation fails.\n\n| Placeholder | Replace with |\n|---|---|\n| `TRIGGER_PHRASE = \"@openhands\"` | `TRIGGER_PHRASE = \"{user_phrase}\"` |\n| `CHANNEL_IDS: list[str] = []` | `CHANNEL_IDS: list[str] = {channel_id_list}` |\n| `DEFAULT_OPENHANDS_URL = \"http://localhost:8000\"` | `DEFAULT_OPENHANDS_URL = \"{url}\"` (keep default if user has no preference) |\n\nWrite the customised script to a temporary directory:\n```bash\nmkdir -p /tmp/slack-monitor-build\n# copy scripts/main.py to /tmp/slack-monitor-build/main.py\n# then replace only the three constants above\n```\n\nValidate syntax before packaging:\n```bash\npython3 -m py_compile /tmp/slack-monitor-build/main.py && echo \"Syntax OK\"\n```\n\nThen run a quick integrity check to confirm the template structure is still\npresent and only the configuration block was customised:\n```bash\ngrep -n 'TRIGGER_PHRASE = \"' /tmp/slack-monitor-build/main.py\ngrep -n 'CHANNEL_IDS: list\\[str\\] =' /tmp/slack-monitor-build/main.py\ngrep -n 'DEFAULT_OPENHANDS_URL = \"' /tmp/slack-monitor-build/main.py\ngrep -n 'def get_secret' /tmp/slack-monitor-build/main.py\ngrep -n 'def _state_file_path' /tmp/slack-monitor-build/main.py\ngrep -n 'def create_conversation' /tmp/slack-monitor-build/main.py\n```\n\nIf any of those checks fail, stop and re-copy the template instead of trying to\nrepair a hand-written variant.\n\n### Step 4 - Package and upload\n\nSet `OPENHANDS_HOST` and `AUTH_HEADER` for the commands below:\n- **OpenHands Cloud or Enterprise** - a `<HOST>` value is in your system prompt:\n use it as `OPENHANDS_HOST`, with `AUTH_HEADER=\"Authorization: Bearer $OPENHANDS_API_KEY\"`\n- **Local Agent Canvas** - an **Automation backend** is listed in the\n `<RUNTIME_SERVICES>` block of your system context: use its `url_from_agent` as\n `OPENHANDS_HOST`, with `AUTH_HEADER=\"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\"`\n\nIf neither is available, stop and tell the user to start the full automation\nstack.\n\nOn OpenHands Cloud or Enterprise each run starts in a fresh sandbox, so the\nautomation keeps its state in the KV store: check that `kvStore` is listed under\n`features` in `GET ${OPENHANDS_HOST}/api/automation/v1/capabilities` before\ndeploying.\n\n```bash\ntar -czf /tmp/slack-monitor.tar.gz -C /tmp/slack-monitor-build .\n\nTARBALL_PATH=$(curl -s -X POST \\\n \"${OPENHANDS_HOST}/api/automation/v1/uploads?name=slack-channel-monitor\" \\\n -H \"$AUTH_HEADER\" \\\n -H \"Content-Type: application/gzip\" \\\n --data-binary @/tmp/slack-monitor.tar.gz \\\n | python3 -c \"import json,sys; print(json.load(sys.stdin)['tarball_path'])\")\n\necho \"Uploaded: $TARBALL_PATH\"\n```\n\nIf the upload fails with a size error, the tarball must be under 1 MB.\n`main.py` is under 15 KB so this should never trigger.\n\n### Step 5 - Create the automation\n\n```bash\ncurl -s -X POST \"${OPENHANDS_HOST}/api/automation/v1\" \\\n -H \"$AUTH_HEADER\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\n \\\"name\\\": \\\"Slack Channel Monitor\\\",\n \\\"trigger\\\": {\\\"type\\\": \\\"cron\\\", \\\"schedule\\\": \\\"* * * * *\\\"},\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 55\n }\" | python3 -m json.tool\n```\n\nA 55-second timeout keeps runs well within the 60-second cron window.\n\nRecord the returned `id` - share it with the user as confirmation.\n\n### Step 6 - Confirm\n\nTell the user:\n\n> โ
**Slack Channel Monitor** is running!\n>\n> - Automation ID: `{id}`\n> - Channels: `{channel list}`\n> - Trigger phrase: `{phrase}`\n> - Polling every minute via cron `* * * * *`\n> - State: the automation service's KV store (a local Agent Canvas without one\n> uses `~/.openhands/workspaces/automation-state/slack_poller_{id}.json`)\n>\n> Send a message containing `{phrase}` in any monitored channel to test it.\n> The bot will react with ๐ and reply with a link to the new conversation.\n\n---\n\n## Runtime Behaviour (per poll)\n\nEach cron run executes `main.py`, which runs **10 polling iterations** (every\n5 seconds) within the 55-second timeout window. Each iteration:\n\n1. **Loads state** from the KV store, or from the JSON file on a local Agent\n Canvas without one (see `references/state-schema.md`).\n2. **Resolves the Slack token** - checks `SLACK_USER_TOKEN` then `SLACK_BOT_TOKEN`.\n3. **Fetches new messages:**\n - User token + `search:read` + > 1 channel โ single `search.messages` call\n (searches for the trigger phrase across all channels).\n - Otherwise โ one `conversations.history` call per channel.\n4. **Fetches due thread replies** - polls at most one tracked thread per\n iteration using per-thread exponential backoff to stay within Slack rate\n limits.\n5. **Processes messages** in chronological order:\n - Skips messages already in `processed_ts` (dedup across the overlap window).\n - Skips bot messages and any `ts` in `bot_message_ts`.\n - Reply in a tracked thread whose text contains the trigger phrase โ forwards\n a follow-up request to the existing conversation and resets the follow-up\n watch window. Replies without the trigger phrase are marked processed and\n ignored.\n - Contains trigger phrase outside a tracked conversation โ ๐ reaction, create\n a new conversation, post link.\n - Records `default` and the concrete model from the server settings already\n used to start the conversation, for the response footer.\n - On OpenHands Cloud and Enterprise the conversation is started through the\n OpenHands API in a sandbox of its own, with the user's own settings; the\n link opens it in the deployment's Canvas, and the footer names the model\n the conversation reports once it is up.\n - Thread replies: agent receives full thread history for context.\n - Root messages: agent receives the trigger text only.\n6. **Checks conversation statuses** - for each active conversation where\n `time.time() - last_activity > 15 s`:\n - If status is `idle`, `finished`, `error`, or `stuck` โ fetch the agent's\n final response via `/api/conversations/{id}/agent_final_response`, append\n the LLM profile and model, and post it to the Slack thread using Slack's\n `markdown_text` field so Markdown formatting renders correctly. Mark the\n record `watching` for five minutes\n so triggered follow-up replies can continue the same conversation.\n7. **Advances `last_poll`** to `now - 10 s` (overlap window prevents boundary\n races). If a conversation creation failed, pins `last_poll` further back to\n retry on the next iteration.\n8. **Saves state** (including `processed_ts`) and continues to the next iteration.\n9. After all iterations, fires the completion callback.\n\nDebug output is written to both stdout and a persistent log at:\n```\n{WORKSPACE_BASE_ROOT}/automation-state/slack_poller_debug.log\n```\n\n---\n\n## Additional Resources\n\n### Reference Files\n\n- **`references/slack-api.md`** - Slack token types, required scopes, API\n endpoint reference, rate limits, and common error codes.\n- **`references/state-schema.md`** - State JSON schema, field definitions,\n example file, and conversation lifecycle diagram.\n\n### Script Template\n\n- **`scripts/main.py`** - The complete automation script. Customise the three\n constants at the top (`TRIGGER_PHRASE`, `CHANNEL_IDS`, `DEFAULT_OPENHANDS_URL`)\n before packaging.\n\n---\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---|---|---|\n| Bot doesn't react to messages | Token missing or bot not in channel | Verify token with `auth.test`; `/invite @botname` |\n| `not_in_channel` error in run logs | Bot token used but bot not a member | Invite bot or switch to user token |\n| `missing_scope` error | Token lacks required scopes | Re-install Slack app with correct scopes (see `references/slack-api.md`) |\n| No messages detected | `last_poll` timestamp is in the future | Delete the state file to reset; it will be recreated on next run |\n| Conversation link 404 | `OPENHANDS_URL` points to wrong host (local Agent Canvas) | Set the `OPENHANDS_URL` secret to the correct base URL |\n| Summary never posted | Conversation stuck in `running` state | Check conversation in the OpenHands UI; the agent may need intervention |\n| Duplicate conversations created | `processed_ts` state missing or corrupted | Delete the state file to reset; dedup will rebuild on next run |\n| Trigger message processed on each cron run | State file deleted between runs | Ensure `automation-state/` directory is persistent across runs |\n| Debug info needed | Need detailed per-message trace | Check `{WORKSPACE_BASE_ROOT}/automation-state/slack_poller_debug.log` |",
|
|
600
600
|
"category": "automations"
|
|
601
601
|
},
|
|
602
602
|
{
|
|
@@ -608,6 +608,16 @@ export const SKILLS_CATALOG = [
|
|
|
608
608
|
"content": "# Slack Standup Digest Automation\n\nSet up a recurring automation that summarizes Slack activity into an async\nstandup digest.\n\n---\n\n## Prerequisites\n\n### Required integration\n\n- **Slack MCP** must be installed in Settings โ MCP.\n\n### Information to collect\n\nAsk the user for:\n\n1. **Source channels** โ which Slack channels to scan for updates (e.g. `#engineering`, `#frontend`, `#backend`)\n2. **Target channel** โ where the digest should be posted (e.g. `#standup`, `#team-updates`)\n3. **Schedule** โ when should the digest run? Default: weekday mornings at 9 AM\n4. **Timezone** โ user's timezone (e.g. `America/New_York`, `Europe/London`)\n5. **Auto-post or draft** โ should the digest post automatically, or be saved for the user to review and approve first?\n6. **Grouping** โ how should updates be organized? Default: by project/channel, with sections for shipped work, active work, blockers, and decisions\n\n---\n\n## Setup Workflow\n\n### Step 1 โ Verify Slack MCP access\n\nConfirm the Slack MCP integration is working:\n```\nUse the Slack MCP to search for recent messages in one of the source channels.\n```\n\nIf it fails, tell the user to install the Slack MCP integration first.\n\n### Step 2 โ Configure the schedule\n\nBuild a cron schedule from the user's preferences:\n- Weekday mornings at 9 AM ET: `0 9 * * 1-5` with timezone `America/New_York`\n- Daily at 8 AM UTC: `0 8 * * *`\n\n### Step 3 โ Build the digest prompt\n\nConstruct a prompt that includes:\n- Source channels to scan\n- Target channel for posting\n- Lookback window (typically \"since previous workday\" โ FridayโMonday for Monday digests)\n- Grouping structure (by project, by channel, etc.)\n- Whether to auto-post or draft\n- What to highlight: blockers, decisions, shipped items, unanswered questions\n\n### Step 4 โ Create the automation\n\nSet `OPENHANDS_HOST` and `AUTH_HEADER` for the request below:\n- **OpenHands Cloud or Enterprise** - a `<HOST>` value is in your system prompt: use it as `OPENHANDS_HOST`, with `AUTH_HEADER=\"Authorization: Bearer $OPENHANDS_API_KEY\"`\n- **Local Agent Canvas** - an **Automation backend** is listed in `<RUNTIME_SERVICES>`: use its `url_from_agent` as `OPENHANDS_HOST`, with `AUTH_HEADER=\"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\"`\n\nUse the **prompt preset** endpoint:\n```bash\ncurl -s -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"$AUTH_HEADER\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Slack Standup Digest\",\n \"prompt\": \"<constructed digest prompt>\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"<schedule>\", \"timezone\": \"<tz>\"}\n }'\n```\n\nPowerShell note: use `curl.exe` for this exact flag syntax, and replace `${OPENHANDS_HOST}` / `$AUTH_HEADER` with `$env:OPENHANDS_HOST` / `$env:AUTH_HEADER` if running it natively.\n\n### Step 5 โ Confirm\n\nTell the user:\n> โ
**Slack Standup Digest** is running!\n>\n> - Automation ID: `{id}`\n> - Source channels: `{channel list}`\n> - Target channel: `{target}`\n> - Schedule: `{cron description}`\n> - Mode: `{auto-post or draft}`",
|
|
609
609
|
"category": "automations"
|
|
610
610
|
},
|
|
611
|
+
{
|
|
612
|
+
"name": "software-factory",
|
|
613
|
+
"description": "Set up an OpenHands \"software factory\" on GitHub fast: ask only for the repositories and where tickets live (GitHub issues, Jira or Linear), create a working issue-to-PR and PR-review pipeline from the automation catalog with defaults, show what runs, then let the user stop or extend it one step at a time (tune settings, triage, auto-merge, stale-CI and QA add-ons).",
|
|
614
|
+
"triggers": [
|
|
615
|
+
"/software-factory",
|
|
616
|
+
"software factory"
|
|
617
|
+
],
|
|
618
|
+
"content": "# Software Factory\n\nBuild a GitHub software factory from unmodified automation catalog bundles\n(`automations/catalog/<id>/manifest.json`); a Jira or Linear station is created\nby its own skill. This skill adds no automation logic.\n\nTwo phases: **Phase 1** gets a working factory after one question. **Phase 2**\nextends it in short rounds, only as far as the user wants. After Phase 1 and\nafter every round, the **Checkpoint** shows what exists and lets the user stop.\n\nCode lives on GitHub. Tickets can come from GitHub issues (default), Jira or\nLinear: only the issue to PR station changes (see \"Ticket source\"). GitLab and\nBitbucket code hosts are not part of this flow.\n\n## Stations\n\n| Station | Catalog id | Phase | Needs agent profile |\n|---|---|---|---|\n| Issue to PR | `github-issue-to-pr`, or the Jira or Linear station | 1 | yes (GitHub issues only) |\n| PR reviewer | `github-pr-reviewer` | 1 | yes |\n| AGENTS.md maintainer | `github-agents-md-maintainer` | 1 | no |\n| Issue triage | `github-issue-triage`, or `linear-triage-assistant` | add-on | yes (GitHub issues only) |\n| Auto-merge watchdog | `github-delivery-watchdog` | add-on | no |\n| Stale-CI PR closer | `github-stale-ci-pr-closer` | add-on | no |\n| QA changes | `qa-changes` | add-on | no |\n\n## Helper script\n\n`scripts/factory.py` in this skill's directory does every catalog and API step\nand prints one JSON document; it exits non-zero with `{\"error\": ...}` on\nfailure. Its usage is below: do not read its source, and do not load the\n`openhands-automation` skill for Phase 1.\n\nSet `OPENHANDS_HOST` to the **Ingress** URL in `<RUNTIME_SERVICES>` on a local\nAgent Canvas (it serves both `/api/automation` and `/api`; the automation\nbackend URL does not), or to `<HOST>` on OpenHands Cloud. Run every command as:\n\n```bash\nGITHUB_PERSONAL_ACCESS_TOKEN=\"$GITHUB_PERSONAL_ACCESS_TOKEN\" \\\nOPENHANDS_AUTOMATION_API_KEY=\"$OPENHANDS_AUTOMATION_API_KEY\" \\\nOPENHANDS_API_KEY=\"$OPENHANDS_API_KEY\" \\\npython3 <this skill's directory>/scripts/factory.py <command> ...\n```\n\n`$F` below stands for that whole line. Keep the secret names in it: the agent\nserver passes a saved secret into a command only when the command names it.\nThe script reads the token and never prints it.\n\n| Command | Does |\n|---|---|\n| `$F preflight --repos a/b,c/d` | Which stations this deployment supports, the agent profile, the GitHub login, push access per repository. |\n| `$F create --repos a/b --ids ID,ID [--set FIELD=VALUE] [--set ID.FIELD=VALUE]` | Fills each station's form (answers, then derived values, then manifest defaults), uploads the bundle and creates it disabled. `FIELD=VALUE` applies to every listed station with that field. Stations that already exist are reported under `existing`, not recreated. |\n| `$F tune --repos a/b --ids ID,ID [--set FIELD=VALUE] [--set ID.FIELD=VALUE]` | Applies new settings to an existing station by updating it in place. Use this to change a value that is fixed at creation. |\n| `$F verify ID... [--no-wait ID...]` | Dispatches once, enables the ones whose run `COMPLETED` and disables the rest, reporting their `error_detail`. `--no-wait` enables without waiting. |\n| `$F status ID...` | The latest run of each. |\n| `$F prs --repos a/b [--branch-prefix P]` | Open pull requests whose branch starts with `P` (default `openhands/agents-md`, the AGENTS.md maintainer's). |\n| `$F reset-token` | Deletes the saved `GITHUB_PERSONAL_ACCESS_TOKEN` (local Agent Canvas), so `request_secret` opens its form again. |\n\n`create` and `verify` return each automation's `url`, its page in Agent Canvas.\nTrust the script's output: do not open pages in the browser to check them, and\ndo not use the task tracker for this flow.\n\nAdd `--profile NAME` to `preflight` or `create` to use a profile other than the\nactive one.\n\n## Rules\n\n- **Supported stations**: create only those `preflight` reports `ok`; say why\n the others are skipped.\n- **Derived values** (the script fills them; never ask): every\n `githubTokenSecret` is `GITHUB_PERSONAL_ACCESS_TOKEN`. The watchdog's\n `branchPrefix` must equal issue to PR's: when it was changed, pass\n `--set github-delivery-watchdog.branchPrefix=<same value>`.\n- **Agent profile**: if `preflight` reports a profile error, skip the stations\n that need one and point the user to the station's catalog card in Agent\n Canvas (Automations), which has a profile picker.\n- **Token**: never ask for its value in chat, echo it, or put it in a tool\n argument or file.\n- **Failures**: if `create` fails, its output lists the stations it already\n created; report them with the error and ask whether to retry or delete them\n (`DELETE ${OPENHANDS_HOST}/api/automation/v1/{id}`).\n- **Existing stations**: a station `create` reports under `existing` was already\n set up by an earlier run and was left unchanged. Say so rather than presenting\n it as new, and change it with `$F tune` if the user wants different settings.\n\n## Phase 1 - Working factory\n\n1. Ask, in one message: \"Which GitHub repositories (`owner/repo`)? Where do\n tickets live: GitHub issues (default), Jira or Linear?\" Say that everything\n else uses defaults that can be changed afterwards. If the answer names only\n repositories, use GitHub issues; do not ask again.\n2. Run `$F preflight --repos <repos>`. Drop repositories without `push`, and\n say which.\n3. If `github_login` is null, act on `token_error`:\n - `not_set`, and the `request_secret` tool is available: call it once with\n `name: \"GITHUB_PERSONAL_ACCESS_TOKEN\"` and a `reason` asking for Contents,\n Issues, Pull requests and Workflows read and write (classic PAT: `repo`,\n `workflow`). End your turn, wait for the `[secret-request]` message,\n then rerun `preflight`.\n - `rejected`: GitHub refuses the saved token (invalid or expired), and\n `request_secret` cannot replace it: it answers `saved` at once for a name\n that exists. Say so in one sentence and ask \"Shall I remove it and open\n the form for a new one?\" On yes, run `$F reset-token`, then handle it as\n `not_set`. If `reset-token` fails, tell the user to replace it in\n Settings -> Secrets and say when done.\n - `not_set` without the tool: tell the user to add it in Settings ->\n Secrets and stop.\n4. `$F create --repos <repos> --ids github-pr-reviewer,github-agents-md-maintainer`,\n adding `github-issue-to-pr` for GitHub issues. For Jira or Linear, also set\n up the issue to PR station as \"Ticket source\" describes.\n5. `$F verify <reviewer id> [<issue to PR id>] --no-wait <maintainer id>`. The\n maintainer's run opens one `AGENTS.md` pull request per repository.\n6. Go to the Checkpoint.\n\n## Checkpoint\n\nWrite one short message, in this order:\n\n1. One plain sentence on what runs and how to use it: \"Your factory is running\n on `<repos>`. Add the `<trigger label>` label to a ticket to get a draft pull\n request; the reviewer reviews it and you merge it.\" (On Jira, add that the\n ticket body must name the `owner/repo`.)\n2. The `AGENTS.md` pull requests from `$F prs --repos <repos>`, asking the user\n to merge them: they give the build agent the repository's build, test and\n lint commands. One opened before this setup started (`created_at`) is\n \"still open from an earlier run\", not new; the maintainer works once a week\n per repository. If there are none yet, say they are on their way.\n3. A small table of the automations, each linked to its `url`:\n\n | Automation | Schedule | Status |\n |---|---|---|\n | [Issue to PR](<url>) | every 15 minutes | on |\n\n Status is `on`, or `off` with the reason in a few words. No automation ids.\n4. The settings in effect, as a short list (the Tune table's current values).\n Mention that the reviewer also reviews any open, non-draft, green pull\n request with no review yet (at most 2 new per scan).\n5. End with only a list of possible next steps, one line each, and nothing\n after it. Mark the first one not done yet as recommended:\n - **Try it**: I open a small test issue with the `<trigger label>` label in\n `<repo>`, only if you say so, and report the pull request.\n - **Tune**: change a setting from the list above.\n - **Add a station**: triage, auto-merge watchdog, stale-CI closer (QA only\n if `preflight` reports it `ok`).\n - **Stop here**: the factory keeps running as it is.\n\n## Phase 2 - Extend, one round at a time\n\nDo only what the user picked. Ask only the questions for it, in one message,\neach with its current value. Apply, then return to the Checkpoint.\n\n### Tune\n\n| Setting | Default | `--set` |\n|---|---|---|\n| Issue trigger label (GitHub issues) | `openhands` | `github-issue-to-pr.triggerLabel` |\n| Draft or ready pull requests | `draft` | `pullRequestMode` (issue to PR, AGENTS.md maintainer) |\n| Branch prefix (GitHub issues) | `openhands/issue` | `github-issue-to-pr.branchPrefix` (and the watchdog's, if present) |\n| Which pull requests get reviewed | label `openhands-review`, reviewer `all-hands-bot` | `github-pr-reviewer.triggerLabel`, `triggerReviewer` |\n| Review tone: concise, thorough, friendly | `concise` | `reviewTone` |\n| Human reviewers requested after agent approval (two or more logins) | none | `maintainers` |\n| Schedules and timezone | issue to PR `*/15 * * * *`, reviewer `*/5 * * * *`, AGENTS.md maintainer `0 9 * * 1`, add-ons per manifest; `UTC` | `ID.schedule`, `timezone` |\n| Agent profile | active profile | `--profile NAME` |\n\nApply:\n\n- Schedule or timezone only:\n `PATCH ${OPENHANDS_HOST}/api/automation/v1/{id}` with `{\"trigger\": {...}}`.\n- Jira or Linear station settings: change them as its skill describes.\n- Anything else is fixed at creation: `$F tune --repos <repos> --ids <id>` with\n the new `--set` values (repeat every earlier non-default value), then\n `$F verify <id>`. `tune` updates the station in place, so there is no\n replacement to delete and no template-id collision.\n\n### Add a station\n\nAsk only the question below, then `$F create` it and `$F verify` it.\n\n- **Auto-merge watchdog** (`github-delivery-watchdog`, GitHub issues only,\n since it merges only branches with the issue to PR prefix): confirm the user\n wants the agent to merge pull requests itself once review, tests and CI are\n green.\n- **Stale-CI closer** (`github-stale-ci-pr-closer`): confirm it may warn on,\n then close, pull requests whose CI keeps failing.\n- **Triage** (`github-issue-triage`): nothing. For Linear, follow the\n `linear-triage` skill instead; for Jira there is none.\n- **QA changes**: which base branch (default `main`). Offer QA only if\n `preflight` reports it `ok` and the `openhands-automation` webhook\n reachability check passes. It has no bundle, so the script cannot create it:\n create it as the `openhands-automation` skill describes for an\n event-triggered prompt preset, with its manifest `prompt` and `filter`, one\n per repository (it takes a single `repository`). It runs only on pull request\n events, so leave it enabled and skip `verify`.\n\n## Ticket source\n\n- **GitHub issues**: `github-issue-to-pr`, created with the others in Phase 1.\n- **Jira**: follow the `jira-issue-to-pr` skill with `git_provider` `github`\n and its defaults (label `create-pr`, `*/5 * * * *`). Ask only what it cannot\n look up (the Jira site if several, or the API-token details when Atlassian\n Rovo is not connected). Tell the user each ticket body must name its\n `owner/repo`.\n- **Linear**: follow the `ticket-to-code-change` skill with GitHub as the\n provider. Ask its questions in one message, proposing the repositories above\n and `*/15 * * * *` as defaults.\n\nOnce its skill has created the station, run `$F verify <id>` on it.\n\n## Troubleshooting\n\n| Symptom | Cause and fix |\n|---|---|\n| Create returns 422 | A value does not match the manifest `setup` block; fix the field at the path the error names. |\n| Issue to PR fails at push | The token lacks Contents or Workflows write, or does not cover that repository. |\n| Reviewer never reviews | The pull request has neither the trigger label nor a request for the trigger reviewer, and is not green. |\n| Watchdog never merges | Its branch prefix differs from issue to PR's, or the pull request lacks an independent approval or green CI. |",
|
|
619
|
+
"category": "automations"
|
|
620
|
+
},
|
|
611
621
|
{
|
|
612
622
|
"name": "spark-version-upgrade",
|
|
613
623
|
"description": "Upgrade Apache Spark applications between major versions (2.xโ3.x, 3.xโ4.x). Covers build files, deprecated APIs, configuration changes, SQL/DataFrame updates, and test validation.",
|
|
@@ -40,7 +40,11 @@ This skill is activated by keywords:
|
|
|
40
40
|
receive a clear error notice, and every result shows the LLM settings source
|
|
41
41
|
and concrete model
|
|
42
42
|
- **Persistent state**: conversation tracking and poll timestamps are stored
|
|
43
|
-
in
|
|
43
|
+
in the automation service's KV store across runs (a local Agent Canvas
|
|
44
|
+
without one uses `automation-state/slack_poller_{automation_id}.json`)
|
|
45
|
+
- **Runs anywhere**: on a local Agent Canvas, and on OpenHands Cloud and
|
|
46
|
+
Enterprise, where each conversation is started through the OpenHands API in
|
|
47
|
+
a sandbox of its own with the user's own settings
|
|
44
48
|
|
|
45
49
|
## Prerequisites
|
|
46
50
|
|
|
@@ -59,7 +63,7 @@ Optional:
|
|
|
59
63
|
|
|
60
64
|
| Secret | Default | Purpose |
|
|
61
65
|
|--------|---------|---------|
|
|
62
|
-
| `OPENHANDS_URL` | `http://localhost:8000` | Base URL for conversation links posted in Slack |
|
|
66
|
+
| `OPENHANDS_URL` | `http://localhost:8000` | Base URL for conversation links posted in Slack (local Agent Canvas only) |
|
|
63
67
|
|
|
64
68
|
## Quick Start
|
|
65
69
|
|
|
@@ -31,8 +31,10 @@ On every subsequent run:
|
|
|
31
31
|
- Completed conversations stay in a short follow-up watch window, allowing
|
|
32
32
|
triggered Slack replies to continue the same OpenHands conversation.
|
|
33
33
|
|
|
34
|
-
>
|
|
35
|
-
>
|
|
34
|
+
> Runs on a local Agent Canvas (`dev:automation` stack) and on OpenHands Cloud
|
|
35
|
+
> and Enterprise. On Cloud and Enterprise each conversation is started through
|
|
36
|
+
> the OpenHands API in a sandbox of its own, with the user's own settings, and
|
|
37
|
+
> the automation keeps its state in the automation service's KV store.
|
|
36
38
|
|
|
37
39
|
---
|
|
38
40
|
|
|
@@ -66,7 +68,7 @@ function without Slack credentials.
|
|
|
66
68
|
|
|
67
69
|
| Secret name | Default | Purpose |
|
|
68
70
|
|---|---|---|
|
|
69
|
-
| `OPENHANDS_URL` | `http://localhost:8000` | Base URL used to build conversation links posted in Slack |
|
|
71
|
+
| `OPENHANDS_URL` | `http://localhost:8000` | Base URL used to build conversation links posted in Slack (local Agent Canvas only; on OpenHands Cloud and Enterprise the links open the deployment's own Canvas) |
|
|
70
72
|
|
|
71
73
|
---
|
|
72
74
|
|
|
@@ -165,23 +167,27 @@ repair a hand-written variant.
|
|
|
165
167
|
|
|
166
168
|
### Step 4 - Package and upload
|
|
167
169
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
-
|
|
170
|
+
Set `OPENHANDS_HOST` and `AUTH_HEADER` for the commands below:
|
|
171
|
+
- **OpenHands Cloud or Enterprise** - a `<HOST>` value is in your system prompt:
|
|
172
|
+
use it as `OPENHANDS_HOST`, with `AUTH_HEADER="Authorization: Bearer $OPENHANDS_API_KEY"`
|
|
173
|
+
- **Local Agent Canvas** - an **Automation backend** is listed in the
|
|
174
|
+
`<RUNTIME_SERVICES>` block of your system context: use its `url_from_agent` as
|
|
175
|
+
`OPENHANDS_HOST`, with `AUTH_HEADER="X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY"`
|
|
172
176
|
|
|
173
|
-
If
|
|
174
|
-
|
|
177
|
+
If neither is available, stop and tell the user to start the full automation
|
|
178
|
+
stack.
|
|
179
|
+
|
|
180
|
+
On OpenHands Cloud or Enterprise each run starts in a fresh sandbox, so the
|
|
181
|
+
automation keeps its state in the KV store: check that `kvStore` is listed under
|
|
182
|
+
`features` in `GET ${OPENHANDS_HOST}/api/automation/v1/capabilities` before
|
|
183
|
+
deploying.
|
|
175
184
|
|
|
176
185
|
```bash
|
|
177
186
|
tar -czf /tmp/slack-monitor.tar.gz -C /tmp/slack-monitor-build .
|
|
178
187
|
|
|
179
|
-
# OPENHANDS_HOST: read from <RUNTIME_SERVICES> Automation backend url_from_agent
|
|
180
|
-
OPENHANDS_HOST="<automation-url-from-runtime-services>"
|
|
181
|
-
|
|
182
188
|
TARBALL_PATH=$(curl -s -X POST \
|
|
183
189
|
"${OPENHANDS_HOST}/api/automation/v1/uploads?name=slack-channel-monitor" \
|
|
184
|
-
-H "
|
|
190
|
+
-H "$AUTH_HEADER" \
|
|
185
191
|
-H "Content-Type: application/gzip" \
|
|
186
192
|
--data-binary @/tmp/slack-monitor.tar.gz \
|
|
187
193
|
| python3 -c "import json,sys; print(json.load(sys.stdin)['tarball_path'])")
|
|
@@ -196,7 +202,7 @@ If the upload fails with a size error, the tarball must be under 1 MB.
|
|
|
196
202
|
|
|
197
203
|
```bash
|
|
198
204
|
curl -s -X POST "${OPENHANDS_HOST}/api/automation/v1" \
|
|
199
|
-
-H "
|
|
205
|
+
-H "$AUTH_HEADER" \
|
|
200
206
|
-H "Content-Type: application/json" \
|
|
201
207
|
-d "{
|
|
202
208
|
\"name\": \"Slack Channel Monitor\",
|
|
@@ -221,7 +227,8 @@ Tell the user:
|
|
|
221
227
|
> - Channels: `{channel list}`
|
|
222
228
|
> - Trigger phrase: `{phrase}`
|
|
223
229
|
> - Polling every minute via cron `* * * * *`
|
|
224
|
-
> - State
|
|
230
|
+
> - State: the automation service's KV store (a local Agent Canvas without one
|
|
231
|
+
> uses `~/.openhands/workspaces/automation-state/slack_poller_{id}.json`)
|
|
225
232
|
>
|
|
226
233
|
> Send a message containing `{phrase}` in any monitored channel to test it.
|
|
227
234
|
> The bot will react with ๐ and reply with a link to the new conversation.
|
|
@@ -233,7 +240,8 @@ Tell the user:
|
|
|
233
240
|
Each cron run executes `main.py`, which runs **10 polling iterations** (every
|
|
234
241
|
5 seconds) within the 55-second timeout window. Each iteration:
|
|
235
242
|
|
|
236
|
-
1. **Loads state** from the JSON file
|
|
243
|
+
1. **Loads state** from the KV store, or from the JSON file on a local Agent
|
|
244
|
+
Canvas without one (see `references/state-schema.md`).
|
|
237
245
|
2. **Resolves the Slack token** - checks `SLACK_USER_TOKEN` then `SLACK_BOT_TOKEN`.
|
|
238
246
|
3. **Fetches new messages:**
|
|
239
247
|
- User token + `search:read` + > 1 channel โ single `search.messages` call
|
|
@@ -253,6 +261,10 @@ Each cron run executes `main.py`, which runs **10 polling iterations** (every
|
|
|
253
261
|
a new conversation, post link.
|
|
254
262
|
- Records `default` and the concrete model from the server settings already
|
|
255
263
|
used to start the conversation, for the response footer.
|
|
264
|
+
- On OpenHands Cloud and Enterprise the conversation is started through the
|
|
265
|
+
OpenHands API in a sandbox of its own, with the user's own settings; the
|
|
266
|
+
link opens it in the deployment's Canvas, and the footer names the model
|
|
267
|
+
the conversation reports once it is up.
|
|
256
268
|
- Thread replies: agent receives full thread history for context.
|
|
257
269
|
- Root messages: agent receives the trigger text only.
|
|
258
270
|
6. **Checks conversation statuses** - for each active conversation where
|
|
@@ -301,7 +313,7 @@ Debug output is written to both stdout and a persistent log at:
|
|
|
301
313
|
| `not_in_channel` error in run logs | Bot token used but bot not a member | Invite bot or switch to user token |
|
|
302
314
|
| `missing_scope` error | Token lacks required scopes | Re-install Slack app with correct scopes (see `references/slack-api.md`) |
|
|
303
315
|
| No messages detected | `last_poll` timestamp is in the future | Delete the state file to reset; it will be recreated on next run |
|
|
304
|
-
| Conversation link 404 | `OPENHANDS_URL` points to wrong host | Set the `OPENHANDS_URL` secret to the correct base URL |
|
|
316
|
+
| Conversation link 404 | `OPENHANDS_URL` points to wrong host (local Agent Canvas) | Set the `OPENHANDS_URL` secret to the correct base URL |
|
|
305
317
|
| Summary never posted | Conversation stuck in `running` state | Check conversation in the OpenHands UI; the agent may need intervention |
|
|
306
318
|
| Duplicate conversations created | `processed_ts` state missing or corrupted | Delete the state file to reset; dedup will rebuild on next run |
|
|
307
319
|
| Trigger message processed on each cron run | State file deleted between runs | Ensure `automation-state/` directory is persistent across runs |
|
|
@@ -31,13 +31,17 @@ Select-String -Path $main -Pattern 'def create_conversation'
|
|
|
31
31
|
|
|
32
32
|
## Package and Upload
|
|
33
33
|
|
|
34
|
+
Set `$env:OPENHANDS_HOST` and `$env:AUTH_HEADER` as SKILL.md Step 4 describes:
|
|
35
|
+
`"Authorization: Bearer $env:OPENHANDS_API_KEY"` on OpenHands Cloud or Enterprise,
|
|
36
|
+
`"X-Session-API-Key: $env:OPENHANDS_AUTOMATION_API_KEY"` on a local Agent Canvas.
|
|
37
|
+
|
|
34
38
|
```powershell
|
|
35
39
|
$tarball = Join-Path $env:TEMP "slack-monitor.tar.gz"
|
|
36
40
|
tar.exe -czf $tarball -C $buildDir .
|
|
37
41
|
|
|
38
42
|
$upload = curl.exe -s -X POST `
|
|
39
43
|
"$env:OPENHANDS_HOST/api/automation/v1/uploads?name=slack-channel-monitor" `
|
|
40
|
-
-H
|
|
44
|
+
-H $env:AUTH_HEADER `
|
|
41
45
|
-H "Content-Type: application/gzip" `
|
|
42
46
|
--data-binary "@$tarball" | ConvertFrom-Json
|
|
43
47
|
|
|
@@ -58,7 +62,7 @@ $body = @"
|
|
|
58
62
|
"@
|
|
59
63
|
|
|
60
64
|
curl.exe -s -X POST "$env:OPENHANDS_HOST/api/automation/v1" `
|
|
61
|
-
-H
|
|
65
|
+
-H $env:AUTH_HEADER `
|
|
62
66
|
-H "Content-Type: application/json" `
|
|
63
67
|
-d $body
|
|
64
68
|
```
|