Skip to content

feat: add github-repo-monitor skill#259

Merged
tofarr merged 11 commits into
mainfrom
add-github-repo-monitor
May 22, 2026
Merged

feat: add github-repo-monitor skill#259
tofarr merged 11 commits into
mainfrom
add-github-repo-monitor

Conversation

@tofarr
Copy link
Copy Markdown
Contributor

@tofarr tofarr commented May 22, 2026

Summary

Adds a new github-repo-monitor skill that creates a cron automation to monitor a GitHub repository for issue and PR comments.

This skill mirrors the structure and conventions of the existing slack-channel-monitor skill, adapted for the GitHub API.

What the skill does

When invoked, the skill guides the agent through a 9-step setup workflow:

  1. Verifies GITHUB_TOKEN is set in OpenHands Secrets
  2. Checks the token has comment-posting permissions on the target repo
  3. Collects the repository (owner/repo format)
  4. Collects the trigger phrase (default: @OpenHands)
  5. Collects event types to monitor (issue_comment, pr_review_comment, or both)
  6. Collects the cron schedule (default: every minute)
  7. Customises and validates main.py
  8. Packages, uploads, and creates the automation
  9. Confirms with the automation ID and state file location

Runtime behaviour (per poll)

Each cron run executes GET /repos/OpenHands/extensions/issues/comments?since={last_poll} (and/or PR review comments). For each comment containing the trigger phrase:

  • Fetches full issue/PR context (title, body, labels, last 10 comments)
  • Creates a new OpenHands conversation (or re-opens a closed one; falls back to new if deleted)
  • Posts a GitHub acknowledgement c- Posts a GitHubconversation URL and an AI disclosure
  • Forwards subsequent trigger comments on the same issue/PR to the existing conversation
  • When the conversation goes idle, posts a summary comment back to the issue/PR

Files added

skills/github-repo-monitor/
├── .claude-plugin              ← Claude plugin pointer
├── .codex-plugin               ← Codex plugin pointer
├── .plugin/plugin.json         ← Plugin metadata
├── README.md                   ← Human-readable overview
├── SKILL.md                    ← Agent setup workflow instructions
├── references/
│   ├── github-api.md           ← GitHub API endpoints, scopes, rate limits
│   └── state-schema.md         ← JSON state file schema and lifecycle diagram
└── scripts/
    └── main.py                 ← Automation script template

Screenshots

Create the automation using a conversation:
image

image

In my github repository I tag the agent:
image

And I get a conversation:
image

And a result posted back to github:
image

Design notes

  • Single repo per automation — one automation instance monitors one owner/repo
  • Cron-based polling — consistent with slack-channel-monitor; local- Cron-based polling — consistent with slack-channel-monitor; local- **Croversation still exists it is re-opened; if deleted a fresh one is created
  • **Differentiated acknowled- **Dts- **Differentiated acknowlay "OpenHands is on it!"; resumed conversations say "Resuming work"
  • Bot loop prevention — skips comments from accounts with user.type == "Bot" or logins ending in [bot]
  • AI disclosure — every comment posted by the automation includes an AI agent disclosure note
  • Draft PRs and closed issues — included by default (no artificial exclusions)

This PR was created by an AI agent (OpenHands) on behalf of the repository maintainer.

Adds a new skill that creates a cron automation to monitor a GitHub
repository for comments on issues and PRs.

When a comment containing a configurable trigger phrase (@OpenHands by
default) is detected the skill:
- Verifies GITHUB_TOKEN is set and has comment-posting permissions
- Fetches the full issue/PR context (title, body, labels, last 10 comments)
- Creates an OpenHands conversation pre-loaded with that context
- Posts a GitHub acknowledgement comment with a conversation link
- Forwards follow-up trigger comments to the running conversation
- Re-opens closed conversations on new triggers (creates fresh if deleted)
- Posts a summary GitHub comment when the conversation finishes

Mirrors the structure and patterns of slack-channel-monitor, adapted for
the GitHub API. Includes SKILL.md setup workflow, main.py template,
state-schema.md, github-api.md reference, and plugin metadata files.

