5.5 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 — macOS LaunchAgent
Create ~/Library/LaunchAgents/com.hans.gh-monitor.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.hans.gh-monitor</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/python3</string>
<string>/Users/hansheinemann/Projects/hans-tools/gh-monitor/poll.py</string>
</array>
<key>StartInterval</key>
<integer>300</integer>
<key>StandardOutPath</key>
<string>/Users/hansheinemann/Projects/hans-tools/gh-monitor/state/stdout.log</string>
<key>StandardErrorPath</key>
<string>/Users/hansheinemann/Projects/hans-tools/gh-monitor/state/stderr.log</string>
<key>RunAtLoad</key>
<false/>
</dict>
</plist>
Load with:
launchctl load ~/Library/LaunchAgents/com.hans.gh-monitor.plist
Pause/resume:
launchctl unload ~/Library/LaunchAgents/com.hans.gh-monitor.plist
launchctl load ~/Library/LaunchAgents/com.hans.gh-monitor.plist
Token cost: Zero during polling. The main session is only woken when
poll.py calls openclaw system event — i.e. only when real PR activity
is detected.
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