183 lines
4.7 KiB
Markdown
183 lines
4.7 KiB
Markdown
# 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:
|
|
```yaml
|
|
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:
|
|
```json
|
|
{
|
|
"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
|