Files
hans-tools/gh-monitor/buildspec.md

4.7 KiB

gh-monitor — Build Spec

Status: Pending Andrew's review
Depends on: design.md approved


Directory Layout

gh-monitor/
├── design.md
├── buildspec.md
├── poll.py               # Main entry point
├── config/
│   └── watched.yaml      # Repos and filter rules
├── state/
│   ├── last_seen.json    # Event cursor (gitignored)
│   └── errors.log        # Error log (gitignored)
├── requirements.txt
└── .gitignore

Build Order

STEP 1 — .gitignore + requirements.txt

.gitignore:

state/last_seen.json
state/errors.log
__pycache__/
*.pyc
.env

requirements.txt:

PyYAML>=6.0

(All other deps: stdlib + gh CLI)

STEP 2 — config/watched.yaml

Starter config watching the-agency repo:

repos:
  - owner: coding-with-hans-heinemann
    repo: the-agency
    notify_on:
      - review_submitted
      - review_comment
      - issue_comment
      - pr_closed

STEP 3 — poll.py: config + state loader

Functions:

  • load_config(path) -> dict
    Reads watched.yaml. Raises on missing file.

  • load_state(path) -> dict
    Reads last_seen.json. Returns {} if file doesn't exist (first run).

  • save_state(state, path)
    Atomically writes last_seen.json (write to .tmp, rename).

STEP 4 — poll.py: GitHub API client

Function:

  • gh_api(endpoint) -> list | dict
    Runs gh api --paginate <endpoint> as subprocess.
    Returns parsed JSON. Raises GHAPIError on non-zero exit.

  • get_open_prs(owner, repo) -> list[dict]
    Calls /repos/{owner}/{repo}/pulls?state=open.
    Returns list of PR dicts (number, title, html_url).

STEP 5 — poll.py: event fetchers

Functions (each returns list of event dicts with event_type, created_at, actor, body, url):

  • get_reviews(owner, repo, pr_number) -> list[dict]
    /repos/{owner}/{repo}/pulls/{pr_number}/reviews

  • get_review_comments(owner, repo, pr_number) -> list[dict]
    /repos/{owner}/{repo}/pulls/{pr_number}/comments

  • get_issue_comments(owner, repo, pr_number) -> list[dict]
    /repos/{owner}/{repo}/issues/{pr_number}/comments

STEP 6 — poll.py: event diffing

Function:

  • new_events_since(events, cursor_ts) -> list[dict]
    Filters events to those with created_at > cursor_ts.
    Returns sorted by created_at ascending.

STEP 7 — poll.py: notification sender

Function:

  • notify(text)
    Runs openclaw system event --text "<text>" --mode now as subprocess.
    Logs warning and continues on non-zero exit (best-effort).

  • format_notification(repo_slug, pr, event) -> str
    Builds the notification string:
    [gh-monitor] PR #N "title" — <actor> <action>:\n"<body[:200]>"\n<url>

STEP 8 — poll.py: error tracking

Module-level logic:

  • log_error(repo_slug, error, state)
    Appends to state/errors.log.
    Increments state["<repo_slug>"]["consecutive_errors"] counter.
    If counter >= 3 and not already alerted: fires one notify() alert.
    Resets counter to 0 on successful poll for that repo.

STEP 9 — poll.py: main poll loop

Function:

  • poll_repo(repo_cfg, state) -> dict

    1. Get cursor from state (or now if first run).
    2. Fetch open PRs.
    3. For each PR: fetch reviews, review_comments, issue_comments.
    4. Filter to new events since cursor.
    5. Fire notify() for each new event.
    6. Update cursor to max(created_at) of processed events (or now if none).
    7. Return updated state slice.
  • main()
    Loads config + state.
    Calls poll_repo() for each repo in watched.yaml.
    Saves state.
    Exits 0.

Entry point: if __name__ == "__main__": main()

STEP 10 — OpenClaw cron job

Register via OpenClaw cron API:

{
  "name": "gh-monitor",
  "schedule": { "kind": "every", "everyMs": 300000 },
  "payload": {
    "kind": "systemEvent",
    "text": "Run GitHub PR monitor: cd ~/Projects/hans-tools/gh-monitor && python3 poll.py"
  },
  "sessionTarget": "main"
}

Note: this is a systemEvent (not agentTurn) so it injects into the main session and Hans handles it inline. If this proves noisy, switch to agentTurn in isolated session.


Testing Plan

Manual test steps (no automated tests for v1):

  1. python3 poll.py with no state file → creates state, no notifications (first-run cursor set to now)
  2. Post a comment on PR #1 → run poll.py → notification fires
  3. Run poll.py again immediately → no duplicate notification (cursor advanced)
  4. Break gh binary path temporarily → error logged, no crash
  5. After 3 failed cycles → single alert fires

What Is NOT in This Build

  • Automated test suite
  • Filtering by comment author
  • Digest/batching mode
  • Any write operations to GitHub
  • Anything touching main branch