Co-authored-by: openhands <openhands@all-hands.dev>
@tofarr tofarr marked this pull request as draft May 22, 2026 19:20
Copy link
Copy Markdown
Contributor

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL ISSUES]

  1. Marketplace Registration Missing: The skill is not registered in any marketplace JSON file. Per repo conventions, every skill under skills/ must be listed in a marketplace or CI will fail with Skills missing from marketplace: [github-repo-monitor]. Add an entry to marketplaces/openhands-extensions.json following the format of slack-channel-monitor.

  2. Hardcoded Trigger Phrase: Line 552 hardcodes "@OpenHands mention" in the acknowledgement message, but TRIGGER_PHRASE is configurable. Users who set a custom trigger will see incorrect acknowledgements.

  3. PR Description Typos: Multiple duplicated/malformed sentences: "Each cron run of the resulting autoEach cron run", "Forwards subsequent trigger comments on the same issue/PR to th - Forwards subsequent", "skips commenent posted by" (typo: commenent → comment).

[IMPROVEMENT OPPORTUNITIES]

See inline comments for suggestions on path handling and acknowledgement message clarity.

[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🟡 MEDIUM

This adds a GitHub polling automation with API access and conversation management. The code quality is good with proper error handling and bot detection. Main risks: API rate limiting (mitigated by 1-min poll interval), state corruption (mitigated by atomic JSON writes), and feedback loops (mitigated by bot detection). The security model is sound - tokens from secrets, proper permission checks.

VERDICT:
Needs rework: Add marketplace registration and fix hardcoded trigger phrase before merging.

KEY INSIGHT:
Clean state management and sensible conversation lifecycle (create → forward → close → re-open), but missing marketplace entry will block CI.


Improve this review? If any feedback above seems incorrect or irrelevant to this repository, you can teach the reviewer to do better:

  1. Add a .agents/skills/custom-codereview-guide.md file to your branch (or edit it if one already exists) with the /codereview trigger and the context the reviewer is missing (e.g., "Security concerns about X do not apply here because Y"). See the customization docs for the required frontmatter format.
  2. Re-request a review - the reviewer reads guidelines from the PR branch, so your changes take effect immediately.
  3. When your PR is merged, the guideline file goes through normal code review by repository maintainers.

Resolve with AI? Install the iterate skill in your agent and run /iterate to automatically drive this PR through CI, review, and QA until it's merge-ready.

Was this review helpful? React with 👍 or 👎 to give feedback.


Was this automated review useful? React with 👍 or 👎 to this review to help us measure review quality.
Workflow run: https://github.com/OpenHands/extensions/actions/runs/26307372305

Comment thread skills/github-repo-monitor/scripts/main.py Outdated
Comment thread skills/github-repo-monitor/scripts/main.py Outdated
Comment thread skills/github-repo-monitor/scripts/main.py Outdated
openhands-agent and others added 2 commits May 22, 2026 19:23
…f hardcoded string

Co-authored-by: openhands <openhands@all-hands.dev>
Three issues flagged by the test suite:

1. Add YAML frontmatter to SKILL.md
   Skill.load() requires a 'description' field in the frontmatter block
   at the top of SKILL.md. Added 'name' and 'description' matching the
   pattern of all other skills in the repo.

2. Convert .claude-plugin and .codex-plugin to symlinks
   The test suite enforces that these must be symlinks pointing to
   .plugin, not regular files. Replaced both plain files with proper
   symlinks (ln -s .plugin .claude-plugin / .codex-plugin).

3. Add skill to openhands-extensions.json marketplace
   Every skill directory must be listed in at least one marketplace
   JSON file. Added the github-repo-monitor entry after github-pr-review.

Co-authored-by: openhands <openhands@all-hands.dev>
@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented May 22, 2026

@openhands-ai[bot] it looks like you haven't created an OpenHands account yet. Please sign up at OpenHands Cloud and try again.

Auto-generated by scripts/sync_extensions.py catalog

Co-authored-by: openhands <openhands@all-hands.dev>
@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented May 22, 2026

@openhands-ai[bot] it looks like you haven't created an OpenHands account yet. Please sign up at OpenHands Cloud and try again.

tofarr and others added 2 commits May 22, 2026 13:52
Both github-repo-monitor and slack-channel-monitor were calling
_get_agent_dict() which read the user's LLM config from /api/settings
but discarded the configured agent type, hardcoding kind='Agent' instead.

'Agent' is the abstract base class — it only exposes think and finish.
'CodeActAgent' is the concrete implementation with bash, file_editor,
browser, and the full action tool suite. Any automation that needs to
actually *do* things (run tests, call APIs, edit files) was silently
getting a neutered agent.

Fix: read agent_settings.agent from /api/settings and use it as the
kind, defaulting to 'CodeActAgent' if not set.

Co-authored-by: openhands <openhands@all-hands.dev>
Two suggestions from all-hands-bot code review:

1. pathlib for state file path (PRRT_kwDOQcEru86ENp2h)
   Replace os.path.dirname/abspath/join/makedirs chain with
   pathlib.Path operations. Clearer intent and more Pythonic.

2. Differentiated acknowledgement messages (PRRT_kwDOQcEru86ENp2l)
   Track whether a conversation was freshly created or resumed from
   a closed state. New conversations get 'OpenHands is on it!';
   resumed conversations get 'Resuming work on this {item_type}.'
   so users are not confused seeing a 'started' message for work
   that was already in progress.

Co-authored-by: openhands <openhands@all-hands.dev>
@tofarr tofarr requested a review from all-hands-bot May 22, 2026 20:01
Copy link
Copy Markdown
Contributor

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Acceptable - Core implementation is solid but has testing gaps.

The github-repo-monitor skill follows good patterns from slack-channel-monitor, uses proper state management, and correctly handles the trigger phrase configuration. Previous review feedback (pathlib usage, differentiated acknowledgements, marketplace registration) has been addressed.

Key concerns: lacks automated tests and end-to-end verification evidence (see inline comments).


Was this automated review useful? React with 👍 or 👎 to this review to help us measure review quality.
Workflow run: https://github.com/OpenHands/extensions/actions/runs/26309248262

Comment thread skills/github-repo-monitor/scripts/main.py Outdated
Comment thread skills/github-repo-monitor/scripts/main.py
Three improvements from all-hands-bot review (PRRT_kwDOQcEru86EOJad,
PRRT_kwDOQcEru86EOJaj):

1. Refactor _process_trigger_comment (PRRT_kwDOQcEru86EOJad)
   Extract two focused helpers:
   - _ensure_conversation() handles all state transitions: re-opens a
     closed conversation if reachable, falls back to creating a new one,
     returns (conv_id, resumed) so the caller knows which path was taken.
   - _post_acknowledgement() owns the GitHub comment wording.
   Also extracted _is_bot_comment() and _has_trigger() from the inline
   poll-loop logic so they are independently testable.
   Guarded the entry point with if __name__ == '__main__' so the module
   can be imported by tests without executing main().

2. Fix load_state for corrupted JSON
   Wraps json.load() in try/except (json.JSONDecodeError, OSError) and
   falls back to the default state rather than crashing the run.

3. Add 29-test unit suite (PRRT_kwDOQcEru86EOJaj)
   tests/test_main.py covers:
   - load_state: missing file, valid JSON, corrupted JSON, empty file
   - save_state/load_state round-trip
   - _is_bot_comment: [bot] suffix, Bot type, human, null/missing user
   - _has_trigger: exact match, case-insensitive, absent, empty/None body
   - processed-comment-id persistence across save/load
   - _ensure_conversation state transitions: new, reopen, fallback-to-new,
     unknown status, create failure
   - _post_acknowledgement: new vs resumed wording, trigger phrase footer

Co-authored-by: openhands <openhands@all-hands.dev>
@tofarr tofarr requested a review from all-hands-bot May 22, 2026 20:18
Copy link
Copy Markdown
Contributor

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Acceptable - Solid implementation following established patterns.

The github-repo-monitor skill is well-structured, mirrors slack-channel-monitor conventions, and includes comprehensive test coverage (29 tests, all passing). Previous review feedback has been fully addressed.

One minor optimization opportunity noted inline, but it's a pragmatic trade-off that won't impact typical use cases.

[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🟢 LOW

This is a new skill addition that doesn't modify existing functionality. It has comprehensive unit tests, follows established automation patterns, proper error handling, and secure secret management. The implementation is straightforward and the blast radius is limited to users who explicitly enable this automation.

VERDICT:
Worth merging: Core logic is sound, well-tested, and ready for production use.

KEY INSIGHT:
This PR demonstrates good engineering judgment by reusing proven patterns from slack-channel-monitor while adapting them appropriately for GitHub's API model.


Was this automated review useful? React with 👍 or 👎 to this review to help us measure review quality.
Workflow run: https://github.com/OpenHands/extensions/actions/runs/26309956229

f"/repos/{repo}/issues/{issue_number}/comments",
{"per_page": 100},
)
recent_comments = all_comments[-CONTEXT_COMMENT_LIMIT:]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: This fetches ALL comments via pagination then takes the last 10. For issues with 1000+ comments, this consumes unnecessary API quota and bandwidth.

However, this is a pragmatic trade-off:

  • Simpler logic
  • Most issues have < 100 comments
  • Only happens once per trigger (not on every poll)
  • GitHub API doesn't have native "last N comments" support

Consider adding a comment documenting this behavior and the trade-off. No action required unless targeting repos with extremely high-volume issues.

@tofarr tofarr marked this pull request as ready for review May 22, 2026 20:22
Copy link
Copy Markdown
Contributor

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Good taste - Clean implementation following proven patterns.

The github-repo-monitor skill is well-structured and mirrors the slack-channel-monitor conventions effectively. Previous review feedback has been fully addressed: marketplace registration ✓, pathlib usage ✓, differentiated acknowledgements ✓, comprehensive test coverage (29 tests) ✓, bot detection ✓, trigger phrase handling ✓.

The unresolved pagination comment (line 322) represents a pragmatic trade-off that won't impact typical use cases.

[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🟢 LOW

This is a new skill addition with no impact on existing functionality. The implementation follows established patterns, includes comprehensive unit test coverage, and has been through multiple review cycles. The only runtime risk is to users who opt-in by creating this automation.

VERDICT:
Worth merging: Solid implementation with good test coverage.

KEY INSIGHT:
The skill demonstrates good architectural taste by extracting focused helper functions (_ensure_conversation, _post_acknowledgement, _is_bot_comment, _has_trigger) that keep complexity low and make the code maintainable.


Was this automated review useful? React with 👍 or 👎 to this review to help us measure review quality.
Workflow run: https://github.com/OpenHands/extensions/actions/runs/26310116466

tofarr and others added 4 commits May 22, 2026 14:38
…gent

dict.get(key, default) only uses the default when the key is ABSENT.
/api/settings returns agent_settings.agent=null for most users, so
.get('agent', 'CodeActAgent') returns None — producing {kind: null}
in the conversation creation payload and a 500 from the server.

Fixed in both skills with: .get('agent') or 'CodeActAgent'

Added regression tests: TestGetAgentDict (3 cases).

Co-authored-by: openhands <openhands@all-hands.dev>
…think/finish limitation

Two related bugs fixed in both github-repo-monitor and slack-channel-monitor:

1. Wrong default agent kind
   'CodeActAgent' is a full-app class not registered in the SDK agent
   registry. The only valid SDK kinds are 'Agent' and 'ACPAgent'.
   Change fallback: 'CodeActAgent' -> 'Agent'.

2. No tools in conversation agent dict
   The SDK's Agent has ONLY FinishTool and ThinkTool by default (see
   openhands/sdk/tool/builtins/__init__.py: BUILT_IN_TOOLS = [FinishTool,
   ThinkTool]). Bash and file editing come from openhands-tools and must
   be explicitly listed in the 'tools' field. Without this, conversations
   report 'I only have think and finish tools'.
   Add: tools=[{name:TerminalTool},{name:FileEditorTool}] to agent dict.

Updated tests: renamed fallback tests to expect 'Agent' (not
'CodeActAgent'), added test_tools_always_included to guard against
regressions on the tools omission.

Co-authored-by: openhands <openhands@all-hands.dev>
… 'file_editor')

