Watch one or more GitHub repositories for new issues, pull requests, comments, PR reviews, and inline diff comments — emits a fresh turn each time something actionable happens. Use when you've opened PRs / issues mimir should know about review activity for, when you want operator-actionable signals for repos mimir maintains, or when you need to track upstream activity on a watched repo. Filters out events authored by mimir's own GitHub identity (set ``MIMIR_GITHUB_SELF_LOGIN``) so the bot doe...
Scanned 9/1/2026
Install to Claude Code
npx -y skills add jasoncarreira/mimir --skill github-poller --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Github Poller?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/jasoncarreira-github-poller)More formats (shields.io, HTML) on the badges page.
---
name: github-poller
description: "Watch one or more GitHub repositories for new issues, pull requests, comments, PR reviews, and inline diff comments — emits a fresh turn each time something actionable happens. Use when you've opened PRs / issues mimir should know about review activity for, when you want operator-actionable signals for repos mimir maintains, or when you need to track upstream activity on a watched repo. Filters out events authored by mimir's own GitHub identity (set ``MIMIR_GITHUB_SELF_LOGIN``) so the bot doesn't wake itself with its own comments. Companion to the ``pollers`` framework skill (mechanics) and the ``world-scanning`` skill (catalog of *what's worth polling*). Opt-in: copy this directory into ``<home>/skills/github-poller/`` then set the env vars below."
env:
required:
- name: GITHUB_REPOS
description: "Comma-separated owner/repo list (e.g. jasoncarreira/mimir,jasoncarreira/mimirbot)"
example: "jasoncarreira/mimir"
optional:
- name: GITHUB_TOKEN
description: "GitHub PAT with repo scope. Falls back to gh auth token when unset."
example: "ghp_..."
- name: MIMIR_GITHUB_SELF_LOGIN
description: "GitHub login mimir authors as. Events from this login are filtered out."
example: "mimir-carreira"
- name: MIMIR_GITHUB_PRELOAD_REVIEW_SKILL
description: "Set to true to inline the bundled review skill's SKILL.md into PR-review prompts. Auto-resolved from <home>/skills/review/ or <home>/.mimir_builtin_skills/review/ — no path needed for a standard install (the review skill ships bundled)."
example: "true"
- name: MIMIR_GITHUB_REVIEW_SKILL_PATH
description: "Optional absolute-path override for the review SKILL.md. Defaults to the auto-resolved <home>/skills|.mimir_builtin_skills/review/ location; only set this for a non-standard layout."
example: "<absolute path to review SKILL.md on your install>"
only_if: "MIMIR_GITHUB_PRELOAD_REVIEW_SKILL=true"
---
# github-poller — watch GitHub repos for activity
This is an **opt-in poller skill** that ships with mimir under
``mimir/optional-skills/`` but is NOT auto-installed. Most installs
won't watch a GitHub repo, so the framework doesn't seed it by default.
## Installation
1. Copy the directory into your agent home:
```
cp -r mimir/optional-skills/github-poller <home>/skills/
```
(Or run from inside the container against ``/workspace/mimir``.)
2. Configure env vars:
| Variable | Required | Description |
|---|---|---|
| `GITHUB_REPOS` | yes | Comma-separated `owner/repo` list (e.g. `jasoncarreira/mimir,jasoncarreira/mimirbot`) |
| `GITHUB_TOKEN` | recommended | GitHub PAT. Falls back to `gh auth token` when unset. |
| `MIMIR_GITHUB_SELF_LOGIN` | optional | GitHub login mimir authors as. Events from this login get filtered out (mimir doesn't wake itself). Leave empty if mimir uses the operator's PAT — the operator is the signal you want, not the noise. |
These env vars must be exported in mimir's process environment
(e.g. via the container's env file). The framework gates subprocess
env keys two ways: a built-in allowlist (``PATH``, ``HOME``,
locale, XDG, CA bundles — everything else is stripped) plus a deny
filter on top (``*_TOKEN``, ``*_SECRET``, ``*_API_KEY``,
``*_PASSWORD``, ``MIMIR_*`` are stripped even if allowlisted).
The ``pass_env`` field in this skill's ``pollers.json`` declares
all three keys above as explicit pass-throughs, bypassing both
gates — different mechanism per key:
- ``GITHUB_TOKEN`` — matches ``*_TOKEN`` deny pattern → `pass_env`
bypasses the deny filter.
- ``MIMIR_GITHUB_SELF_LOGIN`` — matches ``MIMIR_*`` deny pattern →
`pass_env` bypasses the deny filter.
- ``GITHUB_REPOS`` — not a secret, but not in the built-in
allowlist either → `pass_env` bypasses the allowlist gate. Same
end result (key reaches the subprocess), just gated differently.
If you rename or relocate a var, update ``pollers.json``
accordingly. ``GITHUB_TOKEN`` set but not declared in ``pass_env``
→ silently absent in the subprocess → poller falls through to
``gh auth token`` → zero events forever if ``gh`` isn't authed.
3. Bring it live:
```
reload_pollers
# → "reload_pollers ok: N poller(s) registered — github-activity, ..."
```
(Or restart the container; pollers are auto-loaded at startup.)
## What it watches
For each repo in `GITHUB_REPOS`:
- **New issues** opened (since last poll)
- **New pull requests** opened
- **New issue comments** (covers both issue + PR conversation comments)
- **New PR review comments** (inline diff comments — distinct from conversation comments)
- **New PR reviews** (approve / changes-requested / commented with body)
- **PR pushes/updates** (`pr_synchronize`) — new commits pushed to an existing open PR.
The poller calls `GET /repos/{repo}/compare/{prev}...{new}` to fetch up to 3
commit subjects inline. If multiple commits land between polls, all are
surfaced (total count + first 3 subjects + "… N more" when truncated).
Force-pushes that change the SHA but not the diff also fire — this is a
known false-positive; compare-diff diffing on every poll is too expensive.
- **Review requests** (`pr_review_requested`) — fired when `MIMIR_GITHUB_SELF_LOGIN`
appears in a PR's `requested_reviewers`. **State-reconciling retry** (chainlink
#299): a submitted review removes you from `requested_reviewers`, so while you
remain requested the poller RE-EMITS once per poll (up to
`REVIEW_REQUEST_MAX_ATTEMPTS`, default 3). This recovers a review whose
triggered turn died (e.g. a transient model 503) instead of silently dropping
it — the old emit-once model advanced the cursor past a dead turn and never
re-fired (observed on PR #511). On exhaustion it emits a one-shot
`pr_review_request_gave_up` **signal** (no turn; surfaces as a *negative
algedonic signal* via `feedback.classify`'s `*_gave_up` rule) and goes dormant
for that PR until you're removed and re-requested. If an operator re-requests
you after you already submitted a substantive review (`APPROVED`,
`CHANGES_REQUESTED`, or `COMMENTED`) at the current head SHA, the poller
treats that request as satisfied and suppresses both retry emits and give-up
signals.
- **Stale changes-requested on your own PRs** (`pr_changes_requested_stale`) —
state-reconciling reminder (chainlink #449), the reverse direction of the
above: reviews ON your PRs are otherwise edge-triggered only, so a turn that
reads a request-changes review without pushing fixes loses the work signal
when the cursor advances. Each poll, your open PRs whose latest substantive
review per reviewer is `CHANGES_REQUESTED` **and** whose reviewed diff has
not materially changed are reminded on an hourly elapsed-time cadence, with a
one-minute allowance for per-run timestamp jitter. Polls before the interval
expires do not refresh the timestamp. A substantive fix, later approval
clearing all blocking reviews, or PR close/merge stops reminders; a
content-free rebase with the same patch does not. Reminder emissions use a
bounded attempt series and a one-shot give-up signal; because queued emissions
are not proof of delivery, an unchanged unresolved PR starts a fresh series
after a 24-hour backstop rather than staying silent forever.
- **Mergeability of your own PRs** — GitHub's per-PR `mergeable` result is
reconciled with the declared base/head comparison. A clean branch behind its
base emits `pr_mergeability_rebase`, which uses the existing scoped checkout,
rebases onto the declared base, runs tests, and pushes with a lease against the
observed head. The automatic path is restricted to PRs with no blocking
`CHANGES_REQUESTED` review, so moving the head cannot silently discharge
unaddressed feedback. A conflict emits `pr_mergeability_conflicting`; its
scope cannot push and the turn must reproduce the conflict, name all unmerged
paths and the base SHA, then abort and escalate. A `null` mergeability result
is asynchronous/unknown and is never acted upon. Attempts use hourly backoff,
are bounded per PR/head/reason, and end in the named
`pr_mergeability_rebase_gave_up` signal. At most one such attempt is emitted
across all repositories in a poll cycle. This path never merges a PR.
- **Completed check failures on open PRs** (`pr_ci_failure`) — a newly failed
check set on a PR authored by `MIMIR_GITHUB_SELF_LOGIN` starts one remediation
turn bound to the repository, PR number, immutable head SHA, failed check IDs,
names, conclusions, and log URLs. The repository checkout preflight re-fetches
the PR and checks and stops before mutation when the PR closed, the head was
superseded, or a rerun is green. Failures on external contributors' PRs emit a
`pr_ci_failure_external` notification signal only and never receive repository
mutation authority. CI remediation retains `pr_comment` authority so blocked
or failed repairs can leave an operator-visible trace, but excludes `pr_edit`
and `pr_rerequest`. Stable delivery keys and framework receipts dedupe
overlapping polls while allowing a rejected handoff to retry safely. On
rollout, a head whose failures predate the first observation is deliberately
baselined and remains quiet until its head or failure set changes. Open-PR
discovery is intentionally bounded to the 100 most recently updated PRs; a
newly completed check normally refreshes the affected PR into that window.
Issue / PR / comment / review detection is filtered by `created_at > cursor`
and (when set) `user.login != MIMIR_GITHUB_SELF_LOGIN`. Push + review-request
detection are state-based (head-SHA delta / `requested_reviewers` membership),
not `created_at`-windowed.
## What it doesn't watch (deliberate)
- **Commits** — already handled by `git pull`. No GitHub-API path for "new commit" wakes today.
- **Issue/PR state changes** (close, reopen, merge, label) — adds noise; revisit if it becomes useful.
- **Main-branch workflow failures** — remain the separate `github-ci-watch`
responsibility; this poller only handles checks attached to open PR heads.
- **Notifications API** (`/notifications`) — an alternative path that's higher-noise; this poller takes the targeted-endpoint approach instead.
## Why login-based, not email-based
GitHub's API events expose `user.login` for issues, PRs, comments, and reviews — never the author's commit email. Email-based filtering (e.g. `noreply@mimir-agent.local`) is the right key for **commits** but not for the API surface.
## Cost
Polling 1 repo every 15 min @ 4 endpoints = ~16 calls/hr/repo. For a 5-repo watch that's ~80/hr — well under GitHub's 5000/hr per-PAT rate limit. The `since=` query param keeps each call's payload small.
## Batching
`pollers.json` sets `batch_size=1`: each independent GitHub item gets its own turn and full tool budget. This deliberately avoids combining multiple PR reviews or remediations into one turn whose shared budget can strand later items. Tune only with care: larger batches reduce turn count but couple unrelated GitHub work to one execution budget.
## Cursor file
Persists at `<home>/state/pollers/github-activity/cursor.json` (the framework-injected `STATE_DIR`). Survives container rebuilds. First run looks back 1 hour to bound the backfill burst.
The `pr_changes_requested` key maps `{repo: {pr_number: {"head_sha": sha, "last_reminded_at": ISO-8601 UTC timestamp, "attempts": count}}}`. It tracks unresolved own-PR stale states, the hourly reminder floor, and bounded attempt series while the live snapshot remains semantically blocked. Attempts are charged from framework-written turn outcomes, not reminder emission. Authorization and tool-boundary refusals are reported but exempt from the content-failure budget and retry only at the 24-hour backstop, preventing a permanent configuration fault from firing at poll cadence. At the cap it emits one `pr_changes_requested_gave_up` signal with the charged outcome reasons; a head change re-arms immediately, and an unchanged parked entry re-arms after the same 24-hour delivery-loss backstop. Closed, unblocked, or substantively changed PRs drop out. Pre-cadence bare-string and structured-without-`attempts` entries migrate quietly as attempt 1 with `last_reminded_at` set to that poll's time, preventing an upgrade-time emit storm.
The `pr_review_requests` key maps `{repo: {pr_number: attempts}}` — `attempts` counts `pr_review_requested` emits while you stayed requested (chainlink #299; a dormant PR that gave up parks at `cap + 1`). The pre-#299 bare-list format (`{repo: [pr_number, ...]}`) migrates automatically on first load.
The `pr_mergeability` key maps `{repo: {pr_number: {head_sha, reason, last_attempt_at, attempts}}}`. A new head or changed failure reason starts a fresh bounded series; current and closed PRs drop out during snapshot rebuild.
The `pr_ci_failures` key maps each live red PR to its immutable head, stable
failure-set delivery key, and last emit time. Framework acknowledgements live in
`<home>/state/pollers/github-activity/.delivery-receipts/`; a receipt is created
only after enqueue or external-signal logging succeeds. A missing receipt makes
the same claim retry-eligible after five minutes, while atomic files under
`.delivery-claims/` suppress concurrent poll overlap.
## Disabling temporarily
- Unset `GITHUB_REPOS` — the poller exits silently.
- Or set the cron in `pollers.json` to a far-future expression.
- Or remove the skill directory: `rm -rf <home>/skills/github-poller/` + `reload_pollers`.
## Debugging
| Symptom | Check |
|---|---|
| Poller not firing | `events.jsonl` for `poller_complete{poller=github-activity}` — is it appearing on schedule? |
| No events from a repo where you expected one | `events.jsonl` for `poller_stderr` from this poller — auth or rate-limit issues land here |
| Too many wake-ups | Set `MIMIR_GITHUB_SELF_LOGIN` to mimir's GitHub login (when mimir gets its own bot account) so its own comments stop firing the poller |
| Cursor stuck | `cat <home>/state/pollers/github-activity/cursor.json` — should be a recent ISO timestamp |
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!