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:
grok-bot-andresmgsl 2026-07-18 17:40:05 +00:00
commit 1d00544e86
17 changed files with 1008 additions and 0 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
state.json
logs/
.poll-prompt.txt
config/watcher.env
*.log
.DS_Store

21
LICENSE Normal file
View 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
View 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
View 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).

View 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
}
}

View 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
View 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 bots GitHub token via `gh` / MCP
- Does not store secrets in the repo (state is operational metadata only)

69
docs/OPERATIONS.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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