c48adea6c35f9a774e51cd4c614a7c0bc9f67d1a
Merge new upstream (parent) commits into every fork in the App installation, gated by a per-repo interval (default 1h), inside the existing 5-minute tick (STEP 0, max 2 forks/tick, oldest-first). - Conflicted merges are resolved by Claude Code (merge-reconciler rules: never wholesale --ours/--theirs, verify with the repo's own typecheck+tests, commit --no-edit; Claude never pushes — harness does). - Clean merges get a single Claude Code quality pass commit. - Push path: owner PAT (gh CLI) first — the App lacks workflows:write and a workflows-touching merge is rejected for the App token; App token fallback. - Protected default branch: detected from the push result (GH006 / required-status-check) → upstream-sync-<ts> branch + PR through the normal pipeline; duplicate open sync PRs are skipped. - CI safety: after a direct push, ticks verify the fork CI at our merge sha; red CI at OUR merge (still the tip) → sha-guarded force-revert to pre-merge sha + Discord notify; never reverts foreign commits. - Discord: synced / PR opened / reverted / skipped-once on pr-agent-ops. - merge_pr gains the same PAT fallback (a PR merge touching workflows is a workflow-file push). - CLI: --sync-status, --sync-only <repo> [--dry]. - Tests: scripts/test_pr_queue_sync.py (46 assertions, monkeypatched, no network); py_compile clean. - Plan: .hermes/plans/2026-09-21-upstream-auto-sync.md
PR-Agent Server
Nix-deployed GitHub App server for automated PR review + auto-merge using a custom LLM endpoint (9router/Omniroute).
Architecture
GitHub webhook → Caddy (reverse proxy, :4002)
→ pr-agent-server (Nix profile, uvicorn on :4002)
→ PR-Agent github_app.py (FastAPI)
→ 9router API (custom OpenAI-compatible endpoint)
Project Layout
pr-agent-server/
├── src/ # Main application modules
│ ├── run_server.py # FastAPI server + analytics/metrics + Discord webhook
│ ├── auto_merge_bot.py # Periodic PR review→approve→merge bot
│ ├── trivial_merge.py # Trivial PR fast-path (docs/dependabot/tiny diffs)
│ ├── health-check.py # Model health watchdog (tests primary + fallbacks)
│ ├── sync-key.py # Auto-syncs BWS router key to disk on service start
│ ├── callback_server.py # Dev callback server for GitHub App manifest
│ ├── start_server.py # Legacy server start script
│ └── config/ # Runtime config (gitignored at deploy time)
├── scripts/ # Setup and deployment helpers
│ ├── setup_all.py # Full setup: manifest + config + systemd service
│ ├── setup_app.py # App-specific setup
│ ├── generate_manifest.py # GitHub App manifest URL generator
│ └── generate_manifest_domain.py
├── templates/
│ └── manifest.json # GitHub App manifest template
├── .github/workflows/
│ ├── deploy.yml # CI: syntax → build → deploy → GC
│ ├── flakehub-publish-rolling.yaml
│ └── mirror-gitea.yml
├── flake.nix # Nix build (creates venv + binary wrappers)
├── flake.lock # Pinned Nix dependencies
├── .editorconfig # Editor formatting rules
├── .gitignore
└── README.md
Development
Prerequisites
- Bun 1.3.14+ (runtime + build)
- GitHub App credentials (App ID, private key, webhook secret)
- 9router/OpenAI-compatible key for the LLM
Local testing
cd server
bun install
bunx tsc --noEmit # typecheck
bun test # unit tests (16 tests)
bun src/cli.ts --tool review --repo <owner>/<repo> --pr <n> --no-publish
Run server (after setting up secrets)
# Secrets are resolved at startup: PR_AGENT_APP_ID, private key path,
# omniroute key file (see src/config.ts + src/secrets.ts)
cd server
bun src/index.ts # starts on PORT (default 4023)
Test tools end-to-end (real GitHub + LLM)
bun e2e.ts --repo asepharyana/nextjs-template --pr 19 --publish
bun src/cli.ts --tool describe --repo <owner>/<repo> --pr <n>
bun src/cli.ts --tool improve --repo <owner>/<repo> --pr <n>
Deployment
Deploy is fully automated via GitHub Actions on push to main:
# .github/workflows/deploy.yml
1. build-and-deploy → bun install → typecheck → tests → bun build --compile
→ scp binary to VPS → swap /opt/pr-agent-server/bin/pr-agent-bun
→ restart pr-agent-bun.service → health check on :4023
2. cleanup → nix-gc-vps.sh (cleans legacy Nix store entries)
The production server is a single compiled binary
(/opt/pr-agent-server/bin/pr-agent-bun) running as a systemd service
(pr-agent-bun.service, port 4023, secrets via bws-exec pr-agent).
Secrets required in GitHub Actions:
VPS_HOST— VPS IP addressVPS_USER— SSH userSSH_PRIVATE_KEY— SSH private key for deploy userGITEA_TOKEN— for Gitea mirror (if using mirror workflow)
Ops
- Health watchdog: cron
pr-agent-health-watchdog(every 10 min) →~/.hermes/scripts/pr-agent-health-check.sh→ curlhttp://127.0.0.1:4023/health - Secrets: systemd
ExecStart=/usr/local/bin/bws-exec pr-agent env PORT=4023 /opt/pr-agent-server/bin/pr-agent-bun - Prometheus:
GET /api/metrics→pr_agent_requests_total,pr_agent_model_failures - Analytics:
GET /api/analytics→ JSON summary (legacypr-agent.*.log+pr-agent.bun.jsonl) - Discord:
POST /api/v1/notify_review→ pr-agent-ops webhook
Legacy (Python/Nix — retired 2026-09-21)
The original Python pr_agent server (FastAPI + Nix build, port 4002) is fully
retired: systemd unit deleted, venv removed, src/*.py + scripts/setup_* +
flake removed from the repo. The Bun binary replaced it end-to-end.
License
MIT — see LICENSE if present at deploy.
Languages
TypeScript
53.1%
Python
46.9%