The POST /api/conversations OpenAPI spec has two conflicting naming
conventions for tools:
- The Tool schema examples use class names: 'TerminalTool', 'FileEditorTool'
- The request worked example uses registered names: 'terminal', 'file_editor'

We used the class names and got the same '500 / Object of type ValueError
is not JSON serializable' crash, which is the server's error-handler bug
(it tries json.dumps(caught_ValueError_object) instead of str(e)) making
the real cause invisible.

Switch to the worked-example names ('terminal', 'file_editor') which
match what the running server actually accepts.

Co-authored-by: openhands <openhands@all-hands.dev>
Root cause: /api/settings returns the full-app agent registry name
(e.g. 'CodeActAgent', 'BrowsingAgent') in agent_settings.agent.
The automation SDK is an entirely separate runtime that only accepts
['ACPAgent', 'Agent'] as valid kinds.

The previous 'or "Agent"' fallback only guarded against null/missing
values — a truthy 'CodeActAgent' sailed straight through, producing:
  'Unknown kind CodeActAgent for AgentBase; Expected one of: [...]'

Fix: remove the settings agent name lookup entirely. Hardcode 'Agent'.
Update test_explicit_agent_name_is_used → test_full_app_agent_name_not_forwarded
to assert that all app-registry names (CodeActAgent, BrowsingAgent, …)
are rejected rather than forwarded.

