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
Runsgh api --paginate <endpoint>as subprocess.
Returns parsed JSON. RaisesGHAPIErroron 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 withcreated_at > cursor_ts.
Returns sorted bycreated_atascending.
STEP 7 — poll.py: notification sender
Function:
-
notify(text)
Runsopenclaw system event --text "<text>" --mode nowas 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 tostate/errors.log.
Incrementsstate["<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- Get cursor from state (or now if first run).
- Fetch open PRs.
- For each PR: fetch reviews, review_comments, issue_comments.
- Filter to new events since cursor.
- Fire notify() for each new event.
- Update cursor to max(created_at) of processed events (or now if none).
- 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):
python3 poll.pywith no state file → creates state, no notifications (first-run cursor set to now)- Post a comment on PR #1 → run poll.py → notification fires
- Run poll.py again immediately → no duplicate notification (cursor advanced)
- Break
ghbinary path temporarily → error logged, no crash - 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