# PR Manager
A [Vellum assistant](https://www.vellum.ai) plugin that keeps track of every
GitHub pull request your assistant opens — and nudges the right conversation
back awake when a PR needs attention.
**The problem:** your assistant opens a PR, the conversation goes quiet, and
three days later you find two review comments nobody answered and a red CI
check. **This plugin closes that loop:**
- 📊 **Dashboard app** — every tracked PR with live status chips: draft/ready,
CI passing/failing, unaddressed comments, merge conflicts, approvals,
unread activity, stale PRs.
- ⏰ **Bundled schedule** — polls GitHub every 15 minutes (via your existing
`gh` auth; no extra tokens) and updates the dashboard state. No LLM runs
during polling — it is deterministic and free.
- 🔔 **Conversation nudges** — when a PR gets new review comments or a CI
failure, the plugin wakes **the conversation that created the PR** so the
assistant addresses the feedback with full context. Comment text travels in
a fenced `external_content` block (prompt-injection-safe), never as
instructions.
- 🧠 **Zero prompt overhead** — PR↔conversation mapping is captured
automatically from turn history when the assistant runs `gh pr create`.
No custom tools shadow the GitHub workflows models already know.
## Anti-loop guardrails
Nudges are engineered to never spiral:
| Guardrail | Behavior |
| --- | --- |
| Merge conflicts | **Never nudged.** Shown in the UI only; the bundled skill also forbids the assistant from resolving conflicts unless you explicitly ask. |
| Per-comment dedup | A comment id is nudged at most once, ever. |
| CI dedup | One nudge per (head SHA, failing-check set). A new push resets it. |
| Cooldown | Default 60 minutes between nudges per PR. |
| Daily cap | Default 3 nudges per PR per day. |
| Quiet hours | Optional window where nudges are suppressed. |
| Human pushes | If someone else pushed to the branch, the nudge says so and the skill forbids force-pushing over their work. |
## Install
Requires the `plugin-schedules` feature flag and a `gh`-authenticated machine.
```bash
assistant plugins install vellum-ai/pr-manager
```
The installer lists the bundled `pr-sync` schedule and asks for confirmation.
That's it — the next time your assistant opens a PR, it appears on the
dashboard. Ask your assistant to "open the PR Manager app" to see it.
### Let nudges act autonomously (recommended)
A nudge wakes the conversation in the background, with no interactive client
attached — so tools that need approval (like `bash`) are denied instead of
prompting. Without setup, the assistant can only reply "ping me when you're
back." To let it actually fix PRs while you're away, add a trust rule for
`gh` once: in any chat where the assistant runs a `gh` command, open the
dropdown next to **Allow** and pick **Always allow**, or add a `bash` rule
matching `^gh ` in the trust rules UI (`assistant trust list` shows current
rules). This is a one-time, workspace-level setting.
### Watch extra repos
PRs your assistant creates are discovered automatically. To also watch repos
for PRs you opened yourself, edit `config.json` in the installed plugin
directory (`<workspace>/plugins/pr-manager/config.json`):
```json
{
"repos": ["your-org/your-repo"],
"nudges": {
"reviewComments": true,
"ciFailures": true,
"mergeConflicts": false,
"maxPerPrPerDay": 3,
"cooldownMinutes": 60
},
"quietHours": { "start": "22:00", "end": "08:00" },
"staleAfterDays": 7,
"ignoreBotComments": true,
"backfillMaxAgeDays": 30
}
```
`mergeConflicts: true` enables conflict nudges. It ships off for a reason —
agents rebasing over moved bases in a loop is how token bills catch fire.
Leave it off unless you know you want it.
## How it works
```
┌─ conversation turn ends ──────────────────────────────────────────┐
│ stop hook scans history for `gh pr create` commands + PR URLs │
│ → data/candidates.jsonl (works even with &>/dev/null│
│ — the command is evidence) │
└───────────────────────────────────────────────────────────────────┘
┌─ every 15 min (schedules/pr-sync, script mode — no LLM) ──────────┐
│ 1. verify candidates against GitHub (author + recency) → track │
│ 2. backfill untracked open PRs in watched repos │
│ 3. refresh state/CI/threads/mergeability per open PR │
│ 4. new comments or CI failure? → │
│ assistant conversations wake <convId> --persist │
│ --hint "<trusted framing>" --external-content "<fenced>" │
└───────────────────────────────────────────────────────────────────┘
┌─ dashboard app (apps/dashboard) ──────────────────────────────────┐
│ reads data/ via plugin routes; never calls GitHub itself. │
│ Mark read · Nudge now · Link conversation · Untrack · Refresh │
└───────────────────────────────────────────────────────────────────┘
```
- **Capture is a hint, not the record.** Candidates are verified against
GitHub (right author, created near capture time) before tracking, so a
`gh pr view` in some unrelated conversation can't steal ownership.
- **Orphan adoption.** A PR with no known conversation shows a dashed
"No conversation linked" chip. It gets linked automatically the next time
any conversation that mentions its URL or branch finishes a turn, or
manually from the dashboard, or via the bundled skill's `track.ts` script.
- **Wake attribution.** The sync script runs under the schedule engine, so
woken turns are attributed to the schedule run and inherit its inference
profile.
## Surfaces
| Surface | Path | Purpose |
| --- | --- | --- |
| Hook | `hooks/stop.ts` | Capture PR-creation evidence; adopt orphans |
| Hook | `hooks/init.ts` | Idempotent data-dir bootstrap |
| Schedule | `schedules/pr-sync/` | 15-minute GitHub poll + nudges (script mode) |
| Routes | `routes/prs.ts`, `routes/actions.ts` | Dashboard data feed + mutations |
| App | `apps/dashboard/` | The PR dashboard (Preact) |
| Skill | `skills/pr-manager/` | Nudge-handling workflow + manual tracking |
State lives in `data/` (preserved across upgrades): `state.json` (sync-owned
PR snapshots), `overrides.json` (read marks, untracks, manual links),
`candidates.jsonl`, `events.jsonl` (the activity timeline shown in the app).
## Development
```bash
bun install
bun run typecheck
bun run dev-install # rsync into ~/.vellum/workspace/plugins/pr-manager
assistant plugins list # status should be "ok"
```
Run one sync tick by hand:
```bash
bun run src/sync/main.ts
```
## License
MIT