Co-authored-by: openhands <openhands@all-hands.dev>
@tofarr tofarr merged commit 503e5a8 into main May 22, 2026
4 checks passed
@enyst
Copy link
Copy Markdown
Member

enyst commented May 23, 2026

@OpenHands read this PR; clone yourself agent-sdk and automation repos from the same org to investigate the code as you may need.

Does this skill or anything in the code enforce that the automation set up by this skill ONLY responds to particular logins? e.g. if “enyst” sets it up on their local machine using this skill, is the agent told to only do it for the github_token’s login, or is the code enforcing any of that somehow?

In other words, if “tofarr” has the github monitor for “yeehaw!” on their machine, can “enyst” trigger a conversation by merely commenting with “yeehaw!” on the monitored repo?

Do not guess. Investigate. Respond directly on github here. Note that this PR is closed, you must not push code changes, although you can make changes locally if you need to test.

@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented May 23, 2026

I'm on it! enyst can track my progress at all-hands.dev

Copy link
Copy Markdown
Member

enyst commented May 23, 2026

Investigated the PR diff, the merged github-repo-monitor files, and also cloned/checked OpenHands/software-agent-sdk and OpenHands/automation as requested.

Short answer: no, this skill/code does not enforce "only respond to the GitHub login that owns GITHUB_TOKEN" or any other allowed-login list.

