Initial release: org-wide heavy-duty GitHub review watcher
Polls all heavy-duty/* repos every 15m for assignee/review-requested work, reviews with clear Verdicts until agreement, and includes tmux restore + hourly health self-heal. Fully documented.
This commit is contained in:
commit
1d00544e86
17 changed files with 1008 additions and 0 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
state.json
|
||||
logs/
|
||||
.poll-prompt.txt
|
||||
config/watcher.env
|
||||
*.log
|
||||
.DS_Store
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2026 grok-bot-andresmgsl
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
178
POLL_INSTRUCTIONS.md
Normal file
178
POLL_INSTRUCTIONS.md
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
# heavy-duty org watcher — poll cycle instructions
|
||||
|
||||
You are the GitHub collaboration agent for the **entire [heavy-duty](https://github.com/heavy-duty) organization**, authenticated as **`grok-bot-andresmgsl`**.
|
||||
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| State | `/home/grok/heavy-duty-watcher/state.json` (or `./state.json` when running from clone) |
|
||||
| Logs | `logs/poll.log` |
|
||||
| This file | Authoritative review rules |
|
||||
|
||||
Live install defaults to `/home/grok/heavy-duty-watcher`. The published repo is the source of truth; deploy by cloning and running `scripts/install-live.sh`.
|
||||
|
||||
## Mission
|
||||
|
||||
Every poll, find open **Issues** and **PRs across all `heavy-duty/*` repos** where this bot is:
|
||||
|
||||
1. **Assignee** (`assignee:grok-bot-andresmgsl`), or
|
||||
2. **Requested reviewer** (`review-requested:grok-bot-andresmgsl`)
|
||||
|
||||
**Only act on those.** Do not drive-by comment on unassigned items.
|
||||
|
||||
**Until you agree with the solution:** when review-requested, keep reviewing on each material update (new commits, re-request, author response). Use **Request changes** while blockers remain; **Approve** only when you fully agree. Do not leave stale REQUEST_CHANGES standing without re-checking after fixes.
|
||||
|
||||
## Scope
|
||||
|
||||
```text
|
||||
org: heavy-duty
|
||||
bot: grok-bot-andresmgsl
|
||||
discovery: org-wide GitHub search (not a single repo)
|
||||
```
|
||||
|
||||
Repos the org repo list when useful:
|
||||
|
||||
```bash
|
||||
gh api orgs/heavy-duty/repos --paginate --jq '.[].full_name'
|
||||
```
|
||||
|
||||
## Each poll cycle (strict order)
|
||||
|
||||
### 1. Discover (org-wide)
|
||||
|
||||
```bash
|
||||
gh search prs --owner heavy-duty --review-requested=@me --state open \
|
||||
--json number,title,url,updatedAt,author,assignees,repository --limit 50
|
||||
gh search prs --owner heavy-duty --assignee=@me --state open \
|
||||
--json number,title,url,updatedAt,author,assignees,repository --limit 50
|
||||
gh search issues --owner heavy-duty --assignee=@me --state open \
|
||||
--json number,title,url,updatedAt,author,assignees,repository --limit 50
|
||||
```
|
||||
|
||||
Optional notifications:
|
||||
|
||||
```bash
|
||||
gh api 'notifications?participating=true&per_page=50' \
|
||||
--jq '.[] | select(.repository.owner.login=="heavy-duty") | {repo: .repository.full_name, title: .subject.title, reason, type: .subject.type}'
|
||||
```
|
||||
|
||||
### 2. For each match — read everything first
|
||||
|
||||
**Never comment without full context.** Derive `OWNER/REPO` from `repository.nameWithOwner` (or `heavy-duty/<name>`).
|
||||
|
||||
For **PRs** (`N` = number, `REPO` = full name):
|
||||
|
||||
```bash
|
||||
gh pr view N --repo REPO --json title,body,author,assignees,reviewRequests,reviews,comments,commits,files,baseRefName,headRefName,headRefOid,isDraft,state,url,updatedAt
|
||||
gh api repos/REPO/pulls/N/comments --paginate
|
||||
gh api repos/REPO/issues/N/comments --paginate
|
||||
gh pr diff N --repo REPO
|
||||
gh pr checks N --repo REPO
|
||||
```
|
||||
|
||||
For **Issues**:
|
||||
|
||||
```bash
|
||||
gh issue view N --repo REPO --json title,body,author,assignees,comments,labels,state,url
|
||||
# paginate comments if needed
|
||||
```
|
||||
|
||||
**Always scan prior comments/reviews by `grok-bot-andresmgsl`** so you never repeat yourself.
|
||||
|
||||
### 3. Decide whether to speak
|
||||
|
||||
Comment / review only when **at least one** is true:
|
||||
|
||||
- First time this bot is assigned / requested on this item
|
||||
- New commits since last review (PR `headRefOid` changed vs state)
|
||||
- New human comments that need a response (especially replies to our blockers)
|
||||
- CI status changed in a way that affects the review
|
||||
- Explicit re-request of review
|
||||
|
||||
If nothing material changed → **silent poll**. Log it. Do not spam.
|
||||
|
||||
### 4. Feedback style (GitHub collaboration best practices)
|
||||
|
||||
**Be explicit about the verdict at the top:**
|
||||
|
||||
| Kind | When |
|
||||
|------|------|
|
||||
| `**Verdict: Approve** — I agree with this as-is.` | Fully agree; no remaining blockers |
|
||||
| `**Verdict: Comment** — overall direction is good; feedback below.` | Non-blocking notes only |
|
||||
| `**Verdict: Request changes** — blockers listed below.` | Must fix before approval |
|
||||
|
||||
For issues:
|
||||
|
||||
- `**Status: Aligned**`
|
||||
- `**Status: Feedback**`
|
||||
- `**Status: Need info**`
|
||||
|
||||
Rules:
|
||||
|
||||
- **Specific**: cite file paths, line ranges, commit SHAs, behaviors
|
||||
- **Non-redundant**: do not restate the author; do not re-open resolved threads without new evidence
|
||||
- **Actionable**: each point implies a concrete next step (or mark `optional / nit`)
|
||||
- **Separate blockers from nits**: `### Blockers` and `### Nits / optional`
|
||||
- **Tone**: professional, collaborative, concise — no filler
|
||||
- **Draft PRs**: lighter touch unless asked for a full review
|
||||
- Prefer **`gh pr review`** for code over scattered conversation comments
|
||||
- Prefer **threaded replies** when continuing an existing review discussion
|
||||
- When peers already said the same blocker, **agree briefly and add only net-new signal**
|
||||
|
||||
### 5. Posting
|
||||
|
||||
```bash
|
||||
# PR review
|
||||
gh pr review N --repo REPO --approve --body "..."
|
||||
gh pr review N --repo REPO --request-changes --body "..."
|
||||
gh pr review N --repo REPO --comment --body "..."
|
||||
|
||||
# Issue / conversation comment
|
||||
gh issue comment N --repo REPO --body "..."
|
||||
```
|
||||
|
||||
**Never** force-push, merge, close, or re-assign unless the operator explicitly asks in-session.
|
||||
|
||||
### 6. Update state
|
||||
|
||||
Write `state.json`:
|
||||
|
||||
- `scope`: `"org:heavy-duty"`
|
||||
- `last_poll_at`: ISO UTC
|
||||
- `last_poll_summary`: one line
|
||||
- `items` keyed as `owner/repo#pr:N` or `owner/repo#issue:N`, each with:
|
||||
- `url`, `title`, `role` (`assignee` | `reviewer` | both)
|
||||
- `last_seen_updated_at`, `last_head_sha` (PRs)
|
||||
- `last_action` (`reviewed` | `commented` | `silent` | `skipped`)
|
||||
- `last_action_at`, `review_event` if any, `notes`
|
||||
|
||||
Append to `logs/poll.log`:
|
||||
|
||||
```text
|
||||
[ISO_UTC] summary...
|
||||
```
|
||||
|
||||
### 7. Report back
|
||||
|
||||
Briefly list: scanned counts, what was posted (with links), what was intentionally silent and why.
|
||||
|
||||
If zero assignments: update state, log `0 actionable`, stop. Do not invent work.
|
||||
|
||||
## Review loop (“until you agree”)
|
||||
|
||||
```text
|
||||
discover → read all context → decide
|
||||
│
|
||||
├─ blockers remain → Request changes (specific, actionable)
|
||||
├─ only nits → Comment (optional) or Approve if nits truly optional
|
||||
└─ fully agree → Approve
|
||||
│
|
||||
└─ later: new commits / re-request / author reply
|
||||
→ re-read ALL comments + new diff vs last_head_sha
|
||||
→ Approve or Request changes again (no zombie opinions)
|
||||
```
|
||||
|
||||
## Identity
|
||||
|
||||
- Bot: `grok-bot-andresmgsl`
|
||||
- Org: `heavy-duty`
|
||||
- Role: assigned reviewer / assignee agent — **not** an unsolicited drive-by bot
|
||||
120
README.md
Normal file
120
README.md
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
# heavy-duty-watcher
|
||||
|
||||
**Org-wide GitHub review agent** for the [heavy-duty](https://github.com/heavy-duty) organization.
|
||||
|
||||
Runs as bot **`grok-bot-andresmgsl`**. Every 15 minutes it discovers open Issues/PRs where this bot is **assignee** or **requested reviewer** across **all** `heavy-duty/*` repos, reads the full discussion, and posts structured reviews until it **agrees** (Approve) or still has blockers (Request changes).
|
||||
|
||||
> Not a drive-by bot. No unsolicited comments. No merge/close/reassign unless an operator asks.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Org-wide scope** — one watcher for every repo under `heavy-duty`
|
||||
- **Strict collaboration rules** — clear Verdict/Status, specific, non-redundant, blockers vs nits
|
||||
- **Stateful de-dupe** — tracks per-item head SHA and last action (`owner/repo#pr:N`)
|
||||
- **Dual durability** — Grok durable schedulers **and** local tmux loops
|
||||
- **Reboot recovery** — `scripts/restore.sh` rebuilds tmux + resumes the agent
|
||||
- **Hourly health watchdog** — verifies poll loop, gh auth, freshness; self-heals
|
||||
|
||||
---
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
git clone https://github.com/grok-bot-andresmgsl/heavy-duty-watcher.git
|
||||
cd heavy-duty-watcher
|
||||
gh auth status # bot account with org access
|
||||
./scripts/install-live.sh
|
||||
./scripts/restore.sh # after reboot: always this
|
||||
```
|
||||
|
||||
Arm durable schedulers from a live Grok session using prompts in [`docs/SCHEDULER_PROMPTS.md`](docs/SCHEDULER_PROMPTS.md).
|
||||
|
||||
---
|
||||
|
||||
## Repository layout
|
||||
|
||||
```text
|
||||
heavy-duty-watcher/
|
||||
├── README.md ← you are here
|
||||
├── POLL_INSTRUCTIONS.md ← authoritative review rules (read every cycle)
|
||||
├── config/
|
||||
│ ├── state.template.json
|
||||
│ └── watcher.env.example
|
||||
├── scripts/
|
||||
│ ├── discover.sh ← org-wide actionable list
|
||||
│ ├── poll-once.sh ← one headless poll (Grok)
|
||||
│ ├── poll-loop.sh ← every 15m
|
||||
│ ├── health-check.sh
|
||||
│ ├── health-loop.sh ← every 1h + self-heal
|
||||
│ ├── restore.sh ← reboot recovery (tmux)
|
||||
│ └── install-live.sh ← deploy under ~/heavy-duty-watcher
|
||||
├── docs/
|
||||
│ ├── ARCHITECTURE.md
|
||||
│ ├── OPERATIONS.md
|
||||
│ ├── SCHEDULER_PROMPTS.md
|
||||
│ └── REVIEW_PLAYBOOK.md
|
||||
└── logs/ ← runtime (gitignored)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How a poll works
|
||||
|
||||
```text
|
||||
discover (org search)
|
||||
→ for each match
|
||||
read ALL comments + reviews + diff + checks
|
||||
compare head SHA / prior bot comments (state.json)
|
||||
if material change:
|
||||
post Verdict: Request changes | Comment | Approve
|
||||
else:
|
||||
silent
|
||||
→ update state.json + logs/poll.log
|
||||
```
|
||||
|
||||
Full rules: [`POLL_INSTRUCTIONS.md`](POLL_INSTRUCTIONS.md).
|
||||
|
||||
---
|
||||
|
||||
## Documentation map
|
||||
|
||||
| Doc | Audience |
|
||||
|-----|----------|
|
||||
| [POLL_INSTRUCTIONS.md](POLL_INSTRUCTIONS.md) | Agent — must follow every cycle |
|
||||
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | Design / durability model |
|
||||
| [docs/OPERATIONS.md](docs/OPERATIONS.md) | Humans operating the watcher |
|
||||
| [docs/SCHEDULER_PROMPTS.md](docs/SCHEDULER_PROMPTS.md) | Copy-paste durable task prompts |
|
||||
| [docs/REVIEW_PLAYBOOK.md](docs/REVIEW_PLAYBOOK.md) | Review style examples |
|
||||
|
||||
---
|
||||
|
||||
## After reboot
|
||||
|
||||
```bash
|
||||
~/heavy-duty-watcher/scripts/restore.sh
|
||||
```
|
||||
|
||||
Creates/attaches tmux session `heavy-duty-watcher` with:
|
||||
|
||||
| Window | Role |
|
||||
|--------|------|
|
||||
| `grok` | Interactive agent (resume session when possible) |
|
||||
| `poll-loop` | Headless 15m polls |
|
||||
| `health-loop` | Hourly health + self-heal |
|
||||
| `tools` | Manual shell |
|
||||
|
||||
---
|
||||
|
||||
## Safety
|
||||
|
||||
- Only comments when **assigned** or **review-requested**
|
||||
- Never merges, closes, force-pushes, or reassigns by default
|
||||
- Secrets are **not** stored in this repo — use `gh auth` on the host
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT — see [LICENSE](LICENSE).
|
||||
22
config/state.template.json
Normal file
22
config/state.template.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"scope": "org:heavy-duty",
|
||||
"org": "heavy-duty",
|
||||
"bot_login": "grok-bot-andresmgsl",
|
||||
"session_id": null,
|
||||
"poll_interval_minutes": 15,
|
||||
"health_interval_minutes": 60,
|
||||
"created_at": null,
|
||||
"last_poll_at": null,
|
||||
"last_poll_summary": null,
|
||||
"items": {},
|
||||
"notes": "items keyed as 'owner/repo#pr:N' or 'owner/repo#issue:N'. Track last_seen_updated_at, last_head_sha (PRs), last_action, review_event.",
|
||||
"scheduler": {},
|
||||
"restore": {
|
||||
"command": "./scripts/restore.sh",
|
||||
"tmux_session": "heavy-duty-watcher"
|
||||
},
|
||||
"health": {
|
||||
"last_check_at": null,
|
||||
"status": null
|
||||
}
|
||||
}
|
||||
8
config/watcher.env.example
Normal file
8
config/watcher.env.example
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Copy to watcher.env (gitignored) for local overrides.
|
||||
WATCHER_DIR="${WATCHER_DIR:-$HOME/heavy-duty-watcher}"
|
||||
ORG="${ORG:-heavy-duty}"
|
||||
BOT_LOGIN="${BOT_LOGIN:-grok-bot-andresmgsl}"
|
||||
POLL_INTERVAL_SEC="${POLL_INTERVAL_SEC:-900}"
|
||||
HEALTH_INTERVAL_SEC="${HEALTH_INTERVAL_SEC:-3600}"
|
||||
STALE_POLL_MINUTES="${STALE_POLL_MINUTES:-45}"
|
||||
TMUX_SESSION="${TMUX_SESSION:-heavy-duty-watcher}"
|
||||
78
docs/ARCHITECTURE.md
Normal file
78
docs/ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# Architecture
|
||||
|
||||
## What this is
|
||||
|
||||
A **durable, org-wide GitHub review agent** for [heavy-duty](https://github.com/heavy-duty).
|
||||
|
||||
It polls GitHub for Issues/PRs where bot **`grok-bot-andresmgsl`** is:
|
||||
|
||||
- requested as **reviewer**, or
|
||||
- **assignee**
|
||||
|
||||
…across **every repository** in the organization. It reads full thread history, reviews diffs, and posts structured feedback until it can **Approve**.
|
||||
|
||||
## Components
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Grok durable scheduler (15m) ──► poll cycle (interactive) │
|
||||
│ Grok durable scheduler (1h) ──► health watchdog │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ tmux session: heavy-duty-watcher │
|
||||
│ ├─ poll-loop (scripts/poll-loop.sh → poll-once.sh) │
|
||||
│ ├─ health-loop (scripts/health-loop.sh → health-check.sh) │
|
||||
│ ├─ tools (manual gh / logs) │
|
||||
│ └─ grok (optional interactive resume) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
GitHub org:heavy-duty
|
||||
state.json + logs/
|
||||
```
|
||||
|
||||
### Why two poll paths?
|
||||
|
||||
| Path | Survives |
|
||||
|------|----------|
|
||||
| Grok durable scheduler | New Grok sessions (durable flag); may need re-arm after ~7 days |
|
||||
| Local `poll-loop.sh` in tmux | Box uptime; independent of Grok UI |
|
||||
|
||||
After reboot: run `scripts/restore.sh`.
|
||||
|
||||
## State model
|
||||
|
||||
`state.json` is the single source of truth for de-duplication:
|
||||
|
||||
```json
|
||||
{
|
||||
"scope": "org:heavy-duty",
|
||||
"items": {
|
||||
"heavy-duty/box#pr:79": {
|
||||
"last_head_sha": "abc…",
|
||||
"last_action": "reviewed",
|
||||
"review_event": "REQUEST_CHANGES"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Keys are always `owner/repo#pr:N` or `owner/repo#issue:N` so multiple repos never collide.
|
||||
|
||||
## Review policy (summary)
|
||||
|
||||
Full rules live in [`../POLL_INSTRUCTIONS.md`](../POLL_INSTRUCTIONS.md).
|
||||
|
||||
- Act only when assignee or review-requested
|
||||
- Read **all** comments before speaking
|
||||
- Clear **Verdict** / **Status** line first
|
||||
- **Request changes** until agreement; **Approve** when fully satisfied
|
||||
- Silent when nothing material changed
|
||||
|
||||
## Security / safety
|
||||
|
||||
- No merge / close / force-push / reassignment unless operator asks
|
||||
- Uses the bot’s GitHub token via `gh` / MCP
|
||||
- Does not store secrets in the repo (state is operational metadata only)
|
||||
69
docs/OPERATIONS.md
Normal file
69
docs/OPERATIONS.md
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# Operations runbook
|
||||
|
||||
## First-time setup
|
||||
|
||||
```bash
|
||||
# 1. Clone
|
||||
git clone https://github.com/grok-bot-andresmgsl/heavy-duty-watcher.git
|
||||
cd heavy-duty-watcher
|
||||
|
||||
# 2. Auth
|
||||
gh auth status # must be grok-bot-andresmgsl (or your reviewer bot)
|
||||
|
||||
# 3. Install live + start tmux loops
|
||||
./scripts/install-live.sh
|
||||
|
||||
# 4. In a Grok session: arm durable schedulers (15m poll + 1h health)
|
||||
# Or restore interactive session:
|
||||
./scripts/restore.sh
|
||||
```
|
||||
|
||||
## After PC reboot / box restart
|
||||
|
||||
```bash
|
||||
gh auth status
|
||||
~/heavy-duty-watcher/scripts/restore.sh
|
||||
# or:
|
||||
~/heavy-duty-watcher/scripts/restore.sh --status
|
||||
```
|
||||
|
||||
## Day-to-day
|
||||
|
||||
```bash
|
||||
# What is waiting for us right now?
|
||||
./scripts/discover.sh
|
||||
|
||||
# One-shot health
|
||||
./scripts/health-check.sh
|
||||
|
||||
# Follow activity
|
||||
tail -f logs/poll.log logs/health.log
|
||||
|
||||
# Attach tmux
|
||||
tmux attach -t heavy-duty-watcher
|
||||
```
|
||||
|
||||
## Schedulers (Grok)
|
||||
|
||||
When the agent is live, it should keep two durable recurring tasks:
|
||||
|
||||
1. **Poll every 15 minutes** — org-wide discovery + review
|
||||
2. **Health every 1 hour** — `health-check.sh` + self-heal poll-loop
|
||||
|
||||
If either disappears, re-create from the prompts in `docs/SCHEDULER_PROMPTS.md`.
|
||||
|
||||
## Migration from `~/rig-watcher`
|
||||
|
||||
The original single-repo install lived at `~/rig-watcher`.
|
||||
`install-live.sh` can symlink `~/rig-watcher` → `~/heavy-duty-watcher` for compatibility.
|
||||
Item keys changed from `pr:N` to `heavy-duty/rig#pr:N`.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Check |
|
||||
|---------|--------|
|
||||
| No reviews happening | `./scripts/discover.sh` empty? Not requested. |
|
||||
| Stale last_poll | `health-check.sh` WARN; is poll-loop alive? |
|
||||
| gh 401 | `gh auth login` |
|
||||
| Duplicate spam | Inspect `state.json` `last_head_sha` / `last_action` |
|
||||
| Scheduler missing | Re-arm via Grok; see `docs/SCHEDULER_PROMPTS.md` |
|
||||
51
docs/REVIEW_PLAYBOOK.md
Normal file
51
docs/REVIEW_PLAYBOOK.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# Review playbook
|
||||
|
||||
## When we act
|
||||
|
||||
| Signal | Action |
|
||||
|--------|--------|
|
||||
| First review request | Full review |
|
||||
| Head SHA changed after our review | Re-review delta + full thread |
|
||||
| Author replies to our blockers | Re-read thread; Approve or keep REQUEST_CHANGES |
|
||||
| Explicit re-request | Full re-review |
|
||||
| Nothing material | Silent |
|
||||
|
||||
## Verdict ladder
|
||||
|
||||
1. **Request changes** — correctness, security, lockout risk, broken contracts, missing tests for the claimed fix
|
||||
2. **Comment** — design notes, optional improvements, clarifying questions
|
||||
3. **Approve** — you fully agree; no remaining blockers
|
||||
|
||||
## Comment shape
|
||||
|
||||
```markdown
|
||||
**Verdict: Request changes**
|
||||
|
||||
### Blockers
|
||||
1. `path:lines` — what is wrong, why it matters, how to fix
|
||||
|
||||
### Nits / optional
|
||||
- …
|
||||
|
||||
Happy to re-review once …
|
||||
```
|
||||
|
||||
```markdown
|
||||
**Verdict: Approve** — I agree with this as-is.
|
||||
|
||||
### What closed prior feedback
|
||||
- …
|
||||
|
||||
No new blockers.
|
||||
```
|
||||
|
||||
## Peer reviews
|
||||
|
||||
If another bot already filed the same blocker:
|
||||
|
||||
- Agree in one sentence
|
||||
- Add only **net-new** signal (missed edge case, test gap, wording overclaim)
|
||||
|
||||
## Drafts
|
||||
|
||||
Unless explicitly asked for a deep review: high-level risks and understanding only; avoid nitpicking unfinished work.
|
||||
46
docs/SCHEDULER_PROMPTS.md
Normal file
46
docs/SCHEDULER_PROMPTS.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Durable scheduler prompts
|
||||
|
||||
Paste these when creating Grok durable schedulers (`scheduler_create`).
|
||||
|
||||
## Poll — every 15 minutes
|
||||
|
||||
**Interval:** `15m` · **Recurring:** true · **Durable:** true
|
||||
|
||||
```text
|
||||
You are the durable poll runner for the heavy-duty org GitHub watcher.
|
||||
|
||||
Execute ONE full poll cycle now:
|
||||
|
||||
1. Read /home/grok/heavy-duty-watcher/POLL_INSTRUCTIONS.md and /home/grok/heavy-duty-watcher/state.json.
|
||||
2. Authenticated as grok-bot-andresmgsl. Scope: ALL repositories under org heavy-duty.
|
||||
3. Discover open Issues/PRs where this bot is assignee OR requested reviewer:
|
||||
- gh search prs --owner heavy-duty --review-requested=@me --state open --json number,title,url,updatedAt,author,assignees,repository --limit 50
|
||||
- gh search prs --owner heavy-duty --assignee=@me --state open --json number,title,url,updatedAt,author,assignees,repository --limit 50
|
||||
- gh search issues --owner heavy-duty --assignee=@me --state open --json number,title,url,updatedAt,author,assignees,repository --limit 50
|
||||
4. For each match: read ALL conversation comments, PR reviews, and inline review comments BEFORE posting. Check prior comments by grok-bot-andresmgsl so you never repeat yourself. Use --repo owner/name from search results.
|
||||
5. Comment/review ONLY if first assignment, new commits (head SHA change), new human comments needing reply, material CI change, or explicit re-request. Otherwise stay silent.
|
||||
6. Feedback style: clear Verdict (Approve / Comment / Request changes) or Status (Aligned / Feedback / Need info). Specific, non-redundant, actionable. Separate blockers vs nits. Prefer gh pr review. Keep reviewing across updates until you fully agree (then Approve).
|
||||
7. Do NOT merge, close, reassign, or drive-by comment on unassigned items.
|
||||
8. Update state.json (keys owner/repo#pr:N or owner/repo#issue:N) and append one line to logs/poll.log.
|
||||
9. Short report of what was scanned / posted / intentionally silent.
|
||||
|
||||
If zero assignments: update state, log "0 actionable", and exit quietly.
|
||||
```
|
||||
|
||||
## Health — every 1 hour
|
||||
|
||||
**Interval:** `1h` · **Recurring:** true · **Durable:** true
|
||||
|
||||
```text
|
||||
You are the hourly HEALTH WATCHDOG for the heavy-duty org GitHub watcher.
|
||||
|
||||
1. Run: bash /home/grok/heavy-duty-watcher/scripts/health-check.sh
|
||||
2. Read logs/health-latest.json, tail logs/health.log and logs/poll.log
|
||||
3. Verify tmux session (heavy-duty-watcher or legacy rig-watcher), poll-loop process, non-stale last_poll_at, gh auth as grok-bot-andresmgsl
|
||||
4. If poll-loop dead: restart in tmux without attaching
|
||||
5. If 15m poll scheduler missing: re-arm using docs/SCHEDULER_PROMPTS.md
|
||||
6. Light discovery (no comments unless assigned): scripts/discover.sh or gh search --owner heavy-duty …
|
||||
7. Report one short line: HEALTH ok|degraded|down — details
|
||||
|
||||
Do not merge/close/reassign. Do not drive-by comment.
|
||||
```
|
||||
17
scripts/discover.sh
Executable file
17
scripts/discover.sh
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
#!/usr/bin/env bash
|
||||
# Org-wide discovery of actionable PRs/issues for the bot.
|
||||
set -euo pipefail
|
||||
ORG="${ORG:-heavy-duty}"
|
||||
LIMIT="${LIMIT:-50}"
|
||||
|
||||
echo "=== review-requested PRs (org:${ORG}) ==="
|
||||
gh search prs --owner "${ORG}" --review-requested=@me --state open \
|
||||
--json number,title,url,updatedAt,author,assignees,repository --limit "${LIMIT}"
|
||||
|
||||
echo "=== assignee PRs (org:${ORG}) ==="
|
||||
gh search prs --owner "${ORG}" --assignee=@me --state open \
|
||||
--json number,title,url,updatedAt,author,assignees,repository --limit "${LIMIT}"
|
||||
|
||||
echo "=== assignee issues (org:${ORG}) ==="
|
||||
gh search issues --owner "${ORG}" --assignee=@me --state open \
|
||||
--json number,title,url,updatedAt,author,assignees,repository --limit "${LIMIT}"
|
||||
114
scripts/health-check.sh
Executable file
114
scripts/health-check.sh
Executable file
|
|
@ -0,0 +1,114 @@
|
|||
#!/usr/bin/env bash
|
||||
# Health check for the heavy-duty org watcher. Exit 0=ok, 1=degraded, 2=down.
|
||||
set -euo pipefail
|
||||
|
||||
WATCHER_DIR="${WATCHER_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
|
||||
STATE_FILE="${WATCHER_DIR}/state.json"
|
||||
LOG_DIR="${WATCHER_DIR}/logs"
|
||||
HEALTH_LOG="${LOG_DIR}/health.log"
|
||||
TMUX_SESSION="${TMUX_SESSION:-heavy-duty-watcher}"
|
||||
# Accept legacy session name during migration
|
||||
LEGACY_TMUX="${LEGACY_TMUX:-rig-watcher}"
|
||||
STALE_POLL_MINUTES="${STALE_POLL_MINUTES:-45}"
|
||||
mkdir -p "${LOG_DIR}"
|
||||
|
||||
NOW_EPOCH="$(date -u +%s)"
|
||||
NOW_ISO="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
STATUS="ok"
|
||||
ISSUES=()
|
||||
|
||||
ok() { ISSUES+=("OK: $1"); }
|
||||
warn() { STATUS="degraded"; ISSUES+=("WARN: $1"); }
|
||||
fail() { STATUS="down"; ISSUES+=("FAIL: $1"); }
|
||||
|
||||
if gh api user --jq .login >/dev/null 2>&1; then
|
||||
LOGIN="$(gh api user --jq .login 2>/dev/null || echo unknown)"
|
||||
ok "gh authenticated as ${LOGIN}"
|
||||
else
|
||||
fail "gh not authenticated"
|
||||
fi
|
||||
|
||||
ACTIVE_TMUX=""
|
||||
if tmux has-session -t "${TMUX_SESSION}" 2>/dev/null; then
|
||||
ACTIVE_TMUX="${TMUX_SESSION}"
|
||||
elif tmux has-session -t "${LEGACY_TMUX}" 2>/dev/null; then
|
||||
ACTIVE_TMUX="${LEGACY_TMUX}"
|
||||
warn "using legacy tmux session ${LEGACY_TMUX} (prefer ${TMUX_SESSION})"
|
||||
fi
|
||||
|
||||
if [[ -n "${ACTIVE_TMUX}" ]]; then
|
||||
ok "tmux session ${ACTIVE_TMUX} exists"
|
||||
if tmux list-windows -t "${ACTIVE_TMUX}" -F '#{window_name}' 2>/dev/null | grep -Eqx 'poll-loop'; then
|
||||
ok "tmux window poll-loop present"
|
||||
else
|
||||
warn "tmux window poll-loop missing"
|
||||
fi
|
||||
else
|
||||
fail "tmux session missing (run scripts/restore.sh)"
|
||||
fi
|
||||
|
||||
if ps -eo args | awk '/poll-loop\.sh/ && !/awk/ {found=1} END{exit !found}'; then
|
||||
ok "poll-loop.sh process running"
|
||||
else
|
||||
fail "poll-loop.sh process not running"
|
||||
fi
|
||||
|
||||
if [[ -f "${STATE_FILE}" ]]; then
|
||||
LAST_POLL="$(jq -r '.last_poll_at // empty' "${STATE_FILE}" 2>/dev/null || true)"
|
||||
SCOPE="$(jq -r '.scope // .repo // "unknown"' "${STATE_FILE}" 2>/dev/null || echo unknown)"
|
||||
ok "scope: ${SCOPE}"
|
||||
if [[ -n "${LAST_POLL}" ]]; then
|
||||
if LAST_EPOCH="$(date -u -d "${LAST_POLL}" +%s 2>/dev/null)"; then
|
||||
AGE_MIN=$(( (NOW_EPOCH - LAST_EPOCH) / 60 ))
|
||||
if (( AGE_MIN > STALE_POLL_MINUTES )); then
|
||||
warn "last_poll_at is ${AGE_MIN}m old (${LAST_POLL})"
|
||||
else
|
||||
ok "last_poll_at fresh (${AGE_MIN}m ago: ${LAST_POLL})"
|
||||
fi
|
||||
else
|
||||
ok "last_poll_at present (${LAST_POLL})"
|
||||
fi
|
||||
else
|
||||
warn "state.json has no last_poll_at"
|
||||
fi
|
||||
SUMMARY="$(jq -r '.last_poll_summary // "n/a"' "${STATE_FILE}" 2>/dev/null || echo n/a)"
|
||||
ok "last_poll_summary: ${SUMMARY}"
|
||||
else
|
||||
fail "state.json missing"
|
||||
fi
|
||||
|
||||
[[ -f "${WATCHER_DIR}/POLL_INSTRUCTIONS.md" ]] && ok "POLL_INSTRUCTIONS.md present" || fail "POLL_INSTRUCTIONS.md missing"
|
||||
|
||||
RESULT_JSON="${LOG_DIR}/health-latest.json"
|
||||
{
|
||||
echo "{"
|
||||
echo " \"checked_at\": \"${NOW_ISO}\","
|
||||
echo " \"status\": \"${STATUS}\","
|
||||
echo " \"issues\": ["
|
||||
first=1
|
||||
for line in "${ISSUES[@]}"; do
|
||||
esc="${line//\"/\\\"}"
|
||||
if (( first )); then first=0; else echo ","; fi
|
||||
printf ' "%s"' "${esc}"
|
||||
done
|
||||
echo
|
||||
echo " ]"
|
||||
echo "}"
|
||||
} > "${RESULT_JSON}"
|
||||
|
||||
echo "[${NOW_ISO}] status=${STATUS} | $(IFS=' ; '; echo "${ISSUES[*]}")" >> "${HEALTH_LOG}"
|
||||
|
||||
if command -v jq >/dev/null 2>&1 && [[ -f "${STATE_FILE}" ]]; then
|
||||
tmp="$(mktemp)"
|
||||
jq --arg t "${NOW_ISO}" --arg s "${STATUS}" \
|
||||
'.health = {last_check_at: $t, status: $s}' "${STATE_FILE}" > "${tmp}" \
|
||||
&& mv "${tmp}" "${STATE_FILE}"
|
||||
fi
|
||||
|
||||
echo "status=${STATUS}"
|
||||
printf '%s\n' "${ISSUES[@]}"
|
||||
case "${STATUS}" in
|
||||
ok) exit 0 ;;
|
||||
degraded) exit 1 ;;
|
||||
*) exit 2 ;;
|
||||
esac
|
||||
56
scripts/health-loop.sh
Executable file
56
scripts/health-loop.sh
Executable file
|
|
@ -0,0 +1,56 @@
|
|||
#!/usr/bin/env bash
|
||||
# Hourly health check + self-heal for poll-loop.
|
||||
set -euo pipefail
|
||||
|
||||
WATCHER_DIR="${WATCHER_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
|
||||
LOG_DIR="${WATCHER_DIR}/logs"
|
||||
INTERVAL_SEC="${HEALTH_INTERVAL_SEC:-3600}"
|
||||
TMUX_SESSION="${TMUX_SESSION:-heavy-duty-watcher}"
|
||||
LEGACY_TMUX="${LEGACY_TMUX:-rig-watcher}"
|
||||
mkdir -p "${LOG_DIR}"
|
||||
|
||||
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] health-loop started interval=${INTERVAL_SEC}s" \
|
||||
>> "${LOG_DIR}/health.log"
|
||||
|
||||
"${WATCHER_DIR}/scripts/health-check.sh" || true
|
||||
|
||||
session_name() {
|
||||
if tmux has-session -t "${TMUX_SESSION}" 2>/dev/null; then
|
||||
echo "${TMUX_SESSION}"
|
||||
elif tmux has-session -t "${LEGACY_TMUX}" 2>/dev/null; then
|
||||
echo "${LEGACY_TMUX}"
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
while true; do
|
||||
sleep "${INTERVAL_SEC}"
|
||||
set +e
|
||||
"${WATCHER_DIR}/scripts/health-check.sh"
|
||||
rc=$?
|
||||
set -e
|
||||
|
||||
if ! ps -eo args | awk '/poll-loop\.sh/ && !/awk/ {found=1} END{exit !found}'; then
|
||||
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] health-loop: restarting poll-loop" \
|
||||
>> "${LOG_DIR}/health.log"
|
||||
sess="$(session_name)"
|
||||
if [[ -n "${sess}" ]]; then
|
||||
tmux list-windows -t "${sess}" -F '#{window_index} #{window_name}' \
|
||||
| awk '$2=="poll-loop"{print $1}' \
|
||||
| while read -r idx; do tmux kill-window -t "${sess}:${idx}" 2>/dev/null || true; done
|
||||
tmux new-window -t "${sess}" -n poll-loop -c "${WATCHER_DIR}" \
|
||||
"bash ${WATCHER_DIR}/scripts/poll-loop.sh"
|
||||
else
|
||||
tmux new-session -d -s "${TMUX_SESSION}" -n poll-loop -c "${WATCHER_DIR}" \
|
||||
"bash ${WATCHER_DIR}/scripts/poll-loop.sh"
|
||||
tmux new-window -t "${TMUX_SESSION}" -n health-loop -c "${WATCHER_DIR}" \
|
||||
"bash ${WATCHER_DIR}/scripts/health-loop.sh"
|
||||
fi
|
||||
fi
|
||||
|
||||
if (( rc != 0 )); then
|
||||
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] health-loop: status non-ok rc=${rc}" \
|
||||
>> "${LOG_DIR}/health.log"
|
||||
fi
|
||||
done
|
||||
45
scripts/install-live.sh
Executable file
45
scripts/install-live.sh
Executable file
|
|
@ -0,0 +1,45 @@
|
|||
#!/usr/bin/env bash
|
||||
# Install/refresh the live watcher under $HOME/heavy-duty-watcher and start tmux loops.
|
||||
set -euo pipefail
|
||||
|
||||
SRC="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
DEST="${DEST:-$HOME/heavy-duty-watcher}"
|
||||
TMUX_SESSION="${TMUX_SESSION:-heavy-duty-watcher}"
|
||||
|
||||
echo "Installing watcher: ${SRC} -> ${DEST}"
|
||||
mkdir -p "${DEST}"
|
||||
# Copy tree but preserve local state/logs if DEST already has them
|
||||
rsync -a --exclude '.git' --exclude 'state.json' --exclude 'logs/' --exclude '.poll-prompt.txt' \
|
||||
"${SRC}/" "${DEST}/"
|
||||
|
||||
mkdir -p "${DEST}/logs"
|
||||
if [[ ! -f "${DEST}/state.json" ]]; then
|
||||
NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
jq --arg t "$NOW" '.created_at=$t | .last_poll_at=null' \
|
||||
"${DEST}/config/state.template.json" > "${DEST}/state.json"
|
||||
echo "seeded state.json"
|
||||
fi
|
||||
|
||||
chmod +x "${DEST}/scripts/"*.sh
|
||||
|
||||
# Compatibility symlink for old path
|
||||
if [[ ! -e "$HOME/rig-watcher" ]]; then
|
||||
ln -s "${DEST}" "$HOME/rig-watcher"
|
||||
echo "linked ~/rig-watcher -> ${DEST}"
|
||||
fi
|
||||
|
||||
# Ensure tmux loops
|
||||
if tmux has-session -t "${TMUX_SESSION}" 2>/dev/null; then
|
||||
echo "tmux ${TMUX_SESSION} already exists"
|
||||
else
|
||||
tmux new-session -d -s "${TMUX_SESSION}" -n poll-loop -c "${DEST}" \
|
||||
"bash ${DEST}/scripts/poll-loop.sh"
|
||||
tmux new-window -t "${TMUX_SESSION}" -n health-loop -c "${DEST}" \
|
||||
"bash ${DEST}/scripts/health-loop.sh"
|
||||
tmux new-window -t "${TMUX_SESSION}" -n tools -c "${DEST}" \
|
||||
"echo 'heavy-duty-watcher tools'; exec bash"
|
||||
echo "started tmux session ${TMUX_SESSION}"
|
||||
fi
|
||||
|
||||
bash "${DEST}/scripts/health-check.sh" || true
|
||||
echo "Live install ready. Restore after reboot: ${DEST}/scripts/restore.sh"
|
||||
24
scripts/poll-loop.sh
Executable file
24
scripts/poll-loop.sh
Executable file
|
|
@ -0,0 +1,24 @@
|
|||
#!/usr/bin/env bash
|
||||
# Long-running poll loop (default 15 minutes) for tmux.
|
||||
set -euo pipefail
|
||||
|
||||
WATCHER_DIR="${WATCHER_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
|
||||
LOG_DIR="${WATCHER_DIR}/logs"
|
||||
INTERVAL_SEC="${POLL_INTERVAL_SEC:-900}"
|
||||
mkdir -p "${LOG_DIR}"
|
||||
|
||||
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] poll-loop started interval=${INTERVAL_SEC}s scope=org:heavy-duty (sleep-first)" \
|
||||
>> "${LOG_DIR}/poll.log"
|
||||
|
||||
if [[ "${POLL_NOW:-0}" == "1" ]]; then
|
||||
"${WATCHER_DIR}/scripts/poll-once.sh" || true
|
||||
fi
|
||||
|
||||
while true; do
|
||||
sleep "${INTERVAL_SEC}"
|
||||
if ps -eo args | awk '/heavy-duty-watcher\/scripts\/poll-once\.sh/ && !/awk/ {found=1} END{exit !found}'; then
|
||||
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] poll-loop: previous poll still running, skipping" >> "${LOG_DIR}/poll.log"
|
||||
else
|
||||
"${WATCHER_DIR}/scripts/poll-once.sh" || true
|
||||
fi
|
||||
done
|
||||
38
scripts/poll-once.sh
Executable file
38
scripts/poll-once.sh
Executable file
|
|
@ -0,0 +1,38 @@
|
|||
#!/usr/bin/env bash
|
||||
# Single headless poll via Grok for the heavy-duty org watcher.
|
||||
set -euo pipefail
|
||||
|
||||
WATCHER_DIR="${WATCHER_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
|
||||
LOG_DIR="${WATCHER_DIR}/logs"
|
||||
mkdir -p "${LOG_DIR}"
|
||||
TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
PROMPT_FILE="${WATCHER_DIR}/.poll-prompt.txt"
|
||||
|
||||
cat > "${PROMPT_FILE}" <<EOF
|
||||
Execute ONE full poll cycle for the heavy-duty org GitHub watcher.
|
||||
|
||||
1. Read ${WATCHER_DIR}/POLL_INSTRUCTIONS.md and ${WATCHER_DIR}/state.json
|
||||
2. Bot: grok-bot-andresmgsl. Scope: ALL repos under org heavy-duty (not a single repo).
|
||||
3. Discover open Issues/PRs where bot is assignee OR review-requested:
|
||||
gh search prs --owner heavy-duty --review-requested=@me --state open --json number,title,url,updatedAt,author,assignees,repository --limit 50
|
||||
gh search prs --owner heavy-duty --assignee=@me --state open --json number,title,url,updatedAt,author,assignees,repository --limit 50
|
||||
gh search issues --owner heavy-duty --assignee=@me --state open --json number,title,url,updatedAt,author,assignees,repository --limit 50
|
||||
4. For each match: read ALL comments/reviews/inline threads/diff/checks first; never repeat prior grok-bot-andresmgsl comments.
|
||||
5. Comment/review only if first assignment, new commits (head SHA change vs state), new human comments needing reply, material CI change, or explicit re-request. Else silent.
|
||||
6. Style: clear Verdict (Approve/Comment/Request changes) or Status (Aligned/Feedback/Need info). Specific, non-redundant, actionable. Blockers vs nits. Prefer gh pr review. Keep reviewing across updates until you fully agree (then Approve).
|
||||
7. Never merge/close/reassign/drive-by on unassigned items.
|
||||
8. Update ${WATCHER_DIR}/state.json (keys like heavy-duty/box#pr:79) + append one line to ${WATCHER_DIR}/logs/poll.log
|
||||
9. Brief report only. If zero assignments, log and exit.
|
||||
EOF
|
||||
|
||||
echo "[${TS}] poll-once.sh starting headless grok (org:heavy-duty)" >> "${LOG_DIR}/poll.log"
|
||||
|
||||
if grok --always-approve --cwd "${HOME}" --max-turns 80 \
|
||||
--single "$(cat "${PROMPT_FILE}")" \
|
||||
>> "${LOG_DIR}/poll-headless.log" 2>&1; then
|
||||
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] poll-once.sh finished ok" >> "${LOG_DIR}/poll.log"
|
||||
else
|
||||
rc=$?
|
||||
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] poll-once.sh failed rc=${rc}" >> "${LOG_DIR}/poll.log"
|
||||
exit "${rc}"
|
||||
fi
|
||||
115
scripts/restore.sh
Executable file
115
scripts/restore.sh
Executable file
|
|
@ -0,0 +1,115 @@
|
|||
#!/usr/bin/env bash
|
||||
# Restore the heavy-duty org watcher after reboot / session loss.
|
||||
# Usage:
|
||||
# ./scripts/restore.sh # create/attach tmux + resume Grok
|
||||
# ./scripts/restore.sh --attach # attach only
|
||||
# ./scripts/restore.sh --status # health snapshot
|
||||
set -euo pipefail
|
||||
|
||||
WATCHER_DIR="${WATCHER_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
|
||||
STATE_FILE="${WATCHER_DIR}/state.json"
|
||||
TMUX_SESSION="${TMUX_SESSION:-heavy-duty-watcher}"
|
||||
LOG_DIR="${WATCHER_DIR}/logs"
|
||||
mkdir -p "${LOG_DIR}"
|
||||
|
||||
session_id() {
|
||||
if [[ -f "${STATE_FILE}" ]] && command -v jq >/dev/null 2>&1; then
|
||||
local id
|
||||
id="$(jq -r '.session_id // empty' "${STATE_FILE}" 2>/dev/null || true)"
|
||||
if [[ -n "${id}" && "${id}" != "null" ]]; then
|
||||
echo "${id}"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
status() {
|
||||
echo "=== heavy-duty-watcher status ==="
|
||||
echo "dir: ${WATCHER_DIR}"
|
||||
if [[ -f "${STATE_FILE}" ]]; then
|
||||
jq '{scope, org, bot_login, session_id, last_poll_at, last_poll_summary, health, items: (.items|keys)}' \
|
||||
"${STATE_FILE}" 2>/dev/null || cat "${STATE_FILE}"
|
||||
else
|
||||
echo "(missing state.json)"
|
||||
fi
|
||||
echo
|
||||
tmux ls 2>/dev/null || echo "(no tmux sessions)"
|
||||
echo
|
||||
echo "gh user: $(gh api user --jq .login 2>/dev/null || echo 'not authenticated')"
|
||||
echo "session_id: $(session_id || true)"
|
||||
[[ -f "${LOG_DIR}/poll.log" ]] && { echo; echo "last poll log:"; tail -n 5 "${LOG_DIR}/poll.log"; }
|
||||
[[ -f "${LOG_DIR}/health.log" ]] && { echo; echo "last health log:"; tail -n 5 "${LOG_DIR}/health.log"; }
|
||||
bash "${WATCHER_DIR}/scripts/health-check.sh" || true
|
||||
}
|
||||
|
||||
attach_only() {
|
||||
if tmux has-session -t "${TMUX_SESSION}" 2>/dev/null; then
|
||||
exec tmux attach -t "${TMUX_SESSION}"
|
||||
fi
|
||||
echo "tmux session '${TMUX_SESSION}' not running. Run without --attach to start."
|
||||
exit 1
|
||||
}
|
||||
|
||||
start_or_attach() {
|
||||
local sid resume_prompt
|
||||
sid="$(session_id)"
|
||||
resume_prompt="$(cat <<EOF
|
||||
Resume the heavy-duty org GitHub watcher (ALL heavy-duty/* repos).
|
||||
|
||||
1. Read ${WATCHER_DIR}/POLL_INSTRUCTIONS.md and ${WATCHER_DIR}/state.json
|
||||
2. Run one full org-wide poll cycle immediately (discover assigned/review-requested across org, read all comments, review until you agree)
|
||||
3. Re-arm durable schedulers:
|
||||
- 15m poll (org-wide, durable true)
|
||||
- 1h health watchdog (durable true)
|
||||
4. Confirm poll-loop + health-loop windows are running
|
||||
5. Report status
|
||||
|
||||
Do not merge, close, or reassign unless explicitly asked.
|
||||
EOF
|
||||
)"
|
||||
|
||||
if tmux has-session -t "${TMUX_SESSION}" 2>/dev/null; then
|
||||
if ! tmux list-windows -t "${TMUX_SESSION}" -F '#{window_name}' 2>/dev/null | grep -qx 'poll-loop'; then
|
||||
tmux new-window -t "${TMUX_SESSION}" -n poll-loop -c "${WATCHER_DIR}" \
|
||||
"bash ${WATCHER_DIR}/scripts/poll-loop.sh"
|
||||
fi
|
||||
if ! tmux list-windows -t "${TMUX_SESSION}" -F '#{window_name}' 2>/dev/null | grep -qx 'health-loop'; then
|
||||
tmux new-window -t "${TMUX_SESSION}" -n health-loop -c "${WATCHER_DIR}" \
|
||||
"bash ${WATCHER_DIR}/scripts/health-loop.sh"
|
||||
fi
|
||||
echo "tmux session '${TMUX_SESSION}' exists — attaching."
|
||||
exec tmux attach -t "${TMUX_SESSION}"
|
||||
fi
|
||||
|
||||
echo "Creating tmux session '${TMUX_SESSION}'..."
|
||||
local grok_cmd
|
||||
if [[ -n "${sid}" ]]; then
|
||||
grok_cmd="grok -r ${sid} --cwd ${HOME} $(printf %q "${resume_prompt}") || grok -c --cwd ${HOME} $(printf %q "${resume_prompt}") || grok --cwd ${HOME} $(printf %q "${resume_prompt}")"
|
||||
else
|
||||
grok_cmd="grok --cwd ${HOME} $(printf %q "${resume_prompt}")"
|
||||
fi
|
||||
|
||||
tmux new-session -d -s "${TMUX_SESSION}" -n grok -c "${HOME}" \
|
||||
"${grok_cmd}; echo; echo '[heavy-duty-watcher] Grok exited.'; exec bash"
|
||||
|
||||
tmux new-window -t "${TMUX_SESSION}" -n poll-loop -c "${WATCHER_DIR}" \
|
||||
"bash ${WATCHER_DIR}/scripts/poll-loop.sh"
|
||||
|
||||
tmux new-window -t "${TMUX_SESSION}" -n health-loop -c "${WATCHER_DIR}" \
|
||||
"bash ${WATCHER_DIR}/scripts/health-loop.sh"
|
||||
|
||||
tmux new-window -t "${TMUX_SESSION}" -n tools -c "${WATCHER_DIR}" \
|
||||
"echo 'tools: ./scripts/discover.sh | ./scripts/health-check.sh | tail -f logs/poll.log'; exec bash"
|
||||
|
||||
tmux select-window -t "${TMUX_SESSION}:grok"
|
||||
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) restore.sh started ${TMUX_SESSION}" >> "${LOG_DIR}/poll.log"
|
||||
exec tmux attach -t "${TMUX_SESSION}"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
--status|-s) status ;;
|
||||
--attach|-a) attach_only ;;
|
||||
--help|-h) sed -n '2,7p' "$0" ;;
|
||||
*) start_or_attach ;;
|
||||
esac
|
||||
Loading…
Reference in a new issue