merge-trail
Live PR-merge leaderboard for AI coding agents on public GitHub.
Sibling of agent-bloat (which measures PR size). merge-trail measures the complementary question: of the PRs an agent opens, how many actually merge — and how long do humans spend dragging them across the line?
Eight agents tracked: Claude Code, GitHub Copilot, Cursor, OpenAI Codex, Devin, Aider, Gemini, OpenAI (generic).
What it shows
Per agent, over a rolling 30-day window:
- Merge rate =
merged / (merged + closed_unmerged). Open PRs are excluded from the denominator so a flood of new opens does not tank the score. - Median time-to-merge (and p90) — only over merged PRs.
- Stale rate = open PRs older than 14 days, divided by all open PRs.
- Change-request rate = PRs with ≥1 CHANGES_REQUESTED review, divided by total PRs.
- Volume (raw PR count) — shown alongside so a high merge rate on N=2 PRs is visually deprioritised.
Tie-break ordering on the leaderboard: merge_rate desc, total_prs desc, median_ttm_min asc.
Data sources (all real, all public — no mocks anywhere)
| Endpoint | URL | Cadence |
|---|---|---|
| commits.search | https://api.github.com/search/commits?q="Co-Authored-By: <agent>" committer-date:>=<since> | every 30 min |
| commits.pulls | https://api.github.com/repos/{owner}/{repo}/commits/{sha}/pulls | every 10 min for new commits |
| pulls.detail | https://api.github.com/repos/{owner}/{repo}/pulls/{number} | every 10 min for open + recently-merged PRs |
| pulls.reviews | https://api.github.com/repos/{owner}/{repo}/pulls/{number}/reviews | every 30 min |
Every outbound call is logged to the SQLite fetches table with status, duration_ms, rows, and timestamp — so an auditor can verify every number on the dashboard end-to-end. The transparency endpoint is GET /merge-trail/api/sources.
The leaderboard recomputes every 15 minutes from the pulls table. Daily snapshots go to agent_stats_daily for charting trends over time.
No mocks, no seeds, no Math.random()
This product is built under a strict no-fake-data rule:
- No hardcoded "preset" PR counts. The DB starts empty; numbers populate only as real fetches complete.
- No
Math.random()jitter anywhere. - If the initial commit search returns zero results (no token, rate-limited, or no PRs in window), the UI honestly shows "No PRs detected yet — first fetch in progress." It does not invent rows.
- If
GITHUB_TOKENis missing, a yellow banner explicitly says so; the underlying free-tier 10 req/min on/search/commitsmeans data will be sparse but still real.
Endpoints
| Path | Description |
|---|---|
| GET /merge-trail/ | SPA |
| GET /merge-trail/health | {ok, version, last_refresh_at, total_prs_tracked, has_github_token} — auth-free, used by deploy smoke check |
| GET /merge-trail/api/leaderboard | Current per-agent rollup |
| GET /merge-trail/api/agent/:slug | Per-agent detail + recent PRs + TTM histogram |
| GET /merge-trail/api/prs?agent=&state=&limit= | Paginated PR list |
| GET /merge-trail/api/series/:slug?days=30 | Daily merge-rate time series |
| GET /merge-trail/api/stats | Global counters + fetch-error rate + token status |
| GET /merge-trail/api/sources | Transparency: every endpoint called, last fetched_at, total rows |
No authentication on any route. No Basic Auth, no admin password, no requireAuth middleware anywhere — Arda wants to inspect the dashboard instantly.
Stack
- Node.js 18+, Express,
helmet,compression better-sqlite3(WAL) for storagenode-cronfor scheduling- Vanilla JS SPA, dark theme, English labels
- Chart.js from CDN (
https://cdn.jsdelivr.net/npm/chart.js@4)
Run locally
cp .env.example .env
# Edit .env to add GITHUB_TOKEN (optional but recommended).
npm install
node server.js
Then open <http://localhost:4791/merge-trail/>.
Deploy
DEPLOY_MANIFEST.json follows the RNDLAB orchestrator schema. The Mac watcher under ~/.openclaw/cowork-deploy-bridge/queue.jsonl picks up the trigger, rsyncs the source to /var/www/projects/merge-trail, registers the systemd unit, attaches the nginx route at /merge-trail, and POSTs to the showcase. Vault injection replaces __INJECT_FROM_VAULT__ with the real GITHUB_TOKEN.
Layout
merge-trail/
├── server.js -- Express bootstrap, cron scheduling
├── db.js -- SQLite schema + prepared statements
├── package.json
├── lib/
│ ├── agents.js -- 8-agent identity table
│ ├── github.js -- GitHub REST client + fetch logger
│ ├── attribution.js -- commit-message → agent slug
│ ├── metrics.js -- median/percentile/summarise (pure)
│ └── time.js -- ISO/relative-time helpers
├── fetchers/
│ ├── commits.js -- /search/commits sweep
│ ├── pulls.js -- commit→PR resolution + PR refresh
│ └── reviews.js -- CHANGES_REQUESTED counting
├── workers/
│ ├── refresh.js -- aggregate roll-up
│ └── prune.js -- old-row cleanup
├── routes/
│ ├── api.js -- /api/* JSON endpoints
│ └── ui.js -- SPA + static serving
└── public/
├── index.html
├── app.js -- vanilla JS SPA
└── style.css -- dark theme
License
MIT. Built by the Cowork R&D pipeline.