What I found:

  • The setup instructions verify GITHUB_TOKEN by calling /user, but only to prove the token is valid. The automation script's _verify_token_and_repo() also reads/prints/returns the authenticated username, but main() ignores that return value. There is no comparison to comment.user.login.
  • The actual trigger loop is just:
    1. skip already-processed comment IDs,
    2. skip bot accounts (login.endswith("[bot]") or user.type == "Bot"),
    3. check whether the comment body contains TRIGGER_PHRASE case-insensitively,
    4. extract the issue/PR number and create/resume/forward the conversation.
  • _build_initial_prompt() includes the triggering author in the prompt (Triggering comment by @...), but that is context only, not authorization.
  • The docs/setup flow do not ask for an allowed login. In fact the confirmation text says to post the trigger phrase on any issue or PR in the monitored repo to test it.
  • I checked the automation service repo too. For this skill, the automation is cron-based and the service just schedules/runs the uploaded tarball; it does not know the GitHub comment author. The event-triggered path in OpenHands/automation can evaluate an optional JMESPath filter, but this skill is not using that path and does not create a sender.login == ... filter.
  • I checked the agent SDK/server side as well. The script creates/sends messages to conversations through the session API key. Those endpoints have no GitHub sender identity concept, so there is no hidden enforcement there either.

So for the concrete example: yes - assuming enyst can comment on the monitored repo and the automation is configured for TRIGGER_PHRASE = "yeehaw!" and the relevant event type, an enyst comment containing yeehaw! would pass the current code's filters and trigger a conversation. The main built-in exceptions are bot accounts, comments outside the poll window/already processed, or comments on event types the automation was not configured to monitor.

This PR comment was created by an AI agent (OpenHands) on behalf of the requester.

Copy link
Copy Markdown
Member

enyst commented May 23, 2026

Follow-up recommendation: put the login restriction in the deterministic monitor script, not in the prompt, SDK, or automation service.

Best shape:

  • Add explicit config near the top of skills/github-repo-monitor/scripts/main.py, e.g. ALLOWED_GITHUB_LOGINS = [] / None / sentinel for "token owner only".
  • Capture the username returned by _verify_token_and_repo(github_token, REPO) in main().
  • In the main event-processing loop, after the bot-account skip and before _has_trigger() / _process_trigger_comment(), reject comments whose comment.user.login is not allowed. Mark them processed and log a short skip message so the same unauthorized trigger comment is not reconsidered on later runs.
  • Update SKILL.md to ask the setup user who may trigger the automation, with a safe default. My preferred default is token owner only, with an explicit option to allow additional GitHub logins or all repo commenters.

I would not put this solely in _build_initial_prompt() because that is only an instruction to the agent after the conversation has already been triggered. I also would not rely on the automation service/SDK for this particular skill because it is cron-polling GitHub directly; the service just runs the tarball and the SDK conversation API has no GitHub sender identity concept.

If this later becomes an event/webhook-based automation, an extra sender.login/comment.user.login filter at the automation trigger layer would be useful defense-in-depth, but the script should still enforce it before creating or resuming conversations.

This PR comment was created by an AI agent (OpenHands) on behalf of the requester.

Copy link
Copy Markdown
Member

enyst commented May 23, 2026

I created a new branch from main and opened a nondraft PR with the deterministic login check here: #262

Summary: the monitor now defaults to allowing only the authenticated GITHUB_TOKEN owner to trigger conversations, with an explicit ALLOWED_GITHUB_LOGINS config for additional/exact logins or * for any non-bot commenter. The check happens in main.py before creating/resuming/forwarding conversations.

This PR comment was created by an AI agent (OpenHands) on behalf of the requester.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants